Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

schemadiff: multi-error in schema normalization #12675

Merged
merged 45 commits into from
Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
c4c213b
validate table columns referenced by views
shlomi-noach Jan 24, 2023
3dac907
validate views' columns
shlomi-noach Jan 24, 2023
e88d426
support star expression
shlomi-noach Jan 25, 2023
61de6b0
removed 'TODO'
shlomi-noach Jan 25, 2023
389872c
rename: errs
shlomi-noach Jan 25, 2023
56c8421
use mutierr
shlomi-noach Jan 25, 2023
11d6379
excessive test, removed
shlomi-noach Jan 25, 2023
f4fd673
go mod tidy
shlomi-noach Jan 25, 2023
a4ae98d
update to latest mutierr
shlomi-noach Jan 25, 2023
ef16bdf
merge main, resolve conflict
shlomi-noach Jan 26, 2023
54fb73a
avoid setting entity columns in case of error
shlomi-noach Jan 26, 2023
2d3e70b
grammar
shlomi-noach Jan 30, 2023
b69ede6
schemadiff: fix scenario where no tables exist in schema and with jus…
shlomi-noach Jan 30, 2023
bc34369
dual, not dual2
shlomi-noach Jan 30, 2023
bc8799d
stripped irrelevant comments
shlomi-noach Jan 30, 2023
dd1bc82
merge main, resolve conflict
shlomi-noach Feb 5, 2023
7f6fd13
using AllErrorRecorder instead of multierr package
shlomi-noach Feb 6, 2023
4939d01
multierr update
shlomi-noach Feb 20, 2023
89d6cba
using FakeSI as table-column model
shlomi-noach Mar 7, 2023
2194291
rename variable
shlomi-noach Mar 7, 2023
38eab14
clone
shlomi-noach Mar 7, 2023
7eb83c1
add InvalidStarExprInViewError
shlomi-noach Mar 7, 2023
f98729b
Make 'args()' accessible
shlomi-noach Mar 7, 2023
36075ec
using 'go/vt/vtgate/semantics' to analyze view queries. Handling star…
shlomi-noach Mar 7, 2023
26e9577
more test cases
shlomi-noach Mar 7, 2023
5718b58
typo
shlomi-noach Mar 8, 2023
fe90b31
unexpected error
shlomi-noach Mar 8, 2023
db43534
use ColumnName()
shlomi-noach Mar 8, 2023
3bf836f
iterate columns instead of Walk
shlomi-noach Mar 8, 2023
95a963e
Merge branch 'main' into schemadiff-validate-view-columns-semantics
shlomi-noach Mar 9, 2023
ebf8a12
Merge branch 'main' into schemadiff-validate-view-columns-semantics
shlomi-noach Mar 9, 2023
1244c0b
simplify InvalidColumnReferencedInViewError
shlomi-noach Mar 20, 2023
8b07d21
do not use FakeSI, create my own implementation of semantics.SchemaIn…
shlomi-noach Mar 20, 2023
017e10d
Refactor: go/vt/vtgate/engine/opcode to reduce semantics package depe…
shlomi-noach Mar 20, 2023
eac2860
add new package
shlomi-noach Mar 20, 2023
4daa08b
copyright
shlomi-noach Mar 20, 2023
1699955
fix function comment
shlomi-noach Mar 20, 2023
c66b6e9
Merge branch 'resolve-semantics-engine-deps' into schemadiff-validate…
shlomi-noach Mar 20, 2023
3004533
aggregate errors in normalize()
shlomi-noach Mar 20, 2023
1586e03
Unwrap logic for go 1.20 error wrapping
shlomi-noach Mar 20, 2023
05e2364
aggregate errors using go1.20 errors.Join(); aggregate errors on s.Va…
shlomi-noach Mar 20, 2023
1098f65
merge main, resolve conflict
shlomi-noach Mar 21, 2023
8a03a3f
collect errors
shlomi-noach Mar 21, 2023
2bb6e71
unit tests for Unwrap* functions
shlomi-noach Mar 22, 2023
8c49bd2
refactor: new 'errors' package
shlomi-noach Mar 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions go/errors/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
Copyright 2023 The Vitess Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package errors

// Wrapped is used to unwrap an error created by errors.Join() in Go 1.20
type Wrapped interface {
Unwrap() []error
}

// Unwrap unwraps an error created by errors.Join() in Go 1.20, into its components
func Unwrap(err error) []error {
if err == nil {
return nil
}
if u, ok := err.(Wrapped); ok {
return u.Unwrap()
}
return nil
}

// Unwrap unwraps an error created by errors.Join() in Go 1.20, into its components, recursively
func UnwrapAll(err error) (errs []error) {
if err == nil {
return nil
}
if u, ok := err.(Wrapped); ok {
for _, e := range u.Unwrap() {
errs = append(errs, UnwrapAll(e)...)
}
return errs
}
return []error{err}
}

