Files
2026-07-13 12:32:25 +08:00

332 lines
9.6 KiB
Go
Executable File

// Copyright 2025 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 pgcatalog
import (
"io"
"iter"
"github.com/dolthub/go-mysql-server/sql"
"github.com/google/btree"
)
// inMemIndexScanIter is a sql.RowIter that uses an in-memory btree index to satisfy index lookups
// on pg_catalog tables.
type inMemIndexScanIter[T any] struct {
lookup sql.IndexLookup
rangeConverter RangeConverter[T]
btreeAccess BTreeStorageAccess[T]
rowConverter rowConverter[T]
rangeIdx int
next func() (T, bool)
stop func()
}
var _ sql.RowIter = (*inMemIndexScanIter[any])(nil)
// RangeConverter knows how to convert a Range to bounds for a btree scan. The two values returned are the
// greater-than-or-equal lower bound, and the less-than upper bound for this index.
type RangeConverter[T any] interface {
getIndexScanRange(rng sql.Range, index sql.Index) (T, bool, T, bool)
}
// BTreeStorageAccess knows how to get a btree index by name. This interface needs two methods because
// unique and non-unique indexes have different types as stored in the btree package.
type BTreeStorageAccess[T any] interface {
getIndex(name string) *inMemIndexStorage[T]
}
// rowConverter converts a value of type T to a sql.Row.
type rowConverter[T any] func(T) sql.Row
// Next implements the sql.RowIter interface.
func (l *inMemIndexScanIter[T]) Next(ctx *sql.Context) (sql.Row, error) {
nextClass, err := l.nextItem()
if err != nil {
return nil, err
}
return l.rowConverter(*nextClass), nil
}
// Close implements the sql.RowIter interface.
func (l *inMemIndexScanIter[T]) Close(ctx *sql.Context) error {
if l.stop != nil {
l.stop()
}
return nil
}
// nextItem returns the next item from the index lookup, or io.EOF if there are no more items.
// Needs to return a pointer to T so that we can return nil for EOF.
func (l *inMemIndexScanIter[T]) nextItem() (*T, error) {
if l.rangeIdx >= l.lookup.Ranges.Len() {
return nil, io.EOF
}
if l.next != nil {
next, ok := l.next()
if !ok {
l.stop()
l.next = nil
l.stop = nil
l.rangeIdx++
return l.nextItem()
}
return &next, nil
}
inMemIndex := l.lookup.Index.(pgCatalogInMemIndex)
rng := l.lookup.Ranges.ToRanges()[l.rangeIdx]
gte, hasLowerBound, lt, hasUpperBound := l.rangeConverter.getIndexScanRange(rng, l.lookup.Index)
idx := l.btreeAccess.getIndex(inMemIndex.name)
if hasLowerBound && hasUpperBound {
l.next, l.stop = idx.IterRange(gte, lt)
} else if hasLowerBound {
l.next, l.stop = idx.IterGreaterThanEqual(gte)
} else if hasUpperBound {
l.next, l.stop = idx.IterLessThan(lt)
} else {
// We don't support nil lookups for this kind of index, there are never nillable elements
return nil, io.EOF
}
return l.nextItem()
}
// pgCatalogInMemIndex is an in-memory implementation of sql.Index for pg_catalog tables.
type pgCatalogInMemIndex struct {
name string
tblName string
dbName string
uniq bool
columnExprs []sql.ColumnExpressionType
}
var _ sql.Index = (*pgCatalogInMemIndex)(nil)
// ID implements the interface sql.Index.
func (p pgCatalogInMemIndex) ID() string {
return p.name
}
// Database implements the interface sql.Index.
func (p pgCatalogInMemIndex) Database() string {
return p.dbName
}
// Table implements the interface sql.Index.
func (p pgCatalogInMemIndex) Table() string {
return p.tblName
}
// Expressions implements the interface sql.Index.
func (p pgCatalogInMemIndex) Expressions() []string {
exprs := make([]string, len(p.columnExprs))
for i, expr := range p.columnExprs {
exprs[i] = expr.Expression
}
return exprs
}
// IsUnique implements the interface sql.Index.
func (p pgCatalogInMemIndex) IsUnique() bool {
return p.uniq
}
// IsSpatial implements the interface sql.Index.
func (p pgCatalogInMemIndex) IsSpatial() bool {
return false
}
// IsFullText implements the interface sql.Index.
func (p pgCatalogInMemIndex) IsFullText() bool {
return false
}
// IsVector implements the interface sql.Index.
func (p pgCatalogInMemIndex) IsVector() bool {
return false
}
// Comment implements the interface sql.Index.
func (p pgCatalogInMemIndex) Comment() string {
return ""
}
// IndexType implements the interface sql.Index.
func (p pgCatalogInMemIndex) IndexType() string {
return "BTREE"
}
// IsGenerated implements the interface sql.Index.
func (p pgCatalogInMemIndex) IsGenerated() bool {
return false
}
// ColumnExpressionTypes implements the interface sql.Index.
func (p pgCatalogInMemIndex) ColumnExpressionTypes(ctx *sql.Context) []sql.ColumnExpressionType {
return p.columnExprs
}
// CanSupport implements the interface sql.Index.
func (p pgCatalogInMemIndex) CanSupport(context *sql.Context, r ...sql.Range) bool {
return true
}
// CanSupportOrderBy implements the interface sql.Index.
func (p pgCatalogInMemIndex) CanSupportOrderBy(expr sql.Expression) bool {
return true
}
// PrefixLengths implements the interface sql.Index.
func (p pgCatalogInMemIndex) PrefixLengths() []uint16 {
return make([]uint16, len(p.columnExprs))
}
var _ sql.Index = (*pgCatalogInMemIndex)(nil)
// inMemIndexPartition is a sql.Partition that represents the single partition for an in memory index lookup.
type inMemIndexPartition struct {
idxName string
lookup sql.IndexLookup
}
var _ sql.Partition = (*inMemIndexPartition)(nil)
// Key implements the interface sql.Partition.
func (p inMemIndexPartition) Key() []byte {
return []byte(p.idxName)
}
// inMemIndexPartIter is a sql.PartitionIter that returns a single partition for an in memory index lookup.
type inMemIndexPartIter struct {
used bool
part inMemIndexPartition
}
var _ sql.PartitionIter = (*inMemIndexPartIter)(nil)
// Close implements the interface sql.PartitionIter.
func (p inMemIndexPartIter) Close(context *sql.Context) error {
return nil
}
// Next implements the interface sql.PartitionIter.
func (p *inMemIndexPartIter) Next(context *sql.Context) (sql.Partition, error) {
if p.used {
return nil, io.EOF
}
p.used = true
return p.part, nil
}
// inMemIndexStorage is an in-memory storage for an index using a btree, abstracting away the differences between
// unique and non-unique indexes.
type inMemIndexStorage[T any] struct {
uniqTree *btree.BTreeG[T]
nonUniqTree *btree.BTreeG[[]T]
}
// NewUniqueInMemIndexStorage creates a new in-memory index storage for a unique index.
func NewUniqueInMemIndexStorage[T any](lessFunc func(a, b T) bool) *inMemIndexStorage[T] {
return &inMemIndexStorage[T]{
uniqTree: btree.NewG[T](2, lessFunc),
}
}
// NewNonUniqueInMemIndexStorage creates a new in-memory index storage for a non-unique index.
func NewNonUniqueInMemIndexStorage[T any](lessFunc func(a, b []T) bool) *inMemIndexStorage[T] {
return &inMemIndexStorage[T]{
nonUniqTree: btree.NewG[[]T](2, lessFunc),
}
}
// Add adds a value to the in-memory index storage.
func (s *inMemIndexStorage[T]) Add(val T) {
if s.uniqTree != nil {
s.uniqTree.ReplaceOrInsert(val)
} else {
existing, replaced := s.nonUniqTree.ReplaceOrInsert([]T{val})
if replaced {
existing = append(existing, val)
s.nonUniqTree.ReplaceOrInsert(existing)
}
}
}
// IterRange implements an in-order iteration over the index values in the range [gte, lt). All values in the
// index in the range are sent to the channel
func (s *inMemIndexStorage[T]) IterRange(gte, lt T) (next func() (T, bool), stop func()) {
if s.uniqTree != nil {
return iter.Pull(func(yield func(T) bool) {
s.uniqTree.AscendRange(gte, lt, yield)
})
} else {
next, stop := iter.Pull(func(yield func([]T) bool) {
s.nonUniqTree.AscendRange([]T{gte}, []T{lt}, yield)
})
return s.unnestIter(next, stop)
}
}
// IterGreaterThanEqual implements an in-order iteration over the index values greater than or equal to the given value.
// All values in the index greater than or equal to the given value are sent to the channel.
func (s *inMemIndexStorage[T]) IterGreaterThanEqual(gte T) (next func() (T, bool), stop func()) {
if s.uniqTree != nil {
return iter.Pull(func(yield func(T) bool) {
s.uniqTree.AscendGreaterOrEqual(gte, yield)
})
} else {
next, stop := iter.Pull(func(yield func([]T) bool) {
s.nonUniqTree.AscendGreaterOrEqual([]T{gte}, yield)
})
return s.unnestIter(next, stop)
}
}
// IterLessThan implements an in-order iteration over the index values less than the given value.
// All values in the index less than or equal to the given value are sent to the channel.
func (s *inMemIndexStorage[T]) IterLessThan(lt T) (next func() (T, bool), stop func()) {
if s.uniqTree != nil {
return iter.Pull(func(yield func(T) bool) {
s.uniqTree.AscendLessThan(lt, yield)
})
} else {
next, stop := iter.Pull(func(yield func([]T) bool) {
s.nonUniqTree.AscendLessThan([]T{lt}, yield)
})
return s.unnestIter(next, stop)
}
}
// unnestIter takes an iterator that returns slices of T, and returns an iterator that returns individual T values.
func (s *inMemIndexStorage[T]) unnestIter(sNext func() ([]T, bool), sStop func()) (next func() (T, bool), stop func()) {
return iter.Pull(func(yield func(T) bool) {
defer sStop()
for {
items, ok := sNext()
if !ok {
return
}
for _, item := range items {
if !yield(item) {
return
}
}
}
})
}