Sync paired each declared index with the first existing index that had an equal definition and did not mark that index as taken. Two declared indexes with the same definition could therefore claim the same database index: the other one was dropped as unmatched and never recreated, or a missing one was never added. Claim an existing index with the same name and an equal definition first, then let the remaining declared indexes fall back to any unclaimed existing index with an equal definition, so renamed indexes are still kept as before. Iterate in name order so the result no longer depends on map order.
695 lines
29 KiB
Go
695 lines
29 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"
|
|
"sort"
|
|
"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
|
|
}
|
|
|
|
// matchIndexes pairs every expected index with at most one existing index.
|
|
// An existing index with the same name and an equal definition is claimed
|
|
// first, so indexes sharing a definition cannot claim each other's database
|
|
// index. The remaining expected indexes fall back to any unclaimed existing
|
|
// index with an equal definition, which keeps renamed indexes in place.
|
|
// It returns the names of the claimed existing indexes and the expected
|
|
// indexes that have no counterpart and must be created.
|
|
func matchIndexes(expected, existing map[string]*schemas.Index) (map[string]bool, map[string]*schemas.Index) {
|
|
claimed := make(map[string]bool, len(existing))
|
|
missing := make(map[string]*schemas.Index)
|
|
|
|
var unmatched []string
|
|
for _, name := range sortedIndexNames(expected) {
|
|
if actual, ok := existing[name]; ok && expected[name].Equal(actual) {
|
|
claimed[name] = true
|
|
continue
|
|
}
|
|
unmatched = append(unmatched, name)
|
|
}
|
|
|
|
existingNames := sortedIndexNames(existing)
|
|
for _, name := range unmatched {
|
|
index := expected[name]
|
|
found := false
|
|
for _, existingName := range existingNames {
|
|
if !claimed[existingName] && index.Equal(existing[existingName]) {
|
|
claimed[existingName] = true
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
missing[name] = index
|
|
}
|
|
}
|
|
return claimed, missing
|
|
}
|
|
|
|
func sortedIndexNames(indexes map[string]*schemas.Index) []string {
|
|
names := make([]string, 0, len(indexes))
|
|
for name := range indexes {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
return names
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
foundIndexNames, addedNames := matchIndexes(syncTable.Indexes, oriTable.Indexes)
|
|
|
|
// 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
|
|
}
|