diff --git a/apps/web/.env.example b/apps/web/.env.example index 40bbd45f..423421db 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -68,6 +68,7 @@ PGLITE_DB_PATH=/data/dory # ------------------------------ # External Postgres (optional) # Uncomment if using Postgres instead of PGLite +# Note: DB_TYPE=neon is not supported for app storage; Neon is only supported as an external connection type. # ------------------------------ # DB_TYPE=postgres # DATABASE_URL="postgresql://user:password@host:5432/dbname" diff --git a/apps/web/app/(app)/[organization]/[connectionId]/sql-console/components/sql-editor/use-sql-monaco-editor.ts b/apps/web/app/(app)/[organization]/[connectionId]/sql-console/components/sql-editor/use-sql-monaco-editor.ts index 1336a9f7..3a8e0451 100644 --- a/apps/web/app/(app)/[organization]/[connectionId]/sql-console/components/sql-editor/use-sql-monaco-editor.ts +++ b/apps/web/app/(app)/[organization]/[connectionId]/sql-console/components/sql-editor/use-sql-monaco-editor.ts @@ -18,6 +18,7 @@ import { editorSelectionByTabAtom } from '../../sql-console.store'; import { useTranslations } from 'next-intl'; import type { ConnectionType } from '@/types/connections'; import { getSqlDialectConfigForConnectionType, getSqlDialectParser, type SqlDialectParser } from '@/lib/sql/sql-dialect'; +import { isPostgresFamilyConnectionType } from '@/lib/connection/postgres-family'; const MAX_SQL_LEN_FOR_PARSE = 20000; @@ -140,7 +141,7 @@ const registerDtSqlCompletion = ( getSchemas: () => any[], getActiveDatabase?: () => string, ) => { - const isPostgres = currentConnectionType === 'postgres'; + const isPostgres = isPostgresFamilyConnectionType(currentConnectionType); return monaco.languages.registerCompletionItemProvider(languageId, { triggerCharacters: [' ', '.', ',', '(', '=', '\n'], @@ -166,7 +167,6 @@ const registerDtSqlCompletion = ( const offset = model.getOffsetAt(position); const prefixText = sql.slice(0, offset); - let suggestion: any = {}; try { suggestion = parser.getSuggestionAtCaretPosition?.(sql, caretPos) || {}; @@ -186,7 +186,6 @@ const registerDtSqlCompletion = ( const syntaxList = Array.isArray(syntax) ? syntax : []; const columnPrefix = buildColumnPrefix(syntaxList, currentWord); - if (Array.isArray(keywords)) { for (const kw of keywords) { items.push({ @@ -205,7 +204,6 @@ const registerDtSqlCompletion = ( const hasTableContext = syntaxList.some(s => s.syntaxContextType === 'table'); const hasDatabaseContext = syntaxList.some(s => s.syntaxContextType === 'database' || s.syntaxContextType === 'databaseCreate'); - if (hasTableContext) { const tableSyntax = syntaxList.find(s => s.syntaxContextType === 'table'); const typedTablePrefix = @@ -258,7 +256,6 @@ const registerDtSqlCompletion = ( } } - if (isPostgres) { const normalizedSchemaPrefix = (qualifierPrefixRaw || typedTablePrefix || currentWord).toLowerCase(); @@ -338,34 +335,29 @@ const registerDtSqlCompletion = ( } } - if (hasColumnContext) { - const rawPrefix = columnPrefix.trim(); + const rawPrefix = columnPrefix.trim(); let targetTables: string[] = []; let filterPrefix = rawPrefix.toLowerCase(); - const aliasMatch = rawPrefix.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\.(.*)$/); if (aliasMatch) { const aliasPart = aliasMatch[1]; // c - const afterDotPart = aliasMatch[2]; + const afterDotPart = aliasMatch[2]; const tableFromAlias = resolveTableFromAliasInSql(sql, aliasPart); if (tableFromAlias) { targetTables = [tableFromAlias]; - filterPrefix = (afterDotPart || '').toLowerCase(); } } - if (!targetTables.length) { const caretOffset = model.getOffsetAt(position); targetTables = resolveTablesForColumnContext(parser, sql, caretPos, tables, caretOffset); if (!targetTables.length) { - targetTables = tables.map(t => normalizeTableName(resolveTableName(t))).filter(Boolean); } } @@ -428,7 +420,7 @@ export function useSqlMonacoEditor({ const setSelectionByTab = useSetAtom(editorSelectionByTabAtom); const { databases } = useDatabases(); const { tables } = useTables(activeDatabase); - const { schemas } = useSchemas(activeDatabase, currentConnectionType === 'postgres'); + const { schemas } = useSchemas(activeDatabase, isPostgresFamilyConnectionType(currentConnectionType)); const { refresh: refreshColumns } = useColumns(); const refreshColumnsRef = useRef(refreshColumns); const t = useTranslations('SqlConsole'); @@ -497,7 +489,6 @@ export function useSqlMonacoEditor({ const dialectConfig = getSqlDialectConfigForConnectionType(currentConnectionType); const languageId = dialectConfig.monacoLanguageId; - const parser = await getSqlDialectParser(dialectConfig.dialect); console.log(`[useSqlMonacoEditor] Loaded parser for dialect=${dialectConfig.dialect}`); 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 ac4822f3..c660bc51 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 @@ -1,5 +1,6 @@ import type { ConnectionType } from '@/types/connections'; import type { SidebarConfig } from './types'; +import { isPostgresFamilyConnectionType } from '@/lib/connection/postgres-family'; const DEFAULT_CONFIG: SidebarConfig = { dialect: 'default', @@ -28,6 +29,12 @@ const SIDEBAR_CONFIG_BY_DIALECT: Record = { supportsSchemas: false, hiddenDatabases: ['information_schema', 'mysql', 'performance_schema', 'sys'], }, + neon: { + dialect: 'postgres', + supportsSchemas: true, + defaultSchemaName: 'public', + hiddenDatabases: ['system', 'information_schema'], + }, postgres: { dialect: 'postgres', supportsSchemas: true, @@ -46,5 +53,9 @@ export function getSidebarConfig(connectionType?: ConnectionType | null): Sideba return DEFAULT_CONFIG; } + if (isPostgresFamilyConnectionType(connectionType)) { + return SIDEBAR_CONFIG_BY_DIALECT.postgres; + } + return SIDEBAR_CONFIG_BY_DIALECT[connectionType] ?? DEFAULT_CONFIG; } diff --git a/apps/web/app/(app)/[organization]/components/table-browser/components/stats/index.tsx b/apps/web/app/(app)/[organization]/components/table-browser/components/stats/index.tsx index 7d859785..7e011265 100644 --- a/apps/web/app/(app)/[organization]/components/table-browser/components/stats/index.tsx +++ b/apps/web/app/(app)/[organization]/components/table-browser/components/stats/index.tsx @@ -11,6 +11,7 @@ import { TableHealthReportCard } from './components/ai-insight'; import PostgresTableStatsView from './postgres-stats'; import { useTableStatsQuery } from '../table-queries'; import { useTranslations } from 'next-intl'; +import { isPostgresFamilyConnectionType } from '@/lib/connection/postgres-family'; // import TTLCard from './components/ttl-card'; type TableStatsProps = { @@ -28,9 +29,7 @@ function ClickhouseTableStatsView({ databaseName, tableName }: Omit{t('Select table to view stats')}; @@ -45,12 +44,7 @@ function ClickhouseTableStatsView({ databaseName, tableName }: Omit{error} ) : null} - + @@ -61,9 +55,8 @@ function ClickhouseTableStatsView({ databaseName, tableName }: Omit; } return ; } - diff --git a/apps/web/app/(app)/[organization]/components/table-browser/driver-table-browser.tsx b/apps/web/app/(app)/[organization]/components/table-browser/driver-table-browser.tsx index d01d884c..014d03ed 100644 --- a/apps/web/app/(app)/[organization]/components/table-browser/driver-table-browser.tsx +++ b/apps/web/app/(app)/[organization]/components/table-browser/driver-table-browser.tsx @@ -3,7 +3,6 @@ import { useEffect, useMemo, useState } from 'react'; import { useTranslations } from 'next-intl'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/registry/new-york-v4/ui/tabs'; -import type { ExplorerDriver } from '@/lib/explorer/types'; import type { SQLTab } from '@/types/tabs'; import { TableOverview } from './components/overview'; import TableStats from './components/stats'; @@ -12,9 +11,10 @@ import TableDataPreview from './components/data-preview'; import { TableIndexesTab } from './components/indexes'; import { TableViewTabs } from './table-view-tabs'; import type { TableSubTab } from './types'; +import { isPostgresFamilyConnectionType } from '@/lib/connection/postgres-family'; type DriverTableBrowserProps = { - driver?: ExplorerDriver; + driver?: string; activeTab?: SQLTab; connectionId?: string; databaseName?: string; @@ -27,8 +27,8 @@ type DriverTableBrowserProps = { const DEFAULT_TAB: TableSubTab = 'data'; const POSTGRES_SUB_TABS: TableSubTab[] = ['overview', 'data', 'structure', 'stats', 'indexes']; -function normalizeTab(driver: ExplorerDriver | undefined, tab?: TableSubTab): TableSubTab { - if (driver !== 'postgres' && tab === 'indexes') { +function normalizeTab(driver: string | undefined, tab?: TableSubTab): TableSubTab { + if (!isPostgresFamilyConnectionType(driver) && tab === 'indexes') { return DEFAULT_TAB; } @@ -60,7 +60,7 @@ export function DriverTableBrowser({ onSubTabChange?.(next); }; - if (driver !== 'postgres') { + if (!isPostgresFamilyConnectionType(driver)) { return (
> { - console.log('addConnection params:', params); const res = await fetchJsonResponse( '/api/connection', { 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 285189f8..66d35207 100644 --- a/apps/web/app/(app)/[organization]/connections/components/connection-dialog.tsx +++ b/apps/web/app/(app)/[organization]/connections/components/connection-dialog.tsx @@ -27,6 +27,7 @@ import { ConnectionDialogFormSchema } from '../form-schema'; import { useAtomValue } from 'jotai'; import { currentConnectionAtom } from '@/shared/stores/app.store'; import { getConnectionDriver } from './forms/connection/drivers'; +import { buildNeonConnectionStringForForm, normalizeNeonIdentityFromConnectionString } from './forms/connection/drivers/neon'; type Mode = 'Create' | 'Edit'; @@ -62,6 +63,9 @@ export function ConnectionDialog({ const { control, handleSubmit, reset } = form; const connectionType = useWatch({ control, name: 'connection.type' }); const isSqlite = connectionType === 'sqlite'; + const isNeon = connectionType === 'neon'; + const hidesIdentityForm = isSqlite || isNeon; + const hidesSshForm = isSqlite || isNeon; const isEditMode = mode === 'Edit' && Boolean(connectionItem?.connection?.id); @@ -82,6 +86,16 @@ export function ConnectionDialog({ }; const normalizeIdentityValues = (identityValues: any) => { + if (isNeon) { + const fallbackIdentity = connectionItem?.identities?.find((iden: any) => iden.isDefault); + + return normalizeNeonIdentityFromConnectionString(form.getValues('connection.host'), { + id: fallbackIdentity?.id, + username: fallbackIdentity?.username, + database: fallbackIdentity?.database, + }); + } + if (!isSqlite) { return identityValues; } @@ -112,21 +126,25 @@ export function ConnectionDialog({ ...formIdentity, }, } as any; + if (connectionItem.connection?.type === 'neon') { + nextValues.connection.host = buildNeonConnectionStringForForm(connectionItem.connection, formIdentity); + } reset(nextValues); - setSshOpen(Boolean((connectionItem as any).ssh?.enabled)); + setSshOpen(connectionItem.connection?.type === 'neon' ? false : Boolean((connectionItem as any).ssh?.enabled)); } else { reset(NEW_CONNECTION_DEFAULT_VALUES as any); setSshOpen(Boolean((NEW_CONNECTION_DEFAULT_VALUES as any).ssh?.enabled)); } }, [open, isEditMode, connectionItem, reset]); - const onSaveSubmit = async (values: any) => { setSubmitting(true); try { const connectionId = connectionItem?.connection?.id; const defaultIdentity = connectionItem?.identities?.find((iden: any) => iden.isDefault); - const sshPayload = isSqlite ? null : normalizeSshValues(values.ssh, isEditMode ? connectionId : null); + const sshPayload = hidesSshForm + ? { enabled: false, host: null, port: null, username: null, authMethod: null } + : normalizeSshValues(values.ssh, isEditMode ? connectionId : null); const driver = getConnectionDriver(values.connection?.type); const normalizedConnection = driver.normalizeForSubmit(values.connection); const normalizedIdentity = normalizeIdentityValues(values.identity); @@ -137,16 +155,16 @@ export function ConnectionDialog({ identities: [ isEditMode ? { - ...normalizedIdentity, - id: normalizedIdentity?.id ?? defaultIdentity?.id, - } + ...normalizedIdentity, + id: normalizedIdentity?.id ?? defaultIdentity?.id, + } : normalizedIdentity, ], }; console.log('onSaveSubmit values:', values, 'savedValues:', savedValues); if (isEditMode && connectionItem?.connection?.id) { console.log('isEditMode true, updating connection'); - + const updateValues: any = { ...savedValues, id: connectionItem.connection.id, @@ -165,15 +183,14 @@ export function ConnectionDialog({ } }; - const onValidTest = async (values: any) => { - const sshPayload = isSqlite ? null : normalizeSshValues(values.ssh); + const sshPayload = hidesSshForm ? { enabled: false, host: null, port: null, username: null, authMethod: 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; + const mergedSsh = sshPayload ? { ...currentConnection?.ssh, ...sshPayload } : (currentConnection?.ssh ?? null); testPayload = { connection: { ...currentConnection?.connection, ...normalizedConnection }, identity: { ...currentConnection?.identities?.find((iden: any) => iden.isDefault), ...normalizedIdentity }, @@ -192,13 +209,11 @@ export function ConnectionDialog({ } }; - const onInvalidTest = (errors: any) => { console.log('test connection validation errors:', errors); toast.error(t('Fix Form Errors Before Testing')); }; - const handleTestConnection = () => { handleSubmit(onValidTest, onInvalidTest)(); }; @@ -228,7 +243,6 @@ export function ConnectionDialog({
-
@@ -242,12 +256,12 @@ export function ConnectionDialog({
- {!isSqlite ?

{t('Authentication Info')}

: null} - {!isSqlite ? : null} + {!hidesIdentityForm ?

{t('Authentication Info')}

: null} + {!hidesIdentityForm ? : null}
- {!isSqlite ? ( + {!hidesSshForm ? (
@@ -297,7 +311,6 @@ export function ConnectionDialog({
-