Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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'],
Expand All @@ -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) || {};
Expand All @@ -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({
Expand All @@ -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 =
Expand Down Expand Up @@ -258,7 +256,6 @@ const registerDtSqlCompletion = (
}
}


if (isPostgres) {
const normalizedSchemaPrefix = (qualifierPrefixRaw || typedTablePrefix || currentWord).toLowerCase();

Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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}`);

Expand Down
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -28,6 +29,12 @@ const SIDEBAR_CONFIG_BY_DIALECT: Record<ConnectionType, SidebarConfig> = {
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,
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -28,9 +29,7 @@ function ClickhouseTableStatsView({ databaseName, tableName }: Omit<TableStatsPr

const stats = statsQuery.data ?? null;
const loading = statsQuery.isLoading;
const error =
(!connectionId && databaseName && tableName ? t('No available connection') : null) ||
(statsQuery.error ? (statsQuery.error as Error).message : null);
const error = (!connectionId && databaseName && tableName ? t('No available connection') : null) || (statsQuery.error ? (statsQuery.error as Error).message : null);

if (!databaseName || !tableName) {
return <div className="h-full flex items-center justify-center text-sm text-muted-foreground">{t('Select table to view stats')}</div>;
Expand All @@ -45,12 +44,7 @@ function ClickhouseTableStatsView({ databaseName, tableName }: Omit<TableStatsPr
<AlertDescription>{error}</AlertDescription>
</Alert>
) : null}
<TableHealthReportCard
tableStats={stats}
databaseName={databaseName}
tableName={tableName}
connectionId={connectionId}
/>
<TableHealthReportCard tableStats={stats} databaseName={databaseName} tableName={tableName} connectionId={connectionId} />
<SizeAndRowsCard stats={stats} loading={loading} />
<PartitionsCard stats={stats} loading={loading} />
<StorageHealthCard stats={stats} loading={loading} />
Expand All @@ -61,9 +55,8 @@ function ClickhouseTableStatsView({ databaseName, tableName }: Omit<TableStatsPr
}

export default function TableStatsView({ databaseName, tableName, driver }: TableStatsProps) {
if (driver === 'postgres') {
if (isPostgresFamilyConnectionType(driver)) {
return <PostgresTableStatsView databaseName={databaseName} tableName={tableName} />;
}
return <ClickhouseTableStatsView databaseName={databaseName} tableName={tableName} />;
}

Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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;
}

Expand Down Expand Up @@ -60,7 +60,7 @@ export function DriverTableBrowser({
onSubTabChange?.(next);
};

if (driver !== 'postgres') {
if (!isPostgresFamilyConnectionType(driver)) {
return (
<div className="p-6 h-full flex flex-col">
<TableViewTabs
Expand Down
1 change: 0 additions & 1 deletion apps/web/app/(app)/[organization]/connections/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ function translateConnectionsApi(key: string) {


export async function addConnection(params: CreateConnectionPayload): Promise<ResponseObject<ConnectionListItem>> {
console.log('addConnection params:', params);
const res = await fetchJsonResponse<ConnectionListItem>(
'/api/connection',
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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);

Expand All @@ -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;
}
Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand All @@ -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 },
Expand All @@ -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)();
};
Expand Down Expand Up @@ -228,7 +243,6 @@ export function ConnectionDialog({
<form className="flex flex-col flex-1" onSubmit={handleSubmit(onSaveSubmit)}>
<ScrollArea className="overflow-hidden pr-2 h-[70vh]">
<div className="space-y-4 pb-4">

<section className="rounded-xl border border-border/70 bg-background/80 p-4 space-y-4">
<div className="flex items-center gap-2">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-muted">
Expand All @@ -242,12 +256,12 @@ export function ConnectionDialog({
<div className="space-y-4">
<ConnectionForm form={form} />

{!isSqlite ? <p className="text-xs text-muted-foreground mt-3">{t('Authentication Info')}</p> : null}
{!isSqlite ? <IdentityForm form={form} /> : null}
{!hidesIdentityForm ? <p className="text-xs text-muted-foreground mt-3">{t('Authentication Info')}</p> : null}
{!hidesIdentityForm ? <IdentityForm form={form} /> : null}
</div>
</section>

{!isSqlite ? (
{!hidesSshForm ? (
<section className="mt-2 rounded-xl border border-border/70 bg-background/80">
<Collapsible open={sshOpen} onOpenChange={setSshOpen}>
<div className="flex items-center justify-between px-4 py-3">
Expand Down Expand Up @@ -297,7 +311,6 @@ export function ConnectionDialog({
</div>
</ScrollArea>


<DialogFooter className="shrink-0 pt-4 mt-2 bg-background flex lg:justify-between">
<div>
<Button type="button" onClick={handleTestConnection} disabled={submitting || testing} data-testid="test-connection">
Expand Down
Loading
Loading