🐛 fix(sqlserver): 修复表 DDL 与索引创建语句生成

- DDL:为 SQL Server 表结构补充 CREATE TABLE fallback 生成
- 索引:在已有索引选择和新增索引弹窗中展示 CREATE INDEX 语句
- 测试:补充 SQL Server DDL fallback 与索引 SQL 预览回归测试
This commit is contained in:
Syngnat
2026-05-16 08:46:51 +08:00
parent 16836375c4
commit 1dd1cb9e44
8 changed files with 313 additions and 87 deletions

View File

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

View File

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

View File

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

View 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('请输入索引名');
});
});

View 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});` };
};

View File

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

View File

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

View File

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