From 4d5b61467ac895050f6b8c20694a58f0960c245f Mon Sep 17 00:00:00 2001 From: Shlomi Noach <2607934+shlomi-noach@users.noreply.github.com> Date: Wed, 18 Oct 2023 12:03:17 +0300 Subject: [PATCH] schemadiff: improved heuristic for dependent migration permutation evaluation time (#14249) Signed-off-by: Shlomi Noach <2607934+shlomi-noach@users.noreply.github.com> --- go/vt/schemadiff/diff_test.go | 9 +- go/vt/schemadiff/errors.go | 5 +- go/vt/schemadiff/schema_diff.go | 103 ++++++++++++++++++---- go/vt/schemadiff/schema_diff_test.go | 126 +++++++++++++++++++++++---- go/vt/schemadiff/table.go | 61 +++++++++---- go/vt/schemadiff/view.go | 45 +++++++--- 6 files changed, 280 insertions(+), 69 deletions(-) diff --git a/go/vt/schemadiff/diff_test.go b/go/vt/schemadiff/diff_test.go index 291049a22ad..8676e1bab29 100644 --- a/go/vt/schemadiff/diff_test.go +++ b/go/vt/schemadiff/diff_test.go @@ -17,6 +17,7 @@ limitations under the License. package schemadiff import ( + "context" "testing" "github.com/stretchr/testify/assert" @@ -403,6 +404,7 @@ func TestDiffViews(t *testing.T) { } func TestDiffSchemas(t *testing.T) { + ctx := context.Background() tt := []struct { name string from string @@ -806,7 +808,7 @@ func TestDiffSchemas(t *testing.T) { } else { assert.NoError(t, err) - diffs, err := diff.OrderedDiffs() + diffs, err := diff.OrderedDiffs(ctx) assert.NoError(t, err) statements := []string{} cstatements := []string{} @@ -858,6 +860,7 @@ func TestDiffSchemas(t *testing.T) { } func TestSchemaApplyError(t *testing.T) { + ctx := context.Background() tt := []struct { name string from string @@ -900,7 +903,7 @@ func TestSchemaApplyError(t *testing.T) { { diff, err := schema1.SchemaDiff(schema2, hints) require.NoError(t, err) - diffs, err := diff.OrderedDiffs() + diffs, err := diff.OrderedDiffs(ctx) assert.NoError(t, err) assert.NotEmpty(t, diffs) _, err = schema1.Apply(diffs) @@ -911,7 +914,7 @@ func TestSchemaApplyError(t *testing.T) { { diff, err := schema2.SchemaDiff(schema1, hints) require.NoError(t, err) - diffs, err := diff.OrderedDiffs() + diffs, err := diff.OrderedDiffs(ctx) assert.NoError(t, err) assert.NotEmpty(t, diffs, "schema1: %v, schema2: %v", schema1.ToSQL(), schema2.ToSQL()) _, err = schema2.Apply(diffs) diff --git a/go/vt/schemadiff/errors.go b/go/vt/schemadiff/errors.go index 771c650e51d..8317fbe9cea 100644 --- a/go/vt/schemadiff/errors.go +++ b/go/vt/schemadiff/errors.go @@ -40,8 +40,9 @@ type ImpossibleApplyDiffOrderError struct { func (e *ImpossibleApplyDiffOrderError) Error() string { var b strings.Builder - b.WriteString("no valid applicable order for diffs. Diffs found conflicting:") - for _, s := range e.ConflictingStatements() { + conflictingStatements := e.ConflictingStatements() + b.WriteString(fmt.Sprintf("no valid applicable order for diffs. %d diffs found conflicting:", len(conflictingStatements))) + for _, s := range conflictingStatements { b.WriteString("\n") b.WriteString(s) } diff --git a/go/vt/schemadiff/schema_diff.go b/go/vt/schemadiff/schema_diff.go index b6c539aea95..8fef7c29d28 100644 --- a/go/vt/schemadiff/schema_diff.go +++ b/go/vt/schemadiff/schema_diff.go @@ -17,7 +17,9 @@ limitations under the License. package schemadiff import ( + "context" "fmt" + "sort" "vitess.io/vitess/go/mathutil" ) @@ -68,6 +70,16 @@ func (d *DiffDependency) Type() DiffDependencyType { return d.typ } +// IsInOrder returns true if this dependency indicates a known order +func (d *DiffDependency) IsInOrder() bool { + return d.typ >= DiffDependencyInOrderCompletion +} + +// IsSequential returns true if this is a sequential dependency +func (d *DiffDependency) IsSequential() bool { + return d.typ >= DiffDependencySequentialExecution +} + /* The below is adapted from https://yourbasic.org/golang/generate-permutation-slice-string/ Licensed under https://creativecommons.org/licenses/by/3.0/ @@ -76,31 +88,74 @@ Modified to have an early break // permutateDiffs calls `callback` with each permutation of a. If the function returns `true`, that means // the callback has returned `true` for an early break, thus possibly not all permutations have been evaluated. -func permutateDiffs(a []EntityDiff, callback func([]EntityDiff) (earlyBreak bool)) (earlyBreak bool) { - if len(a) == 0 { - return false +func permutateDiffs(ctx context.Context, diffs []EntityDiff, callback func([]EntityDiff) (earlyBreak bool)) (earlyBreak bool, err error) { + if len(diffs) == 0 { + return false, nil } - return permDiff(a, callback, 0) + // Sort by a heristic (DROPs first, ALTERs next, CREATEs last). This ordering is then used first in the permutation + // search and serves as seed for the rest of permutations. + + return permDiff(ctx, diffs, callback, 0) } // permDiff is a recursive function to permutate given `a` and call `callback` for each permutation. // If `callback` returns `true`, then so does this function, and this indicates a request for an early // break, in which case this function will not be called again. -func permDiff(a []EntityDiff, callback func([]EntityDiff) (earlyBreak bool), i int) (earlyBreak bool) { +func permDiff(ctx context.Context, a []EntityDiff, callback func([]EntityDiff) (earlyBreak bool), i int) (earlyBreak bool, err error) { + if err := ctx.Err(); err != nil { + return true, err // early break + } if i > len(a) { - return callback(a) + return callback(a), nil } - if permDiff(a, callback, i+1) { - return true + if brk, err := permDiff(ctx, a, callback, i+1); brk { + return true, err } for j := i + 1; j < len(a); j++ { + // An optimization: we don't really need all possible permutations. We can skip some of the recursive search. + // We know we begin with a heuristic order where DROP VIEW comes first, then DROP TABLE, then ALTER TABLE & VIEW, + // then CREATE TABLE, then CREATE VIEW. And the entities in that initial order are sorted by dependency. That's + // thank's to Schema's UnorderedDiffs() existing heuristic. + // Now, some pairs of statements should be permutated, but some others will have absolutely no advantage to permutate. + // For example, a DROP VIEW and CREATE VIEW: there's no advantage to permutate the two. If the initial order is + // inapplicable, then so will be the permutated order. + // The next section identifies some no-brainers conditions for skipping swapping of elements. + // There could be even more fine grained scenarios, which we can deal with in the future. + iIsCreateDropView := false + iIsTable := false + switch a[i].(type) { + case *DropViewEntityDiff, *CreateViewEntityDiff: + iIsCreateDropView = true + case *DropTableEntityDiff, *AlterTableEntityDiff, *CreateTableEntityDiff: + iIsTable = true + } + + jIsCreateDropView := false + jIsTable := false + switch a[j].(type) { + case *DropViewEntityDiff, *CreateViewEntityDiff: + jIsCreateDropView = true + case *DropTableEntityDiff, *AlterTableEntityDiff, *CreateTableEntityDiff: + jIsTable = true + } + + if iIsCreateDropView && jIsCreateDropView { + continue + } + if iIsCreateDropView && jIsTable { + continue + } + if iIsTable && jIsCreateDropView { + continue + } + // End of optimization a[i], a[j] = a[j], a[i] - if permDiff(a, callback, i+1) { - return true + if brk, err := permDiff(ctx, a, callback, i+1); brk { + return true, err } a[i], a[j] = a[j], a[i] } - return false + return false, nil } // SchemaDiff is a rich diff between two schemas. It includes the following: @@ -232,10 +287,15 @@ func (d *SchemaDiff) HasSequentialExecutionDependencies() bool { // OrderedDiffs returns the list of diff in applicable order, if possible. This is a linearized representation // where diffs may be applied in-order one after another, keeping the schema in valid state at all times. -func (d *SchemaDiff) OrderedDiffs() ([]EntityDiff, error) { - lastGoodSchema := d.schema +func (d *SchemaDiff) OrderedDiffs(ctx context.Context) ([]EntityDiff, error) { + lastGoodSchema := d.schema.copy() var orderedDiffs []EntityDiff m := d.r.Map() + + unorderedDiffsMap := map[string]int{} + for i, diff := range d.UnorderedDiffs() { + unorderedDiffsMap[diff.CanonicalStatementString()] = i + } // The order of classes in the quivalence relation is, generally speaking, loyal to the order of original diffs. for _, class := range d.r.OrderedClasses() { classDiffs := []EntityDiff{} @@ -247,15 +307,18 @@ func (d *SchemaDiff) OrderedDiffs() ([]EntityDiff, error) { } classDiffs = append(classDiffs, diff) } + sort.SliceStable(classDiffs, func(i, j int) bool { + return unorderedDiffsMap[classDiffs[i].CanonicalStatementString()] < unorderedDiffsMap[classDiffs[j].CanonicalStatementString()] + }) + // We will now permutate the diffs in this equivalence class, and hopefully find // a valid permutation (one where if we apply the diffs in-order, the schema remains valid throughout the process) - foundValidPathForClass := permutateDiffs(classDiffs, func(permutatedDiffs []EntityDiff) bool { - permutationSchema := lastGoodSchema + foundValidPathForClass, err := permutateDiffs(ctx, classDiffs, func(permutatedDiffs []EntityDiff) bool { + permutationSchema := lastGoodSchema.copy() // We want to apply the changes one by one, and validate the schema after each change - var err error for i := range permutatedDiffs { - permutationSchema, err = permutationSchema.Apply(permutatedDiffs[i : i+1]) - if err != nil { + // apply inline + if err := permutationSchema.apply(permutatedDiffs[i : i+1]); err != nil { // permutation is invalid return false // continue searching } @@ -265,6 +328,9 @@ func (d *SchemaDiff) OrderedDiffs() ([]EntityDiff, error) { lastGoodSchema = permutationSchema return true // early break! No need to keep searching }) + if err != nil { + return nil, err + } if !foundValidPathForClass { // In this equivalence class, there is no valid permutation. We cannot linearize the diffs. return nil, &ImpossibleApplyDiffOrderError{ @@ -272,6 +338,7 @@ func (d *SchemaDiff) OrderedDiffs() ([]EntityDiff, error) { ConflictingDiffs: classDiffs, } } + // Done taking care of this equivalence class. } return orderedDiffs, nil diff --git a/go/vt/schemadiff/schema_diff_test.go b/go/vt/schemadiff/schema_diff_test.go index 670e84c6f1a..03d7c3e2766 100644 --- a/go/vt/schemadiff/schema_diff_test.go +++ b/go/vt/schemadiff/schema_diff_test.go @@ -17,6 +17,7 @@ limitations under the License. package schemadiff import ( + "context" "strings" "testing" @@ -25,6 +26,7 @@ import ( ) func TestPermutations(t *testing.T) { + ctx := context.Background() tt := []struct { name string fromQueries []string @@ -81,7 +83,80 @@ func TestPermutations(t *testing.T) { "create view v2 as select id from t2", }, expectDiffs: 4, - expectPermutations: 24, + expectPermutations: 8, // because CREATE VIEW does not permutate with TABLE operations + }, + { + name: "multiple drop view diffs", + fromQueries: []string{ + "create table t1 (id int primary key, info int not null);", + "create view v1 as select id from t1", + "create view v2 as select id from v1", + "create view v0 as select id from v2", + }, + toQueries: []string{ + "create table t1 (id int primary key, info int not null);", + }, + expectDiffs: 3, + expectPermutations: 1, // because DROP VIEW don't permutate between themselves + }, + { + name: "multiple drop view diffs with ALTER TABLE", + fromQueries: []string{ + "create table t1 (id int primary key, info int not null);", + "create view v1 as select id from t1", + "create view v2 as select id from v1", + "create view v0 as select id from v2", + }, + toQueries: []string{ + "create table t1 (id int primary key, info bigint not null);", + }, + expectDiffs: 4, + expectPermutations: 1, // because DROP VIEW don't permutate between themselves and with TABLE operations + }, + { + name: "multiple create view diffs", + fromQueries: []string{ + "create table t1 (id int primary key, info int not null);", + }, + toQueries: []string{ + "create table t1 (id int primary key, info int not null);", + "create view v1 as select id from t1", + "create view v2 as select id from v1", + "create view v3 as select id from v2", + }, + expectDiffs: 3, + expectPermutations: 1, // because CREATE VIEW don't permutate between themselves + }, + { + name: "multiple create view diffs with ALTER TABLE", + fromQueries: []string{ + "create table t1 (id int primary key, info int not null);", + }, + toQueries: []string{ + "create table t1 (id int primary key, info bigint not null);", + "create view v1 as select id from t1", + "create view v2 as select id from v1", + "create view v3 as select id from v2", + }, + expectDiffs: 4, + expectPermutations: 1, // because CREATE VIEW don't permutate between themselves and with TABLE operations + }, + { + name: "multiple create and drop view diffs with ALTER TABLE", + fromQueries: []string{ + "create table t1 (id int primary key, info int not null);", + "create view v101 as select id from t1", + "create view v102 as select id from v101", + "create view v103 as select id from v102", + }, + toQueries: []string{ + "create table t1 (id int primary key, info bigint not null);", + "create view v201 as select id from t1", + "create view v202 as select id from v201", + "create view v203 as select id from v202", + }, + expectDiffs: 7, + expectPermutations: 1, // because CREATE/DROP VIEW don't permutate between themselves and with TABLE operations }, } hints := &DiffHints{RangeRotationStrategy: RangeRotationDistinctStatements} @@ -112,29 +187,34 @@ func TestPermutations(t *testing.T) { t.Run("no early break", func(t *testing.T) { iteration := 0 allPerms := map[string]bool{} + allPermsStatements := []string{} allDiffs := schemaDiff.UnorderedDiffs() originalSingleString := toSingleString(allDiffs) - earlyBreak := permutateDiffs(allDiffs, func(pdiffs []EntityDiff) (earlyBreak bool) { + numEquals := 0 + earlyBreak, err := permutateDiffs(ctx, allDiffs, func(pdiffs []EntityDiff) (earlyBreak bool) { + defer func() { iteration++ }() // cover all permutations - allPerms[toSingleString(pdiffs)] = true - if iteration == 0 { - // First permutation should be the same as original - require.Equal(t, originalSingleString, toSingleString(pdiffs)) - } else { - // rest of permutations must be different than original (later we also verify they are all unique) - require.NotEqualf(t, originalSingleString, toSingleString(pdiffs), "in iteration %d", iteration) + singleString := toSingleString(pdiffs) + allPerms[singleString] = true + allPermsStatements = append(allPermsStatements, singleString) + if originalSingleString == singleString { + numEquals++ } - iteration++ return false }) + assert.NoError(t, err) + if len(allDiffs) > 0 { + assert.Equal(t, numEquals, 1) + } + assert.False(t, earlyBreak) - assert.Equal(t, tc.expectPermutations, len(allPerms)) + assert.Equalf(t, tc.expectPermutations, len(allPerms), "all perms: %v", strings.Join(allPermsStatements, "\n")) }) t.Run("early break", func(t *testing.T) { allPerms := map[string]bool{} allDiffs := schemaDiff.UnorderedDiffs() originalSingleString := toSingleString(allDiffs) - earlyBreak := permutateDiffs(allDiffs, func(pdiffs []EntityDiff) (earlyBreak bool) { + earlyBreak, err := permutateDiffs(ctx, allDiffs, func(pdiffs []EntityDiff) (earlyBreak bool) { // Single visit allPerms[toSingleString(pdiffs)] = true // First permutation should be the same as original @@ -142,6 +222,7 @@ func TestPermutations(t *testing.T) { // early break; this callback function should not be invoked again return true }) + assert.NoError(t, err) if len(allDiffs) > 0 { assert.True(t, earlyBreak) assert.Equal(t, 1, len(allPerms)) @@ -155,7 +236,20 @@ func TestPermutations(t *testing.T) { } } +func TestPermutationsContext(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + allDiffs := []EntityDiff{&DropViewEntityDiff{}} + earlyBreak, err := permutateDiffs(ctx, allDiffs, func(pdiffs []EntityDiff) (earlyBreak bool) { + return false + }) + assert.True(t, earlyBreak) // proves that termination was due to context cancel + assert.Error(t, err) // proves that termination was due to context cancel +} + func TestSchemaDiff(t *testing.T) { + ctx := context.Background() var ( createQueries = []string{ "create table t1 (id int primary key, info int not null);", @@ -336,13 +430,11 @@ func TestSchemaDiff(t *testing.T) { "create table t1 (id int primary key, info int not null, dt datetime);", "create table t2 (id int primary key, ts timestamp, v varchar);", "create view v1 as select id from t1", - // "create view v2 as select id from t1", "create view v2 as select info, ts from t1, t2", - // "create view v2 as select info, ts from t1, t2", }, expectDiffs: 3, expectDeps: 2, - entityOrder: []string{"t1", "v2", "t2"}, + entityOrder: []string{"t1", "t2", "v2"}, }, { name: "alter view depending on 2 tables, uses new column, alter tables", @@ -682,7 +774,7 @@ func TestSchemaDiff(t *testing.T) { assert.Equalf(t, tc.expectDeps, len(deps), "found deps: %v", depsKeys) assert.Equal(t, tc.sequential, schemaDiff.HasSequentialExecutionDependencies()) - orderedDiffs, err := schemaDiff.OrderedDiffs() + orderedDiffs, err := schemaDiff.OrderedDiffs(ctx) if tc.conflictingDiffs > 0 { require.Greater(t, tc.conflictingDiffs, 1) // self integrity. If there's a conflict, then obviously there's at least two conflicting diffs (a single diff has nothing to conflict with) assert.Error(t, err) @@ -690,7 +782,7 @@ func TestSchemaDiff(t *testing.T) { assert.True(t, ok) assert.Equal(t, tc.conflictingDiffs, len(impossibleOrderErr.ConflictingDiffs)) } else { - require.NoError(t, err) + require.NoErrorf(t, err, "Unordered diffs: %v", allDiffsStatements) } diffStatementStrings := []string{} for _, diff := range orderedDiffs { diff --git a/go/vt/schemadiff/table.go b/go/vt/schemadiff/table.go index fee70c96e88..73178ee31fe 100644 --- a/go/vt/schemadiff/table.go +++ b/go/vt/schemadiff/table.go @@ -36,7 +36,8 @@ type AlterTableEntityDiff struct { to *CreateTableEntity alterTable *sqlparser.AlterTable - subsequentDiff *AlterTableEntityDiff + canonicalStatementString string + subsequentDiff *AlterTableEntityDiff } // IsEmpty implements EntityDiff @@ -79,11 +80,16 @@ func (d *AlterTableEntityDiff) StatementString() (s string) { } // CanonicalStatementString implements EntityDiff -func (d *AlterTableEntityDiff) CanonicalStatementString() (s string) { - if stmt := d.Statement(); stmt != nil { - s = sqlparser.CanonicalString(stmt) +func (d *AlterTableEntityDiff) CanonicalStatementString() string { + if d == nil { + return "" } - return s + if d.canonicalStatementString == "" { + if stmt := d.Statement(); stmt != nil { + d.canonicalStatementString = sqlparser.CanonicalString(stmt) + } + } + return d.canonicalStatementString } // SubsequentDiff implements EntityDiff @@ -118,6 +124,8 @@ func (d *AlterTableEntityDiff) addSubsequentDiff(diff *AlterTableEntityDiff) { type CreateTableEntityDiff struct { to *CreateTableEntity createTable *sqlparser.CreateTable + + canonicalStatementString string } // IsEmpty implements EntityDiff @@ -160,11 +168,16 @@ func (d *CreateTableEntityDiff) StatementString() (s string) { } // CanonicalStatementString implements EntityDiff -func (d *CreateTableEntityDiff) CanonicalStatementString() (s string) { - if stmt := d.Statement(); stmt != nil { - s = sqlparser.CanonicalString(stmt) +func (d *CreateTableEntityDiff) CanonicalStatementString() string { + if d == nil { + return "" } - return s + if d.canonicalStatementString == "" { + if stmt := d.Statement(); stmt != nil { + d.canonicalStatementString = sqlparser.CanonicalString(stmt) + } + } + return d.canonicalStatementString } // SubsequentDiff implements EntityDiff @@ -179,6 +192,8 @@ func (d *CreateTableEntityDiff) SetSubsequentDiff(EntityDiff) { type DropTableEntityDiff struct { from *CreateTableEntity dropTable *sqlparser.DropTable + + canonicalStatementString string } // IsEmpty implements EntityDiff @@ -221,11 +236,16 @@ func (d *DropTableEntityDiff) StatementString() (s string) { } // CanonicalStatementString implements EntityDiff -func (d *DropTableEntityDiff) CanonicalStatementString() (s string) { - if stmt := d.Statement(); stmt != nil { - s = sqlparser.CanonicalString(stmt) +func (d *DropTableEntityDiff) CanonicalStatementString() string { + if d == nil { + return "" } - return s + if d.canonicalStatementString == "" { + if stmt := d.Statement(); stmt != nil { + d.canonicalStatementString = sqlparser.CanonicalString(stmt) + } + } + return d.canonicalStatementString } // SubsequentDiff implements EntityDiff @@ -241,6 +261,8 @@ type RenameTableEntityDiff struct { from *CreateTableEntity to *CreateTableEntity renameTable *sqlparser.RenameTable + + canonicalStatementString string } // IsEmpty implements EntityDiff @@ -283,11 +305,16 @@ func (d *RenameTableEntityDiff) StatementString() (s string) { } // CanonicalStatementString implements EntityDiff -func (d *RenameTableEntityDiff) CanonicalStatementString() (s string) { - if stmt := d.Statement(); stmt != nil { - s = sqlparser.CanonicalString(stmt) +func (d *RenameTableEntityDiff) CanonicalStatementString() string { + if d == nil { + return "" } - return s + if d.canonicalStatementString == "" { + if stmt := d.Statement(); stmt != nil { + d.canonicalStatementString = sqlparser.CanonicalString(stmt) + } + } + return d.canonicalStatementString } // SubsequentDiff implements EntityDiff diff --git a/go/vt/schemadiff/view.go b/go/vt/schemadiff/view.go index 1937200e5f9..4e32dfd9910 100644 --- a/go/vt/schemadiff/view.go +++ b/go/vt/schemadiff/view.go @@ -26,6 +26,8 @@ type AlterViewEntityDiff struct { from *CreateViewEntity to *CreateViewEntity alterView *sqlparser.AlterView + + canonicalStatementString string } // IsEmpty implements EntityDiff @@ -68,11 +70,16 @@ func (d *AlterViewEntityDiff) StatementString() (s string) { } // CanonicalStatementString implements EntityDiff -func (d *AlterViewEntityDiff) CanonicalStatementString() (s string) { - if stmt := d.Statement(); stmt != nil { - s = sqlparser.CanonicalString(stmt) +func (d *AlterViewEntityDiff) CanonicalStatementString() string { + if d == nil { + return "" } - return s + if d.canonicalStatementString == "" { + if stmt := d.Statement(); stmt != nil { + d.canonicalStatementString = sqlparser.CanonicalString(stmt) + } + } + return d.canonicalStatementString } // SubsequentDiff implements EntityDiff @@ -86,6 +93,8 @@ func (d *AlterViewEntityDiff) SetSubsequentDiff(EntityDiff) { type CreateViewEntityDiff struct { createView *sqlparser.CreateView + + canonicalStatementString string } // IsEmpty implements EntityDiff @@ -129,11 +138,16 @@ func (d *CreateViewEntityDiff) StatementString() (s string) { } // CanonicalStatementString implements EntityDiff -func (d *CreateViewEntityDiff) CanonicalStatementString() (s string) { - if stmt := d.Statement(); stmt != nil { - s = sqlparser.CanonicalString(stmt) +func (d *CreateViewEntityDiff) CanonicalStatementString() string { + if d == nil { + return "" } - return s + if d.canonicalStatementString == "" { + if stmt := d.Statement(); stmt != nil { + d.canonicalStatementString = sqlparser.CanonicalString(stmt) + } + } + return d.canonicalStatementString } // SubsequentDiff implements EntityDiff @@ -148,6 +162,8 @@ func (d *CreateViewEntityDiff) SetSubsequentDiff(EntityDiff) { type DropViewEntityDiff struct { from *CreateViewEntity dropView *sqlparser.DropView + + canonicalStatementString string } // IsEmpty implements EntityDiff @@ -182,11 +198,16 @@ func (d *DropViewEntityDiff) DropView() *sqlparser.DropView { } // CanonicalStatementString implements EntityDiff -func (d *DropViewEntityDiff) CanonicalStatementString() (s string) { - if stmt := d.Statement(); stmt != nil { - s = sqlparser.CanonicalString(stmt) +func (d *DropViewEntityDiff) CanonicalStatementString() string { + if d == nil { + return "" } - return s + if d.canonicalStatementString == "" { + if stmt := d.Statement(); stmt != nil { + d.canonicalStatementString = sqlparser.CanonicalString(stmt) + } + } + return d.canonicalStatementString } // StatementString implements EntityDiff