Files
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

594 lines
17 KiB
Go

// Copyright 2020 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package schema
import (
"fmt"
"sort"
"strings"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/expression/function/vector"
)
type IndexCollection interface {
// AddIndex adds the given index, overwriting any current indexes with the same name or columns.
// It does not perform any kind of checking, and is intended for schema modifications.
AddIndex(indexes ...Index)
// AddIndexByColNames adds an index with the given name and columns (in index order).
AddIndexByColNames(indexName string, cols []string, prefixLengths []uint16, props IndexProperties) (Index, error)
// AddIndexByColTags adds an index with the given name and column tags (in index order).
AddIndexByColTags(indexName string, tags []uint64, prefixLengths []uint16, props IndexProperties) (Index, error)
// todo: this method is trash, clean up this interface
UnsafeAddIndexByColTags(indexName string, tags []uint64, prefixLengths []uint16, props IndexProperties) (Index, error)
// AllIndexes returns a slice containing all of the indexes in this collection.
AllIndexes() []Index
// Contains returns whether the given index name already exists for this table.
Contains(indexName string) bool
// Count returns the number of indexes in this collection.
Count() int
// Equals returns whether this index collection is equivalent to another. Indexes are compared by everything except
// for their name, the names of all columns, and anything relating to the parent table's primary keys.
Equals(other IndexCollection) bool
// GetByName returns the index with the given name, or nil if it does not exist.
GetByName(indexName string) Index
// GetByNameCaseInsensitive returns the index with a matching case-insensitive name, the bool return value indicates if a match was found.
GetByNameCaseInsensitive(indexName string) (Index, bool)
// GetIndexByColumnNames returns whether the collection contains an index that has this exact collection and ordering of columns.
GetIndexByColumnNames(cols ...string) (Index, bool)
// GetIndexByTags returns whether the collection contains an index that has this exact collection and ordering of columns.
// Note that if an index collection contains multiple indexes that cover the same column tags (e.g. different index
// types) then this method will return one of them, but it is not guaranteed which one and can easily result in a
// race condition.
GetIndexByTags(tags ...uint64) (Index, bool)
// GetIndexesByTags returns all indexes from this collection that cover the same columns identified by |tags|, in the
// same order specified. This method is preferred over GetIndexByTags.
GetIndexesByTags(tags ...uint64) []Index
// IndexesWithColumn returns all indexes that index the given column.
IndexesWithColumn(columnName string) []Index
// IndexesWithTag returns all indexes that index the given tag.
IndexesWithTag(tag uint64) []Index
// Iter iterated over the indexes in the collection, calling the cb function on each.
Iter(cb func(index Index) (stop bool, err error)) error
// Merge adds the given index if it does not already exist. Indexed columns are referenced by column name,
// rather than by tag number, which allows an index from a different table to be added as long as they have matching
// column names. If an index with the same name or column structure already exists, or the index contains different
// columns, then it is skipped.
Merge(indexes ...Index)
// RemoveIndex removes an index from the table metadata.
RemoveIndex(indexName string) (Index, error)
// RenameIndex renames an index in the table metadata.
RenameIndex(oldName, newName string) (Index, error)
//SetPks changes the pks or pk ordinals
SetPks([]uint64) error
// ContainsFullTextIndex returns whether the collection contains at least one Full-Text index.
ContainsFullTextIndex() bool
// Copy returns a copy of this index collection that can be modified without affecting the original.
Copy() IndexCollection
}
type IndexProperties struct {
IsUnique bool
IsSpatial bool
IsFullText bool
IsUserDefined bool
Comment string
// Predicate is the WHERE clause expression string for partial indexes.
Predicate string
FullTextProperties
IsVector bool
VectorProperties
}
type FullTextProperties struct {
ConfigTable string
PositionTable string
DocCountTable string
GlobalCountTable string
RowCountTable string
KeyType uint8
KeyName string
KeyPositions []uint16
}
type VectorProperties struct {
DistanceType vector.DistanceType
}
type indexCollectionImpl struct {
colColl *ColCollection
indexes map[string]*indexImpl
colTagToIndex map[uint64][]*indexImpl
pks []uint64
}
func NewIndexCollection(cols *ColCollection, pkCols *ColCollection) IndexCollection {
ixc := &indexCollectionImpl{
colColl: cols,
indexes: make(map[string]*indexImpl),
colTagToIndex: make(map[uint64][]*indexImpl),
}
if cols != nil {
for _, col := range cols.cols {
ixc.colTagToIndex[col.Tag] = nil
if col.IsPartOfPK {
ixc.pks = append(ixc.pks, col.Tag)
}
}
}
if pkCols != nil {
for i, col := range pkCols.cols {
ixc.pks[i] = col.Tag
}
}
return ixc
}
func (ixc *indexCollectionImpl) Copy() IndexCollection {
// no need to copy the colColl, it's immutable
nixc := &indexCollectionImpl{
colColl: ixc.colColl,
}
if ixc.pks != nil {
nixc.pks = make([]uint64, len(ixc.pks))
copy(nixc.pks, ixc.pks)
}
if ixc.indexes != nil {
nixc.indexes = make(map[string]*indexImpl, len(ixc.indexes))
for name, index := range ixc.indexes {
nixc.indexes[name] = index.copy()
}
}
if ixc.colTagToIndex != nil {
nixc.colTagToIndex = make(map[uint64][]*indexImpl, len(ixc.colTagToIndex))
for tag, indexes := range ixc.colTagToIndex {
var indexesCopy []*indexImpl
if indexes != nil {
indexesCopy = make([]*indexImpl, len(indexes))
for i, index := range indexes {
indexesCopy[i] = index.copy()
}
}
nixc.colTagToIndex[tag] = indexesCopy
}
}
return nixc
}
func (ixc *indexCollectionImpl) AddIndex(indexes ...Index) {
for _, indexInterface := range indexes {
index, ok := indexInterface.(*indexImpl)
if !ok {
panic(fmt.Errorf("unknown index type: %T", indexInterface))
}
index = index.copy()
index.indexColl = ixc
index.allTags = combineAllTags(index.tags, ixc.pks)
lowerName := strings.ToLower(index.name)
oldNamedIndex, ok := ixc.indexes[lowerName]
if ok {
ixc.removeIndex(oldNamedIndex)
}
ixc.indexes[lowerName] = index
for _, tag := range index.tags {
ixc.colTagToIndex[tag] = append(ixc.colTagToIndex[tag], index)
}
}
}
func (ixc *indexCollectionImpl) AddIndexByColNames(indexName string, cols []string, prefixLengths []uint16, props IndexProperties) (Index, error) {
tags, ok := ixc.columnNamesToTags(cols)
if !ok {
return nil, fmt.Errorf("the table does not contain at least one of the following columns: `%v`", cols)
}
return ixc.AddIndexByColTags(indexName, tags, prefixLengths, props)
}
func (ixc *indexCollectionImpl) AddIndexByColTags(indexName string, tags []uint64, prefixLengths []uint16, props IndexProperties) (Index, error) {
lowerName := strings.ToLower(indexName)
if strings.HasPrefix(lowerName, "dolt_") && !strings.HasPrefix(lowerName, "dolt_ci_") {
return nil, fmt.Errorf("indexes cannot be prefixed with `dolt_`")
}
if ixc.Contains(lowerName) {
return nil, sql.ErrDuplicateKey.New(lowerName)
}
if !ixc.tagsExist(tags...) {
return nil, fmt.Errorf("tags %v do not exist on this table", tags)
}
// TAGS: Use of tags here is safe since it's constrained to a single table
for _, tag := range tags {
// we already validated the tag exists
c, _ := ixc.colColl.GetByTag(tag)
err := validateColumnIndexable(c)
if err != nil {
return nil, err
}
}
index := &indexImpl{
indexColl: ixc,
name: indexName,
tags: tags,
allTags: combineAllTags(tags, ixc.pks),
isUnique: props.IsUnique,
isSpatial: props.IsSpatial,
isFullText: props.IsFullText,
isVector: props.IsVector,
isUserDefined: props.IsUserDefined,
comment: props.Comment,
predicate: props.Predicate,
prefixLengths: prefixLengths,
fullTextProps: props.FullTextProperties,
vectorProperties: props.VectorProperties,
}
ixc.indexes[lowerName] = index
for _, tag := range tags {
ixc.colTagToIndex[tag] = append(ixc.colTagToIndex[tag], index)
}
return index, nil
}
// validateColumnIndexable returns an error if the column given cannot be used in an index
func validateColumnIndexable(c Column) error {
return nil
}
func (ixc *indexCollectionImpl) UnsafeAddIndexByColTags(indexName string, tags []uint64, prefixLengths []uint16, props IndexProperties) (Index, error) {
index := &indexImpl{
indexColl: ixc,
name: indexName,
tags: tags,
allTags: combineAllTags(tags, ixc.pks),
isUnique: props.IsUnique,
isSpatial: props.IsSpatial,
isFullText: props.IsFullText,
isVector: props.IsVector,
isUserDefined: props.IsUserDefined,
comment: props.Comment,
prefixLengths: prefixLengths,
fullTextProps: props.FullTextProperties,
}
ixc.indexes[strings.ToLower(indexName)] = index
for _, tag := range tags {
ixc.colTagToIndex[tag] = append(ixc.colTagToIndex[tag], index)
}
return index, nil
}
func (ixc *indexCollectionImpl) AllIndexes() []Index {
indexes := make([]Index, len(ixc.indexes))
i := 0
for _, index := range ixc.indexes {
indexes[i] = index
i++
}
sort.Slice(indexes, func(i, j int) bool {
return indexes[i].Name() < indexes[j].Name()
})
return indexes
}
func (ixc *indexCollectionImpl) Contains(indexName string) bool {
_, ok := ixc.indexes[strings.ToLower(indexName)]
return ok
}
func (ixc *indexCollectionImpl) Count() int {
return len(ixc.indexes)
}
func (ixc *indexCollectionImpl) Equals(other IndexCollection) bool {
otherIxc, ok := other.(*indexCollectionImpl)
if !ok || len(ixc.indexes) != len(otherIxc.indexes) {
// if the lengths don't match then we can quickly return
return false
}
// if named indexes are all equal, then we can skip the checks which compare indexes by their tags.
nameCmp := true
for name, i1 := range ixc.indexes {
i2, ok := otherIxc.indexes[name]
if !ok {
nameCmp = false
break
}
if !i1.Equals(i2) {
nameCmp = false
break
}
}
if nameCmp {
return true
}
for _, index := range ixc.indexes {
otherIndex := otherIxc.containsColumnTagCollection(index.tags...)
if otherIndex == nil || !index.Equals(otherIndex) {
return false
}
}
return true
}
func (ixc *indexCollectionImpl) GetByName(indexName string) Index {
ix, ok := ixc.indexes[strings.ToLower(indexName)]
if ok {
return ix
}
return nil
}
func (ixc *indexCollectionImpl) GetByNameCaseInsensitive(indexName string) (Index, bool) {
for name, ix := range ixc.indexes {
if strings.EqualFold(name, indexName) {
return ix, true
}
}
return nil, false
}
func (ixc *indexCollectionImpl) hasIndexOnColumns(cols ...string) bool {
tags := make([]uint64, len(cols))
for i, col := range cols {
col, ok := ixc.colColl.NameToCol[col]
if !ok {
return false
}
tags[i] = col.Tag
}
return ixc.hasIndexOnTags(tags...)
}
func (ixc *indexCollectionImpl) GetIndexByColumnNames(cols ...string) (Index, bool) {
tags := make([]uint64, len(cols))
for i, col := range cols {
col, ok := ixc.colColl.NameToCol[col]
if !ok {
return nil, false
}
tags[i] = col.Tag
}
return ixc.GetIndexByTags(tags...)
}
func (ixc *indexCollectionImpl) GetIndexByTags(tags ...uint64) (Index, bool) {
idx := ixc.containsColumnTagCollection(tags...)
if idx == nil {
return nil, false
}
return idx, true
}
// GetIndexesByTags implements the schema.Index interface
func (ixc *indexCollectionImpl) GetIndexesByTags(tags ...uint64) []Index {
var result []Index
tagCount := len(tags)
for _, idx := range ixc.indexes {
if tagCount != len(idx.tags) {
continue
}
allMatch := true
for i, idxTag := range idx.tags {
if tags[i] != idxTag {
allMatch = false
break
}
}
if allMatch {
result = append(result, idx)
}
}
return result
}
func (ixc *indexCollectionImpl) hasIndexOnTags(tags ...uint64) bool {
_, ok := ixc.GetIndexByTags(tags...)
return ok
}
func (ixc *indexCollectionImpl) IndexesWithColumn(columnName string) []Index {
col, ok := ixc.colColl.NameToCol[columnName]
if !ok {
return nil
}
return ixc.IndexesWithTag(col.Tag)
}
func (ixc *indexCollectionImpl) IndexesWithTag(tag uint64) []Index {
indexImpls := ixc.colTagToIndex[tag]
indexes := make([]Index, len(indexImpls))
for i, idx := range indexImpls {
indexes[i] = idx
}
return indexes
}
func (ixc *indexCollectionImpl) Iter(cb func(index Index) (stop bool, err error)) error {
for _, ind := range ixc.indexes {
stop, err := cb(ind)
if err != nil {
return err
}
if stop {
break
}
}
return nil
}
func (ixc *indexCollectionImpl) Merge(indexes ...Index) {
for _, index := range indexes {
if tags, ok := ixc.columnNamesToTags(index.ColumnNames()); ok && !ixc.Contains(index.Name()) {
newIndex := &indexImpl{
name: index.Name(),
tags: tags,
indexColl: ixc,
isUnique: index.IsUnique(),
isSpatial: index.IsSpatial(),
isFullText: index.IsFullText(),
isVector: index.IsVector(),
isUserDefined: index.IsUserDefined(),
comment: index.Comment(),
prefixLengths: index.PrefixLengths(),
fullTextProps: index.FullTextProperties(),
}
ixc.AddIndex(newIndex)
}
}
}
func (ixc *indexCollectionImpl) RemoveIndex(indexName string) (Index, error) {
lowerName := strings.ToLower(indexName)
if !ixc.Contains(lowerName) {
return nil, sql.ErrIndexNotFound.New(indexName)
}
index := ixc.indexes[lowerName]
delete(ixc.indexes, lowerName)
for _, tag := range index.tags {
indexesRefThisCol := ixc.colTagToIndex[tag]
for i, comparisonIndex := range indexesRefThisCol {
if comparisonIndex == index {
ixc.colTagToIndex[tag] = append(indexesRefThisCol[:i], indexesRefThisCol[i+1:]...)
break
}
}
}
return index, nil
}
func (ixc *indexCollectionImpl) RenameIndex(oldName, newName string) (Index, error) {
lowerCaseOldName := strings.ToLower(oldName)
lowerCaseNewName := strings.ToLower(newName)
if !ixc.Contains(lowerCaseOldName) {
return nil, sql.ErrIndexNotFound.New(oldName)
}
if ixc.Contains(lowerCaseNewName) {
return nil, sql.ErrDuplicateKey.New(newName)
}
index := ixc.indexes[lowerCaseOldName]
delete(ixc.indexes, lowerCaseOldName)
index.name = newName
ixc.indexes[lowerCaseNewName] = index
return index, nil
}
func (ixc *indexCollectionImpl) columnNamesToTags(cols []string) ([]uint64, bool) {
tags := make([]uint64, len(cols))
for i, colName := range cols {
col, ok := ixc.colColl.NameToCol[colName]
if !ok {
return nil, false
}
tags[i] = col.Tag
}
return tags, true
}
func (ixc *indexCollectionImpl) containsColumnTagCollection(tags ...uint64) *indexImpl {
tagCount := len(tags)
for _, idx := range ixc.indexes {
if tagCount == len(idx.tags) {
allMatch := true
for i, idxTag := range idx.tags {
if tags[i] != idxTag {
allMatch = false
break
}
}
if allMatch {
return idx
}
}
}
return nil
}
func (ixc *indexCollectionImpl) removeIndex(index *indexImpl) {
delete(ixc.indexes, strings.ToLower(index.name))
for _, tag := range index.tags {
var newReferences []*indexImpl
for _, referencedIndex := range ixc.colTagToIndex[tag] {
if referencedIndex != index {
newReferences = append(newReferences, referencedIndex)
}
}
ixc.colTagToIndex[tag] = newReferences
}
}
func (ixc *indexCollectionImpl) tagsExist(tags ...uint64) bool {
if len(tags) == 0 {
return false
}
tagToCol := ixc.colColl.TagToCol
for _, tag := range tags {
if _, ok := tagToCol[tag]; !ok {
return false
}
}
return true
}
func (ixc *indexCollectionImpl) SetPks(tags []uint64) error {
if len(tags) != len(ixc.pks) {
return ErrInvalidPkOrdinals
}
ixc.pks = tags
return nil
}
func (ixc *indexCollectionImpl) ContainsFullTextIndex() bool {
for _, idx := range ixc.indexes {
if idx.isFullText {
return true
}
}
return false
}
// TableNameSlice returns the table names as a slice, which may be used to easily grab all of the tables using a for loop.
func (props FullTextProperties) TableNameSlice() []string {
return []string{
props.ConfigTable,
props.PositionTable,
props.DocCountTable,
props.GlobalCountTable,
props.RowCountTable,
}
}
func combineAllTags(tags []uint64, pks []uint64) []uint64 {
allTags := make([]uint64, len(tags))
_ = copy(allTags, tags)
foundCols := make(map[uint64]struct{})
for _, tag := range tags {
foundCols[tag] = struct{}{}
}
for _, pk := range pks {
if _, ok := foundCols[pk]; !ok {
allTags = append(allTags, pk)
}
}
return allTags
}