github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/schema/index_coll.go (about)

     1  // Copyright 2020 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package schema
    16  
    17  import (
    18  	"fmt"
    19  	"sort"
    20  	"strings"
    21  )
    22  
    23  type IndexCollection interface {
    24  	// AddIndex adds the given index, overwriting any current indexes with the same name or columns.
    25  	// It does not perform any kind of checking, and is intended for schema modifications.
    26  	AddIndex(indexes ...Index)
    27  	// AddIndexByColNames adds an index with the given name and columns (in index order).
    28  	AddIndexByColNames(indexName string, cols []string, prefixLengths []uint16, props IndexProperties) (Index, error)
    29  	// AddIndexByColTags adds an index with the given name and column tags (in index order).
    30  	AddIndexByColTags(indexName string, tags []uint64, prefixLengths []uint16, props IndexProperties) (Index, error)
    31  	// todo: this method is trash, clean up this interface
    32  	UnsafeAddIndexByColTags(indexName string, tags []uint64, prefixLengths []uint16, props IndexProperties) (Index, error)
    33  	// AllIndexes returns a slice containing all of the indexes in this collection.
    34  	AllIndexes() []Index
    35  	// Contains returns whether the given index name already exists for this table.
    36  	Contains(indexName string) bool
    37  	// Count returns the number of indexes in this collection.
    38  	Count() int
    39  	// Equals returns whether this index collection is equivalent to another. Indexes are compared by everything except
    40  	// for their name, the names of all columns, and anything relating to the parent table's primary keys.
    41  	Equals(other IndexCollection) bool
    42  	// GetByName returns the index with the given name, or nil if it does not exist.
    43  	GetByName(indexName string) Index
    44  	// GetByName returns the index with a matching case-insensitive name, the bool return value indicates if a match was found.
    45  	GetByNameCaseInsensitive(indexName string) (Index, bool)
    46  	// GetIndexByColumnNames returns whether the collection contains an index that has this exact collection and ordering of columns.
    47  	GetIndexByColumnNames(cols ...string) (Index, bool)
    48  	// GetIndexByTags returns whether the collection contains an index that has this exact collection and ordering of columns.
    49  	// Note that if an index collection contains multiple indexes that cover the same column tags (e.g. different index
    50  	// types) then this method will return one of them, but it is not guaranteed which one and can easily result in a
    51  	// race condition.
    52  	GetIndexByTags(tags ...uint64) (Index, bool)
    53  	// GetIndexesByTags returns all indexes from this collection that cover the same columns identified by |tags|, in the
    54  	// same order specified. This method is preferred over GetIndexByTags.
    55  	GetIndexesByTags(tags ...uint64) []Index
    56  	// IndexesWithColumn returns all indexes that index the given column.
    57  	IndexesWithColumn(columnName string) []Index
    58  	// IndexesWithTag returns all indexes that index the given tag.
    59  	IndexesWithTag(tag uint64) []Index
    60  	// Iter iterated over the indexes in the collection, calling the cb function on each.
    61  	Iter(cb func(index Index) (stop bool, err error)) error
    62  	// Merge adds the given index if it does not already exist. Indexed columns are referenced by column name,
    63  	// rather than by tag number, which allows an index from a different table to be added as long as they have matching
    64  	// column names. If an index with the same name or column structure already exists, or the index contains different
    65  	// columns, then it is skipped.
    66  	Merge(indexes ...Index)
    67  	// RemoveIndex removes an index from the table metadata.
    68  	RemoveIndex(indexName string) (Index, error)
    69  	// RenameIndex renames an index in the table metadata.
    70  	RenameIndex(oldName, newName string) (Index, error)
    71  	//SetPks changes the pks or pk ordinals
    72  	SetPks([]uint64) error
    73  	// ContainsFullTextIndex returns whether the collection contains at least one Full-Text index.
    74  	ContainsFullTextIndex() bool
    75  	// Copy returns a copy of this index collection that can be modified without affecting the original.
    76  	Copy() IndexCollection
    77  }
    78  
    79  type IndexProperties struct {
    80  	IsUnique      bool
    81  	IsSpatial     bool
    82  	IsFullText    bool
    83  	IsUserDefined bool
    84  	Comment       string
    85  	FullTextProperties
    86  }
    87  
    88  type FullTextProperties struct {
    89  	ConfigTable      string
    90  	PositionTable    string
    91  	DocCountTable    string
    92  	GlobalCountTable string
    93  	RowCountTable    string
    94  	KeyType          uint8
    95  	KeyName          string
    96  	KeyPositions     []uint16
    97  }
    98  
    99  type indexCollectionImpl struct {
   100  	colColl       *ColCollection
   101  	indexes       map[string]*indexImpl
   102  	colTagToIndex map[uint64][]*indexImpl
   103  	pks           []uint64
   104  }
   105  
   106  func NewIndexCollection(cols *ColCollection, pkCols *ColCollection) IndexCollection {
   107  	ixc := &indexCollectionImpl{
   108  		colColl:       cols,
   109  		indexes:       make(map[string]*indexImpl),
   110  		colTagToIndex: make(map[uint64][]*indexImpl),
   111  	}
   112  	if cols != nil {
   113  		for _, col := range cols.cols {
   114  			ixc.colTagToIndex[col.Tag] = nil
   115  			if col.IsPartOfPK {
   116  				ixc.pks = append(ixc.pks, col.Tag)
   117  			}
   118  		}
   119  	}
   120  	if pkCols != nil {
   121  		for i, col := range pkCols.cols {
   122  			ixc.pks[i] = col.Tag
   123  		}
   124  	}
   125  	return ixc
   126  }
   127  
   128  func (ixc indexCollectionImpl) Copy() IndexCollection {
   129  	if ixc.pks != nil {
   130  		pks := make([]uint64, len(ixc.pks))
   131  		copy(pks, ixc.pks)
   132  		ixc.pks = pks
   133  	}
   134  
   135  	if ixc.indexes != nil {
   136  		indexes := make(map[string]*indexImpl, len(ixc.indexes))
   137  		for name, index := range ixc.indexes {
   138  			indexes[name] = index.copy()
   139  		}
   140  		ixc.indexes = indexes
   141  	}
   142  
   143  	if ixc.colTagToIndex != nil {
   144  		colTagToIndex := make(map[uint64][]*indexImpl, len(ixc.colTagToIndex))
   145  		for tag, indexes := range ixc.colTagToIndex {
   146  			var indexesCopy []*indexImpl
   147  			if indexes != nil {
   148  				indexesCopy = make([]*indexImpl, len(indexes))
   149  				for i, index := range indexes {
   150  					indexesCopy[i] = index.copy()
   151  				}
   152  			}
   153  			colTagToIndex[tag] = indexesCopy
   154  		}
   155  		ixc.colTagToIndex = colTagToIndex
   156  	}
   157  
   158  	// no need to copy the colColl, it's immutable
   159  	return &ixc
   160  }
   161  
   162  func (ixc *indexCollectionImpl) AddIndex(indexes ...Index) {
   163  	for _, indexInterface := range indexes {
   164  		index, ok := indexInterface.(*indexImpl)
   165  		if !ok {
   166  			panic(fmt.Errorf("unknown index type: %T", indexInterface))
   167  		}
   168  		index = index.copy()
   169  		index.indexColl = ixc
   170  		index.allTags = combineAllTags(index.tags, ixc.pks)
   171  		lowerName := strings.ToLower(index.name)
   172  		oldNamedIndex, ok := ixc.indexes[lowerName]
   173  		if ok {
   174  			ixc.removeIndex(oldNamedIndex)
   175  		}
   176  		oldTaggedIndex := ixc.containsColumnTagCollection(index.tags...)
   177  		if oldTaggedIndex != nil {
   178  			ixc.removeIndex(oldTaggedIndex)
   179  		}
   180  		ixc.indexes[lowerName] = index
   181  		for _, tag := range index.tags {
   182  			ixc.colTagToIndex[tag] = append(ixc.colTagToIndex[tag], index)
   183  		}
   184  	}
   185  }
   186  
   187  func (ixc *indexCollectionImpl) AddIndexByColNames(indexName string, cols []string, prefixLengths []uint16, props IndexProperties) (Index, error) {
   188  	tags, ok := ixc.columnNamesToTags(cols)
   189  	if !ok {
   190  		return nil, fmt.Errorf("the table does not contain at least one of the following columns: `%v`", cols)
   191  	}
   192  	return ixc.AddIndexByColTags(indexName, tags, prefixLengths, props)
   193  }
   194  
   195  func (ixc *indexCollectionImpl) AddIndexByColTags(indexName string, tags []uint64, prefixLengths []uint16, props IndexProperties) (Index, error) {
   196  	lowerName := strings.ToLower(indexName)
   197  	if strings.HasPrefix(lowerName, "dolt_") {
   198  		return nil, fmt.Errorf("indexes cannot be prefixed with `dolt_`")
   199  	}
   200  	if ixc.Contains(lowerName) {
   201  		return nil, fmt.Errorf("`%s` already exists as an index for this table", lowerName)
   202  	}
   203  	if !ixc.tagsExist(tags...) {
   204  		return nil, fmt.Errorf("tags %v do not exist on this table", tags)
   205  	}
   206  
   207  	for _, tag := range tags {
   208  		// we already validated the tag exists
   209  		c, _ := ixc.colColl.GetByTag(tag)
   210  		err := validateColumnIndexable(c)
   211  		if err != nil {
   212  			return nil, err
   213  		}
   214  	}
   215  
   216  	index := &indexImpl{
   217  		indexColl:     ixc,
   218  		name:          indexName,
   219  		tags:          tags,
   220  		allTags:       combineAllTags(tags, ixc.pks),
   221  		isUnique:      props.IsUnique,
   222  		isSpatial:     props.IsSpatial,
   223  		isFullText:    props.IsFullText,
   224  		isUserDefined: props.IsUserDefined,
   225  		comment:       props.Comment,
   226  		prefixLengths: prefixLengths,
   227  		fullTextProps: props.FullTextProperties,
   228  	}
   229  	ixc.indexes[lowerName] = index
   230  	for _, tag := range tags {
   231  		ixc.colTagToIndex[tag] = append(ixc.colTagToIndex[tag], index)
   232  	}
   233  	return index, nil
   234  }
   235  
   236  // validateColumnIndexable returns an error if the column given cannot be used in an index
   237  func validateColumnIndexable(c Column) error {
   238  	return nil
   239  }
   240  
   241  func (ixc *indexCollectionImpl) UnsafeAddIndexByColTags(indexName string, tags []uint64, prefixLengths []uint16, props IndexProperties) (Index, error) {
   242  	index := &indexImpl{
   243  		indexColl:     ixc,
   244  		name:          indexName,
   245  		tags:          tags,
   246  		allTags:       combineAllTags(tags, ixc.pks),
   247  		isUnique:      props.IsUnique,
   248  		isSpatial:     props.IsSpatial,
   249  		isFullText:    props.IsFullText,
   250  		isUserDefined: props.IsUserDefined,
   251  		comment:       props.Comment,
   252  		prefixLengths: prefixLengths,
   253  		fullTextProps: props.FullTextProperties,
   254  	}
   255  	ixc.indexes[strings.ToLower(indexName)] = index
   256  	for _, tag := range tags {
   257  		ixc.colTagToIndex[tag] = append(ixc.colTagToIndex[tag], index)
   258  	}
   259  	return index, nil
   260  }
   261  
   262  func (ixc *indexCollectionImpl) AllIndexes() []Index {
   263  	indexes := make([]Index, len(ixc.indexes))
   264  	i := 0
   265  	for _, index := range ixc.indexes {
   266  		indexes[i] = index
   267  		i++
   268  	}
   269  	sort.Slice(indexes, func(i, j int) bool {
   270  		return indexes[i].Name() < indexes[j].Name()
   271  	})
   272  	return indexes
   273  }
   274  
   275  func (ixc *indexCollectionImpl) Contains(indexName string) bool {
   276  	_, ok := ixc.indexes[strings.ToLower(indexName)]
   277  	return ok
   278  }
   279  
   280  func (ixc *indexCollectionImpl) Count() int {
   281  	return len(ixc.indexes)
   282  }
   283  
   284  func (ixc *indexCollectionImpl) Equals(other IndexCollection) bool {
   285  	otherIxc, ok := other.(*indexCollectionImpl)
   286  	if !ok || len(ixc.indexes) != len(otherIxc.indexes) {
   287  		// if the lengths don't match then we can quickly return
   288  		return false
   289  	}
   290  	for _, index := range ixc.indexes {
   291  		otherIndex := otherIxc.containsColumnTagCollection(index.tags...)
   292  		if otherIndex == nil || !index.Equals(otherIndex) {
   293  			return false
   294  		}
   295  	}
   296  	return true
   297  }
   298  
   299  func (ixc *indexCollectionImpl) GetByName(indexName string) Index {
   300  	ix, ok := ixc.indexes[strings.ToLower(indexName)]
   301  	if ok {
   302  		return ix
   303  	}
   304  	return nil
   305  }
   306  
   307  func (ixc *indexCollectionImpl) GetByNameCaseInsensitive(indexName string) (Index, bool) {
   308  	for name, ix := range ixc.indexes {
   309  		if strings.ToLower(name) == strings.ToLower(indexName) {
   310  			return ix, true
   311  		}
   312  	}
   313  	return nil, false
   314  }
   315  
   316  func (ixc *indexCollectionImpl) hasIndexOnColumns(cols ...string) bool {
   317  	tags := make([]uint64, len(cols))
   318  	for i, col := range cols {
   319  		col, ok := ixc.colColl.NameToCol[col]
   320  		if !ok {
   321  			return false
   322  		}
   323  		tags[i] = col.Tag
   324  	}
   325  	return ixc.hasIndexOnTags(tags...)
   326  }
   327  
   328  func (ixc *indexCollectionImpl) GetIndexByColumnNames(cols ...string) (Index, bool) {
   329  	tags := make([]uint64, len(cols))
   330  	for i, col := range cols {
   331  		col, ok := ixc.colColl.NameToCol[col]
   332  		if !ok {
   333  			return nil, false
   334  		}
   335  		tags[i] = col.Tag
   336  	}
   337  	return ixc.GetIndexByTags(tags...)
   338  }
   339  
   340  func (ixc *indexCollectionImpl) GetIndexByTags(tags ...uint64) (Index, bool) {
   341  	idx := ixc.containsColumnTagCollection(tags...)
   342  	if idx == nil {
   343  		return nil, false
   344  	}
   345  	return idx, true
   346  }
   347  
   348  // GetIndexesByTags implements the schema.Index interface
   349  func (ixc *indexCollectionImpl) GetIndexesByTags(tags ...uint64) []Index {
   350  	var result []Index
   351  
   352  	tagCount := len(tags)
   353  	for _, idx := range ixc.indexes {
   354  		if tagCount != len(idx.tags) {
   355  			continue
   356  		}
   357  
   358  		allMatch := true
   359  		for i, idxTag := range idx.tags {
   360  			if tags[i] != idxTag {
   361  				allMatch = false
   362  				break
   363  			}
   364  		}
   365  		if allMatch {
   366  			result = append(result, idx)
   367  		}
   368  	}
   369  	return result
   370  }
   371  
   372  func (ixc *indexCollectionImpl) hasIndexOnTags(tags ...uint64) bool {
   373  	_, ok := ixc.GetIndexByTags(tags...)
   374  	return ok
   375  }
   376  
   377  func (ixc *indexCollectionImpl) IndexesWithColumn(columnName string) []Index {
   378  	col, ok := ixc.colColl.NameToCol[columnName]
   379  	if !ok {
   380  		return nil
   381  	}
   382  	return ixc.IndexesWithTag(col.Tag)
   383  }
   384  
   385  func (ixc *indexCollectionImpl) IndexesWithTag(tag uint64) []Index {
   386  	indexImpls := ixc.colTagToIndex[tag]
   387  	indexes := make([]Index, len(indexImpls))
   388  	for i, idx := range indexImpls {
   389  		indexes[i] = idx
   390  	}
   391  	return indexes
   392  }
   393  
   394  func (ixc *indexCollectionImpl) Iter(cb func(index Index) (stop bool, err error)) error {
   395  	for _, ind := range ixc.indexes {
   396  		stop, err := cb(ind)
   397  		if err != nil {
   398  			return err
   399  		}
   400  		if stop {
   401  			break
   402  		}
   403  	}
   404  	return nil
   405  }
   406  
   407  func (ixc *indexCollectionImpl) Merge(indexes ...Index) {
   408  	for _, index := range indexes {
   409  		if tags, ok := ixc.columnNamesToTags(index.ColumnNames()); ok && !ixc.Contains(index.Name()) {
   410  			newIndex := &indexImpl{
   411  				name:          index.Name(),
   412  				tags:          tags,
   413  				indexColl:     ixc,
   414  				isUnique:      index.IsUnique(),
   415  				isSpatial:     index.IsSpatial(),
   416  				isFullText:    index.IsFullText(),
   417  				isUserDefined: index.IsUserDefined(),
   418  				comment:       index.Comment(),
   419  				prefixLengths: index.PrefixLengths(),
   420  				fullTextProps: index.FullTextProperties(),
   421  			}
   422  			ixc.AddIndex(newIndex)
   423  		}
   424  	}
   425  }
   426  
   427  func (ixc *indexCollectionImpl) RemoveIndex(indexName string) (Index, error) {
   428  	lowerName := strings.ToLower(indexName)
   429  	if !ixc.Contains(lowerName) {
   430  		return nil, fmt.Errorf("`%s` does not exist as an index for this table", lowerName)
   431  	}
   432  	index := ixc.indexes[lowerName]
   433  	delete(ixc.indexes, lowerName)
   434  	for _, tag := range index.tags {
   435  		indexesRefThisCol := ixc.colTagToIndex[tag]
   436  		for i, comparisonIndex := range indexesRefThisCol {
   437  			if comparisonIndex == index {
   438  				ixc.colTagToIndex[tag] = append(indexesRefThisCol[:i], indexesRefThisCol[i+1:]...)
   439  				break
   440  			}
   441  		}
   442  	}
   443  	return index, nil
   444  }
   445  
   446  func (ixc *indexCollectionImpl) RenameIndex(oldName, newName string) (Index, error) {
   447  	if !ixc.Contains(oldName) {
   448  		return nil, fmt.Errorf("`%s` does not exist as an index for this table", oldName)
   449  	}
   450  	if ixc.Contains(newName) {
   451  		return nil, fmt.Errorf("`%s` already exists as an index for this table", newName)
   452  	}
   453  	index := ixc.indexes[oldName]
   454  	delete(ixc.indexes, oldName)
   455  	index.name = newName
   456  	ixc.indexes[newName] = index
   457  	return index, nil
   458  }
   459  
   460  func (ixc *indexCollectionImpl) columnNamesToTags(cols []string) ([]uint64, bool) {
   461  	tags := make([]uint64, len(cols))
   462  	for i, colName := range cols {
   463  		col, ok := ixc.colColl.NameToCol[colName]
   464  		if !ok {
   465  			return nil, false
   466  		}
   467  		tags[i] = col.Tag
   468  	}
   469  	return tags, true
   470  }
   471  
   472  func (ixc *indexCollectionImpl) containsColumnTagCollection(tags ...uint64) *indexImpl {
   473  	tagCount := len(tags)
   474  	for _, idx := range ixc.indexes {
   475  		if tagCount == len(idx.tags) {
   476  			allMatch := true
   477  			for i, idxTag := range idx.tags {
   478  				if tags[i] != idxTag {
   479  					allMatch = false
   480  					break
   481  				}
   482  			}
   483  			if allMatch {
   484  				return idx
   485  			}
   486  		}
   487  	}
   488  	return nil
   489  }
   490  
   491  func (ixc *indexCollectionImpl) removeIndex(index *indexImpl) {
   492  	delete(ixc.indexes, strings.ToLower(index.name))
   493  	for _, tag := range index.tags {
   494  		var newReferences []*indexImpl
   495  		for _, referencedIndex := range ixc.colTagToIndex[tag] {
   496  			if referencedIndex != index {
   497  				newReferences = append(newReferences, referencedIndex)
   498  			}
   499  		}
   500  		ixc.colTagToIndex[tag] = newReferences
   501  	}
   502  }
   503  
   504  func (ixc *indexCollectionImpl) tagsExist(tags ...uint64) bool {
   505  	if len(tags) == 0 {
   506  		return false
   507  	}
   508  	tagToCol := ixc.colColl.TagToCol
   509  	for _, tag := range tags {
   510  		if _, ok := tagToCol[tag]; !ok {
   511  			return false
   512  		}
   513  	}
   514  	return true
   515  }
   516  
   517  func (ixc *indexCollectionImpl) SetPks(tags []uint64) error {
   518  	if len(tags) != len(ixc.pks) {
   519  		return ErrInvalidPkOrdinals
   520  	}
   521  	ixc.pks = tags
   522  	return nil
   523  }
   524  
   525  func (ixc *indexCollectionImpl) ContainsFullTextIndex() bool {
   526  	for _, idx := range ixc.indexes {
   527  		if idx.isFullText {
   528  			return true
   529  		}
   530  	}
   531  	return false
   532  }
   533  
   534  // TableNameSlice returns the table names as a slice, which may be used to easily grab all of the tables using a for loop.
   535  func (props FullTextProperties) TableNameSlice() []string {
   536  	return []string{
   537  		props.ConfigTable,
   538  		props.PositionTable,
   539  		props.DocCountTable,
   540  		props.GlobalCountTable,
   541  		props.RowCountTable,
   542  	}
   543  }
   544  
   545  func combineAllTags(tags []uint64, pks []uint64) []uint64 {
   546  	allTags := make([]uint64, len(tags))
   547  	_ = copy(allTags, tags)
   548  	foundCols := make(map[uint64]struct{})
   549  	for _, tag := range tags {
   550  		foundCols[tag] = struct{}{}
   551  	}
   552  	for _, pk := range pks {
   553  		if _, ok := foundCols[pk]; !ok {
   554  			allTags = append(allTags, pk)
   555  		}
   556  	}
   557  	return allTags
   558  }