Files
gitea-xorm-xorm/sync.go
T

666 lines
27 KiB
Go

// Copyright 2023 The Xorm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package xorm
import (
"fmt"
"strings"
"xorm.io/xorm/dialects"
"xorm.io/xorm/internal/utils"
"xorm.io/xorm/schemas"
)
// columnTypeSyncAction is the action to take when a struct column's type
// does not render identically to the database column's type.
type columnTypeSyncAction int
const (
// columnTypeSyncActionNone means the difference is silently accepted.
columnTypeSyncActionNone columnTypeSyncAction = iota
// columnTypeSyncActionWarn means a warning is logged but nothing changes.
columnTypeSyncActionWarn
// columnTypeSyncActionWarnTextFromVarchar means a warning is logged for
// an unsupported text-from-varchar conversion. v1 logs this with a
// trailing newline, unlike the plain columnTypeSyncActionWarn message,
// so it needs its own action to reproduce that format exactly.
columnTypeSyncActionWarnTextFromVarchar
// columnTypeSyncActionModify means ModifyColumnSQL converts the column type.
columnTypeSyncActionModify
// columnTypeSyncActionModifyVarcharExpand means ModifyColumnSQL widens a varchar column.
columnTypeSyncActionModifyVarcharExpand
)
// columnSyncDecision is what Sync should do for one column once its
// comparison against the database column is known.
type columnSyncDecision struct {
typeAction columnTypeSyncAction
modifyComment bool
// modifyCommentOnly is set alongside modifyComment when
// resolveCommentSyncDecision resolved the sync through a dialect's
// comment-only DDL path (ColumnSyncFeatures.ColumnCommentOnly), so
// applyColumnSyncDecision calls ModifyColumnCommentSQL - which emits
// no type-altering clause at all - instead of the full-rewrite
// ModifyColumnSQL.
modifyCommentOnly bool
// commentSkippedTypeMismatch is set when the comment differs and the
// dialect supports comment sync, but resolveCommentSyncDecision
// refused to run it because the rendered types are not byte-identical
// and the dialect has no comment-only DDL path to fall back to.
// applyColumnSyncDecision logs this at Infof (not Warnf) so the
// trade-off is visible instead of silent, without resurfacing the
// warnings xorm/xorm#2583 and go-gitea/gitea#22275 removed: every row
// this can fire on is ColumnCompareEquivalent, a pair
// compareColumnTypes itself considers the same type (xorm/xorm#2591).
commentSkippedTypeMismatch bool
warnDefault bool
warnNullable bool
}
// resolveColumnTypeSyncAction decides what to do about a type difference.
// It mirrors the inner switch of v1's Sync column loop:
// - text-from-varchar always gets a decision (modify if the dialect
// supports it, otherwise warn);
// - varchar-to-varchar only modifies when the dialect supports widening
// and the database column is actually shorter, and stays silent
// otherwise (no warning for a shrink or an unsupported dialect);
// - anything else warns. A bare struct type against a differently-sized
// literal column type of the same base name (in either synonym
// direction, e.g. "NUMERIC" struct tag against a literal
// "DECIMAL(10,2)" column) never reaches this arm at all: compareColumnTypes'
// level 4 base-sql-type-name fallback already classifies it
// ColumnCompareEquivalent and IsDifferent() is false, so the early
// return above handles it.
func resolveColumnTypeSyncAction(features dialects.ColumnSyncFeatures, comparison dialects.ColumnComparison) columnTypeSyncAction {
if !comparison.Type.IsDifferent() {
return columnTypeSyncActionNone
}
expectedType := comparison.Type.Expected
actualType := comparison.Type.Actual
switch {
case expectedType == schemas.Text && strings.HasPrefix(actualType, schemas.Varchar):
if features.TextFromVarchar {
return columnTypeSyncActionModify
}
return columnTypeSyncActionWarnTextFromVarchar
case isVarcharToVarchar(expectedType, actualType):
if features.VarcharLengthChange && comparison.Actual.Length < comparison.Expected.Length {
return columnTypeSyncActionModifyVarcharExpand
}
return columnTypeSyncActionNone
default:
return columnTypeSyncActionWarn
}
}
// isVarcharToVarchar reports whether both rendered types are varchar,
// checked as a raw string prefix rather than through compareColumnTypes's
// alias/normalisation logic: neither resolveColumnTypeSyncAction (above)
// nor resolveVarcharWidenSyncAction (below) needs anything past this,
// since no dialect aliases varchar to or from another type name.
func isVarcharToVarchar(expectedType, actualType string) bool {
return strings.HasPrefix(actualType, schemas.Varchar) && strings.HasPrefix(expectedType, schemas.Varchar)
}
// resolveBareVarcharSyncAction handles v1's second outer-switch case,
// `case expectedType == schemas.Varchar`, which only fires once the first
// case (`!columnTypesMatch`, i.e. comparison.Type.IsDifferent()) is false.
// mysql/postgres/gbase8s only render a bare "VARCHAR" (no length suffix)
// when Length == 0, so `actual.Length < expected.Length` can never hold
// here and this arm can never actually widen a column - but v1 still
// dedicates an outer-switch case to it, and reaching that case, taken or
// not, prevents the next case (comment sync) from ever running for that
// column. It must be kept as its own branch so that exclusivity holds.
func resolveBareVarcharSyncAction(features dialects.ColumnSyncFeatures, comparison dialects.ColumnComparison) columnTypeSyncAction {
if features.VarcharLengthChange && comparison.Actual.Length < comparison.Expected.Length {
return columnTypeSyncActionModifyVarcharExpand
}
return columnTypeSyncActionNone
}
// resolveVarcharWidenSyncAction decides what to do once buildColumnSyncDecision
// has confirmed both rendered types are varchar and the type comparison
// already matches: compareColumnTypes's level 4 base-sql-type-name
// fallback reports any varchar/varchar pair as ColumnCompareEquivalent
// regardless of length - the same fallback that lets "NUMERIC" match a
// literal "DECIMAL(10,2)" column - so this is the only place a varchar
// length difference is visible to the sync policy in practice (see
// isVarcharToVarchar's case in resolveColumnTypeSyncAction above, which
// requires a genuine ColumnCompareDifferent and is therefore unreachable
// from a real dialect for a varchar/varchar pair).
//
// It widens only when the dialect supports it and the database column is
// both bounded and shorter than the struct wants. The bounded check
// matters: postgres reports an unbounded "character varying" column with
// Length == 0, and 0 < N would otherwise read as "shorter" and narrow an
// unbounded column into a bounded one - a silent capacity reduction, or a
// failing ALTER against data that no longer fits (xorm/xorm#2588 review).
// Otherwise it returns columnTypeSyncActionNone, never a warning, which is
// what keeps xorm/xorm#2583's suppression for a shrink or an unsupported
// dialect.
func resolveVarcharWidenSyncAction(features dialects.ColumnSyncFeatures, comparison dialects.ColumnComparison) columnTypeSyncAction {
if features.VarcharLengthChange && comparison.Actual.Length > 0 && comparison.Actual.Length < comparison.Expected.Length {
return columnTypeSyncActionModifyVarcharExpand
}
return columnTypeSyncActionNone
}
// resolveCommentSyncDecision is the single place that decides whether a
// differing comment should be synced, and whether to run it through the
// dialect's comment-only DDL (ColumnSyncFeatures.ColumnCommentOnly) or its
// full-rewrite ModifyColumnSQL. For a dialect without a comment-only path,
// applyColumnSyncDecision's modifyComment branch runs a full
// ModifyColumnSQL using the column's entire rendered Expected definition,
// not just its comment, so syncing a comment when the rendered types are
// merely ColumnCompareEquivalent (or worse, ColumnCompareDifferent)
// silently rewrites the column's type as a side effect - see
// xorm/xorm#2591, where a real MySQL/MariaDB DECIMAL(19,4) column against
// a struct tagged DECIMAL(10,2) plus a comment change rounded
// already-stored data with Sync reporting success. Requiring
// ColumnCompareEqual there makes ModifyColumnSQL's own type text a no-op,
// since it reissues the column's own already-matching rendered type.
//
// A dialect with ColumnCommentOnly true (postgres, gbase8s) does not need
// that restriction: ModifyColumnCommentSQL never renders a type clause at
// all, so it is safe whenever the type comparison is not genuinely
// ColumnCompareDifferent - which buildColumnSyncDecision's own switch
// already guarantees before reaching this function (see its comment).
// This also closes xorm/xorm#2594's residual hole: a real SMALLINT column
// backed by nextval(...) renders "SERIAL" the same as a genuine
// integer-backed serial column (postgres.SQLType's default arm), so the
// pair is ColumnCompareEqual even though ALTER COLUMN ... TYPE INTEGER
// would genuinely widen it - the comment-only path avoids that ALTER
// entirely, rather than depending on the type substitution being exact.
//
// Every switch arm in buildColumnSyncDecision that might reach a comment
// sync MUST call this function rather than checking
// comparison.Comment.IsDifferent() directly - that is what makes the gate
// structurally hard to bypass: a second, ad hoc comment-sync site cannot
// reopen this hole as long as it goes through here instead of
// reimplementing the check.
//
// The cost of requiring ColumnCompareEqual is real and not marginal for a
// dialect stuck on the full-rewrite path - an Equivalent-but-not-Equal
// pair with a differing comment no longer gets its comment synced, on any
// struct tag combination that lands in ColumnCompareEquivalent rather than
// ColumnCompareEqual. Three shapes are common enough to name:
// - MariaDB 10.6 and MySQL <= 8.0.18 report a plain INT column as
// "int(11)"; a bare `xorm:"INT"` struct tag never renders a width, so
// every commented int-family column stops syncing its comment, on
// every table, including ones xorm itself created.
// - MySQL 8.0 (all versions, not just old ones) reports a TEXT column as
// "text(65535)"; every commented TEXT/BLOB-family column has the same
// problem there.
// - PostgreSQL reports a column declared DECIMAL(p,s) back as
// NUMERIC(p,s) (its own catalog normalizes the spelling); every
// commented DECIMAL/NUMERIC column loses comment sync as a result.
//
// In all three cases the reissued ModifyColumnSQL type text would have
// been a provable no-op ("INT(11)" == "INT", "TEXT(65535)" == "TEXT",
// "DECIMAL(10,2)" == "NUMERIC(10,2)" for Sync's purposes) - only the third
// one is recovered today, because postgres has a comment-only path;
// mysql/mariadb do not (MODIFY COLUMN's comment clause is part of the
// full column definition, with no standalone equivalent), so the first
// two stay gated on ColumnCompareEqual and keep paying this cost. Losing
// a comment update there is preferable to silently narrowing or rounding
// a column.
func resolveCommentSyncDecision(features dialects.ColumnSyncFeatures, comparison dialects.ColumnComparison) (modifyComment, commentOnly, skippedTypeMismatch bool) {
if !features.ColumnComment || !comparison.Comment.IsDifferent() {
return false, false, false
}
if features.ColumnCommentOnly {
return true, true, false
}
if comparison.Type.Status != dialects.ColumnCompareEqual {
return false, false, true
}
return true, false, false
}
// columnSyncFeaturesFor returns dialect's ColumnSyncFeatures with
// ColumnCommentOnly set from a live type assertion against
// dialects.ColumnCommentModifier, rather than from whatever the dialect's
// own Features() method happened to say. This is the single place that
// decides the flag, so it cannot disagree with the actual capability:
// buildColumnSyncDecision only ever sees ColumnCommentOnly true when
// dialect genuinely implements ModifyColumnCommentSQL, and
// applyColumnSyncDecision's own type assertion (see its
// decision.modifyCommentOnly branch) is checking the exact same fact.
func columnSyncFeaturesFor(dialect dialects.Dialect) dialects.ColumnSyncFeatures {
features := dialect.Features().ColumnSync
_, features.ColumnCommentOnly = dialect.(dialects.ColumnCommentModifier)
return features
}
// buildColumnSyncDecision turns a ColumnComparison into a columnSyncDecision.
// It mirrors v1's single outer switch, which has exactly one of four
// mutually exclusive outcomes for a column:
// 1. the type comparison differs (resolveColumnTypeSyncAction decides
// what, if anything, to do);
// 2. the type comparison already matches and the expected type renders as
// a bare "VARCHAR" (resolveBareVarcharSyncAction, effectively a no-op);
// 3. the type comparison already matches, both rendered types are
// varchar (resolveVarcharWidenSyncAction decides what, if anything, to
// do about the length; either way it then falls through to
// resolveCommentSyncDecision, same as outcome 4);
// 4. the type comparison already matches, the expected type is not a bare
// "VARCHAR" nor a varchar/varchar pair, and the comment differs
// (resolveCommentSyncDecision decides whether to sync it - see that
// function for why "already matches" here means ColumnCompareEqual,
// not merely not-Different);
// 5. none of the above (nothing to do for the type/comment).
//
// Because Go's switch takes the first matching case, v1 could reach the
// comment case only through outcome 4, never through 1 or 2 - even when
// those arms performed no SQL and logged nothing. Comment sync must stay
// unreachable from 1 and 2 for the same reason, which is why it is a
// separate switch case here rather than a condition on the resolved
// typeAction.
//
// xorm/xorm#2588 added outcome 3: without it, a varchar/varchar pair with
// a length difference is always ColumnCompareEquivalent (level 4's
// base-sql-type-name fallback), never Different, so it silently fell into
// outcome 4 or 5 and Sync stopped widening varchar columns. Outcome 3 is
// NOT exclusivity-equivalent to outcome 1 (a genuine ColumnCompareDifferent
// shadows the comment case unconditionally): when
// resolveVarcharWidenSyncAction resolves to no action (a shrink, an
// unbounded actual column, or a dialect without VarcharLengthChange, such
// as gbase8s), outcome 3 falls through to resolveCommentSyncDecision
// instead of shadowing it - and that shared gate is what stops a P1
// regression found in review: an earlier revision of this fix synced the
// comment unconditionally on that fall-through, without checking whether
// the type comparison was ColumnCompareEqual. Since a varchar/varchar pair
// with a length difference is never Equal, that unconditional sync still
// ran ModifyColumnSQL with the struct's full (narrower) column definition,
// silently shrinking the actual column as a side effect of "just" syncing
// its comment - exactly the bug resolveCommentSyncDecision exists to
// prevent. Routing outcome 3's fall-through through the same function
// closes that hole structurally instead of relying on this arm
// remembering to re-check it.
//
// Outcome 3 must be checked AFTER outcome 2: a bare "VARCHAR" expected
// type against a sized actual varchar satisfies both this case's
// condition and outcome 2's, and outcome 2's no-op-that-still-shadows-
// comment-sync behavior must keep taking priority for that shape, exactly
// as before this fix.
//
// This is the third time this switch's exclusivity has mattered (see the
// #2585 and #2586 history above); the next person changing it should read
// this whole comment, not just the case they are touching.
func buildColumnSyncDecision(features dialects.ColumnSyncFeatures, comparison dialects.ColumnComparison) columnSyncDecision {
decision := columnSyncDecision{
warnDefault: comparison.Default.IsDifferent(),
warnNullable: comparison.Nullable.IsDifferent(),
}
switch {
case comparison.Type.IsDifferent():
decision.typeAction = resolveColumnTypeSyncAction(features, comparison)
case comparison.Type.Expected == schemas.Varchar:
decision.typeAction = resolveBareVarcharSyncAction(features, comparison)
case isVarcharToVarchar(comparison.Type.Expected, comparison.Type.Actual):
decision.typeAction = resolveVarcharWidenSyncAction(features, comparison)
if decision.typeAction == columnTypeSyncActionNone {
decision.modifyComment, decision.modifyCommentOnly, decision.commentSkippedTypeMismatch = resolveCommentSyncDecision(features, comparison)
}
case comparison.Comment.IsDifferent():
decision.modifyComment, decision.modifyCommentOnly, decision.commentSkippedTypeMismatch = resolveCommentSyncDecision(features, comparison)
}
return decision
}
// applyColumnSyncDecision executes at most one statement for the column
// (ModifyColumnSQL, or ModifyColumnCommentSQL for a comment-only
// decision), then logs the default/nullable warnings. It mirrors v1's
// structure of assigning any exec error to a shared local, always running
// the default/nullable checks, and only returning the error afterwards -
// so a failing ALTER still produces both warnings before Sync aborts. A
// decision.modifyCommentOnly whose dialect turns out not to implement
// dialects.ColumnCommentModifier follows the same path: err is set, no
// statement runs, and the warnings still log before the error returns.
func applyColumnSyncDecision(session *Session, tableName, tableNameWithSchema string, comparison dialects.ColumnComparison, decision columnSyncDecision) error {
engine := session.engine
expected := comparison.Expected
actual := comparison.Actual
var err error
switch decision.typeAction {
case columnTypeSyncActionModifyVarcharExpand:
engine.logger.Infof("Table %s column %s change type from varchar(%d) to varchar(%d)\n",
tableNameWithSchema, expected.Name, actual.Length, expected.Length)
_, err = session.exec(engine.dialect.ModifyColumnSQL(tableNameWithSchema, expected))
case columnTypeSyncActionModify:
engine.logger.Infof("Table %s column %s change type from %s to %s\n",
tableNameWithSchema, expected.Name, comparison.Type.Actual, comparison.Type.Expected)
_, err = session.exec(engine.dialect.ModifyColumnSQL(tableNameWithSchema, expected))
case columnTypeSyncActionWarn:
engine.logger.Warnf("Table %s column %s db type is %s, struct type is %s",
tableNameWithSchema, expected.Name, comparison.Type.Actual, comparison.Type.Expected)
case columnTypeSyncActionWarnTextFromVarchar:
engine.logger.Warnf("Table %s column %s db type is %s, struct type is %s\n",
tableNameWithSchema, expected.Name, comparison.Type.Actual, comparison.Type.Expected)
}
if decision.modifyComment {
if decision.modifyCommentOnly {
modifier, ok := engine.dialect.(dialects.ColumnCommentModifier)
if !ok {
// decision.modifyCommentOnly is only ever set when the
// same type assertion succeeded moments earlier, in
// Sync's column loop, which is what sets
// features.ColumnCommentOnly in the first place - see
// that loop's comment. Reaching here means a decision
// was built by hand (as some unit tests do) against a
// dialect that does not actually implement
// ColumnCommentModifier; failing loudly here is
// deliberate, since silently falling back to
// ModifyColumnSQL's full rewrite is exactly the
// type-rewriting side effect this whole path exists to
// rule out.
err = fmt.Errorf("xorm: comment-only sync requested for column %q but dialect %T does not implement dialects.ColumnCommentModifier", expected.Name, engine.dialect)
} else {
_, err = session.exec(modifier.ModifyColumnCommentSQL(tableNameWithSchema, expected))
}
} else {
_, err = session.exec(engine.dialect.ModifyColumnSQL(tableNameWithSchema, expected))
}
} else if decision.commentSkippedTypeMismatch {
engine.logger.Infof("Table %s column %s comment not synced because db type is %s, struct type is %s",
tableNameWithSchema, expected.Name, comparison.Type.Actual, comparison.Type.Expected)
}
if decision.warnDefault {
engine.logger.Warnf("Table %s Column %s db default is %s, struct default is %s",
tableName, expected.Name, actual.Default, expected.Default)
}
if decision.warnNullable {
engine.logger.Warnf("Table %s Column %s db nullable is %v, struct nullable is %v",
tableName, expected.Name, actual.Nullable, expected.Nullable)
}
return err
}
type SyncOptions struct {
WarnIfDatabaseColumnMissed bool
// IgnoreConstrains will not add, delete or update unique constrains
IgnoreConstrains bool
// IgnoreIndices will not add or delete indices
IgnoreIndices bool
// IgnoreDropIndices will not delete indices
IgnoreDropIndices bool
}
type SyncResult struct{}
func shouldSyncColumn(col *schemas.Column) bool {
return col.MapType != schemas.ONLYFROMDB
}
func schemaTableForSync(table *schemas.Table) *schemas.Table {
filtered := schemas.NewTable(table.Name, table.Type)
filtered.StoreEngine = table.StoreEngine
filtered.Charset = table.Charset
filtered.Comment = table.Comment
filtered.Collation = table.Collation
includedColumns := make(map[string]struct{}, len(table.Columns()))
for _, col := range table.Columns() {
if !shouldSyncColumn(col) {
continue
}
clone := *col
clone.FieldIndex = append([]int(nil), col.FieldIndex...)
clone.Indexes = make(map[string]int, len(col.Indexes))
for name, idxType := range col.Indexes {
clone.Indexes[name] = idxType
}
filtered.AddColumn(&clone)
includedColumns[strings.ToLower(col.Name)] = struct{}{}
}
for _, index := range table.Indexes {
keep := true
for _, colName := range index.Cols {
if _, ok := includedColumns[strings.ToLower(colName)]; !ok {
keep = false
break
}
}
if !keep {
continue
}
filtered.AddIndex(&schemas.Index{
IsRegular: index.IsRegular,
Name: index.Name,
Type: index.Type,
Cols: append([]string(nil), index.Cols...),
})
}
return filtered
}
// Sync the new struct changes to database, this method will automatically add
// table, column, index, unique. but will not delete or change anything.
// If you change some field, you should change the database manually.
func (engine *Engine) Sync(beans ...any) error {
session := engine.NewSession()
defer session.Close()
return session.Sync(beans...)
}
// SyncWithOptions sync the database schemas according options and table structs
func (engine *Engine) SyncWithOptions(opts SyncOptions, beans ...any) (*SyncResult, error) {
session := engine.NewSession()
defer session.Close()
return session.SyncWithOptions(opts, beans...)
}
// Sync2 synchronize structs to database tables
// Depricated
func (engine *Engine) Sync2(beans ...any) error {
return engine.Sync(beans...)
}
// Sync2 synchronize structs to database tables
// Depricated
func (session *Session) Sync2(beans ...any) error {
return session.Sync(beans...)
}
// Sync synchronize structs to database tables
func (session *Session) Sync(beans ...any) error {
_, err := session.SyncWithOptions(SyncOptions{
WarnIfDatabaseColumnMissed: false,
IgnoreConstrains: false,
IgnoreIndices: false,
IgnoreDropIndices: false,
}, beans...)
return err
}
func (session *Session) SyncWithOptions(opts SyncOptions, beans ...any) (*SyncResult, error) {
engine := session.engine
if session.isAutoClose {
session.isAutoClose = false
defer session.Close()
}
tables, err := engine.dialect.GetTables(session.getQueryer(), session.ctx)
if err != nil {
return nil, err
}
session.autoResetStatement = false
defer func() {
session.autoResetStatement = true
session.resetStatement()
}()
var syncResult SyncResult
for _, bean := range beans {
v := utils.ReflectValue(bean)
table, err := engine.tagParser.ParseWithCache(v)
if err != nil {
return nil, err
}
syncTable := schemaTableForSync(table)
var tbName string
if len(session.statement.AltTableName) > 0 {
tbName = session.statement.AltTableName
} else {
tbName = engine.TableName(bean)
}
tbNameWithSchema := engine.tbNameWithSchema(tbName)
var oriTable *schemas.Table
for _, tb := range tables {
if strings.EqualFold(engine.tbNameWithSchema(tb.Name), engine.tbNameWithSchema(tbName)) {
oriTable = tb
break
}
}
// this is a new table
if oriTable == nil {
syncTable.StoreEngine = session.statement.StoreEngine
syncTable.Charset = session.statement.Charset
session.statement.RefTable = syncTable
session.statement.SetTableName(tbNameWithSchema)
err = session.createCurrentTable()
if err != nil {
return nil, err
}
if !opts.IgnoreConstrains {
err = session.createCurrentUniques()
if err != nil {
return nil, err
}
}
if !opts.IgnoreIndices {
err = session.createCurrentIndexes()
if err != nil {
return nil, err
}
}
continue
}
// this will modify an old table
if err = engine.loadTableInfo(session.ctx, oriTable); err != nil {
return nil, err
}
// check columns
for _, col := range syncTable.Columns() {
var oriCol *schemas.Column
for _, col2 := range oriTable.Columns() {
if strings.EqualFold(col.Name, col2.Name) {
oriCol = col2
break
}
}
// column is not exist on table
if oriCol == nil {
session.statement.RefTable = syncTable
session.statement.SetTableName(tbNameWithSchema)
if err = session.addColumn(col.Name); err != nil {
return nil, err
}
continue
}
comparison := engine.dialect.CompareColumns(col, oriCol)
decision := buildColumnSyncDecision(columnSyncFeaturesFor(engine.dialect), comparison)
if err = applyColumnSyncDecision(session, tbName, tbNameWithSchema, comparison, decision); err != nil {
return nil, err
}
}
// indices found in orig table
foundIndexNames := make(map[string]bool)
// indices to be added
addedNames := make(map[string]*schemas.Index)
// drop indices that exist in orig and new table schema but are not equal
for name, index := range syncTable.Indexes {
var oriIndex *schemas.Index
for name2, index2 := range oriTable.Indexes {
if index.Equal(index2) {
oriIndex = index2
foundIndexNames[name2] = true
break
}
}
if oriIndex == nil {
addedNames[name] = index
}
}
// drop all indices that do not exist in new schema or have changed
for name2, index2 := range oriTable.Indexes {
if _, ok := foundIndexNames[name2]; !ok {
// ignore based on there type
if (index2.Type == schemas.IndexType && (opts.IgnoreIndices || opts.IgnoreDropIndices)) ||
(index2.Type == schemas.UniqueType && opts.IgnoreConstrains) {
// make sure we do not add a index with same name later
delete(addedNames, name2)
continue
}
sql := engine.dialect.DropIndexSQL(tbNameWithSchema, index2)
_, err = session.exec(sql)
if err != nil {
return nil, err
}
}
}
// Add new indices because either they did not exist before or were dropped to update them
for name, index := range addedNames {
if index.Type == schemas.UniqueType && !opts.IgnoreConstrains {
session.statement.RefTable = syncTable
session.statement.SetTableName(tbNameWithSchema)
err = session.addUnique(tbNameWithSchema, name)
} else if index.Type == schemas.IndexType && !opts.IgnoreIndices {
session.statement.RefTable = syncTable
session.statement.SetTableName(tbNameWithSchema)
err = session.addIndex(tbNameWithSchema, name)
}
if err != nil {
return nil, err
}
}
if opts.WarnIfDatabaseColumnMissed {
// check all the columns which removed from struct fields but left on database tables.
for _, colName := range oriTable.ColumnsSeq() {
if table.GetColumn(colName) == nil {
engine.logger.Warnf("Table %s has column %s but struct has not related field", engine.TableName(oriTable.Name, true), colName)
}
}
}
}
return &syncResult, nil
}