mirror of
https://github.com/dolthub/dolt.git
synced 2026-05-08 11:21:17 -05:00
Type change tests and bug fixes
This commit is contained in:
committed by
Daylon Wilkins
parent
bedfdcf488
commit
e425ec40b5
@@ -23,7 +23,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Test All
|
||||
working-directory: ./go
|
||||
run: go test -race ./...
|
||||
run: go test -timeout 30m -race ./...
|
||||
- name: Discord Notify
|
||||
if: always()
|
||||
uses: dolthub/ga-discord-notify@master
|
||||
|
||||
@@ -441,7 +441,7 @@ SQL
|
||||
[[ `echo "$output" | tr -d "\n" | tr -s " "` =~ 'CONSTRAINT `fk1` FOREIGN KEY (`v1_new`) REFERENCES `parent` (`v1_new`)' ]] || false
|
||||
}
|
||||
|
||||
@test "foreign-keys: ALTER TABLE MODIFY COLUMN" {
|
||||
@test "foreign-keys: ALTER TABLE MODIFY COLUMN type change not allowed" {
|
||||
dolt sql <<SQL
|
||||
ALTER TABLE child ADD CONSTRAINT fk1 FOREIGN KEY (v1) REFERENCES parent(v1);
|
||||
SQL
|
||||
|
||||
@@ -154,6 +154,14 @@ CREATE TABLE test2(
|
||||
PRIMARY KEY(pk1, pk2)
|
||||
);
|
||||
SQL
|
||||
run dolt schema tags -r=csv
|
||||
[ "$status" -eq "0" ]
|
||||
[[ "$output" =~ "table,column,tag" ]] || false
|
||||
[[ "$output" =~ "test2,pk1,6801" ]] || false
|
||||
[[ "$output" =~ "test2,pk2,4776" ]] || false
|
||||
[[ "$output" =~ "test2,v1,10579" ]] || false
|
||||
[[ "$output" =~ "test2,v2,7704" ]] || false
|
||||
|
||||
run dolt sql -q "INSERT INTO test2 (pk1, pk2, v1, v2) VALUES (1, 1, 'abc', 'def')"
|
||||
[ "$status" -eq 0 ]
|
||||
dolt add .
|
||||
@@ -167,9 +175,18 @@ SQL
|
||||
dolt push origin master
|
||||
dolt clone file://remotedir original
|
||||
|
||||
dolt schema change-type Test2 V1 'varchar(300)'
|
||||
dolt schema change-type TEST2 PK2 'tinyint'
|
||||
dolt schema change-type Test2 V2 'varchar(1024)'
|
||||
dolt sql -q "ALTER TABLE Test2 MODIFY V1 varchar(300) not null"
|
||||
dolt sql -q "ALTER TABLE TEST2 MODIFY PK2 tinyint not null"
|
||||
dolt sql -q "ALTER TABLE Test2 MODIFY V2 varchar(1024) not null"
|
||||
|
||||
# verify that the tags have not changed
|
||||
run dolt schema tags -r=csv
|
||||
[ "$status" -eq "0" ]
|
||||
[[ "$output" =~ "table,column,tag" ]] || false
|
||||
[[ "$output" =~ "test2,pk1,6801" ]] || false
|
||||
[[ "$output" =~ "test2,pk2,4776" ]] || false
|
||||
[[ "$output" =~ "test2,v1,10579" ]] || false
|
||||
[[ "$output" =~ "test2,v2,7704" ]] || false
|
||||
|
||||
run dolt diff
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
@@ -538,6 +538,55 @@ SQL
|
||||
[[ "$output" =~ 'PRIMARY KEY (`pk`)' ]] || false
|
||||
}
|
||||
|
||||
@test "sql alter table modify column type success" {
|
||||
dolt sql <<SQL
|
||||
CREATE TABLE t1(pk BIGINT PRIMARY KEY, v1 INT, INDEX(v1));
|
||||
CREATE TABLE t2(pk BIGINT PRIMARY KEY, v1 VARCHAR(20), INDEX(v1));
|
||||
CREATE TABLE t3(pk BIGINT PRIMARY KEY, v1 DATETIME, INDEX(v1));
|
||||
INSERT INTO t1 VALUES (0,-1),(1,1);
|
||||
INSERT INTO t2 VALUES (0,'hi'),(1,'bye');
|
||||
INSERT INTO t3 VALUES (0,'1999-11-02 17:39:38'),(1,'2021-01-08 02:59:27');
|
||||
ALTER TABLE t1 MODIFY COLUMN v1 BIGINT;
|
||||
ALTER TABLE t2 MODIFY COLUMN v1 VARCHAR(2000);
|
||||
ALTER TABLE t3 MODIFY COLUMN v1 TIMESTAMP;
|
||||
SQL
|
||||
run dolt sql -q "SELECT * FROM t1 ORDER BY pk" -r=csv
|
||||
[ "$status" -eq "0" ]
|
||||
[[ "$output" =~ "pk,v1" ]] || false
|
||||
[[ "$output" =~ "0,-1" ]] || false
|
||||
[[ "$output" =~ "1,1" ]] || false
|
||||
[[ "${#lines[@]}" = "3" ]] || false
|
||||
run dolt sql -q "SELECT * FROM t2 ORDER BY pk" -r=csv
|
||||
[ "$status" -eq "0" ]
|
||||
[[ "$output" =~ "pk,v1" ]] || false
|
||||
[[ "$output" =~ "0,hi" ]] || false
|
||||
[[ "$output" =~ "1,bye" ]] || false
|
||||
[[ "${#lines[@]}" = "3" ]] || false
|
||||
run dolt sql -q "SELECT * FROM t3 ORDER BY pk" -r=csv
|
||||
[ "$status" -eq "0" ]
|
||||
[[ "$output" =~ "pk,v1" ]] || false
|
||||
[[ "$output" =~ "0,1999-11-02 17:39:38" ]] || false
|
||||
[[ "$output" =~ "1,2021-01-08 02:59:27" ]] || false
|
||||
[[ "${#lines[@]}" = "3" ]] || false
|
||||
}
|
||||
|
||||
@test "sql alter table modify column type failure" {
|
||||
dolt sql <<SQL
|
||||
CREATE TABLE t1(pk BIGINT PRIMARY KEY, v1 INT, INDEX(v1));
|
||||
CREATE TABLE t2(pk BIGINT PRIMARY KEY, v1 VARCHAR(20), INDEX(v1));
|
||||
CREATE TABLE t3(pk BIGINT PRIMARY KEY, v1 DATETIME, INDEX(v1));
|
||||
INSERT INTO t1 VALUES (0,-1),(1,1);
|
||||
INSERT INTO t2 VALUES (0,'hi'),(1,'bye');
|
||||
INSERT INTO t3 VALUES (0,'1999-11-02 17:39:38'),(1,'3021-01-08 02:59:27');
|
||||
SQL
|
||||
run dolt sql -q "ALTER TABLE t1 MODIFY COLUMN v1 INT UNSIGNED"
|
||||
[ "$status" -eq "1" ]
|
||||
run dolt sql -q "ALTER TABLE t2 MODIFY COLUMN v1 VARCHAR(2)"
|
||||
[ "$status" -eq "1" ]
|
||||
run dolt sql -q "ALTER TABLE t3 MODIFY COLUMN v1 TIMESTAMP"
|
||||
[ "$status" -eq "1" ]
|
||||
}
|
||||
|
||||
@test "sql drop table" {
|
||||
dolt sql -q "drop table one_pk"
|
||||
run dolt ls
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
// Copyright 2021 Dolthub, Inc.
|
||||
//
|
||||
// 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 schcmds
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/dolthub/go-mysql-server/sql"
|
||||
"github.com/dolthub/vitess/go/sqltypes"
|
||||
|
||||
"github.com/dolthub/dolt/go/cmd/dolt/cli"
|
||||
commands "github.com/dolthub/dolt/go/cmd/dolt/commands"
|
||||
"github.com/dolthub/dolt/go/cmd/dolt/errhand"
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/env"
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/schema"
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/schema/typeinfo"
|
||||
"github.com/dolthub/dolt/go/libraries/utils/argparser"
|
||||
"github.com/dolthub/dolt/go/libraries/utils/filesys"
|
||||
)
|
||||
|
||||
type ChangeTypeCmd struct{}
|
||||
|
||||
func (cmd ChangeTypeCmd) Hidden() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (cmd ChangeTypeCmd) Name() string {
|
||||
return "change-type"
|
||||
}
|
||||
|
||||
func (cmd ChangeTypeCmd) Description() string {
|
||||
return "Changes the type of a column in place"
|
||||
}
|
||||
|
||||
func (cmd ChangeTypeCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv) int {
|
||||
ap := cmd.createArgParser()
|
||||
help, usage := cli.HelpAndUsagePrinters(cli.GetCommandDocumentation(commandStr, tblSchemaDocs, ap))
|
||||
apr := cli.ParseArgs(ap, args, help)
|
||||
|
||||
if apr.NArg() < 3 {
|
||||
cli.PrintUsage("change-type <table> <column> <type>",
|
||||
[]string{"Changes the type of a column in place, without modifying any row data.\n" +
|
||||
"This is an unsafe operation in general, but widening a type is safe.\n" +
|
||||
"Only VARCHAR and INTEGER types are currently supported.\n"},
|
||||
ap)
|
||||
return 1
|
||||
}
|
||||
|
||||
tableName, column, typ := apr.Arg(0), apr.Arg(1), apr.Arg(2)
|
||||
|
||||
root, verr := commands.GetWorkingWithVErr(dEnv)
|
||||
if verr != nil {
|
||||
return commands.HandleVErrAndExitCode(verr, usage)
|
||||
}
|
||||
|
||||
table, tableCase, ok, err := root.GetTableInsensitive(ctx, tableName)
|
||||
if err != nil {
|
||||
return commands.HandleVErrAndExitCode(errhand.BuildDError("unable to get table '%s'", tableName).AddCause(err).Build(), usage)
|
||||
} else if !ok {
|
||||
return commands.HandleVErrAndExitCode(errhand.BuildDError("couldn't find table '%s'", tableName).Build(), usage)
|
||||
}
|
||||
|
||||
sch, err := table.GetSchema(ctx)
|
||||
if err != nil {
|
||||
return commands.HandleVErrAndExitCode(errhand.BuildDError("unable to get table '%s'", tableName).AddCause(err).Build(), usage)
|
||||
}
|
||||
|
||||
cols := make([]schema.Column, sch.GetAllCols().Size())
|
||||
i := 0
|
||||
sch.GetAllCols().Iter(func(tag uint64, col schema.Column) (stop bool, err error) {
|
||||
if strings.ToLower(col.Name) == strings.ToLower(column) {
|
||||
cols[i] = alterColumnType(typ, col)
|
||||
} else {
|
||||
cols[i] = col
|
||||
}
|
||||
i++
|
||||
|
||||
return false, nil
|
||||
})
|
||||
|
||||
collection := schema.NewColCollection(cols...)
|
||||
|
||||
newSch, err := schema.SchemaFromCols(collection)
|
||||
if err != nil {
|
||||
return commands.HandleVErrAndExitCode(errhand.BuildDError("unable to create new schema '%s'", tableName).AddCause(err).Build(), usage)
|
||||
}
|
||||
|
||||
newTable, err := table.UpdateSchema(ctx, newSch)
|
||||
if err != nil {
|
||||
return commands.HandleVErrAndExitCode(errhand.BuildDError("unable to create new schema '%s'", tableName).AddCause(err).Build(), usage)
|
||||
}
|
||||
|
||||
root, err = root.PutTable(ctx, tableCase, newTable)
|
||||
if err != nil {
|
||||
return commands.HandleVErrAndExitCode(errhand.BuildDError("unable to write new table '%s'", tableName).AddCause(err).Build(), usage)
|
||||
}
|
||||
|
||||
return commands.HandleVErrAndExitCode(commands.UpdateWorkingWithVErr(dEnv, root), usage)
|
||||
}
|
||||
|
||||
func alterColumnType(typ string, col schema.Column) schema.Column {
|
||||
nc := col
|
||||
typ = strings.ToLower(typ)
|
||||
switch true {
|
||||
// TODO: support for other types, nullability
|
||||
case strings.HasPrefix(typ, "varchar"):
|
||||
lengthStr := typ[len("varchar")+1 : len(typ)-1]
|
||||
length, err := strconv.ParseInt(lengthStr, 10, 64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ti := col.TypeInfo.ToSqlType().(sql.StringType)
|
||||
sqlType := sql.MustCreateString(sqltypes.VarChar, length, ti.Collation())
|
||||
nc.TypeInfo, err = typeinfo.FromSqlType(sqlType)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
case typ == "tinyint":
|
||||
nc.TypeInfo = typeinfo.Int8Type
|
||||
case typ == "smallint":
|
||||
nc.TypeInfo = typeinfo.Int16Type
|
||||
case typ == "mediumint":
|
||||
nc.TypeInfo = typeinfo.Int24Type
|
||||
case typ == "int", typ == "integer":
|
||||
nc.TypeInfo = typeinfo.Int32Type
|
||||
case typ == "bigint":
|
||||
nc.TypeInfo = typeinfo.Int64Type
|
||||
case typ == "tinyint unsigned":
|
||||
nc.TypeInfo = typeinfo.Uint8Type
|
||||
case typ == "smallint unsigned":
|
||||
nc.TypeInfo = typeinfo.Uint16Type
|
||||
case typ == "mediumint unsigned":
|
||||
nc.TypeInfo = typeinfo.Uint24Type
|
||||
case typ == "int unsigned", typ == "integer unsigned":
|
||||
nc.TypeInfo = typeinfo.Uint32Type
|
||||
case typ == "bigint unsigned":
|
||||
nc.TypeInfo = typeinfo.Uint64Type
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported type %s", typ))
|
||||
}
|
||||
|
||||
return nc
|
||||
}
|
||||
|
||||
func (cmd ChangeTypeCmd) createArgParser() *argparser.ArgParser {
|
||||
ap := argparser.NewArgParser()
|
||||
ap.ArgListHelp = append(ap.ArgListHelp, [2]string{"table", "table(s) whose schema is being changed"})
|
||||
ap.ArgListHelp = append(ap.ArgListHelp, [2]string{"column", "column whose type is being changed"})
|
||||
ap.ArgListHelp = append(ap.ArgListHelp, [2]string{"type", "new column type as a SQL type string"})
|
||||
return ap
|
||||
}
|
||||
|
||||
func (cmd ChangeTypeCmd) CreateMarkdown(fs filesys.Filesys, path, commandStr string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ cli.Command = ChangeTypeCmd{}
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
var Commands = cli.NewSubCommandHandler("schema", "Commands for showing and importing table schemas.", []cli.Command{
|
||||
ExportCmd{},
|
||||
ImportCmd{},
|
||||
ChangeTypeCmd{},
|
||||
ShowCmd{},
|
||||
TagsCmd{},
|
||||
})
|
||||
|
||||
@@ -42,6 +42,9 @@ func ModifyColumn(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strings.ToLower(existingCol.Name) == strings.ToLower(newCol.Name) {
|
||||
newCol.Name = existingCol.Name
|
||||
}
|
||||
if err := validateModifyColumn(ctx, tbl, existingCol, newCol); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -49,6 +52,16 @@ func ModifyColumn(
|
||||
// Modify statements won't include key info, so fill it in from the old column
|
||||
if existingCol.IsPartOfPK {
|
||||
newCol.IsPartOfPK = true
|
||||
foundNotNullConstraint := false
|
||||
for _, constraint := range newCol.Constraints {
|
||||
if _, ok := constraint.(schema.NotNullConstraint); ok {
|
||||
foundNotNullConstraint = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundNotNullConstraint {
|
||||
newCol.Constraints = append(newCol.Constraints, schema.NotNullConstraint{})
|
||||
}
|
||||
}
|
||||
|
||||
newSchema, err := replaceColumnInSchema(sch, existingCol, newCol, order)
|
||||
@@ -105,6 +118,9 @@ func updateTableWithModifiedColumn(ctx context.Context, tbl *doltdb.Table, oldSc
|
||||
}
|
||||
|
||||
if !oldCol.TypeInfo.Equals(modifiedCol.TypeInfo) {
|
||||
if schema.IsKeyless(newSch) {
|
||||
return nil, fmt.Errorf("keyless table column type alteration is not yet supported")
|
||||
}
|
||||
rowData, err = updateRowDataWithNewType(ctx, rowData, tbl.ValueReadWriter(), oldSch, newSch, oldCol, modifiedCol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -18,12 +18,14 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/dolthub/go-mysql-server/sql"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/dtestutils"
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/row"
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/schema"
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/schema/typeinfo"
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/table/editor"
|
||||
"github.com/dolthub/dolt/go/store/types"
|
||||
)
|
||||
@@ -36,6 +38,17 @@ func TestModifyColumn(t *testing.T) {
|
||||
schema.NewColumn("is_married", dtestutils.IsMarriedTag, types.BoolKind, false, schema.NotNullConstraint{}),
|
||||
schema.NewColumn("title", dtestutils.TitleTag, types.StringKind, false),
|
||||
)
|
||||
ti, err := typeinfo.FromSqlType(sql.TinyText)
|
||||
require.NoError(t, err)
|
||||
newNameColSameTag, err := schema.NewColumnWithTypeInfo("name", dtestutils.NameTag, ti, false, "", false, "", schema.NotNullConstraint{})
|
||||
require.NoError(t, err)
|
||||
alteredTypeSch2 := dtestutils.CreateSchema(
|
||||
schema.NewColumn("id", dtestutils.IdTag, types.UUIDKind, true, schema.NotNullConstraint{}),
|
||||
newNameColSameTag,
|
||||
schema.NewColumn("age", dtestutils.AgeTag, types.UintKind, false, schema.NotNullConstraint{}),
|
||||
schema.NewColumn("is_married", dtestutils.IsMarriedTag, types.BoolKind, false, schema.NotNullConstraint{}),
|
||||
schema.NewColumn("title", dtestutils.TitleTag, types.StringKind, false),
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -61,12 +74,12 @@ func TestModifyColumn(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "remove null constraint",
|
||||
existingColumn: schema.NewColumn("id", dtestutils.IdTag, types.UUIDKind, true, schema.NotNullConstraint{}),
|
||||
newColumn: schema.NewColumn("newId", dtestutils.IdTag, types.UUIDKind, true),
|
||||
existingColumn: schema.NewColumn("age", dtestutils.AgeTag, types.UintKind, false, schema.NotNullConstraint{}),
|
||||
newColumn: schema.NewColumn("newAge", dtestutils.AgeTag, types.UintKind, false),
|
||||
expectedSchema: dtestutils.CreateSchema(
|
||||
schema.NewColumn("newId", dtestutils.IdTag, types.UUIDKind, true),
|
||||
schema.NewColumn("id", dtestutils.IdTag, types.UUIDKind, true, schema.NotNullConstraint{}),
|
||||
schema.NewColumn("name", dtestutils.NameTag, types.StringKind, false, schema.NotNullConstraint{}),
|
||||
schema.NewColumn("age", dtestutils.AgeTag, types.UintKind, false, schema.NotNullConstraint{}),
|
||||
schema.NewColumn("newAge", dtestutils.AgeTag, types.UintKind, false),
|
||||
schema.NewColumn("is_married", dtestutils.IsMarriedTag, types.BoolKind, false, schema.NotNullConstraint{}),
|
||||
schema.NewColumn("title", dtestutils.TitleTag, types.StringKind, false),
|
||||
),
|
||||
@@ -144,6 +157,13 @@ func TestModifyColumn(t *testing.T) {
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "type change same tag",
|
||||
existingColumn: schema.NewColumn("name", dtestutils.NameTag, types.StringKind, false, schema.NotNullConstraint{}),
|
||||
newColumn: newNameColSameTag,
|
||||
expectedSchema: alteredTypeSch2,
|
||||
expectedRows: dtestutils.TypedRows,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -16,10 +16,15 @@ package typeinfo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/dolthub/go-mysql-server/sql"
|
||||
"github.com/dolthub/vitess/go/sqltypes"
|
||||
|
||||
"github.com/dolthub/dolt/go/store/types"
|
||||
)
|
||||
@@ -206,7 +211,19 @@ func bitTypeConverter(ctx context.Context, src *bitType, destTi TypeInfo) (tc Ty
|
||||
case *boolType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
case *datetimeType:
|
||||
return nil, false, IncompatibleTypeConversion.New(src.String(), destTi.String())
|
||||
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
|
||||
if v == nil || v == types.NullValue {
|
||||
return types.NullValue, nil
|
||||
}
|
||||
val, ok := v.(types.Uint)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type converting bit to %s: %T", strings.ToLower(dest.String()), v)
|
||||
}
|
||||
if val == 0 {
|
||||
return types.Timestamp(sql.Datetime.Zero().(time.Time)), nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid %s value: %d", strings.ToLower(dest.String()), uint64(val))
|
||||
}, true, nil
|
||||
case *decimalType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
case *enumType:
|
||||
@@ -226,12 +243,59 @@ func bitTypeConverter(ctx context.Context, src *bitType, destTi TypeInfo) (tc Ty
|
||||
case *uuidType:
|
||||
return nil, false, IncompatibleTypeConversion.New(src.String(), destTi.String())
|
||||
case *varBinaryType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
return bitTypeConverterInterpretAsString(ctx, src, destTi)
|
||||
case *varStringType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
if dest.sqlStringType.Type() == sqltypes.Text {
|
||||
return bitTypeConverterInterpretAsString(ctx, src, destTi)
|
||||
} else {
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
}
|
||||
case *yearType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
default:
|
||||
return nil, false, UnhandledTypeConversion.New(src.String(), destTi.String())
|
||||
}
|
||||
}
|
||||
|
||||
func bitTypeConverterInterpretAsString(ctx context.Context, src *bitType, destTi TypeInfo) (tc TypeConverter, needsConversion bool, err error) {
|
||||
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
|
||||
if v == nil || v == types.NullValue {
|
||||
return types.NullValue, nil
|
||||
}
|
||||
val, ok := v.(types.Uint)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type interpreting bit as string: %T", v)
|
||||
}
|
||||
bytes := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(bytes, uint64(val))
|
||||
|
||||
numOfBits := src.sqlBitType.NumberOfBits()
|
||||
switch true {
|
||||
case numOfBits <= 8:
|
||||
bytes = bytes[:1]
|
||||
case numOfBits <= 16:
|
||||
bytes = bytes[:2]
|
||||
case numOfBits <= 24:
|
||||
bytes = bytes[:3]
|
||||
case numOfBits <= 32:
|
||||
bytes = bytes[:4]
|
||||
case numOfBits <= 40:
|
||||
bytes = bytes[:5]
|
||||
case numOfBits <= 48:
|
||||
bytes = bytes[:6]
|
||||
case numOfBits <= 56:
|
||||
bytes = bytes[:7]
|
||||
}
|
||||
// MySQL's BIT strings are reversed
|
||||
for i, j := 0, len(bytes)-1; i < j; i, j = i+1, j-1 {
|
||||
bytes[i], bytes[j] = bytes[j], bytes[i]
|
||||
}
|
||||
s := string(bytes)
|
||||
if dest, ok := destTi.(*varStringType); ok && dest.sqlStringType.Type() == sqltypes.Text {
|
||||
if !utf8.ValidString(s) {
|
||||
return nil, fmt.Errorf(`invalid %s value: "%s"`, strings.ToLower(dest.String()), s)
|
||||
}
|
||||
}
|
||||
return destTi.ConvertValueToNomsValue(ctx, vrw, s)
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
@@ -199,7 +199,16 @@ func (ti *decimalType) ToSqlType() sql.Type {
|
||||
func decimalTypeConverter(ctx context.Context, src *decimalType, destTi TypeInfo) (tc TypeConverter, needsConversion bool, err error) {
|
||||
switch dest := destTi.(type) {
|
||||
case *bitType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
|
||||
if v == nil || v == types.NullValue {
|
||||
return types.NullValue, nil
|
||||
}
|
||||
val, ok := v.(types.Decimal)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type converting decimal to bit: %T", v)
|
||||
}
|
||||
return dest.ConvertValueToNomsValue(ctx, vrw, decimal.Decimal(val))
|
||||
}, true, nil
|
||||
case *boolType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
case *datetimeType:
|
||||
@@ -207,27 +216,101 @@ func decimalTypeConverter(ctx context.Context, src *decimalType, destTi TypeInfo
|
||||
case *decimalType:
|
||||
return wrapIsValid(dest.IsValid, src, dest)
|
||||
case *enumType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
if src.sqlDecimalType.Scale() > 0 {
|
||||
return nil, false, IncompatibleTypeConversion.New(src.String(), destTi.String())
|
||||
}
|
||||
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
|
||||
if v == nil || v == types.NullValue {
|
||||
return types.NullValue, nil
|
||||
}
|
||||
val, ok := v.(types.Decimal)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type converting decimal to enum: %T", v)
|
||||
}
|
||||
uintVal, err := sql.Uint64.Convert(decimal.Decimal(val))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if uintVal.(uint64) == 0 {
|
||||
return types.Uint(0), nil
|
||||
}
|
||||
return dest.ConvertValueToNomsValue(ctx, vrw, uintVal)
|
||||
}, true, nil
|
||||
case *floatType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
case *inlineBlobType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
|
||||
s, err := src.ConvertNomsValueToValue(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dest.ConvertValueToNomsValue(ctx, vrw, s)
|
||||
}, true, nil
|
||||
case *intType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
|
||||
if v == nil || v == types.NullValue {
|
||||
return types.NullValue, nil
|
||||
}
|
||||
val, ok := v.(types.Decimal)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type converting decimal to int: %T", v)
|
||||
}
|
||||
return dest.ConvertValueToNomsValue(ctx, vrw, decimal.Decimal(val).Round(0))
|
||||
}, true, nil
|
||||
case *setType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
|
||||
s, err := src.ConvertNomsValueToValue(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dest.ConvertValueToNomsValue(ctx, vrw, s)
|
||||
}, true, nil
|
||||
case *timeType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
case *uintType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
|
||||
if v == nil || v == types.NullValue {
|
||||
return types.NullValue, nil
|
||||
}
|
||||
val, ok := v.(types.Decimal)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type converting decimal to uint: %T", v)
|
||||
}
|
||||
return dest.ConvertValueToNomsValue(ctx, vrw, decimal.Decimal(val).Round(0))
|
||||
}, true, nil
|
||||
case *uuidType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
case *varBinaryType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
|
||||
s, err := src.ConvertNomsValueToValue(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dest.ConvertValueToNomsValue(ctx, vrw, s)
|
||||
}, true, nil
|
||||
case *varStringType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
|
||||
s, err := src.ConvertNomsValueToValue(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dest.ConvertValueToNomsValue(ctx, vrw, s)
|
||||
}, true, nil
|
||||
case *yearType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
|
||||
if v == nil || v == types.NullValue {
|
||||
return types.NullValue, nil
|
||||
}
|
||||
val, ok := v.(types.Decimal)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type converting decimal to year: %T", v)
|
||||
}
|
||||
intVal, err := sql.Int64.Convert(decimal.Decimal(val))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dest.ConvertValueToNomsValue(ctx, vrw, intVal)
|
||||
}, true, nil
|
||||
default:
|
||||
return nil, false, UnhandledTypeConversion.New(src.String(), destTi.String())
|
||||
}
|
||||
|
||||
@@ -69,6 +69,9 @@ func CreateEnumTypeFromParams(params map[string]string) (TypeInfo, error) {
|
||||
// ConvertNomsValueToValue implements TypeInfo interface.
|
||||
func (ti *enumType) ConvertNomsValueToValue(v types.Value) (interface{}, error) {
|
||||
if val, ok := v.(types.Uint); ok {
|
||||
if val == 0 {
|
||||
return "", nil
|
||||
}
|
||||
res, err := ti.sqlEnumType.Unmarshal(int64(val))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`"%v" cannot convert "%v" to value`, ti.String(), val)
|
||||
@@ -87,6 +90,9 @@ func (ti *enumType) ReadFrom(_ *types.NomsBinFormat, reader types.CodecReader) (
|
||||
switch k {
|
||||
case types.UintKind:
|
||||
n := reader.ReadUint()
|
||||
if n == 0 {
|
||||
return "", nil
|
||||
}
|
||||
res, err := ti.sqlEnumType.Unmarshal(int64(n))
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
@@ -225,7 +231,24 @@ func enumTypeConverter(ctx context.Context, src *enumType, destTi TypeInfo) (tc
|
||||
case *decimalType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
case *enumType:
|
||||
return wrapIsValid(dest.IsValid, src, dest)
|
||||
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
|
||||
if v == nil || v == types.NullValue {
|
||||
return types.NullValue, nil
|
||||
}
|
||||
val, ok := v.(types.Uint)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type converting enum to other enum: %T", v)
|
||||
}
|
||||
valStr, err := src.sqlEnumType.Unmarshal(int64(val))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newVal, err := dest.sqlEnumType.Marshal(valStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return types.Uint(newVal), nil
|
||||
}, true, nil
|
||||
case *floatType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
case *inlineBlobType:
|
||||
|
||||
@@ -61,7 +61,7 @@ func TestEnumConvertNomsValueToValue(t *testing.T) {
|
||||
generateEnumType(t, 2),
|
||||
0,
|
||||
"",
|
||||
true,
|
||||
false,
|
||||
},
|
||||
{
|
||||
generateEnumType(t, 3),
|
||||
@@ -178,7 +178,7 @@ func TestEnumFormatValue(t *testing.T) {
|
||||
generateEnumType(t, 2),
|
||||
0,
|
||||
"",
|
||||
true,
|
||||
false,
|
||||
},
|
||||
{
|
||||
generateEnumType(t, 3),
|
||||
|
||||
@@ -17,7 +17,9 @@ package typeinfo
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/dolthub/go-mysql-server/sql"
|
||||
"github.com/dolthub/vitess/go/sqltypes"
|
||||
@@ -216,7 +218,21 @@ func (ti *floatType) ToSqlType() sql.Type {
|
||||
func floatTypeConverter(ctx context.Context, src *floatType, destTi TypeInfo) (tc TypeConverter, needsConversion bool, err error) {
|
||||
switch dest := destTi.(type) {
|
||||
case *bitType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
|
||||
if v == nil || v == types.NullValue {
|
||||
return types.NullValue, nil
|
||||
}
|
||||
val, ok := v.(types.Float)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type converting float to enum: %T", v)
|
||||
}
|
||||
fltVal := floatTypeRoundToZero(float64(val))
|
||||
intVal, err := sql.Int64.Convert(fltVal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dest.ConvertValueToNomsValue(ctx, vrw, uint64(intVal.(int64)))
|
||||
}, true, nil
|
||||
case *boolType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
case *datetimeType:
|
||||
@@ -224,19 +240,43 @@ func floatTypeConverter(ctx context.Context, src *floatType, destTi TypeInfo) (t
|
||||
case *decimalType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
case *enumType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
|
||||
if v == nil || v == types.NullValue {
|
||||
return types.NullValue, nil
|
||||
}
|
||||
val, ok := v.(types.Float)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type converting float to enum: %T", v)
|
||||
}
|
||||
if val == 0 {
|
||||
return types.Uint(0), nil
|
||||
}
|
||||
return dest.ConvertValueToNomsValue(ctx, vrw, float64(val))
|
||||
}, true, nil
|
||||
case *floatType:
|
||||
return wrapIsValid(dest.IsValid, src, dest)
|
||||
case *inlineBlobType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
case *intType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
return floatTypeConverterRoundToZero(ctx, src, destTi)
|
||||
case *setType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
|
||||
if v == nil || v == types.NullValue {
|
||||
return types.NullValue, nil
|
||||
}
|
||||
val, ok := v.(types.Float)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type converting float to set: %T", v)
|
||||
}
|
||||
if float64(val) != math.Trunc(float64(val)) { // not a whole number
|
||||
return nil, fmt.Errorf("invalid set value: %v", float64(val))
|
||||
}
|
||||
return dest.ConvertValueToNomsValue(ctx, vrw, float64(val))
|
||||
}, true, nil
|
||||
case *timeType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
case *uintType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
return floatTypeConverterRoundToZero(ctx, src, destTi)
|
||||
case *uuidType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
case *varBinaryType:
|
||||
@@ -249,3 +289,24 @@ func floatTypeConverter(ctx context.Context, src *floatType, destTi TypeInfo) (t
|
||||
return nil, false, UnhandledTypeConversion.New(src.String(), destTi.String())
|
||||
}
|
||||
}
|
||||
|
||||
func floatTypeRoundToZero(val float64) float64 {
|
||||
truncated := math.Trunc(val)
|
||||
if math.Abs(val-truncated) > 0.5 {
|
||||
return truncated + math.Copysign(1, val)
|
||||
}
|
||||
return truncated
|
||||
}
|
||||
|
||||
func floatTypeConverterRoundToZero(ctx context.Context, src *floatType, destTi TypeInfo) (tc TypeConverter, needsConversion bool, err error) {
|
||||
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
|
||||
if v == nil || v == types.NullValue {
|
||||
return types.NullValue, nil
|
||||
}
|
||||
val, ok := v.(types.Float)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type converting float to %s: %T", strings.ToLower(destTi.String()), v)
|
||||
}
|
||||
return destTi.ConvertValueToNomsValue(ctx, vrw, floatTypeRoundToZero(float64(val)))
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
@@ -268,7 +268,19 @@ func intTypeConverter(ctx context.Context, src *intType, destTi TypeInfo) (tc Ty
|
||||
case *decimalType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
case *enumType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
|
||||
if v == nil || v == types.NullValue {
|
||||
return types.NullValue, nil
|
||||
}
|
||||
val, ok := v.(types.Int)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type converting int to enum: %T", v)
|
||||
}
|
||||
if val == 0 {
|
||||
return types.Uint(0), nil
|
||||
}
|
||||
return dest.ConvertValueToNomsValue(ctx, vrw, int64(val))
|
||||
}, true, nil
|
||||
case *floatType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
case *inlineBlobType:
|
||||
|
||||
@@ -268,7 +268,19 @@ func uintTypeConverter(ctx context.Context, src *uintType, destTi TypeInfo) (tc
|
||||
case *decimalType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
case *enumType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
|
||||
if v == nil || v == types.NullValue {
|
||||
return types.NullValue, nil
|
||||
}
|
||||
val, ok := v.(types.Uint)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type converting uint to enum: %T", v)
|
||||
}
|
||||
if val == 0 {
|
||||
return types.Uint(0), nil
|
||||
}
|
||||
return dest.ConvertValueToNomsValue(ctx, vrw, uint64(val))
|
||||
}, true, nil
|
||||
case *floatType:
|
||||
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
|
||||
case *inlineBlobType:
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
# Type Change Tests
|
||||
|
||||
### What are these tests?
|
||||
|
||||
All of the tests contained in this directory that begin with `modify_` are a part of the collection of tests observing MySQL 8.0's behavior when changing a type from one to another. The goal of the tests are to ensure that we, as close as we can, copy MySQL's behavior when changing between the different types. This is necessary as the behavior of each modification is not fully documented, therefore the only way to know how MySQL operates is to painstakingly try every combination. It is not feasible to try the entire set of type changes, and so we aim to try as wide a range of conversions as possible without extending full-repository testing times to an unreasonable amount.
|
||||
|
||||
The `common_test` file is the heart of these tests, containing the running logic that each file references. The testing files are just a large selection of tests that pass their contents as parameters to the `RunModifyTypeTests` function. In addition, the common file hosts any helper functions that the test files may need. This is due to the generated nature of the test files, although it is not required that all functions live in the common file. It is probable that a helper in one file will be useful in another, thus it's a convenient location for all.
|
||||
|
||||
Additionally, every test assumes that the table has the following signature: `CREATE TABLE test(pk BIGINT PRIMARY KEY, v1 X);`, with `v1` having the type defined by the test.
|
||||
|
||||
### How were these files generated?
|
||||
|
||||
Each file follows a basic template: copyright header, package name, imports, test function name, test array, and call to `RunModifyTypeTests`. All but the function name and test array were static strings prepended and appended accordingly. The function name was added by hand for each file.
|
||||
|
||||
The test array is an array of the predefinied struct `ModifyTypeTest`.
|
||||
* `FromType`: The type that we start with and are converting from. This is the same for every test in a file.
|
||||
* `ToType`: The type that we are converting to.
|
||||
* `InsertValues`: These are the values that are being inserted into the column before the conversion. These are necessary as all conversions are valid when there are no rows. For the statement `INSERT INTO test VALUES %s;`, this string is copied to `%s`. All values should normally insert for the given type, as the insert is expected to succeed.
|
||||
* `SelectRes`: This is an array of values that we expect from running `SELECT v1 FROM test ORDER BY pk;`. These are all expected to be the widest type for a particular _base_ type. For example, if you're expecting an unsigned integer with the value 1, then your value would be `uint64(1)`, even if the column actually returns `uint8(1)`. This simplifies comparisons, as we enforce that each type returns the correct type elsewhere.
|
||||
* `ExpectedErr`: If the conversion should fail, then this will be true (skipping the aforementioned `SELECT` statement). Otherwise, it should be false.
|
||||
|
||||
To generate each test file, the process was generally as follows.
|
||||
1) Select the `FromType` for the file you are generating.
|
||||
2) Determine the set of values to be tested. For signed integer types, this was negative values up to the lowest value represented by that type, going to positive values up to the highest value represented for that type. Then adding values in the middle, including 0, and other "bit maximums" if they fit (such as `2^7-1`, `2^8-1`, `2^16-1`, `-2^7`, etc.).
|
||||
3) Determine the combinations of values from the set to be tested. For signed integers, this was essentially starting at zero and fanning out in both directions. As an example, given the numbers `-2|-1|0|1|2`, the tested sets were `0`, `0|1`, `-1|0|1`, `0|1|2`, and `-2|-1|0|1|2`. This strategy does mean that an invalid value of a smaller number could falsely paint a type as being completely incompatible. For example, if `0` did not convert between the integer type and another type, then all conversions would fail since they all contain `0`. In practice though, the smallest values seem to be the most convertible.
|
||||
|
||||
From here, a list of all the types to test was referenced (see the bottom of the README), and a MySQL client created a table, inserted the one of the value sets, and converted the target column (`v1`) to one of the referenced types. If the change failed, then we record that there was an error (details on the error aren't necessary). If it succeeded, we ran the same `SELECT` statement mentioned earlier in the README, and recorded the output. This then was formatted into a valid `ModifyTypeTest` struct, and appended to a string buffer, which was written to a file once all of the permutations were tested.
|
||||
|
||||
Once the test file was moved to Dolt, all of the tests were ran to see if the placeholder logic was sufficient for that conversion, and changed if it wasn't. In some cases, Dolt and MySQL slightly differ (such as with `FLOAT` rounding, giving `1.124005` vs `1.124005042`), so the tests were manually adjusted to reflect Dolt's comparable output rather than MySQL's exact output.
|
||||
|
||||
### Where is the generation program?
|
||||
|
||||
No program is provided, as the source was constantly changed to generate each test file. This is because the bulk of the code dealt with generating the permutations of the values, along with determining which values to use (which was an arbitrary decision). Although a more formal program could have been written with more reusable code, it was determined to not be worth the effort at the time. Using the information given above, it is relatively trivial to construct your own program to generate test files. And even that is just a time saver—you can just as easily write tests by hand. The test format was chosen to be simple to read and simple to write. Generating tests through a program is **NOT REQUIRED**.
|
||||
|
||||
### Type Reference
|
||||
|
||||
Here are all of the types that are tested from/to. Similar types are grouped together:
|
||||
|
||||
TINYINT<br>
|
||||
SMALLINT<br>
|
||||
MEDIUMINT<br>
|
||||
INT<br>
|
||||
BIGINT<br>
|
||||
<br>
|
||||
TINYINT UNSIGNED<br>
|
||||
SMALLINT UNSIGNED<br>
|
||||
MEDIUMINT UNSIGNED<br>
|
||||
INT UNSIGNED<br>
|
||||
BIGINT UNSIGNED<br>
|
||||
<br>
|
||||
FLOAT<br>
|
||||
DOUBLE<br>
|
||||
<br>
|
||||
DECIMAL(1,0)<br>
|
||||
DECIMAL(15,0)<br>
|
||||
DECIMAL(30,0)<br>
|
||||
DECIMAL(65,0)<br>
|
||||
DECIMAL(1,1)<br>
|
||||
DECIMAL(15,1)<br>
|
||||
DECIMAL(30,1)<br>
|
||||
DECIMAL(65,1)<br>
|
||||
DECIMAL(15,15)<br>
|
||||
DECIMAL(30,15)<br>
|
||||
DECIMAL(65,15)<br>
|
||||
DECIMAL(30,30)<br>
|
||||
DECIMAL(65,30)<br>
|
||||
<br>
|
||||
BIT(1)<br>
|
||||
BIT(8)<br>
|
||||
BIT(16)<br>
|
||||
BIT(24)<br>
|
||||
BIT(32)<br>
|
||||
BIT(48)<br>
|
||||
BIT(64)<br>
|
||||
<br>
|
||||
TINYBLOB<br>
|
||||
BLOB<br>
|
||||
MEDIUMBLOB<br>
|
||||
LONGBLOB<br>
|
||||
<br>
|
||||
TINYTEXT<br>
|
||||
TEXT<br>
|
||||
MEDIUMTEXT<br>
|
||||
LONGTEXT<br>
|
||||
<br>
|
||||
CHAR(1)<br>
|
||||
CHAR(10)<br>
|
||||
CHAR(100)<br>
|
||||
CHAR(255)<br>
|
||||
<br>
|
||||
BINARY(1)<br>
|
||||
BINARY(10)<br>
|
||||
BINARY(100)<br>
|
||||
BINARY(255)<br>
|
||||
<br>
|
||||
VARCHAR(1)<br>
|
||||
VARCHAR(10)<br>
|
||||
VARCHAR(100)<br>
|
||||
VARCHAR(255)<br>
|
||||
VARCHAR(1023)<br>
|
||||
VARCHAR(4095)<br>
|
||||
VARCHAR(16383)<br>
|
||||
<br>
|
||||
VARBINARY(1)<br>
|
||||
VARBINARY(10)<br>
|
||||
VARBINARY(100)<br>
|
||||
VARBINARY(255)<br>
|
||||
VARBINARY(1023)<br>
|
||||
VARBINARY(4095)<br>
|
||||
VARBINARY(16383)<br>
|
||||
<br>
|
||||
YEAR<br>
|
||||
<br>
|
||||
DATE<br>
|
||||
<br>
|
||||
TIME<br>
|
||||
<br>
|
||||
TIMESTAMP<br>
|
||||
DATETIME<br>
|
||||
<br>
|
||||
ENUM('A')<br>
|
||||
ENUM('B')<br>
|
||||
ENUM('C')<br>
|
||||
ENUM('A','B')<br>
|
||||
ENUM('A','C')<br>
|
||||
ENUM('B','C')<br>
|
||||
ENUM('A','B','C')<br>
|
||||
ENUM('C','A','B')<br>
|
||||
<br>
|
||||
SET('A')<br>
|
||||
SET('B')<br>
|
||||
SET('C')<br>
|
||||
SET('A','B')<br>
|
||||
SET('A','C')<br>
|
||||
SET('B','C')<br>
|
||||
SET('A','B','C')<br>
|
||||
SET('C','A','B')<br>
|
||||
@@ -0,0 +1,162 @@
|
||||
// Copyright 2021 Dolthub, Inc.
|
||||
//
|
||||
// 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 altertests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dolthub/go-mysql-server/sql"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/dtestutils"
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/env"
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/sqle"
|
||||
)
|
||||
|
||||
type ModifyTypeTest struct {
|
||||
FromType string
|
||||
ToType string
|
||||
InsertValues string
|
||||
SelectRes []interface{}
|
||||
ExpectedErr bool
|
||||
}
|
||||
|
||||
func RunModifyTypeTests(t *testing.T, tests []ModifyTypeTest) {
|
||||
dEnv := dtestutils.CreateTestEnv()
|
||||
for _, test := range tests {
|
||||
name := fmt.Sprintf("%s -> %s: %s", test.FromType, test.ToType, test.InsertValues)
|
||||
if len(name) > 200 {
|
||||
name = name[:200]
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
root, err := dEnv.WorkingRoot(ctx)
|
||||
require.NoError(t, err)
|
||||
root, err = executeModify(ctx, dEnv, root, fmt.Sprintf("CREATE TABLE test(pk BIGINT PRIMARY KEY, v1 %s);", test.FromType))
|
||||
require.NoError(t, err)
|
||||
root, err = executeModify(ctx, dEnv, root, fmt.Sprintf("INSERT INTO test VALUES %s;", test.InsertValues))
|
||||
require.NoError(t, err)
|
||||
root, err = executeModify(ctx, dEnv, root, fmt.Sprintf("ALTER TABLE test MODIFY v1 %s;", test.ToType))
|
||||
if test.ExpectedErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
res, err := executeSelect(ctx, dEnv, root, "SELECT v1 FROM test ORDER BY pk;")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.SelectRes, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func widenValue(v interface{}) interface{} {
|
||||
switch x := v.(type) {
|
||||
case int:
|
||||
return int64(x)
|
||||
case int8:
|
||||
return int64(x)
|
||||
case int16:
|
||||
return int64(x)
|
||||
case int32:
|
||||
return int64(x)
|
||||
case uint:
|
||||
return uint64(x)
|
||||
case uint8:
|
||||
return uint64(x)
|
||||
case uint16:
|
||||
return uint64(x)
|
||||
case uint32:
|
||||
return uint64(x)
|
||||
case float32:
|
||||
return float64(x)
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func parseTime(timestampLayout bool, value string) time.Time {
|
||||
var t time.Time
|
||||
var err error
|
||||
if timestampLayout {
|
||||
t, err = time.Parse("2006-01-02 15:04:05.999999", value)
|
||||
} else {
|
||||
t, err = time.Parse("2006-01-02", value)
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return t.UTC()
|
||||
}
|
||||
|
||||
func executeSelect(ctx context.Context, dEnv *env.DoltEnv, root *doltdb.RootValue, query string) ([]interface{}, error) {
|
||||
var err error
|
||||
db := sqle.NewDatabase("dolt", dEnv.DbData())
|
||||
engine, sqlCtx, err := sqle.NewTestEngine(ctx, db, root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, iter, err := engine.Query(sqlCtx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var vals []interface{}
|
||||
var r sql.Row
|
||||
for r, err = iter.Next(); err == nil; r, err = iter.Next() {
|
||||
if len(r) == 1 {
|
||||
// widen the values since we're testing values rather than types
|
||||
vals = append(vals, widenValue(r[0]))
|
||||
} else if len(r) > 1 {
|
||||
return nil, fmt.Errorf("expected return of single value from select: %q", query)
|
||||
} else { // no values
|
||||
vals = append(vals, nil)
|
||||
}
|
||||
}
|
||||
if err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
return vals, nil
|
||||
}
|
||||
|
||||
func executeModify(ctx context.Context, dEnv *env.DoltEnv, root *doltdb.RootValue, query string) (*doltdb.RootValue, error) {
|
||||
db := sqle.NewDatabase("dolt", dEnv.DbData())
|
||||
engine, sqlCtx, err := sqle.NewTestEngine(ctx, db, root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, iter, err := engine.Query(sqlCtx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for {
|
||||
_, err := iter.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
err = iter.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db.GetRoot(sqlCtx)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -711,7 +711,7 @@ func TestModifyColumnType(t *testing.T) {
|
||||
name: "alter modify column type different types reversed",
|
||||
setupStmts: []string{
|
||||
"create table test(pk bigint primary key, v1 varchar(20), index (v1))",
|
||||
"insert into test values (0, 3), (1, 2)",
|
||||
`insert into test values (0, "3"), (1, "2")`,
|
||||
},
|
||||
alterStmt: "alter table test modify column v1 bigint",
|
||||
tableName: "test",
|
||||
|
||||
@@ -724,7 +724,7 @@ func (t *AlterableDoltTable) ModifyColumn(ctx *sql.Context, columnName string, c
|
||||
return err
|
||||
}
|
||||
|
||||
existingCol, ok := sch.GetAllCols().GetByName(columnName)
|
||||
existingCol, ok := sch.GetAllCols().GetByNameCaseInsensitive(columnName)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("Column %s not found. This is a bug.", columnName))
|
||||
}
|
||||
@@ -761,14 +761,16 @@ func (t *AlterableDoltTable) ModifyColumn(ctx *sql.Context, columnName string, c
|
||||
}
|
||||
}
|
||||
}
|
||||
tags, err := root.GenerateTagsForNewColumns(ctx, t.name, []string{col.Name}, []types.NomsKind{col.Kind})
|
||||
if err != nil {
|
||||
return err
|
||||
if existingCol.Kind != col.Kind { // We only change the tag when the underlying Noms kind changes
|
||||
tags, err := root.GenerateTagsForNewColumns(ctx, t.name, []string{col.Name}, []types.NomsKind{col.Kind})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(tags) != 1 {
|
||||
return fmt.Errorf("expected a generated tag length of 1")
|
||||
}
|
||||
col.Tag = tags[0]
|
||||
}
|
||||
if len(tags) != 1 {
|
||||
return fmt.Errorf("expected a generated tag length of 1")
|
||||
}
|
||||
col.Tag = tags[0]
|
||||
}
|
||||
|
||||
updatedTable, err := alterschema.ModifyColumn(ctx, table, existingCol, col, orderToOrder(order))
|
||||
|
||||
Reference in New Issue
Block a user