dolthub--dolt
3413 行
102 KiB
Go
3413 行
102 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 sqle
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"os"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/dolthub/go-mysql-server/sql"
|
|
"github.com/dolthub/go-mysql-server/sql/expression"
|
|
"github.com/dolthub/go-mysql-server/sql/expression/function/vector"
|
|
"github.com/dolthub/go-mysql-server/sql/fulltext"
|
|
"github.com/dolthub/go-mysql-server/sql/sets"
|
|
sqltypes "github.com/dolthub/go-mysql-server/sql/types"
|
|
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/branch_control"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/dconfig"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb/durable"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/ref"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/schema"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/schema/typecompatibility"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/schema/typeinfo"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dtables"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/fk"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/index"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/sqlutil"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/writer"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/table/editor"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/table/editor/creation"
|
|
"github.com/dolthub/dolt/go/store/hash"
|
|
"github.com/dolthub/dolt/go/store/types"
|
|
"github.com/dolthub/dolt/go/store/val"
|
|
)
|
|
|
|
const (
|
|
partitionMultiplier = 2.0
|
|
)
|
|
|
|
var MaxRowsPerPartition uint64 = 32 * 1024
|
|
var MinRowsPerPartition uint64 = 1024
|
|
|
|
func init() {
|
|
isTest := false
|
|
for _, arg := range os.Args {
|
|
lwr := strings.ToLower(arg)
|
|
if lwr == "-test.v" ||
|
|
lwr == "-test.run" ||
|
|
strings.HasPrefix(lwr, "-test.testlogfile") ||
|
|
strings.HasPrefix(lwr, "-test.timeout") ||
|
|
strings.HasPrefix(lwr, "-test.count") {
|
|
isTest = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if isTest {
|
|
MinRowsPerPartition = 2
|
|
}
|
|
}
|
|
|
|
var _ dtables.VersionableTable = (*DoltTable)(nil)
|
|
|
|
// DoltTable implements the sql.Table interface and gives access to dolt table rows and schema.
|
|
type DoltTable struct {
|
|
db dsess.SqlDatabase
|
|
lockedToRoot doltdb.RootValue
|
|
|
|
// overriddenSchema is set when the @@dolt_override_schema system var is in use
|
|
overriddenSchema schema.Schema
|
|
sch schema.Schema
|
|
|
|
nbf *types.NomsBinFormat
|
|
tableName string
|
|
autoIncCol schema.Column
|
|
sqlSch sql.PrimaryKeySchema
|
|
opts editor.Options
|
|
projectedCols []uint64
|
|
projectedSchema sql.Schema
|
|
}
|
|
|
|
func (t *DoltTable) TableName() doltdb.TableName {
|
|
return doltdb.TableName{Name: t.tableName, Schema: t.db.Schema()}
|
|
}
|
|
|
|
func (t *DoltTable) DatabaseSchema() sql.DatabaseSchema {
|
|
return t.db
|
|
}
|
|
|
|
func (t *DoltTable) SkipIndexCosting() bool {
|
|
return false
|
|
}
|
|
|
|
func (t *DoltTable) LookupForExpressions(ctx *sql.Context, exprs ...sql.Expression) (sql.IndexLookup, *sql.FuncDepSet, sql.Expression, bool, error) {
|
|
root, err := t.workingRoot(ctx)
|
|
if err != nil {
|
|
return sql.IndexLookup{}, nil, nil, false, err
|
|
}
|
|
|
|
schHash, err := doltdb.GetSchemaHash(ctx, root, t.TableName(), t.overriddenSchema)
|
|
if err != nil {
|
|
return sql.IndexLookup{}, nil, nil, false, err
|
|
}
|
|
|
|
sess, ok := ctx.Session.(*dsess.DoltSession)
|
|
if !ok {
|
|
return sql.IndexLookup{}, nil, nil, false, nil
|
|
}
|
|
|
|
dbState, ok, err := sess.LookupDbState(ctx, t.db.AliasedName())
|
|
if err != nil {
|
|
return sql.IndexLookup{}, nil, nil, false, nil
|
|
}
|
|
if !ok {
|
|
return sql.IndexLookup{}, nil, nil, false, fmt.Errorf("no state for database %s", t.db.AliasedName())
|
|
}
|
|
|
|
var lookupCols []expression.LookupColumn
|
|
var leftoverExpr sql.Expression
|
|
for _, e := range exprs {
|
|
col, ok := expression.LookupEqualityColumn(t.db.Name(), t.tableName, e)
|
|
if ok {
|
|
lookupCols = append(lookupCols, col)
|
|
} else if leftoverExpr == nil {
|
|
leftoverExpr = e
|
|
} else {
|
|
leftoverExpr = expression.NewAnd(leftoverExpr, e)
|
|
}
|
|
}
|
|
|
|
colset := sets.NewFastIntSet()
|
|
schCols := t.sch.GetAllCols()
|
|
for _, c := range lookupCols {
|
|
col := schCols.LowerNameToCol[c.Col]
|
|
if !sql.IsConvertibleKeyType(col.TypeInfo.ToSqlType(), c.Lit.Type(ctx)) {
|
|
return sql.IndexLookup{}, nil, nil, false, nil
|
|
}
|
|
idx := schCols.TagToIdx[col.Tag]
|
|
colset.Add(idx + 1)
|
|
}
|
|
|
|
schKey := doltdb.DataCacheKey{Hash: schHash}
|
|
|
|
lookups, ok := dbState.SessionCache().GetCachedStrictLookup(schKey)
|
|
if !ok {
|
|
indexes, err := t.GetIndexes(ctx)
|
|
if err != nil {
|
|
return sql.IndexLookup{}, nil, nil, false, err
|
|
}
|
|
lookups = index.GetStrictLookups(ctx, schCols, indexes)
|
|
dbState.SessionCache().CacheStrictLookup(schKey, lookups)
|
|
}
|
|
|
|
cols := schCols.GetColumns()
|
|
for _, lookup := range lookups {
|
|
// Every column must be specified in search
|
|
if lookup.Cols.Intersection(colset).Len() != lookup.Cols.Len() {
|
|
continue
|
|
}
|
|
// (1) assign lookup columns to range expressions in the appropriate order for the given lookup.
|
|
// (2) aggregate the unused expressions into the return filter.
|
|
rb := sql.NewEqualityIndexBuilder(lookup.Idx)
|
|
for _, c2 := range lookupCols {
|
|
matchIdx := -1
|
|
for i, ord := range lookup.Ordinals {
|
|
// the ordinals redirection accounts for index
|
|
// columns not in schema order
|
|
idx := ord - 1
|
|
c := cols[idx]
|
|
if strings.EqualFold(c2.Col, c.Name) {
|
|
matchIdx = i
|
|
break
|
|
}
|
|
}
|
|
if matchIdx != -1 {
|
|
if err = rb.AddEquality(ctx, matchIdx, c2.Lit.Value()); err != nil {
|
|
return sql.IndexLookup{}, nil, nil, false, nil
|
|
}
|
|
continue
|
|
}
|
|
if leftoverExpr == nil {
|
|
leftoverExpr = c2.Eq
|
|
} else {
|
|
leftoverExpr = expression.NewAnd(leftoverExpr, c2.Eq)
|
|
}
|
|
}
|
|
|
|
var ret sql.IndexLookup
|
|
ret, err = rb.Build(ctx)
|
|
if err != nil {
|
|
return sql.IndexLookup{}, nil, nil, false, err
|
|
}
|
|
return ret, lookup.Fds, leftoverExpr, true, nil
|
|
}
|
|
|
|
return sql.IndexLookup{}, nil, nil, false, nil
|
|
|
|
}
|
|
|
|
func NewDoltTable(ctx *sql.Context, name string, sch schema.Schema, tbl *doltdb.Table, db dsess.SqlDatabase, opts editor.Options) (*DoltTable, error) {
|
|
var autoCol schema.Column
|
|
_ = sch.GetAllCols().Iter(func(tag uint64, col schema.Column) (stop bool, err error) {
|
|
if col.AutoIncrement {
|
|
autoCol = col
|
|
stop = true
|
|
}
|
|
return
|
|
})
|
|
|
|
sqlSch, err := sqlutil.FromDoltSchema(ctx, db.Name(), name, sch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &DoltTable{
|
|
tableName: name,
|
|
db: db,
|
|
nbf: tbl.Format(),
|
|
sch: sch,
|
|
sqlSch: sqlSch,
|
|
autoIncCol: autoCol,
|
|
projectedCols: nil,
|
|
overriddenSchema: tbl.GetOverriddenSchema(),
|
|
opts: opts,
|
|
}, nil
|
|
}
|
|
|
|
// LockedToRoot returns a version of this table with its root value locked to the given value. The table's values will
|
|
// not change as the session's root value changes. Appropriate for AS OF queries, or other use cases where the table's
|
|
// values should not change throughout execution of a session.
|
|
func (t *DoltTable) LockedToRoot(ctx *sql.Context, root doltdb.RootValue) (sql.IndexAddressableTable, error) {
|
|
tbl, ok, err := root.GetTable(ctx, t.TableName())
|
|
if err != nil {
|
|
return nil, err
|
|
} else if !ok {
|
|
return nil, doltdb.ErrTableNotFound
|
|
}
|
|
|
|
sch, err := tbl.GetSchema(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var autoCol schema.Column
|
|
_ = sch.GetAllCols().Iter(func(tag uint64, col schema.Column) (stop bool, err error) {
|
|
if col.AutoIncrement {
|
|
autoCol = col
|
|
stop = true
|
|
}
|
|
return
|
|
})
|
|
|
|
sqlSch, err := sqlutil.FromDoltSchema(ctx, t.db.Name(), t.tableName, sch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dt := &DoltTable{
|
|
tableName: t.tableName,
|
|
db: t.db,
|
|
nbf: tbl.Format(),
|
|
sch: sch,
|
|
sqlSch: sqlSch,
|
|
autoIncCol: autoCol,
|
|
opts: t.opts,
|
|
lockedToRoot: root,
|
|
overriddenSchema: t.overriddenSchema,
|
|
}
|
|
dtProj, err := dt.WithProjections(ctx, t.Projections())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return dtProj.(*DoltTable), nil
|
|
}
|
|
|
|
// Internal interface for declaring the interfaces that read-only dolt tables are expected to implement
|
|
// Add new interfaces supported here, rather than in separate type assertions
|
|
type doltReadOnlyTableInterface interface {
|
|
sql.TemporaryTable
|
|
sql.IndexAddressableTable
|
|
sql.ForeignKeyTable
|
|
sql.StatisticsTable
|
|
sql.CheckTable
|
|
sql.PrimaryKeyTable
|
|
sql.CommentedTable
|
|
sql.DatabaseSchemaTable
|
|
}
|
|
|
|
var _ doltReadOnlyTableInterface = (*DoltTable)(nil)
|
|
var _ sql.ProjectedTable = (*DoltTable)(nil)
|
|
var _ sql.IndexSearchable = (*DoltTable)(nil)
|
|
|
|
// IndexedAccess implements sql.IndexAddressableTable
|
|
func (t *DoltTable) IndexedAccess(ctx *sql.Context, lookup sql.IndexLookup) sql.IndexedTable {
|
|
return NewIndexedDoltTable(t, lookup.Index.(index.DoltIndex))
|
|
}
|
|
|
|
// DoltTable returns the underlying doltTable from the current session
|
|
func (t *DoltTable) DoltTable(ctx *sql.Context) (*doltdb.Table, error) {
|
|
root, err := t.workingRoot(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
table, ok, err := root.GetTable(ctx, t.TableName())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !ok {
|
|
return nil, sql.ErrTableNotFound.New(t.tableName)
|
|
}
|
|
|
|
if t.overriddenSchema != nil {
|
|
table.OverrideSchema(t.overriddenSchema)
|
|
}
|
|
|
|
return table, nil
|
|
}
|
|
|
|
// DataCacheKey returns an opaque key that can be compared for equality to see if this
|
|
// table's data still matches a previous view of the data that was retrieved
|
|
// through DoltTable() or NumRows(), for example.
|
|
//
|
|
// Returns |false| for |ok| if this table's data is not cacheable.
|
|
func (t *DoltTable) DataCacheKey(ctx *sql.Context) (doltdb.DataCacheKey, bool, error) {
|
|
r, err := t.workingRoot(ctx)
|
|
if err != nil {
|
|
return doltdb.DataCacheKey{}, false, err
|
|
}
|
|
key, err := doltdb.NewDataCacheKey(r)
|
|
if err != nil {
|
|
return doltdb.DataCacheKey{}, false, err
|
|
}
|
|
|
|
return key, true, nil
|
|
}
|
|
|
|
func (t *DoltTable) IndexCacheKey(ctx *sql.Context) (doltdb.DataCacheKey, bool, error) {
|
|
root, err := t.workingRoot(ctx)
|
|
if err != nil {
|
|
return doltdb.DataCacheKey{}, false, err
|
|
}
|
|
|
|
key, err := doltdb.GetSchemaHash(ctx, root, t.TableName(), t.overriddenSchema)
|
|
if err != nil {
|
|
return doltdb.DataCacheKey{}, false, err
|
|
}
|
|
|
|
return doltdb.DataCacheKey{Hash: key}, true, nil
|
|
}
|
|
|
|
func (t *DoltTable) workingRoot(ctx *sql.Context) (doltdb.RootValue, error) {
|
|
root := t.lockedToRoot
|
|
if root == nil {
|
|
return t.getRoot(ctx)
|
|
}
|
|
return root, nil
|
|
}
|
|
|
|
// getRoot returns the current root value for this session, to be used for all table data access.
|
|
func (t *DoltTable) getRoot(ctx *sql.Context) (doltdb.RootValue, error) {
|
|
return t.db.GetRoot(ctx)
|
|
}
|
|
|
|
// GetIndexes implements sql.IndexedTable
|
|
func (t *DoltTable) GetIndexes(ctx *sql.Context) ([]sql.Index, error) {
|
|
// If a schema override is in place, we can't trust that the indexes stored with the data
|
|
// will match up to the overridden schema, so we disable indexes. We could improve this by
|
|
// adding schema mapping for the indexes.
|
|
if t.overriddenSchema != nil {
|
|
return nil, nil
|
|
}
|
|
|
|
key, tableIsCacheable, err := t.IndexCacheKey(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !tableIsCacheable {
|
|
tbl, err := t.DoltTable(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return index.DoltIndexesFromTable(ctx, t.db.Name(), t.tableName, tbl)
|
|
}
|
|
|
|
sess := dsess.DSessFromSess(ctx.Session)
|
|
dbState, ok, err := sess.LookupDbState(ctx, t.db.RevisionQualifiedName())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !ok {
|
|
return nil, fmt.Errorf("couldn't find db state for database %s", t.db.Name())
|
|
}
|
|
|
|
indexes, ok := dbState.SessionCache().GetTableIndexesCache(key, t.Name())
|
|
if ok {
|
|
return indexes, nil
|
|
}
|
|
|
|
tbl, err := t.DoltTable(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
indexes, err = index.DoltIndexesFromTable(ctx, t.db.Name(), t.tableName, tbl)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dbState.SessionCache().CacheTableIndexes(key, t.Name(), indexes)
|
|
return indexes, nil
|
|
}
|
|
|
|
func (t *DoltTable) PreciseMatch() bool {
|
|
return true
|
|
}
|
|
|
|
// HasIndex returns whether the given index is present in the table
|
|
func (t *DoltTable) HasIndex(ctx *sql.Context, idx sql.Index) (bool, error) {
|
|
tbl, err := t.DoltTable(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return index.TableHasIndex(ctx, t.db.Name(), t.tableName, tbl, idx)
|
|
}
|
|
|
|
// PeekNextAutoIncrementValue gets the next AUTO_INCREMENT value
|
|
func (t *DoltTable) PeekNextAutoIncrementValue(ctx *sql.Context) (uint64, error) {
|
|
table, err := t.DoltTable(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return table.GetAutoIncrementValue(ctx)
|
|
}
|
|
|
|
// Name returns the name of the table.
|
|
func (t *DoltTable) Name() string {
|
|
return t.tableName
|
|
}
|
|
|
|
// String returns a human-readable string to display the name of this SQL node.
|
|
func (t *DoltTable) String() string {
|
|
return t.tableName
|
|
}
|
|
func (t *DoltTable) DebugString() string {
|
|
p := sql.NewTreePrinter()
|
|
|
|
children := []string{fmt.Sprintf("name: %s", t.tableName)}
|
|
|
|
cols := t.sch.GetAllCols()
|
|
if len(t.projectedCols) > 0 {
|
|
var projections []string
|
|
for _, tag := range t.projectedCols {
|
|
projections = append(projections, fmt.Sprintf("%d", cols.TagToIdx[tag]))
|
|
}
|
|
children = append(children, fmt.Sprintf("projections: %s", projections))
|
|
|
|
}
|
|
|
|
_ = p.WriteNode("Table")
|
|
p.WriteChildren(children...)
|
|
return p.String()
|
|
}
|
|
|
|
// NumRows returns the unfiltered count of rows contained in the table
|
|
func (t *DoltTable) numRows(ctx *sql.Context) (uint64, error) {
|
|
table, err := t.DoltTable(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
m, err := table.GetRowData(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return m.Count()
|
|
}
|
|
|
|
// Format returns the NomsBinFormat for the underlying table
|
|
func (t *DoltTable) Format() *types.NomsBinFormat {
|
|
return t.nbf
|
|
}
|
|
|
|
// Schema returns the schema for this table.
|
|
func (t *DoltTable) Schema(ctx *sql.Context) sql.Schema {
|
|
// If this table has been set with a specific projection, always prefer returning that as the schema. This
|
|
// enables rules like eraseProjections to operate correctly.
|
|
if t.projectedSchema != nil {
|
|
return t.projectedSchema
|
|
}
|
|
|
|
// If there is an overridden schema, prefer that next
|
|
if t.overriddenSchema != nil {
|
|
sqlSchema, err := sqlutil.FromDoltSchema(ctx, t.db.Name(), t.tableName, t.overriddenSchema)
|
|
if err != nil {
|
|
// panic'ing isn't ideal, but this method doesn't allow returning an error.
|
|
// We could log this and return nil, but that will just cause a problem when
|
|
// the caller tries to use the value, so panic'ing seems appropriate.
|
|
panic("error converting to sql schema: " + err.Error())
|
|
}
|
|
return sqlSchema.Schema
|
|
}
|
|
|
|
// Finally, use the original schema that matches the data if nothing has been overridden
|
|
return t.sqlSchema(ctx).Schema
|
|
}
|
|
|
|
// Collation returns the collation for this table.
|
|
func (t *DoltTable) Collation() sql.CollationID {
|
|
return sql.CollationID(t.sch.GetCollation())
|
|
}
|
|
|
|
// Comment returns the comment for this table.
|
|
func (t *DoltTable) Comment() string {
|
|
return t.sch.GetComment()
|
|
}
|
|
|
|
func (t *DoltTable) sqlSchema(ctx *sql.Context) sql.PrimaryKeySchema {
|
|
// TODO: this should consider projections
|
|
if len(t.sqlSch.Schema) > 0 {
|
|
return t.sqlSch
|
|
}
|
|
|
|
// TODO: fix panics
|
|
sqlSch, err := sqlutil.FromDoltSchema(ctx, t.db.RevisionQualifiedName(), t.tableName, t.sch)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
t.sqlSch = sqlSch
|
|
return sqlSch
|
|
}
|
|
|
|
// Partitions returns the partitions for this table.
|
|
func (t *DoltTable) Partitions(ctx *sql.Context) (sql.PartitionIter, error) {
|
|
table, err := t.DoltTable(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rows, err := table.GetRowData(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
partitions, err := partitionsFromRows(ctx, rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newDoltTablePartitionIter(rows, partitions...), nil
|
|
}
|
|
|
|
func (t *DoltTable) IsTemporary() bool {
|
|
return false
|
|
}
|
|
|
|
// DataLength implements the sql.StatisticsTable interface.
|
|
func (t *DoltTable) DataLength(ctx *sql.Context) (uint64, error) {
|
|
numBytesPerRow := schema.SchemaAvgLength(t.Schema(ctx))
|
|
numRows, err := t.numRows(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return numBytesPerRow * numRows, nil
|
|
}
|
|
|
|
// RowCount implements the sql.StatisticsTable interface.
|
|
func (t *DoltTable) RowCount(ctx *sql.Context) (uint64, bool, error) {
|
|
rows, err := t.numRows(ctx)
|
|
return rows, true, err
|
|
}
|
|
|
|
func (t *DoltTable) PrimaryKeySchema(ctx *sql.Context) sql.PrimaryKeySchema {
|
|
if t.overriddenSchema != nil {
|
|
doltSchema, err := sqlutil.FromDoltSchema(ctx, t.db.Name(), t.tableName, t.overriddenSchema)
|
|
if err != nil {
|
|
// panic'ing isn't ideal, but this method doesn't allow returning an error.
|
|
// We could log this and return nil, but that will just cause a problem when
|
|
// the caller tries to use the value, so panic'ing seems appropriate.
|
|
panic("error converting to sql schema: " + err.Error())
|
|
}
|
|
return doltSchema
|
|
}
|
|
|
|
return t.sqlSchema(ctx)
|
|
}
|
|
|
|
// PartitionRows returns the table rows for the partition given
|
|
func (t *DoltTable) PartitionRows(ctx *sql.Context, partition sql.Partition) (sql.RowIter, error) {
|
|
table, err := t.DoltTable(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If we DON'T have an overridden schema, then we can pass in our projected columns as the
|
|
// ones set in the table that the analyzer has told us need to be projected. This will limit our returned
|
|
// sql.Row to only the requested projected columns. If we DO have an overridden schema in use, then we need
|
|
// to pass in the full column projection for the original/data schema so that we get all columns back. Then,
|
|
// the mappingRowIterator that we apply on top of the original row iterator will take care of mapping the
|
|
// original row and shrinking it down to the projected columns.
|
|
projCols := t.projectedCols
|
|
if t.overriddenSchema != nil {
|
|
originalSchemaCols := t.sch.GetAllCols().GetColumns()
|
|
projCols = make([]uint64, len(originalSchemaCols))
|
|
for i, col := range originalSchemaCols {
|
|
projCols[i] = col.Tag
|
|
}
|
|
}
|
|
|
|
originalRowIter, err := partitionRows(ctx, table, projCols, partition)
|
|
if err != nil {
|
|
return originalRowIter, err
|
|
}
|
|
|
|
if t.overriddenSchema != nil {
|
|
return newMappingRowIter(ctx, t, originalRowIter)
|
|
} else {
|
|
return originalRowIter, err
|
|
}
|
|
}
|
|
|
|
func partitionRows(ctx *sql.Context, t *doltdb.Table, projCols []uint64, partition sql.Partition) (sql.RowIter, error) {
|
|
switch typedPartition := partition.(type) {
|
|
case doltTablePartition:
|
|
return newRowIterator(ctx, t, projCols, typedPartition)
|
|
case index.SinglePartition:
|
|
return newRowIterator(ctx, t, projCols, doltTablePartition{rowData: typedPartition.RowData, end: NoUpperBound})
|
|
}
|
|
|
|
return nil, errors.New("unsupported partition type")
|
|
}
|
|
|
|
// WritableDoltTable allows updating, deleting, and inserting new rows. It implements sql.UpdatableTable and friends.
|
|
type WritableDoltTable struct {
|
|
pinnedWriteSession dsess.WriteSession
|
|
*DoltTable
|
|
db Database
|
|
}
|
|
|
|
var _ doltTableInterface = (*WritableDoltTable)(nil)
|
|
|
|
// WritableDoltTableWrapper is an interface that allows a table to be returned as an sql.Table, but actually be a wrapped
|
|
// fake table. Specifically, databases.getTableInsensitiveWithRoot will returns an sql.Table, and there are cases where we
|
|
// want to return a table that hasn't been materialized yet.
|
|
type WritableDoltTableWrapper interface {
|
|
// Unwrap returns the underlying WritableDoltTable, nil returns are expected when the wrapped table hasn't been materialized
|
|
UnWrap() *WritableDoltTable
|
|
}
|
|
|
|
// Internal interface for declaring the interfaces that writable dolt tables are expected to implement
|
|
type doltTableInterface interface {
|
|
sql.UpdatableTable
|
|
sql.DeletableTable
|
|
sql.InsertableTable
|
|
sql.ReplaceableTable
|
|
sql.AutoIncrementTable
|
|
sql.TruncateableTable
|
|
sql.ProjectedTable
|
|
sql.Databaseable
|
|
}
|
|
|
|
func (t *WritableDoltTable) Database() string {
|
|
return t.db.baseName
|
|
}
|
|
|
|
func (t *WritableDoltTable) setRoot(ctx *sql.Context, newRoot doltdb.RootValue) error {
|
|
return t.db.SetRoot(ctx, newRoot)
|
|
}
|
|
|
|
func (t *WritableDoltTable) IndexedAccess(ctx *sql.Context, lookup sql.IndexLookup) sql.IndexedTable {
|
|
return NewWritableIndexedDoltTable(t, lookup.Index.(index.DoltIndex))
|
|
}
|
|
|
|
// WithProjections implements sql.ProjectedTable
|
|
func (t *WritableDoltTable) WithProjections(ctx *sql.Context, colNames []string) (sql.Table, error) {
|
|
dtProj, err := t.DoltTable.WithProjections(ctx, colNames)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &WritableDoltTable{
|
|
DoltTable: dtProj.(*DoltTable),
|
|
db: t.db,
|
|
}, nil
|
|
}
|
|
|
|
// Inserter implements sql.InsertableTable
|
|
func (t *WritableDoltTable) Inserter(ctx *sql.Context) sql.RowInserter {
|
|
if err := dsess.CheckAccessForDb(ctx, t.db, branch_control.Permissions_Write); err != nil {
|
|
return sqlutil.NewStaticErrorEditor(err)
|
|
}
|
|
te, err := t.getTableEditor(ctx)
|
|
if err != nil {
|
|
return sqlutil.NewStaticErrorEditor(err)
|
|
}
|
|
return te
|
|
}
|
|
|
|
func (t *WritableDoltTable) getTableEditor(ctx *sql.Context) (ed dsess.TableWriter, err error) {
|
|
ds := dsess.DSessFromSess(ctx.Session)
|
|
var writeSession dsess.WriteSession
|
|
if t.pinnedWriteSession != nil {
|
|
writeSession = t.pinnedWriteSession
|
|
} else {
|
|
state, _, err := ds.LookupDbState(ctx, t.db.RevisionQualifiedName())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
writeSession = state.WriteSession()
|
|
}
|
|
|
|
ed, err = writeSession.GetTableWriter(ctx, t.TableName())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if t.sch.Indexes().ContainsFullTextIndex() {
|
|
ftEditor, err := t.getFullTextEditor(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
multiEditor, err := fulltext.CreateMultiTableEditor(ctx, ed, ftEditor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return multiEditor.(dsess.TableWriter), nil
|
|
}
|
|
return ed, nil
|
|
}
|
|
|
|
// getFullTextEditor gathers all pseudo-index tables for a Full-Text index and returns an editor that will write
|
|
// to all of them. This assumes that there are Full-Text indexes in the schema.
|
|
func (t *WritableDoltTable) getFullTextEditor(ctx *sql.Context) (fulltext.TableEditor, error) {
|
|
workingRoot, err := t.workingRoot(ctx)
|
|
if err != nil {
|
|
return fulltext.TableEditor{}, err
|
|
}
|
|
|
|
configTable, sets, err := t.fulltextTableSets(ctx, workingRoot)
|
|
if err != nil {
|
|
return fulltext.TableEditor{}, err
|
|
}
|
|
|
|
return fulltext.CreateEditor(ctx, t, configTable, sets...)
|
|
}
|
|
|
|
func (t *WritableDoltTable) fulltextTableSets(ctx *sql.Context, workingRoot doltdb.RootValue) (fulltext.EditableTable, []fulltext.TableSet, error) {
|
|
var configTable fulltext.EditableTable
|
|
var sets []fulltext.TableSet
|
|
for _, idx := range t.sch.Indexes().AllIndexes() {
|
|
if !idx.IsFullText() {
|
|
continue
|
|
}
|
|
props := idx.FullTextProperties()
|
|
// We only load the config table once since it's shared by all indexes
|
|
// TODO: should this load directly from the root, bypassing the session?
|
|
if configTable == nil {
|
|
tbl, ok, err := t.db.getTable(ctx, workingRoot, props.ConfigTable)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
} else if !ok {
|
|
return nil, nil, fmt.Errorf("missing Full-Text table: %s", props.ConfigTable)
|
|
}
|
|
configTable = tbl.(fulltext.EditableTable)
|
|
}
|
|
// Load the rest of the tables
|
|
positionTable, ok, err := t.db.getTable(ctx, workingRoot, props.PositionTable)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
} else if !ok {
|
|
return nil, nil, fmt.Errorf("missing Full-Text table: %s", props.PositionTable)
|
|
}
|
|
docCountTable, ok, err := t.db.getTable(ctx, workingRoot, props.DocCountTable)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
} else if !ok {
|
|
return nil, nil, fmt.Errorf("missing Full-Text table: %s", props.DocCountTable)
|
|
}
|
|
globalCountTable, ok, err := t.db.getTable(ctx, workingRoot, props.GlobalCountTable)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
} else if !ok {
|
|
return nil, nil, fmt.Errorf("missing Full-Text table: %s", props.GlobalCountTable)
|
|
}
|
|
rowCountTable, ok, err := t.db.getTable(ctx, workingRoot, props.RowCountTable)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
} else if !ok {
|
|
return nil, nil, fmt.Errorf("missing Full-Text table: %s", props.RowCountTable)
|
|
}
|
|
// Convert the index into a sql.Index
|
|
sqlIdx, err := index.ConvertFullTextToSql(ctx, t.db.RevisionQualifiedName(), t.tableName, t.sch, idx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
sets = append(sets, fulltext.TableSet{
|
|
Index: sqlIdx.(fulltext.Index),
|
|
Position: positionTable.(fulltext.EditableTable),
|
|
DocCount: docCountTable.(fulltext.EditableTable),
|
|
GlobalCount: globalCountTable.(fulltext.EditableTable),
|
|
RowCount: rowCountTable.(fulltext.EditableTable),
|
|
})
|
|
}
|
|
return configTable, sets, nil
|
|
}
|
|
|
|
// tableSetsForRewrite returns the fulltext.TableSet for each Full-Text index in the table, truncated and modified
|
|
// for a table rewrite operation. Returns the root given with all full-text pseudo tables updated with their new
|
|
// truncated value.
|
|
func (t *WritableDoltTable) tableSetsForRewrite(
|
|
ctx *sql.Context,
|
|
workingRoot doltdb.RootValue,
|
|
) (doltdb.RootValue, fulltext.EditableTable, []fulltext.TableSet, error) {
|
|
configTable, sets, err := t.fulltextTableSets(ctx, workingRoot)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
// truncate each of the fulltext tables in each set before returning them
|
|
_, insertCols, err := fulltext.GetKeyColumns(ctx, t)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
newSets := make([]fulltext.TableSet, len(sets))
|
|
for i := range sets {
|
|
set := sets[i]
|
|
|
|
positionSch, err := fulltext.NewSchema(fulltext.SchemaPosition, insertCols, set.Position.Name(), t.Collation())
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
posTableDolt, posTable, err := emptyFulltextTable(ctx, t, workingRoot, set.Position, positionSch)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
docCountSch, err := fulltext.NewSchema(fulltext.SchemaDocCount, insertCols, set.DocCount.Name(), t.Collation())
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
dcTableDolt, dcTable, err := emptyFulltextTable(ctx, t, workingRoot, set.DocCount, docCountSch)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
globalCountSch, err := fulltext.NewSchema(fulltext.SchemaGlobalCount, nil, set.GlobalCount.Name(), t.Collation())
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
gcTableDolt, gcTable, err := emptyFulltextTable(ctx, t, workingRoot, set.GlobalCount, globalCountSch)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
rowCountSch, err := fulltext.NewSchema(fulltext.SchemaRowCount, nil, set.RowCount.Name(), t.Collation())
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
rcTableDolt, rcTable, err := emptyFulltextTable(ctx, t, workingRoot, set.RowCount, rowCountSch)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
set.Position = posTable
|
|
set.DocCount = dcTable
|
|
set.GlobalCount = gcTable
|
|
set.RowCount = rcTable
|
|
|
|
workingRoot, err = workingRoot.PutTable(ctx, doltdb.TableName{Name: posTable.Name(), Schema: t.db.schemaName}, posTableDolt)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
workingRoot, err = workingRoot.PutTable(ctx, doltdb.TableName{Name: dcTable.Name(), Schema: t.db.schemaName}, dcTableDolt)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
workingRoot, err = workingRoot.PutTable(ctx, doltdb.TableName{Name: gcTable.Name(), Schema: t.db.schemaName}, gcTableDolt)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
workingRoot, err = workingRoot.PutTable(ctx, doltdb.TableName{Name: rcTable.Name(), Schema: t.db.schemaName}, rcTableDolt)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
newSets[i] = set
|
|
}
|
|
|
|
return workingRoot, configTable, newSets, nil
|
|
}
|
|
|
|
// emptyFulltextTable returns a new empty fulltext table with the given schema, and the underlying dolt table.
|
|
func emptyFulltextTable(
|
|
ctx *sql.Context,
|
|
parentTable *WritableDoltTable,
|
|
workingRoot doltdb.RootValue,
|
|
fulltextTable fulltext.EditableTable,
|
|
fulltextSch sql.Schema,
|
|
) (*doltdb.Table, fulltext.EditableTable, error) {
|
|
doltTable, ok := fulltextTable.(*AlterableDoltTable)
|
|
if !ok {
|
|
return nil, nil, fmt.Errorf("unexpected row count table type: %T", fulltextTable)
|
|
}
|
|
|
|
// TODO: this should be the head root, not working root
|
|
doltSchema, err := sqlutil.ToDoltSchema(ctx, workingRoot, doltTable.TableName(), sql.NewPrimaryKeySchema(fulltextSch), workingRoot, parentTable.Collation())
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
dt, err := doltTable.DoltTable.DoltTable(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
empty, err := durable.NewEmptyPrimaryIndex(ctx, dt.ValueReadWriter(), dt.NodeStore(), doltSchema)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
dt, err = doltdb.NewTable(ctx, dt.ValueReadWriter(), dt.NodeStore(), doltSchema, empty, nil, nil)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
newTable, err := parentTable.db.newDoltTable(ctx, fulltextTable.Name(), doltSchema, dt)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return dt, newTable.(fulltext.EditableTable), nil
|
|
}
|
|
|
|
// Deleter implements sql.DeletableTable
|
|
func (t *WritableDoltTable) Deleter(ctx *sql.Context) sql.RowDeleter {
|
|
if err := dsess.CheckAccessForDb(ctx, t.db, branch_control.Permissions_Write); err != nil {
|
|
return sqlutil.NewStaticErrorEditor(err)
|
|
}
|
|
te, err := t.getTableEditor(ctx)
|
|
if err != nil {
|
|
return sqlutil.NewStaticErrorEditor(err)
|
|
}
|
|
return te
|
|
}
|
|
|
|
// Replacer implements sql.ReplaceableTable
|
|
func (t *WritableDoltTable) Replacer(ctx *sql.Context) sql.RowReplacer {
|
|
if err := dsess.CheckAccessForDb(ctx, t.db, branch_control.Permissions_Write); err != nil {
|
|
return sqlutil.NewStaticErrorEditor(err)
|
|
}
|
|
te, err := t.getTableEditor(ctx)
|
|
if err != nil {
|
|
return sqlutil.NewStaticErrorEditor(err)
|
|
}
|
|
return te
|
|
}
|
|
|
|
// Truncate implements sql.TruncateableTable
|
|
func (t *WritableDoltTable) Truncate(ctx *sql.Context) (int, error) {
|
|
if err := dsess.CheckAccessForDb(ctx, t.db, branch_control.Permissions_Write); err != nil {
|
|
return 0, err
|
|
}
|
|
table, err := t.DoltTable.DoltTable(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
sch, err := table.GetSchema(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
rowData, err := table.GetRowData(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
c, err := rowData.Count()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
numOfRows := int(c)
|
|
|
|
sess := dsess.DSessFromSess(ctx.Session)
|
|
newTable, err := t.truncate(ctx, table, sch, sess)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
root, err := t.getRoot(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
newRoot, err := root.PutTable(ctx, t.TableName(), newTable)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
err = t.setRoot(ctx, newRoot)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return numOfRows, nil
|
|
}
|
|
|
|
// truncate returns an empty copy of the table given by setting the rows and indexes to empty. The schema can be
|
|
// updated at the same time.
|
|
func (t *WritableDoltTable) truncate(
|
|
ctx *sql.Context,
|
|
table *doltdb.Table,
|
|
sch schema.Schema,
|
|
sess *dsess.DoltSession,
|
|
) (*doltdb.Table, error) {
|
|
idxSet, err := table.GetIndexSet(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, idx := range sch.Indexes().AllIndexes() {
|
|
empty, err := durable.NewEmptyIndexFromTableSchema(ctx, table.ValueReadWriter(), table.NodeStore(), idx, sch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
idxSet, err = idxSet.PutIndex(ctx, idx.Name(), empty)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
ws, err := sess.WorkingSet(ctx, t.db.RevisionQualifiedName())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if schema.HasAutoIncrement(sch) {
|
|
ddb, _ := sess.GetDoltDB(ctx, t.db.RevisionQualifiedName())
|
|
err = t.db.removeTableFromAutoIncrementTracker(ctx, t.Name(), ddb, ws.Ref())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
empty, err := durable.NewEmptyPrimaryIndex(ctx, table.ValueReadWriter(), table.NodeStore(), sch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// truncate table resets auto-increment value
|
|
newEmptyTable, err := doltdb.NewTable(ctx, table.ValueReadWriter(), table.NodeStore(), sch, empty, idxSet, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
newEmptyTable, err = copyConstraintViolationsAndConflicts(ctx, table, newEmptyTable)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newEmptyTable, nil
|
|
}
|
|
|
|
func copyConstraintViolationsAndConflicts(ctx context.Context, from, to *doltdb.Table) (*doltdb.Table, error) {
|
|
arts, err := from.GetArtifacts(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return to.SetArtifacts(ctx, arts)
|
|
}
|
|
|
|
// Updater implements sql.UpdatableTable
|
|
func (t *WritableDoltTable) Updater(ctx *sql.Context) sql.RowUpdater {
|
|
if err := dsess.CheckAccessForDb(ctx, t.db, branch_control.Permissions_Write); err != nil {
|
|
// A conflicts-table writer delegating into this source-table writer
|
|
// will set a bypass marker on the context after admitting the caller
|
|
// through its own check. Honor that marker so a merge-permission
|
|
// caller is not rejected here. The marker is keyed by (db, branch,
|
|
// table), so a marker for a different table will not match.
|
|
dbName, branch := doltdb.SplitRevisionDbName(t.db.RevisionQualifiedName())
|
|
if !dsess.ConflictsBypassFor(ctx, dbName, branch, t.TableName()) {
|
|
return sqlutil.NewStaticErrorEditor(err)
|
|
}
|
|
}
|
|
te, err := t.getTableEditor(ctx)
|
|
if err != nil {
|
|
return sqlutil.NewStaticErrorEditor(err)
|
|
}
|
|
return te
|
|
}
|
|
|
|
// AutoIncrementSetter implements sql.AutoIncrementTable
|
|
func (t *WritableDoltTable) AutoIncrementSetter(ctx *sql.Context) sql.AutoIncrementSetter {
|
|
if err := dsess.CheckAccessForDb(ctx, t.db, branch_control.Permissions_Write); err != nil {
|
|
return sqlutil.NewStaticErrorEditor(err)
|
|
}
|
|
te, err := t.getTableEditor(ctx)
|
|
if err != nil {
|
|
return sqlutil.NewStaticErrorEditor(err)
|
|
}
|
|
return te
|
|
}
|
|
|
|
// PeekNextAutoIncrementValue implements sql.AutoIncrementGetter
|
|
func (t *WritableDoltTable) PeekNextAutoIncrementValue(ctx *sql.Context) (uint64, error) {
|
|
if !t.autoIncCol.AutoIncrement {
|
|
return 0, sql.ErrNoAutoIncrementCol
|
|
}
|
|
|
|
return t.DoltTable.PeekNextAutoIncrementValue(ctx)
|
|
}
|
|
|
|
// GetNextAutoIncrementValue implements sql.AutoIncrementTable
|
|
func (t *WritableDoltTable) GetNextAutoIncrementValue(ctx *sql.Context, potentialVal interface{}) (uint64, error) {
|
|
if !t.autoIncCol.AutoIncrement {
|
|
return 0, sql.ErrNoAutoIncrementCol
|
|
}
|
|
|
|
ed, err := t.getTableEditor(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if multiTableEditor, ok := ed.(fulltext.MultiTableEditor); ok {
|
|
return multiTableEditor.PrimaryEditor().(writer.AutoIncrementGetter).GetNextAutoIncrementValue(ctx, potentialVal)
|
|
} else {
|
|
return ed.(writer.AutoIncrementGetter).GetNextAutoIncrementValue(ctx, potentialVal)
|
|
}
|
|
}
|
|
|
|
func (t *DoltTable) GetChecks(ctx *sql.Context) ([]sql.CheckDefinition, error) {
|
|
table, err := t.DoltTable(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
key, tableIsCacheable, err := t.IndexCacheKey(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !tableIsCacheable {
|
|
sch, err := table.GetSchema(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return checksInSchema(sch), nil
|
|
}
|
|
|
|
sess := dsess.DSessFromSess(ctx.Session)
|
|
dbState, ok, err := sess.LookupDbState(ctx, t.db.RevisionQualifiedName())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !ok {
|
|
return nil, fmt.Errorf("couldn't find db state for database %s", t.db.Name())
|
|
}
|
|
|
|
checks, ok := dbState.SessionCache().GetCachedTableChecks(key)
|
|
if ok {
|
|
return checks, nil
|
|
}
|
|
|
|
sch, err := table.GetSchema(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
checks = checksInSchema(sch)
|
|
|
|
dbState.SessionCache().CacheTableChecks(key, checks)
|
|
return checks, nil
|
|
}
|
|
|
|
func checksInSchema(sch schema.Schema) []sql.CheckDefinition {
|
|
if sch.Checks() == nil {
|
|
return nil
|
|
}
|
|
|
|
checks := make([]sql.CheckDefinition, sch.Checks().Count())
|
|
for i, check := range sch.Checks().AllChecks() {
|
|
checks[i] = sql.CheckDefinition{
|
|
Name: check.Name(),
|
|
CheckExpression: check.Expression(),
|
|
Enforced: check.Enforced(),
|
|
IsNotValid: check.IsNotValid(),
|
|
}
|
|
}
|
|
return checks
|
|
}
|
|
|
|
// GetForeignKeyEditor implements sql.ForeignKeyTable
|
|
func (t *WritableDoltTable) GetForeignKeyEditor(ctx *sql.Context) sql.ForeignKeyEditor {
|
|
te, err := t.getTableEditor(ctx)
|
|
if err != nil {
|
|
return sqlutil.NewStaticErrorEditor(err)
|
|
}
|
|
return te
|
|
}
|
|
|
|
// GetDeclaredForeignKeys implements sql.ForeignKeyTable
|
|
func (t *DoltTable) GetDeclaredForeignKeys(ctx *sql.Context) ([]sql.ForeignKeyConstraint, error) {
|
|
root, err := t.workingRoot(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fkc, err := root.GetForeignKeyCollection(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
declaredFks, _ := fkc.KeysForTable(t.TableName())
|
|
toReturn := make([]sql.ForeignKeyConstraint, len(declaredFks))
|
|
|
|
for i, fk := range declaredFks {
|
|
if len(fk.UnresolvedFKDetails.TableColumns) > 0 && len(fk.UnresolvedFKDetails.ReferencedTableColumns) > 0 {
|
|
// TODO: implement multi-db support for foreign keys
|
|
toReturn[i] = sql.ForeignKeyConstraint{
|
|
Name: fk.Name,
|
|
Database: t.db.Name(),
|
|
Table: fk.TableName.Name,
|
|
SchemaName: fk.TableName.Schema,
|
|
Columns: fk.UnresolvedFKDetails.TableColumns,
|
|
ParentDatabase: t.db.Name(),
|
|
ParentTable: fk.ReferencedTableName.Name,
|
|
ParentSchema: fk.ReferencedTableName.Schema,
|
|
ParentColumns: fk.UnresolvedFKDetails.ReferencedTableColumns,
|
|
OnUpdate: sqlutil.ToReferentialAction(fk.OnUpdate),
|
|
OnDelete: sqlutil.ToReferentialAction(fk.OnDelete),
|
|
IsResolved: fk.IsResolved(),
|
|
IsNotValid: fk.IsNotValid,
|
|
MatchType: sql.ForeignKeyMatchType(fk.MatchType),
|
|
}
|
|
continue
|
|
}
|
|
parent, ok, err := root.GetTable(ctx, fk.ReferencedTableName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !ok {
|
|
return nil, fmt.Errorf("cannot find table %s referenced in foreign key %s", fk.ReferencedTableName, fk.Name)
|
|
}
|
|
parentSch, err := parent.GetSchema(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
toReturn[i], err = sqlutil.ToForeignKeyConstraint(fk, t.db.Name(), t.sch, parentSch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return toReturn, nil
|
|
}
|
|
|
|
// GetReferencedForeignKeys implements sql.ForeignKeyTable
|
|
func (t *DoltTable) GetReferencedForeignKeys(ctx *sql.Context) ([]sql.ForeignKeyConstraint, error) {
|
|
root, err := t.getRoot(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return fk.GetReferencedForeignKeys(ctx, root, t.db.Name(), t.TableName(), t.sch)
|
|
}
|
|
|
|
// CreateIndexForForeignKey implements sql.ForeignKeyTable
|
|
func (t *DoltTable) CreateIndexForForeignKey(ctx *sql.Context, idx sql.IndexDef) error {
|
|
return fmt.Errorf("no foreign key operations on a read-only table")
|
|
}
|
|
|
|
// AddForeignKey implements sql.ForeignKeyTable
|
|
func (t *DoltTable) AddForeignKey(ctx *sql.Context, fk sql.ForeignKeyConstraint) error {
|
|
return fmt.Errorf("no foreign key operations on a read-only table")
|
|
}
|
|
|
|
// DropForeignKey implements sql.ForeignKeyTable
|
|
func (t *DoltTable) DropForeignKey(ctx *sql.Context, fkName string, tableName string, schemaName string) error {
|
|
return fmt.Errorf("no foreign key operations on a read-only table")
|
|
}
|
|
|
|
// UpdateForeignKey implements sql.ForeignKeyTable
|
|
func (t *DoltTable) UpdateForeignKey(ctx *sql.Context, fkName string, fk sql.ForeignKeyConstraint) error {
|
|
return fmt.Errorf("no foreign key operations on a read-only table")
|
|
}
|
|
|
|
// GetForeignKeyEditor implements sql.ForeignKeyTable
|
|
func (t *DoltTable) GetForeignKeyEditor(ctx *sql.Context) sql.ForeignKeyEditor {
|
|
return nil
|
|
}
|
|
|
|
// Projections implements sql.ProjectedTable
|
|
func (t *DoltTable) Projections() []string {
|
|
// The semantics of nil v. empty are important for this interface, they display differently in explain plans
|
|
if t.projectedCols == nil {
|
|
return nil
|
|
}
|
|
|
|
names := make([]string, len(t.projectedCols))
|
|
cols := t.sch.GetAllCols()
|
|
for i := range t.projectedCols {
|
|
col := cols.TagToCol[t.projectedCols[i]]
|
|
names[i] = col.Name
|
|
}
|
|
return names
|
|
}
|
|
|
|
func (t *DoltTable) ProjectedTags() []uint64 {
|
|
if t.projectedCols != nil {
|
|
return t.projectedCols
|
|
}
|
|
return t.sch.GetAllCols().Tags
|
|
}
|
|
|
|
// WithProjections implements sql.ProjectedTable
|
|
func (t *DoltTable) WithProjections(ctx *sql.Context, colNames []string) (sql.Table, error) {
|
|
nt := *t
|
|
|
|
if colNames == nil {
|
|
nt.projectedCols = nil
|
|
nt.projectedSchema = nil
|
|
return &nt, nil
|
|
}
|
|
|
|
// In the case of the history table, some columns may not exist, so the projected schema may be smaller than the
|
|
// requested column list in that case.
|
|
nt.projectedCols = make([]uint64, 0)
|
|
nt.projectedSchema = make(sql.Schema, 0)
|
|
sch := t.Schema(ctx)
|
|
schemaSchema := t.sch
|
|
if t.overriddenSchema != nil {
|
|
schemaSchema = t.overriddenSchema
|
|
}
|
|
cols := schemaSchema.GetAllCols()
|
|
for i := range colNames {
|
|
lowerName := strings.ToLower(colNames[i])
|
|
col, ok := cols.LowerNameToCol[lowerName]
|
|
if !ok {
|
|
// The history iter projects a new schema onto an
|
|
// older table. When a requested projection does not
|
|
// exist in the older schema, the table will ignore
|
|
// the field. The history table is responsible for
|
|
// filling the gaps with nil values.
|
|
continue
|
|
}
|
|
nt.projectedCols = append(nt.projectedCols, col.Tag)
|
|
nt.projectedSchema = append(nt.projectedSchema, sch[sch.IndexOfColName(lowerName)])
|
|
}
|
|
|
|
return &nt, nil
|
|
}
|
|
|
|
var _ sql.PartitionIter = (*doltTablePartitionIter)(nil)
|
|
|
|
// doltTablePartitionIter, an object that knows how to return the single partition exactly once.
|
|
type doltTablePartitionIter struct {
|
|
rowData durable.Index
|
|
mu *sync.Mutex
|
|
partitions []doltTablePartition
|
|
i int
|
|
}
|
|
|
|
func newDoltTablePartitionIter(rowData durable.Index, partitions ...doltTablePartition) *doltTablePartitionIter {
|
|
return &doltTablePartitionIter{i: 0, mu: &sync.Mutex{}, rowData: rowData, partitions: partitions}
|
|
}
|
|
|
|
// Close is required by the sql.PartitionIter interface. Does nothing.
|
|
func (itr *doltTablePartitionIter) Close(*sql.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// Next returns the next partition if there is one, or io.EOF if there isn't.
|
|
func (itr *doltTablePartitionIter) Next(*sql.Context) (sql.Partition, error) {
|
|
itr.mu.Lock()
|
|
defer itr.mu.Unlock()
|
|
|
|
if itr.i >= len(itr.partitions) {
|
|
return nil, io.EOF
|
|
}
|
|
|
|
partition := itr.partitions[itr.i]
|
|
itr.i++
|
|
|
|
return partition, nil
|
|
}
|
|
|
|
var _ sql.Partition = doltTablePartition{}
|
|
|
|
const NoUpperBound = math.MaxUint64
|
|
|
|
type doltTablePartition struct {
|
|
rowData durable.Index
|
|
start uint64
|
|
end uint64
|
|
}
|
|
|
|
func partitionsFromRows(ctx context.Context, rows durable.Index) ([]doltTablePartition, error) {
|
|
empty, err := rows.Empty()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if empty {
|
|
return []doltTablePartition{
|
|
{start: 0, end: 0, rowData: rows},
|
|
}, nil
|
|
}
|
|
|
|
return partitionsFromTableRows(rows)
|
|
}
|
|
|
|
func partitionsFromTableRows(rows durable.Index) ([]doltTablePartition, error) {
|
|
numElements, err := rows.Count()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
itemsPerPartition := MaxRowsPerPartition
|
|
numPartitions := (numElements / itemsPerPartition) + 1
|
|
|
|
if numPartitions < uint64(partitionMultiplier*runtime.NumCPU()) {
|
|
itemsPerPartition = numElements / uint64(partitionMultiplier*runtime.NumCPU())
|
|
if itemsPerPartition == 0 {
|
|
itemsPerPartition = numElements
|
|
numPartitions = 1
|
|
} else {
|
|
numPartitions = (numElements / itemsPerPartition) + 1
|
|
}
|
|
}
|
|
|
|
partitions := make([]doltTablePartition, numPartitions)
|
|
for i := uint64(0); i < numPartitions-1; i++ {
|
|
partitions[i] = doltTablePartition{
|
|
start: i * itemsPerPartition,
|
|
end: (i + 1) * itemsPerPartition,
|
|
rowData: rows,
|
|
}
|
|
}
|
|
|
|
partitions[numPartitions-1] = doltTablePartition{
|
|
start: (numPartitions - 1) * itemsPerPartition,
|
|
end: numElements,
|
|
rowData: rows,
|
|
}
|
|
|
|
return partitions, nil
|
|
}
|
|
|
|
// Key returns the key for this partition, which must uniquely identity the partition.
|
|
func (p doltTablePartition) Key() []byte {
|
|
return []byte(strconv.FormatUint(p.start, 10) + " >= i < " + strconv.FormatUint(p.end, 10))
|
|
}
|
|
|
|
// AlterableDoltTable allows altering the schema of the table. It implements sql.AlterableTable.
|
|
type AlterableDoltTable struct {
|
|
WritableDoltTable
|
|
}
|
|
|
|
func (t *AlterableDoltTable) PrimaryKeySchema(ctx *sql.Context) sql.PrimaryKeySchema {
|
|
if t.overriddenSchema != nil {
|
|
doltSchema, err := sqlutil.FromDoltSchema(ctx, t.db.Name(), t.tableName, t.overriddenSchema)
|
|
if err != nil {
|
|
// panic'ing isn't ideal, but this method doesn't allow returning an error.
|
|
// We could log this and return nil, but that will just cause a problem when
|
|
// the caller tries to use the value, so panic'ing seems appropriate.
|
|
panic("error converting to sql schema: " + err.Error())
|
|
}
|
|
return doltSchema
|
|
}
|
|
|
|
return t.sqlSch
|
|
}
|
|
|
|
// Internal interface for declaring the interfaces that dolt tables with an alterable schema are expected to implement
|
|
// Add new interfaces supported here, rather than in separate type assertions
|
|
type doltAlterableTableInterface interface {
|
|
sql.AlterableTable
|
|
sql.IndexAlterableTable
|
|
sql.ForeignKeyTable
|
|
sql.CheckAlterableTable
|
|
sql.PrimaryKeyAlterableTable
|
|
sql.ProjectedTable
|
|
sql.CollationAlterableTable
|
|
sql.CommentAlterableTable
|
|
sql.TargetRowSizeAlterableTable
|
|
fulltext.IndexAlterableTable
|
|
}
|
|
|
|
var _ doltAlterableTableInterface = (*AlterableDoltTable)(nil)
|
|
var _ sql.RewritableTable = (*AlterableDoltTable)(nil)
|
|
|
|
func (t *AlterableDoltTable) WithProjections(ctx *sql.Context, colNames []string) (sql.Table, error) {
|
|
wdtProj, err := t.WritableDoltTable.WithProjections(ctx, colNames)
|
|
return &AlterableDoltTable{WritableDoltTable: *wdtProj.(*WritableDoltTable)}, err
|
|
}
|
|
|
|
// AddColumn implements sql.AlterableTable
|
|
func (t *AlterableDoltTable) AddColumn(ctx *sql.Context, column *sql.Column, order *sql.ColumnOrder) error {
|
|
if err := dsess.CheckAccessForDb(ctx, t.db, branch_control.Permissions_Write); err != nil {
|
|
return err
|
|
}
|
|
root, err := t.getRoot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
table, _, err := root.GetTable(ctx, t.TableName())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ti, err := typeinfo.FromSqlType(column.Type)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tags, err := doltdb.GenerateTagsForNewColumns(ctx, root, t.TableName(), []string{column.Name}, []types.NomsKind{ti.NomsKind()}, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
col, err := sqlutil.ToDoltCol(tags[0], column)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if col.IsPartOfPK {
|
|
return errors.New("adding primary keys is not supported")
|
|
}
|
|
|
|
nullable := NotNull
|
|
if col.IsNullable() {
|
|
nullable = Null
|
|
}
|
|
|
|
updatedTable, err := addColumnToTable(ctx, root, table, t.tableName, col.Tag, col.Name, col.TypeInfo, nullable, column.Default, col.Comment, order)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if column.AutoIncrement {
|
|
ait, err := t.db.gs.AutoIncrementTracker(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ait.AddNewTable(t.tableName)
|
|
}
|
|
|
|
newRoot, err := root.PutTable(ctx, t.TableName(), updatedTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = t.setRoot(ctx, newRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return t.updateFromRoot(ctx, newRoot)
|
|
}
|
|
|
|
func (t *AlterableDoltTable) ShouldRewriteTable(
|
|
ctx *sql.Context,
|
|
oldSchema sql.PrimaryKeySchema,
|
|
newSchema sql.PrimaryKeySchema,
|
|
oldColumn *sql.Column,
|
|
newColumn *sql.Column,
|
|
) bool {
|
|
return t.columnChangeRequiresRewrite(oldColumn, newColumn) ||
|
|
orderChanged(oldSchema, newSchema, oldColumn, newColumn) ||
|
|
isColumnDrop(oldSchema, newSchema) ||
|
|
isPrimaryKeyChange(oldSchema, newSchema)
|
|
}
|
|
|
|
func orderChanged(oldSchema, newSchema sql.PrimaryKeySchema, oldColumn, newColumn *sql.Column) bool {
|
|
if oldColumn == nil || newColumn == nil {
|
|
return false
|
|
}
|
|
|
|
return oldSchema.Schema.IndexOfColName(oldColumn.Name) != newSchema.Schema.IndexOfColName(newColumn.Name)
|
|
}
|
|
|
|
func (t *AlterableDoltTable) columnChangeRequiresRewrite(oldColumn *sql.Column, newColumn *sql.Column) bool {
|
|
if oldColumn == nil || newColumn == nil {
|
|
return false
|
|
}
|
|
|
|
existingCol, _ := t.sch.GetAllCols().GetByNameCaseInsensitive(oldColumn.Name)
|
|
newCol, err := sqlutil.ToDoltCol(schema.SystemTableReservedMin, newColumn)
|
|
if err != nil {
|
|
panic(err) // should be impossible, we check compatibility before this point
|
|
}
|
|
|
|
if !existingCol.TypeInfo.Equals(newCol.TypeInfo) {
|
|
if schema.IsColSpatialType(newCol) {
|
|
// TODO: we need to do this because some spatial type changes require a full table check, but not all.
|
|
// We could narrow this check down.
|
|
return true
|
|
} else {
|
|
// This is overly broad, we could narrow this down a bit
|
|
compatibilityChecker := typecompatibility.NewTypeCompatabilityChecker()
|
|
typeChangeInfo := compatibilityChecker.IsTypeChangeCompatible(existingCol.TypeInfo, newCol.TypeInfo)
|
|
return !typeChangeInfo.Compatible || typeChangeInfo.RewriteRows || typeChangeInfo.InvalidateSecondaryIndexes
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func isColumnDrop(oldSchema sql.PrimaryKeySchema, newSchema sql.PrimaryKeySchema) bool {
|
|
return len(oldSchema.Schema) > len(newSchema.Schema)
|
|
}
|
|
|
|
func getDroppedColumn(oldSchema sql.PrimaryKeySchema, newSchema sql.PrimaryKeySchema) *sql.Column {
|
|
for _, col := range oldSchema.Schema {
|
|
if newSchema.IndexOf(col.Name, col.Source) < 0 {
|
|
return col
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isPrimaryKeyChange(oldSchema sql.PrimaryKeySchema,
|
|
newSchema sql.PrimaryKeySchema) bool {
|
|
return len(newSchema.PkOrdinals) != len(oldSchema.PkOrdinals)
|
|
}
|
|
|
|
func (t *AlterableDoltTable) RewriteInserter(
|
|
ctx *sql.Context,
|
|
oldSchema sql.PrimaryKeySchema,
|
|
newSchema sql.PrimaryKeySchema,
|
|
oldColumn *sql.Column,
|
|
newColumn *sql.Column,
|
|
idxCols []sql.IndexColumn,
|
|
) (sql.RowInserter, error) {
|
|
if _, isSet := os.LookupEnv(dconfig.EnvAssertNoTableRewrite); isSet {
|
|
return nil, fmt.Errorf("attempted to rewrite table but %s was set", dconfig.EnvAssertNoTableRewrite)
|
|
}
|
|
if err := dsess.CheckAccessForDb(ctx, t.db, branch_control.Permissions_Write); err != nil {
|
|
return nil, err
|
|
}
|
|
err := validateSchemaChange(t.Name(), oldSchema, newSchema, oldColumn, newColumn, idxCols)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sess := dsess.DSessFromSess(ctx.Session)
|
|
|
|
// Begin by creating a new table with the same name and the new schema, then removing all its existing rows
|
|
dbState, ok, err := sess.LookupDbState(ctx, t.db.RevisionQualifiedName())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !ok {
|
|
return nil, fmt.Errorf("database %s not found in session", t.db.Name())
|
|
}
|
|
|
|
ws := dbState.WorkingSet()
|
|
if ws == nil {
|
|
return nil, doltdb.ErrOperationNotSupportedInDetachedHead
|
|
}
|
|
|
|
head, err := sess.GetHeadCommit(ctx, t.db.RevisionQualifiedName())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
headRoot, err := head.GetRootValue(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dt, err := t.DoltTable.DoltTable(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
oldSch, err := dt.GetSchema(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
newSch, err := t.createSchemaForColumnChange(ctx, oldColumn, newColumn, oldSch, newSchema, ws.WorkingRoot(), headRoot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
newSch = schema.CopyChecksConstraints(oldSch, newSch)
|
|
|
|
isModifyColumn := newColumn != nil && oldColumn != nil
|
|
if isColumnDrop(oldSchema, newSchema) {
|
|
newSch, err = dropIndexesOnDroppedColumn(newSch, oldSch, oldSchema, newSchema, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else if isModifyColumn {
|
|
newSch, err = modifyIndexesForTableRewrite(ctx, oldSch, oldColumn, newColumn, newSch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
// we need a temp version of a sql.Table here to get key columns
|
|
newTbl, err := t.db.newDoltTable(ctx, t.Name(), newSch, dt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
keyCols, _, err := fulltext.GetKeyColumns(ctx, newTbl)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// this copies over all non-full-text indexes in place
|
|
newSch, err = modifyFulltextIndexesForRewrite(ctx, keyCols, oldSch, newSch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// If we have an auto increment column, we need to set it here before we begin the rewrite process (it may have changed)
|
|
err = newSch.GetAllCols().Iter(func(tag uint64, col schema.Column) (stop bool, err error) {
|
|
if col.AutoIncrement {
|
|
t.autoIncCol = col
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Grab the next auto_increment value before we call truncate, since truncate will delete the table
|
|
// and clear out the auto_increment tracking for this table.
|
|
var nextAutoIncValue uint64
|
|
if t.autoIncCol.AutoIncrement {
|
|
nextAutoIncValue, err = t.PeekNextAutoIncrementValue(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// TODO: test for this when the table is auto increment and exists on another branch
|
|
dt, err = t.truncate(ctx, dt, newSch, sess)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
newRoot, err := ws.WorkingRoot().PutTable(ctx, t.TableName(), dt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
isPrimaryKeyDrop := len(oldSchema.PkOrdinals) > 0 && len(newSchema.PkOrdinals) == 0
|
|
if isPrimaryKeyDrop {
|
|
newRoot, err = t.adjustForeignKeysForDroppedPk(ctx, t.Name(), newRoot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// We can't just call getTableEditor for this operation because it uses the session state, which we can't update
|
|
// until after the rewrite operation
|
|
if newSch.Indexes().ContainsFullTextIndex() {
|
|
return fullTextRewriteEditor(ctx, t, newSch, dt, ws, sess, dbState, newRoot)
|
|
}
|
|
|
|
// TODO: figure out locking. Other DBs automatically lock a table during this kind of operation, we should probably
|
|
// do the same. We're messing with global auto-increment values here and it's not safe.
|
|
ait, err := t.db.gs.AutoIncrementTracker(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
newWs := ws.WithWorkingRoot(newRoot)
|
|
|
|
// Restore the next auto increment value, since it was cleared when we truncated the table
|
|
if t.autoIncCol.AutoIncrement {
|
|
err = t.AutoIncrementSetter(ctx).SetAutoIncrementValue(ctx, nextAutoIncValue)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if ws := dbState.WriteSession(); ws == nil {
|
|
return nil, fmt.Errorf("cannot rebuild index on a headless branch")
|
|
}
|
|
|
|
writeSession := writer.NewWriteSession(t.db.RevisionQualifiedName(), newWs, ait, sess.SetWorkingRoot, dbState.WriteSession().GetOptions())
|
|
ed, err := writeSession.GetTableWriter(ctx, t.TableName())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ed, nil
|
|
}
|
|
|
|
func fullTextRewriteEditor(
|
|
ctx *sql.Context,
|
|
t *AlterableDoltTable,
|
|
newSch schema.Schema,
|
|
dt *doltdb.Table,
|
|
ws *doltdb.WorkingSet,
|
|
sess *dsess.DoltSession,
|
|
dbState dsess.SessionState,
|
|
workingRoot doltdb.RootValue,
|
|
) (sql.RowInserter, error) {
|
|
// We need our own write session for the rewrite operation. The connection's session must continue to return rows of
|
|
// the table as it existed before the rewrite operation began until it completes, at which point we update the
|
|
// session with the rewritten table.
|
|
if ws := dbState.WriteSession(); ws == nil {
|
|
return nil, fmt.Errorf("cannot rebuild index on read only database %s", t.Name())
|
|
}
|
|
|
|
newTable, err := t.db.newDoltTable(ctx, t.Name(), newSch, dt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
updatedRoot, configTable, tableSets, err := newTable.(*AlterableDoltTable).tableSetsForRewrite(ctx, workingRoot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
newWs := ws.WithWorkingRoot(updatedRoot)
|
|
|
|
// TODO: figure out locking. Other DBs automatically lock a table during this kind of operation, we should probably
|
|
// do the same. We're messing with global auto-increment values here and it's not safe.
|
|
ait, err := t.db.gs.AutoIncrementTracker(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// We need our own write session for the rewrite operation. The connection's session must continue to return rows of
|
|
// the table as it existed before the rewrite operation began until it completes, at which point we update the
|
|
// session with the rewritten table.
|
|
writeSession := writer.NewWriteSession(t.db.RevisionQualifiedName(), newWs, ait, sess.SetWorkingRoot, dbState.WriteSession().GetOptions())
|
|
parentEditor, err := writeSession.GetTableWriter(ctx, t.TableName())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// There's a layer of indirection here: the call to fulltext.CreateEditor is going to in turn ask each of these
|
|
// tables for an Inserter, and we need to return the one we're using to do the rewrite, not a fresh one from the
|
|
// session's data (which still has the tables as they existed before the rewrite began). To get around this,
|
|
// we manually set the writeSession in these tables before passing control back to the engine. Then in Inserter(),
|
|
// we check for a pinned writeSession and return that one, not the session one.
|
|
for i := range tableSets {
|
|
tableSets[i].Position.(*AlterableDoltTable).SetWriteSession(writeSession)
|
|
tableSets[i].DocCount.(*AlterableDoltTable).SetWriteSession(writeSession)
|
|
tableSets[i].GlobalCount.(*AlterableDoltTable).SetWriteSession(writeSession)
|
|
tableSets[i].RowCount.(*AlterableDoltTable).SetWriteSession(writeSession)
|
|
}
|
|
|
|
ftEditor, err := fulltext.CreateEditor(ctx, newTable, configTable, tableSets...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
multiEditor, err := fulltext.CreateMultiTableEditor(ctx, parentEditor, ftEditor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return multiEditor, nil
|
|
}
|
|
|
|
// modifyFulltextIndexesForRewrite modifies the fulltext indexes of a table to correspond to the new schema before
|
|
// a table rewrite. All non-full-text indexes are copied from the old schema directly.
|
|
func modifyFulltextIndexesForRewrite(ctx *sql.Context, keyCols fulltext.KeyColumns, oldSch schema.Schema, newSch schema.Schema) (schema.Schema, error) {
|
|
for _, idx := range oldSch.Indexes().AllIndexes() {
|
|
if !idx.IsFullText() {
|
|
newSch.Indexes().AddIndex(idx)
|
|
continue
|
|
}
|
|
|
|
ft := idx.FullTextProperties()
|
|
keyColPositions := make([]uint16, len(keyCols.Positions))
|
|
for i, pos := range keyCols.Positions {
|
|
keyColPositions[i] = uint16(pos)
|
|
}
|
|
|
|
ft.KeyPositions = keyColPositions
|
|
ft.KeyType = uint8(keyCols.Type)
|
|
|
|
props := schema.IndexProperties{
|
|
IsUnique: idx.IsUnique(),
|
|
IsSpatial: idx.IsSpatial(),
|
|
IsFullText: true,
|
|
IsVector: false,
|
|
IsUserDefined: true,
|
|
Comment: idx.Comment(),
|
|
FullTextProperties: ft,
|
|
}
|
|
|
|
newSch.Indexes().AddIndexByColNames(idx.Name(), idx.ColumnNames(), idx.PrefixLengths(), props)
|
|
}
|
|
|
|
return newSch, nil
|
|
}
|
|
|
|
// dropIndexesOnDroppedColumn removes from the schema any indexes which contain a dropped column.
|
|
func dropIndexesOnDroppedColumn(newSch schema.Schema, oldSch schema.Schema, oldSchema sql.PrimaryKeySchema, newSchema sql.PrimaryKeySchema, err error) (schema.Schema, error) {
|
|
newSch = schema.CopyIndexes(oldSch, newSch)
|
|
droppedCol := getDroppedColumn(oldSchema, newSchema)
|
|
for _, index := range newSch.Indexes().IndexesWithColumn(droppedCol.Name) {
|
|
_, err = newSch.Indexes().RemoveIndex(index.Name())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// For fulltext indexes, we don't just remove them entirely on a column drop, we modify them to contain only the
|
|
// remaining columns
|
|
if index.IsFullText() {
|
|
modifyFulltextIndexForColumnDrop(index, newSch, droppedCol)
|
|
}
|
|
}
|
|
|
|
return newSch, nil
|
|
}
|
|
|
|
// modifyFulltextIndexForColumnDrop modifies a fulltext index to remove a column that was dropped, adding it to the
|
|
// schema's indexes only if there are still remaining columns in the index after the drop
|
|
func modifyFulltextIndexForColumnDrop(index schema.Index, newSch schema.Schema, droppedCol *sql.Column) {
|
|
if len(index.ColumnNames()) == 1 {
|
|
// if there was only one column left in the index, we remove it entirely
|
|
return
|
|
}
|
|
|
|
var i int
|
|
colNames := make([]string, len(index.ColumnNames())-1)
|
|
for _, col := range index.ColumnNames() {
|
|
if col == droppedCol.Name {
|
|
continue
|
|
}
|
|
colNames[i] = col
|
|
i++
|
|
}
|
|
|
|
newSch.Indexes().AddIndexByColNames(
|
|
index.Name(),
|
|
colNames,
|
|
index.PrefixLengths(),
|
|
schema.IndexProperties{
|
|
IsUnique: index.IsUnique(),
|
|
IsSpatial: false,
|
|
IsFullText: true,
|
|
IsVector: false,
|
|
IsUserDefined: index.IsUserDefined(),
|
|
Comment: index.Comment(),
|
|
FullTextProperties: index.FullTextProperties(),
|
|
})
|
|
}
|
|
|
|
func modifyIndexesForTableRewrite(ctx *sql.Context, oldSch schema.Schema, oldColumn *sql.Column, newColumn *sql.Column, newSch schema.Schema) (schema.Schema, error) {
|
|
for _, index := range oldSch.Indexes().AllIndexes() {
|
|
if index.IsFullText() {
|
|
err := validateFullTextColumnChange(ctx, index, oldColumn, newColumn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
var colNames []string
|
|
prefixLengths := index.PrefixLengths()
|
|
for i, colName := range index.ColumnNames() {
|
|
if strings.EqualFold(oldColumn.Name, colName) {
|
|
colNames = append(colNames, newColumn.Name)
|
|
if len(prefixLengths) > 0 {
|
|
if !sqltypes.IsText(newColumn.Type) {
|
|
// drop prefix lengths if column is not a string type
|
|
prefixLengths[i] = 0
|
|
} else if uint32(prefixLengths[i]) > newColumn.Type.MaxTextResponseByteLength(ctx) {
|
|
// drop prefix length if prefixLength is too long
|
|
prefixLengths[i] = 0
|
|
}
|
|
}
|
|
} else {
|
|
colNames = append(colNames, colName)
|
|
}
|
|
}
|
|
|
|
// check if prefixLengths should be dropped entirely
|
|
var nonZeroPrefixLength bool
|
|
for _, prefixLength := range prefixLengths {
|
|
if prefixLength > 0 {
|
|
nonZeroPrefixLength = true
|
|
break
|
|
}
|
|
}
|
|
if !nonZeroPrefixLength {
|
|
prefixLengths = nil
|
|
}
|
|
|
|
newSch.Indexes().AddIndexByColNames(
|
|
index.Name(),
|
|
colNames,
|
|
prefixLengths,
|
|
schema.IndexProperties{
|
|
IsUnique: index.IsUnique(),
|
|
IsSpatial: index.IsSpatial(),
|
|
IsFullText: index.IsFullText(),
|
|
IsVector: index.IsVector(),
|
|
IsUserDefined: index.IsUserDefined(),
|
|
Comment: index.Comment(),
|
|
FullTextProperties: index.FullTextProperties(),
|
|
VectorProperties: index.VectorProperties(),
|
|
})
|
|
}
|
|
|
|
return newSch, nil
|
|
}
|
|
|
|
// validateFullTextColumnChange returns an error if the column change given violates this full text index.
|
|
func validateFullTextColumnChange(ctx *sql.Context, idx schema.Index, oldColumn *sql.Column, newColumn *sql.Column) error {
|
|
colNames := idx.ColumnNames()
|
|
for _, colName := range colNames {
|
|
if oldColumn.Name != colName && newColumn.Name != colName {
|
|
continue
|
|
}
|
|
if !sqltypes.IsTextOnly(newColumn.Type) {
|
|
return sql.ErrFullTextInvalidColumnType.New()
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// createSchemaForColumnChange creates a new Dolt schema based on the old Dolt schema (|oldSch|) changing to the new
|
|
// SQL schema (|newSchema|) from the SQL column |oldColumn| changing to the SQL column |newColumn|. The working root
|
|
// is provided in |root| and the branch head root is provided in |headRoot|. If any problems are encountered, a nil
|
|
// Dolt schema is returned along with an error.
|
|
func (t *AlterableDoltTable) createSchemaForColumnChange(ctx context.Context, oldColumn, newColumn *sql.Column, oldSch schema.Schema, newSchema sql.PrimaryKeySchema, root, headRoot doltdb.RootValue) (schema.Schema, error) {
|
|
// Adding or dropping a column
|
|
if oldColumn == nil || newColumn == nil {
|
|
newSch, err := sqlutil.ToDoltSchema(ctx, root, t.TableName(), newSchema, headRoot, sql.CollationID(oldSch.GetCollation()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
newSch = preserveSurvivingColumnEncodings(oldSch, newSch)
|
|
return newSch, nil
|
|
}
|
|
|
|
// Modifying a column
|
|
newSch, err := sqlutil.ToDoltSchema(ctx, root, t.TableName(), newSchema, headRoot, sql.CollationID(oldSch.GetCollation()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
oldDoltCol, ok := oldSch.GetAllCols().GetByName(oldColumn.Name)
|
|
if !ok {
|
|
return nil, fmt.Errorf("expected column %s to exist in the old schema but did not find it", oldColumn.Name)
|
|
}
|
|
|
|
newSch = preserveSurvivingColumnEncodings(oldSch, newSch)
|
|
|
|
newColCollection := replaceColumnTagInCollection(newSch.GetAllCols(), oldDoltCol.Name, oldDoltCol.Tag)
|
|
newPkColCollection := replaceColumnTagInCollection(newSch.GetPKCols(), oldDoltCol.Name, oldDoltCol.Tag)
|
|
return schema.NewSchema(newColCollection, newSch.GetPkOrdinals(), newSch.GetCollation(),
|
|
schema.NewIndexCollection(newColCollection, newPkColCollection), newSch.Checks())
|
|
}
|
|
|
|
// preserveSurvivingColumnEncodings returns a new schema based on |newSch| but with the storage encodings of any
|
|
// surviving columns preserved from |oldSch|. This is required to perform ALTER TABLE statements for that modify
|
|
// the type of a column in ways that typically don't require a full table rewrite. If we didn't preserve the
|
|
// encoding of such columns, table rewrites would be required in those cases.
|
|
func preserveSurvivingColumnEncodings(oldSch, newSch schema.Schema) schema.Schema {
|
|
if oldSch == nil || newSch == nil {
|
|
return newSch
|
|
}
|
|
cols := newSch.GetAllCols().GetColumns()
|
|
changed := false
|
|
for i := range cols {
|
|
oldCol, ok := oldSch.GetAllCols().GetByNameCaseInsensitive(cols[i].Name)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if oldCol.Kind != cols[i].Kind {
|
|
continue
|
|
}
|
|
preserved := typeinfo.PreserveAdaptiveEncoding(oldCol.TypeInfo, cols[i].TypeInfo)
|
|
if preserved == cols[i].TypeInfo {
|
|
continue
|
|
}
|
|
cols[i].TypeInfo = preserved
|
|
changed = true
|
|
}
|
|
if !changed {
|
|
return newSch
|
|
}
|
|
newColCollection := schema.NewColCollection(cols...)
|
|
pkCols := newSch.GetPKCols().GetColumns()
|
|
for i := range pkCols {
|
|
if updated, ok := newColCollection.GetByName(pkCols[i].Name); ok {
|
|
pkCols[i] = updated
|
|
}
|
|
}
|
|
newPkColCollection := schema.NewColCollection(pkCols...)
|
|
rebuilt, err := schema.NewSchema(newColCollection, newSch.GetPkOrdinals(), newSch.GetCollation(),
|
|
schema.NewIndexCollection(newColCollection, newPkColCollection), newSch.Checks())
|
|
if err != nil {
|
|
// Preserve original behaviour on the (extremely unlikely) error path: fall back
|
|
// to the not-encoding-preserved schema rather than failing the ALTER outright.
|
|
return newSch
|
|
}
|
|
return rebuilt
|
|
}
|
|
|
|
// replaceColumnTagInCollection returns a new ColCollection, based on |cc|, with the column named |name| updated
|
|
// to have the specified column tag |tag|. If the column is not found, no changes are made, no errors are returned,
|
|
// and the returned ColCollection will be identical to |cc|.
|
|
func replaceColumnTagInCollection(cc *schema.ColCollection, name string, tag uint64) *schema.ColCollection {
|
|
newColumns := cc.GetColumns()
|
|
for i := range newColumns {
|
|
if newColumns[i].Name == name {
|
|
newColumns[i].Tag = tag
|
|
break
|
|
}
|
|
}
|
|
return schema.NewColCollection(newColumns...)
|
|
}
|
|
|
|
// validateSchemaChange returns an error if the schema change given is not legal
|
|
func validateSchemaChange(
|
|
tableName string,
|
|
oldSchema sql.PrimaryKeySchema,
|
|
newSchema sql.PrimaryKeySchema,
|
|
oldColumn *sql.Column,
|
|
newColumn *sql.Column,
|
|
idxCols []sql.IndexColumn,
|
|
) error {
|
|
for _, idxCol := range idxCols {
|
|
idx := newSchema.Schema.IndexOfColName(idxCol.Name)
|
|
if idx < 0 { // avoid panics
|
|
return sql.ErrColumnNotFound.New(idxCol.Name)
|
|
}
|
|
col := newSchema.Schema[idx]
|
|
if col.PrimaryKey && idxCol.Length > 0 && sqltypes.IsText(col.Type) {
|
|
return sql.ErrUnsupportedIndexPrefix.New(col.Name)
|
|
}
|
|
}
|
|
|
|
if newColumn != nil {
|
|
newCol, err := sqlutil.ToDoltCol(schema.SystemTableReservedMin, newColumn)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if newCol.IsPartOfPK && schema.IsColSpatialType(newCol) {
|
|
return schema.ErrUsingSpatialKey.New(tableName)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (t *AlterableDoltTable) adjustForeignKeysForDroppedPk(ctx *sql.Context, tbl string, root doltdb.RootValue) (doltdb.RootValue, error) {
|
|
err := sql.ValidatePrimaryKeyDrop(ctx, t, t.PrimaryKeySchema(ctx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fkc, err := root.GetForeignKeyCollection(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fkcUpdates, err := backupFkcIndexesForPkDrop(ctx, tbl, t.sch, fkc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = fkc.UpdateIndexes(ctx, t.sch, fkcUpdates)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
root, err = root.PutForeignKeyCollection(ctx, fkc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return root, nil
|
|
}
|
|
|
|
// DropColumn implements sql.AlterableTable
|
|
func (t *AlterableDoltTable) DropColumn(*sql.Context, string) error {
|
|
return fmt.Errorf("not implemented: AlterableDoltTable.DropColumn()")
|
|
}
|
|
|
|
// ModifyColumn implements sql.AlterableTable. ModifyColumn operations are only used for operations that change only
|
|
// the schema of a table, not the data. For those operations, |RewriteInserter| is used.
|
|
func (t *AlterableDoltTable) ModifyColumn(ctx *sql.Context, columnName string, column *sql.Column, order *sql.ColumnOrder) error {
|
|
if err := dsess.CheckAccessForDb(ctx, t.db, branch_control.Permissions_Write); err != nil {
|
|
return err
|
|
}
|
|
ws, err := t.db.GetWorkingSet(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
root := ws.WorkingRoot()
|
|
|
|
table, _, err := root.GetTable(ctx, t.TableName())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sch, err := table.GetSchema(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
existingCol, ok := sch.GetAllCols().GetByNameCaseInsensitive(columnName)
|
|
if !ok {
|
|
panic(fmt.Sprintf("Column %s not found. This is a bug.", columnName))
|
|
}
|
|
|
|
col, err := sqlutil.ToDoltCol(existingCol.Tag, column)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if existingCol.Kind == col.Kind {
|
|
col.TypeInfo = typeinfo.PreserveAdaptiveEncoding(existingCol.TypeInfo, col.TypeInfo)
|
|
}
|
|
|
|
// TODO: move this logic into ShouldRewrite
|
|
if !existingCol.TypeInfo.Equals(col.TypeInfo) {
|
|
if existingCol.Kind != col.Kind {
|
|
panic("table cannot be modified in place")
|
|
}
|
|
}
|
|
|
|
updatedTable, err := modifyColumn(ctx, table, existingCol, col, order)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// For auto columns modified to be auto increment, we have more work to do
|
|
if !existingCol.AutoIncrement && col.AutoIncrement {
|
|
// TODO: delegate this to tracker?
|
|
seq, err := t.getFirstAutoIncrementValue(ctx, columnName, column.Type, updatedTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
updatedTable, err = updatedTable.SetAutoIncrementValue(ctx, seq)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ait, err := t.db.gs.AutoIncrementTracker(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO: this isn't transactional, and it should be (but none of the auto increment tracking is)
|
|
ait.AddNewTable(t.tableName)
|
|
// Since this is a new auto increment table, we don't need to exclude the current working set from consideration
|
|
// when computing its new sequence value, hence the empty ref
|
|
_, err = ait.Set(ctx, t.tableName, updatedTable, ref.WorkingSetRef{}, seq)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// If we're removing an auto inc property, we just need to update global auto increment tracking
|
|
if existingCol.AutoIncrement && !col.AutoIncrement {
|
|
// TODO: this isn't transactional, and it should be
|
|
sess := dsess.DSessFromSess(ctx.Session)
|
|
ddb, _ := sess.GetDoltDB(ctx, t.db.RevisionQualifiedName())
|
|
err = t.db.removeTableFromAutoIncrementTracker(ctx, t.Name(), ddb, ws.Ref())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
newRoot, err := root.PutTable(ctx, t.TableName(), updatedTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = t.setRoot(ctx, newRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return t.updateFromRoot(ctx, newRoot)
|
|
}
|
|
|
|
// getFirstAutoIncrementValue returns the next auto increment value for a table that just acquired one through an
|
|
// ALTER statement.
|
|
// TODO: this could use an index and avoid a full table scan in many cases
|
|
func (t *AlterableDoltTable) getFirstAutoIncrementValue(
|
|
ctx *sql.Context,
|
|
columnName string,
|
|
columnType sql.Type,
|
|
table *doltdb.Table,
|
|
) (uint64, error) {
|
|
updatedSch, err := table.GetSchema(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
rowData, err := table.GetRowData(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// Note that we aren't calling the public PartitionRows, because it always gets the table data from the session
|
|
// root, which hasn't been updated yet
|
|
rowIter, err := partitionRows(ctx, table, t.projectedCols, index.SinglePartition{RowData: rowData})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
initialValue := columnType.Zero()
|
|
colIdx := updatedSch.GetAllCols().IndexOf(columnName)
|
|
|
|
for {
|
|
r, err := rowIter.Next(ctx)
|
|
if err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
cmp, err := columnType.Compare(ctx, initialValue, r[colIdx])
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if cmp < 0 {
|
|
initialValue = r[colIdx]
|
|
}
|
|
}
|
|
|
|
seq, err := dsess.CoerceAutoIncrementValue(ctx, initialValue)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
seq++
|
|
|
|
return seq, nil
|
|
}
|
|
|
|
// hasNonZeroPrefixLength will return true if at least one of the sql.IndexColumns has a Length > 0
|
|
func hasNonZeroPrefixLength(idxCols []sql.IndexColumn) bool {
|
|
for _, idxCol := range idxCols {
|
|
if idxCol.Length > 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// allocatePrefixLengths will return a []uint16 populated with the Length field from sql.IndexColumn
|
|
// if all the lengths have a value of 0, it will return nil
|
|
func allocatePrefixLengths(idxCols []sql.IndexColumn) []uint16 {
|
|
if !hasNonZeroPrefixLength(idxCols) {
|
|
return nil
|
|
}
|
|
prefixLengths := make([]uint16, len(idxCols))
|
|
for i, idxCol := range idxCols {
|
|
prefixLengths[i] = uint16(idxCol.Length)
|
|
}
|
|
return prefixLengths
|
|
}
|
|
|
|
// CreateIndex implements sql.IndexAlterableTable
|
|
func (t *AlterableDoltTable) CreateIndex(ctx *sql.Context, idx sql.IndexDef) error {
|
|
if err := dsess.CheckAccessForDb(ctx, t.db, branch_control.Permissions_Write); err != nil {
|
|
return err
|
|
}
|
|
if idx.Constraint != sql.IndexConstraint_None && idx.Constraint != sql.IndexConstraint_Unique && idx.Constraint != sql.IndexConstraint_Spatial && idx.Constraint != sql.IndexConstraint_Vector {
|
|
return fmt.Errorf("only the following types of index constraints are supported: none, unique, spatial")
|
|
}
|
|
|
|
var vectorProperties schema.VectorProperties
|
|
if idx.Constraint == sql.IndexConstraint_Vector {
|
|
vectorProperties = schema.VectorProperties{
|
|
DistanceType: vector.DistanceL2Squared{},
|
|
}
|
|
}
|
|
return t.createIndex(ctx, idx, fulltext.KeyColumns{}, fulltext.IndexTableNames{}, vectorProperties)
|
|
}
|
|
|
|
// DropIndex implements sql.IndexAlterableTable
|
|
func (t *AlterableDoltTable) DropIndex(ctx *sql.Context, indexName string) error {
|
|
if err := dsess.CheckAccessForDb(ctx, t.db, branch_control.Permissions_Write); err != nil {
|
|
return err
|
|
}
|
|
// 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, indexName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
root, err := t.getRoot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newRoot, err := root.PutTable(ctx, t.TableName(), newTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = t.setRoot(ctx, newRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return t.updateFromRoot(ctx, newRoot)
|
|
}
|
|
|
|
// RenameIndex implements sql.IndexAlterableTable
|
|
func (t *AlterableDoltTable) RenameIndex(ctx *sql.Context, fromIndexName string, toIndexName string) error {
|
|
if err := dsess.CheckAccessForDb(ctx, t.db, branch_control.Permissions_Write); err != nil {
|
|
return err
|
|
}
|
|
// RenameIndex will error if there is a name collision or an index does not exist
|
|
_, err := t.sch.Indexes().RenameIndex(fromIndexName, toIndexName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
table, err := t.DoltTable.DoltTable(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newTable, err := table.UpdateSchema(ctx, t.sch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newTable, err = newTable.RenameIndexRowData(ctx, fromIndexName, toIndexName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
root, err := t.getRoot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newRoot, err := root.PutTable(ctx, t.TableName(), newTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = t.setRoot(ctx, newRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return t.updateFromRoot(ctx, newRoot)
|
|
}
|
|
|
|
// CreateFulltextIndex implements fulltext.IndexAlterableTable
|
|
func (t *AlterableDoltTable) CreateFulltextIndex(ctx *sql.Context, idx sql.IndexDef, keyCols fulltext.KeyColumns, tableNames fulltext.IndexTableNames) error {
|
|
if err := dsess.CheckAccessForDb(ctx, t.db, branch_control.Permissions_Write); err != nil {
|
|
return err
|
|
}
|
|
if !idx.IsFullText() {
|
|
return fmt.Errorf("attempted to create non-FullText index through FullText interface")
|
|
}
|
|
|
|
return t.createIndex(ctx, idx, keyCols, tableNames, schema.VectorProperties{})
|
|
}
|
|
|
|
// createIndex handles the common functionality between CreateIndex and CreateFulltextIndex.
|
|
func (t *AlterableDoltTable) createIndex(ctx *sql.Context, idx sql.IndexDef, keyCols fulltext.KeyColumns, tableNames fulltext.IndexTableNames, vectorProperties schema.VectorProperties) error {
|
|
columns := make([]string, len(idx.Columns))
|
|
for i, indexCol := range idx.Columns {
|
|
columns[i] = indexCol.Name
|
|
}
|
|
|
|
table, err := t.DoltTable.DoltTable(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var keyPositions []uint16
|
|
if len(keyCols.Positions) > 0 {
|
|
keyPositions = make([]uint16, len(keyCols.Positions))
|
|
for i := range keyPositions {
|
|
keyPositions[i] = uint16(keyCols.Positions[i])
|
|
}
|
|
}
|
|
|
|
var predicateStr string
|
|
if idx.Predicate != nil {
|
|
predicateStr = idx.Predicate.String()
|
|
}
|
|
|
|
idxProperties := schema.IndexProperties{
|
|
IsUnique: idx.Constraint == sql.IndexConstraint_Unique,
|
|
IsSpatial: idx.Constraint == sql.IndexConstraint_Spatial,
|
|
IsFullText: idx.Constraint == sql.IndexConstraint_Fulltext,
|
|
IsVector: idx.Constraint == sql.IndexConstraint_Vector,
|
|
IsUserDefined: true,
|
|
Comment: idx.Comment,
|
|
Predicate: predicateStr,
|
|
FullTextProperties: schema.FullTextProperties{
|
|
ConfigTable: tableNames.Config,
|
|
PositionTable: tableNames.Position,
|
|
DocCountTable: tableNames.DocCount,
|
|
GlobalCountTable: tableNames.GlobalCount,
|
|
RowCountTable: tableNames.RowCount,
|
|
KeyType: uint8(keyCols.Type),
|
|
KeyName: keyCols.Name,
|
|
KeyPositions: keyPositions,
|
|
},
|
|
VectorProperties: vectorProperties,
|
|
}
|
|
|
|
ret, err := creation.CreateIndex(ctx, table, t.Name(), idx.Name, columns, allocatePrefixLengths(idx.Columns), idxProperties, t.opts, idx.Predicate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
root, err := t.getRoot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ret.OldIndex != nil && ret.OldIndex != ret.NewIndex { // old index was replaced, so we update foreign keys
|
|
fkc, err := root.GetForeignKeyCollection(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, fk := range fkc.AllKeys() {
|
|
newFk := fk
|
|
if t.TableName() == fk.TableName && fk.TableIndex == ret.OldIndex.Name() {
|
|
newFk.TableIndex = ret.NewIndex.Name()
|
|
}
|
|
if t.TableName() == fk.ReferencedTableName && fk.ReferencedTableIndex == ret.OldIndex.Name() {
|
|
newFk.ReferencedTableIndex = ret.NewIndex.Name()
|
|
}
|
|
fkc.RemoveKeys(fk)
|
|
err = fkc.AddKeys(newFk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
root, err = root.PutForeignKeyCollection(ctx, fkc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
newRoot, err := root.PutTable(ctx, t.TableName(), ret.NewTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = t.setRoot(ctx, newRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return t.updateFromRoot(ctx, newRoot)
|
|
}
|
|
|
|
// createForeignKey creates a doltdb.ForeignKey from a sql.ForeignKeyConstraint
|
|
func (t *WritableDoltTable) createForeignKey(
|
|
ctx *sql.Context,
|
|
root doltdb.RootValue,
|
|
tbl *doltdb.Table,
|
|
sqlFk sql.ForeignKeyConstraint,
|
|
onUpdateRefAction, onDeleteRefAction doltdb.ForeignKeyReferentialAction) (doltdb.ForeignKey, error) {
|
|
|
|
if !sqlFk.IsResolved {
|
|
return doltdb.ForeignKey{
|
|
Name: sqlFk.Name,
|
|
TableName: doltdb.TableName{Name: sqlFk.Table, Schema: sqlFk.SchemaName},
|
|
TableIndex: "",
|
|
TableColumns: nil,
|
|
ReferencedTableName: doltdb.TableName{Name: sqlFk.ParentTable, Schema: sqlFk.ParentSchema},
|
|
ReferencedTableIndex: "",
|
|
ReferencedTableColumns: nil,
|
|
OnUpdate: onUpdateRefAction,
|
|
OnDelete: onDeleteRefAction,
|
|
UnresolvedFKDetails: doltdb.UnresolvedFKDetails{
|
|
TableColumns: sqlFk.Columns,
|
|
ReferencedTableColumns: sqlFk.ParentColumns,
|
|
},
|
|
}, nil
|
|
}
|
|
colTags := make([]uint64, len(sqlFk.Columns))
|
|
for i, col := range sqlFk.Columns {
|
|
tableCol, ok := t.sch.GetAllCols().GetByNameCaseInsensitive(col)
|
|
if !ok {
|
|
return doltdb.ForeignKey{}, fmt.Errorf("table `%s` does not have column `%s`", sqlFk.Table, col)
|
|
}
|
|
colTags[i] = tableCol.Tag
|
|
}
|
|
|
|
var refSch schema.Schema
|
|
if sqlFk.IsSelfReferential() {
|
|
refSch = t.sch
|
|
} else {
|
|
// NOTE: If we're creating a foreign key to a system table, we need to use a different
|
|
// API to look up the system table and find its schema.
|
|
lwrTableName := strings.ToLower(sqlFk.ParentTable)
|
|
isDoltSystemTable := strings.HasPrefix(lwrTableName, "dolt_")
|
|
if isDoltSystemTable {
|
|
// FKs referencing system tables, like dolt_branches, that are not backed by a real
|
|
// Dolt table, do not currently allow non-default referential actions. Other system
|
|
// tables, like the dolt_ci_ system tables, are backed by dolt tables and rely on
|
|
// referential actions to work properly.
|
|
if !strings.HasPrefix(strings.ToLower(sqlFk.ParentTable), "dolt_ci_") {
|
|
if sqlFk.OnDelete != sql.ForeignKeyReferentialAction_NoAction &&
|
|
sqlFk.OnDelete != sql.ForeignKeyReferentialAction_DefaultAction {
|
|
|
|
return doltdb.ForeignKey{}, fmt.Errorf(
|
|
"foreign keys referencing Dolt system tables do not support referential actions")
|
|
|
|
} else if sqlFk.OnUpdate != sql.ForeignKeyReferentialAction_NoAction &&
|
|
sqlFk.OnUpdate != sql.ForeignKeyReferentialAction_DefaultAction {
|
|
|
|
return doltdb.ForeignKey{}, fmt.Errorf(
|
|
"foreign keys referencing Dolt system tables do not support referential actions")
|
|
|
|
}
|
|
}
|
|
|
|
var ok bool
|
|
sqlRefTbl, ok, err := t.db.GetTableInsensitive(ctx, sqlFk.ParentTable)
|
|
if err != nil {
|
|
return doltdb.ForeignKey{}, err
|
|
}
|
|
if !ok {
|
|
return doltdb.ForeignKey{}, fmt.Errorf("referenced table `%s` does not exist", sqlFk.ParentTable)
|
|
}
|
|
|
|
sqlRefSch := sqlRefTbl.Schema(ctx)
|
|
pkOrdinals := make([]int, 0)
|
|
for i, col := range sqlRefSch {
|
|
if col.PrimaryKey {
|
|
pkOrdinals = append(pkOrdinals, i)
|
|
}
|
|
}
|
|
pkSch := sql.NewPrimaryKeySchema(sqlRefSch, pkOrdinals...)
|
|
|
|
refSch, err = sqlutil.ToDoltSchema(ctx, root, doltdb.TableName{Name: sqlFk.ParentTable, Schema: sqlFk.ParentSchema}, pkSch, root, t.Collation())
|
|
if err != nil {
|
|
return doltdb.ForeignKey{}, err
|
|
}
|
|
} else {
|
|
refTbl, err := t.getDoltTableForFK(ctx, root, lwrTableName, sqlFk)
|
|
if err != nil {
|
|
return doltdb.ForeignKey{}, err
|
|
}
|
|
refSch, err = refTbl.GetSchema(ctx)
|
|
if err != nil {
|
|
return doltdb.ForeignKey{}, err
|
|
}
|
|
}
|
|
}
|
|
|
|
refColTags := make([]uint64, len(sqlFk.ParentColumns))
|
|
for i, name := range sqlFk.ParentColumns {
|
|
refCol, ok := refSch.GetAllCols().GetByNameCaseInsensitive(name)
|
|
if !ok {
|
|
return doltdb.ForeignKey{}, fmt.Errorf("table `%s` does not have column `%s`", sqlFk.ParentTable, name)
|
|
}
|
|
refColTags[i] = refCol.Tag
|
|
}
|
|
|
|
var tableIndexName, refTableIndexName string
|
|
tableIndex, ok, err := FindIndexWithPrefix(t.sch, sqlFk.Columns)
|
|
if err != nil {
|
|
return doltdb.ForeignKey{}, err
|
|
}
|
|
// Use secondary index if found; otherwise it will use empty string, indicating primary key
|
|
if ok {
|
|
tableIndexName = tableIndex.Name()
|
|
}
|
|
refTableIndex, ok, err := FindIndexWithPrefix(refSch, sqlFk.ParentColumns)
|
|
if err != nil {
|
|
return doltdb.ForeignKey{}, err
|
|
}
|
|
// Use secondary index if found; otherwise it will use empty string, indicating primary key
|
|
if ok {
|
|
refTableIndexName = refTableIndex.Name()
|
|
}
|
|
|
|
return doltdb.ForeignKey{
|
|
Name: sqlFk.Name,
|
|
TableName: doltdb.TableName{Name: sqlFk.Table, Schema: t.db.SchemaName()},
|
|
TableIndex: tableIndexName,
|
|
TableColumns: colTags,
|
|
ReferencedTableName: doltdb.TableName{Name: sqlFk.ParentTable, Schema: sqlFk.ParentSchema},
|
|
ReferencedTableIndex: refTableIndexName,
|
|
ReferencedTableColumns: refColTags,
|
|
OnUpdate: onUpdateRefAction,
|
|
OnDelete: onDeleteRefAction,
|
|
UnresolvedFKDetails: doltdb.UnresolvedFKDetails{
|
|
TableColumns: sqlFk.Columns,
|
|
ReferencedTableColumns: sqlFk.ParentColumns,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (t *WritableDoltTable) getDoltTableForFK(ctx *sql.Context, root doltdb.RootValue, lwrTableName string, sqlFk sql.ForeignKeyConstraint) (refTbl *doltdb.Table, err error) {
|
|
_, refTbl, nonlocalTableExists, err := t.db.getNonlocalDoltDBTable(ctx, root, doltdb.TableName{Name: lwrTableName, Schema: sqlFk.ParentSchema})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if nonlocalTableExists {
|
|
if sqlFk.OnDelete != sql.ForeignKeyReferentialAction_NoAction &&
|
|
sqlFk.OnDelete != sql.ForeignKeyReferentialAction_DefaultAction {
|
|
return nil, fmt.Errorf(
|
|
"foreign keys referencing nonlocal tables do not support referential actions")
|
|
}
|
|
if sqlFk.OnUpdate != sql.ForeignKeyReferentialAction_NoAction &&
|
|
sqlFk.OnUpdate != sql.ForeignKeyReferentialAction_DefaultAction {
|
|
return nil, fmt.Errorf(
|
|
"foreign keys referencing nonlocal tables do not support referential actions")
|
|
}
|
|
return refTbl, nil
|
|
} else {
|
|
var ok bool
|
|
refTbl, _, ok, err = doltdb.GetTableInsensitive(ctx, root, doltdb.TableName{Name: sqlFk.ParentTable, Schema: sqlFk.ParentSchema})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !ok {
|
|
return nil, fmt.Errorf("referenced table `%s` does not exist", sqlFk.ParentTable)
|
|
}
|
|
return refTbl, nil
|
|
}
|
|
}
|
|
|
|
// AddForeignKey implements sql.ForeignKeyTable
|
|
func (t *AlterableDoltTable) AddForeignKey(ctx *sql.Context, sqlFk sql.ForeignKeyConstraint) error {
|
|
if err := dsess.CheckAccessForDb(ctx, t.db, branch_control.Permissions_Write); err != nil {
|
|
return err
|
|
}
|
|
// empty string foreign key names are replaced with a generated name elsewhere
|
|
if sqlFk.Name != "" && !doltdb.IsValidIdentifier(sqlFk.Name) {
|
|
return fmt.Errorf("invalid foreign key name `%s`", sqlFk.Name)
|
|
}
|
|
if !strings.EqualFold(sqlFk.Database, sqlFk.ParentDatabase) || !strings.EqualFold(sqlFk.Database, t.db.Name()) {
|
|
return fmt.Errorf("only foreign keys on the same database are currently supported")
|
|
}
|
|
root, err := t.getRoot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tbl, err := t.DoltTable.DoltTable(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := dsess.CheckAccessForDb(ctx, t.db, branch_control.Permissions_Write); err != nil {
|
|
return err
|
|
}
|
|
// empty string foreign key names are replaced with a generated name elsewhere
|
|
if sqlFk.Name != "" && !doltdb.IsValidIdentifier(sqlFk.Name) {
|
|
return fmt.Errorf("invalid foreign key name `%s`", sqlFk.Name)
|
|
}
|
|
|
|
if strings.ToLower(sqlFk.Database) != strings.ToLower(sqlFk.ParentDatabase) || strings.ToLower(sqlFk.Database) != strings.ToLower(t.db.Name()) {
|
|
return fmt.Errorf("only foreign keys on the same database are currently supported")
|
|
}
|
|
|
|
onUpdateRefAction, err := ParseFkReferentialAction(sqlFk.OnUpdate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
onDeleteRefAction, err := ParseFkReferentialAction(sqlFk.OnDelete)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
doltFk, err := t.createForeignKey(ctx, root, tbl, sqlFk, onUpdateRefAction, onDeleteRefAction)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
doltFk.IsNotValid = sqlFk.IsNotValid
|
|
doltFk.MatchType = doltdb.ForeignKeyMatchType(sqlFk.MatchType)
|
|
|
|
fkc, err := root.GetForeignKeyCollection(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = fkc.AddKeys(doltFk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
root, err = root.PutForeignKeyCollection(ctx, fkc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = t.setRoot(ctx, root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return t.updateFromRoot(ctx, root)
|
|
}
|
|
|
|
// DropForeignKey implements sql.ForeignKeyTable
|
|
func (t *AlterableDoltTable) DropForeignKey(ctx *sql.Context, fkName string, tableName string, schemaName string) error {
|
|
if err := dsess.CheckAccessForDb(ctx, t.db, branch_control.Permissions_Write); err != nil {
|
|
return err
|
|
}
|
|
root, err := t.getRoot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fkc, err := root.GetForeignKeyCollection(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !fkc.RemoveKeyByName(fkName, doltdb.TableName{Name: tableName, Schema: schemaName}) {
|
|
return sql.ErrForeignKeyNotFound.New(fkName, t.tableName)
|
|
}
|
|
newRoot, err := root.PutForeignKeyCollection(ctx, fkc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = t.setRoot(ctx, newRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return t.updateFromRoot(ctx, newRoot)
|
|
}
|
|
|
|
// UpdateForeignKey implements sql.ForeignKeyTable
|
|
// This interface really belongs on AlterableDoltTable (which embeds WritableDoltTable), but it's here because we
|
|
// actually have a WritableDoltTable at runtime in some cases when we want to update a foreign key. This happens in the
|
|
// case when a foreign key is created without foreign key checks on, which causes its IsResolved flag to become enabled
|
|
// the first time it is referenced in a statement with foreign keys enabled. This is kind of terrible, as means that
|
|
// an update statement (including a no-op write statement) has the side-effect of causing a schema change.
|
|
// TODO: get rid of explicit IsResolved tracking
|
|
func (t *WritableDoltTable) UpdateForeignKey(ctx *sql.Context, fkName string, sqlFk sql.ForeignKeyConstraint) error {
|
|
if err := dsess.CheckAccessForDb(ctx, t.db, branch_control.Permissions_Write); err != nil {
|
|
return err
|
|
}
|
|
root, err := t.getRoot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fkc, err := root.GetForeignKeyCollection(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO: need schema name in foreign key defn
|
|
schemaName := sqlFk.SchemaName
|
|
if schemaName == "" {
|
|
schemaName = t.db.SchemaName()
|
|
}
|
|
tblName := doltdb.TableName{Name: sqlFk.Table, Schema: schemaName}
|
|
|
|
doltFk, ok := fkc.GetByNameCaseInsensitive(fkName, tblName)
|
|
if !ok {
|
|
return sql.ErrForeignKeyNotFound.New(fkName, t.tableName)
|
|
}
|
|
fkc.RemoveKeyByName(doltFk.Name, tblName)
|
|
doltFk.Name = sqlFk.Name
|
|
doltFk.TableName = tblName
|
|
doltFk.ReferencedTableName = doltdb.TableName{Name: sqlFk.ParentTable, Schema: schemaName}
|
|
doltFk.UnresolvedFKDetails.TableColumns = sqlFk.Columns
|
|
doltFk.UnresolvedFKDetails.ReferencedTableColumns = sqlFk.ParentColumns
|
|
|
|
if !doltFk.IsResolved() || !sqlFk.IsResolved {
|
|
tbl, _, ok, err := doltdb.GetTableInsensitive(ctx, root, t.TableName())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !ok {
|
|
return sql.ErrTableNotFound.New(t.tableName)
|
|
}
|
|
doltFk, err = t.createForeignKey(ctx, root, tbl, sqlFk, doltFk.OnUpdate, doltFk.OnDelete)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
doltFk.IsNotValid = sqlFk.IsNotValid
|
|
doltFk.MatchType = doltdb.ForeignKeyMatchType(sqlFk.MatchType)
|
|
|
|
err = fkc.AddKeys(doltFk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newRoot, err := root.PutForeignKeyCollection(ctx, fkc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = t.setRoot(ctx, newRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return t.updateFromRoot(ctx, newRoot)
|
|
}
|
|
|
|
// CreateIndexForForeignKey implements sql.ForeignKeyTable
|
|
func (t *AlterableDoltTable) CreateIndexForForeignKey(ctx *sql.Context, idx sql.IndexDef) error {
|
|
if idx.Constraint != sql.IndexConstraint_None && idx.Constraint != sql.IndexConstraint_Unique && idx.Constraint != sql.IndexConstraint_Spatial {
|
|
return fmt.Errorf("only the following types of index constraints are supported for foreign keys: none, unique, spatial")
|
|
}
|
|
columns := make([]string, len(idx.Columns))
|
|
for i, indexCol := range idx.Columns {
|
|
columns[i] = indexCol.Name
|
|
}
|
|
|
|
table, err := t.DoltTable.DoltTable(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ret, err := creation.CreateIndex(ctx, table, t.Name(), idx.Name, columns, allocatePrefixLengths(idx.Columns), schema.IndexProperties{
|
|
IsUnique: idx.Constraint == sql.IndexConstraint_Unique,
|
|
IsSpatial: idx.Constraint == sql.IndexConstraint_Spatial,
|
|
IsFullText: false,
|
|
IsVector: false,
|
|
IsUserDefined: false,
|
|
Comment: "",
|
|
}, t.opts, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
root, err := t.getRoot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newRoot, err := root.PutTable(ctx, t.TableName(), ret.NewTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = t.setRoot(ctx, newRoot)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return t.updateFromRoot(ctx, newRoot)
|
|
}
|
|
|
|
func ParseFkReferentialAction(refOp sql.ForeignKeyReferentialAction) (doltdb.ForeignKeyReferentialAction, error) {
|
|
switch refOp {
|
|
case sql.ForeignKeyReferentialAction_DefaultAction:
|
|
return doltdb.ForeignKeyReferentialAction_DefaultAction, nil
|
|
case sql.ForeignKeyReferentialAction_Restrict:
|
|
return doltdb.ForeignKeyReferentialAction_Restrict, nil
|
|
case sql.ForeignKeyReferentialAction_Cascade:
|
|
return doltdb.ForeignKeyReferentialAction_Cascade, nil
|
|
case sql.ForeignKeyReferentialAction_NoAction:
|
|
return doltdb.ForeignKeyReferentialAction_NoAction, nil
|
|
case sql.ForeignKeyReferentialAction_SetNull:
|
|
return doltdb.ForeignKeyReferentialAction_SetNull, nil
|
|
case sql.ForeignKeyReferentialAction_SetDefault:
|
|
return doltdb.ForeignKeyReferentialAction_SetDefault, nil
|
|
default:
|
|
return doltdb.ForeignKeyReferentialAction_DefaultAction, fmt.Errorf("unknown foreign key referential action: %v", refOp)
|
|
}
|
|
}
|
|
|
|
// dropIndex drops the given index on the given table with the given schema. Returns the updated table and updated schema.
|
|
func (t *AlterableDoltTable) dropIndex(ctx *sql.Context, indexName string) (*doltdb.Table, schema.Schema, error) {
|
|
// RemoveIndex returns an error if the index does not exist, no need to do twice
|
|
oldIdx, err := t.sch.Indexes().RemoveIndex(indexName)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// any foreign keys that used this underlying index need to find another one
|
|
root, err := t.getRoot(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
fkc, err := root.GetForeignKeyCollection(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
for _, fk := range fkc.AllKeys() {
|
|
if fk.ReferencedTableIndex != oldIdx.Name() {
|
|
continue
|
|
}
|
|
// get column names from tags in foreign key
|
|
fkParentCols := make([]string, len(fk.ReferencedTableColumns))
|
|
for i, colTag := range fk.ReferencedTableColumns {
|
|
col, _ := oldIdx.GetColumn(colTag)
|
|
fkParentCols[i] = col.Name
|
|
}
|
|
newIdx, ok, err := FindIndexWithPrefix(t.sch, fkParentCols)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
newFk := fk
|
|
if ok {
|
|
newFk.ReferencedTableIndex = newIdx.Name()
|
|
} else {
|
|
// if a replacement index wasn't found; it matched on primary key, so use empty string
|
|
newFk.ReferencedTableIndex = ""
|
|
}
|
|
fkc.RemoveKeys(fk)
|
|
err = fkc.AddKeys(newFk)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
root, err = root.PutForeignKeyCollection(ctx, fkc)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
err = t.setRoot(ctx, root)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
table, err := t.DoltTable.DoltTable(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
newTable, err := table.UpdateSchema(ctx, t.sch)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
newTable, err = newTable.DeleteIndexRowData(ctx, indexName)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
tblSch, err := newTable.GetSchema(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return newTable, tblSch, nil
|
|
}
|
|
|
|
// updateFromRoot updates the table using data and schema in the root given. This is necessary for some schema change
|
|
// statements that take place in multiple steps (e.g. adding a foreign key may create an index, then add a constraint).
|
|
// We can't update the session's working set until the statement boundary, so we have to do it here.
|
|
// TODO: eliminate this pattern, store all table data and schema in the session rather than in these objects.
|
|
func (t *WritableDoltTable) updateFromRoot(ctx *sql.Context, root doltdb.RootValue) error {
|
|
updatedTableSql, ok, err := t.db.getTable(ctx, root, t.tableName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !ok {
|
|
return fmt.Errorf("table `%s` cannot find itself", t.tableName)
|
|
}
|
|
var updatedTable *AlterableDoltTable
|
|
if doltdb.IsSystemTable(t.TableName()) && !doltdb.IsReadOnlySystemTable(t.TableName()) && !doltdb.IsDoltCITable(t.tableName) {
|
|
updatedTable = &AlterableDoltTable{*updatedTableSql.(*WritableDoltTable)}
|
|
} else {
|
|
updatedTable = updatedTableSql.(*AlterableDoltTable)
|
|
}
|
|
t.DoltTable = updatedTable.WritableDoltTable.DoltTable
|
|
|
|
// When we update this table we need to also clear any cached versions of the object, since they may now have
|
|
// incorrect schema information
|
|
sess := dsess.DSessFromSess(ctx.Session)
|
|
dbState, ok, err := sess.LookupDbState(ctx, t.db.RevisionQualifiedName())
|
|
if !ok {
|
|
return fmt.Errorf("no db state found for %s", t.db.RevisionQualifiedName())
|
|
}
|
|
|
|
dbState.SessionCache().ClearTableCache()
|
|
|
|
return nil
|
|
}
|
|
|
|
// CreateCheck implements sql.CheckAlterableTable
|
|
func (t *AlterableDoltTable) CreateCheck(ctx *sql.Context, check *sql.CheckDefinition) error {
|
|
root, sch, err := t.getWritableSchema(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
check = &(*check)
|
|
if check.Name == "" {
|
|
var err error
|
|
check.Name, err = t.generateCheckName(ctx, check)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
_, err = sch.Checks().AddCheck(check.Name, check.CheckExpression, check.Enforced, check.IsNotValid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return t.updateFromSchema(ctx, root, sch)
|
|
}
|
|
|
|
// DropCheck implements sql.CheckAlterableTable
|
|
func (t *AlterableDoltTable) DropCheck(ctx *sql.Context, chName string) error {
|
|
root, sch, err := t.getWritableSchema(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = sch.Checks().DropCheck(chName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return t.updateFromSchema(ctx, root, sch)
|
|
}
|
|
|
|
// ModifyStoredCollation implements sql.CollationAlterableTable
|
|
func (t *AlterableDoltTable) ModifyStoredCollation(ctx *sql.Context, collation sql.CollationID) error {
|
|
return fmt.Errorf("converting the collations of columns is not yet supported")
|
|
}
|
|
|
|
// ModifyDefaultCollation implements sql.CollationAlterableTable
|
|
func (t *AlterableDoltTable) ModifyDefaultCollation(ctx *sql.Context, collation sql.CollationID) error {
|
|
root, sch, err := t.getWritableSchema(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sch.SetCollation(schema.Collation(collation))
|
|
|
|
return t.updateFromSchema(ctx, root, sch)
|
|
}
|
|
|
|
// ModifyComment implements sql.CommentAlterableTable
|
|
func (t *AlterableDoltTable) ModifyComment(ctx *sql.Context, comment string) error {
|
|
root, sch, err := t.getWritableSchema(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sch.SetComment(comment)
|
|
|
|
return t.updateFromSchema(ctx, root, sch)
|
|
}
|
|
|
|
// HasTargetRowSize implements sql.TargetRowSizeTable
|
|
func (t *AlterableDoltTable) HasTargetRowSize() bool {
|
|
return t.sch.GetTargetRowSize() != val.DefaultTupleLengthTarget
|
|
}
|
|
|
|
// GetTargetRowSize implements sql.TargetRowSizeTable
|
|
func (t *AlterableDoltTable) GetTargetRowSize() uint64 {
|
|
return uint64(t.sch.GetTargetRowSize())
|
|
}
|
|
|
|
// ModifyTargetRowSize implements sql.TargetRowSizeAlterableTable
|
|
func (t *AlterableDoltTable) ModifyTargetRowSize(ctx *sql.Context, value uint64) error {
|
|
if value > uint64(math.MaxUint16) {
|
|
return fmt.Errorf("target_row_size %d exceeds maximum allowed value %d", value, uint64(math.MaxUint16))
|
|
}
|
|
root, sch, err := t.getWritableSchema(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sch.SetTargetRowSize(uint16(value))
|
|
|
|
return t.updateFromSchema(ctx, root, sch)
|
|
}
|
|
|
|
// getWritableSchema checks for write permissions and gets the root and table schema
|
|
func (t *AlterableDoltTable) getWritableSchema(ctx *sql.Context) (doltdb.RootValue, schema.Schema, error) {
|
|
if err := dsess.CheckAccessForDb(ctx, t.db, branch_control.Permissions_Write); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
root, err := t.getRoot(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
currentTable, _, err := root.GetTable(ctx, t.TableName())
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
sch, err := currentTable.GetSchema(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return root, sch, nil
|
|
}
|
|
|
|
// updateFromSchema takes a schema and makes updates to the table and root
|
|
func (t *AlterableDoltTable) updateFromSchema(ctx *sql.Context, root doltdb.RootValue, sch schema.Schema) error {
|
|
table, err := t.DoltTable.DoltTable(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newTable, err := table.UpdateSchema(ctx, sch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newRoot, err := root.PutTable(ctx, t.TableName(), newTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = t.setRoot(ctx, newRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return t.updateFromRoot(ctx, newRoot)
|
|
}
|
|
|
|
func (t *AlterableDoltTable) generateCheckName(ctx *sql.Context, check *sql.CheckDefinition) (string, error) {
|
|
var bb bytes.Buffer
|
|
bb.Write([]byte(check.CheckExpression))
|
|
hash := hash.Of(bb.Bytes())
|
|
|
|
hashedName := fmt.Sprintf("%s_chk_%s", t.tableName, hash.String()[:8])
|
|
name := hashedName
|
|
|
|
var i int
|
|
for {
|
|
exists, err := t.constraintNameExists(ctx, name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !exists {
|
|
break
|
|
}
|
|
|
|
name = fmt.Sprintf("%s_%d", hashedName, i)
|
|
i++
|
|
}
|
|
|
|
return name, nil
|
|
}
|
|
|
|
func (t *AlterableDoltTable) constraintNameExists(ctx *sql.Context, name string) (bool, error) {
|
|
keys, err := t.GetDeclaredForeignKeys(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
for _, key := range keys {
|
|
if strings.EqualFold(key.Name, name) {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
checks, err := t.GetChecks(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
for _, check := range checks {
|
|
if strings.EqualFold(check.Name, name) {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func (t *AlterableDoltTable) CreatePrimaryKey(*sql.Context, []sql.IndexColumn) error {
|
|
return fmt.Errorf("not implemented: AlterableDoltTable.CreatePrimaryKey()")
|
|
}
|
|
|
|
func (t *AlterableDoltTable) DropPrimaryKey(ctx *sql.Context) error {
|
|
return fmt.Errorf("not implemented: AlterableDoltTable.DropPrimaryKey()")
|
|
}
|
|
|
|
func (t *WritableDoltTable) SetWriteSession(session dsess.WriteSession) {
|
|
t.pinnedWriteSession = session
|
|
}
|
|
|
|
var _ dsess.CacheableDoltTable = (*DoltTable)(nil)
|
|
var _ dsess.CacheableDoltTable = (*WritableDoltTable)(nil)
|
|
var _ dsess.CacheableDoltTable = (*AlterableDoltTable)(nil)
|
|
|
|
// RebindDatabase implements dsess.CacheableDoltTable.
|
|
// Shallow-copies the table and refreshes the db-derived fields.
|
|
func (t *DoltTable) RebindDatabase(ctx *sql.Context, newDb dsess.SqlDatabase) (dsess.CacheableDoltTable, error) {
|
|
db := newDb.(Database)
|
|
sqlSch, err := sqlutil.FromDoltSchema(ctx, db.Name(), t.tableName, t.sch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cp := *t
|
|
cp.db = db
|
|
cp.opts = db.editOpts
|
|
cp.sqlSch = sqlSch
|
|
return &cp, nil
|
|
}
|
|
|
|
// RebindDatabase implements dsess.CacheableDoltTable.
|
|
func (t *WritableDoltTable) RebindDatabase(ctx *sql.Context, newDb dsess.SqlDatabase) (dsess.CacheableDoltTable, error) {
|
|
db := newDb.(Database)
|
|
inner, err := t.DoltTable.RebindDatabase(ctx, newDb)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cp := *t
|
|
cp.DoltTable = inner.(*DoltTable)
|
|
cp.db = db
|
|
return &cp, nil
|
|
}
|
|
|
|
// RebindDatabase implements dsess.CacheableDoltTable.
|
|
func (t *AlterableDoltTable) RebindDatabase(ctx *sql.Context, newDb dsess.SqlDatabase) (dsess.CacheableDoltTable, error) {
|
|
inner, err := t.WritableDoltTable.RebindDatabase(ctx, newDb)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &AlterableDoltTable{WritableDoltTable: *inner.(*WritableDoltTable)}, nil
|
|
}
|
|
|
|
func FindIndexWithPrefix(sch schema.Schema, prefixCols []string) (schema.Index, bool, error) {
|
|
type idxWithLen struct {
|
|
schema.Index
|
|
colLen int
|
|
}
|
|
|
|
prefixCols = lowercaseSlice(prefixCols)
|
|
indexes := sch.Indexes().AllIndexes()
|
|
colLen := len(prefixCols)
|
|
var indexesWithLen []idxWithLen
|
|
for _, idx := range indexes {
|
|
idxCols := lowercaseSlice(idx.ColumnNames())
|
|
if ok, prefixCount := colsAreIndexSubset(prefixCols, idxCols); ok && prefixCount == colLen {
|
|
indexesWithLen = append(indexesWithLen, idxWithLen{idx, len(idxCols)})
|
|
}
|
|
}
|
|
if len(indexesWithLen) == 0 {
|
|
return nil, false, nil
|
|
}
|
|
|
|
sort.Slice(indexesWithLen, func(i, j int) bool {
|
|
idxI := indexesWithLen[i]
|
|
idxJ := indexesWithLen[j]
|
|
if idxI.colLen == colLen && idxJ.colLen != colLen {
|
|
return true
|
|
} else if idxI.colLen != colLen && idxJ.colLen == colLen {
|
|
return false
|
|
} else if idxI.colLen != idxJ.colLen {
|
|
return idxI.colLen > idxJ.colLen
|
|
} else if idxI.IsUnique() != idxJ.IsUnique() {
|
|
// prefer unique indexes
|
|
return idxI.IsUnique() && !idxJ.IsUnique()
|
|
} else {
|
|
return idxI.Index.Name() < idxJ.Index.Name()
|
|
}
|
|
})
|
|
sortedIndexes := make([]schema.Index, len(indexesWithLen))
|
|
for i := 0; i < len(sortedIndexes); i++ {
|
|
sortedIndexes[i] = indexesWithLen[i].Index
|
|
}
|
|
return sortedIndexes[0], true, nil
|
|
}
|
|
|
|
func colsAreIndexSubset(cols, indexCols []string) (ok bool, prefixCount int) {
|
|
if len(cols) > len(indexCols) {
|
|
return false, 0
|
|
}
|
|
|
|
visitedIndexCols := make([]bool, len(indexCols))
|
|
for _, expr := range cols {
|
|
found := false
|
|
for j, indexExpr := range indexCols {
|
|
if visitedIndexCols[j] {
|
|
continue
|
|
}
|
|
if expr == indexExpr {
|
|
visitedIndexCols[j] = true
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false, 0
|
|
}
|
|
}
|
|
|
|
// This checks the length of the prefix by checking how many true booleans are encountered before the first false
|
|
for i, visitedCol := range visitedIndexCols {
|
|
if visitedCol {
|
|
continue
|
|
}
|
|
return true, i
|
|
}
|
|
|
|
return true, len(cols)
|
|
}
|
|
|
|
func lowercaseSlice(strs []string) []string {
|
|
newStrs := make([]string, len(strs))
|
|
for i, str := range strs {
|
|
newStrs[i] = strings.ToLower(str)
|
|
}
|
|
return newStrs
|
|
}
|