diff --git a/frontend/src/components/ai/aiExternalSqlFileInsights.test.ts b/frontend/src/components/ai/aiExternalSqlFileInsights.test.ts
index 553a39b9..369e187b 100644
--- a/frontend/src/components/ai/aiExternalSqlFileInsights.test.ts
+++ b/frontend/src/components/ai/aiExternalSqlFileInsights.test.ts
@@ -14,6 +14,16 @@ const connections: SavedConnection[] = [
user: 'root',
},
},
+ {
+ id: 'conn-2',
+ name: '报表库',
+ config: {
+ type: 'sqlserver',
+ host: '192.168.1.10',
+ port: 1433,
+ user: 'reporter',
+ },
+ },
];
describe('aiExternalSqlFileInsights', () => {
@@ -25,6 +35,11 @@ describe('aiExternalSqlFileInsights', () => {
path: 'D:/sql/reports',
connectionId: 'conn-1',
dbName: 'crm',
+ fileBindings: [{
+ filePath: 'D:/sql/reports/daily.sql',
+ connectionId: 'conn-2',
+ dbName: 'reporting',
+ }],
createdAt: 1,
},
];
@@ -56,9 +71,10 @@ describe('aiExternalSqlFileInsights', () => {
expect(snapshot.hasMatchedDirectory).toBe(true);
expect(snapshot.directory).toMatchObject({
name: '报表脚本',
- connectionName: '本地开发库',
- connectionType: 'mysql',
- dbName: 'crm',
+ connectionName: '报表库',
+ connectionType: 'sqlserver',
+ dbName: 'reporting',
+ bindingSource: 'file',
});
expect(snapshot.hasOpenTab).toBe(true);
expect(snapshot.openTabCount).toBe(1);
@@ -66,4 +82,32 @@ describe('aiExternalSqlFileInsights', () => {
expect(snapshot.contentPreview).toBe('SELECT * FRO');
expect(snapshot.truncated).toBe(true);
});
+
+ it('restores the original directory metadata after a file-specific binding is cleared', () => {
+ const snapshot = buildExternalSQLFileSnapshot({
+ filePath: 'D:/sql/reports/daily.sql',
+ readResult: {
+ content: 'SELECT 1;',
+ filePath: 'D:/sql/reports/daily.sql',
+ name: 'daily.sql',
+ },
+ externalSQLDirectories: [{
+ id: 'dir-1',
+ name: '报表脚本',
+ path: 'D:/sql/reports',
+ connectionId: 'conn-1',
+ dbName: 'crm',
+ createdAt: 1,
+ }],
+ connections,
+ });
+
+ expect(snapshot.directory).toMatchObject({
+ connectionId: 'conn-1',
+ connectionName: '本地开发库',
+ connectionType: 'mysql',
+ dbName: 'crm',
+ });
+ expect(snapshot.directory).not.toHaveProperty('bindingSource');
+ });
});
diff --git a/frontend/src/components/ai/aiExternalSqlFileInsights.ts b/frontend/src/components/ai/aiExternalSqlFileInsights.ts
index 23702836..fe90da9e 100644
--- a/frontend/src/components/ai/aiExternalSqlFileInsights.ts
+++ b/frontend/src/components/ai/aiExternalSqlFileInsights.ts
@@ -1,4 +1,5 @@
import type { ExternalSQLDirectory, SavedConnection, TabData } from '../../types';
+import { resolveExternalSQLFileBinding } from '../../utils/externalSqlTree';
import {
findBestMatchingExternalSQLDirectory,
normalizeExternalSQLPath,
@@ -73,7 +74,15 @@ export const buildExternalSQLFileSnapshot = (params: {
const payload = normalizeFileReadPayload(readResult);
const resolvedFilePath = normalizeExternalSQLPath(payload.filePath || requestedFilePath);
const matchedDirectory = findBestMatchingExternalSQLDirectory(resolvedFilePath, externalSQLDirectories);
- const matchedConnection = connections.find((item) => item.id === matchedDirectory?.connectionId);
+ const fileBinding = resolveExternalSQLFileBinding(
+ externalSQLDirectories,
+ resolvedFilePath,
+ matchedDirectory
+ ? { connectionId: matchedDirectory.connectionId, dbName: matchedDirectory.dbName }
+ : undefined,
+ );
+ const effectiveConnectionId = fileBinding?.connectionId || matchedDirectory?.connectionId || '';
+ const matchedConnection = connections.find((item) => item.id === effectiveConnectionId);
const matchingTabs = tabs.filter(
(tab) => normalizeExternalSQLPath(tab.filePath || '').toLowerCase() === resolvedFilePath.toLowerCase(),
);
@@ -91,10 +100,11 @@ export const buildExternalSQLFileSnapshot = (params: {
id: matchedDirectory.id,
name: matchedDirectory.name,
path: matchedDirectory.path,
- connectionId: matchedDirectory.connectionId || '',
+ connectionId: effectiveConnectionId,
connectionName: matchedConnection?.name || '',
connectionType: matchedConnection?.config?.type || '',
- dbName: matchedDirectory.dbName || '',
+ dbName: fileBinding?.dbName || matchedDirectory.dbName || '',
+ ...(fileBinding ? { bindingSource: 'file' } : {}),
} : null,
hasOpenTab: matchingTabs.length > 0,
openTabCount: matchingTabs.length,
diff --git a/frontend/src/components/sidebar/SidebarExternalSqlWorkflow.tsx b/frontend/src/components/sidebar/SidebarExternalSqlWorkflow.tsx
index afa793d1..351368bc 100644
--- a/frontend/src/components/sidebar/SidebarExternalSqlWorkflow.tsx
+++ b/frontend/src/components/sidebar/SidebarExternalSqlWorkflow.tsx
@@ -1,5 +1,5 @@
-import React, { useCallback, useState } from 'react';
-import { Button, Form, Input, Progress, message } from 'antd';
+import React, { useCallback, useRef, useState } from 'react';
+import { Button, Form, Input, Progress, Select, message } from 'antd';
import type { FormInstance } from 'antd/es/form';
import Modal from '../common/ResizableDraggableModal';
import type { SavedConnection, ExternalSQLDirectory } from '../../types';
@@ -7,9 +7,17 @@ import { noAutoCapInputProps } from '../../utils/inputAutoCap';
import {
buildExternalSQLDirectoryId,
buildExternalSQLTabId,
+ moveExternalSQLFileBindings,
normalizeExternalSQLPath,
+ removeExternalSQLFileBindings,
+ resolveExternalSQLFileBinding,
+ setExternalSQLFileBinding,
} from '../../utils/externalSqlTree';
import { buildSQLFileExecutionWorkbenchTab } from '../../utils/sqlFileExecutionTab';
+import { buildRpcConnectionConfig } from '../../utils/connectionRpcConfig';
+import { filterVisibleDatabaseNames } from '../../utils/databaseVisibility';
+import { getDataSourceCapabilities } from '../../utils/dataSourceCapabilities';
+import { resolveConnectionHostSummary } from '../../utils/tabDisplay';
import { t } from '../../i18n';
import { resolveSidebarNodeConnectionId } from '../sidebarV2Utils';
import {
@@ -20,6 +28,7 @@ import {
OpenSQLFile,
SelectSQLDirectory,
ReadSQLFile,
+ DBGetDatabases,
CreateSQLFile,
CreateSQLDirectory,
DeleteSQLFile,
@@ -94,6 +103,22 @@ type SQLFileExecutionModalProps = {
onClose: () => void;
};
+type ExternalSQLBindingModalProps = {
+ open: boolean;
+ form: FormInstance;
+ connections: SavedConnection[];
+ filePath: string;
+ databaseOptions: string[];
+ loadingDatabases: boolean;
+ databaseLoadError: string;
+ hasExplicitBinding: boolean;
+ saving: boolean;
+ onConnectionChange: (connectionId: string) => void;
+ onClearBinding: () => void;
+ onOk: () => void;
+ onCancel: () => void;
+};
+
const normalizeExternalSQLFileName = (rawName: unknown): string => {
const name = String(rawName || '').trim();
if (!name) return '';
@@ -265,6 +290,90 @@ export const ExternalSQLFileModal: React.FC
= ({
);
+export const ExternalSQLBindingModal: React.FC = ({
+ open,
+ form,
+ connections,
+ filePath,
+ databaseOptions,
+ loadingDatabases,
+ databaseLoadError,
+ hasExplicitBinding,
+ saving,
+ onConnectionChange,
+ onClearBinding,
+ onOk,
+ onCancel,
+}) => {
+ const connectionOptions = connections
+ .filter((connection) => getDataSourceCapabilities(connection.config).supportsQueryEditor)
+ .map((connection) => {
+ const host = resolveConnectionHostSummary(connection.config);
+ return {
+ value: connection.id,
+ label: host ? `${connection.name || connection.id} (${host})` : connection.name || connection.id,
+ };
+ });
+
+ return (
+
+
+ {filePath}
+
+
+
+
+
+
+
+ {hasExplicitBinding && (
+
+ )}
+
+ );
+};
+
export const SQLFileExecutionModal: React.FC = ({
title,
state,
@@ -324,6 +433,58 @@ export const useSidebarExternalSqlWorkflow = ({
const [externalSQLFileForm] = Form.useForm();
const [externalSQLFileModalMode, setExternalSQLFileModalMode] = useState('create');
const [externalSQLFileTarget, setExternalSQLFileTarget] = useState(null);
+ const [isExternalSQLBindingModalOpen, setIsExternalSQLBindingModalOpen] = useState(false);
+ const [externalSQLBindingForm] = Form.useForm();
+ const [externalSQLBindingTarget, setExternalSQLBindingTarget] = useState(null);
+ const [externalSQLBindingDatabases, setExternalSQLBindingDatabases] = useState([]);
+ const [externalSQLBindingDatabaseError, setExternalSQLBindingDatabaseError] = useState('');
+ const [loadingExternalSQLBindingDatabases, setLoadingExternalSQLBindingDatabases] = useState(false);
+ const [savingExternalSQLBinding, setSavingExternalSQLBinding] = useState(false);
+ const externalSQLBindingDatabaseRequestRef = useRef(0);
+
+ const loadExternalSQLBindingDatabases = useCallback(async (
+ connectionId: string,
+ preferredDbName = '',
+ ) => {
+ const requestId = ++externalSQLBindingDatabaseRequestRef.current;
+ const connection = connections.find((item) => item.id === String(connectionId || '').trim());
+ setExternalSQLBindingDatabaseError('');
+ if (!connection) {
+ setExternalSQLBindingDatabases([]);
+ setLoadingExternalSQLBindingDatabases(false);
+ return;
+ }
+
+ setLoadingExternalSQLBindingDatabases(true);
+ const fallbackNames = [preferredDbName, String(connection.config.database || '').trim()].filter(Boolean);
+ try {
+ const result = await DBGetDatabases(buildRpcConnectionConfig(connection.config) as any);
+ if (requestId !== externalSQLBindingDatabaseRequestRef.current) return;
+ if (!result.success) {
+ setExternalSQLBindingDatabases(Array.from(new Set(fallbackNames)));
+ setExternalSQLBindingDatabaseError(result.message || t('data_export.message.load_databases_failed'));
+ return;
+ }
+ const names = (Array.isArray(result.data) ? result.data : [])
+ .map((row: any) => String(row?.Database || row?.database || Object.values(row || {})[0] || '').trim())
+ .filter(Boolean);
+ const visibleNames = filterVisibleDatabaseNames(connection, names);
+ setExternalSQLBindingDatabases(
+ Array.from(new Set([...fallbackNames, ...visibleNames]))
+ .sort((left, right) => left.localeCompare(right)),
+ );
+ } catch (error) {
+ if (requestId !== externalSQLBindingDatabaseRequestRef.current) return;
+ setExternalSQLBindingDatabases(Array.from(new Set(fallbackNames)));
+ setExternalSQLBindingDatabaseError(
+ error instanceof Error ? error.message : t('data_export.message.load_databases_failed'),
+ );
+ } finally {
+ if (requestId === externalSQLBindingDatabaseRequestRef.current) {
+ setLoadingExternalSQLBindingDatabases(false);
+ }
+ }
+ }, [connections]);
const selectSQLFileForExecution = useCallback(async () => {
const backendApp = typeof window !== 'undefined' ? (window as any).go?.app?.App : undefined;
@@ -409,9 +570,19 @@ export const useSidebarExternalSqlWorkflow = ({
message.error(t('sidebar.message.sql_file_path_incomplete'));
return;
}
+ const fileBinding = resolveExternalSQLFileBinding(
+ externalSQLDirectories,
+ data.filePath,
+ {
+ connectionId: String(ctx?.connectionId || '').trim(),
+ dbName: String(ctx?.dbName || '').trim(),
+ },
+ );
+ const connectionId = fileBinding?.connectionId || ctx.connectionId;
+ const dbName = fileBinding?.dbName || String(ctx.dbName || '').trim();
openSQLFileExecutionWorkbench({
- connectionId: ctx.connectionId,
- dbName: ctx.dbName || '',
+ connectionId,
+ dbName,
filePath: data.filePath,
fileName: data.fileName,
fileSizeMB: data.fileSizeMB,
@@ -441,6 +612,125 @@ export const useSidebarExternalSqlWorkflow = ({
};
};
+ const closeExternalSQLBindingModal = () => {
+ externalSQLBindingDatabaseRequestRef.current += 1;
+ setIsExternalSQLBindingModalOpen(false);
+ setExternalSQLBindingTarget(null);
+ setExternalSQLBindingDatabases([]);
+ setExternalSQLBindingDatabaseError('');
+ setLoadingExternalSQLBindingDatabases(false);
+ externalSQLBindingForm.resetFields();
+ };
+
+ const openExternalSQLBindingModal = (fileNode: any) => {
+ const filePath = String(fileNode?.dataRef?.path || '').trim();
+ const directoryId = String(fileNode?.dataRef?.directoryId || '').trim();
+ if (!filePath || !directoryId) {
+ message.error(t('sidebar.message.sql_file_path_incomplete'));
+ return;
+ }
+ const fallbackContext = resolveExternalSQLExecutionContext();
+ const fileConnectionId = String(fileNode?.dataRef?.connectionId || '').trim();
+ const fallbackConnectionId = String(fallbackContext.connectionId || '').trim();
+ const connectionId = [fileConnectionId, fallbackConnectionId]
+ .find((candidate) => connections.some((connection) => connection.id === candidate)) || '';
+ const dbName = connectionId === fileConnectionId
+ ? String(fileNode?.dataRef?.dbName || '').trim()
+ : String(fallbackContext.dbName || '').trim();
+ setExternalSQLBindingTarget(fileNode);
+ externalSQLBindingForm.setFieldsValue({
+ connectionId: connectionId || undefined,
+ dbName: dbName || undefined,
+ });
+ setIsExternalSQLBindingModalOpen(true);
+ if (connectionId) {
+ void loadExternalSQLBindingDatabases(connectionId, dbName);
+ } else {
+ setExternalSQLBindingDatabases([]);
+ setExternalSQLBindingDatabaseError('');
+ }
+ };
+
+ const handleExternalSQLBindingConnectionChange = (connectionId: string) => {
+ externalSQLBindingForm.setFieldsValue({ dbName: undefined });
+ void loadExternalSQLBindingDatabases(connectionId);
+ };
+
+ const saveExternalSQLBindingTarget = async (
+ target: { connectionId: string; dbName: string } | null,
+ ) => {
+ const filePath = String(externalSQLBindingTarget?.dataRef?.path || '').trim();
+ const directoryId = String(externalSQLBindingTarget?.dataRef?.directoryId || '').trim();
+ const directory = externalSQLDirectories.find((item) => item.id === directoryId);
+ if (!filePath || !directory) {
+ message.error(t('sidebar.message.external_sql_directory_not_found'));
+ return false;
+ }
+ if (target) {
+ const connection = connections.find((item) => item.id === target.connectionId);
+ if (!connection || !getDataSourceCapabilities(connection.config).supportsQueryEditor) {
+ message.error(t('sidebar.message.connection_config_not_found'));
+ return false;
+ }
+ }
+ const nextDirectory = setExternalSQLFileBinding(directory, filePath, target);
+ saveExternalSQLDirectory(nextDirectory);
+ const nextDirectories = externalSQLDirectories.map((item) => (
+ item.id === directoryId ? nextDirectory : item
+ ));
+ await refreshGlobalExternalSQLRootNode(false, nextDirectories);
+ return true;
+ };
+
+ const handleExternalSQLBindingOk = async () => {
+ try {
+ const values = await externalSQLBindingForm.validateFields();
+ setSavingExternalSQLBinding(true);
+ const saved = await saveExternalSQLBindingTarget({
+ connectionId: String(values.connectionId || '').trim(),
+ dbName: String(values.dbName || '').trim(),
+ });
+ if (!saved) return;
+ message.success(t('sidebar.message.external_sql_file_binding_saved'));
+ closeExternalSQLBindingModal();
+ } catch (error) {
+ if (!(error && typeof error === 'object' && 'errorFields' in error)) {
+ message.error(error instanceof Error ? error.message : t('common.unknown'));
+ }
+ } finally {
+ setSavingExternalSQLBinding(false);
+ }
+ };
+
+ const handleClearExternalSQLBinding = async () => {
+ try {
+ setSavingExternalSQLBinding(true);
+ const saved = await saveExternalSQLBindingTarget(null);
+ if (!saved) return;
+ message.success(t('sidebar.message.external_sql_file_binding_cleared'));
+ closeExternalSQLBindingModal();
+ } catch (error) {
+ message.error(error instanceof Error ? error.message : t('common.unknown'));
+ } finally {
+ setSavingExternalSQLBinding(false);
+ }
+ };
+
+ const transformExternalSQLDirectoryBindings = (
+ transform: (directory: ExternalSQLDirectory) => ExternalSQLDirectory,
+ ): ExternalSQLDirectory[] | undefined => {
+ let changed = false;
+ const nextDirectories = externalSQLDirectories.map((directory) => {
+ const nextDirectory = transform(directory);
+ if (nextDirectory !== directory) {
+ changed = true;
+ saveExternalSQLDirectory(nextDirectory);
+ }
+ return nextDirectory;
+ });
+ return changed ? nextDirectories : undefined;
+ };
+
const openExternalSQLFile = async (fileNode: any) => {
const fileContext = {
connectionId: String(fileNode?.dataRef?.connectionId || '').trim(),
@@ -455,7 +745,6 @@ export const useSidebarExternalSqlWorkflow = ({
message.error(t('sidebar.message.sql_file_path_incomplete'));
return;
}
-
const res = await ReadSQLFile(filePath);
if (!res.success) {
if (res.message !== '已取消') {
@@ -583,10 +872,14 @@ export const useSidebarExternalSqlWorkflow = ({
}
const payload = (res.data && typeof res.data === 'object') ? res.data as Record : {};
const nextFilePath = String(payload.filePath || '').trim();
+ let nextDirectories: ExternalSQLDirectory[] | undefined;
if (nextFilePath) {
updateRecentSQLFilePath(filePath, nextFilePath);
+ nextDirectories = transformExternalSQLDirectoryBindings(
+ (directory) => moveExternalSQLFileBindings(directory, filePath, nextFilePath),
+ );
}
- await refreshGlobalExternalSQLRootNode(false);
+ await refreshGlobalExternalSQLRootNode(false, nextDirectories);
message.success(t('sidebar.message.sql_file_renamed'));
} else if (externalSQLFileModalMode === 'create-directory') {
const directoryPath = getExternalSQLParentDirectoryPath(externalSQLFileTarget);
@@ -636,8 +929,9 @@ export const useSidebarExternalSqlWorkflow = ({
matchingDirectories.forEach((directory) => {
const connectionId = String(directory.connectionId || '').trim();
const dbName = String(directory.dbName || '').trim();
+ const movedDirectory = moveExternalSQLFileBindings(directory, directoryPath, nextPath);
const nextDirectory: ExternalSQLDirectory = {
- ...directory,
+ ...movedDirectory,
id: buildExternalSQLDirectoryId(connectionId, dbName, nextPath),
name: nextName || nextPath.split(/[\\/]/).filter(Boolean).pop() || t('sidebar.sql_directory.default_name'),
path: nextPath,
@@ -656,7 +950,12 @@ export const useSidebarExternalSqlWorkflow = ({
nextDirectoriesById.forEach((directory) => saveExternalSQLDirectory(directory));
await refreshGlobalExternalSQLRootNode(false, nextDirectories);
} else {
- await refreshGlobalExternalSQLRootNode(false);
+ const nextDirectories = nextPath
+ ? transformExternalSQLDirectoryBindings(
+ (directory) => moveExternalSQLFileBindings(directory, directoryPath, nextPath),
+ )
+ : undefined;
+ await refreshGlobalExternalSQLRootNode(false, nextDirectories);
}
message.success(t('sidebar.message.sql_directory_renamed'));
}
@@ -688,7 +987,10 @@ export const useSidebarExternalSqlWorkflow = ({
return;
}
removeRecentSQLFilesByPath(filePath);
- await refreshGlobalExternalSQLRootNode(false);
+ const nextDirectories = transformExternalSQLDirectoryBindings(
+ (directory) => removeExternalSQLFileBindings(directory, filePath),
+ );
+ await refreshGlobalExternalSQLRootNode(false, nextDirectories);
message.success(t('sidebar.message.sql_file_deleted'));
},
});
@@ -731,7 +1033,10 @@ export const useSidebarExternalSqlWorkflow = ({
await refreshGlobalExternalSQLRootNode(false);
}
} else {
- await refreshGlobalExternalSQLRootNode(false);
+ const nextDirectories = transformExternalSQLDirectoryBindings(
+ (directory) => removeExternalSQLFileBindings(directory, directoryPath),
+ );
+ await refreshGlobalExternalSQLRootNode(false, nextDirectories);
}
message.success(t('sidebar.message.sql_directory_deleted'));
},
@@ -803,6 +1108,7 @@ export const useSidebarExternalSqlWorkflow = ({
handleRunSQLFile,
handleOpenSQLFileFromToolbar,
openExternalSQLFile,
+ openExternalSQLBindingModal,
openCreateExternalSQLFileModal,
openRenameExternalSQLFileModal,
openCreateExternalSQLDirectoryModal,
@@ -820,5 +1126,22 @@ export const useSidebarExternalSqlWorkflow = ({
onOk: handleExternalSQLFileModalOk,
onCancel: closeExternalSQLFileModal,
},
+ externalSQLBindingModalProps: {
+ open: isExternalSQLBindingModalOpen,
+ form: externalSQLBindingForm,
+ connections,
+ filePath: String(externalSQLBindingTarget?.dataRef?.path || '').trim(),
+ databaseOptions: externalSQLBindingDatabases,
+ loadingDatabases: loadingExternalSQLBindingDatabases,
+ databaseLoadError: externalSQLBindingDatabaseError,
+ hasExplicitBinding: externalSQLBindingTarget?.dataRef?.hasExplicitBinding === true,
+ saving: savingExternalSQLBinding,
+ onConnectionChange: handleExternalSQLBindingConnectionChange,
+ onClearBinding: () => { void handleClearExternalSQLBinding(); },
+ onOk: () => { void handleExternalSQLBindingOk(); },
+ onCancel: () => {
+ if (!savingExternalSQLBinding) closeExternalSQLBindingModal();
+ },
+ },
};
};
diff --git a/frontend/src/components/sidebar/sidebarLegacyNodeMenu.external-sql.test.tsx b/frontend/src/components/sidebar/sidebarLegacyNodeMenu.external-sql.test.tsx
new file mode 100644
index 00000000..75baa77a
--- /dev/null
+++ b/frontend/src/components/sidebar/sidebarLegacyNodeMenu.external-sql.test.tsx
@@ -0,0 +1,32 @@
+import { describe, expect, it, vi } from 'vitest';
+
+import { buildSidebarLegacyNodeMenuItems } from './sidebarLegacyNodeMenu';
+
+describe('external SQL file context menu', () => {
+ it('opens the persisted database binding workflow from the shared legacy/v2 menu', () => {
+ const openExternalSQLFile = vi.fn();
+ const openExternalSQLBindingModal = vi.fn();
+ const items = buildSidebarLegacyNodeMenuItems({
+ type: 'external-sql-file',
+ title: 'report.sql',
+ dataRef: {
+ path: 'D:/sql/report.sql',
+ directoryId: 'dir-1',
+ },
+ }, {
+ openExternalSQLFile,
+ openExternalSQLBindingModal,
+ openRenameExternalSQLFileModal: vi.fn(),
+ openCreateExternalSQLFileModal: vi.fn(),
+ openCreateExternalSQLDirectoryModal: vi.fn(),
+ handleDeleteExternalSQLFile: vi.fn(),
+ }) as any[];
+
+ expect(items.map((item) => item?.key)).toContain('bind-external-sql-file-database');
+ items.find((item) => item?.key === 'bind-external-sql-file-database')?.onClick?.();
+ expect(openExternalSQLBindingModal).toHaveBeenCalledWith(expect.objectContaining({
+ type: 'external-sql-file',
+ }));
+ expect(openExternalSQLFile).not.toHaveBeenCalled();
+ });
+});
diff --git a/frontend/src/components/sidebar/sidebarLegacyNodeMenu.tsx b/frontend/src/components/sidebar/sidebarLegacyNodeMenu.tsx
index 3978e665..29198455 100644
--- a/frontend/src/components/sidebar/sidebarLegacyNodeMenu.tsx
+++ b/frontend/src/components/sidebar/sidebarLegacyNodeMenu.tsx
@@ -382,6 +382,7 @@ export const buildSidebarLegacyNodeMenuItems = (
handleDeleteExternalSQLDirectory,
handleRemoveExternalSQLDirectory,
openExternalSQLFile,
+ openExternalSQLBindingModal,
openRenameExternalSQLFileModal,
handleDeleteExternalSQLFile,
extractObjectName,
@@ -1855,6 +1856,14 @@ export const buildSidebarLegacyNodeMenuItems = (
void openExternalSQLFile(node);
}
},
+ {
+ key: 'bind-external-sql-file-database',
+ label: t('sidebar.menu.bind_sql_file_database'),
+ icon: ,
+ onClick: () => {
+ openExternalSQLBindingModal(node);
+ }
+ },
{
key: 'rename-external-sql-file',
label: t('sidebar.menu.rename_sql_file'),
diff --git a/frontend/src/store.test.ts b/frontend/src/store.test.ts
index e10b52dc..d2b64c0c 100644
--- a/frontend/src/store.test.ts
+++ b/frontend/src/store.test.ts
@@ -1958,6 +1958,13 @@ describe('store appearance persistence', () => {
id: 'ext-1',
name: 'scripts',
path: 'D:/sql/scripts',
+ fileBindings: [
+ {
+ filePath: 'D:\\sql\\scripts\\report.sql',
+ connectionId: 'conn-2',
+ dbName: 'reporting',
+ },
+ ],
createdAt: 1,
});
@@ -1967,6 +1974,13 @@ describe('store appearance persistence', () => {
id: 'ext-1',
name: 'scripts',
path: 'D:/sql/scripts',
+ fileBindings: [
+ {
+ filePath: 'D:/sql/scripts/report.sql',
+ connectionId: 'conn-2',
+ dbName: 'reporting',
+ },
+ ],
createdAt: 1,
},
]);
@@ -1996,6 +2010,13 @@ describe('store appearance persistence', () => {
id: 'ext-1',
name: 'scripts',
path: 'D:/sql/scripts',
+ fileBindings: [
+ {
+ filePath: 'D:/sql/scripts/report.sql',
+ connectionId: 'conn-2',
+ dbName: 'reporting',
+ },
+ ],
createdAt: 1,
},
{
diff --git a/frontend/src/store.ts b/frontend/src/store.ts
index 05f48a30..4915c4d4 100644
--- a/frontend/src/store.ts
+++ b/frontend/src/store.ts
@@ -30,7 +30,10 @@ import {
type ShortcutPlatformBinding,
type ShortcutPlatform,
} from "./utils/shortcuts";
-import { buildExternalSQLDirectoryId } from "./utils/externalSqlTree";
+import {
+ buildExternalSQLDirectoryId,
+ normalizeExternalSQLPath,
+} from "./utils/externalSqlTree";
import {
DEFAULT_SQL_SNIPPETS,
BUILTIN_SNIPPET_MAP,
@@ -2171,6 +2174,23 @@ const resolveExternalSQLDirectoryName = (name: unknown, path: string): string =>
return pathSegment || translate("sidebar.sql_directory.default_name");
};
+const sanitizeExternalSQLFileBindings = (
+ value: unknown,
+): NonNullable => {
+ if (!Array.isArray(value)) return [];
+ const bindings = new Map[number]>();
+ value.forEach((entry) => {
+ if (!entry || typeof entry !== "object") return;
+ const raw = entry as Record;
+ const filePath = normalizeExternalSQLPath(toTrimmedString(raw.filePath));
+ const connectionId = toTrimmedString(raw.connectionId);
+ const dbName = toTrimmedString(raw.dbName);
+ if (!filePath || !connectionId || !dbName) return;
+ bindings.set(filePath, { filePath, connectionId, dbName });
+ });
+ return [...bindings.values()];
+};
+
const sanitizeExternalSQLDirectories = (
value: unknown,
): ExternalSQLDirectory[] => {
@@ -2184,6 +2204,7 @@ const sanitizeExternalSQLDirectories = (
if (!path) return;
const connectionId = toTrimmedString(raw.connectionId);
const dbName = toTrimmedString(raw.dbName);
+ const fileBindings = sanitizeExternalSQLFileBindings(raw.fileBindings);
const id =
toTrimmedString(
raw.id,
@@ -2197,6 +2218,7 @@ const sanitizeExternalSQLDirectories = (
path,
...(connectionId ? { connectionId } : {}),
...(dbName ? { dbName } : {}),
+ ...(fileBindings.length > 0 ? { fileBindings } : {}),
createdAt: Number.isFinite(Number(raw.createdAt))
? Number(raw.createdAt)
: Date.now(),
@@ -5005,6 +5027,7 @@ export const useStore = create()(
}
const connectionId = toTrimmedString(directory.connectionId);
const dbName = toTrimmedString(directory.dbName);
+ const fileBindings = sanitizeExternalSQLFileBindings(directory.fileBindings);
const nextDirectory: ExternalSQLDirectory = {
id:
toTrimmedString(
@@ -5015,6 +5038,7 @@ export const useStore = create()(
path,
...(connectionId ? { connectionId } : {}),
...(dbName ? { dbName } : {}),
+ ...(fileBindings.length > 0 ? { fileBindings } : {}),
createdAt: Number.isFinite(Number(directory.createdAt))
? Number(directory.createdAt)
: Date.now(),
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index 9187e153..a0e18588 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -623,9 +623,16 @@ export interface ExternalSQLDirectory {
path: string;
connectionId?: string;
dbName?: string;
+ fileBindings?: ExternalSQLFileBinding[];
createdAt: number;
}
+export interface ExternalSQLFileBinding {
+ filePath: string;
+ connectionId: string;
+ dbName: string;
+}
+
export interface ExternalSQLTreeEntry {
name: string;
path: string;
diff --git a/frontend/src/utils/externalSqlTree.test.ts b/frontend/src/utils/externalSqlTree.test.ts
index 12fec87d..a1a3c29c 100644
--- a/frontend/src/utils/externalSqlTree.test.ts
+++ b/frontend/src/utils/externalSqlTree.test.ts
@@ -1,7 +1,14 @@
import { describe, expect, it } from 'vitest';
import type { ExternalSQLDirectory, ExternalSQLTreeEntry } from '../types';
-import { buildExternalSQLRootNode, buildExternalSQLTabId } from './externalSqlTree';
+import {
+ buildExternalSQLRootNode,
+ buildExternalSQLTabId,
+ moveExternalSQLFileBindings,
+ removeExternalSQLFileBindings,
+ resolveExternalSQLFileBinding,
+ setExternalSQLFileBinding,
+} from './externalSqlTree';
describe('externalSqlTree helpers', () => {
it('builds external SQL root node with nested directory and file entries', () => {
@@ -145,6 +152,53 @@ describe('externalSqlTree helpers', () => {
});
});
+ it('uses a file binding ahead of its directory binding without affecting sibling files', () => {
+ const node = buildExternalSQLRootNode({
+ directories: [
+ {
+ id: 'dir-bound',
+ name: 'bound scripts',
+ path: 'D:/sql/bound',
+ connectionId: 'connection-1',
+ dbName: 'orders',
+ fileBindings: [
+ {
+ filePath: 'D:/sql/bound/report.sql',
+ connectionId: 'connection-2',
+ dbName: 'reporting',
+ },
+ ],
+ createdAt: 1,
+ },
+ ],
+ directoryTrees: {
+ 'dir-bound': [
+ {
+ name: 'report.sql',
+ path: 'D:/sql/bound/report.sql',
+ isDir: false,
+ },
+ {
+ name: 'orders.sql',
+ path: 'D:/sql/bound/orders.sql',
+ isDir: false,
+ },
+ ],
+ },
+ });
+
+ expect(node.children?.[0]?.children?.[0]?.dataRef).toMatchObject({
+ connectionId: 'connection-2',
+ dbName: 'reporting',
+ hasExplicitBinding: true,
+ });
+ expect(node.children?.[0]?.children?.[1]?.dataRef).toMatchObject({
+ connectionId: 'connection-1',
+ dbName: 'orders',
+ });
+ expect(node.children?.[0]?.children?.[1]?.dataRef).not.toHaveProperty('hasExplicitBinding');
+ });
+
it('keeps same-path directories separate when they target different databases', () => {
const node = buildExternalSQLRootNode({
directories: [
@@ -183,6 +237,120 @@ describe('externalSqlTree helpers', () => {
});
});
+ it('updates file bindings when a file or containing folder moves and removes deleted subtrees', () => {
+ const directory: ExternalSQLDirectory = {
+ id: 'dir-1',
+ name: 'scripts',
+ path: 'D:/sql/scripts',
+ createdAt: 1,
+ };
+ const bound = setExternalSQLFileBinding(directory, 'D:\\sql\\scripts\\daily.sql', {
+ connectionId: 'connection-1',
+ dbName: 'orders',
+ });
+ const movedFile = moveExternalSQLFileBindings(
+ bound,
+ 'D:/sql/scripts/daily.sql',
+ 'D:/sql/scripts/archive/daily.sql',
+ );
+ const movedFolder = moveExternalSQLFileBindings(
+ movedFile,
+ 'D:/sql/scripts/archive',
+ 'D:/sql/scripts/history',
+ );
+
+ expect(movedFolder.fileBindings).toEqual([{
+ filePath: 'D:/sql/scripts/history/daily.sql',
+ connectionId: 'connection-1',
+ dbName: 'orders',
+ }]);
+ expect(removeExternalSQLFileBindings(movedFolder, 'D:/sql/scripts/history').fileBindings).toBeUndefined();
+ });
+
+ it('resolves only persisted file bindings without replacing directory defaults', () => {
+ const directories: ExternalSQLDirectory[] = [
+ {
+ id: 'dir-1',
+ name: 'scripts',
+ path: 'D:/sql/scripts',
+ connectionId: 'connection-1',
+ dbName: 'orders',
+ fileBindings: [{
+ filePath: 'D:/sql/scripts/report.sql',
+ connectionId: 'connection-2',
+ dbName: 'reporting',
+ }],
+ createdAt: 1,
+ },
+ ];
+
+ expect(resolveExternalSQLFileBinding(directories, 'D:\\sql\\scripts\\report.sql')).toEqual({
+ connectionId: 'connection-2',
+ dbName: 'reporting',
+ hasExplicitBinding: true,
+ });
+ expect(resolveExternalSQLFileBinding(
+ directories,
+ 'D:/sql/scripts/orders.sql',
+ )).toBeUndefined();
+ expect(resolveExternalSQLFileBinding([{
+ id: 'root-dir',
+ name: 'root',
+ path: '/',
+ fileBindings: [{
+ filePath: '/var/sql/report.sql',
+ connectionId: 'connection-root',
+ dbName: 'main',
+ }],
+ createdAt: 1,
+ }], '/var/sql/report.sql')).toEqual({
+ connectionId: 'connection-root',
+ dbName: 'main',
+ hasExplicitBinding: true,
+ });
+ });
+
+ it('scopes explicit binding lookup to the preferred same-path directory', () => {
+ const directories: ExternalSQLDirectory[] = [
+ {
+ id: 'dir-orders',
+ name: 'scripts',
+ path: 'D:/sql/shared',
+ connectionId: 'connection-1',
+ dbName: 'orders',
+ createdAt: 1,
+ },
+ {
+ id: 'dir-reporting',
+ name: 'scripts',
+ path: 'D:/sql/shared',
+ connectionId: 'connection-2',
+ dbName: 'reporting',
+ fileBindings: [{
+ filePath: 'D:/sql/shared/report.sql',
+ connectionId: 'connection-3',
+ dbName: 'warehouse',
+ }],
+ createdAt: 2,
+ },
+ ];
+
+ expect(resolveExternalSQLFileBinding(
+ directories,
+ 'D:/sql/shared/report.sql',
+ { connectionId: 'connection-1', dbName: 'orders' },
+ )).toBeUndefined();
+ expect(resolveExternalSQLFileBinding(
+ directories,
+ 'D:/sql/shared/report.sql',
+ { connectionId: 'connection-2', dbName: 'reporting' },
+ )).toEqual({
+ connectionId: 'connection-3',
+ dbName: 'warehouse',
+ hasExplicitBinding: true,
+ });
+ });
+
it('filters non-sql file entries even when the backend returns them', () => {
const directories: ExternalSQLDirectory[] = [
{
diff --git a/frontend/src/utils/externalSqlTree.ts b/frontend/src/utils/externalSqlTree.ts
index 76b737c3..7a4e42b9 100644
--- a/frontend/src/utils/externalSqlTree.ts
+++ b/frontend/src/utils/externalSqlTree.ts
@@ -1,4 +1,8 @@
-import type { ExternalSQLDirectory, ExternalSQLTreeEntry } from '../types';
+import type {
+ ExternalSQLDirectory,
+ ExternalSQLFileBinding,
+ ExternalSQLTreeEntry,
+} from '../types';
export type ExternalSQLNodeType =
| 'external-sql-root'
@@ -32,6 +36,141 @@ export type ExternalSQLTreeLabels = {
export const normalizeExternalSQLPath = (value: string): string =>
String(value || '').trim().replace(/\\/g, '/');
+export const findExternalSQLFileBinding = (
+ bindings: ExternalSQLFileBinding[] | undefined,
+ filePath: string,
+): ExternalSQLFileBinding | undefined => {
+ const normalizedFilePath = normalizeExternalSQLPath(filePath);
+ if (!normalizedFilePath) return undefined;
+ return bindings?.find(
+ (binding) => normalizeExternalSQLPath(binding.filePath) === normalizedFilePath,
+ );
+};
+
+export const resolveExternalSQLFileBinding = (
+ directories: ExternalSQLDirectory[],
+ filePath: string,
+ preferredContext?: { connectionId?: string; dbName?: string },
+): { connectionId: string; dbName: string; hasExplicitBinding: boolean } | undefined => {
+ const normalizedFilePath = normalizeExternalSQLPath(filePath);
+ if (!normalizedFilePath) return undefined;
+ const matchingDirectories = [...directories]
+ .filter((directory) => {
+ const rawDirectoryPath = normalizeExternalSQLPath(directory.path);
+ const directoryPath = rawDirectoryPath === '/' ? '/' : rawDirectoryPath.replace(/\/+$/u, '');
+ return Boolean(directoryPath) && (
+ directoryPath === '/'
+ ? normalizedFilePath.startsWith('/')
+ : normalizedFilePath === directoryPath || normalizedFilePath.startsWith(`${directoryPath}/`)
+ );
+ })
+ .sort((left, right) => (
+ normalizeExternalSQLPath(right.path).length - normalizeExternalSQLPath(left.path).length
+ ));
+ const preferredConnectionId = String(preferredContext?.connectionId || '').trim();
+ const preferredDbName = String(preferredContext?.dbName || '').trim();
+ const preferredDirectory = preferredConnectionId
+ ? matchingDirectories.find((directory) => (
+ String(directory.connectionId || '').trim() === preferredConnectionId
+ && (!preferredDbName || String(directory.dbName || '').trim() === preferredDbName)
+ ))
+ : undefined;
+ if (preferredDirectory) {
+ const binding = findExternalSQLFileBinding(preferredDirectory.fileBindings, normalizedFilePath);
+ return binding
+ ? {
+ connectionId: String(binding.connectionId || '').trim(),
+ dbName: String(binding.dbName || '').trim(),
+ hasExplicitBinding: true,
+ }
+ : undefined;
+ }
+ for (const directory of matchingDirectories) {
+ const binding = findExternalSQLFileBinding(directory.fileBindings, normalizedFilePath);
+ if (binding) {
+ return {
+ connectionId: String(binding.connectionId || '').trim(),
+ dbName: String(binding.dbName || '').trim(),
+ hasExplicitBinding: true,
+ };
+ }
+ }
+ return undefined;
+};
+
+export const setExternalSQLFileBinding = (
+ directory: ExternalSQLDirectory,
+ filePath: string,
+ target?: { connectionId: string; dbName: string } | null,
+): ExternalSQLDirectory => {
+ const normalizedFilePath = normalizeExternalSQLPath(filePath);
+ if (!normalizedFilePath) return directory;
+ const remainingBindings = (directory.fileBindings || []).filter(
+ (binding) => normalizeExternalSQLPath(binding.filePath) !== normalizedFilePath,
+ );
+ const connectionId = String(target?.connectionId || '').trim();
+ const dbName = String(target?.dbName || '').trim();
+ const fileBindings = connectionId && dbName
+ ? [...remainingBindings, { filePath: normalizedFilePath, connectionId, dbName }]
+ : remainingBindings;
+ const nextDirectory = { ...directory };
+ if (fileBindings.length > 0) {
+ nextDirectory.fileBindings = fileBindings;
+ } else {
+ delete nextDirectory.fileBindings;
+ }
+ return nextDirectory;
+};
+
+const isPathEqualOrInside = (path: string, parentPath: string): boolean => (
+ path === parentPath
+ || (parentPath === '/' ? path.startsWith('/') : path.startsWith(`${parentPath}/`))
+);
+
+export const moveExternalSQLFileBindings = (
+ directory: ExternalSQLDirectory,
+ previousPath: string,
+ nextPath: string,
+): ExternalSQLDirectory => {
+ const normalizedPreviousPath = normalizeExternalSQLPath(previousPath);
+ const normalizedNextPath = normalizeExternalSQLPath(nextPath);
+ if (!normalizedPreviousPath || !normalizedNextPath || !directory.fileBindings?.length) {
+ return directory;
+ }
+ let changed = false;
+ const fileBindings = directory.fileBindings.map((binding) => {
+ const normalizedBindingPath = normalizeExternalSQLPath(binding.filePath);
+ if (!isPathEqualOrInside(normalizedBindingPath, normalizedPreviousPath)) {
+ return binding;
+ }
+ changed = true;
+ return {
+ ...binding,
+ filePath: `${normalizedNextPath}${normalizedBindingPath.slice(normalizedPreviousPath.length)}`,
+ };
+ });
+ return changed ? { ...directory, fileBindings } : directory;
+};
+
+export const removeExternalSQLFileBindings = (
+ directory: ExternalSQLDirectory,
+ targetPath: string,
+): ExternalSQLDirectory => {
+ const normalizedTargetPath = normalizeExternalSQLPath(targetPath);
+ if (!normalizedTargetPath || !directory.fileBindings?.length) return directory;
+ const fileBindings = directory.fileBindings.filter((binding) => (
+ !isPathEqualOrInside(normalizeExternalSQLPath(binding.filePath), normalizedTargetPath)
+ ));
+ if (fileBindings.length === directory.fileBindings.length) return directory;
+ const nextDirectory = { ...directory };
+ if (fileBindings.length > 0) {
+ nextDirectory.fileBindings = fileBindings;
+ } else {
+ delete nextDirectory.fileBindings;
+ }
+ return nextDirectory;
+};
+
const DEFAULT_EXTERNAL_SQL_TREE_LABELS: ExternalSQLTreeLabels = {
root: 'External SQL files',
directoryFallback: 'SQL directory',
@@ -87,7 +226,13 @@ const isExternalSQLFileEntry = (entry: ExternalSQLTreeEntry): boolean => {
const mapExternalSQLTreeEntries = (
entries: ExternalSQLTreeEntry[],
- context: { connectionId: string; dbName: string; dbNodeKey: string; directoryId: string },
+ context: {
+ connectionId: string;
+ dbName: string;
+ dbNodeKey: string;
+ directoryId: string;
+ fileBindings?: ExternalSQLFileBinding[];
+ },
): ExternalSQLTreeNode[] => entries.flatMap((entry): ExternalSQLTreeNode[] => {
const entryPath = normalizeExternalSQLPath(entry.path);
if (entry.isDir) {
@@ -113,14 +258,23 @@ const mapExternalSQLTreeEntries = (
return [];
}
+ const fileBinding = findExternalSQLFileBinding(context.fileBindings, entry.path);
+ const connectionId = String(fileBinding?.connectionId || '').trim() || context.connectionId;
+ const dbName = String(fileBinding?.dbName || '').trim() || context.dbName;
+
return [{
title: entry.name,
key: buildExternalSQLNodeKey('external-sql-file', entryPath, context.directoryId),
type: 'external-sql-file',
isLeaf: true,
dataRef: {
- connectionId: context.connectionId,
- dbName: context.dbName,
+ connectionId,
+ dbName,
+ ...(fileBinding ? {
+ directoryConnectionId: context.connectionId,
+ directoryDbName: context.dbName,
+ hasExplicitBinding: true,
+ } : {}),
dbNodeKey: context.dbNodeKey,
directoryId: context.directoryId,
path: entry.path,
@@ -154,6 +308,7 @@ export const buildExternalSQLRootNode = ({
dbName: directoryDbName,
dbNodeKey,
directoryId: directory.id,
+ fileBindings: directory.fileBindings,
});
return {
title: resolveDirectoryDisplayName(directory, resolvedLabels),
diff --git a/shared/i18n/de-DE.json b/shared/i18n/de-DE.json
index 93e81776..89b47885 100644
--- a/shared/i18n/de-DE.json
+++ b/shared/i18n/de-DE.json
@@ -7315,6 +7315,8 @@
"sidebar.error.unknown": "Unbekannter Fehler",
"sidebar.external_sql.directory_fallback": "SQL-Verzeichnis",
"sidebar.external_sql.root": "Externe SQL-Dateien",
+ "sidebar.external_sql_binding.clear_override": "Dateispezifische Bindung entfernen",
+ "sidebar.external_sql_binding.title": "Ausführungsdatenbank für SQL-Datei auswählen",
"sidebar.external_sql_modal.action.create": "Erstellen",
"sidebar.external_sql_modal.action.rename": "Umbenennen",
"sidebar.external_sql_modal.field.directory_name": "Verzeichnisname",
@@ -7369,6 +7371,7 @@
"sidebar.menu.backup_all_tables_sql": "Alle Tabellen sichern (Schema + Daten-SQL)",
"sidebar.menu.backup_current_schema_sql": "Alle Tabellen im aktuellen schema sichern (Struktur und Daten SQL)",
"sidebar.menu.backup_table_sql": "Tabelle sichern (SQL)",
+ "sidebar.menu.bind_sql_file_database": "Ausführungsdatenbank auswählen",
"sidebar.menu.bind_to_connection": "An Verbindung binden",
"sidebar.menu.browse_keys": "Schlüssel durchsuchen",
"sidebar.menu.browse_materialized_view_data": "Daten der materialisierten Ansicht durchsuchen",
@@ -7525,6 +7528,8 @@
"sidebar.message.external_sql_directory_rename_sync_failed": "Das Verzeichnis wurde umbenannt, aber die externe SQL-Verzeichnisliste konnte nicht synchronisiert werden. Fügen Sie das Verzeichnis erneut hinzu.",
"sidebar.message.external_sql_directory_rename_target_missing": "Das umzubenennende Verzeichnis wurde nicht gefunden.",
"sidebar.message.external_sql_file_delete_target_missing": "Die zu löschende SQL-Datei wurde nicht gefunden.",
+ "sidebar.message.external_sql_file_binding_cleared": "Die dateispezifische Bindung wurde entfernt. Der Standardkontext des Verzeichnisses wird verwendet.",
+ "sidebar.message.external_sql_file_binding_saved": "Ausführungsdatenbank der SQL-Datei gespeichert.",
"sidebar.message.external_sql_file_parent_missing": "Das Verzeichnis zum Erstellen der SQL-Datei wurde nicht gefunden.",
"sidebar.message.external_sql_file_rename_target_missing": "Die umzubenennende SQL-Datei wurde nicht gefunden.",
"sidebar.message.jvm_provider_probe_exception": "Prüfung der JVM-Anbieter fehlgeschlagen: {{error}}",
diff --git a/shared/i18n/en-US.json b/shared/i18n/en-US.json
index 44c0dc40..18bb1b30 100644
--- a/shared/i18n/en-US.json
+++ b/shared/i18n/en-US.json
@@ -7315,6 +7315,8 @@
"sidebar.error.unknown": "Unknown error",
"sidebar.external_sql.directory_fallback": "SQL directory",
"sidebar.external_sql.root": "External SQL files",
+ "sidebar.external_sql_binding.clear_override": "Clear file-specific binding",
+ "sidebar.external_sql_binding.title": "Choose SQL file execution database",
"sidebar.external_sql_modal.action.create": "Create",
"sidebar.external_sql_modal.action.rename": "Rename",
"sidebar.external_sql_modal.field.directory_name": "Directory name",
@@ -7369,6 +7371,7 @@
"sidebar.menu.backup_all_tables_sql": "Back up all tables (schema + data SQL)",
"sidebar.menu.backup_current_schema_sql": "Back up all tables in current schema (schema and data SQL)",
"sidebar.menu.backup_table_sql": "Back up table (SQL)",
+ "sidebar.menu.bind_sql_file_database": "Choose execution database",
"sidebar.menu.bind_to_connection": "Bind to connection",
"sidebar.menu.browse_keys": "Browse keys",
"sidebar.menu.browse_materialized_view_data": "Browse materialized view data",
@@ -7525,6 +7528,8 @@
"sidebar.message.external_sql_directory_rename_sync_failed": "Directory was renamed, but the external SQL directory list could not be synchronized. Add the directory again.",
"sidebar.message.external_sql_directory_rename_target_missing": "Could not find the directory to rename.",
"sidebar.message.external_sql_file_delete_target_missing": "Could not find the SQL file to delete.",
+ "sidebar.message.external_sql_file_binding_cleared": "The file-specific binding was cleared. The directory default context will be used.",
+ "sidebar.message.external_sql_file_binding_saved": "SQL file execution database saved.",
"sidebar.message.external_sql_file_parent_missing": "Could not find the directory for creating an SQL file.",
"sidebar.message.external_sql_file_rename_target_missing": "Could not find the SQL file to rename.",
"sidebar.message.jvm_provider_probe_exception": "JVM provider probe failed: {{error}}",
diff --git a/shared/i18n/ja-JP.json b/shared/i18n/ja-JP.json
index e2a49a1c..c27e5656 100644
--- a/shared/i18n/ja-JP.json
+++ b/shared/i18n/ja-JP.json
@@ -7315,6 +7315,8 @@
"sidebar.error.unknown": "不明なエラー",
"sidebar.external_sql.directory_fallback": "SQL ディレクトリ",
"sidebar.external_sql.root": "外部 SQL ファイル",
+ "sidebar.external_sql_binding.clear_override": "ファイル固有のバインドを解除",
+ "sidebar.external_sql_binding.title": "SQL ファイルの実行データベースを指定",
"sidebar.external_sql_modal.action.create": "作成",
"sidebar.external_sql_modal.action.rename": "名前を変更",
"sidebar.external_sql_modal.field.directory_name": "ディレクトリ名",
@@ -7369,6 +7371,7 @@
"sidebar.menu.backup_all_tables_sql": "すべてのテーブルをバックアップ(スキーマ + データ SQL)",
"sidebar.menu.backup_current_schema_sql": "現在の schema の全テーブルをバックアップ(構造とデータ SQL)",
"sidebar.menu.backup_table_sql": "テーブルをバックアップ(SQL)",
+ "sidebar.menu.bind_sql_file_database": "実行データベースを指定",
"sidebar.menu.bind_to_connection": "接続にバインド",
"sidebar.menu.browse_keys": "キーを参照",
"sidebar.menu.browse_materialized_view_data": "マテリアライズドビューのデータを参照",
@@ -7525,6 +7528,8 @@
"sidebar.message.external_sql_directory_rename_sync_failed": "ディレクトリの名前は変更されましたが、外部 SQL ディレクトリ一覧を同期できません。ディレクトリを追加し直してください。",
"sidebar.message.external_sql_directory_rename_target_missing": "名前を変更できるディレクトリが見つかりません。",
"sidebar.message.external_sql_file_delete_target_missing": "削除する SQL ファイルが見つかりません。",
+ "sidebar.message.external_sql_file_binding_cleared": "ファイル固有のバインドを解除しました。ディレクトリの既定コンテキストを使用します。",
+ "sidebar.message.external_sql_file_binding_saved": "SQL ファイルの実行データベースを保存しました。",
"sidebar.message.external_sql_file_parent_missing": "SQL ファイルを作成するディレクトリが見つかりません。",
"sidebar.message.external_sql_file_rename_target_missing": "名前を変更できる SQL ファイルが見つかりません。",
"sidebar.message.jvm_provider_probe_exception": "JVM プロバイダーの検出に失敗しました: {{error}}",
diff --git a/shared/i18n/ru-RU.json b/shared/i18n/ru-RU.json
index e7969b3f..6c62ce2a 100644
--- a/shared/i18n/ru-RU.json
+++ b/shared/i18n/ru-RU.json
@@ -7315,6 +7315,8 @@
"sidebar.error.unknown": "Неизвестная ошибка",
"sidebar.external_sql.directory_fallback": "SQL-каталог",
"sidebar.external_sql.root": "Внешние SQL-файлы",
+ "sidebar.external_sql_binding.clear_override": "Удалить отдельную привязку файла",
+ "sidebar.external_sql_binding.title": "Выбор базы выполнения SQL-файла",
"sidebar.external_sql_modal.action.create": "Создать",
"sidebar.external_sql_modal.action.rename": "Переименовать",
"sidebar.external_sql_modal.field.directory_name": "Имя каталога",
@@ -7369,6 +7371,7 @@
"sidebar.menu.backup_all_tables_sql": "Создать резервную копию всех таблиц (схема + данные SQL)",
"sidebar.menu.backup_current_schema_sql": "Создать резервную копию всех таблиц текущей schema (структура и данные SQL)",
"sidebar.menu.backup_table_sql": "Создать резервную копию таблицы (SQL)",
+ "sidebar.menu.bind_sql_file_database": "Выбрать базу выполнения",
"sidebar.menu.bind_to_connection": "Привязать к подключению",
"sidebar.menu.browse_keys": "Просмотреть ключи",
"sidebar.menu.browse_materialized_view_data": "Просмотреть данные материализованного представления",
@@ -7525,6 +7528,8 @@
"sidebar.message.external_sql_directory_rename_sync_failed": "Каталог переименован, но список внешних SQL-каталогов не удалось синхронизировать. Добавьте каталог заново.",
"sidebar.message.external_sql_directory_rename_target_missing": "Не найден каталог для переименования.",
"sidebar.message.external_sql_file_delete_target_missing": "SQL-файл для удаления не найден.",
+ "sidebar.message.external_sql_file_binding_cleared": "Отдельная привязка файла удалена. Будет использован контекст каталога по умолчанию.",
+ "sidebar.message.external_sql_file_binding_saved": "База выполнения SQL-файла сохранена.",
"sidebar.message.external_sql_file_parent_missing": "Не найден каталог для создания SQL-файла.",
"sidebar.message.external_sql_file_rename_target_missing": "Не найден SQL-файл для переименования.",
"sidebar.message.jvm_provider_probe_exception": "Проверка провайдеров JVM завершилась ошибкой: {{error}}",
diff --git a/shared/i18n/zh-CN.json b/shared/i18n/zh-CN.json
index 04c1ed12..ac21c2c9 100644
--- a/shared/i18n/zh-CN.json
+++ b/shared/i18n/zh-CN.json
@@ -7315,6 +7315,8 @@
"sidebar.error.unknown": "未知错误",
"sidebar.external_sql.directory_fallback": "SQL 目录",
"sidebar.external_sql.root": "外部 SQL 文件",
+ "sidebar.external_sql_binding.clear_override": "清除文件单独绑定",
+ "sidebar.external_sql_binding.title": "指定 SQL 文件执行数据库",
"sidebar.external_sql_modal.action.create": "新建",
"sidebar.external_sql_modal.action.rename": "重命名",
"sidebar.external_sql_modal.field.directory_name": "目录名",
@@ -7369,6 +7371,7 @@
"sidebar.menu.backup_all_tables_sql": "备份全部表(结构和数据 SQL)",
"sidebar.menu.backup_current_schema_sql": "备份当前模式全部表(结构和数据 SQL)",
"sidebar.menu.backup_table_sql": "备份表(SQL)",
+ "sidebar.menu.bind_sql_file_database": "指定执行数据库",
"sidebar.menu.bind_to_connection": "绑定到连接",
"sidebar.menu.browse_keys": "浏览键",
"sidebar.menu.browse_materialized_view_data": "浏览物化视图数据",
@@ -7525,6 +7528,8 @@
"sidebar.message.external_sql_directory_rename_sync_failed": "目录已重命名,但无法同步外部 SQL 目录列表,请重新添加目录。",
"sidebar.message.external_sql_directory_rename_target_missing": "未找到可重命名的目录。",
"sidebar.message.external_sql_file_delete_target_missing": "未找到可删除的 SQL 文件。",
+ "sidebar.message.external_sql_file_binding_cleared": "已清除 SQL 文件单独绑定,将使用目录默认上下文。",
+ "sidebar.message.external_sql_file_binding_saved": "SQL 文件执行数据库已保存。",
"sidebar.message.external_sql_file_parent_missing": "未找到可新建 SQL 文件的目录。",
"sidebar.message.external_sql_file_rename_target_missing": "未找到可重命名的 SQL 文件。",
"sidebar.message.jvm_provider_probe_exception": "JVM 提供方探测失败:{{error}}",
diff --git a/shared/i18n/zh-TW.json b/shared/i18n/zh-TW.json
index 49f0555d..76647d6e 100644
--- a/shared/i18n/zh-TW.json
+++ b/shared/i18n/zh-TW.json
@@ -7315,6 +7315,8 @@
"sidebar.error.unknown": "未知錯誤",
"sidebar.external_sql.directory_fallback": "SQL 目錄",
"sidebar.external_sql.root": "外部 SQL 檔案",
+ "sidebar.external_sql_binding.clear_override": "清除檔案單獨綁定",
+ "sidebar.external_sql_binding.title": "指定 SQL 檔案執行資料庫",
"sidebar.external_sql_modal.action.create": "新增",
"sidebar.external_sql_modal.action.rename": "重新命名",
"sidebar.external_sql_modal.field.directory_name": "目錄名稱",
@@ -7369,6 +7371,7 @@
"sidebar.menu.backup_all_tables_sql": "備份全部資料表(結構和資料 SQL)",
"sidebar.menu.backup_current_schema_sql": "備份目前模式全部資料表(結構和資料 SQL)",
"sidebar.menu.backup_table_sql": "備份資料表(SQL)",
+ "sidebar.menu.bind_sql_file_database": "指定執行資料庫",
"sidebar.menu.bind_to_connection": "綁定到連線",
"sidebar.menu.browse_keys": "瀏覽鍵",
"sidebar.menu.browse_materialized_view_data": "瀏覽物化檢視資料",
@@ -7525,6 +7528,8 @@
"sidebar.message.external_sql_directory_rename_sync_failed": "目錄已重新命名,但無法同步外部 SQL 目錄清單,請重新新增目錄。",
"sidebar.message.external_sql_directory_rename_target_missing": "找不到可重新命名的目錄。",
"sidebar.message.external_sql_file_delete_target_missing": "找不到可刪除的 SQL 檔案。",
+ "sidebar.message.external_sql_file_binding_cleared": "已清除 SQL 檔案單獨綁定,將使用目錄預設內容。",
+ "sidebar.message.external_sql_file_binding_saved": "SQL 檔案執行資料庫已儲存。",
"sidebar.message.external_sql_file_parent_missing": "找不到可新增 SQL 檔案的目錄。",
"sidebar.message.external_sql_file_rename_target_missing": "找不到可重新命名的 SQL 檔案。",
"sidebar.message.jvm_provider_probe_exception": "JVM 提供者探測失敗:{{error}}",