Upstream gitea-xorm/xorm state of the touched files before ea8835f2c2
This commit is contained in:
1 parent
9fa1420535
commit
ebe852f332
1 file changed
+665
@@ -0,0 +1,665 @@
|
||||
// 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
|
||||
}
|
||||
Reference in new issue
Block a user