Skip to content

Commit ac39104

Browse files
authored
Optional integrator schema validation (#2348)
* Optional integrator schema validation * [ga-format-pr] Run ./format_repo.sh to fix formatting * cleanup * zach comments * remove bad impl --------- Co-authored-by: max-hoffman <[email protected]>
1 parent 060b45b commit ac39104

File tree

8 files changed

+174
-25
lines changed

8 files changed

+174
-25
lines changed

enginetest/engine_only_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"testing"
2626
"time"
2727

28+
"github.com/dolthub/vitess/go/sqltypes"
2829
"github.com/pmezard/go-difflib/difflib"
2930
"github.com/stretchr/testify/assert"
3031
"github.com/stretchr/testify/require"
@@ -981,6 +982,70 @@ func TestTimestampBindingsCanBeCompared(t *testing.T) {
981982
require.Equal(t, 1, count)
982983
}
983984

985+
// TestAlterTableWithBadSchema is a backwards compatibility test that
986+
// ensures tables made with old versions of the engine can be altered.
987+
func TestAlterTableWithBadSchema(t *testing.T) {
988+
harness := enginetest.NewDefaultMemoryHarness()
989+
pro := harness.Provider()
990+
harness.NewDatabases("mydb")
991+
ctx := harness.NewContext()
992+
sqlDb, err := pro.Database(ctx, "mydb")
993+
require.NoError(t, err)
994+
db := sqlDb.(*memory.HistoryDatabase)
995+
996+
sch := sql.NewPrimaryKeySchema(sql.Schema{
997+
{Name: "a", Type: types.MustCreateStringWithDefaults(sqltypes.VarChar, 16383), Source: "mytable"},
998+
{Name: "b", Type: types.MustCreateStringWithDefaults(sqltypes.VarChar, 16383), Source: "mytable"},
999+
{Name: "c", Type: types.MustCreateStringWithDefaults(sqltypes.VarChar, 16383), Source: "mytable"},
1000+
})
1001+
1002+
harness.NewTableAsOf(db, "mytable", sch, nil)
1003+
1004+
engine, err := harness.NewEngine(t)
1005+
require.NoError(t, err)
1006+
1007+
tests := []struct {
1008+
name string
1009+
q string
1010+
err bool
1011+
}{
1012+
{
1013+
name: "noop modify triggers validation",
1014+
q: "alter table mytable rename column a to d",
1015+
err: true,
1016+
},
1017+
{
1018+
name: "partial update with invalid final schema fails",
1019+
q: "alter table mytable modify column a varchar(100), modify column b varchar(100)",
1020+
err: true,
1021+
},
1022+
{
1023+
name: "update with valid final schema succeeds",
1024+
q: "alter table mytable modify column a varchar(100), modify column b varchar(100), modify column c varchar(100)",
1025+
err: false,
1026+
},
1027+
{
1028+
name: "mixed update add with invalid final schema fails",
1029+
q: "alter table mytable modify column a varchar(100), modify column b varchar(100), modify column c varchar(100), add column d varchar(max)",
1030+
err: true,
1031+
},
1032+
}
1033+
for _, tt := range tests {
1034+
t.Run(tt.name, func(t *testing.T) {
1035+
ctx := harness.NewContext()
1036+
_, iter, err := engine.Query(ctx, tt.q)
1037+
// errors should be analyze time, not execution time
1038+
if tt.err {
1039+
require.Error(t, err)
1040+
} else {
1041+
require.NoError(t, err)
1042+
_, err = sql.RowIterToRows(ctx, iter)
1043+
require.NoError(t, err)
1044+
}
1045+
})
1046+
}
1047+
}
1048+
9841049
func newDatabase() (*sql2.DB, func()) {
9851050
// Grab an empty port so that tests do not fail if a specific port is already in use
9861051
listener, err := net.Listen("tcp", ":0")

enginetest/queries/alter_table_queries.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,34 @@ package queries
1717
import (
1818
"github.com/dolthub/vitess/go/mysql"
1919

20-
"github.com/dolthub/go-mysql-server/sql/plan"
21-
2220
"github.com/dolthub/go-mysql-server/sql"
21+
"github.com/dolthub/go-mysql-server/sql/analyzer/analyzererrors"
22+
"github.com/dolthub/go-mysql-server/sql/plan"
2323
"github.com/dolthub/go-mysql-server/sql/types"
2424
)
2525

2626
var AlterTableScripts = []ScriptTest{
27+
{
28+
Name: "multi alter with invalid schemas",
29+
SetUpScript: []string{
30+
"CREATE TABLE t(a int primary key)",
31+
},
32+
Assertions: []ScriptTestAssertion{
33+
{
34+
Query: "alter table t add column b varchar(16383)",
35+
ExpectedErr: analyzererrors.ErrInvalidRowLength,
36+
},
37+
{
38+
// 1 char = 4 bytes with default collation
39+
Query: "alter table t add column b varchar(16000), add column c varchar(16000)",
40+
ExpectedErr: analyzererrors.ErrInvalidRowLength,
41+
},
42+
{
43+
Query: "alter table t add column b varchar(16000), add column c varchar(10)",
44+
Expected: []sql.Row{{types.NewOkResult(0)}},
45+
},
46+
},
47+
},
2748
{
2849
Name: "variety of alter column statements in a single statement",
2950
SetUpScript: []string{

memory/database.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ var _ sql.EventDatabase = (*Database)(nil)
4848
var _ sql.ViewDatabase = (*Database)(nil)
4949
var _ sql.CollatedDatabase = (*Database)(nil)
5050
var _ fulltext.Database = (*Database)(nil)
51+
var _ sql.SchemaValidator = (*BaseDatabase)(nil)
5152

5253
// BaseDatabase is an in-memory database that can't store views, only for testing the engine
5354
type BaseDatabase struct {
@@ -81,6 +82,11 @@ func NewViewlessDatabase(name string) *BaseDatabase {
8182
}
8283
}
8384

85+
// ValidateSchema implements sql.SchemaValidator
86+
func (d *BaseDatabase) ValidateSchema(schema sql.Schema) error {
87+
return validateMaxRowLength(schema)
88+
}
89+
8490
// EnablePrimaryKeyIndexes causes every table created in this database to use an index on its primary partitionKeys
8591
func (d *BaseDatabase) EnablePrimaryKeyIndexes() {
8692
d.primaryKeyIndexes = true
@@ -234,10 +240,6 @@ func (d *BaseDatabase) CreateTable(ctx *sql.Context, name string, schema sql.Pri
234240
return sql.ErrTableAlreadyExists.New(name)
235241
}
236242

237-
if err := validateMaxRowLength(schema.PhysicalSchema()); err != nil {
238-
return err
239-
}
240-
241243
table := NewTableWithCollation(d, name, schema, d.fkColl, collation)
242244
table.db = d
243245
table.data.comment = comment

memory/table.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1165,10 +1165,6 @@ func addColumnToSchema(ctx *sql.Context, data *TableData, newCol *sql.Column, or
11651165
}
11661166
}
11671167

1168-
if err := validateMaxRowLength(newSch.PhysicalSchema()); err != nil {
1169-
return 0, nil, err
1170-
}
1171-
11721168
for _, newSchCol := range newSch {
11731169
newDefault, _, _ := transform.Expr(newSchCol.Default, func(expr sql.Expression) (sql.Expression, transform.TreeIdentity, error) {
11741170
if expr, ok := expr.(*expression.GetField); ok {

sql/analyzer/validate_create_table.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ func validateIdentifiers(name string, spec *plan.TableSpec) error {
155155
func resolveAlterColumn(ctx *sql.Context, a *Analyzer, n sql.Node, scope *plan.Scope, sel RuleSelector) (sql.Node, transform.TreeIdentity, error) {
156156
var sch sql.Schema
157157
var indexes []string
158+
var validator sql.SchemaValidator
158159
keyedColumns := make(map[string]bool)
159160
var err error
160161
transform.Inspect(n, func(n sql.Node) bool {
@@ -163,16 +164,41 @@ func resolveAlterColumn(ctx *sql.Context, a *Analyzer, n sql.Node, scope *plan.S
163164
}
164165
switch n := n.(type) {
165166
case *plan.ModifyColumn:
167+
if rt, ok := n.Table.(*plan.ResolvedTable); ok {
168+
if sv, ok := rt.UnwrappedDatabase().(sql.SchemaValidator); ok {
169+
validator = sv
170+
}
171+
}
166172
keyedColumns, err = getTableIndexColumns(ctx, n.Table)
167173
return false
168174
case *plan.RenameColumn:
175+
if rt, ok := n.Table.(*plan.ResolvedTable); ok {
176+
if sv, ok := rt.UnwrappedDatabase().(sql.SchemaValidator); ok {
177+
validator = sv
178+
}
179+
}
169180
return false
170181
case *plan.AddColumn:
182+
if rt, ok := n.Table.(*plan.ResolvedTable); ok {
183+
if sv, ok := rt.UnwrappedDatabase().(sql.SchemaValidator); ok {
184+
validator = sv
185+
}
186+
}
171187
keyedColumns, err = getTableIndexColumns(ctx, n.Table)
172188
return false
173189
case *plan.DropColumn:
190+
if rt, ok := n.Table.(*plan.ResolvedTable); ok {
191+
if sv, ok := rt.UnwrappedDatabase().(sql.SchemaValidator); ok {
192+
validator = sv
193+
}
194+
}
174195
return false
175196
case *plan.AlterIndex:
197+
if rt, ok := n.Table.(*plan.ResolvedTable); ok {
198+
if sv, ok := rt.UnwrappedDatabase().(sql.SchemaValidator); ok {
199+
validator = sv
200+
}
201+
}
176202
indexes, err = getTableIndexNames(ctx, a, n.Table)
177203
default:
178204
}
@@ -293,6 +319,12 @@ func resolveAlterColumn(ctx *sql.Context, a *Analyzer, n sql.Node, scope *plan.S
293319
return nil, transform.SameTree, err
294320
}
295321

322+
if validator != nil {
323+
if err := validator.ValidateSchema(sch); err != nil {
324+
return nil, transform.SameTree, err
325+
}
326+
}
327+
296328
// We can't evaluate auto-increment until the end of the analysis, since we break adding a new auto-increment unique
297329
// column into two steps: first add the column, then create the index. If there was no index created, that's an error.
298330
if addedColumn {

sql/plan/resolved_table.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
"fmt"
1919
"strings"
2020

21+
"github.com/dolthub/go-mysql-server/sql/mysql_db"
22+
2123
"github.com/dolthub/go-mysql-server/memory"
2224
"github.com/dolthub/go-mysql-server/sql"
2325
)
@@ -91,6 +93,13 @@ func (t *ResolvedTable) Database() sql.Database {
9193
return t.SqlDatabase
9294
}
9395

96+
func (t *ResolvedTable) UnwrappedDatabase() sql.Database {
97+
if privDb, ok := t.SqlDatabase.(mysql_db.PrivilegedDatabase); ok {
98+
return privDb.Unwrap()
99+
}
100+
return t.SqlDatabase
101+
}
102+
94103
func (t *ResolvedTable) WithDatabase(database sql.Database) (sql.Node, error) {
95104
newNode := *t
96105
t.SqlDatabase = database

sql/planbuilder/ddl.go

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/dolthub/go-mysql-server/sql"
2626
"github.com/dolthub/go-mysql-server/sql/expression"
2727
"github.com/dolthub/go-mysql-server/sql/expression/function"
28+
"github.com/dolthub/go-mysql-server/sql/mysql_db"
2829
"github.com/dolthub/go-mysql-server/sql/plan"
2930
"github.com/dolthub/go-mysql-server/sql/types"
3031
)
@@ -212,35 +213,45 @@ func (b *Builder) buildCreateTable(inScope *scope, c *ast.DDL) (outScope *scope)
212213
return b.buildCreateTableLike(inScope, c)
213214
}
214215

216+
qualifier := c.Table.Qualifier.String()
217+
if qualifier == "" {
218+
qualifier = b.ctx.GetCurrentDatabase()
219+
}
220+
database := b.resolveDb(qualifier)
221+
215222
// In the case that no table spec is given but a SELECT Statement return the CREATE TABLE node.
216223
// if the table spec != nil it will get parsed below.
217224
if c.TableSpec == nil && c.OptSelect != nil {
218225
tableSpec := &plan.TableSpec{}
219226

220227
selectScope := b.buildSelectStmt(inScope, c.OptSelect.Select)
221228

222-
dbName := strings.ToLower(c.Table.Qualifier.String())
223-
if dbName == "" {
224-
dbName = b.ctx.GetCurrentDatabase()
225-
}
226-
db := b.resolveDb(dbName)
227-
outScope.node = plan.NewCreateTableSelect(db, c.Table.Name.String(), selectScope.node, tableSpec, plan.IfNotExistsOption(c.IfNotExists), plan.TempTableOption(c.Temporary))
229+
outScope.node = plan.NewCreateTableSelect(database, c.Table.Name.String(), selectScope.node, tableSpec, plan.IfNotExistsOption(c.IfNotExists), plan.TempTableOption(c.Temporary))
228230
return outScope
229231
}
230232

231233
idxDefs := b.buildIndexDefs(inScope, c.TableSpec)
232234

233-
qualifier := c.Table.Qualifier.String()
234-
if qualifier == "" {
235-
qualifier = b.ctx.GetCurrentDatabase()
236-
}
237-
database := b.resolveDb(qualifier)
238235
schema, collation, comment := b.tableSpecToSchema(inScope, outScope, database, strings.ToLower(c.Table.Name.String()), c.TableSpec, false)
239236
fkDefs, chDefs := b.buildConstraintsDefs(outScope, c.Table, c.TableSpec)
240237

241238
schema.Schema = assignColumnIndexesInSchema(schema.Schema)
242239
chDefs = assignColumnIndexesInCheckDefs(chDefs, schema.Schema)
243240

241+
if privDb, ok := database.(mysql_db.PrivilegedDatabase); ok {
242+
if sv, ok := privDb.Unwrap().(sql.SchemaValidator); ok {
243+
if err := sv.ValidateSchema(schema.PhysicalSchema()); err != nil {
244+
b.handleErr(err)
245+
}
246+
}
247+
} else {
248+
if sv, ok := database.(sql.SchemaValidator); ok {
249+
if err := sv.ValidateSchema(schema.PhysicalSchema()); err != nil {
250+
b.handleErr(err)
251+
}
252+
}
253+
}
254+
244255
tableSpec := &plan.TableSpec{
245256
Schema: schema,
246257
IdxDefs: idxDefs,

sql/tables.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -393,20 +393,33 @@ type RewritableTable interface {
393393
RewriteInserter(ctx *Context, oldSchema, newSchema PrimaryKeySchema, oldColumn, newColumn *Column, idxCols []IndexColumn) (RowInserter, error)
394394
}
395395

396-
// AlterableTable should be implemented by tables that can receive ALTER TABLE statements to modify their schemas.
396+
// AlterableTable should be implemented by tables that can receive
397+
// ALTER TABLE statements to modify their schemas.
397398
type AlterableTable interface {
398399
Table
399400
UpdatableTable
400401

401-
// AddColumn adds a column to this table as given. If non-nil, order specifies where in the schema to add the column.
402+
// AddColumn adds a column to this table as given. If non-nil, order
403+
// specifies where in the schema to add the column.
402404
AddColumn(ctx *Context, column *Column, order *ColumnOrder) error
403405
// DropColumn drops the column with the name given.
404406
DropColumn(ctx *Context, columnName string) error
405-
// ModifyColumn modifies the column with the name given, replacing with the new column definition provided (which may
406-
// include a name change). If non-nil, order specifies where in the schema to move the column.
407+
// ModifyColumn modifies the column with the name given, replacing
408+
// with the new column definition provided (which may include a name
409+
// change). If non-nil, order specifies where in the schema to move
410+
// the column.
407411
ModifyColumn(ctx *Context, columnName string, column *Column, order *ColumnOrder) error
408412
}
409413

414+
// SchemaValidator is a database that performs schema compatibility checks
415+
// for CREATE and ALTER TABLE statements.
416+
type SchemaValidator interface {
417+
Database
418+
// ValidateSchema lets storage integrators validate whether they can
419+
// serialize a given schema.
420+
ValidateSchema(Schema) error
421+
}
422+
410423
// UnresolvedTable is a Table that is either unresolved or deferred for until an asOf resolution.
411424
// Used by the analyzer during planning, and is not expected to be implemented by integrators.
412425
type UnresolvedTable interface {

0 commit comments

Comments
 (0)