420 lines
13 KiB
Go
420 lines
13 KiB
Go
// Copyright 2019 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 schema
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
strings "strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/schema/typeinfo"
|
|
"github.com/dolthub/dolt/go/store/types"
|
|
"github.com/dolthub/dolt/go/store/val"
|
|
)
|
|
|
|
const (
|
|
lnColName = "last"
|
|
fnColName = "first"
|
|
addrColName = "address"
|
|
ageColName = "age"
|
|
titleColName = "title"
|
|
reservedColName = "reserved"
|
|
lnColTag = 1
|
|
fnColTag = 0
|
|
addrColTag = 6
|
|
ageColTag = 4
|
|
titleColTag = 40
|
|
reservedColTag = 50
|
|
)
|
|
|
|
var pkCols = []Column{
|
|
{Name: lnColName, Tag: lnColTag, Kind: types.StringKind, IsPartOfPK: true, TypeInfo: typeinfo.StringDefaultType},
|
|
{Name: fnColName, Tag: fnColTag, Kind: types.StringKind, IsPartOfPK: true, TypeInfo: typeinfo.StringDefaultType},
|
|
}
|
|
var nonPkCols = []Column{
|
|
{Name: addrColName, Tag: addrColTag, Kind: types.StringKind, TypeInfo: typeinfo.StringDefaultType},
|
|
{Name: ageColName, Tag: ageColTag, Kind: types.UintKind, TypeInfo: typeinfo.FromKind(types.UintKind)},
|
|
{Name: titleColName, Tag: titleColTag, Kind: types.StringKind, TypeInfo: typeinfo.StringDefaultType},
|
|
{Name: reservedColName, Tag: reservedColTag, Kind: types.StringKind, TypeInfo: typeinfo.StringDefaultType},
|
|
}
|
|
|
|
var allCols = append(append([]Column(nil), pkCols...), nonPkCols...)
|
|
|
|
func TestNewSchema(t *testing.T) {
|
|
allColColl := NewColCollection(allCols...)
|
|
pkColColl := NewColCollection()
|
|
|
|
indexCol := NewIndexCollection(allColColl, pkColColl)
|
|
|
|
checkCol := NewCheckCollection()
|
|
_, err := checkCol.AddCheck("chk_age", "age > 0", true, false)
|
|
require.NoError(t, err)
|
|
|
|
// Nil ordinals
|
|
sch, err := NewSchema(allColColl, nil, Collation_Default, indexCol, checkCol)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []int{0, 1}, sch.GetPkOrdinals())
|
|
require.Equal(t, Collation_Default, sch.GetCollation())
|
|
require.True(t, sch.Indexes().Equals(indexCol))
|
|
require.True(t, sch.Checks().Equals(checkCol))
|
|
|
|
// Set ordinals explicitly
|
|
indexCol.(*indexCollectionImpl).pks = []uint64{fnColTag, lnColTag}
|
|
sch, err = NewSchema(allColColl, []int{1, 0}, Collation_Default, indexCol, checkCol)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []int{1, 0}, sch.GetPkOrdinals())
|
|
require.Equal(t, Collation_Default, sch.GetCollation())
|
|
require.True(t, sch.Indexes().Equals(indexCol))
|
|
require.True(t, sch.Checks().Equals(checkCol))
|
|
}
|
|
|
|
func TestSchema(t *testing.T) {
|
|
colColl := NewColCollection(allCols...)
|
|
schFromCols, err := SchemaFromCols(colColl)
|
|
require.NoError(t, err)
|
|
|
|
testSchema("SchemaFromCols", schFromCols, t)
|
|
|
|
testKeyColColl := NewColCollection(pkCols...)
|
|
testNonKeyColsColl := NewColCollection(nonPkCols...)
|
|
schFromPKAndNonPKCols, _ := SchemaFromPKAndNonPKCols(testKeyColColl, testNonKeyColsColl)
|
|
|
|
testSchema("SchemaFromPKAndNonPKCols", schFromPKAndNonPKCols, t)
|
|
|
|
eq := SchemasAreEqual(schFromCols, schFromPKAndNonPKCols)
|
|
assert.True(t, eq, "schemas should be equal")
|
|
}
|
|
|
|
func TestSchemaWithNoPKs(t *testing.T) {
|
|
colColl := NewColCollection(nonPkCols...)
|
|
_, _ = SchemaFromCols(colColl)
|
|
|
|
assert.NotPanics(t, func() {
|
|
UnkeyedSchemaFromCols(colColl)
|
|
})
|
|
}
|
|
|
|
func TestGetSharedCols(t *testing.T) {
|
|
colColl := NewColCollection(nonPkCols...)
|
|
sch, _ := SchemaFromCols(colColl)
|
|
|
|
names := []string{addrColName, ageColName}
|
|
kinds := []types.NomsKind{types.StringKind, types.UintKind}
|
|
res := GetSharedCols(sch, names, kinds)
|
|
|
|
expected := []Column{
|
|
mustGetCol(colColl, addrColName),
|
|
mustGetCol(colColl, ageColName),
|
|
}
|
|
|
|
assert.Equal(t, expected, res)
|
|
}
|
|
|
|
func TestSetPkOrder(t *testing.T) {
|
|
|
|
// GetPkCols() should always return columns in ordinal order
|
|
// GetAllCols() should always return columns in the defined schema's order
|
|
t.Run("returns the correct GetPkCols() order", func(t *testing.T) {
|
|
allColColl := NewColCollection(allCols...)
|
|
pkColColl := NewColCollection(pkCols...)
|
|
sch, err := SchemaFromCols(allColColl)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, allColColl, sch.GetAllCols())
|
|
require.Equal(t, pkColColl, sch.GetPKCols())
|
|
|
|
err = sch.SetPkOrdinals([]int{1, 0})
|
|
require.NoError(t, err)
|
|
|
|
expectedPkColColl := NewColCollection(pkCols[1], pkCols[0])
|
|
require.Equal(t, expectedPkColColl, sch.GetPKCols())
|
|
require.Equal(t, allColColl, sch.GetAllCols())
|
|
})
|
|
|
|
t.Run("Can round-trip", func(t *testing.T) {
|
|
allColColl := NewColCollection(allCols...)
|
|
pkColColl := NewColCollection(pkCols...)
|
|
sch, err := SchemaFromCols(allColColl)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, allColColl, sch.GetAllCols())
|
|
require.Equal(t, pkColColl, sch.GetPKCols())
|
|
|
|
err = sch.SetPkOrdinals([]int{1, 0})
|
|
require.NoError(t, err)
|
|
|
|
err = sch.SetPkOrdinals([]int{0, 1})
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, allColColl, sch.GetAllCols())
|
|
require.Equal(t, pkColColl, sch.GetPKCols())
|
|
})
|
|
}
|
|
|
|
func mustGetCol(collection *ColCollection, name string) Column {
|
|
col, ok := collection.GetByName(name)
|
|
if !ok {
|
|
panic(fmt.Sprintf("%s not found", name))
|
|
}
|
|
return col
|
|
}
|
|
|
|
func TestIsKeyless(t *testing.T) {
|
|
cc := NewColCollection(allCols...)
|
|
pkSch, err := SchemaFromCols(cc)
|
|
require.NoError(t, err)
|
|
|
|
ok := IsKeyless(pkSch)
|
|
assert.False(t, ok)
|
|
|
|
cc = NewColCollection(nonPkCols...)
|
|
|
|
keylessSch, err := SchemaFromCols(cc)
|
|
assert.NoError(t, err)
|
|
|
|
ok = IsKeyless(keylessSch)
|
|
assert.True(t, ok)
|
|
}
|
|
|
|
func TestValidateForInsert(t *testing.T) {
|
|
t.Run("Validate good", func(t *testing.T) {
|
|
colColl := NewColCollection(allCols...)
|
|
assert.NoError(t, ValidateForInsert(colColl))
|
|
})
|
|
|
|
t.Run("Name collision", func(t *testing.T) {
|
|
cols := append(allCols, Column{Name: titleColName, Tag: 100, Kind: types.StringKind, TypeInfo: typeinfo.StringDefaultType})
|
|
colColl := NewColCollection(cols...)
|
|
|
|
err := ValidateForInsert(colColl)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, err, ErrColNameCollision)
|
|
})
|
|
|
|
t.Run("Case insensitive collision", func(t *testing.T) {
|
|
cols := append(allCols, Column{Name: strings.ToUpper(titleColName), Tag: 100, Kind: types.StringKind, TypeInfo: typeinfo.StringDefaultType})
|
|
colColl := NewColCollection(cols...)
|
|
|
|
err := ValidateForInsert(colColl)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, err, ErrColNameCollision)
|
|
})
|
|
|
|
t.Run("Tag collision", func(t *testing.T) {
|
|
cols := append(allCols, Column{Name: "newCol", Tag: lnColTag, Kind: types.StringKind, TypeInfo: typeinfo.StringDefaultType})
|
|
colColl := NewColCollection(cols...)
|
|
|
|
err := ValidateForInsert(colColl)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, err, ErrColTagCollision)
|
|
})
|
|
|
|
t.Run("AutoIncrement on integer types should be allowed", func(t *testing.T) {
|
|
// Test that auto_increment validation is no longer enforced at database layer
|
|
// The validation should now be handled by the go-mysql-server layer
|
|
// Only integer types should be allowed per MySQL 8.4.5 behavior
|
|
testCases := []struct {
|
|
name string
|
|
kind types.NomsKind
|
|
typeInfo typeinfo.TypeInfo
|
|
}{
|
|
{"INT", types.IntKind, typeinfo.FromKind(types.IntKind)},
|
|
{"UINT", types.UintKind, typeinfo.FromKind(types.UintKind)},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
autoIncCol := Column{
|
|
Name: "auto_inc_col",
|
|
Tag: 999,
|
|
Kind: tc.kind,
|
|
IsPartOfPK: true,
|
|
AutoIncrement: true,
|
|
TypeInfo: tc.typeInfo,
|
|
}
|
|
cols := []Column{autoIncCol}
|
|
colColl := NewColCollection(cols...)
|
|
|
|
// Should not error - validation moved to GMS layer
|
|
err := ValidateForInsert(colColl)
|
|
assert.NoError(t, err, "auto_increment validation should not be enforced at database layer for %s", tc.name)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestArePrimaryKeySetsDiffable(t *testing.T) {
|
|
tests := []struct {
|
|
Name string
|
|
From Schema
|
|
To Schema
|
|
Diffable bool
|
|
KeyMap val.OrdinalMapping
|
|
}{
|
|
{
|
|
Name: "Basic",
|
|
From: MustSchemaFromCols(NewColCollection(
|
|
NewColumn("pk", 0, types.IntKind, true))),
|
|
To: MustSchemaFromCols(NewColCollection(
|
|
NewColumn("pk", 0, types.IntKind, true))),
|
|
Diffable: true,
|
|
KeyMap: val.OrdinalMapping{0},
|
|
},
|
|
{
|
|
Name: "PK-Column renames",
|
|
From: MustSchemaFromCols(NewColCollection(
|
|
NewColumn("pk", 1, types.IntKind, true))),
|
|
To: MustSchemaFromCols(NewColCollection(
|
|
NewColumn("pk2", 1, types.IntKind, true))),
|
|
Diffable: true,
|
|
KeyMap: val.OrdinalMapping{0},
|
|
},
|
|
{
|
|
Name: "Only pk ordering should matter for diffability",
|
|
From: MustSchemaFromCols(NewColCollection(
|
|
NewColumn("col1", 0, types.IntKind, false),
|
|
NewColumn("pk", 1, types.IntKind, true))),
|
|
To: MustSchemaFromCols(NewColCollection(
|
|
NewColumn("pk", 1, types.IntKind, true))),
|
|
Diffable: true,
|
|
KeyMap: val.OrdinalMapping{0},
|
|
},
|
|
{
|
|
Name: "Only pk ordering should matter for diffability - inverse",
|
|
From: MustSchemaFromCols(NewColCollection(
|
|
NewColumn("pk", 1, types.IntKind, true))),
|
|
To: MustSchemaFromCols(NewColCollection(
|
|
NewColumn("col1", 2, types.IntKind, false),
|
|
NewColumn("pk", 1, types.IntKind, true))),
|
|
Diffable: true,
|
|
KeyMap: val.OrdinalMapping{0},
|
|
},
|
|
{
|
|
Name: "Only pk ordering should matter for diffability - compound",
|
|
From: MustSchemaFromCols(NewColCollection(
|
|
NewColumn("pk1", 0, types.IntKind, true),
|
|
NewColumn("col1", 1, types.IntKind, false),
|
|
NewColumn("pk2", 2, types.IntKind, true))),
|
|
To: MustSchemaFromCols(NewColCollection(
|
|
NewColumn("pk1", 0, types.IntKind, true),
|
|
NewColumn("pk2", 2, types.IntKind, true))),
|
|
Diffable: true,
|
|
KeyMap: val.OrdinalMapping{0, 1},
|
|
},
|
|
{
|
|
Name: "Tag mismatches",
|
|
From: MustSchemaFromCols(NewColCollection(
|
|
NewColumn("pk", 0, types.IntKind, true))),
|
|
To: MustSchemaFromCols(NewColCollection(
|
|
NewColumn("pk", 1, types.IntKind, true))),
|
|
Diffable: false,
|
|
},
|
|
{
|
|
Name: "PK Ordinal mismatches",
|
|
From: MustSchemaFromCols(NewColCollection(
|
|
NewColumn("pk1", 0, types.IntKind, true),
|
|
NewColumn("pk2", 1, types.IntKind, true))),
|
|
To: MustSchemaFromCols(NewColCollection(
|
|
NewColumn("pk2", 1, types.IntKind, true),
|
|
NewColumn("pk1", 0, types.IntKind, true))),
|
|
Diffable: false,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.Name, func(t *testing.T) {
|
|
d := ArePrimaryKeySetsDiffable(test.From, test.To)
|
|
require.Equal(t, test.Diffable, d)
|
|
|
|
// If they are diffable then we should be able to map their schemas from one to another.
|
|
if d {
|
|
keyMap, _, err := MapSchemaBasedOnTagAndName(test.From, test.To)
|
|
require.NoError(t, err)
|
|
require.Equal(t, test.KeyMap, keyMap)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestArePrimaryKeySetsDiffableTypeChanges(t *testing.T) {
|
|
// New format compares underlying SQL types
|
|
tests := []struct {
|
|
Name string
|
|
From Schema
|
|
To Schema
|
|
Diffable bool
|
|
}{
|
|
{
|
|
Name: "Int -> String (New Format)",
|
|
From: MustSchemaFromCols(NewColCollection(
|
|
NewColumn("pk", 0, types.IntKind, true))),
|
|
To: MustSchemaFromCols(NewColCollection(
|
|
NewColumn("pk", 0, types.StringKind, true))),
|
|
Diffable: false,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.Name, func(t *testing.T) {
|
|
d := ArePrimaryKeySetsDiffable(test.From, test.To)
|
|
require.Equal(t, test.Diffable, d)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testSchema(method string, sch Schema, t *testing.T) {
|
|
validateCols(t, allCols, sch.GetAllCols(), method+"GetAllCols")
|
|
validateCols(t, pkCols, sch.GetPKCols(), method+"GetPKCols")
|
|
validateCols(t, nonPkCols, sch.GetNonPKCols(), method+"GetNonPKCols")
|
|
|
|
extracted, err := ExtractAllColNames(sch)
|
|
assert.NoError(t, err)
|
|
|
|
expExt := map[uint64]string{
|
|
lnColTag: lnColName,
|
|
fnColTag: fnColName,
|
|
ageColTag: ageColName,
|
|
addrColTag: addrColName,
|
|
titleColTag: titleColName,
|
|
reservedColTag: reservedColName,
|
|
}
|
|
|
|
if !reflect.DeepEqual(extracted, expExt) {
|
|
t.Error("extracted columns did not match expectation")
|
|
}
|
|
|
|
if col, ok := ColFromName(sch, titleColName); !ok {
|
|
t.Error("Failed to get by name")
|
|
} else if col.Tag != titleColTag {
|
|
t.Error("Unexpected tag")
|
|
}
|
|
|
|
if col, ok := ColFromTag(sch, titleColTag); !ok {
|
|
t.Error("Failed to get by name")
|
|
} else if col.Name != titleColName {
|
|
t.Error("Unexpected tag")
|
|
}
|
|
}
|
|
|
|
func validateCols(t *testing.T, cols []Column, colColl *ColCollection, msg string) {
|
|
if !reflect.DeepEqual(cols, colColl.cols) {
|
|
t.Error()
|
|
}
|
|
}
|