chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
// 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()}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// 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 (
|
||||
"context"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/merge"
|
||||
"github.com/dolthub/dolt/go/store/hash"
|
||||
"github.com/dolthub/dolt/go/store/prolly"
|
||||
|
||||
"github.com/dolthub/doltgresql/core/id"
|
||||
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
|
||||
"github.com/dolthub/doltgresql/flatbuffers/gen/serial"
|
||||
)
|
||||
|
||||
// storage is used to read from and write to the root.
|
||||
var storage = objinterface.RootObjectSerializer{
|
||||
Bytes: (*serial.RootValue).ExtensionsBytes,
|
||||
RootValueAdd: serial.RootValueAddExtensions,
|
||||
}
|
||||
|
||||
// HandleMerge implements the interface objinterface.Collection.
|
||||
func (*Collection) HandleMerge(ctx context.Context, mro merge.MergeRootObject) (doltdb.RootObject, *merge.MergeStats, error) {
|
||||
ourExt := mro.OurRootObj.(Extension)
|
||||
theirExt := mro.TheirRootObj.(Extension)
|
||||
// Ensure that they have the same ID
|
||||
if ourExt.ExtName != theirExt.ExtName {
|
||||
return nil, nil, errors.Newf("attempted to merge different extensions: `%s` and `%s`",
|
||||
ourExt.Name().String(), theirExt.Name().String())
|
||||
}
|
||||
ourHash, err := ourExt.HashOf(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
theirHash, err := theirExt.HashOf(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// We always keep the newest extension. I don't think this is actually valid since old extensions are very likely
|
||||
// to have invalid function signatures, but this is a start for now.
|
||||
// TODO: figure out a better method
|
||||
if ourHash.Equal(theirHash) || ourExt.CompareVersions(theirExt) >= 0 {
|
||||
return mro.OurRootObj, &merge.MergeStats{
|
||||
Operation: merge.TableUnmodified,
|
||||
Adds: 0,
|
||||
Deletes: 0,
|
||||
Modifications: 0,
|
||||
DataConflicts: 0,
|
||||
SchemaConflicts: 0,
|
||||
ConstraintViolations: 0,
|
||||
}, nil
|
||||
} else {
|
||||
return mro.TheirRootObj, &merge.MergeStats{
|
||||
Operation: merge.TableModified,
|
||||
Adds: 0,
|
||||
Deletes: 0,
|
||||
Modifications: 1,
|
||||
DataConflicts: 0,
|
||||
SchemaConflicts: 0,
|
||||
ConstraintViolations: 0,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// LoadCollection implements the interface objinterface.Collection.
|
||||
func (*Collection) LoadCollection(ctx context.Context, root objinterface.RootValue) (objinterface.Collection, error) {
|
||||
return LoadExtensions(ctx, root)
|
||||
}
|
||||
|
||||
// LoadCollectionHash implements the interface objinterface.Collection.
|
||||
func (*Collection) LoadCollectionHash(ctx context.Context, root objinterface.RootValue) (hash.Hash, error) {
|
||||
m, ok, err := storage.GetProllyMap(ctx, root)
|
||||
if err != nil || !ok {
|
||||
return hash.Hash{}, err
|
||||
}
|
||||
return m.HashOf(), nil
|
||||
}
|
||||
|
||||
// LoadExtensions loads the extensions collection from the given root.
|
||||
func LoadExtensions(ctx context.Context, root objinterface.RootValue) (*Collection, error) {
|
||||
m, ok, err := storage.GetProllyMap(ctx, root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
m, err = prolly.NewEmptyAddressMap(root.NodeStore())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return NewCollection(ctx, m, root.NodeStore())
|
||||
}
|
||||
|
||||
// ResolveNameFromObjects implements the interface objinterface.Collection.
|
||||
func (*Collection) ResolveNameFromObjects(ctx context.Context, name doltdb.TableName, rootObjects []objinterface.RootObject) (doltdb.TableName, id.Id, error) {
|
||||
tempCollection := Collection{
|
||||
accessCache: make(map[id.Extension]Extension),
|
||||
}
|
||||
for _, rootObject := range rootObjects {
|
||||
if obj, ok := rootObject.(Extension); ok {
|
||||
tempCollection.accessCache[obj.ExtName] = obj
|
||||
}
|
||||
}
|
||||
return tempCollection.ResolveName(ctx, name)
|
||||
}
|
||||
|
||||
// Serializer implements the interface objinterface.Collection.
|
||||
func (*Collection) Serializer() objinterface.RootObjectSerializer {
|
||||
return storage
|
||||
}
|
||||
|
||||
// UpdateRoot implements the interface objinterface.Collection.
|
||||
func (pge *Collection) UpdateRoot(ctx context.Context, root objinterface.RootValue) (objinterface.RootValue, error) {
|
||||
m, err := pge.Map(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return storage.WriteProllyMap(ctx, root, m)
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
|
||||
"github.com/dolthub/doltgresql/core/extensions/pg_extension"
|
||||
)
|
||||
|
||||
var (
|
||||
cachedError error // TODO: is it better for this to be a local object instead of a global since it's always returned?
|
||||
allExtensions map[string]*pg_extension.ExtensionFiles // TODO: should use id.Extension instead of a string
|
||||
allLibraries map[string]*pg_extension.Library // TODO: close these at some point
|
||||
extMutex = &sync.Mutex{}
|
||||
libMutex = &sync.Mutex{}
|
||||
)
|
||||
|
||||
// Version 0 identifiers are encoded like the following:
|
||||
// 00AAA1111111111BBB
|
||||
// `00` is a two-digit version specifier, and we only have version 0 for now
|
||||
// `AAA` is the three-letter platform specifier
|
||||
// `1111111111` is the ten-digit library version specifier (i.e. an encoded form of 1.0, 1.5, etc.)
|
||||
// `BBB` is the extension name, and may be of any length (will have at least 1 character)
|
||||
|
||||
// LibraryIdentifier points to a specific extension, as extension functions are dependent on the extension name,
|
||||
// version, and originating platform (as different platforms may encode data differently).
|
||||
type LibraryIdentifier string
|
||||
|
||||
// InvalidIdentifierReason gives a reason as to why an Identifier is invalid.
|
||||
type InvalidIdentifierReason uint8
|
||||
|
||||
const (
|
||||
InvalidIdentifierReason_MismatchedPlatform InvalidIdentifierReason = iota
|
||||
InvalidIdentifierReason_MissingLibrary
|
||||
InvalidIdentifierReason_InvalidVersion
|
||||
)
|
||||
|
||||
// InvalidIdentifier represents an invalid LibraryIdentifier, providing both the LibraryIdentifier and the reason that
|
||||
// it is invalid.
|
||||
type InvalidIdentifier struct {
|
||||
Identifier LibraryIdentifier
|
||||
Reason InvalidIdentifierReason
|
||||
}
|
||||
|
||||
// GetExtension returns the extension matching the given name. Returns an error if the extension cannot be found.
|
||||
func GetExtension(name string) (_ *pg_extension.ExtensionFiles, err error) {
|
||||
extMutex.Lock()
|
||||
defer extMutex.Unlock()
|
||||
|
||||
if cachedError != nil {
|
||||
return nil, cachedError
|
||||
}
|
||||
if allExtensions == nil {
|
||||
allLibraries = make(map[string]*pg_extension.Library)
|
||||
allExtensions, err = pg_extension.LoadExtensions()
|
||||
if err != nil {
|
||||
allExtensions = make(map[string]*pg_extension.ExtensionFiles)
|
||||
cachedError = err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
ext, ok := allExtensions[name]
|
||||
if !ok {
|
||||
return nil, errors.Errorf(`could not open extension control file "%s.control"`, name)
|
||||
}
|
||||
return ext, nil
|
||||
}
|
||||
|
||||
// GetExtensionFunction returns the function inside the extension matching the given names. Returns an error if the
|
||||
// extension or function cannot be found.
|
||||
func GetExtensionFunction(identifier LibraryIdentifier, funcName string) (_ pg_extension.Function, err error) {
|
||||
libMutex.Lock()
|
||||
defer libMutex.Unlock()
|
||||
|
||||
extName := identifier.ExtensionName()
|
||||
if identifier.Platform() != pg_extension.PLATFORM {
|
||||
return pg_extension.Function{}, errors.Errorf(
|
||||
`function "%s" was initialized through "%s" on a different platform, which is not supported`, funcName, extName)
|
||||
}
|
||||
lib, ok := allLibraries[extName]
|
||||
if !ok {
|
||||
ext, err := GetExtension(extName)
|
||||
if err != nil {
|
||||
return pg_extension.Function{}, err
|
||||
}
|
||||
lib, err = ext.LoadLibrary()
|
||||
if err != nil {
|
||||
return pg_extension.Function{}, err
|
||||
}
|
||||
lib.Version = ext.Control.DefaultVersion
|
||||
allLibraries[extName] = lib
|
||||
}
|
||||
if lib.Version != identifier.Version() {
|
||||
return pg_extension.Function{}, errors.Errorf(
|
||||
`function "%s" was initialized through "%s" v%s, the current platform only supports v%s"`,
|
||||
funcName, extName, identifier.Version().String(), lib.Version.String())
|
||||
}
|
||||
libFunc, ok := lib.Funcs[funcName]
|
||||
if !ok {
|
||||
return pg_extension.Function{}, errors.Errorf(`extension "%s" does not declare the function "%s"`, extName, funcName)
|
||||
}
|
||||
return libFunc, nil
|
||||
}
|
||||
|
||||
// FindInvalidIdentifiers returns all identifiers that are not valid for the current environment. If the return is
|
||||
// empty, then that means all of the given identifiers are valid.
|
||||
func FindInvalidIdentifiers(identifiers ...LibraryIdentifier) []InvalidIdentifier {
|
||||
extMutex.Lock()
|
||||
defer extMutex.Unlock()
|
||||
|
||||
var invalidIdentifiers []InvalidIdentifier
|
||||
if cachedError != nil {
|
||||
invalidIdentifiers = make([]InvalidIdentifier, 0, len(identifiers))
|
||||
for _, identifier := range identifiers {
|
||||
invalidIdentifiers = append(invalidIdentifiers, InvalidIdentifier{
|
||||
Identifier: identifier,
|
||||
Reason: InvalidIdentifierReason_MissingLibrary,
|
||||
})
|
||||
}
|
||||
return invalidIdentifiers
|
||||
}
|
||||
for _, identifier := range identifiers {
|
||||
if identifier.Platform() != pg_extension.PLATFORM {
|
||||
invalidIdentifiers = append(invalidIdentifiers, InvalidIdentifier{
|
||||
Identifier: identifier,
|
||||
Reason: InvalidIdentifierReason_MismatchedPlatform,
|
||||
})
|
||||
continue
|
||||
}
|
||||
ext, ok := allExtensions[identifier.ExtensionName()]
|
||||
if !ok {
|
||||
invalidIdentifiers = append(invalidIdentifiers, InvalidIdentifier{
|
||||
Identifier: identifier,
|
||||
Reason: InvalidIdentifierReason_MissingLibrary,
|
||||
})
|
||||
continue
|
||||
}
|
||||
if ext.Control.DefaultVersion != identifier.Version() {
|
||||
invalidIdentifiers = append(invalidIdentifiers, InvalidIdentifier{
|
||||
Identifier: identifier,
|
||||
Reason: InvalidIdentifierReason_InvalidVersion,
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
return invalidIdentifiers
|
||||
}
|
||||
|
||||
// GetPlatform returns the current platform that Doltgres is being executed on. This is encoded as a three-letter string.
|
||||
func GetPlatform() string {
|
||||
return pg_extension.PLATFORM
|
||||
}
|
||||
|
||||
// CreateLibraryIdentifier creates a LibraryIdentifier using the given information.
|
||||
func CreateLibraryIdentifier(name string, version pg_extension.Version) LibraryIdentifier {
|
||||
return LibraryIdentifier(fmt.Sprintf("00%s%010d%s", pg_extension.PLATFORM, version, name))
|
||||
}
|
||||
|
||||
// Platform returns the platform that this LibraryIdentifier was created on.
|
||||
func (id LibraryIdentifier) Platform() string {
|
||||
return string(id[2:5])
|
||||
}
|
||||
|
||||
// Version returns the library version that this LibraryIdentifier references.
|
||||
func (id LibraryIdentifier) Version() pg_extension.Version {
|
||||
val, err := strconv.ParseUint(string(id[5:15]), 10, 32)
|
||||
if err != nil {
|
||||
// We'll panic for now since this should never happen
|
||||
panic(err)
|
||||
}
|
||||
return pg_extension.Version(val)
|
||||
}
|
||||
|
||||
// ExtensionName returns the extension referenced by this LibraryIdentifier.
|
||||
func (id LibraryIdentifier) ExtensionName() string {
|
||||
return string(id[15:])
|
||||
}
|
||||
|
||||
// DisplayString returns the identifier as a human-readable string.
|
||||
func (id LibraryIdentifier) DisplayString() string {
|
||||
return fmt.Sprintf("%s--%s:%s", id.ExtensionName(), id.Version().String(), id.Platform())
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# Finding Extension Function Imports
|
||||
These are commands that can be used to find the functions that an extension imports, so that we know which ones we need to implement for the extension to load.
|
||||
## Windows
|
||||
On Windows, we make use of `dumpbin`, which is installed alongside Visual Studio (the full version, _not_ Code). We are generally only interested in the functions under `postgres.exe`, as the library should load other DLLs as necessary.
|
||||
```cmd
|
||||
dumpbin /imports "C:/Program Files/PostgreSQL/15/lib/LIBRARY_NAME.dll"
|
||||
```
|
||||
## Linux
|
||||
On Linux, we make use of the built-in `nm` command. We are interested in the `U` functions that do not have an `@` near the end (as those are usually implemented in external libraries).
|
||||
```bash
|
||||
nm -D -u /usr/lib/postgresql/15/lib/LIBRARY_NAME.so
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
// 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 pg_extension
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: "-I${SRCDIR}/library"
|
||||
#include "exports.h"
|
||||
|
||||
static inline Datum CallFmgrFunctionC(FunctionCallInfo fcinfo) {
|
||||
return ((PGFunction)fcinfo->flinfo->fn_addr)(fcinfo);
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
import "unsafe"
|
||||
|
||||
// Datum is a C pointer to some data. Depending on the function being called, it may not be a pointer that should be
|
||||
// freed, as some functions return pointers to static memory.
|
||||
type Datum uintptr
|
||||
|
||||
// NullableDatum is used for arguments to Fmgr function calls.
|
||||
type NullableDatum struct {
|
||||
Value Datum
|
||||
IsNull bool
|
||||
}
|
||||
|
||||
// CallFmgrFunction calls the given function and forwards the arguments.
|
||||
func CallFmgrFunction(fn uintptr, args ...NullableDatum) (result Datum, isNotNull bool) {
|
||||
fi := Malloc[C.FmgrInfo]()
|
||||
defer Free(fi)
|
||||
ZeroMemory(fi)
|
||||
fc := Malloc[C.FunctionCallInfoBaseData]()
|
||||
defer Free(fc)
|
||||
ZeroMemory(fc)
|
||||
fi.fn_addr = unsafe.Pointer(fn)
|
||||
fc.flinfo = fi
|
||||
fc.nargs = C.int16_t(len(args))
|
||||
|
||||
for i, arg := range args {
|
||||
fc.args[i].value = C.Datum(arg.Value)
|
||||
fc.args[i].isnull = C.bool(arg.IsNull)
|
||||
}
|
||||
result = Datum(C.CallFmgrFunctionC(fc))
|
||||
return result, !bool(fc.isnull) && result != 0
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
// 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 pg_extension
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// sqlFunctionCapture is a regex to capture the function name as defined in the library. We'll eventually replace this
|
||||
// and use the nodes from the parser, but this is good enough for the default extensions.
|
||||
var sqlFunctionCapture = regexp.MustCompile(`(?is)create\s+(?:or\s+replace\s+)?function\s+(.*?)\s*\(.*?\)\s+(?:.*?language c.*?as\s+'.*?'\s*,\s*'(.*?)'.*?;|.*?as\s+'.*?'\s*,\s*'(.*?)'.*?language c.*?;|.*?language c.*?;)`)
|
||||
|
||||
// createFunctionStart is a regex to find the beginning of a CREATE FUNCTION statement.
|
||||
var createFunctionStart = regexp.MustCompile(`(?is)create\s+(?:or\s+replace\s+)?function`)
|
||||
|
||||
// ExtensionFiles contains all of the files that are related to or used by an extension.
|
||||
type ExtensionFiles struct {
|
||||
Name string
|
||||
ControlFileName string
|
||||
SQLFileNames []string
|
||||
LibraryFileName string
|
||||
ControlFileDir string
|
||||
LibraryFileDir string
|
||||
Control Control
|
||||
}
|
||||
|
||||
// Control contains the contents of the control file.
|
||||
// https://www.postgresql.org/docs/15/extend-extensions.html#id-1.8.3.20.11
|
||||
type Control struct {
|
||||
Directory string
|
||||
DefaultVersion Version
|
||||
Comment string
|
||||
Encoding string
|
||||
ModulePathname string
|
||||
Requires []string
|
||||
Superuser bool
|
||||
Trusted bool
|
||||
Relocatable bool
|
||||
Schema string
|
||||
Extra map[string]string // All entries in here could not be matched to an expected field
|
||||
}
|
||||
|
||||
// Version specifies the major and minor version numbers for an extension.
|
||||
type Version uint32
|
||||
|
||||
// FilenameVersions returns the versions that were encoded in a filename. `From` is the first number, while `To` is the
|
||||
// second number. If a filename only specifies a single version, this both `From` and `To` will equal one another.
|
||||
type FilenameVersions struct {
|
||||
From Version
|
||||
To Version
|
||||
}
|
||||
|
||||
// LoadExtensions loads information for all extensions that are in the extensions directory of a local Postgres installation.
|
||||
func LoadExtensions() (map[string]*ExtensionFiles, error) {
|
||||
libDir, extDir, err := PostgresDirectories()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dirEntries, err := os.ReadDir(extDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
libEntries, err := os.ReadDir(libDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extensionFiles := make(map[string]*ExtensionFiles)
|
||||
// Look for the control files first
|
||||
for _, dirEntry := range dirEntries {
|
||||
fileName := dirEntry.Name()
|
||||
if !dirEntry.IsDir() && strings.HasSuffix(fileName, ".control") {
|
||||
extensionName := strings.TrimSuffix(fileName, ".control")
|
||||
extensionFiles[extensionName] = &ExtensionFiles{
|
||||
Name: extensionName,
|
||||
ControlFileName: fileName,
|
||||
ControlFileDir: extDir,
|
||||
}
|
||||
}
|
||||
}
|
||||
// Associate the SQL files and libraries
|
||||
for _, extFile := range extensionFiles {
|
||||
for _, dirEntry := range dirEntries {
|
||||
fileName := dirEntry.Name()
|
||||
if !dirEntry.IsDir() && strings.HasPrefix(fileName, extFile.Name+"--") && strings.HasSuffix(fileName, ".sql") {
|
||||
extFile.SQLFileNames = append(extFile.SQLFileNames, fileName)
|
||||
}
|
||||
}
|
||||
for _, libEntry := range libEntries {
|
||||
fileName := libEntry.Name()
|
||||
if !libEntry.IsDir() && strings.HasPrefix(fileName, extFile.Name+".") {
|
||||
extFile.LibraryFileName = fileName
|
||||
extFile.LibraryFileDir = libDir
|
||||
}
|
||||
}
|
||||
slices.SortFunc(extFile.SQLFileNames, func(aStr, bStr string) int {
|
||||
a := DecodeFilenameVersions(extFile.Name, aStr)
|
||||
b := DecodeFilenameVersions(extFile.Name, bStr)
|
||||
return cmp.Or(
|
||||
cmp.Compare(a.From, b.From),
|
||||
cmp.Compare(a.To, b.To),
|
||||
)
|
||||
})
|
||||
// Some SQL files are old migration files that won't apply to us, so we can remove them by starting at the first
|
||||
// non-migration file.
|
||||
for nextLoop := true; nextLoop; {
|
||||
nextLoop = false
|
||||
for i := 1; i < len(extFile.SQLFileNames); i++ {
|
||||
if strings.Count(extFile.SQLFileNames[i], "--") == 1 {
|
||||
extFile.SQLFileNames = extFile.SQLFileNames[i:]
|
||||
nextLoop = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// Load the control file
|
||||
if err = extFile.loadControl(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return extensionFiles, nil
|
||||
}
|
||||
|
||||
// loadControl loads the control file of an extension.
|
||||
func (extFile *ExtensionFiles) loadControl() error {
|
||||
data, err := os.ReadFile(fmt.Sprintf("%s/%s", extFile.ControlFileDir, extFile.ControlFileName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
extFile.Control = Control{ // These are the default values
|
||||
Directory: "",
|
||||
DefaultVersion: 0,
|
||||
Comment: "",
|
||||
Encoding: "",
|
||||
ModulePathname: "",
|
||||
Requires: nil,
|
||||
Superuser: true,
|
||||
Trusted: false,
|
||||
Relocatable: false,
|
||||
Schema: "",
|
||||
Extra: make(map[string]string),
|
||||
}
|
||||
lines := strings.Split(strings.ReplaceAll(string(data), "\r", ""), "\n")
|
||||
for _, originalLine := range lines {
|
||||
line := strings.TrimSpace(originalLine)
|
||||
if commentIdx := strings.Index(line, "#"); commentIdx != -1 {
|
||||
line = line[:commentIdx]
|
||||
}
|
||||
// Line may be empty if it only contained a comment
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
equalsSplit := strings.Index(line, "=")
|
||||
if equalsSplit == -1 {
|
||||
return fmt.Errorf("malformed `%s.control`:\n%s", extFile.Name, string(data))
|
||||
}
|
||||
name := strings.ToLower(strings.TrimSpace(line[:equalsSplit]))
|
||||
value := line[equalsSplit+1:]
|
||||
switch name {
|
||||
case "directory":
|
||||
extFile.Control.Directory = removeStringQuotations(value)
|
||||
case "default_version":
|
||||
value = removeStringQuotations(value)
|
||||
separator := strings.Index(value, ".")
|
||||
if separator == -1 {
|
||||
return fmt.Errorf("malformed `%s.control` line:\n%s", extFile.Name, originalLine)
|
||||
}
|
||||
major, err := strconv.Atoi(value[:separator])
|
||||
if err != nil {
|
||||
return fmt.Errorf("malformed `%s.control` line:\n%s", extFile.Name, originalLine)
|
||||
}
|
||||
minor, err := strconv.Atoi(value[separator+1:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("malformed `%s.control` line:\n%s", extFile.Name, originalLine)
|
||||
}
|
||||
extFile.Control.DefaultVersion = ToVersion(uint16(major), uint16(minor))
|
||||
case "comment":
|
||||
extFile.Control.Comment = removeStringQuotations(value)
|
||||
case "encoding":
|
||||
extFile.Control.Encoding = removeStringQuotations(value)
|
||||
case "module_pathname":
|
||||
extFile.Control.ModulePathname = removeStringQuotations(value)
|
||||
case "requires":
|
||||
value = removeStringQuotations(value)
|
||||
var entries []string
|
||||
for _, entry := range strings.Split(value, ",") {
|
||||
entries = append(entries, strings.TrimSpace(entry))
|
||||
}
|
||||
extFile.Control.Requires = entries
|
||||
case "superuser", "trusted", "relocatable":
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
var boolValue bool
|
||||
if value == "true" {
|
||||
boolValue = true
|
||||
} else if value == "false" {
|
||||
boolValue = false
|
||||
} else {
|
||||
return fmt.Errorf("malformed `%s.control` line:\n%s", extFile.Name, originalLine)
|
||||
}
|
||||
switch name {
|
||||
case "superuser":
|
||||
extFile.Control.Superuser = boolValue
|
||||
case "trusted":
|
||||
extFile.Control.Trusted = boolValue
|
||||
case "relocatable":
|
||||
extFile.Control.Relocatable = boolValue
|
||||
}
|
||||
case "schema":
|
||||
extFile.Control.Schema = removeStringQuotations(value)
|
||||
default:
|
||||
extFile.Control.Extra[name] = value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadSQLFiles loads the contents of the SQL files used by the extension. These will be in the order that they need to
|
||||
// be executed.
|
||||
func (extFile *ExtensionFiles) LoadSQLFiles() ([]string, error) {
|
||||
sqlFiles := make([]string, len(extFile.SQLFileNames))
|
||||
for i, sqlFileName := range extFile.SQLFileNames {
|
||||
data, err := os.ReadFile(fmt.Sprintf("%s/%s", extFile.ControlFileDir, sqlFileName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sqlFiles[i] = string(data)
|
||||
}
|
||||
return sqlFiles, nil
|
||||
}
|
||||
|
||||
// LoadSQLFunctionNames loads all of the library function names that are used by the extension.
|
||||
func (extFile *ExtensionFiles) LoadSQLFunctionNames() ([]string, error) {
|
||||
funcNames := make(map[string]struct{})
|
||||
for _, sqlFileName := range extFile.SQLFileNames {
|
||||
data, err := os.ReadFile(fmt.Sprintf("%s/%s", extFile.ControlFileDir, sqlFileName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fileRemaining := string(data)
|
||||
OuterLoop:
|
||||
for {
|
||||
// We want to advance the file to the start of the next CREATE FUNCTION if one is present
|
||||
startIdx := createFunctionStart.FindStringIndex(fileRemaining)
|
||||
if startIdx == nil {
|
||||
break
|
||||
}
|
||||
fileRemaining = fileRemaining[startIdx[0]:]
|
||||
// We capture the ending semicolon so the regex doesn't match beyond the function definition's boundaries.
|
||||
endIdx := strings.IndexRune(fileRemaining, ';')
|
||||
if endIdx == -1 {
|
||||
break
|
||||
}
|
||||
matches := sqlFunctionCapture.FindStringSubmatch(fileRemaining[:endIdx+1])
|
||||
switch len(matches) {
|
||||
case 0:
|
||||
break OuterLoop
|
||||
case 4:
|
||||
if len(matches[2]) > 0 {
|
||||
funcNames[matches[2]] = struct{}{}
|
||||
} else if len(matches[3]) > 0 {
|
||||
funcNames[matches[3]] = struct{}{}
|
||||
} else {
|
||||
funcNames[matches[1]] = struct{}{}
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid CREATE FUNCTION string: %s", string(data))
|
||||
}
|
||||
// We nudge it forward to guarantee that our next CREATE FUNCTION search will grab the next one
|
||||
fileRemaining = fileRemaining[6:]
|
||||
}
|
||||
}
|
||||
sortedFuncNames := slices.Sorted(maps.Keys(funcNames))
|
||||
return sortedFuncNames, nil
|
||||
}
|
||||
|
||||
// LoadLibrary loads the extension as a library.
|
||||
func (extFile *ExtensionFiles) LoadLibrary() (*Library, error) {
|
||||
if len(extFile.LibraryFileName) == 0 {
|
||||
return nil, fmt.Errorf("extension `%s` does not reference a library", extFile.Name)
|
||||
}
|
||||
funcNames, err := extFile.LoadSQLFunctionNames()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return LoadLibrary(fmt.Sprintf("%s/%s", extFile.LibraryFileDir, extFile.LibraryFileName), funcNames)
|
||||
}
|
||||
|
||||
// ToVersion creates a version from the given major and minor version numbers.
|
||||
func ToVersion(major uint16, minor uint16) Version {
|
||||
return (Version(major) << 16) + Version(minor)
|
||||
}
|
||||
|
||||
// Major returns the encoded major version number.
|
||||
func (v Version) Major() uint16 {
|
||||
return uint16(v >> 16)
|
||||
}
|
||||
|
||||
// Minor returns the encoded minor version number.
|
||||
func (v Version) Minor() uint16 {
|
||||
return uint16(v)
|
||||
}
|
||||
|
||||
// String returns the version in the `major.minor` format.
|
||||
func (v Version) String() string {
|
||||
return fmt.Sprintf("%d.%d", v.Major(), v.Minor())
|
||||
}
|
||||
|
||||
// DecodeFilenameVersions decodes the version information within the file name. The `sqlFileName` should be the full
|
||||
// file name (excluding the path), and the SQL file name should contain only the name as
|
||||
func DecodeFilenameVersions(name string, fileName string) FilenameVersions {
|
||||
var versionSubsection string
|
||||
if strings.HasSuffix(fileName, ".sql") {
|
||||
versionSubsection = strings.TrimSuffix(fileName[len(name)+2: /* We add 2 to account for the -- */], ".sql")
|
||||
} else if strings.HasSuffix(fileName, ".control") {
|
||||
versionSubsection = strings.TrimSuffix(fileName[len(name)+2: /* We add 2 to account for the -- */], ".control")
|
||||
} else {
|
||||
// The given name is not a .SQL or .CONTROL file, so we'll just return
|
||||
return FilenameVersions{}
|
||||
}
|
||||
var from, to string
|
||||
if dashIdx := strings.Index(versionSubsection, "--"); dashIdx == -1 {
|
||||
from = versionSubsection
|
||||
to = versionSubsection
|
||||
} else {
|
||||
from = versionSubsection[:dashIdx]
|
||||
to = versionSubsection[dashIdx+2:]
|
||||
}
|
||||
fromSplit := strings.Index(from, ".")
|
||||
toSplit := strings.Index(to, ".")
|
||||
if fromSplit == -1 || toSplit == -1 {
|
||||
return FilenameVersions{}
|
||||
}
|
||||
fromMajor, err := strconv.Atoi(from[:fromSplit])
|
||||
if err != nil {
|
||||
return FilenameVersions{}
|
||||
}
|
||||
fromMinor, err := strconv.Atoi(from[fromSplit+1:])
|
||||
if err != nil {
|
||||
return FilenameVersions{}
|
||||
}
|
||||
toMajor, err := strconv.Atoi(to[:toSplit])
|
||||
if err != nil {
|
||||
return FilenameVersions{}
|
||||
}
|
||||
toMinor, err := strconv.Atoi(to[toSplit+1:])
|
||||
if err != nil {
|
||||
return FilenameVersions{}
|
||||
}
|
||||
return FilenameVersions{
|
||||
From: ToVersion(uint16(fromMajor), uint16(fromMinor)),
|
||||
To: ToVersion(uint16(toMajor), uint16(toMinor)),
|
||||
}
|
||||
}
|
||||
|
||||
// removeStringQuotations removes the single quotes that are used to specify that a value is a string.
|
||||
func removeStringQuotations(str string) string {
|
||||
str = strings.TrimSpace(str)
|
||||
if strings.HasPrefix(str, "'") {
|
||||
return (str[:len(str)-1])[1:]
|
||||
}
|
||||
return str
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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 pg_extension
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PostgresDirectories returns the installation directories of a local Postgres instance.
|
||||
func PostgresDirectories() (libDir string, extensionDir string, err error) {
|
||||
var buffer bytes.Buffer
|
||||
cmd := exec.Command("pg_config", "--pkglibdir")
|
||||
cmd.Stdout = &buffer
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
libDir = strings.TrimSpace(buffer.String())
|
||||
buffer.Reset()
|
||||
cmd = exec.Command("pg_config", "--sharedir")
|
||||
cmd.Stdout = &buffer
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
extensionDir = strings.TrimSpace(buffer.String()) + "/extension"
|
||||
return libDir, extensionDir, nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
$defFile = "postgres.def"
|
||||
$outDir = Resolve-Path '..\output' -ErrorAction SilentlyContinue -ErrorVariable _dummy
|
||||
if (-not $outDir) { $outDir = (New-Item -ItemType Directory -Path '..\output').FullName }
|
||||
$outFile = Join-Path $outDir 'postgres.exe'
|
||||
|
||||
function TryVS {
|
||||
$vswhere = "$Env:ProgramFiles (x86)\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
if (-not (Test-Path $vswhere)) { return $false }
|
||||
$vsRoot = & $vswhere -latest `
|
||||
-products * `
|
||||
-requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 `
|
||||
-property installationPath |
|
||||
Select-Object -First 1
|
||||
if (-not $vsRoot) { return $false }
|
||||
$msvcDir = Get-ChildItem -Path (Join-Path $vsRoot 'VC\Tools\MSVC') |
|
||||
Sort-Object Name -Descending |
|
||||
Select-Object -First 1
|
||||
$linkExe = Join-Path $msvcDir.FullName 'bin\Hostx64\x64\link.exe'
|
||||
& cmd /c "`"$vsRoot\VC\Auxiliary\Build\vcvars64.bat`" >nul `&`& `"$linkExe`" /DLL /NOENTRY /DEF:$defFile /OUT:`"$outFile`""
|
||||
return $true
|
||||
}
|
||||
|
||||
function TryGCC {
|
||||
$gcc = (& where.exe gcc.exe 2>$null | Select-Object -First 1)
|
||||
if (-not $gcc) { return $false }
|
||||
$args = @(
|
||||
"-shared",
|
||||
"-nostdlib",
|
||||
$defFile,
|
||||
"-o", $outFile
|
||||
)
|
||||
& $gcc @args
|
||||
return $true
|
||||
}
|
||||
|
||||
function TryClang {
|
||||
$lld = (& where.exe lld-link.exe 2>$null | Select-Object -First 1)
|
||||
if ($lld) {
|
||||
& $lld /DLL /NOENTRY /DEF:$defFile /OUT:"$outFile"
|
||||
return $true
|
||||
}
|
||||
$clang = (& where.exe clang.exe 2>$null | Select-Object -First 1)
|
||||
if (-not $clang) { return $false }
|
||||
$args = @(
|
||||
"-shared",
|
||||
"-nostdlib",
|
||||
$defFile,
|
||||
"-o", $outFile
|
||||
)
|
||||
& $clang @args
|
||||
return $true
|
||||
}
|
||||
|
||||
if (TryVS) { Write-Host "Definition file built using Visual Studio"; exit 0 }
|
||||
if (TryGCC) { Write-Host "Definition file built using GCC"; exit 0 }
|
||||
if (TryClang) { Write-Host "Definition file built using Clang"; exit 0 }
|
||||
|
||||
throw "Could not build the definition file"
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
case "$(go env GOOS)" in
|
||||
windows) ext="dll" ;;
|
||||
darwin) ext="dylib" ;;
|
||||
*) ext="so" ;;
|
||||
esac
|
||||
|
||||
# For now, other platforms directly embed the library, but removing this check will allow them to build a shared library as well
|
||||
if [[ "$(go env GOOS)" != "windows" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# MacOS requires that exported functions are present in the calling binary, but other platforms use a dynamic library.
|
||||
# To account for this, we put the exported functions in a normal package by default.
|
||||
# For MacOS, this works just fine as we import the package.
|
||||
# To make a dynamic library, we copy the files into a temporary directory and modify the files to create a valid library.
|
||||
# This lets us use the same code for both scenarios.
|
||||
mkdir -p temp_lib
|
||||
trap 'rm -rf temp_lib' EXIT
|
||||
|
||||
cp ./*.* ./temp_lib
|
||||
|
||||
# For Windows, we also need to build the definition file
|
||||
if [[ "$(go env GOOS)" == "windows" ]]; then
|
||||
powershell.exe -File "build_definitions.ps1"
|
||||
fi
|
||||
|
||||
for f in temp_lib/*.go; do
|
||||
sed 's/^package extension_cgo$/package main/' "$f" > "$f".tmp
|
||||
mv "$f".tmp "$f"
|
||||
done
|
||||
printf "module github.com/dolthub/pg_extension\n\ngo 1.24" > ./temp_lib/go.mod
|
||||
|
||||
(
|
||||
cd temp_lib
|
||||
CGO_ENABLED=1 go build -buildmode=c-shared -o "../../output/pg_extension.${ext}" .
|
||||
)
|
||||
@@ -0,0 +1,57 @@
|
||||
// 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.
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
#define DLLEXPORT __declspec(dllexport)
|
||||
#else
|
||||
#define DLLEXPORT __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
static char last_error[512];
|
||||
|
||||
DLLEXPORT bool errstart(int elevel, const char* domain) {
|
||||
last_error[0] = '\0';
|
||||
return 1;
|
||||
}
|
||||
|
||||
DLLEXPORT bool errstart_cold(int elevel, const char *domain) {
|
||||
return errstart(elevel, domain);
|
||||
}
|
||||
|
||||
DLLEXPORT int errmsg(const char *fmt, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(last_error, sizeof(last_error), fmt, ap);
|
||||
va_end(ap);
|
||||
return 0;
|
||||
}
|
||||
|
||||
DLLEXPORT int errmsg_internal(const char *fmt, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(last_error, sizeof(last_error), fmt, ap);
|
||||
va_end(ap);
|
||||
return 0;
|
||||
}
|
||||
|
||||
DLLEXPORT int errfinish(int dummy, ...) {
|
||||
if (last_error[0]) {
|
||||
fprintf(stderr, "Postgres ERROR: %s\n", last_error);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// 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 extension_cgo
|
||||
|
||||
/*
|
||||
#include "exports.h"
|
||||
|
||||
static inline Datum FunctionPassthrough(PGFunction f, FunctionCallInfoBaseData *fcinfo) {
|
||||
return (*f)(fcinfo);
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func main() {}
|
||||
|
||||
//export errcode
|
||||
func errcode(code C.int) C.int {
|
||||
return code
|
||||
}
|
||||
|
||||
//export palloc
|
||||
func palloc(sz C.size_t) unsafe.Pointer {
|
||||
// TODO: should track this pointer so we know to free it later
|
||||
return C.malloc(sz)
|
||||
}
|
||||
|
||||
//export palloc0
|
||||
func palloc0(sz C.size_t) unsafe.Pointer {
|
||||
// TODO: should track this pointer so we know to free it later
|
||||
ptr := C.malloc(sz)
|
||||
if ptr != nil {
|
||||
C.memset(ptr, 0, sz)
|
||||
}
|
||||
return ptr
|
||||
}
|
||||
|
||||
//export MemoryContextAlloc
|
||||
func MemoryContextAlloc(c unsafe.Pointer, sz C.size_t) unsafe.Pointer {
|
||||
// TODO: should track this pointer so we know to free it later, could use the memory context
|
||||
return C.malloc(sz)
|
||||
}
|
||||
|
||||
//export MemoryContextAllocExtended
|
||||
func MemoryContextAllocExtended(c unsafe.Pointer, sz C.size_t, f C.int) unsafe.Pointer {
|
||||
// TODO: should track this pointer so we know to free it later, could use the memory context
|
||||
return C.malloc(sz)
|
||||
}
|
||||
|
||||
//export pg_detoast_datum_packed
|
||||
func pg_detoast_datum_packed(d unsafe.Pointer) unsafe.Pointer {
|
||||
return d
|
||||
}
|
||||
|
||||
//export text_to_cstring
|
||||
func text_to_cstring(t unsafe.Pointer) *C.char {
|
||||
return C.CString(C.GoString((*C.char)(t)))
|
||||
}
|
||||
|
||||
//export uuid_in
|
||||
func uuid_in(fc C.FunctionCallInfo) C.Datum {
|
||||
uuidInputStr := (*C.pgext_const_char)(unsafe.Pointer(uintptr(fc.args[0].value)))
|
||||
uuidInputBytes := decodeUuidStr([]byte(C.GoString(uuidInputStr)))
|
||||
outputBytes := (*C.pgext_unsigned_char)(C.malloc(C.size_t(len(uuidInputBytes))))
|
||||
C.memcpy(unsafe.Pointer(outputBytes), unsafe.Pointer(&uuidInputBytes[0]), C.size_t(len(uuidInputBytes)))
|
||||
return C.Datum(uintptr(unsafe.Pointer(outputBytes)))
|
||||
}
|
||||
|
||||
// decodeUuidStr is a helper function for uuid_in, which converts the given byte slice to the static array representation.
|
||||
func decodeUuidStr(strBytes []byte) [16]byte {
|
||||
if strBytes[8] != '-' || strBytes[13] != '-' || strBytes[18] != '-' || strBytes[23] != '-' {
|
||||
return [16]byte{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}
|
||||
}
|
||||
u := [16]byte{}
|
||||
src := strBytes
|
||||
dst := u[:]
|
||||
for i, byteGroup := range []int{8, 4, 4, 4, 12} {
|
||||
if i > 0 {
|
||||
src = src[1:]
|
||||
}
|
||||
_, err := hex.Decode(dst[:byteGroup/2], src[:byteGroup])
|
||||
if err != nil {
|
||||
return [16]byte{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}
|
||||
}
|
||||
src = src[byteGroup:]
|
||||
dst = dst[byteGroup/2:]
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
//export uuid_out
|
||||
func uuid_out(ptr unsafe.Pointer) C.Datum {
|
||||
uuidInputBytes := C.GoBytes(ptr, 16)
|
||||
textBuffer := make([]byte, 36)
|
||||
_ = hex.Encode(textBuffer[0:8], uuidInputBytes[0:4])
|
||||
textBuffer[8] = '-'
|
||||
_ = hex.Encode(textBuffer[9:13], uuidInputBytes[4:6])
|
||||
textBuffer[13] = '-'
|
||||
_ = hex.Encode(textBuffer[14:18], uuidInputBytes[6:8])
|
||||
textBuffer[18] = '-'
|
||||
_ = hex.Encode(textBuffer[19:23], uuidInputBytes[8:10])
|
||||
textBuffer[23] = '-'
|
||||
_ = hex.Encode(textBuffer[24:], uuidInputBytes[10:])
|
||||
return C.Datum(uintptr(unsafe.Pointer(C.CString(string(textBuffer)))))
|
||||
}
|
||||
|
||||
//export DirectFunctionCall1Coll
|
||||
func DirectFunctionCall1Coll(fn unsafe.Pointer, collation C.uint32_t, arg1 C.Datum) C.Datum {
|
||||
fc := (*C.FunctionCallInfoBaseData)(C.malloc(C.SZ_FCINFO))
|
||||
if fc == nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "DirectFunctionCall1Coll: out of memory")
|
||||
return 0
|
||||
}
|
||||
defer C.free(unsafe.Pointer(fc))
|
||||
C.memset(unsafe.Pointer(fc), 0, C.SZ_FCINFO)
|
||||
|
||||
fc.isnull = false
|
||||
fc.fncollation = collation
|
||||
fc.nargs = 1
|
||||
fc.args[0].value = arg1
|
||||
fc.args[0].isnull = false
|
||||
|
||||
result := C.FunctionPassthrough(C.PGFunction(fn), fc)
|
||||
if fc.isnull {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "function %p returned NULL\n", fn)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// 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.
|
||||
|
||||
#ifndef PG_EXT_EXPORTS_H
|
||||
#define PG_EXT_EXPORTS_H
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// This doesn't compile unless it has a value, but Postgres defines this as an empty value intentionally
|
||||
#define FLEXIBLE_ARRAY_MEMBER 8
|
||||
|
||||
typedef uintptr_t Datum;
|
||||
typedef struct FunctionCallInfoBaseData* FunctionCallInfo;
|
||||
typedef Datum (*PGFunction) (FunctionCallInfo fcinfo);
|
||||
|
||||
typedef struct NullableDatum {
|
||||
Datum value;
|
||||
bool isnull;
|
||||
} NullableDatum;
|
||||
|
||||
typedef struct FmgrInfo {
|
||||
void* fn_addr;
|
||||
uint32_t fn_oid;
|
||||
short fn_nargs;
|
||||
bool fn_strict;
|
||||
bool fn_retset;
|
||||
unsigned char fn_stats;
|
||||
void* fn_extra;
|
||||
void* fn_mcxt;
|
||||
void* fn_expr;
|
||||
} FmgrInfo;
|
||||
|
||||
typedef struct FunctionCallInfoBaseData {
|
||||
FmgrInfo* flinfo;
|
||||
void* context;
|
||||
void* resultinfo;
|
||||
uint32_t fncollation;
|
||||
bool isnull;
|
||||
short nargs;
|
||||
NullableDatum args[FLEXIBLE_ARRAY_MEMBER];
|
||||
} FunctionCallInfoBaseData;
|
||||
|
||||
enum {
|
||||
SZ_FMGRINFO = sizeof(FmgrInfo),
|
||||
SZ_FCINFO = sizeof(FunctionCallInfoBaseData)
|
||||
};
|
||||
|
||||
typedef const char pgext_const_char;
|
||||
typedef unsigned char pgext_unsigned_char;
|
||||
typedef const uint8_t pgext_const_uint8;
|
||||
|
||||
#endif //PG_EXT_EXPORTS_H
|
||||
@@ -0,0 +1,132 @@
|
||||
// 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 extension_cgo
|
||||
|
||||
/*
|
||||
#include "exports.h"
|
||||
|
||||
typedef enum
|
||||
{
|
||||
PG_MD5 = 0,
|
||||
PG_SHA1,
|
||||
PG_SHA224,
|
||||
PG_SHA256,
|
||||
PG_SHA384,
|
||||
PG_SHA512,
|
||||
} pg_cryptohash_type;
|
||||
|
||||
typedef struct pg_cryptohash_ctx {
|
||||
pg_cryptohash_type hashType;
|
||||
} pg_cryptohash_ctx;
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"hash"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var pg_cryptohash_store sync.Map
|
||||
|
||||
//export pg_cryptohash_create
|
||||
func pg_cryptohash_create(typ C.pg_cryptohash_type) *C.pg_cryptohash_ctx {
|
||||
ctx := (*C.pg_cryptohash_ctx)(C.malloc(C.size_t(unsafe.Sizeof(C.pg_cryptohash_ctx{}))))
|
||||
ctx.hashType = typ
|
||||
ctxPtr := uintptr(unsafe.Pointer(ctx))
|
||||
switch typ {
|
||||
case 1:
|
||||
pg_cryptohash_store.Store(ctxPtr, sha1.New())
|
||||
case C.PG_SHA224:
|
||||
pg_cryptohash_store.Store(ctxPtr, sha512.New512_224())
|
||||
case C.PG_SHA256:
|
||||
pg_cryptohash_store.Store(ctxPtr, sha256.New())
|
||||
case C.PG_SHA384:
|
||||
pg_cryptohash_store.Store(ctxPtr, sha512.New384())
|
||||
case C.PG_SHA512:
|
||||
pg_cryptohash_store.Store(ctxPtr, sha512.New())
|
||||
default:
|
||||
// Default to MD5
|
||||
pg_cryptohash_store.Store(ctxPtr, md5.New())
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
//export pg_cryptohash_init
|
||||
func pg_cryptohash_init(ctx *C.pg_cryptohash_ctx) C.int {
|
||||
if ctx == nil {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
//export pg_cryptohash_update
|
||||
func pg_cryptohash_update(ctx *C.pg_cryptohash_ctx, data *C.pgext_const_uint8, len C.size_t) C.int {
|
||||
if ctx == nil {
|
||||
return -1
|
||||
}
|
||||
if len == 0 {
|
||||
return 0
|
||||
}
|
||||
ctxPtr := uintptr(unsafe.Pointer(ctx))
|
||||
storedHashAny, ok := pg_cryptohash_store.Load(ctxPtr)
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
storedHash := storedHashAny.(hash.Hash)
|
||||
dataSlice := unsafe.Slice((*byte)(unsafe.Pointer(data)), int(len))
|
||||
if _, err := storedHash.Write(dataSlice); err != nil {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
//export pg_cryptohash_final
|
||||
func pg_cryptohash_final(ctx *C.pg_cryptohash_ctx, dest *C.uint8_t, destLen C.size_t) C.int {
|
||||
if ctx == nil {
|
||||
return -1
|
||||
}
|
||||
ctxPtr := uintptr(unsafe.Pointer(ctx))
|
||||
storedHashAny, ok := pg_cryptohash_store.Load(ctxPtr)
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
storedHash := storedHashAny.(hash.Hash)
|
||||
sum := storedHash.Sum(nil)
|
||||
destSlice := unsafe.Slice((*byte)(unsafe.Pointer(dest)), int(destLen))
|
||||
// If the destination slice is too small, then it's invalid
|
||||
if len(sum) > len(destSlice) {
|
||||
return -1
|
||||
}
|
||||
copy(destSlice, sum)
|
||||
return 0
|
||||
}
|
||||
|
||||
//export pg_cryptohash_free
|
||||
func pg_cryptohash_free(ctx *C.pg_cryptohash_ctx) {
|
||||
if ctx != nil {
|
||||
ctxPtr := uintptr(unsafe.Pointer(ctx))
|
||||
pg_cryptohash_store.Delete(ctxPtr)
|
||||
C.free(unsafe.Pointer(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
//export pg_cryptohash_error
|
||||
func pg_cryptohash_error(ctx *C.pg_cryptohash_ctx) *C.pgext_const_char {
|
||||
return C.CString("")
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
LIBRARY "postgres.exe"
|
||||
EXPORTS
|
||||
; ---- functions ----
|
||||
DirectFunctionCall1Coll = pg_extension.DirectFunctionCall1Coll
|
||||
errcode = pg_extension.errcode
|
||||
errfinish = pg_extension.errfinish
|
||||
errmsg = pg_extension.errmsg
|
||||
errmsg_internal = pg_extension.errmsg_internal
|
||||
errstart = pg_extension.errstart
|
||||
errstart_cold = pg_extension.errstart_cold
|
||||
MemoryContextAlloc = pg_extension.MemoryContextAlloc
|
||||
MemoryContextAllocExtended = pg_extension.MemoryContextAllocExtended
|
||||
palloc = pg_extension.palloc
|
||||
palloc0 = pg_extension.palloc0
|
||||
palloc_extended = pg_extension.palloc_extended
|
||||
pg_cryptohash_create = pg_extension.pg_cryptohash_create
|
||||
pg_cryptohash_error = pg_extension.pg_cryptohash_error
|
||||
pg_cryptohash_final = pg_extension.pg_cryptohash_final
|
||||
pg_cryptohash_free = pg_extension.pg_cryptohash_free
|
||||
pg_cryptohash_init = pg_extension.pg_cryptohash_init
|
||||
pg_cryptohash_update = pg_extension.pg_cryptohash_update
|
||||
pg_detoast_datum_packed = pg_extension.pg_detoast_datum_packed
|
||||
strlcpy = pg_extension.strlcpy
|
||||
text_to_cstring = pg_extension.text_to_cstring
|
||||
uuid_in = pg_extension.uuid_in
|
||||
uuid_out = pg_extension.uuid_out
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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.
|
||||
|
||||
//go:build !darwin
|
||||
|
||||
package extension_cgo
|
||||
|
||||
/*
|
||||
#include "exports.h"
|
||||
*/
|
||||
import "C"
|
||||
import "unsafe"
|
||||
|
||||
//export strlcpy
|
||||
func strlcpy(dst *C.char, src *C.pgext_const_char, size C.size_t) C.size_t {
|
||||
var srcLen C.size_t
|
||||
for {
|
||||
if *(*C.char)(unsafe.Pointer(uintptr(unsafe.Pointer(src)) + uintptr(srcLen))) == 0 {
|
||||
break
|
||||
}
|
||||
srcLen++
|
||||
}
|
||||
if size != 0 {
|
||||
n := srcLen
|
||||
if n >= size {
|
||||
n = size - 1
|
||||
}
|
||||
dstSlice := unsafe.Slice((*byte)(unsafe.Pointer(dst)), int(n+1))
|
||||
srcSlice := unsafe.Slice((*byte)(unsafe.Pointer(src)), int(n))
|
||||
copy(dstSlice, srcSlice)
|
||||
dstSlice[n] = 0
|
||||
}
|
||||
return srcLen
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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.
|
||||
|
||||
//go:build darwin
|
||||
|
||||
package extension_cgo
|
||||
|
||||
/*
|
||||
#include "exports.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
func strlcpy(dst *C.char, src *C.pgext_const_char, size C.size_t) C.size_t {
|
||||
return C.strlcpy(dst, src, size)
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// 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 pg_extension
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Library is a fully-loaded extension library.
|
||||
type Library struct {
|
||||
Magic PgMagicStruct
|
||||
Funcs map[string]Function
|
||||
Version Version
|
||||
internal InternalLoadedLibrary
|
||||
}
|
||||
|
||||
// InternalLoadedLibrary is an interface that is implemented by the specific platform to handle library operations.
|
||||
type InternalLoadedLibrary interface {
|
||||
Lookup(sym string) (uintptr, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Function represents an internal library function.
|
||||
type Function struct {
|
||||
Name string
|
||||
Ptr uintptr
|
||||
Args []int
|
||||
APIVersion int
|
||||
// TODO: return type?
|
||||
}
|
||||
|
||||
// PgFunctionInfo is a stand-in for the C struct that reports the function information.
|
||||
type PgFunctionInfo struct {
|
||||
APIVersion int32
|
||||
}
|
||||
|
||||
// PgMagicStruct is a stand-in for the C struct that reports the information of the library.
|
||||
type PgMagicStruct struct {
|
||||
Len int32
|
||||
Version int32
|
||||
FuncMaxArgs int32
|
||||
IndexMaxKeys int32
|
||||
NameDataLen int32
|
||||
Float4ByVal int32
|
||||
Float8ByVal int32
|
||||
}
|
||||
|
||||
var (
|
||||
// loadedLibraries contains all of the loaded libraries.
|
||||
// TODO: need to close all of these before the program ends
|
||||
loadedLibraries = make(map[string]*Library)
|
||||
// loadedLibrariesMutex gates access to the cached libraries.
|
||||
loadedLibrariesMutex = &sync.Mutex{}
|
||||
)
|
||||
|
||||
// LoadLibrary loads the library of the extension, along with preloading all of the functions given.
|
||||
func LoadLibrary(path string, funcNames []string) (*Library, error) {
|
||||
loadedLibrariesMutex.Lock()
|
||||
defer loadedLibrariesMutex.Unlock()
|
||||
|
||||
if lib, ok := loadedLibraries[path]; ok {
|
||||
return lib, nil
|
||||
}
|
||||
internalLib, err := loadLibraryInternal(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
magicPtr, err := internalLib.Lookup("Pg_magic_func")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// We don't free the magic struct since it's a pointer to static memory
|
||||
magicStructDatum, isNotNull := CallFmgrFunction(magicPtr)
|
||||
if !isNotNull {
|
||||
return nil, fmt.Errorf("unable to find magic function for `%s`", path)
|
||||
}
|
||||
magicStruct := *(FromDatum[PgMagicStruct](magicStructDatum))
|
||||
lib := &Library{
|
||||
Magic: magicStruct,
|
||||
Funcs: make(map[string]Function),
|
||||
internal: internalLib,
|
||||
}
|
||||
for _, funcName := range funcNames {
|
||||
finfoPtr, err := internalLib.Lookup(fmt.Sprintf("pg_finfo_%s", funcName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// We don't free finfo since it's a pointer to static memory
|
||||
finfoDatum, isNotNull := CallFmgrFunction(finfoPtr)
|
||||
apiVersion := 0
|
||||
if isNotNull {
|
||||
apiVersion = int(FromDatum[PgFunctionInfo](finfoDatum).APIVersion)
|
||||
}
|
||||
funcPtr, err := internalLib.Lookup(funcName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lib.Funcs[funcName] = Function{
|
||||
Name: funcName,
|
||||
Ptr: funcPtr,
|
||||
Args: nil,
|
||||
APIVersion: apiVersion,
|
||||
}
|
||||
}
|
||||
loadedLibraries[path] = lib
|
||||
return lib, nil
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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.
|
||||
|
||||
//go:build darwin
|
||||
|
||||
package pg_extension
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -ldl
|
||||
#include <dlfcn.h>
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
_ "github.com/dolthub/doltgresql/core/extensions/pg_extension/library"
|
||||
)
|
||||
|
||||
// PLATFORM specifies which platform applies to the current library loader. This will always be a three-letter string.
|
||||
const PLATFORM = "MAC"
|
||||
|
||||
// darwinLib is the Linux-specific implementation of InternalLoadedLibrary.
|
||||
type darwinLib struct {
|
||||
path string
|
||||
handle unsafe.Pointer
|
||||
}
|
||||
|
||||
var _ InternalLoadedLibrary = (*darwinLib)(nil)
|
||||
|
||||
// loadLibraryInternal handles the loading of an extension's SO.
|
||||
func loadLibraryInternal(path string) (InternalLoadedLibrary, error) {
|
||||
pathC := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(pathC))
|
||||
|
||||
handle := C.dlopen(pathC, C.RTLD_LAZY|C.RTLD_GLOBAL)
|
||||
if handle == nil {
|
||||
return nil, fmt.Errorf("error while loading extension `%s`\n%s", path, C.GoString(C.dlerror()))
|
||||
}
|
||||
return &darwinLib{
|
||||
path: path,
|
||||
handle: handle,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Lookup implements the interface InternalLoadedLibrary.
|
||||
func (u *darwinLib) Lookup(sym string) (uintptr, error) {
|
||||
symC := C.CString(sym)
|
||||
defer C.free(unsafe.Pointer(symC))
|
||||
|
||||
ptr := C.dlsym(u.handle, symC)
|
||||
if ptr == nil {
|
||||
return 0, fmt.Errorf("symbol %s not found", sym)
|
||||
}
|
||||
return uintptr(ptr), nil
|
||||
}
|
||||
|
||||
// Close implements the interface InternalLoadedLibrary.
|
||||
func (u *darwinLib) Close() error {
|
||||
if C.dlclose(u.handle) != 0 {
|
||||
return fmt.Errorf("error while closing extension `%s`\n%s", u.path, C.GoString(C.dlerror()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// 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.
|
||||
|
||||
//go:build linux
|
||||
|
||||
package pg_extension
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -ldl
|
||||
#cgo LDFLAGS: -Wl,-E
|
||||
#include <dlfcn.h>
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
_ "github.com/dolthub/doltgresql/core/extensions/pg_extension/library"
|
||||
)
|
||||
|
||||
// PLATFORM specifies which platform applies to the current library loader. This will always be a three-letter string.
|
||||
const PLATFORM = "LIN"
|
||||
|
||||
// unixLib is the Linux-specific implementation of InternalLoadedLibrary.
|
||||
type unixLib struct {
|
||||
path string
|
||||
handle unsafe.Pointer
|
||||
}
|
||||
|
||||
var _ InternalLoadedLibrary = (*unixLib)(nil)
|
||||
|
||||
// loadLibraryInternal handles the loading of an extension's SO.
|
||||
func loadLibraryInternal(path string) (InternalLoadedLibrary, error) {
|
||||
pathC := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(pathC))
|
||||
|
||||
handle := C.dlopen(pathC, C.RTLD_LAZY|C.RTLD_GLOBAL)
|
||||
if handle == nil {
|
||||
return nil, fmt.Errorf("error while loading extension `%s`\n%s", path, C.GoString(C.dlerror()))
|
||||
}
|
||||
return &unixLib{
|
||||
path: path,
|
||||
handle: handle,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Lookup implements the interface InternalLoadedLibrary.
|
||||
func (u *unixLib) Lookup(sym string) (uintptr, error) {
|
||||
symC := C.CString(sym)
|
||||
defer C.free(unsafe.Pointer(symC))
|
||||
|
||||
ptr := C.dlsym(u.handle, symC)
|
||||
if ptr == nil {
|
||||
return 0, fmt.Errorf("symbol %s not found", sym)
|
||||
}
|
||||
return uintptr(ptr), nil
|
||||
}
|
||||
|
||||
// Close implements the interface InternalLoadedLibrary.
|
||||
func (u *unixLib) Close() error {
|
||||
if C.dlclose(u.handle) != 0 {
|
||||
return fmt.Errorf("error while closing extension `%s`\n%s", u.path, C.GoString(C.dlerror()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// 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.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package pg_extension
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// PLATFORM specifies which platform applies to the current library loader. This will always be a three-letter string.
|
||||
const PLATFORM = "WIN"
|
||||
|
||||
//go:embed output/postgres.exe
|
||||
var libDefBytes []byte
|
||||
|
||||
//go:embed output/pg_extension.dll
|
||||
var dllBytes []byte
|
||||
|
||||
// winLib is the Windows-specific implementation of InternalLoadedLibrary.
|
||||
type winLib struct{ dll syscall.Handle }
|
||||
|
||||
var _ InternalLoadedLibrary = (*winLib)(nil)
|
||||
var addPGBinDir = &sync.Once{}
|
||||
|
||||
// loadLibraryInternal handles the loading of an extension's DLL.
|
||||
func loadLibraryInternal(path string) (InternalLoadedLibrary, error) {
|
||||
addPGBinDir.Do(func() {
|
||||
_, currentFileLocation, _, ok := runtime.Caller(0)
|
||||
if !ok || len(currentFileLocation) == 0 {
|
||||
panic("cannot find the directory where this file exists")
|
||||
}
|
||||
// There are three scenarios that we need to consider when attempting to load the DLL:
|
||||
// 1) The DLL exists in an output folder (this will be true for development)
|
||||
// 2) The DLL exists alongside the binary
|
||||
// 3) The DLL does not exist alongside the binary (or is the wrong version)
|
||||
// In the third situation, we write the contained DLL and definition file alongside the binary, so that we'll
|
||||
// always end up in the second situation. This enables both developmental and deployment workflows without
|
||||
// explicit configuration.
|
||||
var dllDir string
|
||||
if _, err := os.Stat(filepath.Join(filepath.Dir(currentFileLocation), "output", "postgres.exe")); err == nil {
|
||||
dllDir = filepath.Join(filepath.Dir(currentFileLocation), "output")
|
||||
} else {
|
||||
currentBinaryLocation, err := os.Executable()
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot find where the executable was launched:\n%s", err.Error()))
|
||||
}
|
||||
dllDir = filepath.Dir(currentBinaryLocation)
|
||||
shouldWriteFiles := false
|
||||
if _, err := os.Stat(filepath.Join(dllDir, "postgres.exe")); err != nil {
|
||||
shouldWriteFiles = true
|
||||
} else {
|
||||
func() {
|
||||
// If the DLL hash doesn't match our hash, then we overwrite it
|
||||
extDll, err := os.Open(filepath.Join(filepath.Dir(currentBinaryLocation), "pg_extension.dll"))
|
||||
if err != nil {
|
||||
shouldWriteFiles = true
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = extDll.Close()
|
||||
}()
|
||||
dllSha := sha256.Sum256(dllBytes)
|
||||
extDllSha := sha256.New()
|
||||
_, _ = io.Copy(extDllSha, extDll)
|
||||
shouldWriteFiles = !bytes.Equal(extDllSha.Sum(nil), dllSha[:])
|
||||
}()
|
||||
}
|
||||
if shouldWriteFiles {
|
||||
writeLocation := filepath.Dir(currentBinaryLocation)
|
||||
_ = os.WriteFile(filepath.Join(writeLocation, "postgres.exe"), libDefBytes, 0755)
|
||||
_ = os.WriteFile(filepath.Join(writeLocation, "pg_extension.dll"), dllBytes, 0755)
|
||||
}
|
||||
}
|
||||
dirPtr, err := syscall.UTF16PtrFromString(dllDir)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_, _, _ = syscall.MustLoadDLL("kernel32.dll").MustFindProc("SetDllDirectoryW").Call(uintptr(unsafe.Pointer(dirPtr)))
|
||||
_, _ = syscall.LoadLibrary(filepath.Join(dllDir, "pg_extension.dll"))
|
||||
})
|
||||
d, err := syscall.LoadLibrary(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &winLib{dll: d}, nil
|
||||
}
|
||||
|
||||
// Lookup implements the interface InternalLoadedLibrary.
|
||||
func (w *winLib) Lookup(sym string) (uintptr, error) {
|
||||
candidates := []string{
|
||||
sym,
|
||||
"_" + sym,
|
||||
sym + "@0",
|
||||
"_" + sym + "@0",
|
||||
}
|
||||
for bytes := 4; bytes <= 64; bytes += 4 {
|
||||
candidates = append(candidates,
|
||||
fmt.Sprintf("%s@%d", sym, bytes),
|
||||
fmt.Sprintf("_%s@%d", sym, bytes))
|
||||
}
|
||||
|
||||
for _, name := range candidates {
|
||||
if p, err := syscall.GetProcAddress(w.dll, name); err == nil {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("symbol %s not found", sym)
|
||||
}
|
||||
|
||||
// Close implements the interface InternalLoadedLibrary.
|
||||
func (w *winLib) Close() error {
|
||||
return syscall.FreeLibrary(w.dll)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// 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 pg_extension
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: "-I${SRCDIR}/library"
|
||||
#include "exports.h"
|
||||
*/
|
||||
import "C"
|
||||
import "unsafe"
|
||||
|
||||
// FromDatum converts the given datum to the type.
|
||||
func FromDatum[T any](d Datum) *T {
|
||||
if d == 0 {
|
||||
return nil
|
||||
}
|
||||
return (*T)(unsafe.Pointer(d))
|
||||
}
|
||||
|
||||
// FromDatumGoString converts the given datum to a string.
|
||||
func FromDatumGoString(d Datum) string {
|
||||
if d == 0 {
|
||||
return ""
|
||||
}
|
||||
return C.GoString((*C.char)(unsafe.Pointer(d)))
|
||||
}
|
||||
|
||||
// FromDatumGoBytes converts the given datum to a byte array of length N.
|
||||
func FromDatumGoBytes(d Datum, n uint) []byte {
|
||||
if d == 0 {
|
||||
return []byte{}
|
||||
}
|
||||
return C.GoBytes(unsafe.Pointer(d), C.int(n))
|
||||
}
|
||||
|
||||
// ToDatum converts the given pointer to a Datum.
|
||||
func ToDatum[T any](val *T) Datum {
|
||||
if val == nil {
|
||||
return 0
|
||||
}
|
||||
return Datum(unsafe.Pointer(val))
|
||||
}
|
||||
|
||||
// ToDatumGoString converts the given string to a Datum.
|
||||
func ToDatumGoString(str string) Datum {
|
||||
return Datum(unsafe.Pointer(C.CString(str)))
|
||||
}
|
||||
|
||||
// ToDatumGoBytes converts the given byte slice to a Datum.
|
||||
func ToDatumGoBytes(data []byte) Datum {
|
||||
return Datum(unsafe.Pointer(C.CBytes(data)))
|
||||
}
|
||||
|
||||
// Malloc allocates the given type within the C heap. These should always be followed up with a Free at some point
|
||||
// afterward.
|
||||
func Malloc[T any]() *T {
|
||||
var structToDetermineSize T
|
||||
return (*T)(C.malloc(C.size_t(unsafe.Sizeof(structToDetermineSize))))
|
||||
}
|
||||
|
||||
// ZeroMemory writes all zeroes to the memory location occupied by the given pointer.
|
||||
func ZeroMemory[T any](val *T) {
|
||||
var structToDetermineSize T
|
||||
C.memset(unsafe.Pointer(val), 0, C.size_t(unsafe.Sizeof(structToDetermineSize)))
|
||||
}
|
||||
|
||||
// Free frees the given pointer from C heap. Generally, this is paired with a pointer returned from Malloc.
|
||||
func Free[T any](val *T) {
|
||||
C.free(unsafe.Pointer(val))
|
||||
}
|
||||
|
||||
// FreeDatum frees the given Datum. Care should be exercised as datums may refer to static memory, and attempting to
|
||||
// free static memory will result in a crash.
|
||||
func FreeDatum(val Datum) {
|
||||
C.free(unsafe.Pointer(val))
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// 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 (
|
||||
"context"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
|
||||
|
||||
"github.com/dolthub/doltgresql/core/id"
|
||||
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
|
||||
pgtypes "github.com/dolthub/doltgresql/server/types"
|
||||
)
|
||||
|
||||
// DeserializeRootObject implements the interface objinterface.Collection.
|
||||
func (pge *Collection) DeserializeRootObject(ctx context.Context, data []byte) (objinterface.RootObject, error) {
|
||||
return DeserializeExtension(ctx, data)
|
||||
}
|
||||
|
||||
// DiffRootObjects implements the interface objinterface.Collection.
|
||||
func (pge *Collection) DiffRootObjects(ctx context.Context, fromHash string, ours objinterface.RootObject, theirs objinterface.RootObject, ancestor objinterface.RootObject) ([]objinterface.RootObjectDiff, objinterface.RootObject, error) {
|
||||
return nil, nil, errors.Errorf("extensions should never produce conflicts")
|
||||
}
|
||||
|
||||
// DropRootObject implements the interface objinterface.Collection.
|
||||
func (pge *Collection) DropRootObject(ctx context.Context, identifier id.Id) error {
|
||||
if identifier.Section() != id.Section_Extension {
|
||||
return errors.Errorf(`extension "%s" does not exist`, identifier.String())
|
||||
}
|
||||
return pge.DropLoadedExtension(ctx, id.Extension(identifier))
|
||||
}
|
||||
|
||||
// GetFieldType implements the interface objinterface.Collection.
|
||||
func (pge *Collection) GetFieldType(ctx context.Context, fieldName string) *pgtypes.DoltgresType {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetID implements the interface objinterface.Collection.
|
||||
func (pge *Collection) GetID() objinterface.RootObjectID {
|
||||
return objinterface.RootObjectID_Extensions
|
||||
}
|
||||
|
||||
// GetRootObject implements the interface objinterface.Collection.
|
||||
func (pge *Collection) GetRootObject(ctx context.Context, identifier id.Id) (objinterface.RootObject, bool, error) {
|
||||
if identifier.Section() != id.Section_Extension {
|
||||
return nil, false, nil
|
||||
}
|
||||
ext, err := pge.GetLoadedExtension(ctx, id.Extension(identifier))
|
||||
return ext, err == nil && ext.Namespace.IsValid(), err
|
||||
}
|
||||
|
||||
// HasRootObject implements the interface objinterface.Collection.
|
||||
func (pge *Collection) HasRootObject(ctx context.Context, identifier id.Id) (bool, error) {
|
||||
if identifier.Section() != id.Section_Extension {
|
||||
return false, nil
|
||||
}
|
||||
return pge.HasLoadedExtension(ctx, id.Extension(identifier)), nil
|
||||
}
|
||||
|
||||
// IDToTableName implements the interface objinterface.Collection.
|
||||
func (pge *Collection) IDToTableName(identifier id.Id) doltdb.TableName {
|
||||
if identifier.Section() != id.Section_Extension {
|
||||
return doltdb.TableName{}
|
||||
}
|
||||
return doltdb.TableName{Name: id.Extension(identifier).Name()}
|
||||
}
|
||||
|
||||
// IterAll implements the interface objinterface.Collection.
|
||||
func (pge *Collection) IterAll(ctx context.Context, callback func(rootObj objinterface.RootObject) (stop bool, err error)) error {
|
||||
for _, extID := range pge.idCache {
|
||||
stop, err := callback(pge.accessCache[extID])
|
||||
if err != nil {
|
||||
return err
|
||||
} else if stop {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IterIDs implements the interface objinterface.Collection.
|
||||
func (pge *Collection) IterIDs(ctx context.Context, callback func(identifier id.Id) (stop bool, err error)) error {
|
||||
for _, extID := range pge.idCache {
|
||||
stop, err := callback(extID.AsId())
|
||||
if err != nil {
|
||||
return err
|
||||
} else if stop {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PutRootObject implements the interface objinterface.Collection.
|
||||
func (pge *Collection) PutRootObject(ctx context.Context, rootObj objinterface.RootObject) error {
|
||||
ext, ok := rootObj.(Extension)
|
||||
if !ok {
|
||||
return errors.Newf("invalid extension root object: %T", rootObj)
|
||||
}
|
||||
return pge.AddLoadedExtension(ctx, ext)
|
||||
}
|
||||
|
||||
// RenameRootObject implements the interface objinterface.Collection.
|
||||
func (pge *Collection) RenameRootObject(ctx context.Context, oldName id.Id, newName id.Id) error {
|
||||
return errors.New(`extensions cannot be renamed`)
|
||||
}
|
||||
|
||||
// ResolveName implements the interface objinterface.Collection.
|
||||
func (pge *Collection) ResolveName(ctx context.Context, name doltdb.TableName) (doltdb.TableName, id.Id, error) {
|
||||
extID := id.NewExtension(name.Name)
|
||||
if pge.HasLoadedExtension(ctx, extID) {
|
||||
return doltdb.TableName{Name: name.Name}, extID.AsId(), nil
|
||||
}
|
||||
return doltdb.TableName{}, id.Null, nil
|
||||
}
|
||||
|
||||
// TableNameToID implements the interface objinterface.Collection.
|
||||
func (pge *Collection) TableNameToID(name doltdb.TableName) id.Id {
|
||||
return id.NewExtension(name.Name).AsId()
|
||||
}
|
||||
|
||||
// UpdateField implements the interface objinterface.Collection.
|
||||
func (pge *Collection) UpdateField(ctx context.Context, rootObject objinterface.RootObject, fieldName string, newValue any) (objinterface.RootObject, error) {
|
||||
return nil, errors.New("updating through the conflicts table for this object type is not yet supported")
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// 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 (
|
||||
"context"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
|
||||
"github.com/dolthub/doltgresql/core/id"
|
||||
"github.com/dolthub/doltgresql/utils"
|
||||
)
|
||||
|
||||
// Serialize returns the Extension as a byte slice. If the Extension is invalid (invalid ExtName), then this returns a
|
||||
// nil slice.
|
||||
func (ext Extension) Serialize(ctx context.Context) ([]byte, error) {
|
||||
if !ext.ExtName.IsValid() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Initialize the writer
|
||||
writer := utils.NewWriter(256)
|
||||
writer.VariableUint(0) // Version
|
||||
// Write the extension data
|
||||
writer.Id(ext.ExtName.AsId())
|
||||
writer.Id(ext.Namespace.AsId())
|
||||
writer.Bool(ext.Relocatable)
|
||||
writer.String(string(ext.LibIdentifier))
|
||||
// Returns the data
|
||||
return writer.Data(), nil
|
||||
}
|
||||
|
||||
// DeserializeExtension returns the Extension that was serialized in the byte slice. Returns an empty Extension (has an
|
||||
// invalid ID) if data is nil or empty.
|
||||
func DeserializeExtension(ctx context.Context, data []byte) (Extension, error) {
|
||||
if len(data) == 0 {
|
||||
return Extension{}, nil
|
||||
}
|
||||
reader := utils.NewReader(data)
|
||||
version := reader.VariableUint()
|
||||
if version != 0 {
|
||||
return Extension{}, errors.Errorf("version %d of extensions are not supported, please upgrade the server", version)
|
||||
}
|
||||
|
||||
// Read from the reader
|
||||
ext := Extension{}
|
||||
ext.ExtName = id.Extension(reader.Id())
|
||||
ext.Namespace = id.Namespace(reader.Id())
|
||||
ext.Relocatable = reader.Bool()
|
||||
ext.LibIdentifier = LibraryIdentifier(reader.String())
|
||||
if !reader.IsEmpty() {
|
||||
return Extension{}, errors.Errorf("extra data found while deserializing an extension")
|
||||
}
|
||||
// Return the deserialized object
|
||||
return ext, nil
|
||||
}
|
||||
Reference in New Issue
Block a user