// Unwrap unwraps an error created by errors.Join() in Go 1.20, into its components, recursively,
// and returns one (the first) unwrapped error
func UnwrapFirst(err error) error {
if err == nil {
return nil
}
return UnwrapAll(err)[0]
}
99 changes: 99 additions & 0 deletions go/errors/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
Copyright 2023 The Vitess Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package errors

import (
"errors"
"testing"

"github.com/stretchr/testify/assert"
)

func TestUnwrap(t *testing.T) {
err1 := errors.New("err1")
err2 := errors.New("err2")
err3 := errors.New("err3")
err4 := errors.New("err4")

tt := []struct {
name string
err error
expectUnwrap []error
expectUnwrapAll []error
expectUnwrapFirst error
}{
{
name: "nil",
expectUnwrap: nil,
expectUnwrapAll: nil,
expectUnwrapFirst: nil,
},
{
name: "single",
err: err1,
expectUnwrap: nil,
expectUnwrapAll: []error{err1},
expectUnwrapFirst: err1,
},
{
name: "wrapped nil",
err: errors.Join(nil),
expectUnwrap: nil,
expectUnwrapAll: nil,
expectUnwrapFirst: nil,
},
{
name: "single wrapped",
err: errors.Join(err1),
expectUnwrap: []error{err1},
expectUnwrapAll: []error{err1},
expectUnwrapFirst: err1,
},
{
name: "flat wrapped",
err: errors.Join(err1, err2, err3, err4),
expectUnwrap: []error{err1, err2, err3, err4},
expectUnwrapAll: []error{err1, err2, err3, err4},
expectUnwrapFirst: err1,
},
{
name: "double wrapped",
err: errors.Join(errors.Join(err1)),
expectUnwrap: []error{errors.Join(err1)},
expectUnwrapAll: []error{err1},
expectUnwrapFirst: err1,
},
{
name: "double nested wrapped",
err: errors.Join(errors.Join(err1, err2), errors.Join(err3, err4)),
expectUnwrap: []error{errors.Join(err1, err2), errors.Join(err3, err4)},
expectUnwrapAll: []error{err1, err2, err3, err4},
expectUnwrapFirst: err1,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
unwrapped := Unwrap(tc.err)
unwrappedAll := UnwrapAll(tc.err)
unwrappedFirst := UnwrapFirst(tc.err)

assert.Equal(t, tc.expectUnwrap, unwrapped)
assert.Equal(t, tc.expectUnwrapAll, unwrappedAll)
assert.Equal(t, tc.expectUnwrapFirst, unwrappedFirst)
})
}
}
16 changes: 16 additions & 0 deletions go/vt/schemadiff/errors.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
Copyright 2023 The Vitess Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package schemadiff

