🐛 fix(sync): 明确未迁移索引并提供补救 DDL (#885)

Fixes #885
This commit is contained in:
mango
2026-08-09 15:48:34 +08:00
parent f887deb44c
commit bb882faac7
17 changed files with 1053 additions and 237 deletions

View File

@@ -7,22 +7,23 @@ import (
)
type TableDiffSummary struct {
Table string `json:"table"`
PKColumn string `json:"pkColumn,omitempty"`
CanSync bool `json:"canSync"`
Inserts int `json:"inserts"`
Updates int `json:"updates"`
Deletes int `json:"deletes"`
Same int `json:"same"`
SchemaDiffCount int `json:"schemaDiffCount,omitempty"`
Message string `json:"message,omitempty"`
HasSchema bool `json:"hasSchema,omitempty"`
TargetTableExists bool `json:"targetTableExists,omitempty"`
PlannedAction string `json:"plannedAction,omitempty"`
Warnings []string `json:"warnings,omitempty"`
UnsupportedObjects []string `json:"unsupportedObjects,omitempty"`
IndexesToCreate int `json:"indexesToCreate,omitempty"`
IndexesSkipped int `json:"indexesSkipped,omitempty"`
Table string `json:"table"`
PKColumn string `json:"pkColumn,omitempty"`
CanSync bool `json:"canSync"`
Inserts int `json:"inserts"`
Updates int `json:"updates"`
Deletes int `json:"deletes"`
Same int `json:"same"`
SchemaDiffCount int `json:"schemaDiffCount,omitempty"`
Message string `json:"message,omitempty"`
HasSchema bool `json:"hasSchema,omitempty"`
TargetTableExists bool `json:"targetTableExists,omitempty"`
PlannedAction string `json:"plannedAction,omitempty"`
Warnings []string `json:"warnings,omitempty"`
UnsupportedObjects []string `json:"unsupportedObjects,omitempty"`
UnmigratedIndexes []UnmigratedIndex `json:"unmigratedIndexes,omitempty"`
IndexesToCreate int `json:"indexesToCreate,omitempty"`
IndexesSkipped int `json:"indexesSkipped,omitempty"`
}
type SyncAnalyzeResult struct {
@@ -134,6 +135,7 @@ func (s *SyncEngine) Analyze(config SyncConfig) SyncAnalyzeResult {
summary.PlannedAction = plan.PlannedAction
summary.Warnings = append(summary.Warnings, plan.Warnings...)
summary.UnsupportedObjects = append(summary.UnsupportedObjects, plan.UnsupportedObjects...)
summary.UnmigratedIndexes = append(summary.UnmigratedIndexes, plan.UnmigratedIndexes...)
summary.IndexesToCreate = plan.IndexesToCreate
summary.IndexesSkipped = plan.IndexesSkipped
summary.SchemaDiffCount = len(plan.PreDataSQL) + len(plan.PostDataSQL)

View File

@@ -72,13 +72,14 @@ func buildTabularToMongoPlan(config SyncConfig, tableName string, sourceDB db.Da
}
plan.PreDataSQL = append(plan.PreDataSQL, createCmd)
if config.CreateIndexes {
indexCmds, warnings, unsupported, created, skipped, err := buildMongoIndexCommands(sourceDB, plan.SourceSchema, plan.SourceTable, plan.TargetTable)
indexCmds, warnings, unsupported, unmigrated, created, skipped, err := buildMongoIndexCommands(sourceDB, plan.SourceSchema, plan.SourceTable, plan.TargetTable)
if err != nil {
plan.Warnings = append(plan.Warnings, fmt.Sprintf("读取源表索引失败,已跳过索引迁移:%v", err))
} else {
plan.PostDataSQL = append(plan.PostDataSQL, indexCmds...)
plan.Warnings = append(plan.Warnings, warnings...)
plan.UnsupportedObjects = append(plan.UnsupportedObjects, unsupported...)
plan.UnmigratedIndexes = append(plan.UnmigratedIndexes, unmigrated...)
plan.IndexesToCreate = created
plan.IndexesSkipped = skipped
}
@@ -139,13 +140,14 @@ func buildMongoToMongoPlan(config SyncConfig, tableName string, sourceDB db.Data
}
plan.PreDataSQL = append(plan.PreDataSQL, createCmd)
if config.CreateIndexes {
indexCmds, indexWarnings, unsupported, created, skipped, err := buildMongoIndexCommands(sourceDB, plan.SourceSchema, plan.SourceTable, plan.TargetTable)
indexCmds, indexWarnings, unsupported, unmigrated, created, skipped, err := buildMongoIndexCommands(sourceDB, plan.SourceSchema, plan.SourceTable, plan.TargetTable)
if err != nil {
plan.Warnings = append(plan.Warnings, fmt.Sprintf("读取源集合索引失败,已跳过索引迁移:%v", err))
} else {
plan.PostDataSQL = append(plan.PostDataSQL, indexCmds...)
plan.Warnings = append(plan.Warnings, indexWarnings...)
plan.UnsupportedObjects = append(plan.UnsupportedObjects, unsupported...)
plan.UnmigratedIndexes = append(plan.UnmigratedIndexes, unmigrated...)
plan.IndexesToCreate = created
plan.IndexesSkipped = skipped
}
@@ -245,15 +247,60 @@ func buildMongoCreateCollectionCommand(collection string) (string, error) {
return string(data), nil
}
func buildMongoIndexCommands(sourceDB db.Database, dbName, tableName, targetCollection string) ([]string, []string, []string, int, int, error) {
func buildMongoIndexCommand(targetCollection string, idx groupedIndex, text bool) (string, error) {
keyParts := make([]string, 0, len(idx.Columns))
for _, column := range idx.Columns {
nameJSON, err := json.Marshal(strings.TrimSpace(column.Name))
if err != nil {
return "", err
}
value := "1"
if text {
value = `"text"`
}
keyParts = append(keyParts, fmt.Sprintf("%s:%s", nameJSON, value))
}
command := struct {
CreateIndexes string `json:"createIndexes"`
Indexes []struct {
Name string `json:"name"`
Key json.RawMessage `json:"key"`
Unique bool `json:"unique"`
} `json:"indexes"`
}{CreateIndexes: strings.TrimSpace(targetCollection)}
command.Indexes = append(command.Indexes, struct {
Name string `json:"name"`
Key json.RawMessage `json:"key"`
Unique bool `json:"unique"`
}{
Name: strings.TrimSpace(idx.Name),
Key: json.RawMessage(`{` + strings.Join(keyParts, ",") + `}`),
Unique: idx.Unique,
})
data, err := json.Marshal(command)
if err != nil {
return "", err
}
return string(data), nil
}
func remediationStatements(command string) []string {
if strings.TrimSpace(command) == "" {
return nil
}
return []string{command}
}
func buildMongoIndexCommands(sourceDB db.Database, dbName, tableName, targetCollection string) ([]string, []string, []string, []UnmigratedIndex, int, int, error) {
indexes, err := sourceDB.GetIndexes(dbName, tableName)
if err != nil {
return nil, nil, nil, 0, 0, err
return nil, nil, nil, nil, 0, 0, err
}
grouped := groupIndexDefinitions(indexes)
cmds := make([]string, 0, len(grouped))
warnings := make([]string, 0)
unsupported := make([]string, 0)
unmigrated := make([]UnmigratedIndex, 0)
created := 0
skipped := 0
for _, idx := range grouped {
@@ -262,41 +309,85 @@ func buildMongoIndexCommands(sourceDB db.Database, dbName, tableName, targetColl
continue
}
if len(idx.Columns) == 0 {
reason := fmt.Sprintf("索引 %s 缺少列定义,已跳过", name)
skipped++
unsupported = append(unsupported, fmt.Sprintf("索引 %s 缺少列定义,已跳过", name))
unsupported = append(unsupported, reason)
unmigrated = append(unmigrated, UnmigratedIndex{
Name: name,
Columns: []IndexMigrationColumn{},
Unique: idx.Unique,
IndexType: idx.IndexType,
ReasonCode: "missing_columns",
Reason: reason,
})
continue
}
kind := strings.ToLower(strings.TrimSpace(idx.IndexType))
if idx.SubPart > 0 {
if hasIndexPrefix(idx.Columns) {
reason := fmt.Sprintf("索引 %s 使用前缀长度MongoDB 目标暂不支持等价迁移", name)
remediation, _ := buildMongoIndexCommand(targetCollection, idx, false)
skipped++
unsupported = append(unsupported, fmt.Sprintf("索引 %s 使用前缀长度MongoDB 目标暂不支持等价迁移", name))
unsupported = append(unsupported, reason)
unmigrated = append(unmigrated, UnmigratedIndex{
Name: name,
Columns: append([]IndexMigrationColumn(nil), idx.Columns...),
Unique: idx.Unique,
IndexType: idx.IndexType,
ReasonCode: "prefix_index_requires_review",
Reason: reason,
RemediationStatements: remediationStatements(remediation),
})
continue
}
if kind == "fulltext" {
reason := fmt.Sprintf("索引 %s 类型=%sMongoDB 目标暂不支持等价迁移", name, idx.IndexType)
remediation, _ := buildMongoIndexCommand(targetCollection, idx, true)
skipped++
unsupported = append(unsupported, reason)
unmigrated = append(unmigrated, UnmigratedIndex{
Name: name,
Columns: append([]IndexMigrationColumn(nil), idx.Columns...),
Unique: idx.Unique,
IndexType: idx.IndexType,
ReasonCode: "fulltext_requires_review",
Reason: reason,
RemediationStatements: remediationStatements(remediation),
})
continue
}
if kind != "" && kind != "btree" {
warnings = append(warnings, fmt.Sprintf("索引 %s 类型=%s 将按普通索引迁移到 MongoDB", name, idx.IndexType))
}
keySpec := make(map[string]int)
for _, col := range idx.Columns {
keySpec[col] = 1
}
command := map[string]interface{}{
"createIndexes": strings.TrimSpace(targetCollection),
"indexes": []map[string]interface{}{{
"name": name,
"key": keySpec,
"unique": idx.Unique,
}},
}
data, err := json.Marshal(command)
if err != nil {
reason := fmt.Sprintf("索引 %s 类型=%sMongoDB 目标暂不支持等价迁移", name, idx.IndexType)
skipped++
unsupported = append(unsupported, fmt.Sprintf("索引 %s 生成 MongoDB createIndexes 命令失败:%v", name, err))
unsupported = append(unsupported, reason)
unmigrated = append(unmigrated, UnmigratedIndex{
Name: name,
Columns: append([]IndexMigrationColumn(nil), idx.Columns...),
Unique: idx.Unique,
IndexType: idx.IndexType,
ReasonCode: "unsupported_index_type",
Reason: reason,
})
continue
}
cmds = append(cmds, string(data))
command, err := buildMongoIndexCommand(targetCollection, idx, false)
if err != nil {
reason := fmt.Sprintf("索引 %s 生成 MongoDB createIndexes 命令失败:%v", name, err)
skipped++
unsupported = append(unsupported, reason)
unmigrated = append(unmigrated, UnmigratedIndex{
Name: name,
Columns: append([]IndexMigrationColumn(nil), idx.Columns...),
Unique: idx.Unique,
IndexType: idx.IndexType,
ReasonCode: "remediation_generation_failed",
Reason: reason,
})
continue
}
cmds = append(cmds, command)
created++
}
return cmds, dedupeStrings(warnings), dedupeStrings(unsupported), created, skipped, nil
return cmds, dedupeStrings(warnings), dedupeStrings(unsupported), unmigrated, created, skipped, nil
}
func inferMongoCollectionColumns(sourceDB db.Database, collection string) ([]connection.ColumnDefinition, []string, error) {
@@ -468,7 +559,7 @@ func buildMongoToMySQLCreateTablePlan(config SyncConfig, targetQueryTable string
}
quotedCols := make([]string, 0, len(idx.Columns))
for _, col := range idx.Columns {
quotedCols = append(quotedCols, quoteIdentByType("mysql", col))
quotedCols = append(quotedCols, quoteIdentByType("mysql", col.Name))
}
prefix := "CREATE INDEX"
if idx.Unique {
@@ -642,7 +733,7 @@ func buildMongoToPGLikeCreateTablePlan(targetType string, config SyncConfig, tar
}
quotedCols := make([]string, 0, len(idx.Columns))
for _, col := range idx.Columns {
quotedCols = append(quotedCols, quoteIdentByType(targetType, col))
quotedCols = append(quotedCols, quoteIdentByType(targetType, col.Name))
}
prefix := "CREATE INDEX"
if idx.Unique {

View File

@@ -18,18 +18,19 @@ type PreviewUpdateRow struct {
}
type TableDiffPreview struct {
Table string `json:"table"`
PKColumn string `json:"pkColumn"`
ColumnTypes map[string]string `json:"columnTypes,omitempty"`
SchemaSummary string `json:"schemaSummary,omitempty"`
SchemaWarnings []string `json:"schemaWarnings,omitempty"`
SchemaStatements []string `json:"schemaStatements,omitempty"`
TotalInserts int `json:"totalInserts"`
TotalUpdates int `json:"totalUpdates"`
TotalDeletes int `json:"totalDeletes"`
Inserts []PreviewRow `json:"inserts"`
Updates []PreviewUpdateRow `json:"updates"`
Deletes []PreviewRow `json:"deletes"`
Table string `json:"table"`
PKColumn string `json:"pkColumn"`
ColumnTypes map[string]string `json:"columnTypes,omitempty"`
SchemaSummary string `json:"schemaSummary,omitempty"`
SchemaWarnings []string `json:"schemaWarnings,omitempty"`
SchemaStatements []string `json:"schemaStatements,omitempty"`
UnmigratedIndexes []UnmigratedIndex `json:"unmigratedIndexes,omitempty"`
TotalInserts int `json:"totalInserts"`
TotalUpdates int `json:"totalUpdates"`
TotalDeletes int `json:"totalDeletes"`
Inserts []PreviewRow `json:"inserts"`
Updates []PreviewUpdateRow `json:"updates"`
Deletes []PreviewRow `json:"deletes"`
}
func (s *SyncEngine) Preview(config SyncConfig, tableName string, limit int) (TableDiffPreview, error) {
@@ -94,10 +95,11 @@ func (s *SyncEngine) Preview(config SyncConfig, tableName string, limit int) (Ta
contentRaw := strings.ToLower(strings.TrimSpace(config.Content))
if contentRaw == "schema" {
return TableDiffPreview{
Table: tableName,
SchemaSummary: firstNonEmpty(plan.PlannedAction, "仅同步结构"),
SchemaWarnings: append([]string(nil), plan.Warnings...),
SchemaStatements: append([]string(nil), schemaStatements...),
Table: tableName,
SchemaSummary: firstNonEmpty(plan.PlannedAction, "仅同步结构"),
SchemaWarnings: append([]string(nil), plan.Warnings...),
SchemaStatements: append([]string(nil), schemaStatements...),
UnmigratedIndexes: append([]UnmigratedIndex(nil), plan.UnmigratedIndexes...),
}, nil
}
@@ -126,18 +128,19 @@ func (s *SyncEngine) Preview(config SyncConfig, tableName string, limit int) (Ta
sourceType := resolveMigrationDBType(config.SourceConfig)
targetType := resolveMigrationDBType(config.TargetConfig)
out := TableDiffPreview{
Table: tableName,
PKColumn: pkCol,
ColumnTypes: make(map[string]string, len(cols)),
SchemaSummary: firstNonEmpty(plan.PlannedAction, "结构预览"),
SchemaWarnings: append([]string(nil), plan.Warnings...),
SchemaStatements: append([]string(nil), schemaStatements...),
TotalInserts: 0,
TotalUpdates: 0,
TotalDeletes: 0,
Inserts: make([]PreviewRow, 0),
Updates: make([]PreviewUpdateRow, 0),
Deletes: make([]PreviewRow, 0),
Table: tableName,
PKColumn: pkCol,
ColumnTypes: make(map[string]string, len(cols)),
SchemaSummary: firstNonEmpty(plan.PlannedAction, "结构预览"),
SchemaWarnings: append([]string(nil), plan.Warnings...),
SchemaStatements: append([]string(nil), schemaStatements...),
UnmigratedIndexes: append([]UnmigratedIndex(nil), plan.UnmigratedIndexes...),
TotalInserts: 0,
TotalUpdates: 0,
TotalDeletes: 0,
Inserts: make([]PreviewRow, 0),
Updates: make([]PreviewUpdateRow, 0),
Deletes: make([]PreviewRow, 0),
}
columnTypes := cols
if hasExplicitSyncMappings(config) {

View File

@@ -22,6 +22,7 @@ type SchemaMigrationPlan struct {
PlannedAction string
Warnings []string
UnsupportedObjects []string
UnmigratedIndexes []UnmigratedIndex
IndexesToCreate int
IndexesSkipped int
CreateTableSQL string
@@ -29,12 +30,26 @@ type SchemaMigrationPlan struct {
PostDataSQL []string
}
type IndexMigrationColumn struct {
Name string `json:"name"`
PrefixLength int `json:"prefixLength,omitempty"`
}
type UnmigratedIndex struct {
Name string `json:"name"`
Columns []IndexMigrationColumn `json:"columns"`
Unique bool `json:"unique"`
IndexType string `json:"indexType"`
ReasonCode string `json:"reasonCode"`
Reason string `json:"reason"`
RemediationStatements []string `json:"remediationStatements,omitempty"`
}
type groupedIndex struct {
Name string
Columns []string
Columns []IndexMigrationColumn
Unique bool
IndexType string
SubPart int
}
func normalizeTargetTableStrategy(strategy string) string {
@@ -188,7 +203,7 @@ func buildSchemaMigrationPlanLegacy(config SyncConfig, tableName string, sourceD
}
plan.AutoCreate = true
plan.PlannedAction = "目标表不存在,将自动建表后导入"
createSQL, postSQL, warnings, unsupported, idxCreate, idxSkip, err := buildMySQLToKingbaseCreateTablePlan(config, plan.TargetQueryTable, sourceCols, sourceDB, plan.SourceSchema, plan.SourceTable)
createSQL, postSQL, warnings, unsupported, unmigrated, idxCreate, idxSkip, err := buildMySQLToKingbaseCreateTablePlan(config, plan.TargetQueryTable, sourceCols, sourceDB, plan.SourceSchema, plan.SourceTable)
if err != nil {
return plan, sourceCols, targetCols, err
}
@@ -196,6 +211,7 @@ func buildSchemaMigrationPlanLegacy(config SyncConfig, tableName string, sourceD
plan.PostDataSQL = append(plan.PostDataSQL, postSQL...)
plan.Warnings = append(plan.Warnings, warnings...)
plan.UnsupportedObjects = append(plan.UnsupportedObjects, unsupported...)
plan.UnmigratedIndexes = append(plan.UnmigratedIndexes, unmigrated...)
plan.IndexesToCreate = idxCreate
plan.IndexesSkipped = idxSkip
return dedupeSchemaMigrationPlan(plan), sourceCols, targetCols, nil
@@ -210,6 +226,11 @@ func dedupeSchemaMigrationPlan(plan SchemaMigrationPlan) SchemaMigrationPlan {
return plan
}
func InspectSchemaMigrationPlan(config SyncConfig, tableName string, sourceDB db.Database, targetDB db.Database) (SchemaMigrationPlan, error) {
plan, _, _, err := buildSchemaMigrationPlan(config, tableName, sourceDB, targetDB)
return plan, err
}
func dedupeStrings(items []string) []string {
if len(items) == 0 {
return items
@@ -291,7 +312,7 @@ func buildMySQLToKingbaseAddColumnSQL(targetQueryTable string, sourceCols, targe
return sqlList, dedupeStrings(warnings)
}
func buildMySQLToKingbaseCreateTablePlan(config SyncConfig, targetQueryTable string, sourceCols []connection.ColumnDefinition, sourceDB db.Database, sourceSchema, sourceTable string) (string, []string, []string, []string, int, int, error) {
func buildMySQLToKingbaseCreateTablePlan(config SyncConfig, targetQueryTable string, sourceCols []connection.ColumnDefinition, sourceDB db.Database, sourceSchema, sourceTable string) (string, []string, []string, []string, []UnmigratedIndex, int, int, error) {
columnDefs := make([]string, 0, len(sourceCols)+1)
warnings := make([]string, 0)
unsupported := make([]string, 0)
@@ -311,51 +332,16 @@ func buildMySQLToKingbaseCreateTablePlan(config SyncConfig, targetQueryTable str
createSQL := fmt.Sprintf("CREATE TABLE %s (\n %s\n)", quoteQualifiedIdentByType("kingbase", targetQueryTable), strings.Join(columnDefs, ",\n "))
if !config.CreateIndexes {
return createSQL, nil, dedupeStrings(warnings), dedupeStrings(unsupported), 0, 0, nil
return createSQL, nil, dedupeStrings(warnings), dedupeStrings(unsupported), nil, 0, 0, nil
}
indexes, err := sourceDB.GetIndexes(sourceSchema, sourceTable)
if err != nil {
warnings = append(warnings, fmt.Sprintf("读取源表索引失败,已跳过索引迁移:%v", err))
return createSQL, nil, dedupeStrings(warnings), dedupeStrings(unsupported), 0, 0, nil
return createSQL, nil, dedupeStrings(warnings), dedupeStrings(unsupported), nil, 0, 0, nil
}
grouped := groupIndexDefinitions(indexes)
postSQL := make([]string, 0, len(grouped))
created := 0
skipped := 0
for _, idx := range grouped {
name := strings.TrimSpace(idx.Name)
if name == "" || strings.EqualFold(name, "primary") {
continue
}
if len(idx.Columns) == 0 {
skipped++
unsupported = append(unsupported, fmt.Sprintf("索引 %s 缺少列定义,已跳过", name))
continue
}
kind := strings.ToLower(strings.TrimSpace(idx.IndexType))
if idx.SubPart > 0 {
skipped++
unsupported = append(unsupported, fmt.Sprintf("索引 %s 使用前缀长度,当前暂不支持迁移", name))
continue
}
if kind != "" && kind != "btree" {
skipped++
unsupported = append(unsupported, fmt.Sprintf("索引 %s 类型=%s当前暂不支持自动迁移", name, idx.IndexType))
continue
}
quotedCols := make([]string, 0, len(idx.Columns))
for _, col := range idx.Columns {
quotedCols = append(quotedCols, quoteIdentByType("kingbase", col))
}
prefix := "CREATE INDEX"
if idx.Unique {
prefix = "CREATE UNIQUE INDEX"
}
postSQL = append(postSQL, fmt.Sprintf("%s %s ON %s (%s)", prefix, quoteIdentByType("kingbase", name), quoteQualifiedIdentByType("kingbase", targetQueryTable), strings.Join(quotedCols, ", ")))
created++
}
return createSQL, postSQL, dedupeStrings(warnings), dedupeStrings(unsupported), created, skipped, nil
postSQL, unsupported, unmigrated, created, skipped := buildMySQLSourceIndexPlan("kingbase", targetQueryTable, indexes)
return createSQL, postSQL, dedupeStrings(warnings), unsupported, unmigrated, created, skipped, nil
}
func buildMySQLToKingbaseColumnDefinition(col connection.ColumnDefinition) (string, []string) {
@@ -534,12 +520,12 @@ func groupIndexDefinitions(indexes []connection.IndexDefinition) []groupedIndex
if strings.TrimSpace(row.IndexType) != "" {
gi.IndexType = row.IndexType
}
if row.SubPart > 0 && gi.SubPart == 0 {
gi.SubPart = row.SubPart
}
col := strings.TrimSpace(row.ColumnName)
if col != "" {
gi.Columns = append(gi.Columns, col)
gi.Columns = append(gi.Columns, IndexMigrationColumn{
Name: col,
PrefixLength: row.SubPart,
})
}
}
grouped = append(grouped, gi)
@@ -547,18 +533,161 @@ func groupIndexDefinitions(indexes []connection.IndexDefinition) []groupedIndex
return grouped
}
func sameColumnNameList(a, b []string) bool {
func sameColumnNameList(a []IndexMigrationColumn, b []string) bool {
if len(a) == 0 || len(a) != len(b) {
return false
}
for i := range a {
if !strings.EqualFold(strings.TrimSpace(a[i]), strings.TrimSpace(b[i])) {
if !strings.EqualFold(strings.TrimSpace(a[i].Name), strings.TrimSpace(b[i])) {
return false
}
}
return true
}
func hasIndexPrefix(columns []IndexMigrationColumn) bool {
for _, column := range columns {
if column.PrefixLength > 0 {
return true
}
}
return false
}
func buildQuotedIndexColumns(targetType string, columns []IndexMigrationColumn, preservePrefix bool) []string {
quoted := make([]string, 0, len(columns))
for _, column := range columns {
name := strings.TrimSpace(column.Name)
if name == "" {
continue
}
value := quoteIdentByType(targetType, name)
if preservePrefix && column.PrefixLength > 0 {
value += fmt.Sprintf("(%d)", column.PrefixLength)
}
quoted = append(quoted, value)
}
return quoted
}
func buildCreateIndexSQL(targetType, targetQueryTable string, idx groupedIndex, preservePrefix bool) string {
prefix := "CREATE INDEX"
if idx.Unique {
prefix = "CREATE UNIQUE INDEX"
}
return fmt.Sprintf("%s %s ON %s (%s)",
prefix,
quoteIdentByType(targetType, idx.Name),
quoteQualifiedIdentByType(targetType, targetQueryTable),
strings.Join(buildQuotedIndexColumns(targetType, idx.Columns, preservePrefix), ", "),
)
}
func buildIndexRemediationStatements(targetType, targetQueryTable string, idx groupedIndex) []string {
kind := strings.ToLower(strings.TrimSpace(idx.IndexType))
if isMySQLRowStoreType(targetType) {
if kind == "fulltext" {
return []string{fmt.Sprintf("CREATE FULLTEXT INDEX %s ON %s (%s)",
quoteIdentByType(targetType, idx.Name),
quoteQualifiedIdentByType(targetType, targetQueryTable),
strings.Join(buildQuotedIndexColumns(targetType, idx.Columns, true), ", "),
)}
}
if hasIndexPrefix(idx.Columns) {
return []string{buildCreateIndexSQL(targetType, targetQueryTable, idx, true)}
}
return nil
}
if kind == "fulltext" && isPGLikeSameFamilyDDLType(targetType) {
parts := make([]string, 0, len(idx.Columns))
for _, column := range idx.Columns {
parts = append(parts, fmt.Sprintf("coalesce(CAST(%s AS text), '')", quoteIdentByType(targetType, column.Name)))
}
return []string{fmt.Sprintf("CREATE INDEX %s ON %s USING GIN (to_tsvector('simple', %s))",
quoteIdentByType(targetType, idx.Name),
quoteQualifiedIdentByType(targetType, targetQueryTable),
strings.Join(parts, " || ' ' || "),
)}
}
if hasIndexPrefix(idx.Columns) && isPGLikeSameFamilyDDLType(targetType) {
columns := make([]string, 0, len(idx.Columns))
for _, column := range idx.Columns {
value := quoteIdentByType(targetType, column.Name)
if column.PrefixLength > 0 {
value = fmt.Sprintf("left(CAST(%s AS text), %d)", value, column.PrefixLength)
}
columns = append(columns, value)
}
prefix := "CREATE INDEX"
if idx.Unique {
prefix = "CREATE UNIQUE INDEX"
}
return []string{fmt.Sprintf("%s %s ON %s (%s)",
prefix,
quoteIdentByType(targetType, idx.Name),
quoteQualifiedIdentByType(targetType, targetQueryTable),
strings.Join(columns, ", "),
)}
}
return nil
}
func buildMySQLSourceIndexPlan(targetType, targetQueryTable string, indexes []connection.IndexDefinition) ([]string, []string, []UnmigratedIndex, int, int) {
grouped := groupIndexDefinitions(indexes)
postSQL := make([]string, 0, len(grouped))
unsupported := make([]string, 0)
unmigrated := make([]UnmigratedIndex, 0)
created := 0
skipped := 0
for _, idx := range grouped {
name := strings.TrimSpace(idx.Name)
if name == "" || strings.EqualFold(name, "primary") {
continue
}
if len(idx.Columns) == 0 {
reason := fmt.Sprintf("索引 %s 缺少列定义,已跳过", name)
unsupported = append(unsupported, reason)
unmigrated = append(unmigrated, UnmigratedIndex{
Name: name,
Columns: []IndexMigrationColumn{},
Unique: idx.Unique,
IndexType: idx.IndexType,
ReasonCode: "missing_columns",
Reason: reason,
})
skipped++
continue
}
kind := strings.ToLower(strings.TrimSpace(idx.IndexType))
if (kind == "" || kind == "btree") && !hasIndexPrefix(idx.Columns) {
postSQL = append(postSQL, buildCreateIndexSQL(targetType, targetQueryTable, idx, false))
created++
continue
}
reasonCode := "unsupported_index_type"
reason := fmt.Sprintf("索引 %s 类型=%s当前暂不支持等价自动迁移", name, idx.IndexType)
if hasIndexPrefix(idx.Columns) {
reasonCode = "prefix_index_requires_review"
reason = fmt.Sprintf("索引 %s 使用前缀长度,当前目标方言暂不支持等价自动迁移", name)
} else if kind == "fulltext" {
reasonCode = "fulltext_requires_review"
}
unsupported = append(unsupported, reason)
unmigrated = append(unmigrated, UnmigratedIndex{
Name: name,
Columns: append([]IndexMigrationColumn(nil), idx.Columns...),
Unique: idx.Unique,
IndexType: idx.IndexType,
ReasonCode: reasonCode,
Reason: reason,
RemediationStatements: buildIndexRemediationStatements(targetType, targetQueryTable, idx),
})
skipped++
}
return postSQL, dedupeStrings(unsupported), unmigrated, created, skipped
}
func intFromAny(v interface{}) int {
switch typed := v.(type) {
case int:
@@ -673,7 +802,7 @@ func buildMySQLToMySQLPlan(config SyncConfig, tableName string, sourceDB db.Data
case "smart", "auto_create_if_missing":
plan.AutoCreate = true
plan.PlannedAction = "目标表不存在,将自动建表后导入"
createSQL, postSQL, warnings, unsupported, idxCreate, idxSkip, err := buildMySQLToMySQLCreateTablePlan(targetType, config, plan.TargetQueryTable, sourceCols, sourceDB, plan.SourceSchema, plan.SourceTable)
createSQL, postSQL, warnings, unsupported, unmigrated, idxCreate, idxSkip, err := buildMySQLToMySQLCreateTablePlan(targetType, config, plan.TargetQueryTable, sourceCols, sourceDB, plan.SourceSchema, plan.SourceTable)
if err != nil {
return plan, sourceCols, targetCols, err
}
@@ -681,6 +810,7 @@ func buildMySQLToMySQLPlan(config SyncConfig, tableName string, sourceDB db.Data
plan.PostDataSQL = append(plan.PostDataSQL, postSQL...)
plan.Warnings = append(plan.Warnings, warnings...)
plan.UnsupportedObjects = append(plan.UnsupportedObjects, unsupported...)
plan.UnmigratedIndexes = append(plan.UnmigratedIndexes, unmigrated...)
plan.IndexesToCreate = idxCreate
plan.IndexesSkipped = idxSkip
return dedupeSchemaMigrationPlan(plan), sourceCols, targetCols, nil
@@ -689,7 +819,7 @@ func buildMySQLToMySQLPlan(config SyncConfig, tableName string, sourceDB db.Data
}
}
func buildMySQLToMySQLCreateTablePlan(targetType string, config SyncConfig, targetQueryTable string, sourceCols []connection.ColumnDefinition, sourceDB db.Database, sourceSchema, sourceTable string) (string, []string, []string, []string, int, int, error) {
func buildMySQLToMySQLCreateTablePlan(targetType string, config SyncConfig, targetQueryTable string, sourceCols []connection.ColumnDefinition, sourceDB db.Database, sourceSchema, sourceTable string) (string, []string, []string, []string, []UnmigratedIndex, int, int, error) {
columnDefs := make([]string, 0, len(sourceCols)+1)
warnings := make([]string, 0)
unsupported := make([]string, 0)
@@ -707,50 +837,15 @@ func buildMySQLToMySQLCreateTablePlan(targetType string, config SyncConfig, targ
}
createSQL := fmt.Sprintf("CREATE TABLE %s (\n %s\n)", quoteQualifiedIdentByType(targetType, targetQueryTable), strings.Join(columnDefs, ",\n "))
if !config.CreateIndexes {
return createSQL, nil, dedupeStrings(warnings), dedupeStrings(unsupported), 0, 0, nil
return createSQL, nil, dedupeStrings(warnings), dedupeStrings(unsupported), nil, 0, 0, nil
}
indexes, err := sourceDB.GetIndexes(sourceSchema, sourceTable)
if err != nil {
warnings = append(warnings, fmt.Sprintf("读取源表索引失败,已跳过索引迁移:%v", err))
return createSQL, nil, dedupeStrings(warnings), dedupeStrings(unsupported), 0, 0, nil
return createSQL, nil, dedupeStrings(warnings), dedupeStrings(unsupported), nil, 0, 0, nil
}
grouped := groupIndexDefinitions(indexes)
postSQL := make([]string, 0, len(grouped))
created := 0
skipped := 0
for _, idx := range grouped {
name := strings.TrimSpace(idx.Name)
if name == "" || strings.EqualFold(name, "primary") {
continue
}
if len(idx.Columns) == 0 {
skipped++
unsupported = append(unsupported, fmt.Sprintf("索引 %s 缺少列定义,已跳过", name))
continue
}
kind := strings.ToLower(strings.TrimSpace(idx.IndexType))
if idx.SubPart > 0 {
skipped++
unsupported = append(unsupported, fmt.Sprintf("索引 %s 使用前缀长度,当前暂不支持迁移", name))
continue
}
if kind != "" && kind != "btree" {
skipped++
unsupported = append(unsupported, fmt.Sprintf("索引 %s 类型=%s当前暂不支持自动迁移", name, idx.IndexType))
continue
}
quotedCols := make([]string, 0, len(idx.Columns))
for _, col := range idx.Columns {
quotedCols = append(quotedCols, quoteIdentByType(targetType, col))
}
prefix := "CREATE INDEX"
if idx.Unique {
prefix = "CREATE UNIQUE INDEX"
}
postSQL = append(postSQL, fmt.Sprintf("%s %s ON %s (%s)", prefix, quoteIdentByType(targetType, name), quoteQualifiedIdentByType(targetType, targetQueryTable), strings.Join(quotedCols, ", ")))
created++
}
return createSQL, postSQL, dedupeStrings(warnings), dedupeStrings(unsupported), created, skipped, nil
postSQL, unsupported, unmigrated, created, skipped := buildMySQLSourceIndexPlan(targetType, targetQueryTable, indexes)
return createSQL, postSQL, dedupeStrings(warnings), unsupported, unmigrated, created, skipped, nil
}
func buildMySQLToMySQLColumnDefinition(col connection.ColumnDefinition) (string, []string) {
@@ -980,7 +1075,7 @@ func buildPGLikeToPGLikeCreateTablePlan(targetType string, config SyncConfig, ta
continue
}
kind := strings.ToLower(strings.TrimSpace(idx.IndexType))
if idx.SubPart > 0 {
if hasIndexPrefix(idx.Columns) {
skipped++
unsupported = append(unsupported, fmt.Sprintf("索引 %s 使用前缀长度,当前暂不支持迁移", name))
continue
@@ -992,7 +1087,7 @@ func buildPGLikeToPGLikeCreateTablePlan(targetType string, config SyncConfig, ta
}
quotedCols := make([]string, 0, len(idx.Columns))
for _, col := range idx.Columns {
quotedCols = append(quotedCols, quoteIdentByType(targetType, col))
quotedCols = append(quotedCols, quoteIdentByType(targetType, col.Name))
}
prefix := "CREATE INDEX"
if idx.Unique {
@@ -1230,7 +1325,7 @@ func buildPGLikeToMySQLCreateTablePlan(config SyncConfig, targetQueryTable strin
continue
}
kind := strings.ToLower(strings.TrimSpace(idx.IndexType))
if idx.SubPart > 0 {
if hasIndexPrefix(idx.Columns) {
skipped++
unsupported = append(unsupported, fmt.Sprintf("索引 %s 使用前缀长度,当前暂不支持迁移", name))
continue
@@ -1242,7 +1337,7 @@ func buildPGLikeToMySQLCreateTablePlan(config SyncConfig, targetQueryTable strin
}
quotedCols := make([]string, 0, len(idx.Columns))
for _, col := range idx.Columns {
quotedCols = append(quotedCols, quoteIdentByType("mysql", col))
quotedCols = append(quotedCols, quoteIdentByType("mysql", col.Name))
}
prefix := "CREATE INDEX"
if idx.Unique {
@@ -1434,7 +1529,7 @@ func buildMySQLToPGLikePlan(config SyncConfig, tableName string, sourceDB db.Dat
case "smart", "auto_create_if_missing":
plan.AutoCreate = true
plan.PlannedAction = "目标表不存在,将自动建表后导入"
createSQL, postSQL, warnings, unsupported, idxCreate, idxSkip, err := buildMySQLToPGLikeCreateTablePlan(targetType, config, plan.TargetQueryTable, sourceCols, sourceDB, plan.SourceSchema, plan.SourceTable)
createSQL, postSQL, warnings, unsupported, unmigrated, idxCreate, idxSkip, err := buildMySQLToPGLikeCreateTablePlan(targetType, config, plan.TargetQueryTable, sourceCols, sourceDB, plan.SourceSchema, plan.SourceTable)
if err != nil {
return plan, sourceCols, targetCols, err
}
@@ -1442,6 +1537,7 @@ func buildMySQLToPGLikePlan(config SyncConfig, tableName string, sourceDB db.Dat
plan.PostDataSQL = append(plan.PostDataSQL, postSQL...)
plan.Warnings = append(plan.Warnings, warnings...)
plan.UnsupportedObjects = append(plan.UnsupportedObjects, unsupported...)
plan.UnmigratedIndexes = append(plan.UnmigratedIndexes, unmigrated...)
plan.IndexesToCreate = idxCreate
plan.IndexesSkipped = idxSkip
return dedupeSchemaMigrationPlan(plan), sourceCols, targetCols, nil
@@ -1483,7 +1579,7 @@ func buildMySQLToPGLikeAddColumnSQL(targetType string, targetQueryTable string,
return sqlList, dedupeStrings(warnings)
}
func buildMySQLToPGLikeCreateTablePlan(targetType string, config SyncConfig, targetQueryTable string, sourceCols []connection.ColumnDefinition, sourceDB db.Database, sourceSchema, sourceTable string) (string, []string, []string, []string, int, int, error) {
func buildMySQLToPGLikeCreateTablePlan(targetType string, config SyncConfig, targetQueryTable string, sourceCols []connection.ColumnDefinition, sourceDB db.Database, sourceSchema, sourceTable string) (string, []string, []string, []string, []UnmigratedIndex, int, int, error) {
columnDefs := make([]string, 0, len(sourceCols)+1)
warnings := make([]string, 0)
unsupported := make([]string, 0)
@@ -1501,50 +1597,15 @@ func buildMySQLToPGLikeCreateTablePlan(targetType string, config SyncConfig, tar
}
createSQL := fmt.Sprintf("CREATE TABLE %s (\n %s\n)", quoteQualifiedIdentByType(targetType, targetQueryTable), strings.Join(columnDefs, ",\n "))
if !config.CreateIndexes {
return createSQL, nil, dedupeStrings(warnings), dedupeStrings(unsupported), 0, 0, nil
return createSQL, nil, dedupeStrings(warnings), dedupeStrings(unsupported), nil, 0, 0, nil
}
indexes, err := sourceDB.GetIndexes(sourceSchema, sourceTable)
if err != nil {
warnings = append(warnings, fmt.Sprintf("读取源表索引失败,已跳过索引迁移:%v", err))
return createSQL, nil, dedupeStrings(warnings), dedupeStrings(unsupported), 0, 0, nil
return createSQL, nil, dedupeStrings(warnings), dedupeStrings(unsupported), nil, 0, 0, nil
}
grouped := groupIndexDefinitions(indexes)
postSQL := make([]string, 0, len(grouped))
created := 0
skipped := 0
for _, idx := range grouped {
name := strings.TrimSpace(idx.Name)
if name == "" || strings.EqualFold(name, "primary") {
continue
}
if len(idx.Columns) == 0 {
skipped++
unsupported = append(unsupported, fmt.Sprintf("索引 %s 缺少列定义,已跳过", name))
continue
}
kind := strings.ToLower(strings.TrimSpace(idx.IndexType))
if idx.SubPart > 0 {
skipped++
unsupported = append(unsupported, fmt.Sprintf("索引 %s 使用前缀长度,当前暂不支持迁移", name))
continue
}
if kind != "" && kind != "btree" {
skipped++
unsupported = append(unsupported, fmt.Sprintf("索引 %s 类型=%s当前暂不支持自动迁移", name, idx.IndexType))
continue
}
quotedCols := make([]string, 0, len(idx.Columns))
for _, col := range idx.Columns {
quotedCols = append(quotedCols, quoteIdentByType(targetType, col))
}
prefix := "CREATE INDEX"
if idx.Unique {
prefix = "CREATE UNIQUE INDEX"
}
postSQL = append(postSQL, fmt.Sprintf("%s %s ON %s (%s)", prefix, quoteIdentByType(targetType, name), quoteQualifiedIdentByType(targetType, targetQueryTable), strings.Join(quotedCols, ", ")))
created++
}
return createSQL, postSQL, dedupeStrings(warnings), dedupeStrings(unsupported), created, skipped, nil
postSQL, unsupported, unmigrated, created, skipped := buildMySQLSourceIndexPlan(targetType, targetQueryTable, indexes)
return createSQL, postSQL, dedupeStrings(warnings), unsupported, unmigrated, created, skipped, nil
}
func buildMySQLToPGLikeColumnDefinition(col connection.ColumnDefinition) (string, []string) {

View File

@@ -120,7 +120,7 @@ func TestBuildMySQLToKingbaseCreateTablePlan_GeneratesAndSkipsIndexes(t *testing
{Name: "note", Type: "text", Nullable: "YES"},
}
cfg := SyncConfig{CreateIndexes: true}
createSQL, postSQL, warnings, unsupported, idxCreate, idxSkip, err := buildMySQLToKingbaseCreateTablePlan(cfg, "public.orders", cols, sourceDB, "shop", "orders")
createSQL, postSQL, warnings, unsupported, unmigrated, idxCreate, idxSkip, err := buildMySQLToKingbaseCreateTablePlan(cfg, "public.orders", cols, sourceDB, "shop", "orders")
if err != nil {
t.Fatalf("buildMySQLToKingbaseCreateTablePlan returned error: %v", err)
}
@@ -140,12 +140,39 @@ func TestBuildMySQLToKingbaseCreateTablePlan_GeneratesAndSkipsIndexes(t *testing
t.Fatalf("unexpected warnings: %v", warnings)
}
wantUnsupported := []string{
"索引 idx_name_prefix 使用前缀长度,当前暂不支持迁移",
"索引 idx_fulltext_note 类型=FULLTEXT当前暂不支持自动迁移",
"索引 idx_name_prefix 使用前缀长度,当前目标方言暂不支持等价自动迁移",
"索引 idx_fulltext_note 类型=FULLTEXT当前暂不支持等价自动迁移",
}
if !reflect.DeepEqual(unsupported, wantUnsupported) {
t.Fatalf("unexpected unsupported objects: got=%v want=%v", unsupported, wantUnsupported)
}
wantUnmigrated := []UnmigratedIndex{
{
Name: "idx_name_prefix",
Columns: []IndexMigrationColumn{{Name: "name", PrefixLength: 12}},
Unique: false,
IndexType: "BTREE",
ReasonCode: "prefix_index_requires_review",
Reason: wantUnsupported[0],
RemediationStatements: []string{
"CREATE INDEX idx_name_prefix ON public.orders (left(CAST(name AS text), 12))",
},
},
{
Name: "idx_fulltext_note",
Columns: []IndexMigrationColumn{{Name: "note"}},
Unique: false,
IndexType: "FULLTEXT",
ReasonCode: "fulltext_requires_review",
Reason: wantUnsupported[1],
RemediationStatements: []string{
"CREATE INDEX idx_fulltext_note ON public.orders USING GIN (to_tsvector('simple', coalesce(CAST(note AS text), '')))",
},
},
}
if !reflect.DeepEqual(unmigrated, wantUnmigrated) {
t.Fatalf("unexpected unmigrated indexes: got=%+v want=%+v", unmigrated, wantUnmigrated)
}
}
func TestBuildSchemaMigrationPlan_AutoCreateWhenTargetMissing(t *testing.T) {
@@ -377,6 +404,124 @@ func TestBuildSchemaMigrationPlan_MySQLToMySQLAutoCreatesMissingTarget(t *testin
}
}
func TestBuildMySQLToMySQLCreateTablePlan_ListsCompositePrefixRemediation(t *testing.T) {
t.Parallel()
sourceDB := &fakeMigrationDB{
indexes: map[string][]connection.IndexDefinition{
"shop.users": {
{Name: "idx_users_lookup", ColumnName: "email", NonUnique: 1, SeqInIndex: 2, IndexType: "BTREE"},
{Name: "idx_users_lookup", ColumnName: "name", NonUnique: 1, SeqInIndex: 1, IndexType: "BTREE", SubPart: 12},
{Name: "idx_users_lookup", ColumnName: "bio", NonUnique: 1, SeqInIndex: 3, IndexType: "BTREE", SubPart: 24},
},
},
}
cols := []connection.ColumnDefinition{
{Name: "name", Type: "varchar(128)", Nullable: "NO"},
{Name: "email", Type: "varchar(255)", Nullable: "NO"},
{Name: "bio", Type: "text", Nullable: "YES"},
}
_, postSQL, warnings, unsupported, unmigrated, idxCreate, idxSkip, err := buildMySQLToMySQLCreateTablePlan(
"mysql",
SyncConfig{CreateIndexes: true},
"app.users",
cols,
sourceDB,
"shop",
"users",
)
if err != nil {
t.Fatalf("buildMySQLToMySQLCreateTablePlan returned error: %v", err)
}
want := "CREATE INDEX `idx_users_lookup` ON `app`.`users` (`name`(12), `email`, `bio`(24))"
if len(postSQL) != 0 || idxCreate != 0 || idxSkip != 1 {
t.Fatalf("prefix index must require review: sql=%v create=%d skip=%d", postSQL, idxCreate, idxSkip)
}
if len(warnings) != 0 || len(unsupported) != 1 || len(unmigrated) != 1 {
t.Fatalf("unexpected warnings/unsupported: warnings=%v unsupported=%v unmigrated=%v", warnings, unsupported, unmigrated)
}
if !reflect.DeepEqual(unmigrated[0].RemediationStatements, []string{want}) {
t.Fatalf("unexpected prefix remediation: got=%v want=%v", unmigrated[0].RemediationStatements, []string{want})
}
}
func TestBuildMySQLToMySQLCreateTablePlan_ListsFulltextRemediation(t *testing.T) {
t.Parallel()
sourceDB := &fakeMigrationDB{
indexes: map[string][]connection.IndexDefinition{
"shop.articles": {
{Name: "idx_fulltext_body", ColumnName: "title", NonUnique: 1, SeqInIndex: 1, IndexType: "FULLTEXT"},
{Name: "idx_fulltext_body", ColumnName: "body", NonUnique: 1, SeqInIndex: 2, IndexType: "FULLTEXT"},
},
},
}
cols := []connection.ColumnDefinition{
{Name: "title", Type: "varchar(255)", Nullable: "NO"},
{Name: "body", Type: "text", Nullable: "NO"},
}
_, postSQL, warnings, unsupported, unmigrated, idxCreate, idxSkip, err := buildMySQLToMySQLCreateTablePlan(
"mysql",
SyncConfig{CreateIndexes: true},
"archive.articles",
cols,
sourceDB,
"shop",
"articles",
)
if err != nil {
t.Fatalf("buildMySQLToMySQLCreateTablePlan returned error: %v", err)
}
if len(postSQL) != 0 || idxCreate != 0 || idxSkip != 1 {
t.Fatalf("fulltext index must require review: sql=%v create=%d skip=%d", postSQL, idxCreate, idxSkip)
}
if len(warnings) != 0 || len(unsupported) != 1 || len(unmigrated) != 1 {
t.Fatalf("unexpected fulltext summary: warnings=%v unsupported=%v unmigrated=%+v", warnings, unsupported, unmigrated)
}
wantSQL := "CREATE FULLTEXT INDEX `idx_fulltext_body` ON `archive`.`articles` (`title`, `body`)"
if !reflect.DeepEqual(unmigrated[0].RemediationStatements, []string{wantSQL}) {
t.Fatalf("unexpected fulltext remediation: got=%v want=%v", unmigrated[0].RemediationStatements, []string{wantSQL})
}
}
func TestBuildMongoIndexCommands_ListsUnmigratedIndexes(t *testing.T) {
t.Parallel()
sourceDB := &fakeMigrationDB{
indexes: map[string][]connection.IndexDefinition{
"shop.articles": {
{Name: "idx_lookup", ColumnName: "category", NonUnique: 1, SeqInIndex: 2, IndexType: "BTREE"},
{Name: "idx_lookup", ColumnName: "author", NonUnique: 1, SeqInIndex: 1, IndexType: "BTREE"},
{Name: "idx_title_prefix", ColumnName: "title", NonUnique: 1, SeqInIndex: 1, IndexType: "BTREE", SubPart: 12},
{Name: "idx_fulltext_body", ColumnName: "title", NonUnique: 1, SeqInIndex: 1, IndexType: "FULLTEXT"},
{Name: "idx_fulltext_body", ColumnName: "body", NonUnique: 1, SeqInIndex: 2, IndexType: "FULLTEXT"},
},
},
}
commands, warnings, unsupported, unmigrated, created, skipped, err := buildMongoIndexCommands(sourceDB, "shop", "articles", "articles")
if err != nil {
t.Fatalf("buildMongoIndexCommands returned error: %v", err)
}
if created != 1 || skipped != 2 || len(commands) != 1 {
t.Fatalf("unexpected Mongo index summary: commands=%v create=%d skip=%d", commands, created, skipped)
}
if !strings.Contains(commands[0], `"key":{"author":1,"category":1}`) {
t.Fatalf("Mongo compound index order was not preserved: %s", commands[0])
}
if len(warnings) != 0 || len(unsupported) != 2 || len(unmigrated) != 2 {
t.Fatalf("unexpected Mongo index warnings: warnings=%v unsupported=%v unmigrated=%+v", warnings, unsupported, unmigrated)
}
if unmigrated[0].ReasonCode != "prefix_index_requires_review" || !strings.Contains(strings.Join(unmigrated[0].RemediationStatements, "\n"), `"key":{"title":1}`) {
t.Fatalf("unexpected prefix remediation: %+v", unmigrated[0])
}
if unmigrated[1].ReasonCode != "fulltext_requires_review" || !strings.Contains(strings.Join(unmigrated[1].RemediationStatements, "\n"), `"key":{"title":"text","body":"text"}`) {
t.Fatalf("unexpected fulltext remediation: %+v", unmigrated[1])
}
}
func TestBuildSchemaMigrationPlan_PGLikeToPGLikeAutoCreatesMissingTarget(t *testing.T) {
t.Parallel()
@@ -536,7 +681,7 @@ func TestBuildMySQLToPGLikeCreateTablePlan_GeneratesPostgresDDL(t *testing.T) {
{Name: "payload", Type: "json", Nullable: "YES"},
}
cfg := SyncConfig{CreateIndexes: true}
createSQL, postSQL, warnings, unsupported, idxCreate, idxSkip, err := buildMySQLToPGLikeCreateTablePlan("postgres", cfg, "public.orders", cols, sourceDB, "shop", "orders")
createSQL, postSQL, warnings, unsupported, unmigrated, idxCreate, idxSkip, err := buildMySQLToPGLikeCreateTablePlan("postgres", cfg, "public.orders", cols, sourceDB, "shop", "orders")
if err != nil {
t.Fatalf("buildMySQLToPGLikeCreateTablePlan returned error: %v", err)
}
@@ -555,8 +700,8 @@ func TestBuildMySQLToPGLikeCreateTablePlan_GeneratesPostgresDDL(t *testing.T) {
if len(postSQL) != 1 || !strings.Contains(postSQL[0], `CREATE INDEX "idx_orders_user"`) {
t.Fatalf("unexpected post SQL: %v", postSQL)
}
if len(warnings) != 0 || len(unsupported) != 0 {
t.Fatalf("unexpected warnings/unsupported: warnings=%v unsupported=%v", warnings, unsupported)
if len(warnings) != 0 || len(unsupported) != 0 || len(unmigrated) != 0 {
t.Fatalf("unexpected warnings/unsupported: warnings=%v unsupported=%v unmigrated=%v", warnings, unsupported, unmigrated)
}
}