diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 6805165f..648b13bc 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -2,6 +2,9 @@ import Modal from './common/ResizableDraggableModal'; import TitleBarQuickActions, { type TitleBarQuickAction } from './TitleBarQuickActions'; import { type DataSyncEntryMode } from './dataSyncEntryMode'; +import type { connection as ConnectionModels } from '../../wailsjs/go/models'; +type DatabaseCharset = ConnectionModels.DatabaseCharset; +type DatabaseCollation = ConnectionModels.DatabaseCollation; import SidebarSearchPanel, { type SidebarSearchPanelProps } from './sidebar/SidebarSearchPanel'; import { buildSidebarLegacyNodeMenuItems } from './sidebar/sidebarLegacyNodeMenu'; import { @@ -1009,6 +1012,9 @@ const Sidebar: React.FC<{ const [isCreateDbModalOpen, setIsCreateDbModalOpen] = useState(false); const [createDbForm] = Form.useForm(); const [targetConnection, setTargetConnection] = useState(null); + const [createDbCharsets, setCreateDbCharsets] = useState([]); + const [createDbCollations, setCreateDbCollations] = useState([]); + const [loadingCreateDbOptions, setLoadingCreateDbOptions] = useState(false); const [isCreateSchemaModalOpen, setIsCreateSchemaModalOpen] = useState(false); const [createSchemaForm] = Form.useForm(); const [createSchemaTarget, setCreateSchemaTarget] = useState(null); @@ -2633,7 +2639,14 @@ const Sidebar: React.FC<{ closeTabsByDatabase, createDbForm, targetConnection, + isCreateDbModalOpen, setIsCreateDbModalOpen, + createDbCharsets, + setCreateDbCharsets, + createDbCollations, + setCreateDbCollations, + loadingCreateDbOptions, + setLoadingCreateDbOptions, createSchemaForm, createSchemaTarget, setCreateSchemaTarget, @@ -3870,6 +3883,10 @@ const Sidebar: React.FC<{ setIsCreateDbModalOpen={setIsCreateDbModalOpen} createDbForm={createDbForm} handleCreateDatabase={handleCreateDatabase} + createDbTarget={targetConnection} + createDbCharsets={createDbCharsets} + createDbCollations={createDbCollations} + loadingCreateDbOptions={loadingCreateDbOptions} isCreateSchemaModalOpen={isCreateSchemaModalOpen} setIsCreateSchemaModalOpen={setIsCreateSchemaModalOpen} createSchemaForm={createSchemaForm} diff --git a/frontend/src/components/sidebar/SidebarEntityModals.tsx b/frontend/src/components/sidebar/SidebarEntityModals.tsx index 00b33154..25e1a0d0 100644 --- a/frontend/src/components/sidebar/SidebarEntityModals.tsx +++ b/frontend/src/components/sidebar/SidebarEntityModals.tsx @@ -1,11 +1,15 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { Checkbox, Empty, Form, Input, Select, Space } from 'antd'; import type { FormInstance } from 'antd/es/form'; import { FolderOpenOutlined } from '@ant-design/icons'; import Modal from '../common/ResizableDraggableModal'; import type { ConnectionTag, SavedConnection, SavedQuery } from '../../types'; +import type { connection as ConnectionModels } from '../../../wailsjs/go/models'; +type DatabaseCharset = ConnectionModels.DatabaseCharset; +type DatabaseCollation = ConnectionModels.DatabaseCollation; import { t } from '../../i18n'; import { noAutoCapInputProps } from '../../utils/inputAutoCap'; +import { getDataSourceCapabilities } from '../../utils/dataSourceCapabilities'; import { DEFAULT_CONNECTION_ENVIRONMENT, normalizeConnectionEnvironmentType, @@ -104,6 +108,10 @@ type SidebarEntityModalsProps = { setIsCreateDbModalOpen: (open: boolean) => void; createDbForm: FormInstance; handleCreateDatabase: () => void; + createDbTarget: any; + createDbCharsets: DatabaseCharset[]; + createDbCollations: DatabaseCollation[]; + loadingCreateDbOptions: boolean; isCreateSchemaModalOpen: boolean; setIsCreateSchemaModalOpen: (open: boolean) => void; createSchemaForm: FormInstance; @@ -158,6 +166,10 @@ export const SidebarEntityModals: React.FC = ({ setIsCreateDbModalOpen, createDbForm, handleCreateDatabase, + createDbTarget, + createDbCharsets, + createDbCollations, + loadingCreateDbOptions, isCreateSchemaModalOpen, setIsCreateSchemaModalOpen, createSchemaForm, @@ -203,6 +215,27 @@ export const SidebarEntityModals: React.FC = ({ editingTagId, ); + const createDatabaseCharsetSupported = Boolean( + createDbTarget?.dataRef?.config + && getDataSourceCapabilities(createDbTarget.dataRef.config).supportsCreateDatabaseCharset, + ); + const selectedCreateDbCharset = Form.useWatch('charset', createDbForm); + const createDatabaseCharsetOptions = useMemo( + () => createDbCharsets.map((charset) => ({ + value: charset.name, + label: charset.defaultCollation + ? `${charset.name}(${charset.defaultCollation})` + : charset.name, + })), + [createDbCharsets], + ); + const createDatabaseCollationOptions = useMemo( + () => createDbCollations + .filter((collation) => !selectedCreateDbCharset || collation.charset === selectedCreateDbCharset) + .map((collation) => ({ value: collation.name, label: collation.name })), + [createDbCollations, selectedCreateDbCharset], + ); + return (<> = ({ > + {createDatabaseCharsetSupported && ( + <> + + + + + )} diff --git a/frontend/src/components/sidebar/useSidebarObjectActions.tsx b/frontend/src/components/sidebar/useSidebarObjectActions.tsx index 27af5424..ab4fbe2a 100644 --- a/frontend/src/components/sidebar/useSidebarObjectActions.tsx +++ b/frontend/src/components/sidebar/useSidebarObjectActions.tsx @@ -1,4 +1,4 @@ -import { useCallback, type Dispatch, type MutableRefObject, type SetStateAction } from 'react'; +import { useCallback, useEffect, type Dispatch, type MutableRefObject, type SetStateAction } from 'react'; import { message } from 'antd'; import type { FormInstance } from 'antd/es/form'; @@ -15,6 +15,9 @@ import { buildStarRocksMaterializedViewPreviewSql } from '../tableDesignerSchema import type { ExportRunResult, RunExportWithProgressOptions } from '../useExportProgressRunner'; import { getTableDataDangerActionMeta, type TableDataDangerActionKind } from '../tableDataDangerActions'; import { confirmCopyTable } from '../tableCopyAction'; +import type { connection as ConnectionModels } from '../../../wailsjs/go/models'; +type DatabaseCharset = ConnectionModels.DatabaseCharset; +type DatabaseCollation = ConnectionModels.DatabaseCollation; import { buildDuckDBMacroDDL, escapeSQLLiteral, @@ -38,6 +41,8 @@ import { DropTable, DropView, ExportTableWithOptions, + ListDatabaseCharsets, + ListDatabaseCollations, RenameDatabase, RenameTable, RenameView, @@ -71,7 +76,14 @@ type UseSidebarObjectActionsArgs = { closeTabsByDatabase: (connectionId: string, dbName: string) => void; createDbForm: FormInstance; targetConnection: any; + isCreateDbModalOpen: boolean; setIsCreateDbModalOpen: Dispatch>; + createDbCharsets: DatabaseCharset[]; + setCreateDbCharsets: Dispatch>; + createDbCollations: DatabaseCollation[]; + setCreateDbCollations: Dispatch>; + loadingCreateDbOptions: boolean; + setLoadingCreateDbOptions: Dispatch>; createSchemaForm: FormInstance; createSchemaTarget: any; setCreateSchemaTarget: Dispatch>; @@ -173,7 +185,14 @@ export const useSidebarObjectActions = ({ closeTabsByDatabase, createDbForm, targetConnection, + isCreateDbModalOpen, setIsCreateDbModalOpen, + createDbCharsets, + setCreateDbCharsets, + createDbCollations, + setCreateDbCollations, + loadingCreateDbOptions, + setLoadingCreateDbOptions, createSchemaForm, createSchemaTarget, setCreateSchemaTarget, @@ -419,7 +438,12 @@ export const useSidebarObjectActions = ({ ssh: conn.config.ssh || { host: '', port: 22, user: '', password: '', keyPath: '' }, }; - const res = await CreateDatabase(buildRpcConnectionConfig(config) as any, values.name); + const res = await CreateDatabase( + buildRpcConnectionConfig(config) as any, + values.name, + String(values.charset || ''), + String(values.collation || ''), + ); if (res.success) { message.success(t('sidebar.message.database_created')); setIsCreateDbModalOpen(false); @@ -433,6 +457,46 @@ export const useSidebarObjectActions = ({ } }; + useEffect(() => { + if (!isCreateDbModalOpen || !targetConnection?.dataRef?.config) return; + if (!getDataSourceCapabilities(targetConnection.dataRef.config).supportsCreateDatabaseCharset) return; + let cancelled = false; + const conn = targetConnection.dataRef; + const config = { + ...conn.config, + port: Number(conn.config.port), + password: conn.config.password || '', + database: '', + useSSH: conn.config.useSSH || false, + ssh: conn.config.ssh || { host: '', port: 22, user: '', password: '', keyPath: '' }, + }; + setLoadingCreateDbOptions(true); + Promise.all([ + ListDatabaseCharsets(buildRpcConnectionConfig(config) as any), + ListDatabaseCollations(buildRpcConnectionConfig(config) as any), + ]) + .then(([charsetsResult, collationsResult]) => { + if (cancelled) return; + if (charsetsResult?.success) { + setCreateDbCharsets(Array.isArray(charsetsResult.data) ? charsetsResult.data : []); + } + if (collationsResult?.success) { + setCreateDbCollations(Array.isArray(collationsResult.data) ? collationsResult.data : []); + } + }) + .catch(() => { + if (!cancelled) { + message.error(t('sidebar.message.create_db_options_load_failed')); + } + }) + .finally(() => { + if (!cancelled) setLoadingCreateDbOptions(false); + }); + return () => { + cancelled = true; + }; + }, [isCreateDbModalOpen, targetConnection, t]); + const openCreateSchemaModal = (node: any) => { const dialect = getMetadataDialect(node?.dataRef as SavedConnection); if (!isPostgresSchemaDialect(dialect)) { diff --git a/frontend/src/utils/dataSourceCapabilities.test.ts b/frontend/src/utils/dataSourceCapabilities.test.ts index 98aef7a5..8d808c78 100644 --- a/frontend/src/utils/dataSourceCapabilities.test.ts +++ b/frontend/src/utils/dataSourceCapabilities.test.ts @@ -31,6 +31,18 @@ describe('dataSourceCapabilities', () => { }); }); + it('enables create-database charset options only for MySQL-family dialects', () => { + expect(getDataSourceCapabilities({ type: 'mysql' }).supportsCreateDatabaseCharset).toBe(true); + expect(getDataSourceCapabilities({ type: 'goldendb' }).supportsCreateDatabaseCharset).toBe(true); + expect(getDataSourceCapabilities({ type: 'mariadb' }).supportsCreateDatabaseCharset).toBe(true); + expect(getDataSourceCapabilities({ type: 'oceanbase', oceanBaseProtocol: 'mysql' }).supportsCreateDatabaseCharset).toBe(true); + expect(getDataSourceCapabilities({ type: 'diros' }).supportsCreateDatabaseCharset).toBe(true); + expect(getDataSourceCapabilities({ type: 'postgres' }).supportsCreateDatabaseCharset).toBe(false); + expect(getDataSourceCapabilities({ type: 'sqlserver' }).supportsCreateDatabaseCharset).toBe(false); + expect(getDataSourceCapabilities({ type: 'clickhouse' }).supportsCreateDatabaseCharset).toBe(false); + expect(getDataSourceCapabilities({ type: 'starrocks' }).supportsCreateDatabaseCharset).toBe(false); + }); + it('uses ClickHouse capabilities for a custom ClickHouse JDBC connection', () => { expect(getDataSourceCapabilities({ type: 'custom', driver: 'clickhouse' })).toMatchObject({ type: 'clickhouse', diff --git a/frontend/src/utils/dataSourceCapabilities.ts b/frontend/src/utils/dataSourceCapabilities.ts index 8c209fe8..8dec8ca1 100644 --- a/frontend/src/utils/dataSourceCapabilities.ts +++ b/frontend/src/utils/dataSourceCapabilities.ts @@ -205,6 +205,7 @@ export type DataSourceCapabilities = { supportsCopyInsert: boolean; supportsCopyTable: boolean; supportsCreateDatabase: boolean; + supportsCreateDatabaseCharset: boolean; supportsRenameDatabase: boolean; supportsDropDatabase: boolean; supportsMessagePublish: boolean; @@ -233,6 +234,15 @@ const CREATE_DATABASE_TYPES = new Set([ 'clickhouse', ]); +// MySQL 系方言支持在建库时指定字符集与排序规则。 +const CREATE_DATABASE_CHARSET_TYPES = new Set([ + 'mysql', + 'goldendb', + 'mariadb', + 'oceanbase', + 'diros', +]); + const RENAME_DATABASE_TYPES = new Set([ 'diros', 'postgres', @@ -278,6 +288,8 @@ export const getDataSourceCapabilities = (config: ConnectionLike): DataSourceCap !structureEditRestricted && COPY_TABLE_TYPES.has(type), supportsCreateDatabase: !structureEditRestricted && CREATE_DATABASE_TYPES.has(type), + supportsCreateDatabaseCharset: + !structureEditRestricted && CREATE_DATABASE_CHARSET_TYPES.has(type), supportsRenameDatabase: !structureEditRestricted && RENAME_DATABASE_TYPES.has(type), supportsDropDatabase: !structureEditRestricted && DROP_DATABASE_TYPES.has(type), supportsMessagePublish: !dataEditRestricted && MESSAGE_PUBLISH_TYPES.has(type), diff --git a/frontend/wailsjs/go/app/App.d.ts b/frontend/wailsjs/go/app/App.d.ts index 5411f1f0..418cd410 100755 --- a/frontend/wailsjs/go/app/App.d.ts +++ b/frontend/wailsjs/go/app/App.d.ts @@ -54,7 +54,7 @@ export function ConfigureGlobalProxy(arg1:boolean,arg2:connection.ProxyConfig):P export function CopyTable(arg1:connection.ConnectionConfig,arg2:string,arg3:string,arg4:string):Promise; -export function CreateDatabase(arg1:connection.ConnectionConfig,arg2:string):Promise; +export function CreateDatabase(arg1:connection.ConnectionConfig,arg2:string,arg3:string,arg4:string):Promise; export function CreateSQLDirectory(arg1:string,arg2:string):Promise; @@ -280,6 +280,10 @@ export function JVMStopMonitoring(arg1:connection.ConnectionConfig,arg2:string): export function ListInstalledFontFamilies():Promise; +export function ListDatabaseCharsets(arg1:connection.ConnectionConfig):Promise; + +export function ListDatabaseCollations(arg1:connection.ConnectionConfig):Promise; + export function ListSQLDirectory(arg1:string):Promise; export function LogWindowDiagnostic(arg1:string,arg2:string):Promise; diff --git a/frontend/wailsjs/go/app/App.js b/frontend/wailsjs/go/app/App.js index afc3a46a..cb3076ca 100755 --- a/frontend/wailsjs/go/app/App.js +++ b/frontend/wailsjs/go/app/App.js @@ -94,8 +94,8 @@ export function CopyTable(arg1, arg2, arg3, arg4) { return window['go']['app']['App']['CopyTable'](arg1, arg2, arg3, arg4); } -export function CreateDatabase(arg1, arg2) { - return window['go']['app']['App']['CreateDatabase'](arg1, arg2); +export function CreateDatabase(arg1, arg2, arg3, arg4) { + return window['go']['app']['App']['CreateDatabase'](arg1, arg2, arg3, arg4); } export function CreateSQLDirectory(arg1, arg2) { @@ -546,6 +546,14 @@ export function ListInstalledFontFamilies() { return window['go']['app']['App']['ListInstalledFontFamilies'](); } +export function ListDatabaseCharsets(arg1) { + return window['go']['app']['App']['ListDatabaseCharsets'](arg1); +} + +export function ListDatabaseCollations(arg1) { + return window['go']['app']['App']['ListDatabaseCollations'](arg1); +} + export function ListSQLDirectory(arg1) { return window['go']['app']['App']['ListSQLDirectory'](arg1); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 5dffe0b2..a6db5745 100755 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -1586,6 +1586,38 @@ export namespace app { export namespace connection { + export class DatabaseCharset { + name: string; + description?: string; + defaultCollation?: string; + maxLength?: number; + + static createFrom(source: any = {}) { + return new DatabaseCharset(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.description = source["description"]; + this.defaultCollation = source["defaultCollation"]; + this.maxLength = source["maxLength"]; + } + } + export class DatabaseCollation { + name: string; + charset: string; + + static createFrom(source: any = {}) { + return new DatabaseCollation(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.charset = source["charset"]; + } + } export class UpdateRow { keys: Record; values: Record;