mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-01 20:19:33 +08:00
🐛 fix(sqlserver): 修复表 DDL 与索引创建语句生成
- DDL:为 SQL Server 表结构补充 CREATE TABLE fallback 生成 - 索引:在已有索引选择和新增索引弹窗中展示 CREATE INDEX 语句 - 测试:补充 SQL Server DDL fallback 与索引 SQL 预览回归测试
This commit is contained in:
@@ -9,6 +9,7 @@ import { TabData, ColumnDefinition, IndexDefinition, ForeignKeyDefinition, Trigg
|
||||
import { useStore } from '../store';
|
||||
import { DBGetColumns, DBGetIndexes, DBQuery, DBGetForeignKeys, DBGetTriggers, DBShowCreateTable } from '../../wailsjs/go/app/App';
|
||||
import { hasIndexFormChanged, normalizeIndexFormFromRow, shouldRestoreOriginalIndex, toggleIndexSelection as getNextIndexSelection, type IndexDisplaySnapshot } from './tableDesignerIndexUtils';
|
||||
import { buildIndexCreateSqlPreview } from './tableDesignerIndexSql';
|
||||
import { buildAlterTablePreviewSql, buildCreateTablePreviewSql, hasAlterTableDraftChanges, type StarRocksCreateTableOptions, type StarRocksDistributionType, type StarRocksKeyModel, type StarRocksTableKind } from './tableDesignerSchemaSql';
|
||||
import TableDesignerSqlPreview from './TableDesignerSqlPreview';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
@@ -1771,84 +1772,45 @@ END;`;
|
||||
setIsIndexModalOpen(true);
|
||||
};
|
||||
|
||||
const buildIndexCreateSql = (form: IndexFormState): string | null => {
|
||||
const getIndexCreateSqlResult = (form: IndexFormState) => {
|
||||
const tableInfo = resolveTableInfo();
|
||||
const dbType = tableInfo.dbType;
|
||||
const kind: IndexKind = form.kind || 'NORMAL';
|
||||
const indexName = String(form.name || '').trim();
|
||||
const cleanedCols = form.columnNames.map(col => String(col || '').trim()).filter(Boolean);
|
||||
if (cleanedCols.length === 0) {
|
||||
message.error('请至少选择一个字段');
|
||||
return null;
|
||||
}
|
||||
const colSql = cleanedCols
|
||||
.map(col => quoteIdentifierPartByDialect(col, dbType))
|
||||
.join(', ');
|
||||
|
||||
if (isMysqlLikeDialect(dbType)) {
|
||||
if (kind === 'PRIMARY') {
|
||||
return `ALTER TABLE ${tableInfo.tableRef}\nADD PRIMARY KEY (${colSql});`;
|
||||
}
|
||||
|
||||
if (!indexName) {
|
||||
message.error('请输入索引名');
|
||||
return null;
|
||||
}
|
||||
|
||||
const indexRef = quoteIdentifierPartByDialect(indexName, dbType);
|
||||
if (kind === 'FULLTEXT') {
|
||||
return `ALTER TABLE ${tableInfo.tableRef}\nADD FULLTEXT INDEX ${indexRef} (${colSql});`;
|
||||
}
|
||||
if (kind === 'SPATIAL') {
|
||||
return `ALTER TABLE ${tableInfo.tableRef}\nADD SPATIAL INDEX ${indexRef} (${colSql});`;
|
||||
}
|
||||
|
||||
const normalizedType = String(form.indexType || '').trim().toUpperCase() || 'DEFAULT';
|
||||
if (normalizedType === 'FULLTEXT' || normalizedType === 'SPATIAL') {
|
||||
message.error(`请将“索引类别”切换为 ${normalizedType} 索引`);
|
||||
return null;
|
||||
}
|
||||
const usingSql = normalizedType !== 'DEFAULT' ? ` USING ${normalizedType}` : '';
|
||||
const prefix = kind === 'UNIQUE' ? 'ADD UNIQUE INDEX' : 'ADD INDEX';
|
||||
return `ALTER TABLE ${tableInfo.tableRef}\n${prefix} ${indexRef}${usingSql} (${colSql});`;
|
||||
}
|
||||
|
||||
if (kind === 'PRIMARY' || kind === 'FULLTEXT' || kind === 'SPATIAL') {
|
||||
message.warning('当前数据库仅支持普通索引与唯一索引维护');
|
||||
return null;
|
||||
}
|
||||
if (!indexName) {
|
||||
message.error('请输入索引名');
|
||||
return null;
|
||||
}
|
||||
|
||||
const indexRef = quoteIdentifierPartByDialect(indexName, dbType);
|
||||
const normalizedType = String(form.indexType || '').trim().toUpperCase() || 'DEFAULT';
|
||||
const uniquePrefix = kind === 'UNIQUE' ? 'UNIQUE ' : '';
|
||||
|
||||
if (isPgLikeDialect(dbType)) {
|
||||
const usingSql = normalizedType !== 'DEFAULT' ? ` USING ${normalizedType}` : '';
|
||||
return `CREATE ${uniquePrefix}INDEX ${indexRef} ON ${tableInfo.tableRef}${usingSql} (${colSql});`;
|
||||
}
|
||||
|
||||
if (isSqlServerDialect(dbType)) {
|
||||
const methodSql = normalizedType === 'CLUSTERED' || normalizedType === 'NONCLUSTERED'
|
||||
? `${normalizedType} `
|
||||
: '';
|
||||
return `CREATE ${uniquePrefix}${methodSql}INDEX ${indexRef} ON ${tableInfo.tableRef} (${colSql});`;
|
||||
}
|
||||
|
||||
if (isOracleLikeDialect(dbType) || dbType === 'sqlite') {
|
||||
return `CREATE ${uniquePrefix}INDEX ${indexRef} ON ${tableInfo.tableRef} (${colSql});`;
|
||||
}
|
||||
|
||||
if (isNonRelationalDialect(dbType)) {
|
||||
message.warning('当前数据源不支持关系型索引维护');
|
||||
return null;
|
||||
}
|
||||
return `CREATE ${uniquePrefix}INDEX ${indexRef} ON ${tableInfo.tableRef} (${colSql});`;
|
||||
return buildIndexCreateSqlPreview({
|
||||
dbType: tableInfo.dbType,
|
||||
tableRef: tableInfo.tableRef,
|
||||
name: form.name,
|
||||
columnNames: form.columnNames,
|
||||
kind: form.kind,
|
||||
indexType: form.indexType,
|
||||
});
|
||||
};
|
||||
|
||||
const buildIndexCreateSql = (form: IndexFormState): string | null => {
|
||||
const result = getIndexCreateSqlResult(form);
|
||||
if (!result.sql) {
|
||||
if (result.severity === 'warning') {
|
||||
message.warning(result.message || '无法生成索引创建语句');
|
||||
} else {
|
||||
message.error(result.message || '无法生成索引创建语句');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return result.sql;
|
||||
};
|
||||
|
||||
const indexCreatePreviewSql = useMemo(() => {
|
||||
if (!isIndexModalOpen) return '';
|
||||
const result = getIndexCreateSqlResult(indexForm);
|
||||
return result.sql || `-- ${result.message || '填写索引信息后生成创建语句'}`;
|
||||
}, [connections, indexForm, isIndexModalOpen, tab.connectionId, tab.dbName, tab.tableName]);
|
||||
|
||||
const selectedIndexCreateSql = useMemo(() => {
|
||||
if (!selectedIndex || selectedIndexKeys.length !== 1) return '';
|
||||
const result = getIndexCreateSqlResult(buildIndexFormFromRow(selectedIndex));
|
||||
return result.sql || `-- ${result.message || '无法生成索引创建语句'}`;
|
||||
}, [connections, selectedIndex, selectedIndexKeys.length, tab.connectionId, tab.dbName, tab.tableName]);
|
||||
|
||||
const indexTableHeight = selectedIndexCreateSql ? Math.max(180, tableHeight - 220) : tableHeight;
|
||||
|
||||
const buildIndexDropSql = (indexName: string): string | null => {
|
||||
const tableInfo = resolveTableInfo();
|
||||
const dbType = tableInfo.dbType;
|
||||
@@ -2813,7 +2775,7 @@ END;`;
|
||||
size="small"
|
||||
pagination={false}
|
||||
loading={loading}
|
||||
scroll={{ x: 960, y: tableHeight }}
|
||||
scroll={{ x: 960, y: indexTableHeight }}
|
||||
components={{
|
||||
header: { cell: ResizableTitle },
|
||||
}}
|
||||
@@ -2824,6 +2786,14 @@ END;`;
|
||||
style: { cursor: 'pointer' }
|
||||
})}
|
||||
/>
|
||||
{selectedIndexCreateSql && selectedIndex && (
|
||||
<div style={{ width: '100%' }}>
|
||||
<div style={{ color: '#666', fontSize: 12, marginBottom: 6 }}>
|
||||
创建语句:{selectedIndex.name}
|
||||
</div>
|
||||
<TableDesignerSqlPreview sql={selectedIndexCreateSql} darkMode={darkMode} height="160px" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
@@ -3137,6 +3107,10 @@ END;`;
|
||||
<div style={{ color: '#888', fontSize: 12 }}>
|
||||
修改索引时若新索引创建失败,系统会尝试自动恢复原索引。
|
||||
</div>
|
||||
<div style={{ width: '100%' }}>
|
||||
<div style={{ color: '#666', fontSize: 12, marginBottom: 6 }}>创建语句</div>
|
||||
<TableDesignerSqlPreview sql={indexCreatePreviewSql} darkMode={darkMode} height="180px" />
|
||||
</div>
|
||||
</Space>
|
||||
</Modal>
|
||||
|
||||
|
||||
@@ -124,6 +124,16 @@ describe('TableDesignerSqlPreview', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('detects CREATE INDEX preview lines as create changes', () => {
|
||||
const highlights = resolveSqlChangeHighlights(
|
||||
'CREATE UNIQUE NONCLUSTERED INDEX [IX_Users_Email] ON [dbo].[Users] ([email]);',
|
||||
);
|
||||
|
||||
expect(highlights).toEqual([
|
||||
expect.objectContaining({ kind: 'create', lineNumber: 1, label: '新建索引' }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('adds Monaco decorations to changed SQL lines only', () => {
|
||||
renderToStaticMarkup(
|
||||
<TableDesignerSqlPreview
|
||||
|
||||
@@ -37,6 +37,7 @@ const CHANGE_LINE_RULES: Array<{
|
||||
{ kind: 'modify', label: '字段属性变更', pattern: /\b(MODIFY\s+COLUMN|ALTER\s+COLUMN|SET\s+DATA\s+TYPE|SET\s+DEFAULT|DROP\s+DEFAULT|SET\s+NOT\s+NULL|DROP\s+NOT\s+NULL)\b/i },
|
||||
{ kind: 'constraint', label: '约束变更', pattern: /\b(ADD\s+CONSTRAINT|DROP\s+CONSTRAINT)\b/i },
|
||||
{ kind: 'comment', label: '备注变更', pattern: /\b(COMMENT\s+ON\s+COLUMN|COMMENT\s+ON\s+TABLE)\b/i },
|
||||
{ kind: 'create', label: '新建索引', pattern: /\bCREATE\s+(UNIQUE\s+)?((CLUSTERED|NONCLUSTERED)\s+)?INDEX\b/i },
|
||||
];
|
||||
|
||||
const CREATE_TABLE_PATTERN = /^\s*CREATE\s+TABLE\b/i;
|
||||
|
||||
46
frontend/src/components/tableDesignerIndexSql.test.ts
Normal file
46
frontend/src/components/tableDesignerIndexSql.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { buildIndexCreateSqlPreview } from './tableDesignerIndexSql';
|
||||
|
||||
describe('tableDesignerIndexSql', () => {
|
||||
it('builds SQL Server nonclustered index create SQL', () => {
|
||||
const result = buildIndexCreateSqlPreview({
|
||||
dbType: 'sqlserver',
|
||||
tableRef: '[dbo].[Users]',
|
||||
name: 'IX_Users_DisplayName',
|
||||
columnNames: ['display_name'],
|
||||
kind: 'NORMAL',
|
||||
indexType: 'NONCLUSTERED',
|
||||
});
|
||||
|
||||
expect(result.sql).toBe('CREATE NONCLUSTERED INDEX [IX_Users_DisplayName] ON [dbo].[Users] ([display_name]);');
|
||||
});
|
||||
|
||||
it('builds SQL Server unique clustered index create SQL', () => {
|
||||
const result = buildIndexCreateSqlPreview({
|
||||
dbType: 'mssql',
|
||||
tableRef: '[dbo].[Users]',
|
||||
name: 'IX_Users_Email',
|
||||
columnNames: ['email'],
|
||||
kind: 'UNIQUE',
|
||||
indexType: 'CLUSTERED',
|
||||
});
|
||||
|
||||
expect(result.sql).toBe('CREATE UNIQUE CLUSTERED INDEX [IX_Users_Email] ON [dbo].[Users] ([email]);');
|
||||
});
|
||||
|
||||
it('returns a validation message before an index name is available', () => {
|
||||
const result = buildIndexCreateSqlPreview({
|
||||
dbType: 'sqlserver',
|
||||
tableRef: '[dbo].[Users]',
|
||||
name: '',
|
||||
columnNames: ['display_name'],
|
||||
kind: 'NORMAL',
|
||||
indexType: 'NONCLUSTERED',
|
||||
});
|
||||
|
||||
expect(result.sql).toBeNull();
|
||||
expect(result.severity).toBe('error');
|
||||
expect(result.message).toContain('请输入索引名');
|
||||
});
|
||||
});
|
||||
97
frontend/src/components/tableDesignerIndexSql.ts
Normal file
97
frontend/src/components/tableDesignerIndexSql.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
isMysqlFamilyDialect,
|
||||
isOracleLikeDialect,
|
||||
isPgLikeDialect,
|
||||
isSqlServerDialect,
|
||||
quoteSqlIdentifierPart,
|
||||
} from '../utils/sqlDialect';
|
||||
|
||||
export type TableDesignerIndexKind = 'NORMAL' | 'UNIQUE' | 'PRIMARY' | 'FULLTEXT' | 'SPATIAL';
|
||||
|
||||
export interface BuildIndexCreateSqlInput {
|
||||
dbType: string;
|
||||
tableRef: string;
|
||||
name: string;
|
||||
columnNames: string[];
|
||||
kind: TableDesignerIndexKind;
|
||||
indexType?: string;
|
||||
}
|
||||
|
||||
export interface BuildIndexCreateSqlResult {
|
||||
sql: string | null;
|
||||
message?: string;
|
||||
severity?: 'error' | 'warning';
|
||||
}
|
||||
|
||||
const isNonRelationalDialect = (dbType: string): boolean => dbType === 'redis' || dbType === 'mongodb';
|
||||
|
||||
export const buildIndexCreateSqlPreview = (input: BuildIndexCreateSqlInput): BuildIndexCreateSqlResult => {
|
||||
const dbType = input.dbType;
|
||||
const kind = input.kind || 'NORMAL';
|
||||
const indexName = String(input.name || '').trim();
|
||||
const cleanedCols = input.columnNames.map(col => String(col || '').trim()).filter(Boolean);
|
||||
if (cleanedCols.length === 0) {
|
||||
return { sql: null, message: '请至少选择一个字段', severity: 'error' };
|
||||
}
|
||||
const colSql = cleanedCols
|
||||
.map(col => quoteSqlIdentifierPart(dbType, col))
|
||||
.join(', ');
|
||||
|
||||
if (isMysqlFamilyDialect(dbType)) {
|
||||
if (kind === 'PRIMARY') {
|
||||
return { sql: `ALTER TABLE ${input.tableRef}\nADD PRIMARY KEY (${colSql});` };
|
||||
}
|
||||
|
||||
if (!indexName) {
|
||||
return { sql: null, message: '请输入索引名', severity: 'error' };
|
||||
}
|
||||
|
||||
const indexRef = quoteSqlIdentifierPart(dbType, indexName);
|
||||
if (kind === 'FULLTEXT') {
|
||||
return { sql: `ALTER TABLE ${input.tableRef}\nADD FULLTEXT INDEX ${indexRef} (${colSql});` };
|
||||
}
|
||||
if (kind === 'SPATIAL') {
|
||||
return { sql: `ALTER TABLE ${input.tableRef}\nADD SPATIAL INDEX ${indexRef} (${colSql});` };
|
||||
}
|
||||
|
||||
const normalizedType = String(input.indexType || '').trim().toUpperCase() || 'DEFAULT';
|
||||
if (normalizedType === 'FULLTEXT' || normalizedType === 'SPATIAL') {
|
||||
return { sql: null, message: `请将“索引类别”切换为 ${normalizedType} 索引`, severity: 'error' };
|
||||
}
|
||||
const usingSql = normalizedType !== 'DEFAULT' ? ` USING ${normalizedType}` : '';
|
||||
const prefix = kind === 'UNIQUE' ? 'ADD UNIQUE INDEX' : 'ADD INDEX';
|
||||
return { sql: `ALTER TABLE ${input.tableRef}\n${prefix} ${indexRef}${usingSql} (${colSql});` };
|
||||
}
|
||||
|
||||
if (kind === 'PRIMARY' || kind === 'FULLTEXT' || kind === 'SPATIAL') {
|
||||
return { sql: null, message: '当前数据库仅支持普通索引与唯一索引维护', severity: 'warning' };
|
||||
}
|
||||
if (!indexName) {
|
||||
return { sql: null, message: '请输入索引名', severity: 'error' };
|
||||
}
|
||||
|
||||
const indexRef = quoteSqlIdentifierPart(dbType, indexName);
|
||||
const normalizedType = String(input.indexType || '').trim().toUpperCase() || 'DEFAULT';
|
||||
const uniquePrefix = kind === 'UNIQUE' ? 'UNIQUE ' : '';
|
||||
|
||||
if (isPgLikeDialect(dbType)) {
|
||||
const usingSql = normalizedType !== 'DEFAULT' ? ` USING ${normalizedType}` : '';
|
||||
return { sql: `CREATE ${uniquePrefix}INDEX ${indexRef} ON ${input.tableRef}${usingSql} (${colSql});` };
|
||||
}
|
||||
|
||||
if (isSqlServerDialect(dbType)) {
|
||||
const methodSql = normalizedType === 'CLUSTERED' || normalizedType === 'NONCLUSTERED'
|
||||
? `${normalizedType} `
|
||||
: '';
|
||||
return { sql: `CREATE ${uniquePrefix}${methodSql}INDEX ${indexRef} ON ${input.tableRef} (${colSql});` };
|
||||
}
|
||||
|
||||
if (isOracleLikeDialect(dbType) || dbType === 'sqlite') {
|
||||
return { sql: `CREATE ${uniquePrefix}INDEX ${indexRef} ON ${input.tableRef} (${colSql});` };
|
||||
}
|
||||
|
||||
if (isNonRelationalDialect(dbType)) {
|
||||
return { sql: null, message: '当前数据源不支持关系型索引维护', severity: 'warning' };
|
||||
}
|
||||
return { sql: `CREATE ${uniquePrefix}INDEX ${indexRef} ON ${input.tableRef} (${colSql});` };
|
||||
};
|
||||
@@ -269,6 +269,27 @@ func normalizeSchemaAndTableByType(dbType string, dbName string, tableName strin
|
||||
}
|
||||
}
|
||||
|
||||
func resolveCreateStatementTargets(config connection.ConnectionConfig, dbType string, dbName string, tableName string) (string, string, string, string) {
|
||||
if dbType == "sqlserver" {
|
||||
metadataDB := strings.TrimSpace(dbName)
|
||||
if metadataDB == "" {
|
||||
metadataDB = strings.TrimSpace(config.Database)
|
||||
}
|
||||
rawTable := strings.TrimSpace(tableName)
|
||||
schema, table := db.SplitSQLQualifiedName(rawTable)
|
||||
if table == "" {
|
||||
table = rawTable
|
||||
}
|
||||
if schema == "" {
|
||||
schema = "dbo"
|
||||
}
|
||||
return metadataDB, rawTable, schema, table
|
||||
}
|
||||
|
||||
schema, table := normalizeSchemaAndTableByType(dbType, dbName, tableName)
|
||||
return schema, table, schema, table
|
||||
}
|
||||
|
||||
func quoteTableIdentByType(dbType string, schema string, table string) string {
|
||||
s := strings.TrimSpace(schema)
|
||||
t := strings.TrimSpace(table)
|
||||
@@ -1001,18 +1022,18 @@ func (a *App) DBShowCreateTable(config connection.ConnectionConfig, dbName strin
|
||||
|
||||
func resolveCreateStatementWithFallback(dbInst db.Database, config connection.ConnectionConfig, dbName string, tableName string) (string, error) {
|
||||
dbType := resolveDDLDBType(config)
|
||||
schemaName, pureTableName := normalizeSchemaAndTableByType(dbType, dbName, tableName)
|
||||
if pureTableName == "" {
|
||||
metadataSchemaName, metadataTableName, ddlSchemaName, ddlTableName := resolveCreateStatementTargets(config, dbType, dbName, tableName)
|
||||
if metadataTableName == "" || ddlTableName == "" {
|
||||
return "", fmt.Errorf("表名不能为空")
|
||||
}
|
||||
|
||||
sqlStr, sourceErr := dbInst.GetCreateStatement(schemaName, pureTableName)
|
||||
sqlStr, sourceErr := dbInst.GetCreateStatement(metadataSchemaName, metadataTableName)
|
||||
if sourceErr == nil && !shouldFallbackCreateStatement(dbType, sqlStr) {
|
||||
return sqlStr, nil
|
||||
}
|
||||
|
||||
if supportsViewCreateStatementLookup(dbType) {
|
||||
if viewDDL, ok := tryGetViewCreateStatement(dbInst, config, dbName, schemaName, pureTableName); ok {
|
||||
if viewDDL, ok := tryGetViewCreateStatement(dbInst, config, dbName, ddlSchemaName, ddlTableName); ok {
|
||||
return viewDDL, nil
|
||||
}
|
||||
}
|
||||
@@ -1024,7 +1045,7 @@ func resolveCreateStatementWithFallback(dbInst db.Database, config connection.Co
|
||||
return sqlStr, nil
|
||||
}
|
||||
|
||||
columns, colErr := dbInst.GetColumns(schemaName, pureTableName)
|
||||
columns, colErr := dbInst.GetColumns(metadataSchemaName, metadataTableName)
|
||||
if colErr != nil {
|
||||
if sourceErr != nil {
|
||||
return "", sourceErr
|
||||
@@ -1032,7 +1053,7 @@ func resolveCreateStatementWithFallback(dbInst db.Database, config connection.Co
|
||||
return "", colErr
|
||||
}
|
||||
|
||||
fallbackDDL, buildErr := buildFallbackCreateStatement(dbType, schemaName, pureTableName, columns)
|
||||
fallbackDDL, buildErr := buildFallbackCreateStatement(dbType, ddlSchemaName, ddlTableName, columns)
|
||||
if buildErr != nil {
|
||||
if sourceErr != nil {
|
||||
return "", sourceErr
|
||||
@@ -1044,7 +1065,7 @@ func resolveCreateStatementWithFallback(dbInst db.Database, config connection.Co
|
||||
|
||||
func supportsCreateStatementFallback(dbType string) bool {
|
||||
switch dbType {
|
||||
case "postgres", "kingbase", "highgo", "vastbase", "opengauss":
|
||||
case "postgres", "kingbase", "highgo", "vastbase", "opengauss", "sqlserver":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -1127,6 +1148,9 @@ func buildFallbackCreateStatement(dbType string, schemaName string, tableName st
|
||||
colName := quoteIdentByType(dbType, colNameRaw)
|
||||
defParts := []string{fmt.Sprintf("%s %s", colName, colType)}
|
||||
|
||||
if dbType == "sqlserver" && strings.Contains(strings.ToLower(strings.TrimSpace(col.Extra)), "auto_increment") {
|
||||
defParts = append(defParts, "IDENTITY(1,1)")
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(col.Nullable), "NO") {
|
||||
defParts = append(defParts, "NOT NULL")
|
||||
}
|
||||
@@ -1138,7 +1162,7 @@ func buildFallbackCreateStatement(dbType string, schemaName string, tableName st
|
||||
}
|
||||
|
||||
columnLines = append(columnLines, " "+strings.Join(defParts, " "))
|
||||
if commentSQL := buildFallbackColumnCommentStatement(dbType, qualifiedTable, colNameRaw, col.Comment); commentSQL != "" {
|
||||
if commentSQL := buildFallbackColumnCommentStatement(dbType, schemaName, table, qualifiedTable, colNameRaw, col.Comment); commentSQL != "" {
|
||||
columnCommentLines = append(columnCommentLines, commentSQL)
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(col.Key), "PRI") {
|
||||
@@ -1166,12 +1190,25 @@ func buildFallbackCreateStatement(dbType string, schemaName string, tableName st
|
||||
return ddl.String(), nil
|
||||
}
|
||||
|
||||
func buildFallbackColumnCommentStatement(dbType string, qualifiedTable string, columnName string, comment string) string {
|
||||
func buildFallbackColumnCommentStatement(dbType string, schemaName string, tableName string, qualifiedTable string, columnName string, comment string) string {
|
||||
colName := strings.TrimSpace(columnName)
|
||||
commentText := strings.TrimSpace(comment)
|
||||
if colName == "" || commentText == "" {
|
||||
return ""
|
||||
}
|
||||
if dbType == "sqlserver" {
|
||||
schema := strings.TrimSpace(schemaName)
|
||||
if schema == "" {
|
||||
schema = "dbo"
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"EXEC sp_addextendedproperty @name = N'MS_Description', @value = N'%s', @level0type = N'SCHEMA', @level0name = N'%s', @level1type = N'TABLE', @level1name = N'%s', @level2type = N'COLUMN', @level2name = N'%s';",
|
||||
strings.ReplaceAll(commentText, "'", "''"),
|
||||
strings.ReplaceAll(schema, "'", "''"),
|
||||
strings.ReplaceAll(strings.TrimSpace(tableName), "'", "''"),
|
||||
strings.ReplaceAll(colName, "'", "''"),
|
||||
)
|
||||
}
|
||||
columnRef := fmt.Sprintf("%s.%s", qualifiedTable, quoteIdentByType(dbType, colName))
|
||||
return fmt.Sprintf("COMMENT ON COLUMN %s IS '%s';", columnRef, strings.ReplaceAll(commentText, "'", "''"))
|
||||
}
|
||||
|
||||
@@ -366,6 +366,67 @@ func TestResolveCreateStatementWithFallback_NoFallbackForMySQL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCreateStatementWithFallback_SQLServerBuildsFallbackDDL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
defaultValue := "((0))"
|
||||
dbInst := &fakeCreateStatementDB{
|
||||
createSQL: "-- SHOW CREATE TABLE not supported for SQL Server in this version.",
|
||||
columns: []connection.ColumnDefinition{
|
||||
{Name: "id", Type: "int", Nullable: "NO", Key: "PRI", Extra: "auto_increment", Comment: "主键"},
|
||||
{Name: "display_name", Type: "nvarchar(128)", Nullable: "YES", Default: &defaultValue, Comment: "显示名's"},
|
||||
},
|
||||
}
|
||||
|
||||
ddl, err := resolveCreateStatementWithFallback(dbInst, connection.ConnectionConfig{
|
||||
Type: "sqlserver",
|
||||
Database: "default_db",
|
||||
}, "appdb", "dbo.Users")
|
||||
if err != nil {
|
||||
t.Fatalf("resolveCreateStatementWithFallback() unexpected error: %v", err)
|
||||
}
|
||||
if dbInst.createSchema != "appdb" || dbInst.createTable != "dbo.Users" {
|
||||
t.Fatalf("expected SQL Server create lookup to use database and raw table, got %q.%q", dbInst.createSchema, dbInst.createTable)
|
||||
}
|
||||
if dbInst.colsSchema != "appdb" || dbInst.colsTable != "dbo.Users" {
|
||||
t.Fatalf("expected SQL Server column lookup to use database and raw table, got %q.%q", dbInst.colsSchema, dbInst.colsTable)
|
||||
}
|
||||
for _, want := range []string{
|
||||
`CREATE TABLE [dbo].[Users]`,
|
||||
`[id] int IDENTITY(1,1) NOT NULL`,
|
||||
`[display_name] nvarchar(128) DEFAULT ((0))`,
|
||||
`PRIMARY KEY ([id])`,
|
||||
`EXEC sp_addextendedproperty @name = N'MS_Description', @value = N'主键', @level0type = N'SCHEMA', @level0name = N'dbo', @level1type = N'TABLE', @level1name = N'Users', @level2type = N'COLUMN', @level2name = N'id';`,
|
||||
`@value = N'显示名''s'`,
|
||||
} {
|
||||
if !strings.Contains(ddl, want) {
|
||||
t.Fatalf("expected SQL Server fallback DDL to contain %q, got: %s", want, ddl)
|
||||
}
|
||||
}
|
||||
if strings.Contains(ddl, "SHOW CREATE TABLE not supported") {
|
||||
t.Fatalf("expected fallback DDL instead of unsupported placeholder, got: %s", ddl)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCreateStatementWithFallback_SQLServerDefaultsToDboSchema(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbInst := &fakeCreateStatementDB{
|
||||
createSQL: "-- SHOW CREATE TABLE not supported for SQL Server in this version.",
|
||||
columns: []connection.ColumnDefinition{
|
||||
{Name: "id", Type: "int", Nullable: "NO"},
|
||||
},
|
||||
}
|
||||
|
||||
ddl, err := resolveCreateStatementWithFallback(dbInst, connection.ConnectionConfig{Type: "sqlserver"}, "appdb", "Users")
|
||||
if err != nil {
|
||||
t.Fatalf("resolveCreateStatementWithFallback() unexpected error: %v", err)
|
||||
}
|
||||
if !strings.Contains(ddl, `CREATE TABLE [dbo].[Users]`) {
|
||||
t.Fatalf("expected SQL Server fallback DDL to default to dbo schema, got: %s", ddl)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCreateStatementWithFallback_FallbackWhenCreateStatementError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -1910,9 +1910,9 @@ func buildViewCreateQueries(config connection.ConnectionConfig, dbName, schemaNa
|
||||
if schema == "" {
|
||||
schema = "dbo"
|
||||
}
|
||||
safeDBName := strings.TrimSpace(config.Database)
|
||||
safeDBName := strings.TrimSpace(dbName)
|
||||
if safeDBName == "" {
|
||||
safeDBName = strings.TrimSpace(dbName)
|
||||
safeDBName = strings.TrimSpace(config.Database)
|
||||
}
|
||||
if safeDBName == "" {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user