Foreign Keys groundwork implementation

This commit is contained in:
Daylon Wilkins
2020-06-13 21:01:36 -07:00
committed by Daylon Wilkins
parent e4abb55c4b
commit b91178ce7a
35 changed files with 2262 additions and 208 deletions
File diff suppressed because it is too large Load Diff
+31
View File
@@ -458,6 +458,37 @@ SQL
! [[ "$output" =~ 'INDEX `idx_bud` (`v2`,`v1`)' ]] || false
}
@test "index: Disallow 'dolt_' name prefix" {
run dolt sql -q "CREATE INDEX dolt_idx_v1 ON onepk(v1)"
[ "$status" -eq "1" ]
run dolt sql -q "ALTER TABLE onepk ADD INDEX dolt_idx_v1 (v1)"
[ "$status" -eq "1" ]
}
@test "index: Disallow dropping 'dolt_' indexes" {
dolt sql <<SQL
CREATE TABLE parent (
id INT PRIMARY KEY
);
CREATE TABLE child (
id INT PRIMARY KEY,
parent_id INT,
FOREIGN KEY (parent_id)
REFERENCES parent(id)
);
SQL
run dolt index ls child
[ "$status" -eq "0" ]
[[ "$output" =~ "dolt_fk_1(parent_id) HIDDEN" ]] || false
run dolt sql -q "DROP INDEX dolt_fk_1 ON child"
[ "$status" -eq "1" ]
[[ "$output" =~ 'internal index' ]] || false
run dolt sql -q "ALTER TABLE child DROP INDEX dolt_fk_1"
[ "$status" -eq "1" ]
[[ "$output" =~ 'internal index' ]] || false
}
@test "index: DROP INDEX" {
dolt sql <<SQL
CREATE INDEX idx_v1 ON onepk(v1);
+1 -1
View File
@@ -487,7 +487,7 @@ func sqlSchemaDiff(ctx context.Context, td diff.TableDelta) errhand.VerboseError
if td.IsDrop() {
cli.Println(sqlfmt.DropTableStmt(td.FromName))
} else if td.IsAdd() {
cli.Println(sqlfmt.CreateTableStmtWithTags(td.ToName, toSch))
cli.Println(sqlfmt.CreateTableStmtWithTags(td.ToName, toSch, td.ToForeignKeys))
} else {
if td.FromName != td.ToName {
cli.Println(sqlfmt.RenameTableStmt(td.FromName, td.ToName))
+5 -1
View File
@@ -109,7 +109,11 @@ func (cmd LsCmd) Exec(ctx context.Context, commandStr string, args []string, dEn
output = append(output, fmt.Sprintf("%s:", tableName))
}
for _, index := range sch.Indexes().AllIndexes() {
output = append(output, fmt.Sprintf(" %s(%s)", index.Name(), strings.Join(index.ColumnNames(), ", ")))
if !index.IsHidden() {
output = append(output, fmt.Sprintf(" %s(%s)", index.Name(), strings.Join(index.ColumnNames(), ", ")))
} else {
output = append(output, fmt.Sprintf(" %s(%s) HIDDEN", index.Name(), strings.Join(index.ColumnNames(), ", ")))
}
}
}
}
+14 -2
View File
@@ -159,11 +159,23 @@ func exportTblSchema(ctx context.Context, tblName string, root *doltdb.RootValue
return errhand.BuildDError("error: failed to get schema for table %s", tblName).AddCause(err).Build()
}
fkc, err := root.GetForeignKeyCollection(ctx)
if err != nil {
return errhand.BuildDError("error: failed to read foreign key struct").AddCause(err).Build()
}
declaresFk, err := fkc.KeysForDisplay(ctx, tblName, root)
if err != nil {
return errhand.BuildDError("error: failed to assemble foreign key information").AddCause(err).Build()
}
var stmt string
if withTags {
stmt = sqlfmt.CreateTableStmtWithTags(tblName, sch)
stmt = sqlfmt.CreateTableStmtWithTags(tblName, sch, declaresFk)
} else {
stmt = sqlfmt.CreateTableStmt(tblName, sch)
stmt = sqlfmt.CreateTableStmt(tblName, sch, declaresFk)
}
_, err = fmt.Fprintln(wr, stmt)
+1 -1
View File
@@ -285,7 +285,7 @@ func importSchema(ctx context.Context, dEnv *env.DoltEnv, apr *argparser.ArgPars
}
tblName := impArgs.tableName
cli.Println(sqlfmt.CreateTableStmtWithTags(tblName, sch))
cli.Println(sqlfmt.CreateTableStmtWithTags(tblName, sch, nil))
if !apr.Contains(dryRunFlag) {
tbl, tblExists, err := root.GetTable(ctx, tblName)
+11 -3
View File
@@ -144,7 +144,15 @@ func printSchemas(ctx context.Context, apr *argparser.ArgParseResults, dEnv *env
if !ok {
notFound = append(notFound, tblName)
} else {
verr = printTblSchema(ctx, cmStr, tblName, tbl)
fkc, err := root.GetForeignKeyCollection(ctx)
if err != nil {
return errhand.BuildDError("error: failed to read foreign key struct").AddCause(err).Build()
}
declaresFk, err := fkc.KeysForDisplay(ctx, tblName, root)
if err != nil {
return errhand.BuildDError("error: failed to assemble foreign key information").AddCause(err).Build()
}
verr = printTblSchema(ctx, cmStr, tblName, tbl, declaresFk)
cli.Println()
}
}
@@ -157,7 +165,7 @@ func printSchemas(ctx context.Context, apr *argparser.ArgParseResults, dEnv *env
return verr
}
func printTblSchema(ctx context.Context, cmStr string, tblName string, tbl *doltdb.Table) errhand.VerboseError {
func printTblSchema(ctx context.Context, cmStr string, tblName string, tbl *doltdb.Table, foreignKeys []*doltdb.DisplayForeignKey) errhand.VerboseError {
cli.Println(bold.Sprint(tblName), "@", cmStr)
sch, err := tbl.GetSchema(ctx)
@@ -165,6 +173,6 @@ func printTblSchema(ctx context.Context, cmStr string, tblName string, tbl *dolt
return errhand.BuildDError("unable to get schema").AddCause(err).Build()
}
cli.Println(sqlfmt.CreateTableStmtWithTags(tblName, sch))
cli.Println(sqlfmt.CreateTableStmtWithTags(tblName, sch, foreignKeys))
return nil
}
+2 -2
View File
@@ -44,7 +44,7 @@ require (
github.com/kch42/buzhash v0.0.0-20160816060738-9bdec3dec7c6
github.com/kr/pretty v0.2.0 // indirect
github.com/liquidata-inc/dolt/go/gen/proto/dolt/services/eventsapi v0.0.0-20200320155049-a8e482faeffd
github.com/liquidata-inc/go-mysql-server v0.5.1-0.20200608182739-104505874162
github.com/liquidata-inc/go-mysql-server v0.5.1-0.20200612104507-c618c3006b3a
github.com/liquidata-inc/ishell v0.0.0-20190514193646-693241f1f2a0
github.com/liquidata-inc/mmap-go v1.0.3
github.com/liquidata-inc/sqllogictest/go v0.0.0-20200320151923-b11801f10e15
@@ -89,6 +89,6 @@ require (
replace github.com/liquidata-inc/dolt/go/gen/proto/dolt/services/eventsapi => ./gen/proto/dolt/services/eventsapi
replace vitess.io/vitess => github.com/liquidata-inc/vitess v0.0.0-20200430040751-192bb76ecd8b
replace vitess.io/vitess => github.com/liquidata-inc/vitess v0.0.0-20200604002149-44dae2034412
go 1.13
+4 -4
View File
@@ -392,10 +392,8 @@ github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/liquidata-inc/go-mysql-server v0.5.1-0.20200605170723-cd4f84b39578 h1:fDv4huMp1Ubh/qVkkStpKxE1LgEIaLLQIkN5NUwAMBE=
github.com/liquidata-inc/go-mysql-server v0.5.1-0.20200605170723-cd4f84b39578/go.mod h1:Qo0l83LdX5Z77p0tTLyJTrttZywFm0S+RYo6Shi97tw=
github.com/liquidata-inc/go-mysql-server v0.5.1-0.20200608182739-104505874162 h1:qwjGQ6+8zWiGya5qWlHqChJZ/b/cqjc87zBDBJG6f3k=
github.com/liquidata-inc/go-mysql-server v0.5.1-0.20200608182739-104505874162/go.mod h1:Qo0l83LdX5Z77p0tTLyJTrttZywFm0S+RYo6Shi97tw=
github.com/liquidata-inc/go-mysql-server v0.5.1-0.20200612104507-c618c3006b3a h1:5SpDyX3lyHneYJwXtBw8cuj8dIgBo5K8ReIEpDkO9sE=
github.com/liquidata-inc/go-mysql-server v0.5.1-0.20200612104507-c618c3006b3a/go.mod h1:KpDrkND/K8cIC0xAQx6EKsi5swStwzqJWdlQ2XRZetM=
github.com/liquidata-inc/ishell v0.0.0-20190514193646-693241f1f2a0 h1:phMgajKClMUiIr+hF2LGt8KRuUa2Vd2GI1sNgHgSXoU=
github.com/liquidata-inc/ishell v0.0.0-20190514193646-693241f1f2a0/go.mod h1:YC1rI9k5gx8D02ljlbxDfZe80s/iq8bGvaaQsvR+qxs=
github.com/liquidata-inc/mmap-go v1.0.3 h1:2LndAeAtup9rpvUmu4wZSYCsjCQ0Zpc+NqE+6+PnT7g=
@@ -404,6 +402,8 @@ github.com/liquidata-inc/sqllogictest/go v0.0.0-20200320151923-b11801f10e15 h1:H
github.com/liquidata-inc/sqllogictest/go v0.0.0-20200320151923-b11801f10e15/go.mod h1:kKRVtyuomkqz15YFRpS0OT8kpsU8y/F3jyiZtvALdKU=
github.com/liquidata-inc/vitess v0.0.0-20200430040751-192bb76ecd8b h1:bD5IABLonIOYGXmXnEtHgJgdZArEh7yi9mUdp4e30JM=
github.com/liquidata-inc/vitess v0.0.0-20200430040751-192bb76ecd8b/go.mod h1:vn/QvIl/1+N6+qjheejcLt8jmX2kQSQwFinzZuoY1VY=
github.com/liquidata-inc/vitess v0.0.0-20200604002149-44dae2034412 h1:zuc2t2NXPnVWVhuXGKNMS8RXO0NJVruQuoCB+M+OvAs=
github.com/liquidata-inc/vitess v0.0.0-20200604002149-44dae2034412/go.mod h1:zCpP+FJseQinxNM5gkVPahLzv7lt3YI0aSvrT5k95Qk=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
+21 -9
View File
@@ -263,10 +263,11 @@ func GetDocDiffs(ctx context.Context, dEnv *env.DoltEnv) (*DocDiffs, *DocDiffs,
}
type TableDelta struct {
FromName string
ToName string
FromTable *doltdb.Table
ToTable *doltdb.Table
FromName string
ToName string
FromTable *doltdb.Table
ToTable *doltdb.Table
ToForeignKeys []*doltdb.DisplayForeignKey // In the event that a table is an add, we'll display the FKs as well
}
// GetTableDeltas returns a list of TableDelta objects for each table that changed between fromRoot and toRoot.
@@ -312,14 +313,25 @@ func GetTableDeltas(ctx context.Context, fromRoot, toRoot *doltdb.RootValue) ([]
pkTag := sch.GetPKCols().GetColumns()[0].Tag
oldName, ok := fromTableNames[pkTag]
fkc, err := toRoot.GetForeignKeyCollection(ctx)
if err != nil {
return true, err
}
toDisplayFks, err := fkc.KeysForDisplay(ctx, name, toRoot)
if err != nil {
return true, err
}
if !ok {
deltas = append(deltas, TableDelta{ToName: name, ToTable: table})
deltas = append(deltas, TableDelta{ToName: name, ToTable: table, ToForeignKeys: toDisplayFks})
} else if oldName != name || fromTableHashes[pkTag] != th {
deltas = append(deltas, TableDelta{
ToName: name,
FromName: fromTableNames[pkTag],
ToTable: table,
FromTable: fromTable[pkTag],
ToName: name,
FromName: fromTableNames[pkTag],
ToTable: table,
FromTable: fromTable[pkTag],
ToForeignKeys: toDisplayFks,
})
}
+10 -1
View File
@@ -21,6 +21,7 @@ import (
"github.com/fatih/color"
"github.com/liquidata-inc/dolt/go/cmd/dolt/errhand"
"github.com/liquidata-inc/dolt/go/libraries/doltcore/doltdb"
"github.com/liquidata-inc/dolt/go/libraries/doltcore/row"
"github.com/liquidata-inc/dolt/go/libraries/doltcore/schema"
@@ -179,7 +180,15 @@ func PrintSqlTableDiffs(ctx context.Context, r1, r2 *doltdb.RootValue, wr io.Wri
if sch, err := tbl.GetSchema(ctx); err != nil {
return errors.New("error unable to get schema for table " + tblName)
} else {
stmt := sqlfmt.CreateTableStmtWithTags(tblName, sch)
fkc, err := r1.GetForeignKeyCollection(ctx)
if err != nil {
return errhand.BuildDError("error: failed to read foreign key struct").AddCause(err).Build()
}
declaresFk, err := fkc.KeysForDisplay(ctx, tblName, r1)
if err != nil {
return errhand.BuildDError("error: failed to assemble foreign key information").AddCause(err).Build()
}
stmt := sqlfmt.CreateTableStmtWithTags(tblName, sch, declaresFk)
if err = iohelp.WriteLine(wr, stmt); err != nil {
return err
}
+3 -3
View File
@@ -67,7 +67,7 @@ func TestSqlTableDiffAdd(t *testing.T) {
var stringWr StringBuilderCloser
_ = PrintSqlTableDiffs(ctx, newRoot, oldRoot, &stringWr)
expectedOutput := sqlfmt.CreateTableStmtWithTags("addTable", sch) + "\n"
expectedOutput := sqlfmt.CreateTableStmtWithTags("addTable", sch, nil) + "\n"
assert.Equal(t, expectedOutput, stringWr.String())
}
@@ -88,7 +88,7 @@ func TestSqlTableDiffAddThenInsert(t *testing.T) {
var stringWr StringBuilderCloser
_ = PrintSqlTableDiffs(ctx, newRoot, oldRoot, &stringWr)
expectedOutput := sqlfmt.CreateTableStmtWithTags("addTable", sch) + "\n"
expectedOutput := sqlfmt.CreateTableStmtWithTags("addTable", sch, nil) + "\n"
expectedOutput = expectedOutput +
"INSERT INTO `addTable` (`id`,`name`,`age`,`is_married`,`title`) " +
"VALUES ('00000000-0000-0000-0000-000000000000','Big Billy',77,FALSE,'Doctor');\n"
@@ -149,7 +149,7 @@ func TestSqlTableDiffRenameChangedTable(t *testing.T) {
_ = PrintSqlTableDiffs(ctx, newRoot, oldRoot, &stringWr)
expectedOutput := "DROP TABLE `renameTable`;\n"
expectedOutput = expectedOutput +
sqlfmt.CreateTableStmtWithTags("newTableName", sch) + "\n" +
sqlfmt.CreateTableStmtWithTags("newTableName", sch, nil) + "\n" +
"INSERT INTO `newTableName` (`id`,`name`,`age`,`is_married`,`title`) " +
"VALUES ('00000000-0000-0000-0000-000000000000','Big Billy',77,FALSE,'Doctor');\n"
assert.Equal(t, expectedOutput, stringWr.String())
+1 -1
View File
@@ -324,7 +324,7 @@ func (ddb *DoltDB) ReadRootValue(ctx context.Context, h hash.Hash) (*RootValue,
if val != nil {
if rootSt, ok := val.(types.Struct); ok && rootSt.Name() == ddbRootStructName {
return &RootValue{ddb.db, rootSt}, nil
return &RootValue{ddb.db, rootSt, nil}, nil
}
}
+2 -2
View File
@@ -53,9 +53,9 @@ func createTestSchema(t *testing.T) schema.Schema {
schema.NewColumn("empty", emptyTag, types.IntKind, false),
)
sch := schema.SchemaFromCols(colColl)
_, err := sch.Indexes().AddIndexByColTags(testSchemaIndexName, []uint64{firstTag, lastTag}, false, "")
_, err := sch.Indexes().AddIndexByColTags(testSchemaIndexName, []uint64{firstTag, lastTag}, schema.IndexProperties{IsUnique: false, IsHidden: false, Comment: ""})
require.NoError(t, err)
_, err = sch.Indexes().AddIndexByColTags(testSchemaIndexAge, []uint64{ageTag}, false, "")
_, err = sch.Indexes().AddIndexByColTags(testSchemaIndexAge, []uint64{ageTag}, schema.IndexProperties{IsUnique: false, IsHidden: false, Comment: ""})
require.NoError(t, err)
return sch
}
@@ -0,0 +1,324 @@
// Copyright 2020 Liquidata, 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 doltdb
import (
"context"
"fmt"
"sort"
"github.com/liquidata-inc/dolt/go/store/marshal"
"github.com/liquidata-inc/dolt/go/store/types"
)
type ForeignKeyCollection struct {
foreignKeys map[string]*ForeignKey
}
type ForeignKeyReferenceOption byte
const (
ForeignKeyReferenceOption_DefaultAction ForeignKeyReferenceOption = iota
ForeignKeyReferenceOption_Cascade
ForeignKeyReferenceOption_NoAction
ForeignKeyReferenceOption_Restrict
ForeignKeyReferenceOption_SetNull
)
// DisplayForeignKey is a representation of a Foreign Key that is meant for display, such as when displaying a schema.
type DisplayForeignKey struct {
Name string
TableName string
TableIndex string
TableColumns []string
ReferencedTableName string
ReferencedTableIndex string
ReferencedTableColumns []string
OnUpdate ForeignKeyReferenceOption
OnDelete ForeignKeyReferenceOption
}
// ForeignKey is the complete, internal representation of a Foreign Key.
type ForeignKey struct {
Name string `noms:"name" json:"name"`
TableName string `noms:"tbl_name" json:"tbl_name"`
TableIndex string `noms:"tbl_index" json:"tbl_index"`
TableColumns []uint64 `noms:"tbl_cols" json:"tbl_cols"`
ReferencedTableName string `noms:"ref_tbl_name" json:"ref_tbl_name"`
ReferencedTableIndex string `noms:"ref_tbl_index" json:"ref_tbl_index"`
ReferencedTableColumns []uint64 `noms:"ref_tbl_cols" json:"ref_tbl_cols"`
OnUpdate ForeignKeyReferenceOption `noms:"on_update" json:"on_update"`
OnDelete ForeignKeyReferenceOption `noms:"on_delete" json:"on_delete"`
}
// LoadForeignKeyCollection returns a new ForeignKeyCollection using the provided map returned previously by GetMap.
func LoadForeignKeyCollection(ctx context.Context, fkMap types.Map) (*ForeignKeyCollection, error) {
fkc := &ForeignKeyCollection{
foreignKeys: make(map[string]*ForeignKey),
}
err := fkMap.IterAll(ctx, func(_, value types.Value) error {
foreignKey := &ForeignKey{}
err := marshal.Unmarshal(ctx, fkMap.Format(), value, foreignKey)
if err != nil {
return err
}
fkc.foreignKeys[foreignKey.Name] = foreignKey
return nil
})
if err != nil {
return nil, err
}
return fkc, nil
}
// AddKey adds the given foreign key to the collection. Checks that the given name is unique in the collection.
// All other validation should occur before being added to the collection.
func (fkc *ForeignKeyCollection) AddKey(key *ForeignKey) error {
if key.Name == "" {
key.Name = fmt.Sprintf("fk_%s_%s_1", key.TableName, key.ReferencedTableName)
for i := 2; fkc.Contains(key.Name); i++ {
key.Name = fmt.Sprintf("fk_%s_%s_%d", key.TableName, key.ReferencedTableName, i)
}
} else if fkc.Contains(key.Name) {
return fmt.Errorf("a foreign key with the name `%s` already exists", key.Name)
}
fkc.foreignKeys[key.Name] = key
return nil
}
// AllKeys returns a slice, sorted by name ascending, containing all of the foreign keys in this collection.
func (fkc *ForeignKeyCollection) AllKeys() []*ForeignKey {
fks := make([]*ForeignKey, len(fkc.foreignKeys))
i := 0
for _, fk := range fkc.foreignKeys {
fks[i] = fk
i++
}
sort.Slice(fks, func(i, j int) bool {
return fks[i].Name < fks[j].Name
})
return fks
}
// Contains returns whether the given foreign key name already exists for this collection.
func (fkc *ForeignKeyCollection) Contains(foreignKeyName string) bool {
_, ok := fkc.foreignKeys[foreignKeyName]
return ok
}
// Count returns the number of indexes in this collection.
func (fkc *ForeignKeyCollection) Count() int {
return len(fkc.foreignKeys)
}
// KeysForDisplay returns display-ready foreign keys that the given table declares. The results are intended only
// for displaying key information to a user, and SHOULD NOT be used elsewhere. The results are sorted by name ascending.
func (fkc *ForeignKeyCollection) KeysForDisplay(ctx context.Context, tableName string, root *RootValue) ([]*DisplayForeignKey, error) {
var declaresFk []*DisplayForeignKey
for _, foreignKey := range fkc.foreignKeys {
if foreignKey.TableName == tableName {
tableColumns, err := fkc.columnTagsToNames(ctx, foreignKey.TableName, foreignKey.Name, foreignKey.TableColumns, root)
if err != nil {
return nil, err
}
refTableColumns, err := fkc.columnTagsToNames(ctx, foreignKey.ReferencedTableName, foreignKey.Name, foreignKey.ReferencedTableColumns, root)
if err != nil {
return nil, err
}
declaresFk = append(declaresFk, &DisplayForeignKey{
Name: foreignKey.Name,
TableName: foreignKey.TableName,
TableIndex: foreignKey.TableIndex,
TableColumns: tableColumns,
ReferencedTableName: foreignKey.ReferencedTableName,
ReferencedTableIndex: foreignKey.ReferencedTableIndex,
ReferencedTableColumns: refTableColumns,
OnUpdate: foreignKey.OnUpdate,
OnDelete: foreignKey.OnDelete,
})
}
}
sort.Slice(declaresFk, func(i, j int) bool {
return declaresFk[i].Name < declaresFk[j].Name
})
return declaresFk, nil
}
// Get returns the foreign key with the given name, or nil if it does not exist.
func (fkc *ForeignKeyCollection) Get(foreignKeyName string) *ForeignKey {
return fkc.foreignKeys[foreignKeyName]
}
// Map returns the collection as a Noms Map for persistence.
func (fkc *ForeignKeyCollection) Map(ctx context.Context, vrw types.ValueReadWriter) (types.Map, error) {
fkMap, err := types.NewMap(ctx, vrw)
if err != nil {
return types.EmptyMap, err
}
fkMapEditor := fkMap.Edit()
for _, foreignKey := range fkc.foreignKeys {
val, err := marshal.Marshal(ctx, vrw, *foreignKey)
if err != nil {
return types.EmptyMap, err
}
fkMapEditor.Set(types.String(foreignKey.Name), val)
}
return fkMapEditor.Map(ctx)
}
// KeysForTable returns all foreign keys that reference the given table in some capacity. The returned array
// declaredFk contains all foreign keys in which this table declared the foreign key. The array referencedByFk contains
// all foreign keys in which this table is the referenced table. If the table contains a self-referential foreign key,
// it will be present in both declaresFk and referencedByFk. Each array is sorted by name ascending.
func (fkc *ForeignKeyCollection) KeysForTable(tableName string) (declaredFk, referencedByFk []*ForeignKey) {
for _, foreignKey := range fkc.foreignKeys {
if foreignKey.TableName == tableName {
declaredFk = append(declaredFk, foreignKey)
}
if foreignKey.ReferencedTableName == tableName {
referencedByFk = append(referencedByFk, foreignKey)
}
}
sort.Slice(declaredFk, func(i, j int) bool {
return declaredFk[i].Name < declaredFk[j].Name
})
sort.Slice(referencedByFk, func(i, j int) bool {
return referencedByFk[i].Name < referencedByFk[j].Name
})
return
}
// RemoveKey removes a foreign key from the collection. It does not remove the associated indexes from their
// respective tables.
func (fkc *ForeignKeyCollection) RemoveKey(foreignKeyName string) (*ForeignKey, error) {
fk, ok := fkc.foreignKeys[foreignKeyName]
if !ok {
return nil, fmt.Errorf("`%s` does not exist as a foreign key", foreignKeyName)
}
delete(fkc.foreignKeys, foreignKeyName)
return fk, nil
}
// RemoveTables removes all foreign keys associated with the given tables, if permitted. The operation assumes that ALL
// tables to be removed are in a single call, as splitting tables into different calls may result in unintended errors.
func (fkc *ForeignKeyCollection) RemoveTables(ctx context.Context, root *RootValue, tables ...string) (*RootValue, error) {
tableSet := make(map[string]struct{})
for _, table := range tables {
tableSet[table] = struct{}{}
}
for _, foreignKey := range fkc.foreignKeys {
_, declaringTable := tableSet[foreignKey.TableName]
_, referenceTable := tableSet[foreignKey.ReferencedTableName]
if referenceTable && !declaringTable {
return nil, fmt.Errorf("unable to remove `%s` since it is referenced from table `%s`", foreignKey.ReferencedTableName, foreignKey.TableName)
}
if declaringTable {
if !referenceTable {
tbl, ok, err := root.GetTable(ctx, foreignKey.ReferencedTableName)
if err != nil {
return nil, err
}
if !ok {
return nil, fmt.Errorf("table `%s` not found, unable to remove foreign key `%s`", foreignKey.ReferencedTableName, foreignKey.Name)
}
sch, err := tbl.GetSchema(ctx)
if err != nil {
return nil, err
}
_, err = sch.Indexes().RemoveIndex(foreignKey.TableIndex)
if err != nil {
return nil, err
}
tbl, err = tbl.UpdateSchema(ctx, sch)
if err != nil {
return nil, err
}
root, err = root.PutTable(ctx, foreignKey.ReferencedTableName, tbl)
if err != nil {
return nil, err
}
}
delete(fkc.foreignKeys, foreignKey.Name)
}
}
return root.PutForeignKeyCollection(ctx, fkc)
}
// RenameTable updates all foreign key entries in the collection with the updated table name. Does not check for name
// collisions.
func (fkc *ForeignKeyCollection) RenameTable(oldTableName, newTableName string) {
for _, foreignKey := range fkc.foreignKeys {
if foreignKey.TableName == oldTableName {
foreignKey.TableName = newTableName
}
if foreignKey.ReferencedTableName == oldTableName {
foreignKey.ReferencedTableName = newTableName
}
}
}
// String returns the SQL reference option in uppercase.
func (refOp ForeignKeyReferenceOption) String() string {
switch refOp {
case ForeignKeyReferenceOption_DefaultAction:
return "NONE SPECIFIED"
case ForeignKeyReferenceOption_Cascade:
return "CASCADE"
case ForeignKeyReferenceOption_NoAction:
return "NO ACTION"
case ForeignKeyReferenceOption_Restrict:
return "RESTRICT"
case ForeignKeyReferenceOption_SetNull:
return "SET NULL"
default:
return "INVALID"
}
}
// columnTagsToNames loads all of the column names for the tags given from the root given.
func (fkc *ForeignKeyCollection) columnTagsToNames(ctx context.Context, tableName string, fkName string, colTags []uint64, root *RootValue) ([]string, error) {
tbl, ok, err := root.GetTable(ctx, tableName)
if err != nil {
return nil, err
}
if !ok {
return nil, fmt.Errorf("foreign key `%s` declares non-existent table `%s`", fkName, tableName)
}
tableSch, err := tbl.GetSchema(ctx)
if err != nil {
return nil, err
}
tableColumns := make([]string, len(colTags))
for i := range colTags {
col, ok := tableSch.GetAllCols().GetByTag(colTags[i])
if !ok {
return nil, fmt.Errorf("foreign key `%s` declares non-existent column with tag `%d`", fkName, colTags[i])
}
tableColumns[i] = col.Name
}
return tableColumns, nil
}
// copy returns an exact copy of the calling collection. As collections are meant to be modified in-place, this ensures
// that the original collection is not affected by any operations applied to the copied collection.
func (fkc *ForeignKeyCollection) copy() *ForeignKeyCollection {
copiedForeignKeys := make(map[string]*ForeignKey)
for _, key := range fkc.foreignKeys {
valueKey := *key // value types are copied, so this essentially copies all fields (the slices never change so it's okay)
copiedForeignKeys[valueKey.Name] = &valueKey
}
return &ForeignKeyCollection{copiedForeignKeys}
}
@@ -44,7 +44,7 @@ func TestIndexEditorConcurrency(t *testing.T) {
schema.NewColumn("v2", 2, types.IntKind, false))
require.NoError(t, err)
tableSch := schema.SchemaFromCols(colColl)
index, err := tableSch.Indexes().AddIndexByColNames("idx_concurrency", []string{"v1"}, false, "")
index, err := tableSch.Indexes().AddIndexByColNames("idx_concurrency", []string{"v1"}, schema.IndexProperties{IsUnique: false, IsHidden: false, Comment: ""})
require.NoError(t, err)
indexSch := index.Schema()
emptyMap, err := types.NewMap(context.Background(), db)
@@ -131,7 +131,7 @@ func TestIndexEditorConcurrencyPostInsert(t *testing.T) {
schema.NewColumn("v2", 2, types.IntKind, false))
require.NoError(t, err)
tableSch := schema.SchemaFromCols(colColl)
index, err := tableSch.Indexes().AddIndexByColNames("idx_concurrency", []string{"v1"}, false, "")
index, err := tableSch.Indexes().AddIndexByColNames("idx_concurrency", []string{"v1"}, schema.IndexProperties{IsUnique: false, IsHidden: false, Comment: ""})
require.NoError(t, err)
indexSch := index.Schema()
emptyMap, err := types.NewMap(context.Background(), db)
@@ -215,7 +215,7 @@ func TestIndexEditorConcurrencyUnique(t *testing.T) {
schema.NewColumn("v2", 2, types.IntKind, false))
require.NoError(t, err)
tableSch := schema.SchemaFromCols(colColl)
index, err := tableSch.Indexes().AddIndexByColNames("idx_concurrency", []string{"v1"}, true, "")
index, err := tableSch.Indexes().AddIndexByColNames("idx_concurrency", []string{"v1"}, schema.IndexProperties{IsUnique: true, IsHidden: false, Comment: ""})
require.NoError(t, err)
indexSch := index.Schema()
emptyMap, err := types.NewMap(context.Background(), db)
@@ -302,7 +302,7 @@ func TestIndexEditorWriteAfterFlush(t *testing.T) {
schema.NewColumn("v2", 2, types.IntKind, false))
require.NoError(t, err)
tableSch := schema.SchemaFromCols(colColl)
index, err := tableSch.Indexes().AddIndexByColNames("idx_concurrency", []string{"v1"}, false, "")
index, err := tableSch.Indexes().AddIndexByColNames("idx_concurrency", []string{"v1"}, schema.IndexProperties{IsUnique: false, IsHidden: false, Comment: ""})
require.NoError(t, err)
indexSch := index.Schema()
emptyMap, err := types.NewMap(context.Background(), db)
+87 -6
View File
@@ -34,12 +34,14 @@ const (
tablesKey = "tables"
superSchemasKey = "super_schemas"
foreignKeyKey = "foreign_key"
)
// RootValue defines the structure used inside all Liquidata noms dbs
type RootValue struct {
vrw types.ValueReadWriter
valueSt types.Struct
fkc *ForeignKeyCollection // cache the first load
}
type DocDetails struct {
@@ -49,7 +51,7 @@ type DocDetails struct {
File string
}
func NewRootValue(ctx context.Context, vrw types.ValueReadWriter, tables map[string]hash.Hash, ssMap types.Map) (*RootValue, error) {
func NewRootValue(ctx context.Context, vrw types.ValueReadWriter, tables map[string]hash.Hash, ssMap types.Map, fkMap types.Map) (*RootValue, error) {
values := make([]types.Value, 2*len(tables))
index := 0
@@ -80,11 +82,11 @@ func NewRootValue(ctx context.Context, vrw types.ValueReadWriter, tables map[str
return nil, err
}
return newRootFromMaps(vrw, tblMap, ssMap)
return newRootFromMaps(vrw, tblMap, ssMap, fkMap)
}
func newRootValue(vrw types.ValueReadWriter, st types.Struct) *RootValue {
return &RootValue{vrw, st}
return &RootValue{vrw, st, nil}
}
func emptyRootValue(ctx context.Context, vrw types.ValueReadWriter) (*RootValue, error) {
@@ -100,13 +102,20 @@ func emptyRootValue(ctx context.Context, vrw types.ValueReadWriter) (*RootValue,
return nil, err
}
return newRootFromMaps(vrw, m, mm)
mmm, err := types.NewMap(ctx, vrw)
if err != nil {
return nil, err
}
return newRootFromMaps(vrw, m, mm, mmm)
}
func newRootFromMaps(vrw types.ValueReadWriter, tblMap types.Map, ssMap types.Map) (*RootValue, error) {
func newRootFromMaps(vrw types.ValueReadWriter, tblMap types.Map, ssMap types.Map, fkMap types.Map) (*RootValue, error) {
sd := types.StructData{
tablesKey: tblMap,
superSchemasKey: ssMap,
foreignKeyKey: fkMap,
}
st, err := types.NewStruct(vrw.Format(), ddbRootStructName, sd)
@@ -982,6 +991,20 @@ func (root *RootValue) RenameTable(ctx context.Context, oldName, newName string)
return newRootValue(root.vrw, rootValSt), nil
}
foreignKeyCollection, err := root.GetForeignKeyCollection(ctx)
if err != nil {
return nil, err
}
foreignKeyCollection.RenameTable(oldName, newName)
fkMap, err := foreignKeyCollection.Map(ctx, root.vrw)
if err != nil {
return nil, err
}
rootValSt, err = rootValSt.Set(foreignKeyKey, fkMap)
if err != nil {
return nil, err
}
return newRootValue(root.vrw, rootValSt), nil
}
@@ -1017,7 +1040,15 @@ func (root *RootValue) RemoveTables(ctx context.Context, tables ...string) (*Roo
return nil, err
}
return newRootValue(root.vrw, rootValSt), nil
newRoot := newRootValue(root.vrw, rootValSt)
fkc, err := root.GetForeignKeyCollection(ctx)
if err != nil {
return nil, err
}
return fkc.RemoveTables(ctx, newRoot, tables...)
}
// DocDiff returns the added, modified and removed docs when comparing a root value with an other (newer) value. If the other value,
@@ -1068,6 +1099,56 @@ func (root *RootValue) DocDiff(ctx context.Context, other *RootValue, docDetails
return a, m, r, nil
}
// GetForeignKeyCollection returns the ForeignKeyCollection for this root. As collections are meant to be modified
// in-place, each returned collection may freely be altered without affecting future returned collections from this root.
func (root *RootValue) GetForeignKeyCollection(ctx context.Context) (*ForeignKeyCollection, error) {
if root.fkc == nil {
fkMap, err := root.GetForeignKeyCollectionMap(ctx)
if err != nil {
return nil, err
}
root.fkc, err = LoadForeignKeyCollection(ctx, fkMap)
if err != nil {
return nil, err
}
}
return root.fkc.copy(), nil
}
// GetForeignKeyCollectionMap returns the persisted noms Map of the foreign key collection on this root. If the intent
// is to retreive a ForeignKeyCollection in particular, it is advised to call GetForeignKeyCollection as it caches the
// result for performance.
func (root *RootValue) GetForeignKeyCollectionMap(ctx context.Context) (types.Map, error) {
v, found, err := root.valueSt.MaybeGet(foreignKeyKey)
if err != nil {
return types.EmptyMap, err
}
var fkMap types.Map
if found {
fkMap = v.(types.Map)
} else {
fkMap, err = types.NewMap(ctx, root.vrw)
if err != nil {
return types.EmptyMap, err
}
}
return fkMap, nil
}
// PutForeignKeyCollection returns a new root with the given foreign key collection.
func (root *RootValue) PutForeignKeyCollection(ctx context.Context, fkc *ForeignKeyCollection) (*RootValue, error) {
fkMap, err := fkc.Map(ctx, root.vrw)
if err != nil {
return nil, err
}
rootValSt, err := root.valueSt.Set(foreignKeyKey, fkMap)
if err != nil {
return nil, err
}
return &RootValue{root.vrw, rootValSt, fkc.copy()}, nil
}
func getDocDetailsBtwnRoots(ctx context.Context, newTbl *Table, newSch schema.Schema, newTblFound bool, oldTbl *Table, oldSch schema.Schema, oldTblFound bool) ([]DocDetails, error) {
var docDetailsBtwnRoots []DocDetails
if newTblFound {
+4 -4
View File
@@ -379,7 +379,7 @@ func TestIndexRebuildingUniqueSuccessOneCol(t *testing.T) {
originalTable, err := createTableWithoutIndexRebuilding(context.Background(), db, schVal, rowData)
require.NoError(t, err)
index, err := sch.Indexes().AddIndexByColTags("idx_v1", []uint64{2}, true, "")
index, err := sch.Indexes().AddIndexByColTags("idx_v1", []uint64{2}, schema.IndexProperties{IsUnique: true, IsHidden: false, Comment: ""})
require.NoError(t, err)
updatedTable, err := originalTable.UpdateSchema(context.Background(), sch)
require.NoError(t, err)
@@ -409,7 +409,7 @@ func TestIndexRebuildingUniqueSuccessTwoCol(t *testing.T) {
originalTable, err := createTableWithoutIndexRebuilding(context.Background(), db, schVal, rowData)
require.NoError(t, err)
index, err := sch.Indexes().AddIndexByColTags("idx_v1", []uint64{2, 3}, true, "")
index, err := sch.Indexes().AddIndexByColTags("idx_v1", []uint64{2, 3}, schema.IndexProperties{IsUnique: true, IsHidden: false, Comment: ""})
require.NoError(t, err)
updatedTable, err := originalTable.UpdateSchema(context.Background(), sch)
require.NoError(t, err)
@@ -439,7 +439,7 @@ func TestIndexRebuildingUniqueFailOneCol(t *testing.T) {
originalTable, err := createTableWithoutIndexRebuilding(context.Background(), db, schVal, rowData)
require.NoError(t, err)
index, err := sch.Indexes().AddIndexByColTags("idx_v1", []uint64{2}, true, "")
index, err := sch.Indexes().AddIndexByColTags("idx_v1", []uint64{2}, schema.IndexProperties{IsUnique: true, IsHidden: false, Comment: ""})
require.NoError(t, err)
updatedTable, err := originalTable.UpdateSchema(context.Background(), sch)
require.NoError(t, err)
@@ -470,7 +470,7 @@ func TestIndexRebuildingUniqueFailTwoCol(t *testing.T) {
originalTable, err := createTableWithoutIndexRebuilding(context.Background(), db, schVal, rowData)
require.NoError(t, err)
index, err := sch.Indexes().AddIndexByColTags("idx_v1", []uint64{2, 3}, true, "")
index, err := sch.Indexes().AddIndexByColTags("idx_v1", []uint64{2, 3}, schema.IndexProperties{IsUnique: true, IsHidden: false, Comment: ""})
require.NoError(t, err)
updatedTable, err := originalTable.UpdateSchema(context.Background(), sch)
require.NoError(t, err)
+2 -2
View File
@@ -98,11 +98,11 @@ func init() {
UntypedRows = append(UntypedRows, r)
}
_, err := TypedSchema.Indexes().AddIndexByColTags(IndexName, []uint64{NameTag}, false, "")
_, err := TypedSchema.Indexes().AddIndexByColTags(IndexName, []uint64{NameTag}, schema.IndexProperties{IsUnique: false, IsHidden: false, Comment: ""})
if err != nil {
panic(err)
}
_, err = UntypedSchema.Indexes().AddIndexByColTags(IndexName, []uint64{NameTag}, false, "")
_, err = UntypedSchema.Indexes().AddIndexByColTags(IndexName, []uint64{NameTag}, schema.IndexProperties{IsUnique: false, IsHidden: false, Comment: ""})
if err != nil {
panic(err)
}
+10 -4
View File
@@ -216,6 +216,12 @@ func CheckoutBranch(ctx context.Context, dEnv *env.DoltEnv, brName string) error
return err
}
fkMap, err := newRoot.GetForeignKeyCollectionMap(ctx)
if err != nil {
return err
}
conflicts := set.NewStrSet([]string{})
wrkTblHashes, err := tblHashesForCO(ctx, currRoots[HeadRoot], newRoot, currRoots[WorkingRoot], conflicts)
@@ -233,13 +239,13 @@ func CheckoutBranch(ctx context.Context, dEnv *env.DoltEnv, brName string) error
return CheckoutWouldOverwrite{conflicts.AsSlice()}
}
wrkHash, err := writeRoot(ctx, dEnv, wrkTblHashes, ssMap)
wrkHash, err := writeRoot(ctx, dEnv, wrkTblHashes, ssMap, fkMap)
if err != nil {
return err
}
stgHash, err := writeRoot(ctx, dEnv, stgTblHashes, ssMap)
stgHash, err := writeRoot(ctx, dEnv, stgTblHashes, ssMap, fkMap)
if err != nil {
return err
@@ -334,14 +340,14 @@ func tblHashesForCO(ctx context.Context, oldRoot, newRoot, changedRoot *doltdb.R
return resultMap, nil
}
func writeRoot(ctx context.Context, dEnv *env.DoltEnv, tblHashes map[string]hash.Hash, ssMap types.Map) (hash.Hash, error) {
func writeRoot(ctx context.Context, dEnv *env.DoltEnv, tblHashes map[string]hash.Hash, ssMap types.Map, fkMap types.Map) (hash.Hash, error) {
for k, v := range tblHashes {
if v == emptyHash {
delete(tblHashes, k)
}
}
root, err := doltdb.NewRootValue(ctx, dEnv.DoltDB.ValueReadWriter(), tblHashes, ssMap)
root, err := doltdb.NewRootValue(ctx, dEnv.DoltDB.ValueReadWriter(), tblHashes, ssMap, fkMap)
if err != nil {
if err == doltdb.ErrHashNotFound {
return emptyHash, errors.New("corrupted database? Can't find hash of current table")
+1 -1
View File
@@ -258,7 +258,7 @@ func init() {
keyTuples[i] = mustTuple(types.NewTuple(types.Format_7_18, keyTag, id))
}
index, _ = sch.Indexes().AddIndexByColTags("idx_name", []uint64{nameTag}, false, "")
index, _ = sch.Indexes().AddIndexByColTags("idx_name", []uint64{nameTag}, schema.IndexProperties{IsUnique: false, IsHidden: false, Comment: ""})
}
func setupMergeTest() (types.ValueReadWriter, *doltdb.Commit, *doltdb.Commit, types.Map, types.Map) {
+11 -1
View File
@@ -21,6 +21,8 @@ import (
"os"
"strings"
"github.com/liquidata-inc/dolt/go/cmd/dolt/errhand"
"github.com/liquidata-inc/dolt/go/libraries/doltcore/doltdb"
"github.com/liquidata-inc/dolt/go/libraries/doltcore/schema"
"github.com/liquidata-inc/dolt/go/libraries/doltcore/table"
@@ -154,7 +156,15 @@ func (dl FileDataLocation) NewCreatingWriter(ctx context.Context, mvOpts DataMov
case JsonFile:
return json.OpenJSONWriter(dl.Path, fs, outSch)
case SqlFile:
return sqlexport.OpenSQLExportWriter(dl.Path, mvOpts.SrcName(), fs, outSch)
fkc, err := root.GetForeignKeyCollection(ctx)
if err != nil {
return nil, errhand.BuildDError("error: failed to read foreign key struct").AddCause(err).Build()
}
declaredFks, err := fkc.KeysForDisplay(ctx, mvOpts.SrcName(), root)
if err != nil {
return nil, errhand.BuildDError("error: failed to assemble foreign key information").AddCause(err).Build()
}
return sqlexport.OpenSQLExportWriter(dl.Path, mvOpts.SrcName(), fs, outSch, declaredFks)
}
panic("Invalid Data Format." + string(dl.Format))
+1 -1
View File
@@ -363,7 +363,7 @@ func replayCommitWithNewTag(ctx context.Context, root, parentRoot, rebasedParent
rebasedSch := schema.SchemaFromCols(schCC)
for _, index := range sch.Indexes().AllIndexes() {
_, err = rebasedSch.Indexes().AddIndexByColNames(index.Name(), index.ColumnNames(), index.IsUnique(), index.Comment())
_, err = rebasedSch.Indexes().AddIndexByColNames(index.Name(), index.ColumnNames(), schema.IndexProperties{IsUnique: index.IsUnique(), IsHidden: index.IsHidden(), Comment: index.Comment()})
if err != nil {
return nil, err
}
+1 -1
View File
@@ -66,7 +66,7 @@ var sch, _ = schema.SchemaFromPKAndNonPKCols(testKeyColColl, testNonKeyColColl)
var index schema.Index
func init() {
index, _ = sch.Indexes().AddIndexByColTags(indexName, []uint64{ageColTag}, false, "")
index, _ = sch.Indexes().AddIndexByColTags(indexName, []uint64{ageColTag}, schema.IndexProperties{IsUnique: false, IsHidden: false, Comment: ""})
}
func newTestRow() (Row, error) {
@@ -135,6 +135,7 @@ type encodedIndex struct {
Tags []uint64 `noms:"tags" json:"tags"`
Comment string `noms:"comment" json:"comment"`
Unique bool `noms:"unique" json:"unique"`
Hidden bool `noms:"hidden,omitempty" json:"hidden,omitempty"`
}
type schemaData struct {
@@ -165,6 +166,7 @@ func toSchemaData(sch schema.Schema) (schemaData, error) {
Tags: index.IndexedColumnTags(),
Comment: index.Comment(),
Unique: index.IsUnique(),
Hidden: index.IsHidden(),
}
}
@@ -191,7 +193,7 @@ func (sd schemaData) decodeSchema() (schema.Schema, error) {
sch := schema.SchemaFromCols(colColl)
for _, encodedIndex := range sd.IndexCollection {
_, err = sch.Indexes().AddIndexByColTags(encodedIndex.Name, encodedIndex.Tags, encodedIndex.Unique, encodedIndex.Comment)
_, err = sch.Indexes().AddIndexByColTags(encodedIndex.Name, encodedIndex.Tags, schema.IndexProperties{IsUnique: encodedIndex.Unique, IsHidden: encodedIndex.Hidden, Comment: encodedIndex.Comment})
if err != nil {
return nil, err
}
@@ -43,7 +43,7 @@ func createTestSchema() schema.Schema {
colColl, _ := schema.NewColCollection(columns...)
sch := schema.SchemaFromCols(colColl)
_, _ = sch.Indexes().AddIndexByColTags("idx_age", []uint64{3}, false, "")
_, _ = sch.Indexes().AddIndexByColTags("idx_age", []uint64{3}, schema.IndexProperties{IsUnique: false, IsHidden: false, Comment: ""})
return sch
}
@@ -231,6 +231,7 @@ type testEncodedIndex struct {
Tags []uint64 `noms:"tags" json:"tags"`
Comment string `noms:"comment" json:"comment"`
Unique bool `noms:"unique" json:"unique"`
Hidden bool `noms:"hidden,omitempty" json:"hidden,omitempty"`
}
type testSchemaData struct {
@@ -276,7 +277,7 @@ func (tsd testSchemaData) decodeSchema() (schema.Schema, error) {
sch := schema.SchemaFromCols(colColl)
for _, encodedIndex := range tsd.IndexCollection {
_, err = sch.Indexes().AddIndexByColTags(encodedIndex.Name, encodedIndex.Tags, encodedIndex.Unique, encodedIndex.Comment)
_, err = sch.Indexes().AddIndexByColTags(encodedIndex.Name, encodedIndex.Tags, schema.IndexProperties{IsUnique: encodedIndex.Unique, IsHidden: encodedIndex.Hidden, Comment: encodedIndex.Comment})
if err != nil {
return nil, err
}
+9
View File
@@ -28,6 +28,9 @@ type Index interface {
GetColumn(tag uint64) (Column, bool)
// IndexedColumnTags returns the tags of the columns in the index.
IndexedColumnTags() []uint64
// IsHidden returns whether the index is hidden and managed internally, such as for a foreign key. Such indexes do
// not cause column nor tag collisions with other indexes.
IsHidden() bool
// IsUnique returns whether the index enforces the UNIQUE constraint.
IsUnique() bool
// Name returns the name of the index.
@@ -45,6 +48,7 @@ type indexImpl struct {
tags []uint64
allTags []uint64
indexColl *indexCollectionImpl
isHidden bool
isUnique bool
comment string
}
@@ -77,6 +81,10 @@ func (ix *indexImpl) IndexedColumnTags() []uint64 {
return ix.tags
}
func (ix *indexImpl) IsHidden() bool {
return ix.isHidden
}
func (ix *indexImpl) IsUnique() bool {
return ix.isUnique
}
@@ -122,6 +130,7 @@ func (ix *indexImpl) copy() *indexImpl {
tags: tags,
allTags: allTags,
indexColl: ix.indexColl,
isHidden: ix.isHidden,
isUnique: ix.isUnique,
comment: ix.comment,
}
+27 -19
View File
@@ -17,6 +17,7 @@ package schema
import (
"fmt"
"sort"
"strings"
)
type IndexCollection interface {
@@ -24,9 +25,9 @@ type IndexCollection interface {
// It does not perform any kind of checking, and is intended for schema modifications.
AddIndex(indexes ...Index)
// AddIndexByColNames adds an index with the given name and columns (in index order).
AddIndexByColNames(indexName string, cols []string, isUnique bool, comment string) (Index, error)
AddIndexByColNames(indexName string, cols []string, props IndexProperties) (Index, error)
// AddIndexByColTags adds an index with the given name and column tags (in index order).
AddIndexByColTags(indexName string, tags []uint64, isUnique bool, comment string) (Index, error)
AddIndexByColTags(indexName string, tags []uint64, props IndexProperties) (Index, error)
// AllIndexes returns a slice containing all of the indexes in this collection.
AllIndexes() []Index
// Contains returns whether the given index name already exists for this table.
@@ -35,11 +36,11 @@ type IndexCollection interface {
Count() int
// Get returns the index with the given name, or nil if it does not exist.
Get(indexName string) Index
// HasIndexes returns whether this collection has any indexes.
HasIndexes() bool
// HasIndexOnColumns returns whether the collection contains an index that has this exact collection and ordering of columns.
// Any hidden indexes are ignored.
HasIndexOnColumns(cols ...string) bool
// HasIndexOnTags returns whether the collection contains an index that has this exact collection and ordering of columns.
// Any hidden indexes are ignored.
HasIndexOnTags(tags ...uint64) bool
// IndexesWithColumn returns all indexes that index the given column.
IndexesWithColumn(columnName string) []Index
@@ -56,6 +57,12 @@ type IndexCollection interface {
RenameIndex(oldName, newName string) (Index, error)
}
type IndexProperties struct {
IsUnique bool
IsHidden bool
Comment string
}
type indexCollectionImpl struct {
colColl *ColCollection
indexes map[string]*indexImpl
@@ -104,31 +111,39 @@ func (ixc *indexCollectionImpl) AddIndex(indexes ...Index) {
}
}
func (ixc *indexCollectionImpl) AddIndexByColNames(indexName string, cols []string, isUnique bool, comment string) (Index, error) {
func (ixc *indexCollectionImpl) AddIndexByColNames(indexName string, cols []string, props IndexProperties) (Index, error) {
tags, ok := ixc.columnNamesToTags(cols)
if !ok {
return nil, fmt.Errorf("the table does not contain at least one of the following columns: `%v`", cols)
}
return ixc.AddIndexByColTags(indexName, tags, isUnique, comment)
return ixc.AddIndexByColTags(indexName, tags, props)
}
func (ixc *indexCollectionImpl) AddIndexByColTags(indexName string, tags []uint64, isUnique bool, comment string) (Index, error) {
func (ixc *indexCollectionImpl) AddIndexByColTags(indexName string, tags []uint64, props IndexProperties) (Index, error) {
if props.IsHidden && !strings.HasPrefix(indexName, "dolt_") {
return nil, fmt.Errorf("hidden indexes must be prefixed with `dolt_`")
} else if !props.IsHidden && strings.HasPrefix(indexName, "dolt_") {
return nil, fmt.Errorf("indexes cannot be prefixed with `dolt_`")
}
if ixc.Contains(indexName) {
return nil, fmt.Errorf("`%s` already exists as an index for this table", indexName)
}
if !ixc.tagsExist(tags...) {
return nil, fmt.Errorf("tags %v do not exist on this table", tags)
}
if ixc.HasIndexOnTags(tags...) {
return nil, fmt.Errorf("cannot create a duplicate index on this table")
if !props.IsHidden {
if ixc.HasIndexOnTags(tags...) {
return nil, fmt.Errorf("cannot create a duplicate index on this table")
}
}
index := &indexImpl{
indexColl: ixc,
name: indexName,
tags: tags,
allTags: combineAllTags(tags, ixc.pks),
isUnique: isUnique,
comment: comment,
isHidden: props.IsHidden,
isUnique: props.IsUnique,
comment: props.Comment,
}
ixc.indexes[indexName] = index
for _, tag := range tags {
@@ -167,13 +182,6 @@ func (ixc *indexCollectionImpl) Get(indexName string) Index {
return nil
}
func (ixc *indexCollectionImpl) HasIndexes() bool {
if len(ixc.indexes) > 0 {
return true
}
return false
}
func (ixc *indexCollectionImpl) HasIndexOnColumns(cols ...string) bool {
tags := make([]uint64, len(cols))
for i, col := range cols {
@@ -188,7 +196,7 @@ func (ixc *indexCollectionImpl) HasIndexOnColumns(cols ...string) bool {
func (ixc *indexCollectionImpl) HasIndexOnTags(tags ...uint64) bool {
idx := ixc.containsColumnTagCollection(tags...)
if idx == nil {
if idx == nil || idx.isHidden {
return false
}
return true
+14 -50
View File
@@ -198,7 +198,7 @@ func TestIndexCollectionAddIndexByColNames(t *testing.T) {
assert.False(t, indexColl.HasIndexOnTags(testIndex.index.IndexedColumnTags()...))
assert.Nil(t, indexColl.Get(testIndex.index.Name()))
resIndex, err := indexColl.AddIndexByColNames(testIndex.index.Name(), testIndex.cols, testIndex.index.IsUnique(), testIndex.index.Comment())
resIndex, err := indexColl.AddIndexByColNames(testIndex.index.Name(), testIndex.cols, IndexProperties{IsUnique: testIndex.index.IsUnique(), IsHidden: false, Comment: testIndex.index.Comment()})
assert.NoError(t, err)
assert.Equal(t, testIndex.index, resIndex)
assert.Equal(t, testIndex.index, indexColl.Get(resIndex.Name()))
@@ -218,20 +218,20 @@ func TestIndexCollectionAddIndexByColNames(t *testing.T) {
t.Run("Pre-existing", func(t *testing.T) {
for _, testIndex := range testIndexes {
_, err := indexColl.AddIndexByColNames(testIndex.index.Name(), testIndex.cols, testIndex.index.IsUnique(), testIndex.index.Comment())
_, err := indexColl.AddIndexByColNames(testIndex.index.Name(), testIndex.cols, IndexProperties{IsUnique: testIndex.index.IsUnique(), IsHidden: false, Comment: testIndex.index.Comment()})
assert.NoError(t, err)
_, err = indexColl.AddIndexByColNames("nonsense", testIndex.cols, testIndex.index.IsUnique(), testIndex.index.Comment())
_, err = indexColl.AddIndexByColNames("nonsense", testIndex.cols, IndexProperties{IsUnique: testIndex.index.IsUnique(), IsHidden: false, Comment: testIndex.index.Comment()})
assert.Error(t, err)
_, err = indexColl.AddIndexByColNames(testIndex.index.Name(), []string{"v2"}, testIndex.index.IsUnique(), testIndex.index.Comment())
_, err = indexColl.AddIndexByColNames(testIndex.index.Name(), []string{"v2"}, IndexProperties{IsUnique: testIndex.index.IsUnique(), IsHidden: false, Comment: testIndex.index.Comment()})
assert.Error(t, err)
}
indexColl.clear(t)
})
t.Run("Non-existing Columns", func(t *testing.T) {
_, err := indexColl.AddIndexByColNames("nonsense", []string{"v4"}, false, "")
_, err := indexColl.AddIndexByColNames("nonsense", []string{"v4"}, IndexProperties{IsUnique: false, IsHidden: false, Comment: ""})
assert.Error(t, err)
_, err = indexColl.AddIndexByColNames("nonsense", []string{"v1", "v2", "pk3"}, false, "")
_, err = indexColl.AddIndexByColNames("nonsense", []string{"v1", "v2", "pk3"}, IndexProperties{IsUnique: false, IsHidden: false, Comment: ""})
assert.Error(t, err)
})
}
@@ -282,7 +282,7 @@ func TestIndexCollectionAddIndexByColTags(t *testing.T) {
assert.False(t, indexColl.HasIndexOnTags(testIndex.IndexedColumnTags()...))
assert.Nil(t, indexColl.Get(testIndex.Name()))
resIndex, err := indexColl.AddIndexByColTags(testIndex.Name(), testIndex.tags, testIndex.IsUnique(), testIndex.Comment())
resIndex, err := indexColl.AddIndexByColTags(testIndex.Name(), testIndex.tags, IndexProperties{IsUnique: testIndex.IsUnique(), IsHidden: false, Comment: testIndex.Comment()})
assert.NoError(t, err)
assert.Equal(t, testIndex, resIndex)
assert.Equal(t, testIndex, indexColl.Get(resIndex.Name()))
@@ -302,20 +302,20 @@ func TestIndexCollectionAddIndexByColTags(t *testing.T) {
t.Run("Pre-existing", func(t *testing.T) {
for _, testIndex := range testIndexes {
_, err := indexColl.AddIndexByColTags(testIndex.Name(), testIndex.tags, testIndex.IsUnique(), testIndex.Comment())
_, err := indexColl.AddIndexByColTags(testIndex.Name(), testIndex.tags, IndexProperties{IsUnique: testIndex.IsUnique(), IsHidden: false, Comment: testIndex.Comment()})
assert.NoError(t, err)
_, err = indexColl.AddIndexByColTags("nonsense", testIndex.tags, testIndex.IsUnique(), testIndex.Comment())
_, err = indexColl.AddIndexByColTags("nonsense", testIndex.tags, IndexProperties{IsUnique: testIndex.IsUnique(), IsHidden: false, Comment: testIndex.Comment()})
assert.Error(t, err)
_, err = indexColl.AddIndexByColTags(testIndex.Name(), []uint64{4}, testIndex.IsUnique(), testIndex.Comment())
_, err = indexColl.AddIndexByColTags(testIndex.Name(), []uint64{4}, IndexProperties{IsUnique: testIndex.IsUnique(), IsHidden: false, Comment: testIndex.Comment()})
assert.Error(t, err)
}
indexColl.clear(t)
})
t.Run("Non-existing Tags", func(t *testing.T) {
_, err := indexColl.AddIndexByColTags("nonsense", []uint64{6}, false, "")
_, err := indexColl.AddIndexByColTags("nonsense", []uint64{6}, IndexProperties{IsUnique: false, IsHidden: false, Comment: ""})
assert.Error(t, err)
_, err = indexColl.AddIndexByColTags("nonsense", []uint64{3, 4, 10}, false, "")
_, err = indexColl.AddIndexByColTags("nonsense", []uint64{3, 4, 10}, IndexProperties{IsUnique: false, IsHidden: false, Comment: ""})
assert.Error(t, err)
})
}
@@ -335,9 +335,9 @@ func TestIndexCollectionAllIndexes(t *testing.T) {
name: "idx_z",
tags: []uint64{3},
})
_, err = indexColl.AddIndexByColNames("idx_a", []string{"v2"}, false, "")
_, err = indexColl.AddIndexByColNames("idx_a", []string{"v2"}, IndexProperties{IsUnique: false, IsHidden: false, Comment: ""})
require.NoError(t, err)
_, err = indexColl.AddIndexByColTags("idx_n", []uint64{5}, false, "hello there")
_, err = indexColl.AddIndexByColTags("idx_n", []uint64{5}, IndexProperties{IsUnique: false, IsHidden: false, Comment: "hello there"})
require.NoError(t, err)
assert.Equal(t, []Index{
@@ -368,42 +368,6 @@ func TestIndexCollectionAllIndexes(t *testing.T) {
}, indexColl.AllIndexes())
}
func TestIndexCollectionHasIndexes(t *testing.T) {
colColl, err := NewColCollection(
NewColumn("pk1", 1, types.IntKind, true, NotNullConstraint{}),
NewColumn("pk2", 2, types.IntKind, true, NotNullConstraint{}),
NewColumn("v1", 3, types.IntKind, false),
NewColumn("v2", 4, types.UintKind, false),
NewColumn("v3", 5, types.StringKind, false),
)
require.NoError(t, err)
indexColl := NewIndexCollection(colColl).(*indexCollectionImpl)
assert.False(t, indexColl.HasIndexes())
indexColl.AddIndex(&indexImpl{
name: "idx_z",
tags: []uint64{3},
})
assert.True(t, indexColl.HasIndexes())
_, err = indexColl.RemoveIndex("idx_z")
require.NoError(t, err)
assert.False(t, indexColl.HasIndexes())
_, err = indexColl.AddIndexByColNames("idx_a", []string{"v2"}, false, "")
require.NoError(t, err)
assert.True(t, indexColl.HasIndexes())
_, err = indexColl.RemoveIndex("idx_a")
require.NoError(t, err)
assert.False(t, indexColl.HasIndexes())
_, err = indexColl.AddIndexByColTags("idx_n", []uint64{5}, false, "hello there")
require.NoError(t, err)
assert.True(t, indexColl.HasIndexes())
}
func TestIndexCollectionRemoveIndex(t *testing.T) {
colColl, err := NewColCollection(
NewColumn("pk1", 1, types.IntKind, true, NotNullConstraint{}),
@@ -56,7 +56,7 @@ type test struct {
func TestSchemaAsCreateStmt(t *testing.T) {
tSchema := sqltestutil.PeopleTestSchema
stmt := CreateTableStmtWithTags("table_name", tSchema)
stmt := CreateTableStmtWithTags("table_name", tSchema, nil)
assert.Equal(t, expectedCreateSQL, stmt)
}
@@ -18,6 +18,8 @@ import (
"fmt"
"strings"
"github.com/liquidata-inc/dolt/go/libraries/doltcore/doltdb"
"github.com/liquidata-inc/dolt/go/libraries/doltcore/schema"
)
@@ -68,22 +70,22 @@ func FmtColTagComment(tag uint64) string {
}
// CreateTableStmtWithTags generates a SQL CREATE TABLE command
func CreateTableStmt(tableName string, sch schema.Schema) string {
func CreateTableStmt(tableName string, sch schema.Schema, foreignKeys []*doltdb.DisplayForeignKey) string {
return createTableStmt(tableName, sch, func(col schema.Column) string {
return FmtCol(2, 0, 0, col)
})
}, foreignKeys)
}
// CreateTableStmtWithTags generates a SQL CREATE TABLE command that includes the column tags as comments
func CreateTableStmtWithTags(tableName string, sch schema.Schema) string {
func CreateTableStmtWithTags(tableName string, sch schema.Schema, foreignKeys []*doltdb.DisplayForeignKey) string {
return createTableStmt(tableName, sch, func(col schema.Column) string {
return FmtColWithTag(2, 0, 0, col)
})
}, foreignKeys)
}
type fmtColFunc func(col schema.Column) string
func createTableStmt(tableName string, sch schema.Schema, fmtCol fmtColFunc) string {
func createTableStmt(tableName string, sch schema.Schema, fmtCol fmtColFunc, foreignKeys []*doltdb.DisplayForeignKey) string {
sb := &strings.Builder{}
sb.WriteString(fmt.Sprintf("CREATE TABLE %s (\n", QuoteIdentifier(tableName)))
@@ -117,6 +119,9 @@ func createTableStmt(tableName string, sch schema.Schema, fmtCol fmtColFunc) str
sb.WriteRune(')')
for _, index := range sch.Indexes().AllIndexes() {
if index.IsHidden() {
continue
}
sb.WriteString(",\n ")
if index.IsUnique() {
sb.WriteString("UNIQUE ")
@@ -137,6 +142,36 @@ func createTableStmt(tableName string, sch schema.Schema, fmtCol fmtColFunc) str
}
}
for _, foreignKey := range foreignKeys {
sb.WriteString(",\n CONSTRAINT ")
sb.WriteString(QuoteIdentifier(foreignKey.Name))
sb.WriteString(" FOREIGN KEY (")
for i, fkColName := range foreignKey.TableColumns {
if i != 0 {
sb.WriteRune(',')
}
sb.WriteString(QuoteIdentifier(fkColName))
}
sb.WriteString(")\n REFERENCES ")
sb.WriteString(QuoteIdentifier(foreignKey.ReferencedTableName))
sb.WriteString(" (")
for i, fkColName := range foreignKey.ReferencedTableColumns {
if i != 0 {
sb.WriteRune(',')
}
sb.WriteString(QuoteIdentifier(fkColName))
}
sb.WriteRune(')')
if foreignKey.OnDelete != doltdb.ForeignKeyReferenceOption_DefaultAction {
sb.WriteString("\n ON DELETE ")
sb.WriteString(foreignKey.OnDelete.String())
}
if foreignKey.OnUpdate != doltdb.ForeignKeyReferenceOption_DefaultAction {
sb.WriteString("\n ON UPDATE ")
sb.WriteString(foreignKey.OnUpdate.String())
}
}
sb.WriteString("\n);")
return sb.String()
+2 -2
View File
@@ -706,7 +706,7 @@ var BasicSelectTests = []SelectTest{
Query: "select * from dolt_log",
ExpectedRows: []sql.Row{
{
"0e2b6g3oemme1je6g3l2bm3hr5mhgpa2",
"p3hcpn726bhcsellrtitr85ckjotnv8d",
"billy bob",
"bigbillieb@fake.horse",
time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
@@ -736,7 +736,7 @@ var BasicSelectTests = []SelectTest{
ExpectedRows: []sql.Row{
{
"master",
"0e2b6g3oemme1je6g3l2bm3hr5mhgpa2",
"p3hcpn726bhcsellrtitr85ckjotnv8d",
"billy bob", "bigbillieb@fake.horse",
time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
"Initialize data repository",
+326 -68
View File
@@ -18,8 +18,10 @@ import (
"errors"
"fmt"
"io"
"strings"
"github.com/liquidata-inc/go-mysql-server/sql"
"vitess.io/vitess/go/sqltypes"
"github.com/liquidata-inc/dolt/go/libraries/doltcore/doltdb"
"github.com/liquidata-inc/dolt/go/libraries/doltcore/schema"
@@ -258,6 +260,7 @@ type AlterableDoltTable struct {
var _ sql.AlterableTable = (*AlterableDoltTable)(nil)
var _ sql.IndexAlterableTable = (*AlterableDoltTable)(nil)
var _ sql.ForeignKeyAlterableTable = (*AlterableDoltTable)(nil)
// AddColumn implements sql.AlterableTable
func (t *AlterableDoltTable) AddColumn(ctx *sql.Context, column *sql.Column, order *sql.ColumnOrder) error {
@@ -434,58 +437,7 @@ func (t *AlterableDoltTable) ModifyColumn(ctx *sql.Context, columnName string, c
// CreateIndex implements sql.IndexAlterableTable
func (t *AlterableDoltTable) CreateIndex(ctx *sql.Context, indexName string, using sql.IndexUsing, constraint sql.IndexConstraint, columns []sql.IndexColumn, comment string) error {
if constraint != sql.IndexConstraint_None && constraint != sql.IndexConstraint_Unique {
return fmt.Errorf("not yet supported")
}
if !doltdb.IsValidTableName(indexName) {
return fmt.Errorf("invalid index name `%s` as they must match the regular expression %s", indexName, doltdb.TableNameRegexStr)
}
// get the real column names as CREATE INDEX columns are case-insensitive
var realColNames []string
allTableCols := t.sch.GetAllCols()
for _, indexCol := range columns {
tableCol, ok := allTableCols.GetByName(indexCol.Name)
if !ok {
tableCol, ok = allTableCols.GetByNameCaseInsensitive(indexCol.Name)
if !ok {
return fmt.Errorf("column `%s` does not exist for the table", indexCol.Name)
}
}
realColNames = append(realColNames, tableCol.Name)
}
// create the index metadata, will error if index names are taken or an index with the same columns in the same order exists
_, err := t.sch.Indexes().AddIndexByColNames(indexName, realColNames, constraint == sql.IndexConstraint_Unique, comment)
if err != nil {
return err
}
// update the table schema with the new index
newSchemaVal, err := encoding.MarshalSchemaAsNomsValue(ctx, t.table.ValueReadWriter(), t.sch)
if err != nil {
return err
}
tableRowData, err := t.table.GetRowData(ctx)
if err != nil {
return err
}
indexData, err := t.table.GetIndexData(ctx)
if err != nil {
return err
}
newTable, err := doltdb.NewTable(ctx, t.table.ValueReadWriter(), newSchemaVal, tableRowData, &indexData)
if err != nil {
return err
}
// set the index row data and get a new root with the updated table
indexRowData, err := newTable.RebuildIndexRowData(ctx, indexName)
if err != nil {
return err
}
newTable, err = newTable.SetIndexRowData(ctx, indexName, indexRowData)
newTable, _, _, err := t.createIndex(ctx, t.table, t.sch, false, indexName, using, constraint, columns, comment)
if err != nil {
return err
}
@@ -497,27 +449,23 @@ func (t *AlterableDoltTable) CreateIndex(ctx *sql.Context, indexName string, usi
if err != nil {
return err
}
return t.db.SetRoot(ctx, newRoot)
err = t.db.SetRoot(ctx, newRoot)
if err != nil {
return err
}
return t.updateFromRoot(ctx, newRoot)
}
// DropIndex implements sql.IndexAlterableTable
func (t *AlterableDoltTable) DropIndex(ctx *sql.Context, indexName string) error {
// RemoveIndex returns an error if the index does not exist, no need to do twice
_, err := t.sch.Indexes().RemoveIndex(indexName)
// We disallow removing internal dolt_ tables from SQL directly
if strings.HasPrefix(indexName, "dolt_") {
return fmt.Errorf("dolt internal indexes may not be dropped")
}
newTable, _, err := t.dropIndex(ctx, t.table, t.sch, indexName)
if err != nil {
return err
}
newTable, err := t.table.UpdateSchema(ctx, t.sch)
if err != nil {
return err
}
newTable, err = newTable.DeleteIndexRowData(ctx, indexName)
if err != nil {
return err
}
root, err := t.db.GetRoot(ctx)
if err != nil {
return err
@@ -526,7 +474,11 @@ func (t *AlterableDoltTable) DropIndex(ctx *sql.Context, indexName string) error
if err != nil {
return err
}
return t.db.SetRoot(ctx, newRoot)
err = t.db.SetRoot(ctx, newRoot)
if err != nil {
return err
}
return t.updateFromRoot(ctx, newRoot)
}
// RenameIndex implements sql.IndexAlterableTable
@@ -550,5 +502,311 @@ func (t *AlterableDoltTable) RenameIndex(ctx *sql.Context, fromIndexName string,
return err
}
return t.db.SetRoot(ctx, newRoot)
err = t.db.SetRoot(ctx, newRoot)
if err != nil {
return err
}
return t.updateFromRoot(ctx, newRoot)
}
func (t *AlterableDoltTable) CreateForeignKey(ctx *sql.Context, fkName string, columns []string, referencedTable string, referencedColumns []string,
onUpdate, onDelete sql.ForeignKeyReferenceOption) error {
if fkName != "" && !doltdb.IsValidTableName(fkName) {
return fmt.Errorf("invalid foreign key name `%s` as it must match the regular expression %s", fkName, doltdb.TableNameRegexStr)
}
//TODO: move this into go-mysql-server
if len(columns) != len(referencedColumns) {
return fmt.Errorf("the foreign key must reference an equivalent number of columns")
}
root, err := t.db.GetRoot(ctx)
if err != nil {
return err
}
refTable, refTableName, ok, err := root.GetTableInsensitive(ctx, referencedTable)
if err != nil {
return err
}
if !ok {
return fmt.Errorf("referenced table `%s` does not exist", referencedTable)
}
refTableSch, err := refTable.GetSchema(ctx)
if err != nil {
return err
}
tblCols := make([]schema.Column, len(columns))
colTags := make([]uint64, len(columns))
sqlColNames := make([]sql.IndexColumn, len(columns))
for i, col := range columns {
tableCol, ok := t.sch.GetAllCols().GetByNameCaseInsensitive(col)
if !ok {
//TODO: fix go-mysql-server equivalent check, needs two vals
return fmt.Errorf("table `%s` does not have column `%s`", t.name, col)
}
tblCols[i] = tableCol
colTags[i] = tableCol.Tag
sqlColNames[i] = sql.IndexColumn{
Name: tableCol.Name,
Length: 0,
}
}
refColTags := make([]uint64, len(referencedColumns))
sqlRefColNames := make([]sql.IndexColumn, len(referencedColumns))
for i, col := range referencedColumns {
tableCol, ok := refTableSch.GetAllCols().GetByNameCaseInsensitive(col)
if !ok {
return fmt.Errorf("table `%s` does not have column `%s`", refTableName, col)
}
refColTags[i] = tableCol.Tag
sqlRefColNames[i] = sql.IndexColumn{
Name: tableCol.Name,
Length: 0,
}
if !tblCols[i].TypeInfo.Equals(tableCol.TypeInfo) {
//TODO: make this more descriptive about our lack of support of different types for fks, figure it out later
return fmt.Errorf("column type mismatch on `%s` and `%s`", columns[i], tableCol.Name)
}
sqlparserType := tableCol.TypeInfo.ToSqlType().Type()
if sqlparserType == sqltypes.Blob || sqlparserType == sqltypes.Text {
return fmt.Errorf("TEXT/BLOB are not valid types for foreign keys")
}
}
onUpdateRefOp, err := t.parseFkReferenceOption(onUpdate)
if err != nil {
return err
}
onDeleteRefOp, err := t.parseFkReferenceOption(onDelete)
if err != nil {
return err
}
uniqueIndexName := func(tblSch schema.Schema) string {
indexName := "dolt_fk_1"
for i := 2; tblSch.Indexes().Contains(indexName); i++ {
indexName = fmt.Sprintf("dolt_fk_%d", i)
}
return indexName
}
newTable, _, tableIndex, err := t.createIndex(ctx, t.table, t.sch, true, uniqueIndexName(t.sch), sql.IndexUsing_Default, sql.IndexConstraint_None, sqlColNames, "")
if err != nil {
return err
}
newRefTable, _, refTableIndex, err := t.createIndex(ctx, refTable, refTableSch, true, uniqueIndexName(refTableSch), sql.IndexUsing_Default, sql.IndexConstraint_None, sqlRefColNames, "")
if err != nil {
return err
}
newRoot, err := root.PutTable(ctx, t.name, newTable)
if err != nil {
return err
}
newRoot, err = newRoot.PutTable(ctx, refTableName, newRefTable)
if err != nil {
return err
}
foreignKeyCollection, err := newRoot.GetForeignKeyCollection(ctx)
if err != nil {
return err
}
err = foreignKeyCollection.AddKey(&doltdb.ForeignKey{
Name: fkName,
TableName: t.name,
TableIndex: tableIndex.Name(),
TableColumns: colTags,
ReferencedTableName: refTableName,
ReferencedTableIndex: refTableIndex.Name(),
ReferencedTableColumns: refColTags,
OnUpdate: onUpdateRefOp,
OnDelete: onDeleteRefOp,
})
if err != nil {
return err
}
newRoot, err = newRoot.PutForeignKeyCollection(ctx, foreignKeyCollection)
if err != nil {
return err
}
err = t.db.SetRoot(ctx, newRoot)
if err != nil {
return err
}
return t.updateFromRoot(ctx, newRoot)
}
func (t *AlterableDoltTable) DropForeignKey(ctx *sql.Context, fkName string) error {
root, err := t.db.GetRoot(ctx)
if err != nil {
return err
}
fkc, err := root.GetForeignKeyCollection(ctx)
if err != nil {
return err
}
foreignKey, err := fkc.RemoveKey(fkName)
if err != nil {
return err
}
newRoot, err := root.PutForeignKeyCollection(ctx, fkc)
newTable, _, err := t.dropIndex(ctx, t.table, t.sch, foreignKey.TableIndex)
if err != nil {
return err
}
newRoot, err = newRoot.PutTable(ctx, t.name, newTable)
if err != nil {
return err
}
referencedTable, ok, err := newRoot.GetTable(ctx, foreignKey.ReferencedTableName)
if err != nil {
return err
}
if !ok {
return fmt.Errorf("unable to find referenced table `%s`", foreignKey.ReferencedTableName)
}
referencedSch, err := referencedTable.GetSchema(ctx)
if err != nil {
return err
}
newOtherTable, _, err := t.dropIndex(ctx, referencedTable, referencedSch, foreignKey.ReferencedTableIndex)
if err != nil {
return err
}
newRoot, err = newRoot.PutTable(ctx, foreignKey.ReferencedTableName, newOtherTable)
if err != nil {
return err
}
err = t.db.SetRoot(ctx, newRoot)
if err != nil {
return err
}
return t.updateFromRoot(ctx, newRoot)
}
// createIndex creates the given index on the given table with the given schema. Although this is called on an instance
// of AlterableDoltTable, it is only a method to free up the package namespace, and is not dependent (nor modifies) the
// instance. Returns the updated table, updated schema, and created index.
func (t *AlterableDoltTable) createIndex(
ctx *sql.Context,
tbl *doltdb.Table,
tblSch schema.Schema,
hidden bool,
indexName string,
using sql.IndexUsing,
constraint sql.IndexConstraint,
columns []sql.IndexColumn,
comment string,
) (*doltdb.Table, schema.Schema, schema.Index, error) {
if constraint != sql.IndexConstraint_None && constraint != sql.IndexConstraint_Unique {
return nil, nil, nil, fmt.Errorf("not yet supported")
}
if !hidden && !doltdb.IsValidTableName(indexName) {
return nil, nil, nil, fmt.Errorf("invalid index name `%s` as they must match the regular expression %s", indexName, doltdb.TableNameRegexStr)
}
// get the real column names as CREATE INDEX columns are case-insensitive
var realColNames []string
allTableCols := tblSch.GetAllCols()
for _, indexCol := range columns {
tableCol, ok := allTableCols.GetByNameCaseInsensitive(indexCol.Name)
if !ok {
return nil, nil, nil, fmt.Errorf("column `%s` does not exist for the table", indexCol.Name)
}
realColNames = append(realColNames, tableCol.Name)
}
// create the index metadata, will error if index names are taken or an index with the same columns in the same order exists
index, err := tblSch.Indexes().AddIndexByColNames(indexName, realColNames, schema.IndexProperties{IsUnique: constraint == sql.IndexConstraint_Unique, IsHidden: hidden, Comment: comment})
if err != nil {
return nil, nil, nil, err
}
// update the table schema with the new index
newSchemaVal, err := encoding.MarshalSchemaAsNomsValue(ctx, tbl.ValueReadWriter(), tblSch)
if err != nil {
return nil, nil, nil, err
}
tableRowData, err := tbl.GetRowData(ctx)
if err != nil {
return nil, nil, nil, err
}
indexData, err := tbl.GetIndexData(ctx)
if err != nil {
return nil, nil, nil, err
}
newTable, err := doltdb.NewTable(ctx, tbl.ValueReadWriter(), newSchemaVal, tableRowData, &indexData)
if err != nil {
return nil, nil, nil, err
}
// set the index row data and get a new root with the updated table
indexRowData, err := newTable.RebuildIndexRowData(ctx, index.Name())
if err != nil {
return nil, nil, nil, err
}
newTable, err = newTable.SetIndexRowData(ctx, index.Name(), indexRowData)
if err != nil {
return nil, nil, nil, err
}
return newTable, tblSch, index, nil
}
// dropIndex drops the given index on the given table with the given schema. Although this is called on an instance
// of AlterableDoltTable, it is only a method to free up the package namespace, and is not dependent (nor modifies) the
// instance. Returns the updated table and updated schema.
func (t *AlterableDoltTable) dropIndex(ctx *sql.Context, tbl *doltdb.Table, tblSch schema.Schema, indexName string) (*doltdb.Table, schema.Schema, error) {
// RemoveIndex returns an error if the index does not exist, no need to do twice
_, err := tblSch.Indexes().RemoveIndex(indexName)
if err != nil {
return nil, nil, err
}
newTable, err := tbl.UpdateSchema(ctx, tblSch)
if err != nil {
return nil, nil, err
}
newTable, err = newTable.DeleteIndexRowData(ctx, indexName)
if err != nil {
return nil, nil, err
}
return newTable, tblSch, nil
}
func (t *AlterableDoltTable) parseFkReferenceOption(refOp sql.ForeignKeyReferenceOption) (doltdb.ForeignKeyReferenceOption, error) {
switch refOp {
case sql.ForeignKeyReferenceOption_DefaultAction:
return doltdb.ForeignKeyReferenceOption_DefaultAction, nil
case sql.ForeignKeyReferenceOption_Restrict:
return doltdb.ForeignKeyReferenceOption_Restrict, nil
case sql.ForeignKeyReferenceOption_Cascade:
return doltdb.ForeignKeyReferenceOption_Cascade, nil
case sql.ForeignKeyReferenceOption_NoAction:
return doltdb.ForeignKeyReferenceOption_NoAction, nil
case sql.ForeignKeyReferenceOption_SetNull:
return doltdb.ForeignKeyReferenceOption_SetNull, nil
case sql.ForeignKeyReferenceOption_SetDefault:
return doltdb.ForeignKeyReferenceOption_DefaultAction, fmt.Errorf(`"SET DEFAULT" is not supported`)
default:
return doltdb.ForeignKeyReferenceOption_DefaultAction, fmt.Errorf("unknown foreign key reference option: %v", refOp)
}
}
func (t *AlterableDoltTable) updateFromRoot(ctx *sql.Context, root *doltdb.RootValue) error {
updatedTableSql, ok, err := t.db.getTable(ctx, root, t.name)
if err != nil {
return err
}
if !ok {
return fmt.Errorf("table `%s` cannot find itself", t.name)
}
updatedTable := updatedTableSql.(*AlterableDoltTable)
t.WritableDoltTable.DoltTable = updatedTable.WritableDoltTable.DoltTable
return nil
}
@@ -21,10 +21,10 @@ import (
"path/filepath"
"strings"
"github.com/liquidata-inc/dolt/go/libraries/doltcore/sqle/sqlfmt"
"github.com/liquidata-inc/dolt/go/libraries/doltcore/doltdb"
"github.com/liquidata-inc/dolt/go/libraries/doltcore/row"
"github.com/liquidata-inc/dolt/go/libraries/doltcore/schema"
"github.com/liquidata-inc/dolt/go/libraries/doltcore/sqle/sqlfmt"
"github.com/liquidata-inc/dolt/go/libraries/utils/filesys"
"github.com/liquidata-inc/dolt/go/libraries/utils/iohelp"
)
@@ -33,13 +33,14 @@ import (
// SQL database.
type SqlExportWriter struct {
tableName string
foreignKeys []*doltdb.DisplayForeignKey
sch schema.Schema
wr io.WriteCloser
writtenFirstRow bool
}
// OpenSQLExportWriter returns a new SqlWriter for the table given writing to a file with the path given.
func OpenSQLExportWriter(path string, tableName string, fs filesys.WritableFS, sch schema.Schema) (*SqlExportWriter, error) {
func OpenSQLExportWriter(path string, tableName string, fs filesys.WritableFS, sch schema.Schema, foreignKeys []*doltdb.DisplayForeignKey) (*SqlExportWriter, error) {
err := fs.MkDirs(filepath.Dir(path))
if err != nil {
return nil, err
@@ -50,7 +51,7 @@ func OpenSQLExportWriter(path string, tableName string, fs filesys.WritableFS, s
return nil, err
}
return &SqlExportWriter{tableName: tableName, sch: sch, wr: wr}, nil
return &SqlExportWriter{tableName: tableName, foreignKeys: foreignKeys, sch: sch, wr: wr}, nil
}
func NewSQLDiffWriter(wr io.WriteCloser, tableName string, sch schema.Schema) (*SqlExportWriter, error) {
@@ -83,7 +84,7 @@ func (w *SqlExportWriter) maybeWriteDropCreate() error {
var b strings.Builder
b.WriteString(sqlfmt.DropTableIfExistsStmt(w.tableName))
b.WriteRune('\n')
b.WriteString(sqlfmt.CreateTableStmtWithTags(w.tableName, w.sch))
b.WriteString(sqlfmt.CreateTableStmtWithTags(w.tableName, w.sch, w.foreignKeys))
if err := iohelp.WriteLine(w.wr, b.String()); err != nil {
return err
}
@@ -47,7 +47,7 @@ func TestEndToEnd(t *testing.T) {
id := uuid.MustParse("00000000-0000-0000-0000-000000000000")
tableName := "people"
dropCreateStatement := sqlfmt.DropTableIfExistsStmt(tableName) + "\n" + sqlfmt.CreateTableStmtWithTags(tableName, dtestutils.TypedSchema)
dropCreateStatement := sqlfmt.DropTableIfExistsStmt(tableName) + "\n" + sqlfmt.CreateTableStmtWithTags(tableName, dtestutils.TypedSchema, nil)
type test struct {
name string