🐛 fix(table-designer): fix table sorting and column options (#705) (#709)

## 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:
Syngnat
2026-07-24 18:03:33 +08:00
committed by GitHub
20 changed files with 673 additions and 100 deletions

View File

@@ -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'),

View File

@@ -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

View File

@@ -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[] = [];

View File

@@ -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(

View File

@@ -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',

View File

@@ -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)
) {

View File

@@ -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 {

View File

@@ -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');

View File

@@ -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,
};
};

View File

@@ -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 },

View File

@@ -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);

View File

@@ -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 描述表的一个索引定义。

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "名前",

View File

@@ -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": "Имя",

View File

@@ -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": "名称",

View File

@@ -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": "名稱",