chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:32:25 +08:00
commit e014feafe1
2285 changed files with 1131979 additions and 0 deletions
+234
View File
@@ -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()}
}
+134
View File
@@ -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)
}
+199
View File
@@ -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())
}
+12
View File
@@ -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
```
+56
View File
@@ -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
View File
@@ -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)
}
+88
View File
@@ -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))
}
+138
View File
@@ -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")
}
+68
View File
@@ -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
}