import (
Expand Down
29 changes: 15 additions & 14 deletions go/vt/schemadiff/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (
"sort"
"strings"

"vitess.io/vitess/go/vt/concurrency"
"vitess.io/vitess/go/vt/proto/vtrpc"
"vitess.io/vitess/go/vt/sqlparser"
"vitess.io/vitess/go/vt/vterrors"
Expand Down Expand Up @@ -72,7 +71,7 @@ func NewSchemaFromEntities(entities []Entity) (*Schema, error) {
}
}
if err := schema.normalize(); err != nil {
return nil, err
return schema, err
}
return schema, nil
}
Expand Down Expand Up @@ -165,6 +164,8 @@ func getViewDependentTableNames(createView *sqlparser.CreateView) (names []strin
// normalize is called as part of Schema creation process. The user may only get a hold of normalized schema.
// It validates some cross-entity constraints, and orders entity based on dependencies (e.g. tables, views that read from tables, 2nd level views, etc.)
func (s *Schema) normalize() error {
var errs error

s.named = make(map[string]Entity, len(s.tables)+len(s.views))
s.sorted = make([]Entity, 0, len(s.tables)+len(s.views))
// Verify no two entities share same name
Expand Down Expand Up @@ -317,20 +318,20 @@ func (s *Schema) normalize() error {
if _, ok := dependencyLevels[v.Name()]; !ok {
// We _know_ that in this iteration, at least one view is found unassigned a dependency level.
// We return the first one.
return &ViewDependencyUnresolvedError{View: v.ViewName.Name.String()}
errs = errors.Join(errs, &ViewDependencyUnresolvedError{View: v.ViewName.Name.String()})
}
}
}

// Validate views' referenced columns: do these columns actually exist in referenced tables/views?
if err := s.ValidateViewReferences(); err != nil {
return err
errs = errors.Join(errs, err)
}

// Validate table definitions
for _, t := range s.tables {
if err := t.validate(); err != nil {
return err
return errors.Join(errs, err)
shlomi-noach marked this conversation as resolved.
Show resolved Hide resolved
}
}
colTypeEqualForForeignKey := func(a, b *sqlparser.ColumnType) bool {
Expand Down Expand Up @@ -374,24 +375,24 @@ func (s *Schema) normalize() error {
for i, col := range check.Source {
coveredColumn, ok := tableColumns[col.Lowered()]
if !ok {
return &InvalidColumnInForeignKeyConstraintError{Table: t.Name(), Constraint: cs.Name.String(), Column: col.String()}
return errors.Join(errs, &InvalidColumnInForeignKeyConstraintError{Table: t.Name(), Constraint: cs.Name.String(), Column: col.String()})
}
referencedColumnName := check.ReferenceDefinition.ReferencedColumns[i].Lowered()
referencedColumn, ok := referencedColumns[referencedColumnName]
if !ok {
return &InvalidReferencedColumnInForeignKeyConstraintError{Table: t.Name(), Constraint: cs.Name.String(), ReferencedTable: referencedTableName, ReferencedColumn: referencedColumnName}
return errors.Join(errs, &InvalidReferencedColumnInForeignKeyConstraintError{Table: t.Name(), Constraint: cs.Name.String(), ReferencedTable: referencedTableName, ReferencedColumn: referencedColumnName})
}
if !colTypeEqualForForeignKey(coveredColumn.Type, referencedColumn.Type) {
return &ForeignKeyColumnTypeMismatchError{Table: t.Name(), Constraint: cs.Name.String(), Column: coveredColumn.Name.String(), ReferencedTable: referencedTableName, ReferencedColumn: referencedColumnName}
return errors.Join(errs, &ForeignKeyColumnTypeMismatchError{Table: t.Name(), Constraint: cs.Name.String(), Column: coveredColumn.Name.String(), ReferencedTable: referencedTableName, ReferencedColumn: referencedColumnName})
}
}

if !referencedTable.columnsCoveredByInOrderIndex(check.ReferenceDefinition.ReferencedColumns) {
return &MissingForeignKeyReferencedIndexError{Table: t.Name(), Constraint: cs.Name.String(), ReferencedTable: referencedTableName}
return errors.Join(errs, &MissingForeignKeyReferencedIndexError{Table: t.Name(), Constraint: cs.Name.String(), ReferencedTable: referencedTableName})
}
}
}
return nil
return errs
}

// Entities returns this schema's entities in good order (may be applied without error)
Expand Down Expand Up @@ -772,7 +773,7 @@ func (s *Schema) Apply(diffs []EntityDiff) (*Schema, error) {
}

func (s *Schema) ValidateViewReferences() error {
errs := &concurrency.AllErrorRecorder{}
var errs error
schemaInformation := newDeclarativeSchemaInformation()

// Remember that s.Entities() is already ordered by dependency. ie. tables first, then views
Expand All @@ -781,7 +782,7 @@ func (s *Schema) ValidateViewReferences() error {
for _, e := range s.Entities() {
entityColumns, err := s.getEntityColumnNames(e.Name(), schemaInformation)
if err != nil {
errs.RecordError(err)
errs = errors.Join(errs, err)
continue
}
schemaInformation.addTable(e.Name())
Expand Down Expand Up @@ -836,9 +837,9 @@ func (s *Schema) ValidateViewReferences() error {
}
return err
}
errs.RecordError(formalizeErr(err))
errs = errors.Join(errs, formalizeErr(err))
}
return errs.AggrError(vterrors.Aggregate)
return errs
}

// getEntityColumnNames returns the names of columns in given entity (either a table or a view)
Expand Down
7 changes: 6 additions & 1 deletion go/vt/schemadiff/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"vitess.io/vitess/go/errors"

"vitess.io/vitess/go/vt/sqlparser"
)

Expand Down Expand Up @@ -152,7 +154,8 @@ func TestNewSchemaFromQueriesLoop(t *testing.T) {
"create view v8 as select * from t1, v7",
)
_, err := NewSchemaFromQueries(queries)
assert.Error(t, err)
require.Error(t, err)
err = errors.UnwrapFirst(err)
assert.EqualError(t, err, (&ViewDependencyUnresolvedError{View: "v7"}).Error())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we assert err length here as well .. to assure the numbers of errors are as expected

Copy link
Contributor Author

@shlomi-noach shlomi-noach Mar 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UnwrapOne() is guaranteed to return a slice of at least one element.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And yes! I'll add that assertion anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather, I'll create a errors_test.go with a bunch of unit tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unit tests added

}

Expand Down Expand Up @@ -700,6 +703,8 @@ func TestViewReferences(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, schema)
} else {
require.Error(t, err)
err = errors.UnwrapFirst(err)
require.Equal(t, ts.expectErr, err, "received error: %v", err)
}
})
Expand Down