235 lines
7.2 KiB
Go
235 lines
7.2 KiB
Go
// 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 extensions
|
|
|
|
import (
|
|
"cmp"
|
|
"context"
|
|
"maps"
|
|
"slices"
|
|
|
|
"github.com/cockroachdb/errors"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
|
|
"github.com/dolthub/dolt/go/store/hash"
|
|
"github.com/dolthub/dolt/go/store/prolly"
|
|
"github.com/dolthub/dolt/go/store/prolly/tree"
|
|
|
|
"github.com/dolthub/doltgresql/core/id"
|
|
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
|
|
)
|
|
|
|
// Collection contains a collection of loaded extensions.
|
|
type Collection struct {
|
|
accessCache map[id.Extension]Extension // This cache is used for general access
|
|
idCache []id.Extension // This cache simply contains the name of every loaded extension
|
|
mapHash hash.Hash // This is cached so that we don't have to calculate the hash every time
|
|
underlyingMap prolly.AddressMap
|
|
ns tree.NodeStore
|
|
}
|
|
|
|
// Extension represents a loaded extension.
|
|
type Extension struct {
|
|
ExtName id.Extension
|
|
Namespace id.Namespace
|
|
Relocatable bool
|
|
LibIdentifier LibraryIdentifier
|
|
// TODO: keep track of what it references so I can later delete them
|
|
}
|
|
|
|
var _ objinterface.Collection = (*Collection)(nil)
|
|
var _ objinterface.RootObject = Extension{}
|
|
|
|
// NewCollection returns a new Collection.
|
|
func NewCollection(ctx context.Context, underlyingMap prolly.AddressMap, ns tree.NodeStore) (*Collection, error) {
|
|
collection := &Collection{
|
|
accessCache: make(map[id.Extension]Extension),
|
|
idCache: nil,
|
|
mapHash: hash.Hash{},
|
|
underlyingMap: underlyingMap,
|
|
ns: ns,
|
|
}
|
|
return collection, collection.reloadCaches(ctx)
|
|
}
|
|
|
|
// GetLoadedExtension returns the loaded extension with the given name. Returns an extension with an invalid ID if it
|
|
// cannot be found.
|
|
func (pge *Collection) GetLoadedExtension(ctx context.Context, name id.Extension) (Extension, error) {
|
|
if f, ok := pge.accessCache[name]; ok {
|
|
return f, nil
|
|
}
|
|
return Extension{}, nil
|
|
}
|
|
|
|
// HasLoadedExtension returns whether the extension has been loaded.
|
|
func (pge *Collection) HasLoadedExtension(ctx context.Context, name id.Extension) bool {
|
|
_, ok := pge.accessCache[name]
|
|
return ok
|
|
}
|
|
|
|
// AddLoadedExtension adds a new extension, that has already been loaded, to the collection.
|
|
func (pge *Collection) AddLoadedExtension(ctx context.Context, ext Extension) error {
|
|
// First we'll check to see if it exists
|
|
if _, ok := pge.accessCache[ext.ExtName]; ok {
|
|
return errors.Errorf(`extension "%s" already exists`, ext.ExtName)
|
|
}
|
|
|
|
// Now we'll add the extension to our map
|
|
data, err := ext.Serialize(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
h, err := pge.ns.WriteBytes(ctx, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mapEditor := pge.underlyingMap.Editor()
|
|
if err = mapEditor.Add(ctx, string(ext.ExtName), h); err != nil {
|
|
return err
|
|
}
|
|
newMap, err := mapEditor.Flush(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pge.underlyingMap = newMap
|
|
pge.mapHash = pge.underlyingMap.HashOf()
|
|
return pge.reloadCaches(ctx)
|
|
}
|
|
|
|
// DropLoadedExtension drops a loaded extension. This should be called when unloading an extension, but this function
|
|
// itself does not perform the necessary logic.
|
|
func (pge *Collection) DropLoadedExtension(ctx context.Context, names ...id.Extension) error {
|
|
// TODO: should this also handle the unloading logic?
|
|
if len(names) == 0 {
|
|
return nil
|
|
}
|
|
// Check that each name exists before performing any deletions
|
|
for _, name := range names {
|
|
if _, ok := pge.accessCache[name]; !ok {
|
|
return errors.Errorf(`extension "%s" does not exist`, name)
|
|
}
|
|
}
|
|
|
|
// Now we'll remove the extensions from the map
|
|
mapEditor := pge.underlyingMap.Editor()
|
|
for _, name := range names {
|
|
err := mapEditor.Delete(ctx, string(name))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
newMap, err := mapEditor.Flush(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pge.underlyingMap = newMap
|
|
pge.mapHash = pge.underlyingMap.HashOf()
|
|
return pge.reloadCaches(ctx)
|
|
}
|
|
|
|
// Clone returns a new *Collection with the same contents as the original.
|
|
func (pge *Collection) Clone(ctx context.Context) *Collection {
|
|
return &Collection{
|
|
accessCache: maps.Clone(pge.accessCache),
|
|
idCache: slices.Clone(pge.idCache),
|
|
mapHash: pge.mapHash,
|
|
underlyingMap: pge.underlyingMap,
|
|
ns: pge.ns,
|
|
}
|
|
}
|
|
|
|
// Map writes any cached sequences to the underlying map, and then returns the underlying map.
|
|
func (pge *Collection) Map(ctx context.Context) (prolly.AddressMap, error) {
|
|
return pge.underlyingMap, nil
|
|
}
|
|
|
|
// DiffersFrom returns true when the hash that is associated with the underlying map for this collection is different
|
|
// from the hash in the given root.
|
|
func (pge *Collection) DiffersFrom(ctx context.Context, root objinterface.RootValue) bool {
|
|
hashOnGivenRoot, err := pge.LoadCollectionHash(ctx, root)
|
|
if err != nil {
|
|
return true
|
|
}
|
|
if pge.mapHash.Equal(hashOnGivenRoot) {
|
|
return false
|
|
}
|
|
// An empty map should match an uninitialized collection on the root
|
|
count, err := pge.underlyingMap.Count()
|
|
if err == nil && count == 0 && hashOnGivenRoot.IsEmpty() {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// reloadCaches writes the underlying map's contents to the caches.
|
|
func (pge *Collection) reloadCaches(ctx context.Context) error {
|
|
count, err := pge.underlyingMap.Count()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
clear(pge.accessCache)
|
|
pge.mapHash = pge.underlyingMap.HashOf()
|
|
pge.idCache = make([]id.Extension, 0, count)
|
|
|
|
return pge.underlyingMap.IterAll(ctx, func(_ string, h hash.Hash) error {
|
|
if h.IsEmpty() {
|
|
return nil
|
|
}
|
|
data, err := pge.ns.ReadBytes(ctx, h)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ext, err := DeserializeExtension(ctx, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pge.accessCache[ext.ExtName] = ext
|
|
pge.idCache = append(pge.idCache, ext.ExtName)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// CompareVersions compares the major and minor version of the extension versus the given extension.
|
|
func (ext Extension) CompareVersions(other Extension) int {
|
|
return cmp.Or(
|
|
cmp.Compare(ext.LibIdentifier.Version().Major(), other.LibIdentifier.Version().Major()),
|
|
cmp.Compare(ext.LibIdentifier.Version().Minor(), other.LibIdentifier.Version().Minor()),
|
|
)
|
|
}
|
|
|
|
// GetID implements the interface objinterface.RootObject.
|
|
func (ext Extension) GetID() id.Id {
|
|
return ext.ExtName.AsId()
|
|
}
|
|
|
|
// GetRootObjectID implements the interface objinterface.RootObject.
|
|
func (ext Extension) GetRootObjectID() objinterface.RootObjectID {
|
|
return objinterface.RootObjectID_Extensions
|
|
}
|
|
|
|
// HashOf implements the interface objinterface.RootObject.
|
|
func (ext Extension) HashOf(ctx context.Context) (hash.Hash, error) {
|
|
data, err := ext.Serialize(ctx)
|
|
if err != nil {
|
|
return hash.Hash{}, err
|
|
}
|
|
return hash.Of(data), nil
|
|
}
|
|
|
|
// Name implements the interface objinterface.RootObject.
|
|
func (ext Extension) Name() doltdb.TableName {
|
|
return doltdb.TableName{Name: ext.ExtName.Name()}
|
|
}
|