From d61ef96e413434883bf4b948930927cc1b82f539 Mon Sep 17 00:00:00 2001 From: Jeffrey Date: Fri, 27 Mar 2026 15:35:23 +0800 Subject: [PATCH 1/6] feat: add basic sqlite supports --- apps/electron/main.ts | 22 + apps/electron/preload.cts | 1 + .../copilot/types/copilot-context-sql.ts | 2 +- .../app-sidebar/connection-switcher/index.tsx | 16 +- .../sql-console-sidebar/sidebar-config.ts | 5 + .../table-browser/table-view-tabs.tsx | 18 +- .../components/connection-card.tsx | 6 +- .../components/connection-dialog.tsx | 131 +- .../forms/connection/drivers/index.ts | 20 +- .../forms/connection/drivers/sqlite.tsx | 129 + .../components/forms/connection/index.tsx | 2 + .../[organization]/connections/form-schema.ts | 53 +- .../databases/[database]/summary/route.ts | 4 +- apps/web/app/api/connection/connect/route.ts | 2 + apps/web/app/api/connection/test/route.ts | 2 + apps/web/app/api/connection/utils.ts | 10 +- apps/web/lib/connection/base/types.ts | 5 +- apps/web/lib/connection/config-builder.ts | 41 +- apps/web/lib/connection/display.ts | 18 + .../drivers/sqlite/SqliteDatasource.ts | 59 + .../drivers/sqlite/capabilities/metadata.ts | 32 + .../drivers/sqlite/capabilities/table-info.ts | 23 + .../lib/connection/drivers/sqlite/dialect.ts | 6 + .../drivers/sqlite/sqlite-driver.ts | 238 ++ apps/web/lib/connection/registry/index.ts | 2 + apps/web/lib/connection/registry/types.ts | 2 +- apps/web/lib/database/pglite/migrations.json | 10 + .../pglite/migrations/0005_wet_jack_power.sql | 3 + .../pglite/migrations/meta/0005_snapshot.json | 3654 +++++++++++++++++ .../pglite/migrations/meta/_journal.json | 7 + .../postgres/impl/connections/index.ts | 2 + .../migrations/0005_free_sqlite_path.sql | 3 + .../postgres/migrations/meta/_journal.json | 9 +- .../database/postgres/schemas/connections.ts | 7 +- apps/web/lib/database/utils.ts | 4 + apps/web/lib/explorer/capabilities.ts | 4 +- apps/web/lib/sql/sql-dialect.ts | 9 + apps/web/public/locales/en.json | 3 +- apps/web/public/locales/zh.json | 3 +- apps/web/types/common.ts | 2 +- apps/web/types/connections.ts | 17 +- apps/web/types/global.d.ts | 1 + 42 files changed, 4481 insertions(+), 106 deletions(-) create mode 100644 apps/web/app/(app)/[organization]/connections/components/forms/connection/drivers/sqlite.tsx create mode 100644 apps/web/lib/connection/display.ts create mode 100644 apps/web/lib/connection/drivers/sqlite/SqliteDatasource.ts create mode 100644 apps/web/lib/connection/drivers/sqlite/capabilities/metadata.ts create mode 100644 apps/web/lib/connection/drivers/sqlite/capabilities/table-info.ts create mode 100644 apps/web/lib/connection/drivers/sqlite/dialect.ts create mode 100644 apps/web/lib/connection/drivers/sqlite/sqlite-driver.ts create mode 100644 apps/web/lib/database/pglite/migrations/0005_wet_jack_power.sql create mode 100644 apps/web/lib/database/pglite/migrations/meta/0005_snapshot.json create mode 100644 apps/web/lib/database/postgres/migrations/0005_free_sqlite_path.sql diff --git a/apps/electron/main.ts b/apps/electron/main.ts index 967925ca..f1862faf 100644 --- a/apps/electron/main.ts +++ b/apps/electron/main.ts @@ -310,6 +310,28 @@ ipcMain.handle('auth:openExternal', async (_event, url: string) => { await shell.openExternal(url); }); +ipcMain.handle('filesystem:select-sqlite-file', async () => { + const result = await dialog.showOpenDialog({ + properties: ['openFile'], + filters: [ + { + name: 'SQLite Database', + extensions: ['sqlite', 'db', 'sqlite3'], + }, + { + name: 'All Files', + extensions: ['*'], + }, + ], + }); + + if (result.canceled) { + return null; + } + + return result.filePaths[0] ?? null; +}); + ipcMain.on('log:renderer', (_event, level: string, ...args: unknown[]) => { const safeArgs = args.map(arg => { if (typeof arg === 'string') return arg; diff --git a/apps/electron/preload.cts b/apps/electron/preload.cts index 2cf6bc13..40aae22b 100644 --- a/apps/electron/preload.cts +++ b/apps/electron/preload.cts @@ -5,6 +5,7 @@ ipcRenderer.send('log:renderer', 'info', '[preload] loaded'); contextBridge.exposeInMainWorld('electron', { platform: process.platform, isPackaged: process.env.NODE_ENV === 'production' || process.env.ELECTRON_IS_PACKAGED === 'true', + selectSqliteFile: () => ipcRenderer.invoke('filesystem:select-sqlite-file') as Promise, }); contextBridge.exposeInMainWorld('authBridge', { diff --git a/apps/web/app/(app)/[organization]/[connectionId]/chatbot/copilot/types/copilot-context-sql.ts b/apps/web/app/(app)/[organization]/[connectionId]/chatbot/copilot/types/copilot-context-sql.ts index fd833313..e46aabcf 100644 --- a/apps/web/app/(app)/[organization]/[connectionId]/chatbot/copilot/types/copilot-context-sql.ts +++ b/apps/web/app/(app)/[organization]/[connectionId]/chatbot/copilot/types/copilot-context-sql.ts @@ -1,7 +1,7 @@ export type CopilotContextSQL = { baseline: { database?: string | null; - dialect?: 'clickhouse' | 'duckdb' | 'mysql' | 'postgres' | 'unknown'; + dialect?: 'clickhouse' | 'duckdb' | 'mysql' | 'postgres' | 'sqlite' | 'unknown'; }; draft: { diff --git a/apps/web/app/(app)/[organization]/components/app-sidebar/connection-switcher/index.tsx b/apps/web/app/(app)/[organization]/components/app-sidebar/connection-switcher/index.tsx index c603bf6c..d4a62f59 100644 --- a/apps/web/app/(app)/[organization]/components/app-sidebar/connection-switcher/index.tsx +++ b/apps/web/app/(app)/[organization]/components/app-sidebar/connection-switcher/index.tsx @@ -35,6 +35,7 @@ import { ConnectionCheckStatus, ConnectionIdentity, ConnectionListIdentity, Conn import { currentConnectionAtom } from '@/shared/stores/app.store'; import { useConnectConnection } from '../../../connections/hooks/use-connect-connection'; import { useConnections } from '../../../connections/hooks/use-connections'; +import { getConnectionLocationLabel } from '@/lib/connection/display'; function getInitial(text?: string | null) { if (!text) return 'C'; @@ -42,23 +43,12 @@ function getInitial(text?: string | null) { return letter ? letter.toUpperCase() : 'C'; } -function formatHostWithPort(connection?: ConnectionListItem['connection'] | null) { - if (!connection) return null; - const rawHost = connection.host?.trim(); - const port = connection.port; - if (!rawHost && !port) return null; - if (rawHost && port) return `${rawHost}:${port}`; - if (rawHost) return rawHost; - if (typeof port === 'number') return `:${port}`; - return null; -} - function getHostLabel( connection: ConnectionListItem['connection'] | null, isLoading: boolean, t: ReturnType, ) { - const hostWithPort = formatHostWithPort(connection); + const hostWithPort = getConnectionLocationLabel(connection); if (hostWithPort) return hostWithPort; return isLoading ? t('Loading connections') : t('No connections yet'); } @@ -340,7 +330,7 @@ export function ConnectionSwitcher() { const connectionLoadingKey = makeLoadingKey(connection.connection.id, currentIdentity?.id); const connectionLoading = Boolean(connectLoadings?.[connectionLoadingKey]); - const host = formatHostWithPort(connection.connection) ?? t('Unknown host'); + const host = getConnectionLocationLabel(connection.connection) ?? t('Unknown host'); return connection.identities?.length > 1 ? ( diff --git a/apps/web/app/(app)/[organization]/components/sql-console-sidebar/sidebar-config.ts b/apps/web/app/(app)/[organization]/components/sql-console-sidebar/sidebar-config.ts index b5eb80a2..ac4822f3 100644 --- a/apps/web/app/(app)/[organization]/components/sql-console-sidebar/sidebar-config.ts +++ b/apps/web/app/(app)/[organization]/components/sql-console-sidebar/sidebar-config.ts @@ -34,6 +34,11 @@ const SIDEBAR_CONFIG_BY_DIALECT: Record = { defaultSchemaName: 'public', hiddenDatabases: ['system', 'information_schema'], }, + sqlite: { + dialect: 'sqlite', + supportsSchemas: false, + hiddenDatabases: [], + }, }; export function getSidebarConfig(connectionType?: ConnectionType | null): SidebarConfig { diff --git a/apps/web/app/(app)/[organization]/components/table-browser/table-view-tabs.tsx b/apps/web/app/(app)/[organization]/components/table-browser/table-view-tabs.tsx index e30c5952..84853360 100644 --- a/apps/web/app/(app)/[organization]/components/table-browser/table-view-tabs.tsx +++ b/apps/web/app/(app)/[organization]/components/table-browser/table-view-tabs.tsx @@ -19,10 +19,12 @@ type TableViewTabsProps = { onSubTabChange?: (tab: TableSubTab) => void; }; -const SUB_TABS: TableSubTab[] = ['overview', 'data', 'structure', 'stats']; - export function TableViewTabs({ connectionId, databaseName, tableName, driver, activeSubTab, initialSubTab = 'overview', onSubTabChange }: TableViewTabsProps) { const t = useTranslations('TableBrowser'); + const subTabs = useMemo( + () => (driver === 'sqlite' ? ['overview', 'data', 'structure'] : ['overview', 'data', 'structure', 'stats']), + [driver], + ); const [currentTab, setCurrentTab] = useState(activeSubTab ?? initialSubTab); useEffect(() => { @@ -32,7 +34,7 @@ export function TableViewTabs({ connectionId, databaseName, tableName, driver, a }, [activeSubTab]); const handleTabChange = (value: string) => { - const next = (SUB_TABS.find(tab => tab === value) ?? 'data') as TableSubTab; + const next = (subTabs.find(tab => tab === value) ?? 'data') as TableSubTab; setCurrentTab(next); onSubTabChange?.(next); }; @@ -42,7 +44,7 @@ export function TableViewTabs({ connectionId, databaseName, tableName, driver, a return ( - {SUB_TABS.map(tab => ( + {subTabs.map(tab => ( {t(`Tabs.${tab}`)} @@ -59,9 +61,11 @@ export function TableViewTabs({ connectionId, databaseName, tableName, driver, a - - - + {driver !== 'sqlite' ? ( + + + + ) : null} ); diff --git a/apps/web/app/(app)/[organization]/connections/components/connection-card.tsx b/apps/web/app/(app)/[organization]/connections/components/connection-card.tsx index 597e3f68..74ab76cd 100644 --- a/apps/web/app/(app)/[organization]/connections/components/connection-card.tsx +++ b/apps/web/app/(app)/[organization]/connections/components/connection-card.tsx @@ -7,6 +7,7 @@ import { ConnectionCheckStatus, ConnectionListItem } from '@/types/connections'; import { Edit2, Trash2, Loader2 } from 'lucide-react'; import { useTranslations } from 'next-intl'; import { useHasMounted } from '@/hooks/use-has-mounted'; +import { getConnectionLocationLabel } from '@/lib/connection/display'; type Props = { connectionItem: ConnectionListItem; @@ -23,6 +24,7 @@ export default function ConnectionCard({ connectionItem, id, connectLoading, err const hasMounted = useHasMounted(); const connection = connectionItem.connection; + const locationLabel = getConnectionLocationLabel(connection); const lastCheckStatus = (connection?.lastCheckStatus ?? 'unknown') as ConnectionCheckStatus; const lastCheckError = connection?.lastCheckError; const lastCheckAt = connection?.lastCheckAt ? new Date(connection.lastCheckAt) : null; @@ -102,10 +104,10 @@ export default function ConnectionCard({ connectionItem, id, connectLoading, err -

{connectionItem?.connection.host}

+

{locationLabel}

-

{connectionItem?.connection.host}

+

{locationLabel}

diff --git a/apps/web/app/(app)/[organization]/connections/components/connection-dialog.tsx b/apps/web/app/(app)/[organization]/connections/components/connection-dialog.tsx index 4a409c56..285189f8 100644 --- a/apps/web/app/(app)/[organization]/connections/components/connection-dialog.tsx +++ b/apps/web/app/(app)/[organization]/connections/components/connection-dialog.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { useEffect, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; +import { useForm, useWatch } from 'react-hook-form'; import { ChevronDown, ChevronUp, Loader2, Server, Shield } from 'lucide-react'; import { toast } from 'sonner'; import { useTranslations } from 'next-intl'; @@ -60,6 +60,8 @@ export function ConnectionDialog({ }); const { control, handleSubmit, reset } = form; + const connectionType = useWatch({ control, name: 'connection.type' }); + const isSqlite = connectionType === 'sqlite'; const isEditMode = mode === 'Edit' && Boolean(connectionItem?.connection?.id); @@ -79,6 +81,22 @@ export function ConnectionDialog({ return normalized; }; + const normalizeIdentityValues = (identityValues: any) => { + if (!isSqlite) { + return identityValues; + } + + return { + id: identityValues?.id, + name: identityValues?.name ?? 'SQLite', + username: identityValues?.username?.trim?.() || 'sqlite', + role: identityValues?.role ?? null, + password: null, + isDefault: true, + database: 'main', + }; + }; + useEffect(() => { if (!open) return; @@ -89,7 +107,10 @@ export function ConnectionDialog({ const nextValues = { connection: driver.normalizeForForm(connectionItem.connection), ssh: connectionItem.ssh ? { ...connectionItem.ssh } : { ...(NEW_CONNECTION_DEFAULT_VALUES as any).ssh }, - identity: formIdentity, + identity: { + ...(NEW_CONNECTION_DEFAULT_VALUES as any).identity, + ...formIdentity, + }, } as any; reset(nextValues); setSshOpen(Boolean((connectionItem as any).ssh?.enabled)); @@ -105,9 +126,10 @@ export function ConnectionDialog({ try { const connectionId = connectionItem?.connection?.id; const defaultIdentity = connectionItem?.identities?.find((iden: any) => iden.isDefault); - const sshPayload = normalizeSshValues(values.ssh, isEditMode ? connectionId : null); + const sshPayload = isSqlite ? null : normalizeSshValues(values.ssh, isEditMode ? connectionId : null); const driver = getConnectionDriver(values.connection?.type); const normalizedConnection = driver.normalizeForSubmit(values.connection); + const normalizedIdentity = normalizeIdentityValues(values.identity); const savedValues = { connection: isEditMode ? { ...normalizedConnection, id: connectionId } : normalizedConnection, @@ -115,10 +137,10 @@ export function ConnectionDialog({ identities: [ isEditMode ? { - ...values.identity, - id: values.identity?.id ?? defaultIdentity?.id, + ...normalizedIdentity, + id: normalizedIdentity?.id ?? defaultIdentity?.id, } - : values.identity, + : normalizedIdentity, ], }; console.log('onSaveSubmit values:', values, 'savedValues:', savedValues); @@ -145,19 +167,20 @@ export function ConnectionDialog({ const onValidTest = async (values: any) => { - const sshPayload = normalizeSshValues(values.ssh); + const sshPayload = isSqlite ? null : normalizeSshValues(values.ssh); const driver = getConnectionDriver(values.connection?.type); const normalizedConnection = driver.normalizeForSubmit(values.connection); + const normalizedIdentity = normalizeIdentityValues(values.identity); let testPayload = { ...values, ssh: sshPayload }; if (mode === 'Edit') { const mergedSsh = sshPayload ? { ...currentConnection?.ssh, ...sshPayload } : currentConnection?.ssh ?? null; testPayload = { connection: { ...currentConnection?.connection, ...normalizedConnection }, - identity: { ...currentConnection?.identities?.find((iden: any) => iden.isDefault), ...values.identity }, + identity: { ...currentConnection?.identities?.find((iden: any) => iden.isDefault), ...normalizedIdentity }, ssh: mergedSsh, }; } else { - testPayload = { ...values, connection: normalizedConnection, ssh: sshPayload }; + testPayload = { ...values, connection: normalizedConnection, identity: normalizedIdentity, ssh: sshPayload }; } setTesting(true); try { @@ -219,58 +242,58 @@ export function ConnectionDialog({
-

{t('Authentication Info')}

- - + {!isSqlite ?

{t('Authentication Info')}

: null} + {!isSqlite ? : null}
- -
- -
-
-
- + {!isSqlite ? ( +
+ +
+
+
+ +
+
+ {tc('SSH')} +
-
- {tc('SSH')} + +
+ ( + + {t('Enable')} + + { + field.onChange(checked); + setSshOpen(checked); + }} + /> + + + )} + /> + + + +
-
- ( - - {t('Enable')} - - { - field.onChange(checked); - setSshOpen(checked); - }} - /> - - - )} - /> - - - - -
-
- - - - -
-
+ + + + +
+ ) : null} diff --git a/apps/web/app/(app)/[organization]/connections/components/forms/connection/drivers/index.ts b/apps/web/app/(app)/[organization]/connections/components/forms/connection/drivers/index.ts index 7f30bf11..42c1420d 100644 --- a/apps/web/app/(app)/[organization]/connections/components/forms/connection/drivers/index.ts +++ b/apps/web/app/(app)/[organization]/connections/components/forms/connection/drivers/index.ts @@ -23,8 +23,15 @@ import { validatePostgresConnection, } from './postgres'; import { createMysqlConnectionDefaults, MysqlConnectionFields, normalizeMysqlConnectionForForm, normalizeMysqlConnectionForSubmit, validateMysqlConnection } from './mysql'; +import { + createSqliteConnectionDefaults, + normalizeSqliteConnectionForForm, + normalizeSqliteConnectionForSubmit, + SqliteConnectionFields, + validateSqliteConnection, +} from './sqlite'; -export type SupportedConnectionDriver = 'clickhouse' | 'mariadb' | 'mysql' | 'postgres'; +export type SupportedConnectionDriver = 'clickhouse' | 'mariadb' | 'mysql' | 'postgres' | 'sqlite'; type DriverDefinition = { label: string; @@ -68,6 +75,14 @@ const DRIVERS: Record = { normalizeForSubmit: normalizeMysqlConnectionForSubmit, validate: validateMysqlConnection, }, + sqlite: { + label: 'SQLite', + FormComponent: SqliteConnectionFields, + createDefaults: createSqliteConnectionDefaults, + normalizeForForm: normalizeSqliteConnectionForForm, + normalizeForSubmit: normalizeSqliteConnectionForSubmit, + validate: validateSqliteConnection, + }, }; export const CONNECTION_TYPE_OPTIONS = (Object.entries(DRIVERS) as Array<[SupportedConnectionDriver, DriverDefinition]>).map(([value, driver]) => ({ @@ -85,5 +100,8 @@ export function getConnectionDriver(type?: string): DriverDefinition { if (type === 'postgres') { return DRIVERS.postgres; } + if (type === 'sqlite') { + return DRIVERS.sqlite; + } return DRIVERS.clickhouse; } diff --git a/apps/web/app/(app)/[organization]/connections/components/forms/connection/drivers/sqlite.tsx b/apps/web/app/(app)/[organization]/connections/components/forms/connection/drivers/sqlite.tsx new file mode 100644 index 00000000..3e36ccea --- /dev/null +++ b/apps/web/app/(app)/[organization]/connections/components/forms/connection/drivers/sqlite.tsx @@ -0,0 +1,129 @@ +import { type RefinementCtx } from 'zod'; +import { UseFormReturn } from 'react-hook-form'; +import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/registry/new-york-v4/ui/form'; +import { Input } from '@/registry/new-york-v4/ui/input'; +import { Button } from '@/registry/new-york-v4/ui/button'; +import { FieldHelp } from './shared'; +import { useTranslations } from 'next-intl'; +import { isDesktopRuntime } from '@/lib/runtime/runtime'; + +function isAbsolutePath(value: string) { + return /^(\/|[a-zA-Z]:[\\/])/.test(value); +} + +function parseConnectionOptions(raw: unknown): Record { + if (!raw) return {}; + if (typeof raw === 'object' && !Array.isArray(raw)) return { ...(raw as Record) }; + if (typeof raw === 'string') { + try { + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + return parsed as Record; + } + } catch { + return {}; + } + } + return {}; +} + +export function createSqliteConnectionDefaults() { + return { + type: 'sqlite', + name: '', + description: '', + host: null, + port: null, + httpPort: null, + ssl: false, + database: 'main', + path: '', + environment: '', + tags: '', + }; +} + +export function normalizeSqliteConnectionForForm(connection: any) { + return { + ...createSqliteConnectionDefaults(), + ...connection, + host: null, + port: null, + httpPort: null, + ssl: false, + database: connection?.database ?? 'main', + path: connection?.path ?? '', + }; +} + +export function normalizeSqliteConnectionForSubmit(connection: any) { + const options = parseConnectionOptions(connection?.options); + delete options.ssh; + + return { + ...connection, + host: null, + port: null, + httpPort: null, + ssl: false, + database: connection?.database?.trim?.() || 'main', + path: connection?.path?.trim?.() || '', + options: JSON.stringify(options), + }; +} + +export function validateSqliteConnection(value: any, ctx: RefinementCtx) { + const normalizedPath = value?.path?.trim?.() ?? ''; + void ctx; + void normalizedPath; + void isAbsolutePath; +} + +export function SqliteConnectionFields({ form }: { form: UseFormReturn }) { + const t = useTranslations('Connections.ConnectionContent'); + const canPickFile = isDesktopRuntime() && typeof window !== 'undefined' && typeof window.electron?.selectSqliteFile === 'function'; + + return ( +
+ ( + + + + {t('Database File')} + * + + + + +
+ + {canPickFile ? ( + + ) : null} +
+
+ +
+ )} + /> +
+ ); +} diff --git a/apps/web/app/(app)/[organization]/connections/components/forms/connection/index.tsx b/apps/web/app/(app)/[organization]/connections/components/forms/connection/index.tsx index fc2e2ea0..0f17064b 100644 --- a/apps/web/app/(app)/[organization]/connections/components/forms/connection/index.tsx +++ b/apps/web/app/(app)/[organization]/connections/components/forms/connection/index.tsx @@ -28,6 +28,7 @@ export default function ConnectionForm(props: { form: UseFormReturn }) { form.setValue('connection.port', nextDefaults.port, { shouldDirty: true, shouldValidate: false }); form.setValue('connection.httpPort', nextDefaults.httpPort, { shouldDirty: true, shouldValidate: false }); form.setValue('connection.database', nextDefaults.database, { shouldDirty: true, shouldValidate: false }); + form.setValue('connection.path', currentConnection.path ?? nextDefaults.path ?? null, { shouldDirty: true, shouldValidate: false }); form.setValue('connection.ssl', nextDefaults.ssl, { shouldDirty: true, shouldValidate: false }); form.setValue('connection.description', currentConnection.description ?? nextDefaults.description, { shouldDirty: true, @@ -48,6 +49,7 @@ export default function ConnectionForm(props: { form: UseFormReturn }) { 'connection.port', 'connection.httpPort', 'connection.database', + 'connection.path', 'connection.ssl', ]); }; diff --git a/apps/web/app/(app)/[organization]/connections/form-schema.ts b/apps/web/app/(app)/[organization]/connections/form-schema.ts index 0cf7781f..a28bad49 100644 --- a/apps/web/app/(app)/[organization]/connections/form-schema.ts +++ b/apps/web/app/(app)/[organization]/connections/form-schema.ts @@ -10,22 +10,27 @@ const requiredPort = z.preprocess( z.number().int().min(1, 'Please provide a port number').max(65535, 'Port must be between 1 and 65535'), ); +function isAbsolutePath(value: string) { + return /^(\/|[a-zA-Z]:[\\/])/.test(value); +} + export const ConnectionDialogFormSchema = z.object({ connection: z.object({ type: z.string().min(1, 'Please select a connection type'), name: z.string().min(1, 'Please provide a connection name'), description: z.string().optional().nullable(), - host: z.string().min(1, 'Please provide a host'), - port: requiredPort, + host: z.string().optional().nullable(), + port: requiredPort.optional().nullable(), httpPort: requiredPort.optional().nullable(), ssl: z.boolean().default(false), database: z.string().optional().nullable(), + path: z.string().optional().nullable(), environment: z.string().optional(), tags: z.string().optional(), }), identity: z.object({ name: z.string().optional(), - username: z.string().min(1, 'Please provide a username'), + username: z.string().optional().nullable(), role: z.string().optional().nullable(), password: z.string().optional().nullable(), isDefault: z.boolean().optional(), @@ -43,4 +48,46 @@ export const ConnectionDialogFormSchema = z.object({ }).superRefine((value, ctx) => { const driver = getConnectionDriver(value.connection.type); driver.validate(value.connection, ctx); + + if (value.connection.type === 'sqlite') { + const normalizedPath = value.connection.path?.trim() ?? ''; + if (!normalizedPath) { + ctx.addIssue({ + code: 'custom', + path: ['connection', 'path'], + message: 'Please provide a SQLite file path', + }); + } else if (!isAbsolutePath(normalizedPath)) { + ctx.addIssue({ + code: 'custom', + path: ['connection', 'path'], + message: 'SQLite path must be absolute', + }); + } + return; + } + + if (!value.connection.host?.trim()) { + ctx.addIssue({ + code: 'custom', + path: ['connection', 'host'], + message: 'Please provide a host', + }); + } + + if (typeof value.connection.port !== 'number' || !Number.isFinite(value.connection.port)) { + ctx.addIssue({ + code: 'custom', + path: ['connection', 'port'], + message: 'Please provide a port number', + }); + } + + if (!value.identity.username?.trim()) { + ctx.addIssue({ + code: 'custom', + path: ['identity', 'username'], + message: 'Please provide a username', + }); + } }); diff --git a/apps/web/app/api/connection/[id]/databases/[database]/summary/route.ts b/apps/web/app/api/connection/[id]/databases/[database]/summary/route.ts index 98c08443..3645b82a 100644 --- a/apps/web/app/api/connection/[id]/databases/[database]/summary/route.ts +++ b/apps/web/app/api/connection/[id]/databases/[database]/summary/route.ts @@ -21,7 +21,7 @@ const databaseSummarySchema = z.object({ databaseName: z.string(), catalogName: z.string().nullable(), schemaName: z.string().nullable(), - engine: z.enum(['clickhouse', 'doris', 'mariadb', 'mysql', 'postgres', 'unknown']), + engine: z.enum(['clickhouse', 'doris', 'mariadb', 'mysql', 'postgres', 'sqlite', 'unknown']), cluster: z.string().nullable(), owner: z.string().nullable(), tablesCount: z.number().nullable(), @@ -159,7 +159,7 @@ export async function GET(req: NextRequest, context: { params: Promise<{ databas try { const { entry, config } = await ensureConnectionPoolForUser(userId, organizationId, connectionId, null); - const engine = (config.type ?? 'unknown') as 'clickhouse' | 'doris' | 'mariadb' | 'mysql' | 'postgres' | 'unknown'; + const engine = (config.type ?? 'unknown') as 'clickhouse' | 'doris' | 'mariadb' | 'mysql' | 'postgres' | 'sqlite' | 'unknown'; const cluster = config.port ? `${config.host}:${config.port}` : (config.host ?? null); const metadata = entry.instance.capabilities.metadata; if (!hasMetadataCapability(metadata, 'getDatabaseSummary')) { diff --git a/apps/web/app/api/connection/connect/route.ts b/apps/web/app/api/connection/connect/route.ts index 847494b5..408b4ba8 100644 --- a/apps/web/app/api/connection/connect/route.ts +++ b/apps/web/app/api/connection/connect/route.ts @@ -128,6 +128,8 @@ export const POST = withUserAndOrganizationHandler(async ({ req, db, organizatio const message = code === CONNECTION_ERROR_CODES.missingHost ? t('Api.Connection.Errors.MissingHost') + : code === CONNECTION_ERROR_CODES.missingPath + ? t('Api.Connection.Errors.MissingPath') : code === CONNECTION_ERROR_CODES.missingUsername ? t('Api.Connection.Errors.MissingUsername') : messageFromError ?? fallbackMessage; diff --git a/apps/web/app/api/connection/test/route.ts b/apps/web/app/api/connection/test/route.ts index 87bc36ac..4c68a2c9 100644 --- a/apps/web/app/api/connection/test/route.ts +++ b/apps/web/app/api/connection/test/route.ts @@ -22,6 +22,8 @@ export const POST = withUserAndOrganizationHandler(async ({ req, organizationId let message = code === CONNECTION_ERROR_CODES.missingHost ? t('Api.Connection.Errors.MissingHost') + : code === CONNECTION_ERROR_CODES.missingPath + ? t('Api.Connection.Errors.MissingPath') : code === CONNECTION_ERROR_CODES.missingUsername ? t('Api.Connection.Errors.MissingUsername') : code === CONNECTION_ERROR_CODES.missingIdentityInfo diff --git a/apps/web/app/api/connection/utils.ts b/apps/web/app/api/connection/utils.ts index 959571cb..ed845d21 100644 --- a/apps/web/app/api/connection/utils.ts +++ b/apps/web/app/api/connection/utils.ts @@ -13,6 +13,7 @@ type SshWithSecrets = ConnectionSsh & { password?: string | null; privateKey?: s export const CONNECTION_ERROR_CODES = { notFound: 'connection_not_found', missingHost: 'missing_host', + missingPath: 'missing_path', missingUsername: 'missing_username', missingIdentity: 'missing_identity', missingPassword: 'missing_password', @@ -117,7 +118,7 @@ export async function ensureConnectionPoolForUser(userId: string, organizationId export function mapConnectionErrorToResponse( error: unknown, - messages: { notFound: string; missingHost: string; fallback: string }, + messages: { notFound: string; missingHost: string; missingPath?: string; fallback: string }, ) { const code = getConnectionErrorCode(error); @@ -129,6 +130,13 @@ export function mapConnectionErrorToResponse( return NextResponse.json(ResponseUtil.error({ code: ErrorCodes.INVALID_PARAMS, message: messages.missingHost }), { status: 400 }); } + if (code === CONNECTION_ERROR_CODES.missingPath) { + return NextResponse.json( + ResponseUtil.error({ code: ErrorCodes.INVALID_PARAMS, message: messages.missingPath ?? messages.fallback }), + { status: 400 }, + ); + } + return NextResponse.json(ResponseUtil.error({ code: ErrorCodes.ERROR, message: messages.fallback }), { status: 500 }); } diff --git a/apps/web/lib/connection/base/types.ts b/apps/web/lib/connection/base/types.ts index 001e0be1..ed1fa781 100644 --- a/apps/web/lib/connection/base/types.ts +++ b/apps/web/lib/connection/base/types.ts @@ -1,7 +1,7 @@ import { QueryInsightsFilters, QueryInsightsSummary, QueryTimelinePoint, QueryInsightsRow } from '@/types/monitoring'; import { TableIndexInfo, TablePropertiesRow, TableStats } from '@/types/table-info'; -export type ConnectionType = 'clickhouse' | 'mariadb' | 'mysql' | 'postgres'; +export type ConnectionType = 'clickhouse' | 'mariadb' | 'mysql' | 'postgres' | 'sqlite'; export interface BaseConfig { id: string; // datasource_id @@ -11,6 +11,7 @@ export interface BaseConfig { username?: string; password?: string; database?: string; // Default database + path?: string; options?: Record; // Extra driver options (TLS, schema, account, settings) configVersion?: string | number; // ✅ Optional: version awareness updatedAt?: string | number; // ✅ Optional: version awareness @@ -129,7 +130,7 @@ export type DatabaseSummaryRecommendation = { rowsEstimate: number | null; }; -export type DatabaseSummaryEngine = 'clickhouse' | 'doris' | 'mariadb' | 'mysql' | 'postgres' | 'unknown'; +export type DatabaseSummaryEngine = 'clickhouse' | 'doris' | 'mariadb' | 'mysql' | 'postgres' | 'sqlite' | 'unknown'; export type DatabaseSummary = { databaseName: string; diff --git a/apps/web/lib/connection/config-builder.ts b/apps/web/lib/connection/config-builder.ts index 0886481d..021f3d5e 100644 --- a/apps/web/lib/connection/config-builder.ts +++ b/apps/web/lib/connection/config-builder.ts @@ -96,6 +96,26 @@ export function buildStoredConnectionConfig( ssh?: SshWithSecrets | null, createError: ErrorFactory = defaultErrorFactory, ): BaseConfig { + const type = resolveConnectionType(connection.type ?? connection.engine ?? 'clickhouse'); + + if (type === 'sqlite') { + const normalizedPath = connection.path?.trim(); + if (!normalizedPath) { + throw createError('missing_path'); + } + + return { + id: connection.id, + type, + host: '', + path: normalizedPath, + database: identity.database ?? connection.database ?? 'main', + options: parseConnectionOptions(connection.options) ?? undefined, + configVersion: connection.configVersion ?? undefined, + updatedAt: connection.updatedAt instanceof Date ? connection.updatedAt.getTime() : (connection.updatedAt ?? undefined), + }; + } + if (!connection?.host) { throw createError('missing_host'); } @@ -104,7 +124,6 @@ export function buildStoredConnectionConfig( } const options = buildOptions(connection.options, { httpPort: connection.httpPort, port: connection.port }, ssh); - const type = resolveConnectionType(connection.type ?? connection.engine ?? 'clickhouse'); const database = identity.database ?? (connection as any).database ?? undefined; const port = typeof connection.httpPort === 'number' ? connection.httpPort : connection.port; const updatedAt = connection.updatedAt instanceof Date ? connection.updatedAt.getTime() : connection.updatedAt; @@ -128,6 +147,23 @@ export function buildTestConnectionConfig( createError: ErrorFactory = defaultErrorFactory, ): BaseConfig { const { connection, ssh, identity } = payload; + const type = resolveConnectionType(connection.type ?? connection.engine ?? 'clickhouse'); + + if (type === 'sqlite') { + const normalizedPath = connection.path?.trim(); + if (!normalizedPath) { + throw createError('missing_path'); + } + + return { + id: connection.id || connection.name ? `test-${connection.id ?? connection.name}` : 'test-sqlite', + type, + host: '', + path: normalizedPath, + database: connection.database ?? 'main', + options: parseConnectionOptions(connection.options) ?? undefined, + }; + } if (!connection?.host) { throw createError('missing_host'); @@ -140,7 +176,6 @@ export function buildTestConnectionConfig( } const options = buildOptions(connection.options, { httpPort: connection.httpPort, port: connection.port }, ssh); - const type = resolveConnectionType(connection.type ?? connection.engine ?? 'clickhouse'); const database = identity.database ?? connection.database ?? undefined; const id = connection.name ? `test-${connection.name}` : `test-${connection.host}`; @@ -148,7 +183,7 @@ export function buildTestConnectionConfig( id, type, host: connection.host, - port: connection.port, + port: connection.port ?? undefined, username: identity.username, password: identity.password ?? undefined, database, diff --git a/apps/web/lib/connection/display.ts b/apps/web/lib/connection/display.ts new file mode 100644 index 00000000..a4e0e01b --- /dev/null +++ b/apps/web/lib/connection/display.ts @@ -0,0 +1,18 @@ +import type { ConnectionListItem } from '@/types/connections'; + +export function getConnectionLocationLabel(connection?: ConnectionListItem['connection'] | null) { + if (!connection) return null; + + if (connection.type === 'sqlite') { + const normalizedPath = connection.path?.trim(); + return normalizedPath || null; + } + + const rawHost = connection.host?.trim(); + const port = connection.port; + if (!rawHost && !port) return null; + if (rawHost && port) return `${rawHost}:${port}`; + if (rawHost) return rawHost; + if (typeof port === 'number') return `:${port}`; + return null; +} diff --git a/apps/web/lib/connection/drivers/sqlite/SqliteDatasource.ts b/apps/web/lib/connection/drivers/sqlite/SqliteDatasource.ts new file mode 100644 index 00000000..76dfe485 --- /dev/null +++ b/apps/web/lib/connection/drivers/sqlite/SqliteDatasource.ts @@ -0,0 +1,59 @@ +import { BaseConnection } from '../../base/base-connection'; +import type { ConnectionQueryContext, HealthInfo, QueryResult } from '../../base/types'; +import type { DriverQueryParams } from '../../base/params/types'; +import { SqliteDialect } from './dialect'; +import { createSqliteMetadataCapability, type SqliteMetadataAPI } from './capabilities/metadata'; +import { createSqliteTableInfoCapability } from './capabilities/table-info'; +import { executeSqliteQuery, openSqliteDatabase, pingSqlite } from './sqlite-driver'; + +export class SqliteDatasource extends BaseConnection { + readonly dialect = SqliteDialect; + + private database: ReturnType | null = null; + + constructor(config: BaseConnection['config']) { + super(config); + this.capabilities.metadata = createSqliteMetadataCapability(this); + this.capabilities.tableInfo = createSqliteTableInfoCapability(this); + } + + protected async _init(): Promise { + this.database = openSqliteDatabase(this.config); + } + + getDatabase() { + this.assertReady(); + if (!this.database) { + throw new Error('SQLite database is not initialized'); + } + return this.database; + } + + async close(): Promise { + if (this.database?.open) { + this.database.close(); + } + this.database = null; + this._initialized = false; + } + + async ping(): Promise { + return pingSqlite(this.getDatabase()); + } + + async query(sql: string, params?: DriverQueryParams, _context?: ConnectionQueryContext): Promise> { + return executeSqliteQuery(this.getDatabase(), sql, params); + } + + async queryWithContext(sql: string, context?: ConnectionQueryContext & { params?: DriverQueryParams }): Promise> { + return executeSqliteQuery(this.getDatabase(), sql, context?.params); + } + + async command(sql: string, params?: DriverQueryParams, _context?: ConnectionQueryContext): Promise { + executeSqliteQuery(this.getDatabase(), sql, params); + } + + get metadata(): SqliteMetadataAPI { + return this.capabilities.metadata as SqliteMetadataAPI; + } +} diff --git a/apps/web/lib/connection/drivers/sqlite/capabilities/metadata.ts b/apps/web/lib/connection/drivers/sqlite/capabilities/metadata.ts new file mode 100644 index 00000000..b9e3289b --- /dev/null +++ b/apps/web/lib/connection/drivers/sqlite/capabilities/metadata.ts @@ -0,0 +1,32 @@ +import type { ConnectionMetadataAPI } from '@/lib/connection/base/types'; +import { getSqliteDatabases, getSqliteTableColumns, getSqliteTables, getSqliteViews } from '../sqlite-driver'; +import type { SqliteDatasource } from '../SqliteDatasource'; + +export type SqliteMetadataAPI = Required< + Pick +>; + +export function createSqliteMetadataCapability(datasource: SqliteDatasource): SqliteMetadataAPI { + return { + async getDatabases() { + return getSqliteDatabases(); + }, + async getTableColumns(database, table) { + return getSqliteTableColumns(datasource.getDatabase(), database, table); + }, + async getTables(database) { + const tables = getSqliteTables(datasource.getDatabase(), database); + return tables.map(table => ({ + label: table.name, + value: table.name, + database: database ?? 'main', + })); + }, + async getTablesOnly(database) { + return getSqliteTables(datasource.getDatabase(), database); + }, + async getViews(database) { + return getSqliteViews(datasource.getDatabase(), database); + }, + }; +} diff --git a/apps/web/lib/connection/drivers/sqlite/capabilities/table-info.ts b/apps/web/lib/connection/drivers/sqlite/capabilities/table-info.ts new file mode 100644 index 00000000..f139c56e --- /dev/null +++ b/apps/web/lib/connection/drivers/sqlite/capabilities/table-info.ts @@ -0,0 +1,23 @@ +import type { GetTableInfoAPI } from '@/lib/connection/base/types'; +import { getSqliteTableDdl, getSqliteTableIndexes, getSqliteTableProperties, previewSqliteTable } from '../sqlite-driver'; +import type { SqliteDatasource } from '../SqliteDatasource'; + +export function createSqliteTableInfoCapability(datasource: SqliteDatasource): GetTableInfoAPI { + return { + async properties(database, table) { + return getSqliteTableProperties(datasource.getDatabase(), database, table); + }, + async ddl(database, table) { + return getSqliteTableDdl(datasource.getDatabase(), database, table); + }, + async stats() { + return null; + }, + async preview(database, table, options) { + return previewSqliteTable(datasource.getDatabase(), database, table, options?.limit ?? 100); + }, + async indexes(database, table) { + return getSqliteTableIndexes(datasource.getDatabase(), database, table); + }, + }; +} diff --git a/apps/web/lib/connection/drivers/sqlite/dialect.ts b/apps/web/lib/connection/drivers/sqlite/dialect.ts new file mode 100644 index 00000000..be9d692d --- /dev/null +++ b/apps/web/lib/connection/drivers/sqlite/dialect.ts @@ -0,0 +1,6 @@ +import type { ConnectionParameterDialect } from '@/lib/connection/registry/types'; + +export const SqliteDialect: ConnectionParameterDialect = { + id: 'sqlite', + parameterStyle: 'positional', +}; diff --git a/apps/web/lib/connection/drivers/sqlite/sqlite-driver.ts b/apps/web/lib/connection/drivers/sqlite/sqlite-driver.ts new file mode 100644 index 00000000..87fd5ce0 --- /dev/null +++ b/apps/web/lib/connection/drivers/sqlite/sqlite-driver.ts @@ -0,0 +1,238 @@ +import path from 'node:path'; +import Database from 'better-sqlite3'; +import { MAX_RESULT_ROWS } from '@/app/config/sql-console'; +import { enforceSelectLimit } from '@/lib/connection/base/limit'; +import { compileParams } from '@/lib/connection/base/params/compile'; +import type { DriverQueryParams } from '@/lib/connection/base/params/types'; +import type { BaseConfig, HealthInfo, QueryResult, TableColumnInfo } from '@/lib/connection/base/types'; +import type { TableIndexInfo, TablePropertiesRow } from '@/types/table-info'; +import { SqliteDialect } from './dialect'; + +type SqliteDatabase = InstanceType; + +const SQLITE_PRIMARY_DATABASE = 'main'; + +function assertAbsolutePath(filePath?: string): string { + const normalized = filePath?.trim(); + if (!normalized) { + throw new Error('SQLite path is required'); + } + if (!path.isAbsolute(normalized)) { + throw new Error('SQLite path must be absolute'); + } + return normalized; +} + +function normalizeDatabaseName(database?: string | null): string { + const normalized = database?.trim(); + return normalized || SQLITE_PRIMARY_DATABASE; +} + +function normalizeParams(sql: string, params?: DriverQueryParams) { + const compiled = compileParams(SqliteDialect, sql, params); + return { + sql: compiled.sql, + params: compiled.params, + }; +} + +function bindStatement(statement: ReturnType, params?: DriverQueryParams) { + if (!params) return []; + return Array.isArray(params) ? params : [params]; +} + +function normalizeColumns(statement: ReturnType) { + return statement.columns().map(column => ({ + name: column.name, + type: column.type ?? undefined, + })); +} + +function quoteIdentifier(identifier: string) { + return `"${identifier.replace(/"/g, '""')}"`; +} + +function quoteLiteral(value: string) { + return `'${value.replace(/'/g, "''")}'`; +} + +function buildQualifiedName(database: string, objectName: string) { + return `${quoteIdentifier(normalizeDatabaseName(database))}.${quoteIdentifier(objectName)}`; +} + +function buildPragma(database: string, pragmaName: string, value: string) { + return `PRAGMA ${quoteIdentifier(normalizeDatabaseName(database))}.${pragmaName}(${quoteLiteral(value)})`; +} + +function getSqliteVersion(db: SqliteDatabase): string | undefined { + const row = db.prepare('SELECT sqlite_version() AS version').get() as { version?: string } | undefined; + return row?.version; +} + +export function resolveSqlitePath(config: BaseConfig): string { + return assertAbsolutePath(config.path); +} + +export function openSqliteDatabase(config: BaseConfig): SqliteDatabase { + return new Database(resolveSqlitePath(config), { + fileMustExist: true, + }); +} + +export function pingSqlite(db: SqliteDatabase): HealthInfo & { version?: string } { + const started = Date.now(); + db.pragma('schema_version'); + + return { + ok: true, + tookMs: Date.now() - started, + version: getSqliteVersion(db), + }; +} + +export function executeSqliteQuery( + db: SqliteDatabase, + sql: string, + params?: DriverQueryParams, +): QueryResult { + const { sql: compiledSql, params: compiledParams } = normalizeParams(sql, params); + const statement = db.prepare(enforceSelectLimit(compiledSql, MAX_RESULT_ROWS)); + const boundParams = bindStatement(statement, compiledParams); + const started = Date.now(); + + if (statement.reader) { + const rows = statement.all(...boundParams) as Row[]; + return { + rows, + rowCount: rows.length, + columns: normalizeColumns(statement), + limited: /^\s*(select|with)\b/i.test(compiledSql) && rows.length >= MAX_RESULT_ROWS, + limit: /^\s*(select|with)\b/i.test(compiledSql) ? MAX_RESULT_ROWS : undefined, + tookMs: Date.now() - started, + }; + } + + const result = statement.run(...boundParams); + return { + rows: [], + rowCount: result.changes, + tookMs: Date.now() - started, + }; +} + +export function getSqliteDatabases() { + return [{ label: SQLITE_PRIMARY_DATABASE, value: SQLITE_PRIMARY_DATABASE }]; +} + +export function getSqliteTables(db: SqliteDatabase, database?: string | null) { + const targetDatabase = normalizeDatabaseName(database); + const rows = db + .prepare( + `SELECT name, NULL AS comment + FROM ${quoteIdentifier(targetDatabase)}.sqlite_schema + WHERE type = 'table' AND name NOT LIKE 'sqlite_%' + ORDER BY name`, + ) + .all() as Array<{ name: string; comment: string | null }>; + + return rows.map(row => ({ + name: row.name, + comment: row.comment, + })); +} + +export function getSqliteViews(db: SqliteDatabase, database?: string | null) { + const targetDatabase = normalizeDatabaseName(database); + const rows = db + .prepare( + `SELECT name, NULL AS comment + FROM ${quoteIdentifier(targetDatabase)}.sqlite_schema + WHERE type = 'view' + ORDER BY name`, + ) + .all() as Array<{ name: string; comment: string | null }>; + + return rows.map(row => ({ + name: row.name, + comment: row.comment, + })); +} + +export function getSqliteTableColumns(db: SqliteDatabase, database: string, table: string): TableColumnInfo[] { + const rows = db.prepare(buildPragma(database, 'table_xinfo', table)).all() as Array<{ + name: string; + type: string | null; + notnull: number; + dflt_value: string | null; + pk: number; + hidden?: number; + }>; + + return rows + .filter(row => !row.hidden) + .map(row => ({ + columnName: row.name, + columnType: row.type, + defaultExpression: row.dflt_value, + isPrimaryKey: row.pk > 0, + })); +} + +export function getSqliteTableDdl(db: SqliteDatabase, database: string, table: string): string | null { + const row = db + .prepare( + `SELECT sql + FROM ${quoteIdentifier(normalizeDatabaseName(database))}.sqlite_schema + WHERE type IN ('table', 'view') AND name = ?`, + ) + .get(table) as { sql?: string | null } | undefined; + + return row?.sql ?? null; +} + +export function getSqliteTableProperties(db: SqliteDatabase, database: string, table: string): TablePropertiesRow | null { + const columns = getSqliteTableColumns(db, database, table); + if (!columns.length) { + return null; + } + + const countRow = db + .prepare(`SELECT COUNT(*) AS rowCount FROM ${buildQualifiedName(database, table)}`) + .get() as { rowCount?: number } | undefined; + + const primaryKey = columns + .filter(column => Boolean(column.isPrimaryKey)) + .map(column => column.columnName) + .join(', '); + + return { + engine: 'sqlite', + primaryKey: primaryKey || null, + totalRows: countRow?.rowCount ?? null, + totalBytes: null, + }; +} + +export function previewSqliteTable( + db: SqliteDatabase, + database: string, + table: string, + limit: number, +): QueryResult> { + const sql = `SELECT * FROM ${buildQualifiedName(database, table)} LIMIT ?`; + return executeSqliteQuery>(db, sql, [limit]); +} + +export function getSqliteTableIndexes(db: SqliteDatabase, database: string, table: string): TableIndexInfo[] { + const rows = db.prepare(buildPragma(database, 'index_list', table)).all() as Array<{ + name: string; + origin?: string | null; + unique?: number; + }>; + + return rows.map(row => ({ + name: row.name, + isPrimary: row.origin === 'pk', + isUnique: row.unique === 1, + })); +} diff --git a/apps/web/lib/connection/registry/index.ts b/apps/web/lib/connection/registry/index.ts index 70f04f88..3f98f5f1 100644 --- a/apps/web/lib/connection/registry/index.ts +++ b/apps/web/lib/connection/registry/index.ts @@ -4,6 +4,7 @@ import { ClickhouseDatasource } from '../drivers/clickhouse/ClickhouseDatasource import { MariaDbDatasource } from '../drivers/mariadb/MariaDbDatasource'; import { MySqlDatasource } from '../drivers/mysql/MySqlDatasource'; import { PostgresDatasource } from '../drivers/postgres/PostgresDatasource'; +import { SqliteDatasource } from '../drivers/sqlite/SqliteDatasource'; const registry = new Map(); @@ -11,6 +12,7 @@ registry.set('clickhouse', ClickhouseDatasource); registry.set('mariadb', MariaDbDatasource); registry.set('mysql', MySqlDatasource); registry.set('postgres', PostgresDatasource); +registry.set('sqlite', SqliteDatasource); export function registerDriver(type: ConnectionType, ctor: ConnectionDriverCtor) { registry.set(type, ctor); diff --git a/apps/web/lib/connection/registry/types.ts b/apps/web/lib/connection/registry/types.ts index 3786d5b5..16592498 100644 --- a/apps/web/lib/connection/registry/types.ts +++ b/apps/web/lib/connection/registry/types.ts @@ -16,5 +16,5 @@ export type ConnectionParameterDialect = export type ConnectionDriverCtor = new (config: BaseConfig) => BaseConnection; export function isConnectionDriverType(value: unknown): value is ConnectionDriverType { - return value === 'clickhouse' || value === 'mariadb' || value === 'mysql' || value === 'postgres'; + return value === 'clickhouse' || value === 'mariadb' || value === 'mysql' || value === 'postgres' || value === 'sqlite'; } diff --git a/apps/web/lib/database/pglite/migrations.json b/apps/web/lib/database/pglite/migrations.json index f922ecc4..1608b9a2 100644 --- a/apps/web/lib/database/pglite/migrations.json +++ b/apps/web/lib/database/pglite/migrations.json @@ -132,5 +132,15 @@ "bps": true, "folderMillis": 1774367010423, "hash": "9a2a9241dd708fa08461678f152ff3f1f62efc0331b467ff8eafb45bc3712a92" + }, + { + "sql": [ + "ALTER TABLE \"connections\" ALTER COLUMN \"host\" DROP NOT NULL;", + "\nALTER TABLE \"connections\" ALTER COLUMN \"port\" DROP NOT NULL;", + "\nALTER TABLE \"connections\" ADD COLUMN \"path\" text;" + ], + "bps": true, + "folderMillis": 1774596423872, + "hash": "0f8ec7eeaecfacbe82ec3e05e43a0b2da8b8cba8f122e9bb401d794c10a5191e" } ] \ No newline at end of file diff --git a/apps/web/lib/database/pglite/migrations/0005_wet_jack_power.sql b/apps/web/lib/database/pglite/migrations/0005_wet_jack_power.sql new file mode 100644 index 00000000..0862566b --- /dev/null +++ b/apps/web/lib/database/pglite/migrations/0005_wet_jack_power.sql @@ -0,0 +1,3 @@ +ALTER TABLE "connections" ALTER COLUMN "host" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "connections" ALTER COLUMN "port" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "connections" ADD COLUMN "path" text; \ No newline at end of file diff --git a/apps/web/lib/database/pglite/migrations/meta/0005_snapshot.json b/apps/web/lib/database/pglite/migrations/meta/0005_snapshot.json new file mode 100644 index 00000000..174bbff8 --- /dev/null +++ b/apps/web/lib/database/pglite/migrations/meta/0005_snapshot.json @@ -0,0 +1,3654 @@ +{ + "id": "856812aa-7a03-4dcb-9ec7-95ea819dee90", + "prevId": "9d513f21-35a1-4e5b-a6be-a508eee18912", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.tabs": { + "name": "tabs", + "schema": "", + "columns": { + "tab_id": { + "name": "tab_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'sql'" + }, + "tab_name": { + "name": "tab_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'New Query'" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "database_name": { + "name": "database_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "table_name": { + "name": "table_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_sub_tab": { + "name": "active_sub_tab", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'data'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_meta": { + "name": "result_meta", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order_index": { + "name": "order_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_invitation_organization_id": { + "name": "idx_invitation_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_invitation_email": { + "name": "idx_invitation_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_invitation_status": { + "name": "idx_invitation_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "crv": { + "name": "crv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'incomplete'" + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_subscription_reference_id": { + "name": "idx_subscription_reference_id", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_stripe_subscription_id": { + "name": "idx_subscription_stripe_subscription_id", + "columns": [ + { + "expression": "stripe_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_stripe_customer_id": { + "name": "idx_subscription_stripe_customer_id", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_messages": { + "name": "chat_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parts": { + "name": "parts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_chat_messages_session_time": { + "name": "idx_chat_messages_session_time", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_messages_session_id": { + "name": "idx_chat_messages_session_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_messages_organization_conn_time": { + "name": "idx_chat_messages_organization_conn_time", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_session_state": { + "name": "chat_session_state", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_tab_id": { + "name": "active_tab_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_database": { + "name": "active_database", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_schema": { + "name": "active_schema", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "editor_context": { + "name": "editor_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_run_summary": { + "name": "last_run_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "stable_context": { + "name": "stable_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_chat_state_organization_conn": { + "name": "idx_chat_state_organization_conn", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_state_organization_tab": { + "name": "idx_chat_state_organization_tab", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active_tab_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_state_organization_updated": { + "name": "idx_chat_state_organization_updated", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_sessions": { + "name": "chat_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "tab_id": { + "name": "tab_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_database": { + "name": "active_database", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_schema": { + "name": "active_schema", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_chat_sessions_list": { + "name": "idx_chat_sessions_list", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_message_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_sessions_organization_user_type": { + "name": "idx_chat_sessions_organization_user_type", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_sessions_organization_conn": { + "name": "idx_chat_sessions_organization_conn", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_sessions_organization_db": { + "name": "idx_chat_sessions_organization_db", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active_database", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uidx_chat_sessions_copilot_tab": { + "name": "uidx_chat_sessions_copilot_tab", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tab_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat_sessions\".\"type\" = 'copilot'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_chat_sessions_id_organization": { + "name": "uq_chat_sessions_id_organization", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "ck_chat_sessions_type_tab": { + "name": "ck_chat_sessions_type_tab", + "value": "((\"chat_sessions\".\"type\" = 'copilot' AND \"chat_sessions\".\"tab_id\" IS NOT NULL) OR (\"chat_sessions\".\"type\" <> 'copilot' AND \"chat_sessions\".\"tab_id\" IS NULL))" + } + }, + "isRLSEnabled": false + }, + "public.query_audit": { + "name": "query_audit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_id": { + "name": "tab_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_name": { + "name": "connection_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "database_name": { + "name": "database_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "query_id": { + "name": "query_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sql_text": { + "name": "sql_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rows_read": { + "name": "rows_read", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "bytes_read": { + "name": "bytes_read", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rows_written": { + "name": "rows_written", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "extra_json": { + "name": "extra_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_organization_created": { + "name": "idx_organization_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_source_created": { + "name": "idx_source_created", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_query_id": { + "name": "idx_query_id", + "columns": [ + { + "expression": "query_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.members": { + "name": "members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "members_organization_id_user_id_unique": { + "name": "members_organization_id_user_id_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_members_organization": { + "name": "idx_members_organization", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_members_user": { + "name": "idx_members_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "members_user_id_user_id_fk": { + "name": "members_user_id_user_id_fk", + "tableFrom": "members", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "members_organization_id_organizations_id_fk": { + "name": "members_organization_id_organizations_id_fk", + "tableFrom": "members", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organizations_owner_user_id_user_id_fk": { + "name": "organizations_owner_user_id_user_id_fk", + "tableFrom": "organizations", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connection_identities": { + "name": "connection_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local'" + }, + "cloud_id": { + "name": "cloud_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_only'" + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "remote_updated_at": { + "name": "remote_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "database": { + "name": "database", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uniq_conn_identity_connection_name": { + "name": "uniq_conn_identity_connection_name", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"connection_identities\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "uniq_conn_identity_cloud_id": { + "name": "uniq_conn_identity_cloud_id", + "columns": [ + { + "expression": "cloud_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"connection_identities\".\"cloud_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "uniq_conn_identity_default_per_connection": { + "name": "uniq_conn_identity_default_per_connection", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"connection_identities\".\"is_default\" = true AND \"connection_identities\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_conn_identity_connection_id": { + "name": "idx_conn_identity_connection_id", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_conn_identity_created_by_user_id": { + "name": "idx_conn_identity_created_by_user_id", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_conn_identity_organization_id": { + "name": "idx_conn_identity_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_conn_identity_enabled": { + "name": "idx_conn_identity_enabled", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_conn_identity_sync_status": { + "name": "idx_conn_identity_sync_status", + "columns": [ + { + "expression": "sync_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_conn_identity_organization_cloud_id": { + "name": "idx_conn_identity_organization_cloud_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cloud_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connection_identity_secrets": { + "name": "connection_identity_secrets", + "schema": "", + "columns": { + "identity_id": { + "name": "identity_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "password_encrypted": { + "name": "password_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_ref": { + "name": "vault_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_ref": { + "name": "secret_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connection_ssh": { + "name": "connection_ssh", + "schema": "", + "columns": { + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_method": { + "name": "auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_encrypted": { + "name": "password_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key_encrypted": { + "name": "private_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "passphrase_encrypted": { + "name": "passphrase_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chk_connection_ssh_port": { + "name": "chk_connection_ssh_port", + "value": "\"connection_ssh\".\"port\" IS NULL OR (\"connection_ssh\".\"port\" BETWEEN 1 AND 65535)" + } + }, + "isRLSEnabled": false + }, + "public.connections": { + "name": "connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local'" + }, + "cloud_id": { + "name": "cloud_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_only'" + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "remote_updated_at": { + "name": "remote_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "engine": { + "name": "engine", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Untitled connection'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "http_port": { + "name": "http_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "database": { + "name": "database", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "config_version": { + "name": "config_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "validation_errors": { + "name": "validation_errors", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_check_status": { + "name": "last_check_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "last_check_at": { + "name": "last_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_check_latency_ms": { + "name": "last_check_latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_check_error": { + "name": "last_check_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": { + "uniq_connections_organization_name": { + "name": "uniq_connections_organization_name", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"connections\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "uniq_connections_cloud_id": { + "name": "uniq_connections_cloud_id", + "columns": [ + { + "expression": "cloud_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"connections\".\"cloud_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_connections_organization_id_status": { + "name": "idx_connections_organization_id_status", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_connections_created_by_user_id": { + "name": "idx_connections_created_by_user_id", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_connections_sync_status": { + "name": "idx_connections_sync_status", + "columns": [ + { + "expression": "sync_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_connections_organization_cloud_id": { + "name": "idx_connections_organization_cloud_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cloud_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_connections_organization_env": { + "name": "idx_connections_organization_env", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chk_connections_port": { + "name": "chk_connections_port", + "value": "\"connections\".\"port\" IS NULL OR (\"connections\".\"port\" BETWEEN 1 AND 65535)" + }, + "chk_connections_http_port": { + "name": "chk_connections_http_port", + "value": "\"connections\".\"http_port\" IS NULL OR (\"connections\".\"http_port\" BETWEEN 1 AND 65535)" + } + }, + "isRLSEnabled": false + }, + "public.ai_schema_cache": { + "name": "ai_schema_cache", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog": { + "name": "catalog", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "database_name": { + "name": "database_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "table_name": { + "name": "table_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "db_type": { + "name": "db_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_hash": { + "name": "schema_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt_version": { + "name": "prompt_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uniq_ai_cache_organization_conn_catalog_feature_schema_model_prompt": { + "name": "uniq_ai_cache_organization_conn_catalog_feature_schema_model_prompt", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "catalog", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "feature", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "schema_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "prompt_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_cache_organization_conn": { + "name": "idx_ai_cache_organization_conn", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_cache_catalog_db_table": { + "name": "idx_ai_cache_catalog_db_table", + "columns": [ + { + "expression": "catalog", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "database_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_cache_schema_hash": { + "name": "idx_ai_cache_schema_hash", + "columns": [ + { + "expression": "schema_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_queries": { + "name": "saved_queries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sql_text": { + "name": "sql_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "work_id": { + "name": "work_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_saved_queries_organization_user": { + "name": "idx_saved_queries_organization_user", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_saved_queries_updated_at": { + "name": "idx_saved_queries_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_saved_queries_folder_id": { + "name": "idx_saved_queries_folder_id", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_query_folders": { + "name": "saved_query_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_saved_query_folders_organization_user": { + "name": "idx_saved_query_folders_organization_user", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_saved_query_folders_connection_id": { + "name": "idx_saved_query_folders_connection_id", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_usage_events": { + "name": "ai_usage_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_version": { + "name": "prompt_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "algo_version": { + "name": "algo_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ok'" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gateway": { + "name": "gateway", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_micros": { + "name": "cost_micros", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "span_id": { + "name": "span_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_tokens": { + "name": "total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_json": { + "name": "usage_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "from_cache": { + "name": "from_cache", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uidx_ai_usage_events_request_id": { + "name": "uidx_ai_usage_events_request_id", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_events_organization_created": { + "name": "idx_ai_usage_events_organization_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_events_organization_created_total": { + "name": "idx_ai_usage_events_organization_created_total", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "total_tokens", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_events_organization_user_created": { + "name": "idx_ai_usage_events_organization_user_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_events_feature_created": { + "name": "idx_ai_usage_events_feature_created", + "columns": [ + { + "expression": "feature", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_events_organization_feature_created": { + "name": "idx_ai_usage_events_organization_feature_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "feature", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ck_ai_usage_events_status": { + "name": "ck_ai_usage_events_status", + "value": "\"ai_usage_events\".\"status\" in ('ok', 'error', 'aborted')" + } + }, + "isRLSEnabled": false + }, + "public.ai_usage_traces": { + "name": "ai_usage_traces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_text": { + "name": "input_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output_text": { + "name": "output_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_json": { + "name": "input_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "output_json": { + "name": "output_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "redacted": { + "name": "redacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now() + interval '180 days'" + } + }, + "indexes": { + "uidx_ai_usage_traces_request_id": { + "name": "uidx_ai_usage_traces_request_id", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_traces_organization_created": { + "name": "idx_ai_usage_traces_organization_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_traces_organization_user_created": { + "name": "idx_ai_usage_traces_organization_user_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_traces_expires_at": { + "name": "idx_ai_usage_traces_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_operations": { + "name": "sync_operations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connection'" + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_sync_operations_organization_status": { + "name": "idx_sync_operations_organization_status", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sync_operations_entity": { + "name": "idx_sync_operations_entity", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sync_operations_created_at": { + "name": "idx_sync_operations_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/web/lib/database/pglite/migrations/meta/_journal.json b/apps/web/lib/database/pglite/migrations/meta/_journal.json index 98a654c8..b286cd40 100644 --- a/apps/web/lib/database/pglite/migrations/meta/_journal.json +++ b/apps/web/lib/database/pglite/migrations/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1774367010423, "tag": "0004_short_quasimodo", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1774596423872, + "tag": "0005_wet_jack_power", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/web/lib/database/postgres/impl/connections/index.ts b/apps/web/lib/database/postgres/impl/connections/index.ts index b356860e..6c518d44 100644 --- a/apps/web/lib/database/postgres/impl/connections/index.ts +++ b/apps/web/lib/database/postgres/impl/connections/index.ts @@ -90,6 +90,7 @@ export class PostgresConnectionsRepository { port: row.port, httpPort: row.httpPort, database: row.database, + path: row.path, options: row.options, configVersion: row.configVersion, createdAt: row.createdAt, @@ -415,6 +416,7 @@ export class PostgresConnectionsRepository { type: row.type, engine: row.engine, database: row.database, + path: row.path, id: row.id, host: row.host, port: row.port, diff --git a/apps/web/lib/database/postgres/migrations/0005_free_sqlite_path.sql b/apps/web/lib/database/postgres/migrations/0005_free_sqlite_path.sql new file mode 100644 index 00000000..8baaeec2 --- /dev/null +++ b/apps/web/lib/database/postgres/migrations/0005_free_sqlite_path.sql @@ -0,0 +1,3 @@ +ALTER TABLE "connections" ADD COLUMN "path" text; +ALTER TABLE "connections" ALTER COLUMN "host" DROP NOT NULL; +ALTER TABLE "connections" ALTER COLUMN "port" DROP NOT NULL; diff --git a/apps/web/lib/database/postgres/migrations/meta/_journal.json b/apps/web/lib/database/postgres/migrations/meta/_journal.json index a87a4fca..a3fea3c0 100644 --- a/apps/web/lib/database/postgres/migrations/meta/_journal.json +++ b/apps/web/lib/database/postgres/migrations/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1774367009728, "tag": "0004_pretty_morg", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1775000000000, + "tag": "0005_free_sqlite_path", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/apps/web/lib/database/postgres/schemas/connections.ts b/apps/web/lib/database/postgres/schemas/connections.ts index 2f2672dd..735cbc02 100644 --- a/apps/web/lib/database/postgres/schemas/connections.ts +++ b/apps/web/lib/database/postgres/schemas/connections.ts @@ -2,7 +2,7 @@ import { boolean, integer, text, timestamp, pgTable, check, index, uniqueIndex } import { sql } from 'drizzle-orm'; import { newEntityId } from '@/lib/id'; -export type ConnectionType = 'clickhouse' | 'doris' | 'mariadb' | 'mysql' | 'postgres'; +export type ConnectionType = 'clickhouse' | 'doris' | 'mariadb' | 'mysql' | 'postgres' | 'sqlite'; export type ConnectionStatus = 'draft' | 'ready' | 'error' | 'disabled'; export type SyncSource = 'local' | 'cloud'; export type SyncStatus = @@ -44,10 +44,11 @@ export const connections = pgTable( name: text('name').notNull().default('Untitled connection'), description: text('description'), - host: text('host').notNull(), - port: integer('port').notNull(), + host: text('host'), + port: integer('port'), httpPort: integer('http_port'), database: text('database'), + path: text('path'), // Extended config: // App layer handles JSON.parse / JSON.stringify diff --git a/apps/web/lib/database/utils.ts b/apps/web/lib/database/utils.ts index fc4eceee..62fb8eca 100644 --- a/apps/web/lib/database/utils.ts +++ b/apps/web/lib/database/utils.ts @@ -21,6 +21,10 @@ export const DatasourceTypesWithDBEngine = [ type: 'doris', engine: 'doris', }, + { + type: 'sqlite', + engine: 'sqlite', + }, ]; export function getDBEngineViaType(type: string): string { diff --git a/apps/web/lib/explorer/capabilities.ts b/apps/web/lib/explorer/capabilities.ts index 7b478c1f..9a1d9dca 100644 --- a/apps/web/lib/explorer/capabilities.ts +++ b/apps/web/lib/explorer/capabilities.ts @@ -61,10 +61,10 @@ export const EXPLORER_CAPABILITIES: Record = sqlite: { driver: 'sqlite', supportsSchema: false, - supportsDatabase: false, + supportsDatabase: true, supportsCatalog: false, listKinds: ['tables', 'views'], - objectKinds: ['table', 'view'], + objectKinds: ['database', 'table', 'view'], }, trino: { driver: 'trino', diff --git a/apps/web/lib/sql/sql-dialect.ts b/apps/web/lib/sql/sql-dialect.ts index c2b1e3ae..4045c16c 100644 --- a/apps/web/lib/sql/sql-dialect.ts +++ b/apps/web/lib/sql/sql-dialect.ts @@ -41,6 +41,12 @@ const SQL_DIALECT_CONFIGS: Record = { monacoLanguageId: 'pgsql', formatterLanguage: 'postgresql', }, + sqlite: { + dialect: 'sqlite', + parserKey: 'mysql', + monacoLanguageId: 'mysql', + formatterLanguage: 'sqlite', + }, unknown: { dialect: 'unknown', parserKey: 'mysql', @@ -54,6 +60,7 @@ const SQL_DIALECT_BY_CONNECTION_TYPE: Partial>(); @@ -80,6 +87,8 @@ export const normalizeSqlDialect = (value?: string | null): ConnectionDialect => case 'postgres': case 'postgresql': return 'postgres'; + case 'sqlite': + return 'sqlite'; default: return 'unknown'; } diff --git a/apps/web/public/locales/en.json b/apps/web/public/locales/en.json index 7894d913..f322c5a3 100644 --- a/apps/web/public/locales/en.json +++ b/apps/web/public/locales/en.json @@ -1640,6 +1640,7 @@ "FetchCatalogFailed": "Failed to fetch catalog context", "NotFound": "Connection not found or insufficient permissions", "MissingHost": "Connection is missing host information", + "MissingPath": "Connection is missing SQLite file path information", "MissingConnectionId": "Missing connection ID", "MissingUsername": "Missing username", "MissingIdentity": "No available identity found", @@ -2203,4 +2204,4 @@ "Routes": { "SQL Console": "SQL Console" } -} \ No newline at end of file +} diff --git a/apps/web/public/locales/zh.json b/apps/web/public/locales/zh.json index 1385ccbb..107ea1ca 100644 --- a/apps/web/public/locales/zh.json +++ b/apps/web/public/locales/zh.json @@ -1642,6 +1642,7 @@ "FetchCatalogFailed": "获取目录对象失败", "NotFound": "数据源不存在或无权限", "MissingHost": "数据源缺少主机信息", + "MissingPath": "数据源缺少 SQLite 文件路径信息", "MissingConnectionId": "缺少数据源 ID", "MissingUsername": "缺少用户名", "MissingIdentity": "未找到可用身份", @@ -2205,4 +2206,4 @@ "Routes": { "SQL Console": "SQL 控制台" } -} \ No newline at end of file +} diff --git a/apps/web/types/common.ts b/apps/web/types/common.ts index 849797e1..e8c81520 100644 --- a/apps/web/types/common.ts +++ b/apps/web/types/common.ts @@ -3,4 +3,4 @@ export type Timestamp = number; export const UNKNOWN_ID = "unknown"; -export type ConnectionDialect = 'clickhouse' | 'duckdb' | 'mysql' | 'postgres' | 'unknown'; +export type ConnectionDialect = 'clickhouse' | 'duckdb' | 'mysql' | 'postgres' | 'sqlite' | 'unknown'; diff --git a/apps/web/types/connections.ts b/apps/web/types/connections.ts index b0cef8d7..c732c1d0 100644 --- a/apps/web/types/connections.ts +++ b/apps/web/types/connections.ts @@ -1,4 +1,4 @@ -export type ConnectionType = 'clickhouse' | 'doris' | 'mariadb' | 'mysql' | 'postgres'; +export type ConnectionType = 'clickhouse' | 'doris' | 'mariadb' | 'mysql' | 'postgres' | 'sqlite'; export type ConnectionStatus = 'Connected' | 'Error' | 'Disconnected'; export type ConnectionCheckStatus = 'unknown' | 'ok' | 'error'; export type ConnectionIdentityStatus = 'active' | 'disabled'; @@ -15,10 +15,11 @@ export interface Connection { name: string; description: string | null; - host: string; - port: number; + host: string | null; + port: number | null; httpPort: number | null; database: string | null; + path: string | null; options: string; @@ -112,10 +113,11 @@ export interface ConnectionCreateInput { name: string; description?: string; - host: string; - port: number; + host: string | null; + port: number | null; httpPort?: number; database?: string; + path?: string | null; options?: string; status?: ConnectionStatus; @@ -131,10 +133,11 @@ export interface ConnectionUpdateInput { name?: string; description?: string | null; - host?: string; - port?: number; + host?: string | null; + port?: number | null; httpPort?: number | null; database?: string | null; + path?: string | null; options?: string; status?: ConnectionStatus; diff --git a/apps/web/types/global.d.ts b/apps/web/types/global.d.ts index d0243d09..211ef2db 100644 --- a/apps/web/types/global.d.ts +++ b/apps/web/types/global.d.ts @@ -38,5 +38,6 @@ interface Window { electron?: { platform: string; isPackaged: boolean; + selectSqliteFile?: () => Promise; }; } From 14f4389c374ca965ac70e19d4402ab80fbf4e277 Mon Sep 17 00:00:00 2001 From: Jeffrey Date: Fri, 27 Mar 2026 15:55:46 +0800 Subject: [PATCH 2/6] fix: fix the explorer error --- .../components/data-preview/index.tsx | 10 +- .../sidebar/explorer-sidebar-tree.tsx | 24 +++-- .../components/sidebar/explorer-sidebar.tsx | 24 +++-- .../components/explorer/explorer-router.tsx | 9 +- .../database/tabs/materialized-views-tab.tsx | 9 +- .../resources/database/tabs/tables-tab.tsx | 13 ++- .../resources/database/tabs/views-tab.tsx | 8 +- .../database/views/fallback-database-view.tsx | 102 +++++++++++++----- apps/web/lib/explorer/capabilities.ts | 6 ++ 9 files changed, 148 insertions(+), 57 deletions(-) diff --git a/apps/web/app/(app)/[organization]/components/table-browser/components/data-preview/index.tsx b/apps/web/app/(app)/[organization]/components/table-browser/components/data-preview/index.tsx index 40b46e75..0ddedb58 100644 --- a/apps/web/app/(app)/[organization]/components/table-browser/components/data-preview/index.tsx +++ b/apps/web/app/(app)/[organization]/components/table-browser/components/data-preview/index.tsx @@ -85,6 +85,7 @@ function DataPreview({ const [query, setQuery] = useState(''); const [rows, setRows] = useState([]); const [loading, setLoading] = useState(false); + const [hasLoaded, setHasLoaded] = useState(false); const [error, setError] = useState(null); const [stats, setStats] = useState({ filteredCount: 0, totalCount: 0 }); const [inspectorOpen, setInspectorOpen] = useState(false); @@ -95,6 +96,8 @@ function DataPreview({ useEffect(() => { setRows([]); + setLoading(Boolean(connectionId && databaseName && tableName)); + setHasLoaded(false); setSessionMeta({}); setQuery(''); setStats({ filteredCount: 0, totalCount: 0 }); @@ -142,7 +145,10 @@ function DataPreview({ if (e?.name === 'AbortError') return; setError(e?.message ?? t('Failed to load data preview')); } finally { - setLoading(false); + if (!signal?.aborted) { + setLoading(false); + setHasLoaded(true); + } } }, [connectionId, databaseName, source, storageKey, tableName, setSessionMeta, t], @@ -230,7 +236,7 @@ function DataPreview({ ); } - if (loading && rows.length === 0) { + if (!hasLoaded || (loading && rows.length === 0)) { return (
{t('Loading preview')} diff --git a/apps/web/components/explorer/components/sidebar/explorer-sidebar-tree.tsx b/apps/web/components/explorer/components/sidebar/explorer-sidebar-tree.tsx index 67ebf541..00392fdb 100644 --- a/apps/web/components/explorer/components/sidebar/explorer-sidebar-tree.tsx +++ b/apps/web/components/explorer/components/sidebar/explorer-sidebar-tree.tsx @@ -13,6 +13,7 @@ import type { DatabaseObjects, GroupState, SchemaNode, SidebarListKind, SidebarL type ExplorerSidebarTreeProps = { catalogName: string; + groupKeys: (keyof GroupState)[]; showCatalog: boolean; expandedCatalog: boolean; filteredDatabases: { label: string; value: string }[]; @@ -54,6 +55,7 @@ function isAnyGroupLoading(groupState?: GroupState) { export function ExplorerSidebarTree({ catalogName, + groupKeys, showCatalog, expandedCatalog, filteredDatabases, @@ -84,12 +86,22 @@ export function ExplorerSidebarTree({ getSchemaObjects, }: ExplorerSidebarTreeProps) { const t = useTranslations('CatalogSchemaSidebar'); - const groupConfigs: GroupConfig[] = [ - { key: 'tables', label: t('Tables'), icon: Table, emptyLabel: t('No tables') }, - { key: 'views', label: t('Views'), icon: Eye, emptyLabel: t('No views') }, - { key: 'materializedViews', label: t('Materialized views'), icon: Boxes, emptyLabel: t('No materialized views') }, - { key: 'functions', label: t('Functions'), icon: Sigma, emptyLabel: t('No functions') }, - ]; + const groupConfigs: GroupConfig[] = groupKeys + .map(groupKey => { + switch (groupKey) { + case 'tables': + return { key: 'tables', label: t('Tables'), icon: Table, emptyLabel: t('No tables') }; + case 'views': + return { key: 'views', label: t('Views'), icon: Eye, emptyLabel: t('No views') }; + case 'materializedViews': + return { key: 'materializedViews', label: t('Materialized views'), icon: Boxes, emptyLabel: t('No materialized views') }; + case 'functions': + return { key: 'functions', label: t('Functions'), icon: Sigma, emptyLabel: t('No functions') }; + default: + return null; + } + }) + .filter((config): config is GroupConfig => Boolean(config)); const showList = showCatalog ? expandedCatalog : true; return ( diff --git a/apps/web/components/explorer/components/sidebar/explorer-sidebar.tsx b/apps/web/components/explorer/components/sidebar/explorer-sidebar.tsx index 57c33dcf..b9b0142d 100644 --- a/apps/web/components/explorer/components/sidebar/explorer-sidebar.tsx +++ b/apps/web/components/explorer/components/sidebar/explorer-sidebar.tsx @@ -13,6 +13,7 @@ import { useDatabases } from '@/hooks/use-databases'; import type { ResponseObject } from '@/types'; import { getSidebarConfig } from '@/app/(app)/[organization]/components/sql-console-sidebar/sidebar-config'; import { authFetch } from '@/lib/client/auth-fetch'; +import { getDriverCapabilities } from '@/lib/explorer/capabilities'; import { isSuccess } from '@/lib/result'; import { activeDatabaseAtom, currentConnectionAtom } from '@/shared/stores/app.store'; import { ExplorerSidebarTree } from './explorer-sidebar-tree'; @@ -104,6 +105,10 @@ export function ExplorerSidebar({ const connectionId = resolveParam(params?.connectionId) ?? currentConnection?.connection?.id; const connectionType = currentConnection?.connection?.type; const sidebarConfig = useMemo(() => getSidebarConfig(connectionType), [connectionType]); + const supportedGroupKeys = useMemo( + () => GROUP_KEYS.filter(group => getDriverCapabilities(connectionType).listKinds.includes(group)), + [connectionType], + ); const supportsSchemas = sidebarConfig.supportsSchemas; const defaultSchemaName = sidebarConfig.defaultSchemaName ?? 'public'; const showCatalog = false; // For now, we are hiding the catalog level as it's not commonly used and adds extra complexity to the UI. We can revisit this decision in the future if needed. @@ -174,7 +179,7 @@ export function ExplorerSidebar({ const groupQueries = useQueries({ queries: databaseEntries.flatMap(entry => - GROUP_KEYS.map(group => ({ + supportedGroupKeys.map(group => ({ queryKey: ['catalog-db-group', connectionId, entry.value, group] as const, queryFn: async ({ signal }: { signal?: AbortSignal }): Promise => { if (!connectionId) return []; @@ -218,7 +223,7 @@ export function ExplorerSidebar({ databaseEntries.forEach(entry => { const objects: DatabaseObjects = { ...EMPTY_DATABASE_OBJECTS }; - GROUP_KEYS.forEach(group => { + supportedGroupKeys.forEach(group => { const data = groupQueries[index]?.data; if (Array.isArray(data)) { objects[group] = data; @@ -230,7 +235,7 @@ export function ExplorerSidebar({ }); return next; - }, [databaseEntries, groupQueries]); + }, [databaseEntries, groupQueries, supportedGroupKeys]); const loadingGroups = useMemo(() => { const next: Record = {}; @@ -239,7 +244,7 @@ export function ExplorerSidebar({ databaseEntries.forEach(entry => { const databaseLoading: GroupState = { ...DEFAULT_GROUP_STATE }; - GROUP_KEYS.forEach(group => { + supportedGroupKeys.forEach(group => { databaseLoading[group] = Boolean(groupQueries[index]?.isFetching); index += 1; }); @@ -253,7 +258,7 @@ export function ExplorerSidebar({ }); return next; - }, [databaseEntries, expandedSchemas, groupQueries]); + }, [databaseEntries, expandedSchemas, groupQueries, supportedGroupKeys]); const databaseSchemas = useMemo(() => { const next: Record = {}; @@ -296,7 +301,7 @@ export function ExplorerSidebar({ const perSchema: Record = {}; const objects = databaseObjects[entry.value] ?? EMPTY_DATABASE_OBJECTS; - GROUP_KEYS.forEach(group => { + supportedGroupKeys.forEach(group => { objects[group].forEach(item => { const schemaName = resolveSchemaName(item, defaultSchemaName); if (!schemaName) return; @@ -318,7 +323,7 @@ export function ExplorerSidebar({ }); return next; - }, [databaseEntries, databaseObjects, defaultSchemaName, supportsSchemas]); + }, [databaseEntries, databaseObjects, defaultSchemaName, supportedGroupKeys, supportsSchemas]); useEffect(() => { if (!selectedDatabase) return; @@ -451,9 +456,9 @@ export function ExplorerSidebar({ const objects = databaseObjects[db.value]; if (!objects) return false; - return GROUP_KEYS.some(group => filterEntries(objects[group]).length > 0); + return supportedGroupKeys.some(group => filterEntries(objects[group]).length > 0); }); - }, [databaseEntries, databaseObjects, databaseSchemas, filterEntries, normalized]); + }, [databaseEntries, databaseObjects, databaseSchemas, filterEntries, normalized, supportedGroupKeys]); const hasAnyResults = useMemo(() => { if (!normalized) return true; @@ -484,6 +489,7 @@ export function ExplorerSidebar({
: null} {route.pageType === 'namespace' && route.resource ? ( } @@ -38,13 +40,18 @@ export function ExplorerRouter({ baseParams, route }: ExplorerRouterProps) { ) : null} {route.pageType === 'schemaSummary' && route.resource ? ( } /> ) : null} {route.pageType === 'object' && route.resource ? ( - } /> + } + /> ) : null} {route.pageType === 'notFound' ? : null}
diff --git a/apps/web/components/explorer/resources/database/tabs/materialized-views-tab.tsx b/apps/web/components/explorer/resources/database/tabs/materialized-views-tab.tsx index 7fc92117..9e1f0606 100644 --- a/apps/web/components/explorer/resources/database/tabs/materialized-views-tab.tsx +++ b/apps/web/components/explorer/resources/database/tabs/materialized-views-tab.tsx @@ -8,7 +8,6 @@ import { useParams } from 'next/navigation'; import { useLocale, useTranslations } from 'next-intl'; import { StickyDataTable } from '@/components/@dory/ui/sticky-data-table'; import { Input } from '@/registry/new-york-v4/ui/input'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/registry/new-york-v4/ui/select'; import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@/registry/new-york-v4/ui/tooltip'; import { OverflowTooltip } from '@/components/overflow-tooltip'; import type { ResponseObject } from '@/types'; @@ -55,9 +54,11 @@ const formatTimestampWithLocale = (value: string | null | undefined, locale: str timeStyle: 'short', }).format(date); }; +type DatabaseMaterializedViewsProps = { + database?: string | null; +}; - -export default function DatabaseMaterializedViews() { +export default function DatabaseMaterializedViews({ database }: DatabaseMaterializedViewsProps) { const [searchValue, setSearchValue] = React.useState(''); const [rows, setRows] = React.useState([]); const [loading, setLoading] = React.useState(false); @@ -65,7 +66,7 @@ export default function DatabaseMaterializedViews() { const t = useTranslations('Catalog'); const locale = useLocale(); const params = useParams<{ database?: string | string[] }>(); - const databaseParam = resolveParam(params?.database); + const databaseParam = database ?? resolveParam(params?.database); const databaseName = React.useMemo(() => { if (!databaseParam) return ''; try { diff --git a/apps/web/components/explorer/resources/database/tabs/tables-tab.tsx b/apps/web/components/explorer/resources/database/tabs/tables-tab.tsx index 21e79b64..82746cef 100644 --- a/apps/web/components/explorer/resources/database/tabs/tables-tab.tsx +++ b/apps/web/components/explorer/resources/database/tabs/tables-tab.tsx @@ -9,7 +9,6 @@ import { useParams } from 'next/navigation'; import { useLocale, useTranslations } from 'next-intl'; import { StickyDataTable } from '@/components/@dory/ui/sticky-data-table'; import { Input } from '@/registry/new-york-v4/ui/input'; -import { Button } from '@/registry/new-york-v4/ui/button'; import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@/registry/new-york-v4/ui/tooltip'; import { OverflowTooltip } from '@/components/overflow-tooltip'; import { buildExplorerObjectPath } from '@/lib/explorer/build-path'; @@ -58,7 +57,12 @@ const formatTimestampWithLocale = (value: string | null | undefined, locale: str }).format(date); }; -export default function DatabaseTables() { +type DatabaseTablesProps = { + catalog?: string | null; + database?: string | null; +}; + +export default function DatabaseTables({ catalog, database }: DatabaseTablesProps) { const [searchValue, setSearchValue] = React.useState(''); const [rows, setRows] = React.useState([]); const [loading, setLoading] = React.useState(false); @@ -74,15 +78,16 @@ export default function DatabaseTables() { const organizationId = resolveParam(params?.organization); const connectionId = resolveParam(params?.connectionId) ?? currentConnection?.connection.id; const catalogParam = resolveParam(params?.catalog); - const databaseParam = resolveParam(params?.database); + const databaseParam = database ?? resolveParam(params?.database); const catalogName = React.useMemo(() => { + if (catalog) return catalog; if (!catalogParam) return DEFAULT_CATALOG; try { return decodeURIComponent(catalogParam); } catch { return catalogParam; } - }, [catalogParam]); + }, [catalog, catalogParam]); const databaseName = React.useMemo(() => { if (!databaseParam) return ''; try { diff --git a/apps/web/components/explorer/resources/database/tabs/views-tab.tsx b/apps/web/components/explorer/resources/database/tabs/views-tab.tsx index df0bb75c..12011ff0 100644 --- a/apps/web/components/explorer/resources/database/tabs/views-tab.tsx +++ b/apps/web/components/explorer/resources/database/tabs/views-tab.tsx @@ -56,7 +56,11 @@ const formatTimestampWithLocale = (value: string | null | undefined, locale: str }; -export default function DatabaseViews() { +type DatabaseViewsProps = { + database?: string | null; +}; + +export default function DatabaseViews({ database }: DatabaseViewsProps) { const [searchValue, setSearchValue] = React.useState(''); const [rows, setRows] = React.useState([]); const [loading, setLoading] = React.useState(false); @@ -64,7 +68,7 @@ export default function DatabaseViews() { const t = useTranslations('Catalog'); const locale = useLocale(); const params = useParams<{ database?: string | string[] }>(); - const databaseParam = resolveParam(params?.database); + const databaseParam = database ?? resolveParam(params?.database); const databaseName = React.useMemo(() => { if (!databaseParam) return ''; try { diff --git a/apps/web/components/explorer/resources/database/views/fallback-database-view.tsx b/apps/web/components/explorer/resources/database/views/fallback-database-view.tsx index 77233ff5..90a63b72 100644 --- a/apps/web/components/explorer/resources/database/views/fallback-database-view.tsx +++ b/apps/web/components/explorer/resources/database/views/fallback-database-view.tsx @@ -1,8 +1,11 @@ 'use client'; -import { Activity, useEffect, useState } from 'react'; +import { Activity, useEffect, useMemo, useState } from 'react'; +import { useAtomValue } from 'jotai'; import { useTranslations } from 'next-intl'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/registry/new-york-v4/ui/tabs'; +import { getDriverCapabilities, supportsDatabaseSummary } from '@/lib/explorer/capabilities'; +import { currentConnectionAtom } from '@/shared/stores/app.store'; import DatabaseSummary from '../components/database-summary'; import DatabaseTables from '../tabs/tables-tab'; import DatabaseViews from '../tabs/views-tab'; @@ -11,10 +14,35 @@ import type { ExplorerListKind, ExplorerResource } from '@/lib/explorer/types'; type SubTab = 'summary' | 'tables' | 'views' | 'materialized-views'; -const SUB_TABS: SubTab[] = ['summary', 'tables', 'views', 'materialized-views']; +function getAvailableTabs(driver?: string | null): SubTab[] { + const capabilities = getDriverCapabilities(driver); + const tabs: SubTab[] = []; -function resolveInitialTab(resource: Extract): SubTab { - if (resource.kind !== 'list' || resource.schema) return 'summary'; + if (supportsDatabaseSummary(driver)) { + tabs.push('summary'); + } + + if (capabilities.listKinds.includes('tables')) { + tabs.push('tables'); + } + + if (capabilities.listKinds.includes('views')) { + tabs.push('views'); + } + + if (capabilities.listKinds.includes('materializedViews')) { + tabs.push('materialized-views'); + } + + return tabs; +} + +function resolveInitialTab(resource: Extract, availableTabs: SubTab[]): SubTab { + const fallbackTab = availableTabs[0] ?? 'tables'; + + if (resource.kind !== 'list' || resource.schema) { + return availableTabs.includes('summary') ? 'summary' : fallbackTab; + } const map: Partial> = { tables: 'tables', @@ -22,7 +50,13 @@ function resolveInitialTab(resource: Extract(() => resolveInitialTab(resource)); + const currentConnection = useAtomValue(currentConnectionAtom); const t = useTranslations('Catalog'); + const availableTabs = useMemo(() => getAvailableTabs(currentConnection?.connection?.type), [currentConnection?.connection?.type]); + const [currentTab, setCurrentTab] = useState(() => resolveInitialTab(resource, availableTabs)); useEffect(() => { - setCurrentTab(resolveInitialTab(resource)); - }, [resource]); + setCurrentTab(resolveInitialTab(resource, availableTabs)); + }, [availableTabs, resource]); return (
setCurrentTab(value as SubTab)} className="flex flex-col h-full"> - {SUB_TABS.map(tab => ( + {availableTabs.map(tab => ( {t(`Tabs.${tab}`)} ))}
- - - - - - - - - - - - - - - - - - - - + {availableTabs.includes('summary') ? ( + + + + + + ) : null} + {availableTabs.includes('tables') ? ( + + + + + + ) : null} + {availableTabs.includes('views') ? ( + + + + + + ) : null} + {availableTabs.includes('materialized-views') ? ( + + + + + + ) : null}
diff --git a/apps/web/lib/explorer/capabilities.ts b/apps/web/lib/explorer/capabilities.ts index 9a1d9dca..9c5d6ee2 100644 --- a/apps/web/lib/explorer/capabilities.ts +++ b/apps/web/lib/explorer/capabilities.ts @@ -97,6 +97,12 @@ export function getDriverCapabilities(driver?: string | null): DriverCapabilitie return EXPLORER_CAPABILITIES[resolveExplorerDriver(driver)]; } +export function supportsDatabaseSummary(driver?: string | null): boolean { + const resolvedDriver = resolveExplorerDriver(driver); + + return resolvedDriver === 'postgres' || resolvedDriver === 'mysql' || resolvedDriver === 'mariadb' || resolvedDriver === 'clickhouse'; +} + export function driverSupportsSchema(driver?: string | null): boolean { return getDriverCapabilities(driver).supportsSchema; } From f68f3e4cc3addb43586865e1df83697a1236f932 Mon Sep 17 00:00:00 2001 From: Jeffrey Date: Fri, 27 Mar 2026 16:48:14 +0800 Subject: [PATCH 3/6] fix: fix the NODE_ABI error --- apps/electron/package.json | 12 ++++++------ scripts/build-app-standalone.sh | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/apps/electron/package.json b/apps/electron/package.json index 06b5b8a4..2d7d7d85 100644 --- a/apps/electron/package.json +++ b/apps/electron/package.json @@ -10,13 +10,13 @@ "scripts": { "compile": "tsc -p tsconfig.json && node ./scripts/copy-assets.cjs", "dev": "yarn run compile && electron ./dist-electron/main.js", - "dev:app": "yarn run compile && electron-builder --config electron-builder.mjs --mac --dir && open \"dist/mac-arm64/Dory.app\"", + "dev:app": "yarn run electron:standalone && yarn run compile && electron-builder --config electron-builder.mjs --mac --dir && open \"dist/mac-arm64/Dory.app\"", "electron:standalone": "bash ../../scripts/build-app-standalone.sh", - "build": "yarn --cwd ../web run build && yarn run compile && electron-builder --config electron-builder.mjs", - "electron:build:apple": "yarn run compile && electron-builder --config electron-builder.mjs --mac --publish never", - "electron:build:windows": "yarn run compile && electron-builder --config electron-builder.mjs --win --publish never", - "electron:build:apple:quick": "yarn run compile && SKIP_NOTARIZE=1 electron-builder --config electron-builder.mjs --mac zip", - "electron:build:apple:unsigned": "yarn run compile && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --config electron-builder.mjs --mac --dir" + "build": "yarn run electron:standalone && yarn run compile && electron-builder --config electron-builder.mjs", + "electron:build:apple": "yarn run electron:standalone && yarn run compile && electron-builder --config electron-builder.mjs --mac --publish never", + "electron:build:windows": "yarn run electron:standalone && yarn run compile && electron-builder --config electron-builder.mjs --win --publish never", + "electron:build:apple:quick": "yarn run electron:standalone && yarn run compile && SKIP_NOTARIZE=1 electron-builder --config electron-builder.mjs --mac zip", + "electron:build:apple:unsigned": "yarn run electron:standalone && yarn run compile && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --config electron-builder.mjs --mac --dir" }, "devDependencies": { "@types/node": "^24.3.1", diff --git a/scripts/build-app-standalone.sh b/scripts/build-app-standalone.sh index c65f1193..a0f0b9d6 100755 --- a/scripts/build-app-standalone.sh +++ b/scripts/build-app-standalone.sh @@ -4,6 +4,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" WEB_DIR="${ROOT_DIR}/apps/web" +ELECTRON_DIR="${ROOT_DIR}/apps/electron" cd "${ROOT_DIR}" @@ -70,6 +71,19 @@ if [[ -d "${WEB_DIR}/dist-scripts" ]]; then cp -a "${WEB_DIR}/dist-scripts" "${OUT_WEB_DIR}/dist-scripts" fi +BETTER_SQLITE3_DIR="${OUT_DIR}/node_modules/better-sqlite3" +if [[ -d "${BETTER_SQLITE3_DIR}" ]]; then + ELECTRON_VERSION="$(node -e "const fs=require('node:fs'); const pkg=JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); const version=pkg.devDependencies?.electron || pkg.dependencies?.electron || ''; process.stdout.write(String(version).replace(/^[^0-9]*/, ''));" "${ELECTRON_DIR}/package.json")" + + if [[ -n "${ELECTRON_VERSION}" ]]; then + echo "Rebuilding better-sqlite3 for Electron ${ELECTRON_VERSION}..." + ( + cd "${OUT_DIR}" + npm rebuild better-sqlite3 --build-from-source --runtime=electron --target="${ELECTRON_VERSION}" --dist-url=https://electronjs.org/headers + ) + fi +fi + echo "Output ready: ${OUT_DIR}" echo "Included top-level entries:" ls -1A "${OUT_DIR}" From 5d97062b737a39338805c69f74a76f407584f2bf Mon Sep 17 00:00:00 2001 From: dory-finn Date: Fri, 27 Mar 2026 21:46:17 +0800 Subject: [PATCH 4/6] fix: fix the node abi rebuild error --- .nvmrc | 1 + Dockerfile | 4 ++-- docs/contributing.md | 3 ++- package.json | 3 +++ scripts/build-app-standalone.sh | 25 +++++++++++++++++++++++-- 5 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..a45fd52c --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/Dockerfile b/Dockerfile index 0b6a3188..967d3387 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:lts AS installer +FROM node:24-bookworm AS installer WORKDIR /app # Install bun and enable corepack for yarn @@ -29,7 +29,7 @@ RUN yarn run build \ && cp -rn node_modules/@electric-sql/pglite/dist/. apps/web/dist-scripts/ \ && rm -f apps/web/.next/standalone/.env apps/web/.next/standalone/.env.local -FROM node:lts-slim AS runner +FROM node:24-bookworm-slim AS runner ENV NODE_ENV=production \ HOSTNAME=0.0.0.0 \ diff --git a/docs/contributing.md b/docs/contributing.md index 2bdb29e0..6a23e825 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -6,7 +6,8 @@ This repository is a Yarn workspace monorepo. Most product work happens in `apps ## Before You Start -- Use Node and Yarn versions compatible with the repository's `packageManager` setting. +- Use Node 24.x and the Yarn version compatible with the repository's `packageManager` setting. +- `better-sqlite3` is installed from prebuilt binaries for Node 24 in this repo. If you switch Node majors, reinstall dependencies under Node 24 instead of trying to rebuild the module manually. - Install dependencies from the repository root: ```bash diff --git a/package.json b/package.json index 7b5fdf9e..41c38266 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "type": "git", "url": "https://github.com/dorylab/dory.git" }, + "engines": { + "node": "24.x" + }, "packageManager": "yarn@4.13.0+sha512.5c20ba010c99815433e5c8453112165e673f1c7948d8d2b267f4b5e52097538658388ebc9f9580656d9b75c5cc996f990f611f99304a2197d4c56d21eea370e7", "scripts": { "dev": "yarn workspace web run dev", diff --git a/scripts/build-app-standalone.sh b/scripts/build-app-standalone.sh index a0f0b9d6..a6528c89 100755 --- a/scripts/build-app-standalone.sh +++ b/scripts/build-app-standalone.sh @@ -73,13 +73,34 @@ fi BETTER_SQLITE3_DIR="${OUT_DIR}/node_modules/better-sqlite3" if [[ -d "${BETTER_SQLITE3_DIR}" ]]; then + ROOT_BETTER_SQLITE3_DIR="${ROOT_DIR}/node_modules/better-sqlite3" ELECTRON_VERSION="$(node -e "const fs=require('node:fs'); const pkg=JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); const version=pkg.devDependencies?.electron || pkg.dependencies?.electron || ''; process.stdout.write(String(version).replace(/^[^0-9]*/, ''));" "${ELECTRON_DIR}/package.json")" + TARGET_ARCH="${DORY_BUILD_ARCH:-}" + + # Next standalone keeps only runtime files for externals, but rebuilding the native + # addon for Electron requires the package sources and binding.gyp from the full install. + if [[ -d "${ROOT_BETTER_SQLITE3_DIR}" ]] && [[ ! -f "${BETTER_SQLITE3_DIR}/binding.gyp" ]]; then + echo "Overlaying full better-sqlite3 package into standalone output..." + rm -rf "${BETTER_SQLITE3_DIR}" + cp -a "${ROOT_BETTER_SQLITE3_DIR}" "${BETTER_SQLITE3_DIR}" + fi if [[ -n "${ELECTRON_VERSION}" ]]; then - echo "Rebuilding better-sqlite3 for Electron ${ELECTRON_VERSION}..." + REBUILD_ARGS=( + better-sqlite3 + --build-from-source + --runtime=electron + --target="${ELECTRON_VERSION}" + --dist-url=https://electronjs.org/headers + ) + if [[ -n "${TARGET_ARCH}" ]]; then + REBUILD_ARGS+=(--arch="${TARGET_ARCH}") + fi + + echo "Rebuilding better-sqlite3 for Electron ${ELECTRON_VERSION}${TARGET_ARCH:+ (${TARGET_ARCH})}..." ( cd "${OUT_DIR}" - npm rebuild better-sqlite3 --build-from-source --runtime=electron --target="${ELECTRON_VERSION}" --dist-url=https://electronjs.org/headers + npm rebuild "${REBUILD_ARGS[@]}" ) fi fi From 65e58957752f46b5c102e0b10be1ec5d12d0504e Mon Sep 17 00:00:00 2001 From: dory-finn Date: Fri, 27 Mar 2026 22:12:04 +0800 Subject: [PATCH 5/6] fix: download prebuilt package --- scripts/build-app-standalone.sh | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/scripts/build-app-standalone.sh b/scripts/build-app-standalone.sh index a6528c89..62b55634 100755 --- a/scripts/build-app-standalone.sh +++ b/scripts/build-app-standalone.sh @@ -76,6 +76,7 @@ if [[ -d "${BETTER_SQLITE3_DIR}" ]]; then ROOT_BETTER_SQLITE3_DIR="${ROOT_DIR}/node_modules/better-sqlite3" ELECTRON_VERSION="$(node -e "const fs=require('node:fs'); const pkg=JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); const version=pkg.devDependencies?.electron || pkg.dependencies?.electron || ''; process.stdout.write(String(version).replace(/^[^0-9]*/, ''));" "${ELECTRON_DIR}/package.json")" TARGET_ARCH="${DORY_BUILD_ARCH:-}" + PREBUILD_INSTALL_BIN="${ROOT_DIR}/node_modules/.bin/prebuild-install" # Next standalone keeps only runtime files for externals, but rebuilding the native # addon for Electron requires the package sources and binding.gyp from the full install. @@ -86,6 +87,12 @@ if [[ -d "${BETTER_SQLITE3_DIR}" ]]; then fi if [[ -n "${ELECTRON_VERSION}" ]]; then + PREBUILD_ARGS=( + --runtime=electron + --target="${ELECTRON_VERSION}" + --dist-url=https://electronjs.org/headers + --verbose + ) REBUILD_ARGS=( better-sqlite3 --build-from-source @@ -94,11 +101,23 @@ if [[ -d "${BETTER_SQLITE3_DIR}" ]]; then --dist-url=https://electronjs.org/headers ) if [[ -n "${TARGET_ARCH}" ]]; then + PREBUILD_ARGS+=(--arch="${TARGET_ARCH}") REBUILD_ARGS+=(--arch="${TARGET_ARCH}") fi - echo "Rebuilding better-sqlite3 for Electron ${ELECTRON_VERSION}${TARGET_ARCH:+ (${TARGET_ARCH})}..." + echo "Resolving better-sqlite3 for Electron ${ELECTRON_VERSION}${TARGET_ARCH:+ (${TARGET_ARCH})}..." ( + cd "${BETTER_SQLITE3_DIR}" + + if [[ -x "${PREBUILD_INSTALL_BIN}" ]]; then + echo "Trying prebuilt better-sqlite3 binary..." + if "${PREBUILD_INSTALL_BIN}" "${PREBUILD_ARGS[@]}"; then + echo "Using prebuilt better-sqlite3 binary." + exit 0 + fi + fi + + echo "No prebuilt better-sqlite3 binary available, falling back to rebuild..." cd "${OUT_DIR}" npm rebuild "${REBUILD_ARGS[@]}" ) From 3fdc0cc41b28f2a8099e03151b229c4dc15154bb Mon Sep 17 00:00:00 2001 From: dory-finn Date: Fri, 27 Mar 2026 23:25:27 +0800 Subject: [PATCH 6/6] fix: fix the old version error --- .github/workflows/beta.yml | 2 ++ .github/workflows/macos-package.yml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index a560ece0..11371d27 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -147,6 +147,8 @@ jobs: TARGET_SHA: ${{ steps.beta_meta.outputs.commit_sha }} run: | if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + git tag -f "$RELEASE_TAG" "$TARGET_SHA" + git push --force origin "refs/tags/$RELEASE_TAG" gh release edit "$RELEASE_TAG" \ --title "$RELEASE_TAG" \ --notes-file "$NOTES_FILE" \ diff --git a/.github/workflows/macos-package.yml b/.github/workflows/macos-package.yml index 35443a70..90c3da81 100644 --- a/.github/workflows/macos-package.yml +++ b/.github/workflows/macos-package.yml @@ -103,7 +103,7 @@ jobs: - name: Checkout uses: actions/checkout@v6 with: - ref: ${{ needs.resolve_context.outputs.tag != '' && needs.resolve_context.outputs.tag || github.ref }} + ref: ${{ github.event_name == 'release' && needs.resolve_context.outputs.tag || github.ref }} fetch-depth: 0 - name: Setup Node