mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-10 16:53:35 +08:00
## Summary - sort table names in natural numeric order in both the table overview and sidebar - preserve MySQL column default values, including explicit empty-string defaults and supported expressions - add per-column default, character set, collation, and comment editing - generate complete MySQL column definitions while filtering `DEFAULT_GENERATED` - keep column-level character set behavior scoped to MySQL ## Testing - `npm --prefix frontend test -- src/components/TableDesignerSqlPreview.test.tsx src/utils/tableOverviewFilter.test.ts src/utils/columnDefinition.test.ts src/components/tableDesignerSchemaSql.test.ts` (58 passed) - sidebar natural-sort focused test (passed) - `npm --prefix frontend run build` (passed; existing chunk-size warning only) - `go test ./internal/db ./internal/connection` (passed) - i18n JSON validation, Go formatting check, and `git show --check` (passed) ## Validation notes - The full sidebar test file still has 5 unrelated source/CSS sentinel failures. The same 5 failures reproduce against the pre-sidebar-change baseline. - Repository-wide frontend and Go suites contain existing environment/baseline failures unrelated to this change. - Docker was unavailable, so a disposable MySQL round-trip test could not be run. - The development page was reachable, but Playwright is not installed in the repository, so automated UI interaction screenshots were not produced. ## Known limitation MySQL `SHOW FULL COLUMNS` cannot reliably distinguish no `DEFAULT` clause from an explicit `DEFAULT NULL`. This change preserves non-null defaults and explicit empty-string defaults without introducing a full `SHOW CREATE TABLE` parser. Closes #705
This commit is contained in:
@@ -2254,6 +2254,20 @@ describe('Sidebar locate toolbar', () => {
|
||||
expect(metaSource).not.toContain('— 行');
|
||||
});
|
||||
|
||||
it('sorts sidebar table names in natural numeric order', () => {
|
||||
const entries = [
|
||||
{ tableName: 'table_10', displayName: 'table_10' },
|
||||
{ tableName: 'table_2', displayName: 'table_2' },
|
||||
{ tableName: 'table_1', displayName: 'table_1' },
|
||||
];
|
||||
|
||||
expect(sortSidebarTableEntries(entries, {
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'main',
|
||||
sortBy: 'name',
|
||||
}).map((entry) => entry.tableName)).toEqual(['table_1', 'table_2', 'table_10']);
|
||||
});
|
||||
|
||||
it('sorts pinned sidebar tables before the active sort mode', () => {
|
||||
const pinnedSidebarTables = [
|
||||
buildSidebarTablePinKey('conn-1', 'main', 'orders', 'public'),
|
||||
|
||||
@@ -243,6 +243,10 @@ const COMMON_DEFAULTS = [
|
||||
{ value: "''" },
|
||||
];
|
||||
|
||||
const isMySQLCharacterColumnType = (columnType: string): boolean => (
|
||||
/^(?:char|varchar|tinytext|text|mediumtext|longtext|enum|set|nchar|nvarchar)\b/i.test(String(columnType || '').trim())
|
||||
);
|
||||
|
||||
|
||||
const PGLIKE_INDEX_TYPE_OPTIONS = [
|
||||
{ label: 'DEFAULT', value: 'DEFAULT' },
|
||||
@@ -285,7 +289,16 @@ const COLLATIONS = {
|
||||
{ label: 'utf8_unicode_ci', value: 'utf8_unicode_ci' },
|
||||
{ label: 'utf8_general_ci', value: 'utf8_general_ci' },
|
||||
{ label: 'utf8_bin', value: 'utf8_bin' },
|
||||
]
|
||||
],
|
||||
'latin1': [
|
||||
{ label: 'latin1_swedish_ci', value: 'latin1_swedish_ci' },
|
||||
{ label: 'latin1_general_ci', value: 'latin1_general_ci' },
|
||||
{ label: 'latin1_bin', value: 'latin1_bin' },
|
||||
],
|
||||
'ascii': [
|
||||
{ label: 'ascii_general_ci', value: 'ascii_general_ci' },
|
||||
{ label: 'ascii_bin', value: 'ascii_bin' },
|
||||
],
|
||||
};
|
||||
|
||||
const getCollationOptions = (i18nLanguage: string) => Object.fromEntries(
|
||||
@@ -476,7 +489,12 @@ const TableDesigner: React.FC<{ tab: TabData; embedded?: boolean }> = ({ tab, em
|
||||
const [isCommentModalOpen, setIsCommentModalOpen] = useState(false);
|
||||
const [commentEditorColumnKey, setCommentEditorColumnKey] = useState('');
|
||||
const [commentEditorColumnName, setCommentEditorColumnName] = useState('');
|
||||
const [commentEditorColumnType, setCommentEditorColumnType] = useState('');
|
||||
const [commentEditorValue, setCommentEditorValue] = useState('');
|
||||
const [columnDefaultEnabled, setColumnDefaultEnabled] = useState(false);
|
||||
const [columnDefaultValue, setColumnDefaultValue] = useState('');
|
||||
const [columnCharset, setColumnCharset] = useState<string | undefined>();
|
||||
const [columnCollation, setColumnCollation] = useState<string | undefined>();
|
||||
const [inlineCommentEditingKey, setInlineCommentEditingKey] = useState('');
|
||||
|
||||
const connections = useStore(state => state.connections);
|
||||
@@ -515,7 +533,12 @@ const TableDesigner: React.FC<{ tab: TabData; embedded?: boolean }> = ({ tab, em
|
||||
setInlineCommentEditingKey('');
|
||||
setCommentEditorColumnKey(record._key);
|
||||
setCommentEditorColumnName(record.name || '');
|
||||
setCommentEditorColumnType(record.type || '');
|
||||
setCommentEditorValue(record.comment || '');
|
||||
setColumnDefaultEnabled(record.hasDefault === true);
|
||||
setColumnDefaultValue(record.default ?? '');
|
||||
setColumnCharset(record.charset);
|
||||
setColumnCollation(record.collation);
|
||||
setIsCommentModalOpen(true);
|
||||
}, []);
|
||||
|
||||
@@ -523,7 +546,12 @@ const TableDesigner: React.FC<{ tab: TabData; embedded?: boolean }> = ({ tab, em
|
||||
setIsCommentModalOpen(false);
|
||||
setCommentEditorColumnKey('');
|
||||
setCommentEditorColumnName('');
|
||||
setCommentEditorColumnType('');
|
||||
setCommentEditorValue('');
|
||||
setColumnDefaultEnabled(false);
|
||||
setColumnDefaultValue('');
|
||||
setColumnCharset(undefined);
|
||||
setColumnCollation(undefined);
|
||||
}, []);
|
||||
|
||||
// 透明 Monaco Editor 主题由 MonacoEditor 包装组件按需注册(含 stickyScroll 不透明背景)
|
||||
@@ -718,11 +746,25 @@ const TableDesigner: React.FC<{ tab: TabData; embedded?: boolean }> = ({ tab, em
|
||||
dataIndex: 'default',
|
||||
key: 'default',
|
||||
width: 180, // Increased default width
|
||||
render: (text: string, record: EditableColumn) => readOnly ? text : (
|
||||
renderDesignerCellField(
|
||||
<AutoComplete options={COMMON_DEFAULTS} value={text} onChange={val => handleColumnChange(record._key, 'default', val)} style={{ width: '100%' }} variant="borderless" placeholder="NULL" />
|
||||
)
|
||||
)
|
||||
render: (text: string | undefined, record: EditableColumn) => {
|
||||
const value = record.hasDefault
|
||||
? (text === '' ? "''" : (text ?? ''))
|
||||
: undefined;
|
||||
if (readOnly) return value;
|
||||
return renderDesignerCellField(
|
||||
<AutoComplete
|
||||
options={COMMON_DEFAULTS}
|
||||
value={value}
|
||||
onChange={val => {
|
||||
const hasDefault = val.length > 0;
|
||||
handleColumnChange(record._key, 'default', hasDefault ? (val === "''" ? '' : val) : undefined);
|
||||
handleColumnChange(record._key, 'hasDefault', hasDefault);
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
variant="borderless"
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: renderDesignerHeaderTitle(t('table_designer.column.comment', undefined, i18nLanguage)),
|
||||
@@ -754,7 +796,7 @@ const TableDesigner: React.FC<{ tab: TabData; embedded?: boolean }> = ({ tab, em
|
||||
variant="borderless"
|
||||
/>
|
||||
)}
|
||||
<Tooltip title={t('table_designer.tooltip.edit_comment_popup', undefined, i18nLanguage)}>
|
||||
<Tooltip title={t('table_designer.tooltip.edit_column_options', undefined, i18nLanguage)}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
@@ -773,7 +815,7 @@ const TableDesigner: React.FC<{ tab: TabData; embedded?: boolean }> = ({ tab, em
|
||||
onHeaderCell: () => ({ className: 'table-designer-action-column' }),
|
||||
render: (_: any, record: EditableColumn) => (
|
||||
<div className="table-designer-action-cell">
|
||||
<Tooltip title={t('table_designer.tooltip.edit_comment_popup', undefined, i18nLanguage)}>
|
||||
<Tooltip title={t('table_designer.tooltip.edit_column_options', undefined, i18nLanguage)}>
|
||||
<Button type="text" size="small" icon={<EditOutlined />} onClick={() => openCommentEditor(record)} />
|
||||
</Tooltip>
|
||||
<Tooltip title={t('table_designer.action.delete', undefined, i18nLanguage)}>
|
||||
@@ -1310,12 +1352,43 @@ ${selectedTrigger.statement}`;
|
||||
newCol.nullable = 'NO';
|
||||
newCol.type = 'int'; // Suggest INT
|
||||
}
|
||||
if (field === 'type' && getDbType() === 'mysql' && !isMySQLCharacterColumnType(String(value))) {
|
||||
newCol.charset = undefined;
|
||||
newCol.collation = undefined;
|
||||
}
|
||||
return newCol;
|
||||
}
|
||||
return col;
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSaveColumnOptions = () => {
|
||||
if (!commentEditorColumnKey) {
|
||||
closeCommentEditor();
|
||||
return;
|
||||
}
|
||||
|
||||
const isMySQL = getDbType() === 'mysql';
|
||||
const supportsCharacterOptions = isMySQL && isMySQLCharacterColumnType(commentEditorColumnType);
|
||||
const hasDefault = columnDefaultEnabled && (
|
||||
columnDefaultValue.length > 0 || isMySQLCharacterColumnType(commentEditorColumnType)
|
||||
);
|
||||
setColumns(prev => prev.map(col => {
|
||||
if (col._key !== commentEditorColumnKey) return col;
|
||||
return {
|
||||
...col,
|
||||
comment: commentEditorValue,
|
||||
hasDefault,
|
||||
default: hasDefault ? columnDefaultValue : undefined,
|
||||
...(isMySQL ? {
|
||||
charset: supportsCharacterOptions ? columnCharset : undefined,
|
||||
collation: supportsCharacterOptions ? columnCollation : undefined,
|
||||
} : {}),
|
||||
};
|
||||
}));
|
||||
closeCommentEditor();
|
||||
};
|
||||
|
||||
const createNewColumn = useCallback((indexHint: number): EditableColumn => ({
|
||||
name: isNewTable ? 'new_column' : `new_col_${indexHint}`,
|
||||
type: 'varchar(255)',
|
||||
@@ -1323,7 +1396,8 @@ ${selectedTrigger.statement}`;
|
||||
key: '',
|
||||
extra: '',
|
||||
comment: '',
|
||||
default: '',
|
||||
default: undefined,
|
||||
hasDefault: false,
|
||||
_key: `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
isNew: true,
|
||||
isAutoIncrement: false
|
||||
@@ -3429,28 +3503,65 @@ END;`;
|
||||
|
||||
<Modal
|
||||
title={commentEditorColumnName
|
||||
? t('table_designer.modal.column_comment_title_named', { name: commentEditorColumnName }, i18nLanguage)
|
||||
: t('table_designer.modal.column_comment_title', undefined, i18nLanguage)}
|
||||
? t('table_designer.modal.column_options_title_named', { name: commentEditorColumnName }, i18nLanguage)
|
||||
: t('table_designer.modal.column_options_title', undefined, i18nLanguage)}
|
||||
open={isCommentModalOpen}
|
||||
onCancel={closeCommentEditor}
|
||||
onOk={() => {
|
||||
if (commentEditorColumnKey) {
|
||||
handleColumnChange(commentEditorColumnKey, 'comment', commentEditorValue);
|
||||
}
|
||||
closeCommentEditor();
|
||||
}}
|
||||
onOk={handleSaveColumnOptions}
|
||||
okText={t('table_designer.action.apply', undefined, i18nLanguage)}
|
||||
cancelText={t('table_designer.action.cancel', undefined, i18nLanguage)}
|
||||
width={640}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Input.TextArea
|
||||
value={commentEditorValue}
|
||||
onChange={(e) => setCommentEditorValue(e.target.value)}
|
||||
autoSize={{ minRows: 8, maxRows: 18 }}
|
||||
placeholder={t('table_designer.placeholder.column_comment', undefined, i18nLanguage)}
|
||||
maxLength={2000}
|
||||
/>
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||
<Checkbox
|
||||
checked={columnDefaultEnabled}
|
||||
onChange={(event) => setColumnDefaultEnabled(event.target.checked)}
|
||||
>
|
||||
{t('table_designer.column.enable_default', undefined, i18nLanguage)}
|
||||
</Checkbox>
|
||||
<AutoComplete
|
||||
options={COMMON_DEFAULTS}
|
||||
value={columnDefaultValue}
|
||||
onChange={setColumnDefaultValue}
|
||||
disabled={!columnDefaultEnabled}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('table_designer.placeholder.column_default', undefined, i18nLanguage)}
|
||||
/>
|
||||
</Space>
|
||||
{getDbType() === 'mysql' && isMySQLCharacterColumnType(commentEditorColumnType) && (
|
||||
<Space wrap size={12} style={{ width: '100%' }}>
|
||||
<AutoComplete
|
||||
allowClear
|
||||
value={columnCharset}
|
||||
onChange={(value) => {
|
||||
setColumnCharset(value || undefined);
|
||||
const options = value ? (COLLATIONS as any)[value] : undefined;
|
||||
setColumnCollation(options?.[0]?.value);
|
||||
}}
|
||||
options={charsetOptions}
|
||||
placeholder={t('table_designer.column.charset', undefined, i18nLanguage)}
|
||||
style={{ width: 180 }}
|
||||
/>
|
||||
<AutoComplete
|
||||
allowClear
|
||||
value={columnCollation}
|
||||
onChange={(value) => setColumnCollation(value || undefined)}
|
||||
options={columnCharset ? (collationOptions as any)[columnCharset] || [] : []}
|
||||
placeholder={t('table_designer.column.collation', undefined, i18nLanguage)}
|
||||
style={{ width: 260 }}
|
||||
/>
|
||||
</Space>
|
||||
)}
|
||||
<Input.TextArea
|
||||
value={commentEditorValue}
|
||||
onChange={(e) => setCommentEditorValue(e.target.value)}
|
||||
autoSize={{ minRows: 5, maxRows: 12 }}
|
||||
placeholder={t('table_designer.placeholder.column_comment', undefined, i18nLanguage)}
|
||||
maxLength={2000}
|
||||
/>
|
||||
</Space>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
|
||||
@@ -15,7 +15,16 @@ const tableDesignerColumnI18nKeys = [
|
||||
'table_designer.column.default',
|
||||
'table_designer.column.comment',
|
||||
'table_designer.column.actions',
|
||||
'table_designer.tooltip.edit_comment_popup',
|
||||
'table_designer.tooltip.edit_column_options',
|
||||
] as const;
|
||||
|
||||
const tableDesignerColumnOptionI18nKeys = [
|
||||
'table_designer.column.charset',
|
||||
'table_designer.column.collation',
|
||||
'table_designer.column.enable_default',
|
||||
'table_designer.modal.column_options_title',
|
||||
'table_designer.modal.column_options_title_named',
|
||||
'table_designer.placeholder.column_default',
|
||||
] as const;
|
||||
|
||||
const tableDesignerSqlPreviewChangeKeys = [
|
||||
@@ -158,6 +167,15 @@ describe('TableDesignerSqlPreview', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps TableDesigner column option labels in i18n catalogs', () => {
|
||||
for (const localeFile of sharedI18nLocaleFiles) {
|
||||
const catalog = JSON.parse(readFileSync(new URL(localeFile, sharedI18nDir), 'utf8')) as Record<string, string>;
|
||||
for (const key of tableDesignerColumnOptionI18nKeys) {
|
||||
expect(catalog[key], `${localeFile} ${key}`).toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('does not ship corrupted table designer i18n catalog strings', () => {
|
||||
const badLocalizedValuePattern = /\?{2,}|\uFFFD|f\?r|verf\?gbar|Schl\?ssel|Zuf\?llige|Schreibgesch\?tzt|OLAP \?|\{\{count\}\} \?/;
|
||||
const badValues: string[] = [];
|
||||
|
||||
@@ -123,7 +123,11 @@ export const sortSidebarTableEntries = <T extends SidebarTableEntryForSort>(
|
||||
): T[] => {
|
||||
const pinnedKeys = options.pinnedSidebarTables || [];
|
||||
const accessCount = options.tableAccessCount || {};
|
||||
const compareByName = (a: T, b: T) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase());
|
||||
const compareByName = (a: T, b: T) => a.displayName.localeCompare(
|
||||
b.displayName,
|
||||
undefined,
|
||||
{ numeric: true, sensitivity: 'base' },
|
||||
);
|
||||
const compareWithinPinnedGroup = (a: T, b: T) => {
|
||||
if (options.sortBy === 'frequency') {
|
||||
const countA = readTableAccessCount(
|
||||
|
||||
@@ -36,15 +36,20 @@ const translateEn = (key: string, params?: Record<string, string | number | bool
|
||||
catalogTranslate('en-US', key, params);
|
||||
|
||||
const baseColumn = (overrides: Partial<EditableColumnSnapshot>): EditableColumnSnapshot => ({
|
||||
_key: overrides._key || 'col',
|
||||
name: overrides.name || 'id',
|
||||
type: overrides.type || 'int',
|
||||
nullable: overrides.nullable || 'NO',
|
||||
default: overrides.default || '',
|
||||
extra: overrides.extra || '',
|
||||
comment: overrides.comment || '',
|
||||
key: overrides.key || '',
|
||||
isAutoIncrement: overrides.isAutoIncrement || false,
|
||||
_key: overrides._key ?? 'col',
|
||||
name: overrides.name ?? 'id',
|
||||
type: overrides.type ?? 'int',
|
||||
nullable: overrides.nullable ?? 'NO',
|
||||
default: Object.prototype.hasOwnProperty.call(overrides, 'default') ? overrides.default : undefined,
|
||||
hasDefault: Object.prototype.hasOwnProperty.call(overrides, 'hasDefault')
|
||||
? overrides.hasDefault
|
||||
: overrides.default !== undefined && overrides.default !== null && String(overrides.default).trim().length > 0,
|
||||
extra: overrides.extra ?? '',
|
||||
comment: overrides.comment ?? '',
|
||||
key: overrides.key ?? '',
|
||||
charset: overrides.charset,
|
||||
collation: overrides.collation,
|
||||
isAutoIncrement: overrides.isAutoIncrement ?? false,
|
||||
});
|
||||
|
||||
const buildInput = (overrides: Partial<BuildAlterTablePreviewInput>): BuildAlterTablePreviewInput => ({
|
||||
@@ -155,6 +160,157 @@ describe('tableDesignerSchemaSql', () => {
|
||||
expect(sql).toContain('AFTER `id`');
|
||||
});
|
||||
|
||||
it('preserves explicit MySQL default states and expressions', () => {
|
||||
const columns = [
|
||||
baseColumn({ _key: 'none', name: 'none_value', type: 'varchar(16)', nullable: 'YES', hasDefault: false }),
|
||||
baseColumn({ _key: 'null', name: 'null_value', type: 'varchar(16)', nullable: 'YES', default: 'NULL', hasDefault: true }),
|
||||
baseColumn({ _key: 'empty', name: 'empty_value', type: 'varchar(16)', nullable: 'YES', default: '', hasDefault: true }),
|
||||
baseColumn({ _key: 'zero', name: 'zero_value', type: 'int', nullable: 'YES', default: '0', hasDefault: true }),
|
||||
baseColumn({ _key: 'text', name: 'text_value', type: 'varchar(16)', nullable: 'YES', default: 'active', hasDefault: true }),
|
||||
baseColumn({ _key: 'time', name: 'time_value', type: 'timestamp(6)', nullable: 'NO', default: 'CURRENT_TIMESTAMP(6)', hasDefault: true }),
|
||||
baseColumn({ _key: 'bit', name: 'bit_value', type: 'bit(1)', nullable: 'NO', default: "b'0'", hasDefault: true }),
|
||||
baseColumn({ _key: 'hex', name: 'hex_value', type: 'binary(1)', nullable: 'NO', default: '0x00', hasDefault: true }),
|
||||
baseColumn({ _key: 'uuid', name: 'uuid_value', type: 'binary(16)', nullable: 'NO', default: '(uuid_to_bin(uuid()))', hasDefault: true }),
|
||||
];
|
||||
|
||||
const sql = buildCreateTablePreviewSql({
|
||||
tableName: 'defaults_test',
|
||||
dbType: 'mysql',
|
||||
columns,
|
||||
});
|
||||
|
||||
expect(sql).toContain("`none_value` varchar(16) NULL COMMENT ''");
|
||||
expect(sql).not.toContain('`none_value` varchar(16) NULL DEFAULT');
|
||||
expect(sql).toContain('`null_value` varchar(16) NULL DEFAULT NULL');
|
||||
expect(sql).toContain("`empty_value` varchar(16) NULL DEFAULT ''");
|
||||
expect(sql).toContain('`zero_value` int NULL DEFAULT 0');
|
||||
expect(sql).toContain("`text_value` varchar(16) NULL DEFAULT 'active'");
|
||||
expect(sql).toContain('`time_value` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)');
|
||||
expect(sql).toContain("`bit_value` bit(1) NOT NULL DEFAULT b'0'");
|
||||
expect(sql).toContain('`hex_value` binary(1) NOT NULL DEFAULT 0x00');
|
||||
expect(sql).toContain('`uuid_value` binary(16) NOT NULL DEFAULT (uuid_to_bin(uuid()))');
|
||||
});
|
||||
|
||||
it('does not generate an empty string default for non-character columns', () => {
|
||||
const sql = buildCreateTablePreviewSql({
|
||||
tableName: 'invalid_default',
|
||||
dbType: 'mysql',
|
||||
columns: [baseColumn({
|
||||
_key: 'count',
|
||||
name: 'count',
|
||||
type: 'int',
|
||||
nullable: 'NO',
|
||||
default: '',
|
||||
hasDefault: true,
|
||||
})],
|
||||
});
|
||||
|
||||
expect(sql).not.toContain('DEFAULT');
|
||||
});
|
||||
|
||||
it('detects changes between no default and an explicit empty string default', () => {
|
||||
const original = baseColumn({
|
||||
_key: 'status',
|
||||
name: 'status',
|
||||
type: 'varchar(16)',
|
||||
default: undefined,
|
||||
hasDefault: false,
|
||||
});
|
||||
const changed = baseColumn({
|
||||
...original,
|
||||
default: '',
|
||||
hasDefault: true,
|
||||
});
|
||||
|
||||
const sql = buildAlterTablePreviewSql(buildInput({
|
||||
dbType: 'mysql',
|
||||
originalColumns: [original],
|
||||
columns: [changed],
|
||||
}));
|
||||
|
||||
expect(sql).toContain("MODIFY COLUMN `status` varchar(16) NOT NULL DEFAULT ''");
|
||||
});
|
||||
|
||||
it('generates MySQL character options only for character columns', () => {
|
||||
const createSql = buildCreateTablePreviewSql({
|
||||
tableName: 'column_options',
|
||||
dbType: 'mysql',
|
||||
columns: [
|
||||
baseColumn({
|
||||
_key: 'name',
|
||||
name: 'name',
|
||||
type: 'varchar(64)',
|
||||
charset: 'utf8mb4',
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
}),
|
||||
baseColumn({
|
||||
_key: 'count',
|
||||
name: 'count',
|
||||
type: 'int',
|
||||
charset: 'utf8mb4',
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(createSql).toContain('`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci');
|
||||
expect(createSql).not.toContain('`count` int CHARACTER SET');
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: 'charset',
|
||||
change: { charset: 'utf8' },
|
||||
expected: 'CHARACTER SET utf8 COLLATE utf8mb4_general_ci',
|
||||
},
|
||||
{
|
||||
label: 'collation',
|
||||
change: { collation: 'utf8mb4_unicode_ci' },
|
||||
expected: 'CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci',
|
||||
},
|
||||
])('detects a MySQL $label change without extending mysql-family dialects', ({ change, expected }) => {
|
||||
const original = baseColumn({
|
||||
_key: 'name',
|
||||
name: 'name',
|
||||
type: 'varchar(64)',
|
||||
charset: 'utf8mb4',
|
||||
collation: 'utf8mb4_general_ci',
|
||||
});
|
||||
const changed = baseColumn({ ...original, ...change });
|
||||
|
||||
const mysqlSql = buildAlterTablePreviewSql(buildInput({
|
||||
dbType: 'mysql',
|
||||
originalColumns: [original],
|
||||
columns: [changed],
|
||||
}));
|
||||
const mariadbSql = buildAlterTablePreviewSql(buildInput({
|
||||
dbType: 'mariadb',
|
||||
originalColumns: [original],
|
||||
columns: [changed],
|
||||
}));
|
||||
|
||||
expect(mysqlSql).toContain(`MODIFY COLUMN \`name\` varchar(64) ${expected}`);
|
||||
expect(mariadbSql).toBe('');
|
||||
});
|
||||
|
||||
it('filters MySQL DEFAULT_GENERATED metadata while preserving valid extras', () => {
|
||||
const sql = buildCreateTablePreviewSql({
|
||||
tableName: 'generated_defaults',
|
||||
dbType: 'mysql',
|
||||
columns: [baseColumn({
|
||||
_key: 'updated_at',
|
||||
name: 'updated_at',
|
||||
type: 'timestamp',
|
||||
default: 'CURRENT_TIMESTAMP',
|
||||
hasDefault: true,
|
||||
extra: 'DEFAULT_GENERATED on update CURRENT_TIMESTAMP',
|
||||
})],
|
||||
});
|
||||
|
||||
expect(sql).toContain('DEFAULT CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP');
|
||||
expect(sql).not.toContain('DEFAULT_GENERATED');
|
||||
});
|
||||
|
||||
it('builds kingbase alter preview without mysql-only syntax', () => {
|
||||
const sql = buildAlterTablePreviewSql(buildInput({
|
||||
dbType: 'kingbase',
|
||||
|
||||
@@ -21,9 +21,12 @@ export interface EditableColumnSnapshot {
|
||||
type: string;
|
||||
nullable: string;
|
||||
default?: string | null;
|
||||
hasDefault?: boolean;
|
||||
extra?: string;
|
||||
comment?: string;
|
||||
key?: string;
|
||||
charset?: string;
|
||||
collation?: string;
|
||||
isAutoIncrement?: boolean;
|
||||
}
|
||||
|
||||
@@ -117,13 +120,30 @@ const quoteIdentifierPath = (path: string, dbType: string): string => quoteSqlId
|
||||
|
||||
const normalizeDefaultText = (value: unknown): string => String(value ?? '').trim();
|
||||
|
||||
const isKnownDefaultExpression = (trimmed: string): boolean => {
|
||||
const hasDefaultValue = (column: EditableColumnSnapshot): boolean => (
|
||||
typeof column.hasDefault === 'boolean'
|
||||
? column.hasDefault
|
||||
: column.default !== undefined && column.default !== null && normalizeDefaultText(column.default).length > 0
|
||||
);
|
||||
|
||||
const defaultDefinitionChanged = (curr: EditableColumnSnapshot, orig: EditableColumnSnapshot): boolean => (
|
||||
hasDefaultValue(curr) !== hasDefaultValue(orig) ||
|
||||
(hasDefaultValue(curr) && normalizeDefaultText(curr.default) !== normalizeDefaultText(orig.default))
|
||||
);
|
||||
|
||||
const isMySqlCharacterColumnType = (columnType: string): boolean => (
|
||||
/^(?:char|varchar|tinytext|text|mediumtext|longtext|enum|set|nchar|nvarchar)\b/i.test(String(columnType || '').trim())
|
||||
);
|
||||
|
||||
const isKnownDefaultExpression = (trimmed: string, dbType: string): boolean => {
|
||||
if (!trimmed) return false;
|
||||
if (/^N?'.*'$/i.test(trimmed)) return true;
|
||||
if (dbType === 'mysql' && /^(?:b'[01]+'|0b[01]+|x'[0-9a-f]+'|0x[0-9a-f]+)$/i.test(trimmed)) return true;
|
||||
if (/^-?\d+(\.\d+)?$/.test(trimmed)) return true;
|
||||
if (/^(true|false|null)$/i.test(trimmed)) return true;
|
||||
if (/^(current_timestamp|current_date|current_time|localtimestamp|sysdate|systimestamp)$/i.test(trimmed)) return true;
|
||||
if (/^(current_timestamp|current_date|current_time|localtimestamp|sysdate|systimestamp)(?:\s*\(\s*\d+\s*\))?$/i.test(trimmed)) return true;
|
||||
if (/^(now|uuid|newid|sysdatetime)\s*\(\s*\)$/i.test(trimmed)) return true;
|
||||
if (dbType === 'mysql' && /^\([\s\S]+\)$/.test(trimmed)) return true;
|
||||
if (/^nextval\s*\(/i.test(trimmed) || /::/.test(trimmed)) return true;
|
||||
return false;
|
||||
};
|
||||
@@ -131,10 +151,10 @@ const isKnownDefaultExpression = (trimmed: string): boolean => {
|
||||
const formatDefaultExpression = (value: unknown, dbType: string): string => {
|
||||
const trimmed = normalizeDefaultText(value);
|
||||
if (!trimmed) return '';
|
||||
if (isKnownDefaultExpression(trimmed)) {
|
||||
if (isKnownDefaultExpression(trimmed, dbType)) {
|
||||
if (/^(true|false|null)$/i.test(trimmed)) return trimmed.toUpperCase();
|
||||
if (/^(current_timestamp|current_date|current_time|localtimestamp|sysdate|systimestamp)$/i.test(trimmed)) {
|
||||
return trimmed.toUpperCase();
|
||||
if (/^(current_timestamp|current_date|current_time|localtimestamp|sysdate|systimestamp)(?:\s*\(\s*\d+\s*\))?$/i.test(trimmed)) {
|
||||
return trimmed.toUpperCase().replace(/\s+/g, '');
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
@@ -142,40 +162,61 @@ const formatDefaultExpression = (value: unknown, dbType: string): string => {
|
||||
return `${prefix}'${escapeSqlString(trimmed)}'`;
|
||||
};
|
||||
|
||||
const buildDefaultSql = (value: unknown, dbType: string): string => {
|
||||
const defaultValue = normalizeDefaultText(value);
|
||||
if (!defaultValue) return '';
|
||||
const formatEnabledDefaultExpression = (column: EditableColumnSnapshot, dbType: string): string => {
|
||||
const defaultValue = normalizeDefaultText(column.default);
|
||||
return defaultValue ? formatDefaultExpression(defaultValue, dbType) : "''";
|
||||
};
|
||||
|
||||
const buildDefaultSql = (column: EditableColumnSnapshot, dbType: string): string => {
|
||||
if (!hasDefaultValue(column)) return '';
|
||||
const defaultValue = normalizeDefaultText(column.default);
|
||||
if (!defaultValue) {
|
||||
return dbType !== 'mysql' || isMySqlCharacterColumnType(column.type) ? "DEFAULT ''" : '';
|
||||
}
|
||||
return `DEFAULT ${formatDefaultExpression(defaultValue, dbType)}`;
|
||||
};
|
||||
|
||||
const definitionChanged = (curr: EditableColumnSnapshot, orig: EditableColumnSnapshot): boolean => (
|
||||
const definitionChanged = (
|
||||
curr: EditableColumnSnapshot,
|
||||
orig: EditableColumnSnapshot,
|
||||
includeCharacterOptions = false,
|
||||
): boolean => (
|
||||
curr.type !== orig.type ||
|
||||
curr.nullable !== orig.nullable ||
|
||||
normalizeDefaultText(curr.default) !== normalizeDefaultText(orig.default) ||
|
||||
defaultDefinitionChanged(curr, orig) ||
|
||||
(curr.comment || '') !== (orig.comment || '') ||
|
||||
(includeCharacterOptions && (curr.charset || '') !== (orig.charset || '')) ||
|
||||
(includeCharacterOptions && (curr.collation || '') !== (orig.collation || '')) ||
|
||||
Boolean(curr.isAutoIncrement) !== Boolean(orig.isAutoIncrement)
|
||||
);
|
||||
|
||||
const physicalDefinitionChanged = (curr: EditableColumnSnapshot, orig: EditableColumnSnapshot): boolean => (
|
||||
curr.type !== orig.type ||
|
||||
curr.nullable !== orig.nullable ||
|
||||
normalizeDefaultText(curr.default) !== normalizeDefaultText(orig.default) ||
|
||||
defaultDefinitionChanged(curr, orig) ||
|
||||
Boolean(curr.isAutoIncrement) !== Boolean(orig.isAutoIncrement)
|
||||
);
|
||||
|
||||
const buildMySqlColumnDefinition = (column: EditableColumnSnapshot, dbType: string): string => {
|
||||
let extra = String(column.extra || '').trim();
|
||||
let extra = String(column.extra || '').replace(/\bDEFAULT_GENERATED\b/gi, '').replace(/\s+/g, ' ').trim();
|
||||
if (column.isAutoIncrement) {
|
||||
if (!extra.toLowerCase().includes('auto_increment')) {
|
||||
extra = `${extra} AUTO_INCREMENT`.trim();
|
||||
}
|
||||
} else {
|
||||
extra = extra.replace(/auto_increment/gi, '').trim();
|
||||
extra = extra.replace(/auto_increment/gi, '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
const defaultSql = buildDefaultSql(column.default, dbType);
|
||||
const defaultSql = buildDefaultSql(column, dbType);
|
||||
const characterOptionsSql = dbType === 'mysql' && isMySqlCharacterColumnType(column.type)
|
||||
? [
|
||||
column.charset ? `CHARACTER SET ${column.charset}` : '',
|
||||
column.collation ? `COLLATE ${column.collation}` : '',
|
||||
].filter(Boolean).join(' ')
|
||||
: '';
|
||||
return [
|
||||
quoteIdentifierPart(column.name, dbType),
|
||||
String(column.type || '').trim(),
|
||||
characterOptionsSql,
|
||||
column.nullable === 'NO' ? 'NOT NULL' : 'NULL',
|
||||
defaultSql,
|
||||
extra,
|
||||
@@ -196,7 +237,7 @@ const DORIS_AGG_TYPES = new Set([
|
||||
]);
|
||||
|
||||
const buildDorisColumnDefinition = (column: EditableColumnSnapshot, dbType: string): string => {
|
||||
const defaultSql = buildDefaultSql(column.default, dbType);
|
||||
const defaultSql = buildDefaultSql(column, dbType);
|
||||
const autoIncrementSql = column.isAutoIncrement ? 'AUTO_INCREMENT' : '';
|
||||
const keyText = String(column.key || '').trim().toUpperCase();
|
||||
const extraText = String(column.extra || '').trim().toUpperCase();
|
||||
@@ -215,7 +256,7 @@ const buildDorisColumnDefinition = (column: EditableColumnSnapshot, dbType: stri
|
||||
};
|
||||
|
||||
const buildStarRocksColumnDefinition = (column: EditableColumnSnapshot): string => {
|
||||
const defaultSql = buildDefaultSql(column.default, 'starrocks');
|
||||
const defaultSql = buildDefaultSql(column, 'starrocks');
|
||||
const extraText = String(column.extra || '').trim().toUpperCase();
|
||||
const aggregateSql = DORIS_AGG_TYPES.has(extraText) ? extraText : '';
|
||||
return [
|
||||
@@ -241,7 +282,7 @@ const buildStandardColumnDefinition = (
|
||||
parts.push('GENERATED BY DEFAULT AS IDENTITY');
|
||||
}
|
||||
}
|
||||
const defaultSql = buildDefaultSql(column.default, dbType);
|
||||
const defaultSql = buildDefaultSql(column, dbType);
|
||||
if (defaultSql) parts.push(defaultSql);
|
||||
if (column.nullable === 'NO') {
|
||||
parts.push('NOT NULL');
|
||||
@@ -253,7 +294,7 @@ const buildStandardColumnDefinition = (
|
||||
|
||||
const buildPgLikeColumnDefinition = (column: EditableColumnSnapshot, dbType: string): string => {
|
||||
const parts = [quoteIdentifierPart(column.name, dbType), String(column.type || '').trim()];
|
||||
const defaultSql = buildDefaultSql(column.default, dbType);
|
||||
const defaultSql = buildDefaultSql(column, dbType);
|
||||
if (defaultSql) parts.push(defaultSql);
|
||||
if (column.nullable === 'NO') parts.push('NOT NULL');
|
||||
return parts.join(' ').trim();
|
||||
@@ -307,7 +348,7 @@ const buildMySqlAlterPreviewSql = (input: BuildAlterTablePreviewInput, dbType: s
|
||||
return;
|
||||
}
|
||||
|
||||
if (definitionChanged(curr, orig)) {
|
||||
if (definitionChanged(curr, orig, dbType === 'mysql')) {
|
||||
alters.push(`MODIFY COLUMN ${colDef} ${positionSql}`.trim());
|
||||
}
|
||||
});
|
||||
@@ -397,11 +438,11 @@ const buildPgLikeAlterPreviewSql = (input: BuildAlterTablePreviewInput, dbType:
|
||||
statements.push(`ALTER TABLE ${tableRef}\nALTER COLUMN ${quoteIdentifierPart(currentName, dbType)} TYPE ${curr.type};`);
|
||||
}
|
||||
|
||||
const currDefault = normalizeDefaultText(curr.default);
|
||||
const origDefault = normalizeDefaultText(orig.default);
|
||||
if (currDefault !== origDefault) {
|
||||
if (currDefault) {
|
||||
statements.push(`ALTER TABLE ${tableRef}\nALTER COLUMN ${quoteIdentifierPart(currentName, dbType)} SET DEFAULT ${formatDefaultExpression(currDefault, dbType)};`);
|
||||
const currHasDefault = hasDefaultValue(curr);
|
||||
const origHasDefault = hasDefaultValue(orig);
|
||||
if (currHasDefault !== origHasDefault || (currHasDefault && normalizeDefaultText(curr.default) !== normalizeDefaultText(orig.default))) {
|
||||
if (currHasDefault) {
|
||||
statements.push(`ALTER TABLE ${tableRef}\nALTER COLUMN ${quoteIdentifierPart(currentName, dbType)} SET DEFAULT ${formatEnabledDefaultExpression(curr, dbType)};`);
|
||||
} else {
|
||||
statements.push(`ALTER TABLE ${tableRef}\nALTER COLUMN ${quoteIdentifierPart(currentName, dbType)} DROP DEFAULT;`);
|
||||
}
|
||||
@@ -518,15 +559,15 @@ const buildSqlServerAlterPreviewSql = (input: BuildAlterTablePreviewInput): stri
|
||||
}
|
||||
|
||||
if (curr.type !== orig.type || curr.nullable !== orig.nullable || Boolean(curr.isAutoIncrement) !== Boolean(orig.isAutoIncrement)) {
|
||||
statements.push(`ALTER TABLE ${tableRef}\nALTER COLUMN ${buildStandardColumnDefinition({ ...curr, name: currentName, default: '' }, dbType, { includeNull: true, includeIdentity: false })};`);
|
||||
statements.push(`ALTER TABLE ${tableRef}\nALTER COLUMN ${buildStandardColumnDefinition({ ...curr, name: currentName, default: undefined, hasDefault: false }, dbType, { includeNull: true, includeIdentity: false })};`);
|
||||
}
|
||||
|
||||
const currDefault = normalizeDefaultText(curr.default);
|
||||
const origDefault = normalizeDefaultText(orig.default);
|
||||
if (currDefault !== origDefault) {
|
||||
const currHasDefault = hasDefaultValue(curr);
|
||||
const origHasDefault = hasDefaultValue(orig);
|
||||
if (currHasDefault !== origHasDefault || (currHasDefault && normalizeDefaultText(curr.default) !== normalizeDefaultText(orig.default))) {
|
||||
statements.push(buildSqlServerDefaultDropBatch(input.tableName, currentName));
|
||||
if (currDefault) {
|
||||
statements.push(`ALTER TABLE ${tableRef}\nADD DEFAULT ${formatDefaultExpression(currDefault, dbType)} FOR ${quoteIdentifierPart(currentName, dbType)};`);
|
||||
if (currHasDefault) {
|
||||
statements.push(`ALTER TABLE ${tableRef}\nADD DEFAULT ${formatEnabledDefaultExpression(curr, dbType)} FOR ${quoteIdentifierPart(currentName, dbType)};`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,11 +653,11 @@ const buildDuckDbAlterPreviewSql = (input: BuildAlterTablePreviewInput): string
|
||||
if (curr.type !== orig.type) {
|
||||
statements.push(`ALTER TABLE ${tableRef}\nALTER COLUMN ${quoteIdentifierPart(currentName, dbType)} SET DATA TYPE ${curr.type};`);
|
||||
}
|
||||
const currDefault = normalizeDefaultText(curr.default);
|
||||
const origDefault = normalizeDefaultText(orig.default);
|
||||
if (currDefault !== origDefault) {
|
||||
if (currDefault) {
|
||||
statements.push(`ALTER TABLE ${tableRef}\nALTER COLUMN ${quoteIdentifierPart(currentName, dbType)} SET DEFAULT ${formatDefaultExpression(currDefault, dbType)};`);
|
||||
const currHasDefault = hasDefaultValue(curr);
|
||||
const origHasDefault = hasDefaultValue(orig);
|
||||
if (currHasDefault !== origHasDefault || (currHasDefault && normalizeDefaultText(curr.default) !== normalizeDefaultText(orig.default))) {
|
||||
if (currHasDefault) {
|
||||
statements.push(`ALTER TABLE ${tableRef}\nALTER COLUMN ${quoteIdentifierPart(currentName, dbType)} SET DEFAULT ${formatEnabledDefaultExpression(curr, dbType)};`);
|
||||
} else {
|
||||
statements.push(`ALTER TABLE ${tableRef}\nALTER COLUMN ${quoteIdentifierPart(currentName, dbType)} DROP DEFAULT;`);
|
||||
}
|
||||
@@ -663,7 +704,7 @@ const buildLimitedBacktickAlterPreviewSql = (input: BuildAlterTablePreviewInput,
|
||||
const orig = input.originalColumns.find((col) => col._key === curr._key);
|
||||
if (!orig) {
|
||||
statements.push(`ALTER TABLE ${tableRef}\nADD COLUMN ${quoteIdentifierPart(curr.name, dbType)} ${curr.type};`);
|
||||
if (curr.nullable === 'NO' || normalizeDefaultText(curr.default) || String(curr.comment || '').trim()) {
|
||||
if (curr.nullable === 'NO' || hasDefaultValue(curr) || String(curr.comment || '').trim()) {
|
||||
statements.push(translateSchemaSqlComment(input.translate, 'table_designer.schema_sql.limited_column_hint', {
|
||||
dialect: label,
|
||||
}));
|
||||
@@ -681,7 +722,7 @@ const buildLimitedBacktickAlterPreviewSql = (input: BuildAlterTablePreviewInput,
|
||||
}
|
||||
if (
|
||||
curr.nullable !== orig.nullable ||
|
||||
normalizeDefaultText(curr.default) !== normalizeDefaultText(orig.default) ||
|
||||
defaultDefinitionChanged(curr, orig) ||
|
||||
(curr.comment || '') !== (orig.comment || '') ||
|
||||
Boolean(curr.isAutoIncrement) !== Boolean(orig.isAutoIncrement)
|
||||
) {
|
||||
|
||||
@@ -393,8 +393,11 @@ export interface ColumnDefinition {
|
||||
nullable: string;
|
||||
key: string;
|
||||
default?: string;
|
||||
hasDefault?: boolean;
|
||||
extra: string;
|
||||
comment: string;
|
||||
charset?: string;
|
||||
collation?: string;
|
||||
}
|
||||
|
||||
export interface IndexDefinition {
|
||||
|
||||
@@ -75,6 +75,57 @@ describe('columnDefinition metadata normalization', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves an explicitly enabled empty string default', () => {
|
||||
expect(normalizeColumnDefinition({
|
||||
Name: 'status',
|
||||
Type: 'varchar(32)',
|
||||
Default: '',
|
||||
HasDefault: true,
|
||||
})).toMatchObject({
|
||||
default: '',
|
||||
hasDefault: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('respects an explicitly disabled default', () => {
|
||||
expect(normalizeColumnDefinition({
|
||||
name: 'status',
|
||||
type: 'varchar(32)',
|
||||
default: 'active',
|
||||
hasDefault: false,
|
||||
})).toMatchObject({
|
||||
default: undefined,
|
||||
hasDefault: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to legacy non-empty defaults when hasDefault is absent', () => {
|
||||
expect(normalizeColumnDefinition({ name: 'status', type: 'varchar(32)', default: 'active' })).toMatchObject({
|
||||
default: 'active',
|
||||
hasDefault: true,
|
||||
});
|
||||
expect(normalizeColumnDefinition({ name: 'status', type: 'varchar(32)', default: '' })).toMatchObject({
|
||||
default: undefined,
|
||||
hasDefault: false,
|
||||
});
|
||||
expect(normalizeColumnDefinition({ name: 'status', type: 'varchar(32)', default: null })).toMatchObject({
|
||||
default: undefined,
|
||||
hasDefault: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizes charset and collation metadata aliases', () => {
|
||||
expect(normalizeColumnDefinition({
|
||||
COLUMN_NAME: 'status',
|
||||
DATA_TYPE: 'varchar',
|
||||
CHARACTER_SET_NAME: 'utf8mb4',
|
||||
COLLATION_NAME: 'utf8mb4_unicode_ci',
|
||||
})).toMatchObject({
|
||||
charset: 'utf8mb4',
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
});
|
||||
});
|
||||
|
||||
it('maps boolean primary and unique metadata aliases to GoNavi keys', () => {
|
||||
expect(getColumnDefinitionKey({ column_name: 'id', isPrimary: true })).toBe('PRI');
|
||||
expect(getColumnDefinitionKey({ column_name: 'id', primary_key: 't' })).toBe('PRI');
|
||||
|
||||
@@ -162,10 +162,23 @@ export const getColumnDefinitionDefault = (column: unknown): string => (
|
||||
);
|
||||
|
||||
export const hasColumnDefinitionDefault = (column: unknown): boolean => {
|
||||
const explicit = readProperty(column, ['hasDefault', 'HasDefault', 'HAS_DEFAULT', 'has_default']);
|
||||
if (explicit !== undefined && explicit !== null) {
|
||||
return readBooleanProperty(column, ['hasDefault', 'HasDefault', 'HAS_DEFAULT', 'has_default']);
|
||||
}
|
||||
|
||||
const raw = readProperty(column, ['default', 'Default', 'COLUMN_DEFAULT', 'column_default', 'DATA_DEFAULT', 'data_default']);
|
||||
return raw !== undefined && raw !== null;
|
||||
return raw !== undefined && raw !== null && String(raw).length > 0;
|
||||
};
|
||||
|
||||
export const getColumnDefinitionCharset = (column: unknown): string => (
|
||||
readStringProperty(column, ['charset', 'Charset', 'CHARACTER_SET_NAME', 'character_set_name'])
|
||||
);
|
||||
|
||||
export const getColumnDefinitionCollation = (column: unknown): string => (
|
||||
readStringProperty(column, ['collation', 'Collation', 'COLLATION_NAME', 'collation_name'])
|
||||
);
|
||||
|
||||
export const getColumnDefinitionComment = (column: unknown): string => (
|
||||
readStringProperty(column, ['comment', 'Comment', 'COMMENTS', 'comments', 'COLUMN_COMMENT', 'column_comment'])
|
||||
);
|
||||
@@ -180,8 +193,11 @@ export const normalizeColumnDefinition = (column: unknown): ColumnDefinition =>
|
||||
nullable: getColumnDefinitionNullable(column),
|
||||
key: getColumnDefinitionKey(column),
|
||||
default: hasDefault ? getColumnDefinitionDefault(column) : undefined,
|
||||
hasDefault,
|
||||
extra: getColumnDefinitionExtra(column),
|
||||
comment: getColumnDefinitionComment(column),
|
||||
charset: getColumnDefinitionCharset(column) || undefined,
|
||||
collation: getColumnDefinitionCollation(column) || undefined,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -47,6 +47,44 @@ describe('tableOverviewFilter', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('sorts table names in natural numeric order', () => {
|
||||
const indexed = buildTableOverviewSearchIndex([
|
||||
{ name: 'table_11', comment: '', rows: 0, dataSize: 0, indexSize: 0 },
|
||||
{ name: 'table_2', comment: '', rows: 0, dataSize: 0, indexSize: 0 },
|
||||
{ name: 'table_10', comment: '', rows: 0, dataSize: 0, indexSize: 0 },
|
||||
{ name: 'table_1', comment: '', rows: 0, dataSize: 0, indexSize: 0 },
|
||||
]);
|
||||
|
||||
expect(filterAndSortTableOverviewRows(indexed, '', 'name', 'asc').map((item) => item.name)).toEqual([
|
||||
'table_1',
|
||||
'table_2',
|
||||
'table_10',
|
||||
'table_11',
|
||||
]);
|
||||
expect(filterAndSortTableOverviewRows(indexed, '', 'name', 'desc').map((item) => item.name)).toEqual([
|
||||
'table_11',
|
||||
'table_10',
|
||||
'table_2',
|
||||
'table_1',
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses natural table name order when other sort values match', () => {
|
||||
const indexed = buildTableOverviewSearchIndex([
|
||||
{ name: 'table_10', comment: 'same', rows: 1, dataSize: 0, indexSize: 0 },
|
||||
{ name: 'table_2', comment: 'same', rows: 1, dataSize: 0, indexSize: 0 },
|
||||
]);
|
||||
|
||||
expect(filterAndSortTableOverviewRows(indexed, '', 'rows', 'desc').map((item) => item.name)).toEqual([
|
||||
'table_2',
|
||||
'table_10',
|
||||
]);
|
||||
expect(filterAndSortTableOverviewRows(indexed, '', 'comment', 'desc').map((item) => item.name)).toEqual([
|
||||
'table_2',
|
||||
'table_10',
|
||||
]);
|
||||
});
|
||||
|
||||
it('sorts compact table numeric columns while keeping unknown values last', () => {
|
||||
const indexed = buildTableOverviewSearchIndex([
|
||||
{ name: 'unknown', comment: '', rows: -1, dataSize: -1, indexSize: -1 },
|
||||
|
||||
@@ -28,6 +28,9 @@ export interface TableOverviewSearchIndexItem<T extends TableOverviewFilterRow>
|
||||
sortName: string;
|
||||
}
|
||||
|
||||
const compareTableNames = (left: string, right: string): number =>
|
||||
left.localeCompare(right, undefined, { numeric: true, sensitivity: 'base' });
|
||||
|
||||
export const buildTableOverviewSearchIndex = <T extends TableOverviewFilterRow>(
|
||||
rows: T[],
|
||||
): TableOverviewSearchIndexItem<T>[] => rows.map((row) => ({
|
||||
@@ -49,7 +52,7 @@ export const filterAndSortTableOverviewRows = <T extends TableOverviewFilterRow>
|
||||
|
||||
matched.sort((a, b) => {
|
||||
if (sortField === 'name') {
|
||||
const cmp = a.sortName.localeCompare(b.sortName);
|
||||
const cmp = compareTableNames(a.sortName, b.sortName);
|
||||
return sortOrder === 'asc' ? cmp : -cmp;
|
||||
}
|
||||
|
||||
@@ -62,7 +65,7 @@ export const filterAndSortTableOverviewRows = <T extends TableOverviewFilterRow>
|
||||
if (!leftUnknown && left !== right) {
|
||||
return sortOrder === 'asc' ? left - right : right - left;
|
||||
}
|
||||
return a.sortName.localeCompare(b.sortName);
|
||||
return compareTableNames(a.sortName, b.sortName);
|
||||
}
|
||||
|
||||
const left = String(a.row[sortField] || '').trim();
|
||||
@@ -70,11 +73,11 @@ export const filterAndSortTableOverviewRows = <T extends TableOverviewFilterRow>
|
||||
if (!left || !right) {
|
||||
if (!left && right) return 1;
|
||||
if (left && !right) return -1;
|
||||
return a.sortName.localeCompare(b.sortName);
|
||||
return compareTableNames(a.sortName, b.sortName);
|
||||
}
|
||||
const cmp = left.localeCompare(right, undefined, { numeric: true, sensitivity: 'base' });
|
||||
if (cmp !== 0) return sortOrder === 'asc' ? cmp : -cmp;
|
||||
return a.sortName.localeCompare(b.sortName);
|
||||
return compareTableNames(a.sortName, b.sortName);
|
||||
});
|
||||
|
||||
return matched.map((item) => item.row);
|
||||
|
||||
@@ -197,13 +197,16 @@ type DatabaseObject struct {
|
||||
|
||||
// ColumnDefinition 描述表的一个列定义。
|
||||
type ColumnDefinition struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Nullable string `json:"nullable"` // YES/NO
|
||||
Key string `json:"key"` // PRI, UNI, MUL
|
||||
Default *string `json:"default"`
|
||||
Extra string `json:"extra"` // auto_increment
|
||||
Comment string `json:"comment"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Nullable string `json:"nullable"` // YES/NO
|
||||
Key string `json:"key"` // PRI, UNI, MUL
|
||||
Default *string `json:"default"`
|
||||
HasDefault bool `json:"hasDefault,omitempty"`
|
||||
Extra string `json:"extra"` // auto_increment
|
||||
Comment string `json:"comment"`
|
||||
Charset string `json:"charset,omitempty"`
|
||||
Collation string `json:"collation,omitempty"`
|
||||
}
|
||||
|
||||
// IndexDefinition 描述表的一个索引定义。
|
||||
|
||||
@@ -1089,29 +1089,40 @@ func (m *MySQLDB) GetCreateStatement(dbName, tableName string) (string, error) {
|
||||
return "", localizedDatabaseRuntimeError("db.backend.error.create_table_statement_not_found", nil)
|
||||
}
|
||||
|
||||
func buildMySQLColumnDefinition(row map[string]interface{}) connection.ColumnDefinition {
|
||||
col := connection.ColumnDefinition{
|
||||
Name: fmt.Sprintf("%v", row["Field"]),
|
||||
Type: fmt.Sprintf("%v", row["Type"]),
|
||||
Nullable: fmt.Sprintf("%v", row["Null"]),
|
||||
Key: fmt.Sprintf("%v", row["Key"]),
|
||||
Extra: fmt.Sprintf("%v", row["Extra"]),
|
||||
Comment: fmt.Sprintf("%v", row["Comment"]),
|
||||
}
|
||||
|
||||
if row["Default"] != nil {
|
||||
defaultValue := fmt.Sprintf("%v", row["Default"])
|
||||
col.Default = &defaultValue
|
||||
col.HasDefault = true
|
||||
}
|
||||
if row["Collation"] != nil {
|
||||
col.Collation = fmt.Sprintf("%v", row["Collation"])
|
||||
if separator := strings.IndexByte(col.Collation, '_'); separator > 0 {
|
||||
col.Charset = col.Collation[:separator]
|
||||
}
|
||||
}
|
||||
|
||||
return col
|
||||
}
|
||||
|
||||
func (m *MySQLDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefinition, error) {
|
||||
data, _, err := m.Query(buildMySQLShowFullColumnsQuery(dbName, tableName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var columns []connection.ColumnDefinition
|
||||
columns := make([]connection.ColumnDefinition, 0, len(data))
|
||||
for _, row := range data {
|
||||
col := connection.ColumnDefinition{
|
||||
Name: fmt.Sprintf("%v", row["Field"]),
|
||||
Type: fmt.Sprintf("%v", row["Type"]),
|
||||
Nullable: fmt.Sprintf("%v", row["Null"]),
|
||||
Key: fmt.Sprintf("%v", row["Key"]),
|
||||
Extra: fmt.Sprintf("%v", row["Extra"]),
|
||||
Comment: fmt.Sprintf("%v", row["Comment"]),
|
||||
}
|
||||
|
||||
if row["Default"] != nil {
|
||||
d := fmt.Sprintf("%v", row["Default"])
|
||||
col.Default = &d
|
||||
}
|
||||
|
||||
columns = append(columns, col)
|
||||
columns = append(columns, buildMySQLColumnDefinition(row))
|
||||
}
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
@@ -144,6 +144,68 @@ func TestCollectMySQLDatabaseNames_FallsBackToInformationSchemaSchemata(t *testi
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMySQLColumnDefinitionPreservesDefaultAndCollation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
row map[string]interface{}
|
||||
wantDefault *string
|
||||
wantHasDefault bool
|
||||
wantCharset string
|
||||
wantCollation string
|
||||
}{
|
||||
{
|
||||
name: "empty string default",
|
||||
row: map[string]interface{}{
|
||||
"Field": "nickname", "Type": "varchar(64)", "Null": "YES", "Key": "",
|
||||
"Default": "", "Extra": "", "Comment": "", "Collation": "utf8mb4_unicode_ci",
|
||||
},
|
||||
wantDefault: stringPointer(""),
|
||||
wantHasDefault: true,
|
||||
wantCharset: "utf8mb4",
|
||||
wantCollation: "utf8mb4_unicode_ci",
|
||||
},
|
||||
{
|
||||
name: "ordinary default",
|
||||
row: map[string]interface{}{
|
||||
"Field": "status", "Type": "varchar(16)", "Null": "NO", "Key": "",
|
||||
"Default": "active", "Extra": "", "Comment": "", "Collation": "utf8_general_ci",
|
||||
},
|
||||
wantDefault: stringPointer("active"),
|
||||
wantHasDefault: true,
|
||||
wantCharset: "utf8",
|
||||
wantCollation: "utf8_general_ci",
|
||||
},
|
||||
{
|
||||
name: "no default or collation",
|
||||
row: map[string]interface{}{
|
||||
"Field": "id", "Type": "bigint", "Null": "NO", "Key": "PRI",
|
||||
"Default": nil, "Extra": "auto_increment", "Comment": "", "Collation": nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := buildMySQLColumnDefinition(tt.row)
|
||||
if !reflect.DeepEqual(got.Default, tt.wantDefault) {
|
||||
t.Fatalf("default got=%v want=%v", got.Default, tt.wantDefault)
|
||||
}
|
||||
if got.HasDefault != tt.wantHasDefault {
|
||||
t.Fatalf("hasDefault got=%v want=%v", got.HasDefault, tt.wantHasDefault)
|
||||
}
|
||||
if got.Charset != tt.wantCharset || got.Collation != tt.wantCollation {
|
||||
t.Fatalf("charset/collation got=%q/%q want=%q/%q", got.Charset, got.Collation, tt.wantCharset, tt.wantCollation)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func stringPointer(value string) *string {
|
||||
return &value
|
||||
}
|
||||
|
||||
func TestBuildMySQLShowCreateTableQueryNormalizesQuotedIdentifiers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -8026,8 +8026,11 @@
|
||||
"table_designer.action.view_statement": "Anweisung anzeigen",
|
||||
"table_designer.column.actions": "Aktionen",
|
||||
"table_designer.column.auto_increment": "Auto-Inkrement",
|
||||
"table_designer.column.charset": "Zeichensatz",
|
||||
"table_designer.column.collation": "Sortierung",
|
||||
"table_designer.column.comment": "Kommentar",
|
||||
"table_designer.column.default": "Standardwert",
|
||||
"table_designer.column.enable_default": "Standardwert festlegen",
|
||||
"table_designer.column.name": "Name",
|
||||
"table_designer.column.not_null": "NOT NULL",
|
||||
"table_designer.column.primary_key": "Primärschlüssel",
|
||||
@@ -8115,6 +8118,8 @@
|
||||
"table_designer.message.trigger_updated": "Trigger aktualisiert",
|
||||
"table_designer.modal.column_comment_title": "Spaltenkommentar",
|
||||
"table_designer.modal.column_comment_title_named": "Spaltenkommentar - {{name}}",
|
||||
"table_designer.modal.column_options_title": "Spaltenoptionen",
|
||||
"table_designer.modal.column_options_title_named": "Spaltenoptionen - {{name}}",
|
||||
"table_designer.modal.confirm_sql_title": "SQL-Änderungen bestätigen",
|
||||
"table_designer.modal.copy_columns_title": "Ausgewählte Spalten in neue Tabelle kopieren",
|
||||
"table_designer.modal.delete_foreign_key_content": "Fremdschlüssel-Constraint \"{{name}}\" löschen?",
|
||||
@@ -8145,6 +8150,7 @@
|
||||
"table_designer.option.default": "Standard",
|
||||
"table_designer.option.recommended_suffix": "(Empfohlen)",
|
||||
"table_designer.placeholder.column_comment": "Spaltenkommentar eingeben",
|
||||
"table_designer.placeholder.column_default": "Standardwert oder Ausdruck eingeben",
|
||||
"table_designer.placeholder.foreign_key_name": "Name des Fremdschlüssel-Constraints, z. B. fk_order_user",
|
||||
"table_designer.placeholder.index_columns": "Indexspalten auswählen; die Auswahlreihenfolge wird verwendet",
|
||||
"table_designer.placeholder.index_name": "Indexname, z. B. idx_user_name",
|
||||
@@ -8211,6 +8217,7 @@
|
||||
"table_designer.title.default_database": "Standarddatenbank",
|
||||
"table_designer.title.schema_designer": "Schema-Designer",
|
||||
"table_designer.title.untitled_table": "Unbenannte Tabelle",
|
||||
"table_designer.tooltip.edit_column_options": "Spaltenoptionen bearbeiten",
|
||||
"table_designer.tooltip.edit_comment_popup": "Kommentar im Dialog bearbeiten",
|
||||
"table_designer.trigger.column.event": "Ereignis",
|
||||
"table_designer.trigger.column.name": "Name",
|
||||
|
||||
@@ -8026,8 +8026,11 @@
|
||||
"table_designer.action.view_statement": "View statement",
|
||||
"table_designer.column.actions": "Actions",
|
||||
"table_designer.column.auto_increment": "Auto increment",
|
||||
"table_designer.column.charset": "Character set",
|
||||
"table_designer.column.collation": "Collation",
|
||||
"table_designer.column.comment": "Comment",
|
||||
"table_designer.column.default": "Default",
|
||||
"table_designer.column.enable_default": "Set default value",
|
||||
"table_designer.column.name": "Name",
|
||||
"table_designer.column.not_null": "Not NULL",
|
||||
"table_designer.column.primary_key": "Primary key",
|
||||
@@ -8115,6 +8118,8 @@
|
||||
"table_designer.message.trigger_updated": "Trigger updated",
|
||||
"table_designer.modal.column_comment_title": "Column comment",
|
||||
"table_designer.modal.column_comment_title_named": "Column comment - {{name}}",
|
||||
"table_designer.modal.column_options_title": "Column options",
|
||||
"table_designer.modal.column_options_title_named": "Column options - {{name}}",
|
||||
"table_designer.modal.confirm_sql_title": "Confirm SQL changes",
|
||||
"table_designer.modal.copy_columns_title": "Copy selected columns to new table",
|
||||
"table_designer.modal.delete_foreign_key_content": "Delete foreign key constraint \"{{name}}\"?",
|
||||
@@ -8145,6 +8150,7 @@
|
||||
"table_designer.option.default": "Default",
|
||||
"table_designer.option.recommended_suffix": "(Recommended)",
|
||||
"table_designer.placeholder.column_comment": "Enter a column comment",
|
||||
"table_designer.placeholder.column_default": "Enter a default value or expression",
|
||||
"table_designer.placeholder.foreign_key_name": "Foreign key constraint name, for example fk_order_user",
|
||||
"table_designer.placeholder.index_columns": "Select index columns; selection order is used",
|
||||
"table_designer.placeholder.index_name": "Index name, for example idx_user_name",
|
||||
@@ -8211,6 +8217,7 @@
|
||||
"table_designer.title.default_database": "Default database",
|
||||
"table_designer.title.schema_designer": "Schema designer",
|
||||
"table_designer.title.untitled_table": "Untitled table",
|
||||
"table_designer.tooltip.edit_column_options": "Edit column options",
|
||||
"table_designer.tooltip.edit_comment_popup": "Edit comment in popup",
|
||||
"table_designer.trigger.column.event": "Event",
|
||||
"table_designer.trigger.column.name": "Name",
|
||||
|
||||
@@ -8026,8 +8026,11 @@
|
||||
"table_designer.action.view_statement": "文を表示",
|
||||
"table_designer.column.actions": "操作",
|
||||
"table_designer.column.auto_increment": "自動採番",
|
||||
"table_designer.column.charset": "文字セット",
|
||||
"table_designer.column.collation": "照合順序",
|
||||
"table_designer.column.comment": "コメント",
|
||||
"table_designer.column.default": "デフォルト",
|
||||
"table_designer.column.enable_default": "デフォルト値を設定",
|
||||
"table_designer.column.name": "名前",
|
||||
"table_designer.column.not_null": "NOT NULL",
|
||||
"table_designer.column.primary_key": "主キー",
|
||||
@@ -8115,6 +8118,8 @@
|
||||
"table_designer.message.trigger_updated": "トリガーを更新しました",
|
||||
"table_designer.modal.column_comment_title": "列コメント",
|
||||
"table_designer.modal.column_comment_title_named": "列コメント - {{name}}",
|
||||
"table_designer.modal.column_options_title": "列オプション",
|
||||
"table_designer.modal.column_options_title_named": "列オプション - {{name}}",
|
||||
"table_designer.modal.confirm_sql_title": "SQL 変更の確認",
|
||||
"table_designer.modal.copy_columns_title": "選択列を新しいテーブルへコピー",
|
||||
"table_designer.modal.delete_foreign_key_content": "外部キー制約 \"{{name}}\" を削除しますか?",
|
||||
@@ -8145,6 +8150,7 @@
|
||||
"table_designer.option.default": "デフォルト",
|
||||
"table_designer.option.recommended_suffix": "(推奨)",
|
||||
"table_designer.placeholder.column_comment": "列コメントを入力してください",
|
||||
"table_designer.placeholder.column_default": "デフォルト値または式を入力してください",
|
||||
"table_designer.placeholder.foreign_key_name": "外部キー制約名(例: fk_order_user)",
|
||||
"table_designer.placeholder.index_columns": "インデックス列を選択してください。選択順が使われます",
|
||||
"table_designer.placeholder.index_name": "インデックス名(例: idx_user_name)",
|
||||
@@ -8211,6 +8217,7 @@
|
||||
"table_designer.title.default_database": "既定データベース",
|
||||
"table_designer.title.schema_designer": "スキーマデザイナー",
|
||||
"table_designer.title.untitled_table": "未命名テーブル",
|
||||
"table_designer.tooltip.edit_column_options": "列オプションを編集",
|
||||
"table_designer.tooltip.edit_comment_popup": "ポップアップでコメントを編集",
|
||||
"table_designer.trigger.column.event": "イベント",
|
||||
"table_designer.trigger.column.name": "名前",
|
||||
|
||||
@@ -8026,8 +8026,11 @@
|
||||
"table_designer.action.view_statement": "Показать оператор",
|
||||
"table_designer.column.actions": "Действия",
|
||||
"table_designer.column.auto_increment": "Автоинкремент",
|
||||
"table_designer.column.charset": "Кодировка",
|
||||
"table_designer.column.collation": "Правило сортировки",
|
||||
"table_designer.column.comment": "Комментарий",
|
||||
"table_designer.column.default": "По умолчанию",
|
||||
"table_designer.column.enable_default": "Задать значение по умолчанию",
|
||||
"table_designer.column.name": "Имя",
|
||||
"table_designer.column.not_null": "NOT NULL",
|
||||
"table_designer.column.primary_key": "Первичный ключ",
|
||||
@@ -8115,6 +8118,8 @@
|
||||
"table_designer.message.trigger_updated": "Триггер обновлен",
|
||||
"table_designer.modal.column_comment_title": "Комментарий столбца",
|
||||
"table_designer.modal.column_comment_title_named": "Комментарий столбца - {{name}}",
|
||||
"table_designer.modal.column_options_title": "Параметры столбца",
|
||||
"table_designer.modal.column_options_title_named": "Параметры столбца - {{name}}",
|
||||
"table_designer.modal.confirm_sql_title": "Подтверждение изменений SQL",
|
||||
"table_designer.modal.copy_columns_title": "Скопировать выбранные столбцы в новую таблицу",
|
||||
"table_designer.modal.delete_foreign_key_content": "Удалить ограничение внешнего ключа \"{{name}}\"?",
|
||||
@@ -8145,6 +8150,7 @@
|
||||
"table_designer.option.default": "По умолчанию",
|
||||
"table_designer.option.recommended_suffix": "(рекомендуется)",
|
||||
"table_designer.placeholder.column_comment": "Введите комментарий столбца",
|
||||
"table_designer.placeholder.column_default": "Введите значение или выражение по умолчанию",
|
||||
"table_designer.placeholder.foreign_key_name": "Имя ограничения внешнего ключа, например fk_order_user",
|
||||
"table_designer.placeholder.index_columns": "Выберите столбцы индекса; используется порядок выбора",
|
||||
"table_designer.placeholder.index_name": "Имя индекса, например idx_user_name",
|
||||
@@ -8211,6 +8217,7 @@
|
||||
"table_designer.title.default_database": "База данных по умолчанию",
|
||||
"table_designer.title.schema_designer": "Конструктор схемы",
|
||||
"table_designer.title.untitled_table": "Таблица без имени",
|
||||
"table_designer.tooltip.edit_column_options": "Изменить параметры столбца",
|
||||
"table_designer.tooltip.edit_comment_popup": "Редактировать комментарий во всплывающем окне",
|
||||
"table_designer.trigger.column.event": "Событие",
|
||||
"table_designer.trigger.column.name": "Имя",
|
||||
|
||||
@@ -8026,8 +8026,11 @@
|
||||
"table_designer.action.view_statement": "查看语句",
|
||||
"table_designer.column.actions": "操作",
|
||||
"table_designer.column.auto_increment": "自增",
|
||||
"table_designer.column.charset": "字符集",
|
||||
"table_designer.column.collation": "排序规则",
|
||||
"table_designer.column.comment": "注释",
|
||||
"table_designer.column.default": "默认",
|
||||
"table_designer.column.enable_default": "设置默认值",
|
||||
"table_designer.column.name": "名",
|
||||
"table_designer.column.not_null": "不是 NULL",
|
||||
"table_designer.column.primary_key": "主键",
|
||||
@@ -8115,6 +8118,8 @@
|
||||
"table_designer.message.trigger_updated": "触发器修改成功",
|
||||
"table_designer.modal.column_comment_title": "字段注释",
|
||||
"table_designer.modal.column_comment_title_named": "字段注释 - {{name}}",
|
||||
"table_designer.modal.column_options_title": "字段选项",
|
||||
"table_designer.modal.column_options_title_named": "字段选项 - {{name}}",
|
||||
"table_designer.modal.confirm_sql_title": "确认 SQL 变更",
|
||||
"table_designer.modal.copy_columns_title": "复制选中字段到新表",
|
||||
"table_designer.modal.delete_foreign_key_content": "确定删除外键约束 \"{{name}}\" 吗?",
|
||||
@@ -8145,6 +8150,7 @@
|
||||
"table_designer.option.default": "默认",
|
||||
"table_designer.option.recommended_suffix": "(推荐)",
|
||||
"table_designer.placeholder.column_comment": "请输入字段注释",
|
||||
"table_designer.placeholder.column_default": "请输入默认值或表达式",
|
||||
"table_designer.placeholder.foreign_key_name": "外键约束名(例如 fk_order_user)",
|
||||
"table_designer.placeholder.index_columns": "请选择索引字段(按选择顺序生效)",
|
||||
"table_designer.placeholder.index_name": "索引名(例如 idx_user_name)",
|
||||
@@ -8211,6 +8217,7 @@
|
||||
"table_designer.title.default_database": "默认库",
|
||||
"table_designer.title.schema_designer": "结构设计器",
|
||||
"table_designer.title.untitled_table": "未命名表",
|
||||
"table_designer.tooltip.edit_column_options": "编辑字段选项",
|
||||
"table_designer.tooltip.edit_comment_popup": "弹框编辑注释",
|
||||
"table_designer.trigger.column.event": "事件",
|
||||
"table_designer.trigger.column.name": "名称",
|
||||
|
||||
@@ -8026,8 +8026,11 @@
|
||||
"table_designer.action.view_statement": "檢視語句",
|
||||
"table_designer.column.actions": "操作",
|
||||
"table_designer.column.auto_increment": "自增",
|
||||
"table_designer.column.charset": "字元集",
|
||||
"table_designer.column.collation": "排序規則",
|
||||
"table_designer.column.comment": "註解",
|
||||
"table_designer.column.default": "預設",
|
||||
"table_designer.column.enable_default": "設定預設值",
|
||||
"table_designer.column.name": "名稱",
|
||||
"table_designer.column.not_null": "不是 NULL",
|
||||
"table_designer.column.primary_key": "主鍵",
|
||||
@@ -8115,6 +8118,8 @@
|
||||
"table_designer.message.trigger_updated": "觸發器修改成功",
|
||||
"table_designer.modal.column_comment_title": "欄位註解",
|
||||
"table_designer.modal.column_comment_title_named": "欄位註解 - {{name}}",
|
||||
"table_designer.modal.column_options_title": "欄位選項",
|
||||
"table_designer.modal.column_options_title_named": "欄位選項 - {{name}}",
|
||||
"table_designer.modal.confirm_sql_title": "確認 SQL 變更",
|
||||
"table_designer.modal.copy_columns_title": "複製選取欄位到新表",
|
||||
"table_designer.modal.delete_foreign_key_content": "確定要刪除外鍵約束 \"{{name}}\" 嗎?",
|
||||
@@ -8145,6 +8150,7 @@
|
||||
"table_designer.option.default": "預設",
|
||||
"table_designer.option.recommended_suffix": "(建議)",
|
||||
"table_designer.placeholder.column_comment": "請輸入欄位註解",
|
||||
"table_designer.placeholder.column_default": "請輸入預設值或運算式",
|
||||
"table_designer.placeholder.foreign_key_name": "外鍵約束名稱(例如 fk_order_user)",
|
||||
"table_designer.placeholder.index_columns": "請選擇索引欄位(依選取順序生效)",
|
||||
"table_designer.placeholder.index_name": "索引名稱(例如 idx_user_name)",
|
||||
@@ -8211,6 +8217,7 @@
|
||||
"table_designer.title.default_database": "預設庫",
|
||||
"table_designer.title.schema_designer": "結構設計器",
|
||||
"table_designer.title.untitled_table": "未命名表",
|
||||
"table_designer.tooltip.edit_column_options": "編輯欄位選項",
|
||||
"table_designer.tooltip.edit_comment_popup": "在彈窗中編輯註解",
|
||||
"table_designer.trigger.column.event": "事件",
|
||||
"table_designer.trigger.column.name": "名稱",
|
||||
|
||||
Reference in New Issue
Block a user