Revamped extensions to use emulation
This commit is contained in:
committed by
Daylon Wilkins
parent
8e44f2abd9
commit
af0b3a8f1c
@@ -23,29 +23,8 @@ jobs:
|
||||
fi
|
||||
echo "version=$version" >> $GITHUB_OUTPUT
|
||||
|
||||
windows-extension-support:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ secrets.PUBLIC_REPO_ACCESS_TOKEN || secrets.REPO_ACCESS_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
- name: Build SQL Syntax
|
||||
run: ./build.sh
|
||||
working-directory: ./postgres/parser
|
||||
shell: bash
|
||||
- name: Upload Extension Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-extension-artifacts
|
||||
path: |
|
||||
./core/extensions/pg_extension/output/pg_extension.dll
|
||||
./core/extensions/pg_extension/output/postgres.exe
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
create-release:
|
||||
needs: [format-version, windows-extension-support]
|
||||
needs: [format-version]
|
||||
name: Create release
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
@@ -89,11 +68,6 @@ jobs:
|
||||
run: gh pr merge --merge --auto "cd-release"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PUBLIC_REPO_ACCESS_TOKEN || secrets.REPO_ACCESS_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
- name: Download Extension Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: windows-extension-artifacts
|
||||
path: ./core/extensions/pg_extension/output
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
|
||||
@@ -26,6 +26,3 @@ scripts/mini_sysbench
|
||||
# ignore doltgres db created
|
||||
doltgres
|
||||
auth.db
|
||||
|
||||
# ignore extensions output
|
||||
core/extensions/pg_extension/output
|
||||
|
||||
@@ -39,12 +39,12 @@ type Collection struct {
|
||||
ns tree.NodeStore
|
||||
}
|
||||
|
||||
// Extension represents a loaded extension.
|
||||
// Extension represents an extension that has been installed into a database.
|
||||
type Extension struct {
|
||||
ExtName id.Extension
|
||||
Namespace id.Namespace
|
||||
Relocatable bool
|
||||
LibIdentifier LibraryIdentifier
|
||||
Version string
|
||||
// TODO: keep track of what it references so I can later delete them
|
||||
}
|
||||
|
||||
@@ -201,12 +201,9 @@ func (pge *Collection) reloadCaches(ctx context.Context) error {
|
||||
})
|
||||
}
|
||||
|
||||
// CompareVersions compares the major and minor version of the extension versus the given extension.
|
||||
// CompareVersions compares the version of the extension versus the given extension, as opaque strings.
|
||||
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()),
|
||||
)
|
||||
return cmp.Compare(ext.Version, other.Version)
|
||||
}
|
||||
|
||||
// GetID implements the interface objinterface.RootObject.
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
// 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()
|
||||
|
||||
exts, err := getAllExtensions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ext, ok := exts[name]
|
||||
if !ok {
|
||||
return nil, errors.Errorf(`could not open extension control file "%s.control"`, name)
|
||||
}
|
||||
return ext, nil
|
||||
}
|
||||
|
||||
// GetAllExtensions returns all extensions that are available for installation on this system, keyed by extension name.
|
||||
// Returns an error if the extensions could not be loaded (e.g. there is no local Postgres installation). The returned
|
||||
// map must not be modified.
|
||||
func GetAllExtensions() (map[string]*pg_extension.ExtensionFiles, error) {
|
||||
extMutex.Lock()
|
||||
defer extMutex.Unlock()
|
||||
|
||||
return getAllExtensions()
|
||||
}
|
||||
|
||||
// getAllExtensions loads (and caches) all extensions that are available on this system. The mutex extMutex must be
|
||||
// held when this is called.
|
||||
func getAllExtensions() (_ map[string]*pg_extension.ExtensionFiles, err error) {
|
||||
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
|
||||
}
|
||||
}
|
||||
return allExtensions, 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())
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
# 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
|
||||
```
|
||||
@@ -1,56 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,380 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
$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"
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/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}" .
|
||||
)
|
||||
@@ -1,57 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
// 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
|
||||
@@ -1,132 +0,0 @@
|
||||
// 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("")
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
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
|
||||
@@ -1,45 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
// 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"
|
||||
"errors"
|
||||
"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"
|
||||
|
||||
var ErrExtensionSupportUnavailable = errors.New("extension support is unavailable: this binary was built without " +
|
||||
"the Windows extension support artifacts embedded, and pg_extension.dll was not found alongside the executable. " +
|
||||
"Rebuild with the `pg_extension_embed` build tag, after running " +
|
||||
"core/extensions/pg_extension/library/build_library.sh on Windows to produce them")
|
||||
|
||||
// winLib is the Windows-specific implementation of InternalLoadedLibrary.
|
||||
type winLib struct{ dll syscall.Handle }
|
||||
|
||||
var _ InternalLoadedLibrary = (*winLib)(nil)
|
||||
var addPGBinDir = &sync.Once{}
|
||||
var addPGBinDirErr error
|
||||
|
||||
// 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)
|
||||
if len(libDefBytes) == 0 || len(dllBytes) == 0 {
|
||||
if _, err := os.Stat(filepath.Join(dllDir, "pg_extension.dll")); err != nil {
|
||||
addPGBinDirErr = ErrExtensionSupportUnavailable
|
||||
return
|
||||
}
|
||||
} else {
|
||||
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"))
|
||||
})
|
||||
if addPGBinDirErr != nil {
|
||||
return nil, addPGBinDirErr
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// 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 && pg_extension_embed
|
||||
|
||||
package pg_extension
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed output/postgres.exe
|
||||
var libDefBytes []byte
|
||||
|
||||
//go:embed output/pg_extension.dll
|
||||
var dllBytes []byte
|
||||
@@ -1,21 +0,0 @@
|
||||
// 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 && !pg_extension_embed
|
||||
|
||||
package pg_extension
|
||||
|
||||
var libDefBytes []byte
|
||||
|
||||
var dllBytes []byte
|
||||
@@ -1,88 +0,0 @@
|
||||
// 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))
|
||||
}
|
||||
@@ -32,12 +32,12 @@ func (ext Extension) Serialize(ctx context.Context) ([]byte, error) {
|
||||
|
||||
// Initialize the writer
|
||||
writer := utils.NewWriter(256)
|
||||
writer.VariableUint(0) // Version
|
||||
writer.VariableUint(1) // Version
|
||||
// Write the extension data
|
||||
writer.Id(ext.ExtName.AsId())
|
||||
writer.Id(ext.Namespace.AsId())
|
||||
writer.Bool(ext.Relocatable)
|
||||
writer.String(string(ext.LibIdentifier))
|
||||
writer.String(ext.Version)
|
||||
// Returns the data
|
||||
return writer.Data(), nil
|
||||
}
|
||||
@@ -50,7 +50,12 @@ func DeserializeExtension(ctx context.Context, data []byte) (Extension, error) {
|
||||
}
|
||||
reader := utils.NewReader(data)
|
||||
version := reader.VariableUint()
|
||||
if version != 0 {
|
||||
switch version {
|
||||
case 0:
|
||||
return Extension{}, errors.New("extensions have been completely revamped, please reimport your database using a newer version")
|
||||
case 1:
|
||||
// current version
|
||||
default:
|
||||
return Extension{}, errors.Errorf("version %d of extensions are not supported, please upgrade the server", version)
|
||||
}
|
||||
|
||||
@@ -59,7 +64,7 @@ func DeserializeExtension(ctx context.Context, data []byte) (Extension, error) {
|
||||
ext.ExtName = id.Extension(reader.Id())
|
||||
ext.Namespace = id.Namespace(reader.Id())
|
||||
ext.Relocatable = reader.Bool()
|
||||
ext.LibIdentifier = LibraryIdentifier(reader.String())
|
||||
ext.Version = reader.String()
|
||||
if !reader.IsEmpty() {
|
||||
return Extension{}, errors.Errorf("extra data found while deserializing an extension")
|
||||
}
|
||||
|
||||
@@ -519,18 +519,20 @@ func RemoveRootObjectIfExists(ctx context.Context, root objinterface.RootValue,
|
||||
|
||||
// ResolveName returns the fully resolved name of the given item (if the item exists). Also returns the type of the item.
|
||||
func ResolveName(ctx context.Context, root objinterface.RootValue, name doltdb.TableName) (doltdb.TableName, id.Id, objinterface.RootObjectID, error) {
|
||||
colls, err := LoadAllCollections(ctx, root)
|
||||
if err != nil {
|
||||
return doltdb.TableName{}, id.Null, objinterface.RootObjectID_None, err
|
||||
}
|
||||
return ResolveNameOnCollections(ctx, colls, name)
|
||||
}
|
||||
|
||||
// ResolveNameOnCollections is ResolveName, but for collections that have already been loaded.
|
||||
func ResolveNameOnCollections(ctx context.Context, colls []objinterface.Collection, name doltdb.TableName) (doltdb.TableName, id.Id, objinterface.RootObjectID, error) {
|
||||
var resolvedName doltdb.TableName
|
||||
resolvedRawID := id.Null
|
||||
resolvedObjID := objinterface.RootObjectID_None
|
||||
|
||||
for i, emptyColl := range globalCollections {
|
||||
if emptyColl == nil || i == int(objinterface.RootObjectID_Conflicts) {
|
||||
continue
|
||||
}
|
||||
coll, err := emptyColl.LoadCollection(ctx, root)
|
||||
if err != nil {
|
||||
return doltdb.TableName{}, id.Null, objinterface.RootObjectID_None, err
|
||||
}
|
||||
for _, coll := range colls {
|
||||
if coll == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
+8
-1
@@ -254,9 +254,16 @@ func (root *RootValue) DebugString(ctx context.Context, transitive bool) string
|
||||
|
||||
// FilterRootObjectNames implements the interface doltdb.RootValue.
|
||||
func (root *RootValue) FilterRootObjectNames(ctx context.Context, names []doltdb.TableName) ([]doltdb.TableName, error) {
|
||||
if len(names) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
colls, err := rootobject.LoadAllCollections(ctx, root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var returnNames []doltdb.TableName
|
||||
for _, name := range names {
|
||||
_, _, objID, err := rootobject.ResolveName(ctx, root, name)
|
||||
_, _, objID, err := rootobject.ResolveNameOnCollections(ctx, colls, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -52,6 +52,3 @@ gofmt -s -w parser/help_messages.go
|
||||
sed -E 's/^const ([A-Z][_A-Z0-9]*) =.*$/const \1 = lex.\1/g') > parser/sql.go.tmp || rm parser/sql.go.tmp
|
||||
mv -f parser/sql.go.tmp parser/sql.go
|
||||
go run golang.org/x/tools/cmd/goimports -local github.com/dolthub/doltgresql -w parser/sql.go
|
||||
|
||||
# Build extension support
|
||||
../../core/extensions/pg_extension/library/build_library.sh
|
||||
@@ -75,15 +75,6 @@ for tuple in $OS_ARCH_TUPLES; do
|
||||
tags="icu_static"
|
||||
if [ "$os" = windows ]; then
|
||||
bin="$bin.exe"
|
||||
tags="$tags,pg_extension_embed"
|
||||
for f in postgres.exe pg_extension.dll; do
|
||||
if [ ! -f "core/extensions/pg_extension/output/$f" ]; then
|
||||
echo "ERROR: core/extensions/pg_extension/output/$f is missing." >&2
|
||||
echo "It is built by core/extensions/pg_extension/library/build_library.sh on a Windows host," >&2
|
||||
echo "and is required by the pg_extension_embed build tag." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
echo Building "$o/bin/$bin"
|
||||
CGO_ENABLED=1 \
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"github.com/dolthub/go-mysql-server/sql/transform"
|
||||
|
||||
"github.com/dolthub/doltgresql/core"
|
||||
"github.com/dolthub/doltgresql/core/extensions"
|
||||
"github.com/dolthub/doltgresql/core/id"
|
||||
"github.com/dolthub/doltgresql/server/functions"
|
||||
"github.com/dolthub/doltgresql/server/functions/framework"
|
||||
@@ -72,14 +71,14 @@ func ResolveProcedureDefaults(ctx *sql.Context, a *analyzer.Analyzer, node sql.N
|
||||
// TODO: we should probably have procedure equivalents instead of converting these to functions
|
||||
// probably fine for now since we don't implement/support the differing functionality between the two just yet
|
||||
if len(overload.ExtensionName) > 0 {
|
||||
if err = overloadTree.Add(framework.CFunction{
|
||||
if err = overloadTree.Add(framework.ExtensionFunction{
|
||||
ID: id.Function(overload.ID),
|
||||
ReturnType: pgtypes.Void,
|
||||
ParameterTypes: paramTypes,
|
||||
Variadic: false,
|
||||
IsNonDeterministic: true,
|
||||
Strict: false,
|
||||
ExtensionName: extensions.LibraryIdentifier(overload.ExtensionName),
|
||||
ExtensionName: overload.ExtensionName,
|
||||
ExtensionSymbol: overload.ExtensionSymbol,
|
||||
}); err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2026 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 extdef
|
||||
|
||||
import (
|
||||
"github.com/dolthub/go-mysql-server/sql"
|
||||
)
|
||||
|
||||
// Function is the Go implementation of a single function that an extension provides.
|
||||
type Function func(ctx *sql.Context, args ...any) (any, error)
|
||||
|
||||
// Control holds the information that an extension declares in its control file.
|
||||
// https://www.postgresql.org/docs/15/extend-extensions.html#id-1.8.3.20.11
|
||||
type Control struct {
|
||||
DefaultVersion string
|
||||
Comment string
|
||||
Requires []string
|
||||
Superuser bool
|
||||
Trusted bool
|
||||
Relocatable bool
|
||||
Schema string
|
||||
}
|
||||
|
||||
// Extension is a Postgres extension that Doltgres emulates.
|
||||
type Extension struct {
|
||||
Name string
|
||||
Control Control
|
||||
Script string
|
||||
Functions map[string]Function
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright 2026 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 holds the registry of every Postgres extension that Doltgres emulates. Each
|
||||
// emulated extension lives in its own subdirectory and is registered from Init().
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
|
||||
"github.com/dolthub/doltgresql/server/extensions/extdef"
|
||||
uuid_ossp "github.com/dolthub/doltgresql/server/extensions/uuid-ossp"
|
||||
)
|
||||
|
||||
// registry holds every extension that Doltgres emulates, keyed by its case-sensitive name.
|
||||
var registry = map[string]*extdef.Extension{}
|
||||
|
||||
// Init adds every emulated extension to the registry, making them installable through CREATE EXTENSION.
|
||||
func Init() {
|
||||
register(uuid_ossp.Extension())
|
||||
}
|
||||
|
||||
// register adds the given extension to the registry, and strips the psql meta-commands from its Script.
|
||||
func register(ext *extdef.Extension) {
|
||||
if _, ok := registry[ext.Name]; ok {
|
||||
panic(errors.Errorf(`extension "%s" has already been registered`, ext.Name))
|
||||
}
|
||||
ext.Script = stripMetaCommands(ext.Script)
|
||||
registry[ext.Name] = ext
|
||||
}
|
||||
|
||||
// stripMetaCommands removes the psql meta-command lines from an installation script, such as the
|
||||
// `\echo ... \quit` guard that nearly every script Postgres ships opens with.
|
||||
func stripMetaCommands(script string) string {
|
||||
lines := strings.Split(script, "\n")
|
||||
kept := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, `\`) {
|
||||
continue
|
||||
}
|
||||
kept = append(kept, line)
|
||||
}
|
||||
return strings.Join(kept, "\n")
|
||||
}
|
||||
|
||||
// Get returns the emulated extension with the given name, or an error if Doltgres does not emulate it.
|
||||
func Get(name string) (*extdef.Extension, error) {
|
||||
ext, ok := registry[name]
|
||||
if !ok {
|
||||
return nil, errors.Errorf(`extension "%s" is not available`, name)
|
||||
}
|
||||
return ext, nil
|
||||
}
|
||||
|
||||
// GetAll returns every extension that Doltgres emulates, keyed by name. The map must not be modified.
|
||||
func GetAll() map[string]*extdef.Extension {
|
||||
return registry
|
||||
}
|
||||
|
||||
// GetFunction returns the implementation of the given symbol within the given extension.
|
||||
func GetFunction(extensionName string, symbol string) (extdef.Function, error) {
|
||||
ext, err := Get(extensionName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f, ok := ext.Functions[symbol]
|
||||
if !ok {
|
||||
return nil, errors.Errorf(`extension "%s" does not declare the function "%s"`, extensionName, symbol)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2026 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 (
|
||||
"testing"
|
||||
|
||||
"github.com/dolthub/go-mysql-server/sql"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/dolthub/doltgresql/server/extensions/extdef"
|
||||
)
|
||||
|
||||
// testFunction is a stand-in implementation for the extension that these tests register.
|
||||
func testFunction(ctx *sql.Context, args ...any) (any, error) {
|
||||
return "called", nil
|
||||
}
|
||||
|
||||
func TestRegistry(t *testing.T) {
|
||||
register(&extdef.Extension{
|
||||
Name: "doltgres_test",
|
||||
Control: extdef.Control{DefaultVersion: "2.5", Comment: "a test extension", Relocatable: true},
|
||||
Script: "\\echo Use \"CREATE EXTENSION doltgres_test\" to load this file. \\quit\n" +
|
||||
`CREATE FUNCTION alpha() RETURNS uuid AS 'MODULE_PATHNAME', 'alpha' LANGUAGE C;`,
|
||||
Functions: map[string]extdef.Function{"alpha": testFunction},
|
||||
})
|
||||
|
||||
ext, err := Get("doltgres_test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `CREATE FUNCTION alpha() RETURNS uuid AS 'MODULE_PATHNAME', 'alpha' LANGUAGE C;`, ext.Script)
|
||||
require.Equal(t, "2.5", ext.Control.DefaultVersion)
|
||||
require.Equal(t, "a test extension", ext.Control.Comment)
|
||||
require.True(t, ext.Control.Relocatable)
|
||||
require.Contains(t, GetAll(), "doltgres_test")
|
||||
|
||||
f, err := GetFunction("doltgres_test", "alpha")
|
||||
require.NoError(t, err)
|
||||
result, err := f(nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "called", result)
|
||||
|
||||
_, err = Get("doltgres_test_missing")
|
||||
require.ErrorContains(t, err, `extension "doltgres_test_missing" is not available`)
|
||||
// Extension names are case-sensitive in Postgres, so a differently-cased name does not match
|
||||
_, err = Get("DOLTGRES_TEST")
|
||||
require.ErrorContains(t, err, `extension "DOLTGRES_TEST" is not available`)
|
||||
_, err = GetFunction("doltgres_test", "beta")
|
||||
require.ErrorContains(t, err, `extension "doltgres_test" does not declare the function "beta"`)
|
||||
_, err = GetFunction("doltgres_test_missing", "alpha")
|
||||
require.ErrorContains(t, err, `extension "doltgres_test_missing" is not available`)
|
||||
|
||||
// Registering the same extension twice would silently replace the first, so it panics instead
|
||||
require.Panics(t, func() {
|
||||
register(&extdef.Extension{Name: "doltgres_test", Control: extdef.Control{DefaultVersion: "2.5"}})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/* contrib/uuid-ossp/uuid-ossp--1.1.sql */
|
||||
|
||||
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
|
||||
\echo Use '''CREATE EXTENSION "uuid-ossp"''' to load this file. \quit
|
||||
|
||||
CREATE FUNCTION uuid_nil()
|
||||
RETURNS uuid
|
||||
AS 'MODULE_PATHNAME', 'uuid_nil'
|
||||
IMMUTABLE STRICT LANGUAGE C PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION uuid_ns_dns()
|
||||
RETURNS uuid
|
||||
AS 'MODULE_PATHNAME', 'uuid_ns_dns'
|
||||
IMMUTABLE STRICT LANGUAGE C PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION uuid_ns_url()
|
||||
RETURNS uuid
|
||||
AS 'MODULE_PATHNAME', 'uuid_ns_url'
|
||||
IMMUTABLE STRICT LANGUAGE C PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION uuid_ns_oid()
|
||||
RETURNS uuid
|
||||
AS 'MODULE_PATHNAME', 'uuid_ns_oid'
|
||||
IMMUTABLE STRICT LANGUAGE C PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION uuid_ns_x500()
|
||||
RETURNS uuid
|
||||
AS 'MODULE_PATHNAME', 'uuid_ns_x500'
|
||||
IMMUTABLE STRICT LANGUAGE C PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION uuid_generate_v1()
|
||||
RETURNS uuid
|
||||
AS 'MODULE_PATHNAME', 'uuid_generate_v1'
|
||||
VOLATILE STRICT LANGUAGE C PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION uuid_generate_v1mc()
|
||||
RETURNS uuid
|
||||
AS 'MODULE_PATHNAME', 'uuid_generate_v1mc'
|
||||
VOLATILE STRICT LANGUAGE C PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION uuid_generate_v3(namespace uuid, name text)
|
||||
RETURNS uuid
|
||||
AS 'MODULE_PATHNAME', 'uuid_generate_v3'
|
||||
IMMUTABLE STRICT LANGUAGE C PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION uuid_generate_v4()
|
||||
RETURNS uuid
|
||||
AS 'MODULE_PATHNAME', 'uuid_generate_v4'
|
||||
VOLATILE STRICT LANGUAGE C PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION uuid_generate_v5(namespace uuid, name text)
|
||||
RETURNS uuid
|
||||
AS 'MODULE_PATHNAME', 'uuid_generate_v5'
|
||||
IMMUTABLE STRICT LANGUAGE C PARALLEL SAFE;
|
||||
@@ -0,0 +1,142 @@
|
||||
// Copyright 2026 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 uuid_ossp
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/dolthub/go-mysql-server/sql"
|
||||
|
||||
"github.com/dolthub/doltgresql/postgres/parser/uuid"
|
||||
"github.com/dolthub/doltgresql/server/extensions/extdef"
|
||||
)
|
||||
|
||||
//go:embed uuid-ossp--1.1.sql
|
||||
var script string
|
||||
|
||||
// Extension returns the definition of the emulated extension.
|
||||
func Extension() *extdef.Extension {
|
||||
return &extdef.Extension{
|
||||
Name: "uuid-ossp",
|
||||
Control: extdef.Control{
|
||||
DefaultVersion: "1.1",
|
||||
Comment: "generate universally unique identifiers (UUIDs)",
|
||||
Superuser: true,
|
||||
Trusted: true,
|
||||
Relocatable: true,
|
||||
},
|
||||
Script: script,
|
||||
Functions: map[string]extdef.Function{
|
||||
"uuid_nil": uuidNil,
|
||||
"uuid_ns_dns": uuidNsDns,
|
||||
"uuid_ns_url": uuidNsURL,
|
||||
"uuid_ns_oid": uuidNsOID,
|
||||
"uuid_ns_x500": uuidNsX500,
|
||||
"uuid_generate_v1": uuidGenerateV1,
|
||||
"uuid_generate_v1mc": uuidGenerateV1mc,
|
||||
"uuid_generate_v3": uuidGenerateV3,
|
||||
"uuid_generate_v4": uuidGenerateV4,
|
||||
"uuid_generate_v5": uuidGenerateV5,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// uuidNil implements uuid_nil, which returns the "nil" UUID.
|
||||
func uuidNil(ctx *sql.Context, args ...any) (any, error) {
|
||||
return uuid.Nil, nil
|
||||
}
|
||||
|
||||
// uuidNsDns implements uuid_ns_dns, which returns the RFC 4122 namespace identifier for DNS names.
|
||||
func uuidNsDns(ctx *sql.Context, args ...any) (any, error) {
|
||||
return uuid.NamespaceDNS, nil
|
||||
}
|
||||
|
||||
// uuidNsURL implements uuid_ns_url, which returns the RFC 4122 namespace identifier for URLs.
|
||||
func uuidNsURL(ctx *sql.Context, args ...any) (any, error) {
|
||||
return uuid.NamespaceURL, nil
|
||||
}
|
||||
|
||||
// uuidNsOID implements uuid_ns_oid, which returns the RFC 4122 namespace identifier for ISO OIDs.
|
||||
func uuidNsOID(ctx *sql.Context, args ...any) (any, error) {
|
||||
return uuid.NamespaceOID, nil
|
||||
}
|
||||
|
||||
// uuidNsX500 implements uuid_ns_x500, which returns the RFC 4122 namespace identifier for X.500 names.
|
||||
func uuidNsX500(ctx *sql.Context, args ...any) (any, error) {
|
||||
return uuid.NamespaceX500, nil
|
||||
}
|
||||
|
||||
// uuidGenerateV1 implements uuid_generate_v1, which returns a version 1 UUID built from the timestamp,
|
||||
// a clock sequence, and this computer's MAC address.
|
||||
func uuidGenerateV1(ctx *sql.Context, args ...any) (any, error) {
|
||||
return uuid.NewV1()
|
||||
}
|
||||
|
||||
// uuidGenerateV1mc implements uuid_generate_v1mc, which returns a version 1 UUID whose node is a random
|
||||
// address with the IEEE 802 multicast and locally-administered bits set.
|
||||
func uuidGenerateV1mc(ctx *sql.Context, args ...any) (any, error) {
|
||||
newUUID, err := uuid.NewV1()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
randomUUID, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
copy(newUUID[10:], randomUUID[10:])
|
||||
newUUID[10] |= 0x03
|
||||
return newUUID, nil
|
||||
}
|
||||
|
||||
// uuidGenerateV3 implements uuid_generate_v3, which returns a version 3 UUID formed from the MD5 hash
|
||||
// of the given namespace and name.
|
||||
func uuidGenerateV3(ctx *sql.Context, args ...any) (any, error) {
|
||||
namespace, name, err := namespaceAndName(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return uuid.NewV3(namespace, name), nil
|
||||
}
|
||||
|
||||
// uuidGenerateV4 implements uuid_generate_v4, which returns a version 4 UUID composed entirely of
|
||||
// random data.
|
||||
func uuidGenerateV4(ctx *sql.Context, args ...any) (any, error) {
|
||||
return uuid.NewV4()
|
||||
}
|
||||
|
||||
// uuidGenerateV5 implements uuid_generate_v5, which returns a version 5 UUID formed from the SHA-1
|
||||
// hash of the given namespace and name.
|
||||
func uuidGenerateV5(ctx *sql.Context, args ...any) (any, error) {
|
||||
namespace, name, err := namespaceAndName(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return uuid.NewV5(namespace, name), nil
|
||||
}
|
||||
|
||||
// namespaceAndName reads the namespace and name arguments that uuid_generate_v3 and uuid_generate_v5
|
||||
// share.
|
||||
func namespaceAndName(args []any) (uuid.UUID, string, error) {
|
||||
namespace, ok := args[0].(uuid.UUID)
|
||||
if !ok {
|
||||
return uuid.Nil, "", errors.Errorf("expected a UUID namespace, received `%T`", args[0])
|
||||
}
|
||||
name, ok := args[1].(string)
|
||||
if !ok {
|
||||
return uuid.Nil, "", errors.Errorf("expected a TEXT name, received `%T`", args[1])
|
||||
}
|
||||
return namespace, name, nil
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// 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 framework
|
||||
|
||||
import (
|
||||
"github.com/dolthub/doltgresql/core/extensions/pg_extension"
|
||||
|
||||
"github.com/dolthub/doltgresql/core/id"
|
||||
"github.com/dolthub/doltgresql/postgres/parser/uuid"
|
||||
pgtypes "github.com/dolthub/doltgresql/server/types"
|
||||
)
|
||||
|
||||
var cConversionToDatumMap = map[id.Type]func(val any) (pg_extension.NullableDatum, error){
|
||||
pgtypes.Text.ID: textToDatum,
|
||||
pgtypes.Uuid.ID: uuidToDatum,
|
||||
}
|
||||
var cConversionFromDatumMap = map[id.Type]func(datum pg_extension.Datum) (any, error){
|
||||
pgtypes.Text.ID: textFromDatum,
|
||||
pgtypes.Uuid.ID: uuidFromDatum,
|
||||
}
|
||||
|
||||
// textFromDatum converts from a Datum to a TEXT value.
|
||||
func textFromDatum(datum pg_extension.Datum) (any, error) {
|
||||
convertedVal := pg_extension.FromDatumGoString(datum)
|
||||
pg_extension.FreeDatum(datum)
|
||||
return convertedVal, nil
|
||||
}
|
||||
|
||||
// textToDatum converts from a TEXT value to a NullableDatum.
|
||||
func textToDatum(val any) (pg_extension.NullableDatum, error) {
|
||||
if val == nil {
|
||||
return pg_extension.NullableDatum{
|
||||
Value: 0,
|
||||
IsNull: true,
|
||||
}, nil
|
||||
}
|
||||
return pg_extension.NullableDatum{
|
||||
Value: pg_extension.ToDatumGoString(val.(string)),
|
||||
IsNull: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// uuidFromDatum converts from a Datum to a UUID value.
|
||||
func uuidFromDatum(datum pg_extension.Datum) (any, error) {
|
||||
convertedVal := pg_extension.FromDatumGoBytes(datum, 16)
|
||||
pg_extension.FreeDatum(datum)
|
||||
return uuid.FromBytes(convertedVal)
|
||||
}
|
||||
|
||||
// uuidToDatum converts from a UUID value to a NullableDatum.
|
||||
func uuidToDatum(val any) (pg_extension.NullableDatum, error) {
|
||||
if val == nil {
|
||||
return pg_extension.NullableDatum{
|
||||
Value: 0,
|
||||
IsNull: true,
|
||||
}, nil
|
||||
}
|
||||
return pg_extension.NullableDatum{
|
||||
Value: pg_extension.ToDatumGoBytes(val.(uuid.UUID).GetBytes()),
|
||||
IsNull: false,
|
||||
}, nil
|
||||
}
|
||||
@@ -26,10 +26,9 @@ import (
|
||||
|
||||
"github.com/dolthub/doltgresql/core"
|
||||
"github.com/dolthub/doltgresql/core/casts"
|
||||
"github.com/dolthub/doltgresql/core/extensions"
|
||||
"github.com/dolthub/doltgresql/core/extensions/pg_extension"
|
||||
"github.com/dolthub/doltgresql/core/id"
|
||||
procedures2 "github.com/dolthub/doltgresql/core/procedures"
|
||||
"github.com/dolthub/doltgresql/server/extensions"
|
||||
"github.com/dolthub/doltgresql/server/plpgsql"
|
||||
pgtypes "github.com/dolthub/doltgresql/server/types"
|
||||
)
|
||||
@@ -421,36 +420,12 @@ func (c *CompiledFunction) Eval(ctx *sql.Context, row sql.Row) (interface{}, err
|
||||
return f.Callable(ctx, ([8]*pgtypes.DoltgresType)(c.callResolved), args[0], args[1], args[2], args[3], args[4], args[5], args[6])
|
||||
case InterpretedFunction:
|
||||
return plpgsql.Call(ctx, f, c.runner, c.callResolved, args)
|
||||
case CFunction:
|
||||
cfunc, err := extensions.GetExtensionFunction(f.ExtensionName, f.ExtensionSymbol)
|
||||
case ExtensionFunction:
|
||||
extFunc, err := extensions.GetFunction(f.ExtensionName, f.ExtensionSymbol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cargs := make([]pg_extension.NullableDatum, len(args))
|
||||
for i, argType := range f.ParameterTypes { // TODO: ParameterTypes does not account for variadic parameters
|
||||
cConvFunc, ok := cConversionToDatumMap[argType.ID]
|
||||
if !ok {
|
||||
return nil, cerrors.Errorf("no conversion function from Go to C for `%s`", argType.ID.TypeName())
|
||||
}
|
||||
cargs[i], err = cConvFunc(args[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
result, isNotNull := pg_extension.CallFmgrFunction(cfunc.Ptr, cargs...)
|
||||
if isNotNull {
|
||||
cConvFunc, ok := cConversionFromDatumMap[f.ReturnType.ID]
|
||||
if !ok {
|
||||
return nil, cerrors.Errorf("no conversion function from C to Go for `%s`", f.ReturnType.ID.TypeName())
|
||||
}
|
||||
retVal, err := cConvFunc(result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return retVal, nil
|
||||
} else {
|
||||
return nil, nil
|
||||
}
|
||||
return extFunc(ctx, args...)
|
||||
case SQLFunction:
|
||||
return CallSqlFunction(ctx, f, c.runner, args)
|
||||
default:
|
||||
|
||||
+27
-26
@@ -1,4 +1,4 @@
|
||||
// Copyright 2025 Dolthub, Inc.
|
||||
// Copyright 2026 Dolthub, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -17,81 +17,82 @@ package framework
|
||||
import (
|
||||
"github.com/dolthub/go-mysql-server/sql"
|
||||
|
||||
"github.com/dolthub/doltgresql/core/extensions"
|
||||
"github.com/dolthub/doltgresql/core/id"
|
||||
pgtypes "github.com/dolthub/doltgresql/server/types"
|
||||
)
|
||||
|
||||
// CFunction is the implementation of functions that host their logic in a shared library.
|
||||
type CFunction struct {
|
||||
// ExtensionFunction is the implementation of functions that an extension provides, looked up in
|
||||
// server/extensions by extension name and symbol.
|
||||
type ExtensionFunction struct {
|
||||
ID id.Function
|
||||
ReturnType *pgtypes.DoltgresType
|
||||
ParameterTypes []*pgtypes.DoltgresType
|
||||
Variadic bool
|
||||
IsNonDeterministic bool
|
||||
Strict bool
|
||||
ExtensionName extensions.LibraryIdentifier
|
||||
SetOf bool
|
||||
ExtensionName string
|
||||
ExtensionSymbol string
|
||||
}
|
||||
|
||||
var _ FunctionInterface = CFunction{}
|
||||
var _ FunctionInterface = ExtensionFunction{}
|
||||
|
||||
// GetExpectedParameterCount implements the interface FunctionInterface.
|
||||
func (cFunc CFunction) GetExpectedParameterCount() int {
|
||||
return len(cFunc.ParameterTypes)
|
||||
func (extFunc ExtensionFunction) GetExpectedParameterCount() int {
|
||||
return len(extFunc.ParameterTypes)
|
||||
}
|
||||
|
||||
// GetName implements the interface FunctionInterface.
|
||||
func (cFunc CFunction) GetName() string {
|
||||
return cFunc.ID.FunctionName()
|
||||
func (extFunc ExtensionFunction) GetName() string {
|
||||
return extFunc.ID.FunctionName()
|
||||
}
|
||||
|
||||
// GetOutParameters implements the interface FunctionInterface.
|
||||
func (cFunc CFunction) GetOutParameters() sql.Schema {
|
||||
func (extFunc ExtensionFunction) GetOutParameters() sql.Schema {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetInputParameterTypes implements the interface FunctionInterface.
|
||||
func (cFunc CFunction) GetInputParameterTypes() []*pgtypes.DoltgresType {
|
||||
return cFunc.ParameterTypes
|
||||
func (extFunc ExtensionFunction) GetInputParameterTypes() []*pgtypes.DoltgresType {
|
||||
return extFunc.ParameterTypes
|
||||
}
|
||||
|
||||
// GetReturn implements the interface FunctionInterface.
|
||||
func (cFunc CFunction) GetReturn() *pgtypes.DoltgresType {
|
||||
return cFunc.ReturnType
|
||||
func (extFunc ExtensionFunction) GetReturn() *pgtypes.DoltgresType {
|
||||
return extFunc.ReturnType
|
||||
}
|
||||
|
||||
// InternalID implements the interface FunctionInterface.
|
||||
func (cFunc CFunction) InternalID() id.Id {
|
||||
return cFunc.ID.AsId()
|
||||
func (extFunc ExtensionFunction) InternalID() id.Id {
|
||||
return extFunc.ID.AsId()
|
||||
}
|
||||
|
||||
// IsStrict implements the interface FunctionInterface.
|
||||
func (cFunc CFunction) IsStrict() bool {
|
||||
return cFunc.Strict
|
||||
func (extFunc ExtensionFunction) IsStrict() bool {
|
||||
return extFunc.Strict
|
||||
}
|
||||
|
||||
// NonDeterministic implements the interface FunctionInterface.
|
||||
func (cFunc CFunction) NonDeterministic() bool {
|
||||
return cFunc.IsNonDeterministic
|
||||
func (extFunc ExtensionFunction) NonDeterministic() bool {
|
||||
return extFunc.IsNonDeterministic
|
||||
}
|
||||
|
||||
// IsCVariadic implements the FunctionInterface interface.
|
||||
func (cFunc CFunction) IsCVariadic() bool {
|
||||
func (extFunc ExtensionFunction) IsCVariadic() bool {
|
||||
// TODO: implement c-language variadic
|
||||
return false
|
||||
}
|
||||
|
||||
// VariadicIndex implements the interface FunctionInterface.
|
||||
func (cFunc CFunction) VariadicIndex() int {
|
||||
func (extFunc ExtensionFunction) VariadicIndex() int {
|
||||
// TODO: implement variadic
|
||||
return -1
|
||||
}
|
||||
|
||||
// IsSRF implements the interface FunctionInterface.
|
||||
func (cFunc CFunction) IsSRF() bool {
|
||||
return false
|
||||
func (extFunc ExtensionFunction) IsSRF() bool {
|
||||
return extFunc.SetOf
|
||||
}
|
||||
|
||||
// enforceInterfaceInheritance implements the interface FunctionInterface.
|
||||
func (cFunc CFunction) enforceInterfaceInheritance(error) {}
|
||||
func (extFunc ExtensionFunction) enforceInterfaceInheritance(error) {}
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"github.com/dolthub/go-mysql-server/sql"
|
||||
|
||||
"github.com/dolthub/doltgresql/core"
|
||||
"github.com/dolthub/doltgresql/core/extensions"
|
||||
"github.com/dolthub/doltgresql/core/id"
|
||||
pgtypes "github.com/dolthub/doltgresql/server/types"
|
||||
)
|
||||
@@ -82,14 +81,15 @@ func (fp *FunctionProvider) Function(ctx *sql.Context, schema, name string) (sql
|
||||
}
|
||||
}
|
||||
if len(overload.ExtensionName) > 0 {
|
||||
if err = overloadTree.Add(CFunction{
|
||||
if err = overloadTree.Add(ExtensionFunction{
|
||||
ID: overload.ID,
|
||||
ReturnType: returnType,
|
||||
ParameterTypes: paramTypes,
|
||||
Variadic: overload.Variadic,
|
||||
IsNonDeterministic: overload.IsNonDeterministic,
|
||||
Strict: overload.Strict,
|
||||
ExtensionName: extensions.LibraryIdentifier(overload.ExtensionName),
|
||||
SetOf: overload.SetOf,
|
||||
ExtensionName: overload.ExtensionName,
|
||||
ExtensionSymbol: overload.ExtensionSymbol,
|
||||
}); err != nil {
|
||||
return nil, false
|
||||
|
||||
@@ -42,7 +42,12 @@ var uuid_in = framework.Function1{
|
||||
Parameters: [1]*pgtypes.DoltgresType{pgtypes.Cstring},
|
||||
Strict: true,
|
||||
Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) {
|
||||
return uuid.FromString(val.(string))
|
||||
input := val.(string)
|
||||
newUUID, err := uuid.FromString(input)
|
||||
if err != nil {
|
||||
return nil, pgtypes.ErrInvalidSyntaxForType.New("uuid", input)
|
||||
}
|
||||
return newUUID, nil
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/dolthub/doltgresql/server/auth"
|
||||
"github.com/dolthub/doltgresql/server/cast"
|
||||
"github.com/dolthub/doltgresql/server/config"
|
||||
"github.com/dolthub/doltgresql/server/extensions"
|
||||
"github.com/dolthub/doltgresql/server/functions"
|
||||
"github.com/dolthub/doltgresql/server/functions/aggregate"
|
||||
"github.com/dolthub/doltgresql/server/functions/binary"
|
||||
@@ -51,6 +52,7 @@ func Initialize(dEnv *env.DoltEnv, cfg *doltgresservercfg.DoltgresConfig) {
|
||||
once.Do(func() {
|
||||
core.Init()
|
||||
rootobject.Init()
|
||||
extensions.Init()
|
||||
auth.Init(dEnv, cfg)
|
||||
pgtypes.Init()
|
||||
analyzer.Init()
|
||||
|
||||
@@ -25,11 +25,12 @@ import (
|
||||
vitess "github.com/dolthub/vitess/go/vt/sqlparser"
|
||||
|
||||
"github.com/dolthub/doltgresql/core"
|
||||
"github.com/dolthub/doltgresql/core/extensions"
|
||||
coreextensions "github.com/dolthub/doltgresql/core/extensions"
|
||||
"github.com/dolthub/doltgresql/core/id"
|
||||
"github.com/dolthub/doltgresql/postgres/parser/parser"
|
||||
"github.com/dolthub/doltgresql/postgres/parser/sem/tree"
|
||||
pgexprs "github.com/dolthub/doltgresql/server/expression"
|
||||
"github.com/dolthub/doltgresql/server/extensions"
|
||||
)
|
||||
|
||||
// CreateExtension implements CREATE EXTENSION.
|
||||
@@ -89,50 +90,26 @@ func (c *CreateExtension) RowIter(ctx *sql.Context, r sql.Row) (sql.RowIter, err
|
||||
}
|
||||
return nil, errors.Errorf(`extension "%s" already exists`, c.Name)
|
||||
}
|
||||
ext, err := extensions.GetExtension(c.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The returned files are in their proper order of execution, so we can iterate and execute
|
||||
sqlFiles, err := ext.LoadSQLFiles()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// save the current search_path
|
||||
originalSchema, err := ctx.GetSessionVariable(ctx, "search_path")
|
||||
// TODO: install the extensions named by Control.Requires, once an emulated extension declares any
|
||||
ext, err := extensions.Get(c.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.SchemaName != "" {
|
||||
// save the current search_path
|
||||
originalSearchPath, err := ctx.GetSessionVariable(ctx, "search_path")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = ctx.SetSessionVariable(ctx, "search_path", originalSchema)
|
||||
_ = ctx.SetSessionVariable(ctx, "search_path", originalSearchPath)
|
||||
}()
|
||||
|
||||
spErr := ctx.SetSessionVariable(ctx, "search_path", c.SchemaName)
|
||||
if spErr != nil {
|
||||
return nil, spErr
|
||||
if err = ctx.SetSessionVariable(ctx, "search_path", c.SchemaName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, sqlFile := range sqlFiles {
|
||||
// Remove echo PSQL control statements
|
||||
for {
|
||||
echoStartIdx := strings.Index(sqlFile, `\echo`)
|
||||
if echoStartIdx == -1 {
|
||||
break
|
||||
}
|
||||
echoEndIdx := strings.Index(sqlFile[echoStartIdx:], "\n")
|
||||
if echoEndIdx != -1 {
|
||||
// Set the correct absolute position if there is a newline
|
||||
echoEndIdx += echoStartIdx
|
||||
} else {
|
||||
// Set the position at the end of the file if there's no newline (comment appears before EOF)
|
||||
echoEndIdx = len(sqlFile)
|
||||
}
|
||||
sqlFile = strings.Replace(sqlFile, sqlFile[echoStartIdx:echoEndIdx], "", 1)
|
||||
}
|
||||
statements, err := parser.Parse(sqlFile)
|
||||
statements, err := parser.Parse(ext.Script)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -152,17 +129,16 @@ func (c *CreateExtension) RowIter(ctx *sql.Context, r sql.Row) (sql.RowIter, err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace := id.NullNamespace
|
||||
if len(ext.Control.Schema) > 0 {
|
||||
namespace = id.NewNamespace(ext.Control.Schema)
|
||||
schemaName, err := core.GetSchemaName(ctx, nil, c.SchemaName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = extCollection.AddLoadedExtension(ctx, extensions.Extension{
|
||||
err = extCollection.AddLoadedExtension(ctx, coreextensions.Extension{
|
||||
ExtName: id.NewExtension(c.Name),
|
||||
Namespace: namespace,
|
||||
Namespace: id.NewNamespace(schemaName),
|
||||
Relocatable: ext.Control.Relocatable,
|
||||
LibIdentifier: extensions.CreateLibraryIdentifier(c.Name, ext.Control.DefaultVersion),
|
||||
Version: ext.Control.DefaultVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -24,10 +24,10 @@ import (
|
||||
vitess "github.com/dolthub/vitess/go/vt/sqlparser"
|
||||
|
||||
"github.com/dolthub/doltgresql/core"
|
||||
"github.com/dolthub/doltgresql/core/extensions"
|
||||
"github.com/dolthub/doltgresql/core/functions"
|
||||
"github.com/dolthub/doltgresql/core/id"
|
||||
"github.com/dolthub/doltgresql/core/procedures"
|
||||
"github.com/dolthub/doltgresql/server/extensions"
|
||||
"github.com/dolthub/doltgresql/server/plpgsql"
|
||||
pgtypes "github.com/dolthub/doltgresql/server/types"
|
||||
)
|
||||
@@ -145,18 +145,10 @@ func (c *CreateFunction) RowIter(ctx *sql.Context, r sql.Row) (sql.RowIter, erro
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var extName string
|
||||
if len(c.ExtensionName) > 0 {
|
||||
ext, err := extensions.GetExtension(c.ExtensionName)
|
||||
if err != nil {
|
||||
if _, err = extensions.GetFunction(c.ExtensionName, c.ExtensionSymbol); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ident := extensions.CreateLibraryIdentifier(c.ExtensionName, ext.Control.DefaultVersion)
|
||||
_, err = extensions.GetExtensionFunction(extensions.CreateLibraryIdentifier(c.ExtensionName, ext.Control.DefaultVersion), c.ExtensionSymbol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extName = string(ident)
|
||||
}
|
||||
err = funcCollection.AddFunction(ctx, functions.Function{
|
||||
ID: funcID,
|
||||
@@ -166,7 +158,7 @@ func (c *CreateFunction) RowIter(ctx *sql.Context, r sql.Row) (sql.RowIter, erro
|
||||
IsNonDeterministic: true,
|
||||
Strict: c.Strict,
|
||||
Definition: c.Definition,
|
||||
ExtensionName: extName,
|
||||
ExtensionName: c.ExtensionName,
|
||||
ExtensionSymbol: c.ExtensionSymbol,
|
||||
Operations: c.Statements,
|
||||
SQLDefinition: c.SqlDef,
|
||||
|
||||
@@ -23,9 +23,9 @@ import (
|
||||
vitess "github.com/dolthub/vitess/go/vt/sqlparser"
|
||||
|
||||
"github.com/dolthub/doltgresql/core"
|
||||
"github.com/dolthub/doltgresql/core/extensions"
|
||||
"github.com/dolthub/doltgresql/core/id"
|
||||
"github.com/dolthub/doltgresql/core/procedures"
|
||||
"github.com/dolthub/doltgresql/server/extensions"
|
||||
"github.com/dolthub/doltgresql/server/plpgsql"
|
||||
)
|
||||
|
||||
@@ -125,24 +125,16 @@ func (c *CreateProcedure) RowIter(ctx *sql.Context, _ sql.Row) (sql.RowIter, err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var extName string
|
||||
if len(c.ExtensionName) > 0 {
|
||||
ext, err := extensions.GetExtension(c.ExtensionName)
|
||||
if err != nil {
|
||||
if _, err = extensions.GetFunction(c.ExtensionName, c.ExtensionSymbol); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ident := extensions.CreateLibraryIdentifier(c.ExtensionName, ext.Control.DefaultVersion)
|
||||
_, err = extensions.GetExtensionFunction(extensions.CreateLibraryIdentifier(c.ExtensionName, ext.Control.DefaultVersion), c.ExtensionSymbol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extName = string(ident)
|
||||
}
|
||||
err = procCollection.AddProcedure(ctx, procedures.Procedure{
|
||||
ID: procID,
|
||||
AllParams: allParams,
|
||||
Definition: c.Definition,
|
||||
ExtensionName: extName,
|
||||
ExtensionName: c.ExtensionName,
|
||||
ExtensionSymbol: c.ExtensionSymbol,
|
||||
Operations: c.Statements,
|
||||
SQLDefinition: c.SqlDef,
|
||||
|
||||
@@ -21,8 +21,8 @@ import (
|
||||
"github.com/dolthub/go-mysql-server/sql"
|
||||
|
||||
"github.com/dolthub/doltgresql/core"
|
||||
"github.com/dolthub/doltgresql/core/extensions"
|
||||
"github.com/dolthub/doltgresql/core/id"
|
||||
"github.com/dolthub/doltgresql/server/extensions"
|
||||
"github.com/dolthub/doltgresql/server/tables"
|
||||
pgtypes "github.com/dolthub/doltgresql/server/types"
|
||||
)
|
||||
@@ -60,12 +60,7 @@ type pgAvailableExtensionVersion struct {
|
||||
|
||||
// RowIter implements the interface tables.Handler.
|
||||
func (p PgAvailableExtensionVersionsHandler) RowIter(ctx *sql.Context, partition sql.Partition) (sql.RowIter, error) {
|
||||
allExtensions, err := extensions.GetAllExtensions()
|
||||
if err != nil {
|
||||
// Extensions cannot be loaded when there is no local Postgres installation, so we report that no extensions
|
||||
// are available rather than returning an error.
|
||||
return emptyRowIter()
|
||||
}
|
||||
allExtensions := extensions.GetAll()
|
||||
extCollection, err := core.GetExtensionsCollectionFromContext(ctx, ctx.GetCurrentDatabase())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -73,31 +68,31 @@ func (p PgAvailableExtensionVersionsHandler) RowIter(ctx *sql.Context, partition
|
||||
// TODO: Postgres lists a row for every version that has an installation script, but we only track the default
|
||||
// version for each extension.
|
||||
extVersions := make([]pgAvailableExtensionVersion, 0, len(allExtensions))
|
||||
for name, extFiles := range allExtensions {
|
||||
for name, ext := range allExtensions {
|
||||
extVersion := pgAvailableExtensionVersion{
|
||||
name: name,
|
||||
version: extFiles.Control.DefaultVersion.String(),
|
||||
superuser: extFiles.Control.Superuser,
|
||||
trusted: extFiles.Control.Trusted,
|
||||
relocatable: extFiles.Control.Relocatable,
|
||||
version: ext.Control.DefaultVersion,
|
||||
superuser: ext.Control.Superuser,
|
||||
trusted: ext.Control.Trusted,
|
||||
relocatable: ext.Control.Relocatable,
|
||||
}
|
||||
if len(extFiles.Control.Schema) > 0 {
|
||||
extVersion.schema = extFiles.Control.Schema
|
||||
if len(ext.Control.Schema) > 0 {
|
||||
extVersion.schema = ext.Control.Schema
|
||||
}
|
||||
if len(extFiles.Control.Requires) > 0 {
|
||||
requires := make([]any, len(extFiles.Control.Requires))
|
||||
for i, req := range extFiles.Control.Requires {
|
||||
if len(ext.Control.Requires) > 0 {
|
||||
requires := make([]any, len(ext.Control.Requires))
|
||||
for i, req := range ext.Control.Requires {
|
||||
requires[i] = req
|
||||
}
|
||||
extVersion.requires = requires
|
||||
}
|
||||
if len(extFiles.Control.Comment) > 0 {
|
||||
extVersion.comment = extFiles.Control.Comment
|
||||
if len(ext.Control.Comment) > 0 {
|
||||
extVersion.comment = ext.Control.Comment
|
||||
}
|
||||
if installed, err := extCollection.GetLoadedExtension(ctx, id.NewExtension(name)); err != nil {
|
||||
return nil, err
|
||||
} else if installed.ExtName.IsValid() {
|
||||
extVersion.installed = installed.LibIdentifier.Version() == extFiles.Control.DefaultVersion
|
||||
extVersion.installed = installed.Version == ext.Control.DefaultVersion
|
||||
}
|
||||
extVersions = append(extVersions, extVersion)
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import (
|
||||
"github.com/dolthub/go-mysql-server/sql"
|
||||
|
||||
"github.com/dolthub/doltgresql/core"
|
||||
"github.com/dolthub/doltgresql/core/extensions"
|
||||
"github.com/dolthub/doltgresql/core/id"
|
||||
"github.com/dolthub/doltgresql/server/extensions"
|
||||
"github.com/dolthub/doltgresql/server/tables"
|
||||
pgtypes "github.com/dolthub/doltgresql/server/types"
|
||||
)
|
||||
@@ -55,29 +55,24 @@ type pgAvailableExtension struct {
|
||||
|
||||
// RowIter implements the interface tables.Handler.
|
||||
func (p PgAvailableExtensionsHandler) RowIter(ctx *sql.Context, partition sql.Partition) (sql.RowIter, error) {
|
||||
allExtensions, err := extensions.GetAllExtensions()
|
||||
if err != nil {
|
||||
// Extensions cannot be loaded when there is no local Postgres installation, so we report that no extensions
|
||||
// are available rather than returning an error.
|
||||
return emptyRowIter()
|
||||
}
|
||||
allExtensions := extensions.GetAll()
|
||||
extCollection, err := core.GetExtensionsCollectionFromContext(ctx, ctx.GetCurrentDatabase())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
availableExtensions := make([]pgAvailableExtension, 0, len(allExtensions))
|
||||
for name, extFiles := range allExtensions {
|
||||
for name, ext := range allExtensions {
|
||||
availableExtension := pgAvailableExtension{
|
||||
name: name,
|
||||
defaultVersion: extFiles.Control.DefaultVersion.String(),
|
||||
defaultVersion: ext.Control.DefaultVersion,
|
||||
}
|
||||
if len(extFiles.Control.Comment) > 0 {
|
||||
availableExtension.comment = extFiles.Control.Comment
|
||||
if len(ext.Control.Comment) > 0 {
|
||||
availableExtension.comment = ext.Control.Comment
|
||||
}
|
||||
if installed, err := extCollection.GetLoadedExtension(ctx, id.NewExtension(name)); err != nil {
|
||||
return nil, err
|
||||
} else if installed.ExtName.IsValid() {
|
||||
availableExtension.installedVersion = installed.LibIdentifier.Version().String()
|
||||
availableExtension.installedVersion = installed.Version
|
||||
}
|
||||
availableExtensions = append(availableExtensions, availableExtension)
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ func (iter *pgExtensionRowIter) Next(ctx *sql.Context) (sql.Row, error) {
|
||||
id.Null, // extowner // TODO: extension owner is not yet tracked
|
||||
ext.Namespace.AsId(), // extnamespace
|
||||
ext.Relocatable, // extrelocatable
|
||||
ext.LibIdentifier.Version().String(), // extversion
|
||||
ext.Version, // extversion
|
||||
nil, // extconfig
|
||||
nil, // extcondition
|
||||
}, nil
|
||||
|
||||
@@ -15,20 +15,15 @@
|
||||
package _go
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/dolthub/go-mysql-server/sql"
|
||||
)
|
||||
|
||||
func TestCreateExtension(t *testing.T) {
|
||||
if runtime.GOOS == "windows" && os.Getenv("CI") != "" {
|
||||
t.Skip("CI Postgres installation seems to behave weirdly, skipping for now") // TODO: look into this a bit more
|
||||
}
|
||||
RunScripts(t, []ScriptTest{
|
||||
{
|
||||
Name: "Extension Test: uuid-ossp",
|
||||
Name: "uuid-ossp",
|
||||
SetUpScript: []string{
|
||||
`CREATE EXTENSION "uuid-ossp";`,
|
||||
},
|
||||
@@ -38,17 +33,14 @@ func TestCreateExtension(t *testing.T) {
|
||||
Expected: []sql.Row{{"6ba7b811-9dad-11d1-80b4-00c04fd430c8"}},
|
||||
},
|
||||
{
|
||||
Skip: true, // This is returning different results on different platforms for some reason
|
||||
Query: "SELECT uuid_generate_v3('00000000-0000-0000-0000-000000000000'::uuid, 'example text');",
|
||||
Expected: []sql.Row{{"a55b875a-1bd9-31af-ac66-7d8323785c6e"}},
|
||||
},
|
||||
{
|
||||
Skip: true, // For some reason, this returns the same result as above
|
||||
Query: "SELECT uuid_generate_v3('00000000-0000-0000-0000-000000000001'::uuid, 'example text');",
|
||||
Expected: []sql.Row{{"a319ab51-8e26-37c6-942f-7dd5fda5c3ef"}},
|
||||
},
|
||||
{
|
||||
Skip: true, // Need to figure out why the result is wrong
|
||||
Query: "SELECT uuid_generate_v3(uuid_ns_url(), 'example text');",
|
||||
Expected: []sql.Row{{"6541262f-d622-3e35-8873-2b227591bf69"}},
|
||||
},
|
||||
@@ -90,7 +82,6 @@ func TestCreateExtension(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_nil();",
|
||||
// TODO: error message should be "function uuid_nil() does not exist"
|
||||
ExpectedErr: `function: 'uuid_nil' not found`,
|
||||
},
|
||||
{
|
||||
@@ -118,5 +109,393 @@ func TestCreateExtension(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "uuid-ossp namespace functions",
|
||||
SetUpScript: []string{
|
||||
`CREATE EXTENSION "uuid-ossp";`,
|
||||
},
|
||||
Assertions: []ScriptTestAssertion{
|
||||
{
|
||||
Query: "SELECT uuid_nil();",
|
||||
Expected: []sql.Row{{"00000000-0000-0000-0000-000000000000"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_ns_dns();",
|
||||
Expected: []sql.Row{{"6ba7b810-9dad-11d1-80b4-00c04fd430c8"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_ns_url();",
|
||||
Expected: []sql.Row{{"6ba7b811-9dad-11d1-80b4-00c04fd430c8"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_ns_oid();",
|
||||
Expected: []sql.Row{{"6ba7b812-9dad-11d1-80b4-00c04fd430c8"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_ns_x500();",
|
||||
Expected: []sql.Row{{"6ba7b814-9dad-11d1-80b4-00c04fd430c8"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_ns_dns() = uuid_ns_dns(), uuid_ns_dns() = uuid_ns_url();",
|
||||
Expected: []sql.Row{{"t", "f"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "uuid-ossp uuid_generate_v3",
|
||||
SetUpScript: []string{
|
||||
`CREATE EXTENSION "uuid-ossp";`,
|
||||
},
|
||||
Assertions: []ScriptTestAssertion{
|
||||
{
|
||||
Query: "SELECT uuid_generate_v3(uuid_ns_dns(), 'www.postgresql.org');",
|
||||
Expected: []sql.Row{{"9a0d5f51-76ff-394e-ba97-b28a9ff12209"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_generate_v3(uuid_nil(), '');",
|
||||
Expected: []sql.Row{{"4ae71336-e44b-39bf-b9d2-752e234818a5"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_generate_v3(uuid_ns_dns(), 'héllo wörld');",
|
||||
Expected: []sql.Row{{"2f301b42-2eaf-3cc3-8646-69e0ffde841f"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_generate_v3(uuid_ns_url(), repeat('a', 1000));",
|
||||
Expected: []sql.Row{{"d8f8a14e-39ec-3186-8107-4d4e5a41d2c0"}},
|
||||
},
|
||||
{ // The version nibble is the 15th character of the textual form
|
||||
Query: "SELECT substring(uuid_generate_v3(uuid_nil(), 'x')::text, 15, 1);",
|
||||
Expected: []sql.Row{{"3"}},
|
||||
},
|
||||
{ // The variant nibble is the 20th character, and RFC 4122 restricts it to 8, 9, a or b
|
||||
Query: "SELECT substring(uuid_generate_v3(uuid_nil(), 'x')::text, 20, 1) IN ('8', '9', 'a', 'b');",
|
||||
Expected: []sql.Row{{"t"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_generate_v3(uuid_nil(), 'abc') = uuid_generate_v3(uuid_nil(), 'abc');",
|
||||
Expected: []sql.Row{{"t"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_generate_v3(uuid_nil(), 'abc') = uuid_generate_v3(uuid_nil(), 'ABC');",
|
||||
Expected: []sql.Row{{"f"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_generate_v3(uuid_ns_dns(), 'abc') = uuid_generate_v3(uuid_ns_url(), 'abc');",
|
||||
Expected: []sql.Row{{"f"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_generate_v3(NULL, 'abc') IS NULL, uuid_generate_v3(uuid_nil(), NULL) IS NULL;",
|
||||
Expected: []sql.Row{{"t", "t"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_generate_v3('not-a-uuid', 'abc');",
|
||||
ExpectedErr: `invalid input syntax for type uuid: "not-a-uuid"`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "uuid-ossp uuid_generate_v5",
|
||||
SetUpScript: []string{
|
||||
`CREATE EXTENSION "uuid-ossp";`,
|
||||
},
|
||||
Assertions: []ScriptTestAssertion{
|
||||
{
|
||||
Query: "SELECT uuid_generate_v5(uuid_ns_url(), 'example text');",
|
||||
Expected: []sql.Row{{"59edfb26-7819-5209-86a3-79a6da9035ba"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_generate_v5(uuid_ns_dns(), 'www.postgresql.org');",
|
||||
Expected: []sql.Row{{"1826c6c4-4d1f-534f-9dcd-7a15978dfeb9"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_generate_v5(uuid_ns_oid(), 'example text');",
|
||||
Expected: []sql.Row{{"5758e964-d604-5cde-9b32-57368fc3b1ff"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_generate_v5(uuid_ns_x500(), 'example text');",
|
||||
Expected: []sql.Row{{"1d8aac60-3096-5014-bfd7-1f816c37cf50"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_generate_v5(uuid_nil(), '');",
|
||||
Expected: []sql.Row{{"e129f27c-5103-5c5c-844b-cdf0a15e160d"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_generate_v5(uuid_ns_dns(), 'héllo wörld');",
|
||||
Expected: []sql.Row{{"d24fabfc-fb83-5476-8201-39e27376a62b"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_generate_v5(uuid_ns_url(), repeat('a', 1000));",
|
||||
Expected: []sql.Row{{"7f46a8f9-f8ba-5a67-983a-ffc2101475df"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT substring(uuid_generate_v5(uuid_nil(), 'x')::text, 15, 1);",
|
||||
Expected: []sql.Row{{"5"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT substring(uuid_generate_v5(uuid_nil(), 'x')::text, 20, 1) IN ('8', '9', 'a', 'b');",
|
||||
Expected: []sql.Row{{"t"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_generate_v5(uuid_nil(), 'abc') = uuid_generate_v5(uuid_nil(), 'abc');",
|
||||
Expected: []sql.Row{{"t"}},
|
||||
},
|
||||
{ // Version 3 hashes with MD5 while version 5 hashes with SHA-1, so they'll never agree
|
||||
Query: "SELECT uuid_generate_v3(uuid_nil(), 'abc') = uuid_generate_v5(uuid_nil(), 'abc');",
|
||||
Expected: []sql.Row{{"f"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_generate_v5(NULL, 'abc') IS NULL, uuid_generate_v5(uuid_nil(), NULL) IS NULL;",
|
||||
Expected: []sql.Row{{"t", "t"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "uuid-ossp uuid_generate_v1 and uuid_generate_v1mc",
|
||||
SetUpScript: []string{
|
||||
`CREATE EXTENSION "uuid-ossp";`,
|
||||
},
|
||||
Assertions: []ScriptTestAssertion{
|
||||
{
|
||||
Query: "SELECT substring(uuid_generate_v1()::text, 15, 1);",
|
||||
Expected: []sql.Row{{"1"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT substring(uuid_generate_v1mc()::text, 15, 1);",
|
||||
Expected: []sql.Row{{"1"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT substring(uuid_generate_v1()::text, 20, 1) IN ('8', '9', 'a', 'b');",
|
||||
Expected: []sql.Row{{"t"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT substring(uuid_generate_v1mc()::text, 20, 1) IN ('8', '9', 'a', 'b');",
|
||||
Expected: []sql.Row{{"t"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT substring(uuid_generate_v1mc()::text, 26, 1) IN ('3', '7', 'b', 'f');",
|
||||
Expected: []sql.Row{{"t"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT substring(uuid_generate_v1()::text, 25) = substring(uuid_generate_v1mc()::text, 25);",
|
||||
Expected: []sql.Row{{"f"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT count(DISTINCT id::text) FROM (SELECT uuid_generate_v1() AS id FROM generate_series(1, 50)) t;",
|
||||
Expected: []sql.Row{{50}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT count(DISTINCT id::text) FROM (SELECT uuid_generate_v1mc() AS id FROM generate_series(1, 50)) t;",
|
||||
Expected: []sql.Row{{50}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT length(uuid_generate_v1()::text), length(uuid_generate_v1mc()::text);",
|
||||
Expected: []sql.Row{{36, 36}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "uuid-ossp uuid_generate_v4",
|
||||
SetUpScript: []string{
|
||||
`CREATE EXTENSION "uuid-ossp";`,
|
||||
},
|
||||
Assertions: []ScriptTestAssertion{
|
||||
{
|
||||
Query: "SELECT substring(uuid_generate_v4()::text, 15, 1);",
|
||||
Expected: []sql.Row{{"4"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT substring(uuid_generate_v4()::text, 20, 1) IN ('8', '9', 'a', 'b');",
|
||||
Expected: []sql.Row{{"t"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT count(DISTINCT id::text) FROM (SELECT uuid_generate_v4() AS id FROM generate_series(1, 100)) t;",
|
||||
Expected: []sql.Row{{100}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "uuid-ossp functions used by a table",
|
||||
SetUpScript: []string{
|
||||
`CREATE EXTENSION "uuid-ossp";`,
|
||||
`CREATE TABLE items (id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), name text NOT NULL);`,
|
||||
`INSERT INTO items (name) VALUES ('first'), ('second'), ('third');`,
|
||||
`CREATE TABLE named (id uuid PRIMARY KEY, name text NOT NULL);`,
|
||||
`INSERT INTO named VALUES (uuid_generate_v5(uuid_ns_url(), 'example text'), 'example');`,
|
||||
},
|
||||
Assertions: []ScriptTestAssertion{
|
||||
{
|
||||
Query: "SELECT count(*), count(DISTINCT id::text) FROM items;",
|
||||
Expected: []sql.Row{{3, 3}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT name FROM items ORDER BY name;",
|
||||
Expected: []sql.Row{{"first"}, {"second"}, {"third"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT id, name FROM named;",
|
||||
Expected: []sql.Row{{"59edfb26-7819-5209-86a3-79a6da9035ba", "example"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT name FROM named WHERE id = uuid_generate_v5(uuid_ns_url(), 'example text');",
|
||||
Expected: []sql.Row{{"example"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "uuid-ossp catalog tables",
|
||||
SetUpScript: []string{
|
||||
`CREATE EXTENSION "uuid-ossp";`,
|
||||
},
|
||||
Assertions: []ScriptTestAssertion{
|
||||
{
|
||||
Query: `SELECT extname, extrelocatable, extversion FROM pg_catalog.pg_extension WHERE extname = 'uuid-ossp';`,
|
||||
Expected: []sql.Row{{"uuid-ossp", "t", "1.1"}},
|
||||
},
|
||||
{
|
||||
Query: `SELECT name, default_version, installed_version, comment FROM pg_catalog.pg_available_extensions WHERE name = 'uuid-ossp';`,
|
||||
Expected: []sql.Row{
|
||||
{"uuid-ossp", "1.1", "1.1", "generate universally unique identifiers (UUIDs)"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Query: `SELECT name, version, installed, superuser, trusted, relocatable, schema, requires FROM pg_catalog.pg_available_extension_versions WHERE name = 'uuid-ossp';`,
|
||||
Expected: []sql.Row{
|
||||
{"uuid-ossp", "1.1", "t", "t", "t", "t", nil, nil},
|
||||
},
|
||||
},
|
||||
{
|
||||
Query: `SELECT count(*) FROM pg_catalog.pg_proc WHERE proname LIKE 'uuid_ns_%' OR proname LIKE 'uuid_generate_%' OR proname = 'uuid_nil';`,
|
||||
Expected: []sql.Row{{10}},
|
||||
},
|
||||
{
|
||||
Query: `SELECT proname FROM pg_catalog.pg_proc WHERE proname LIKE 'uuid_ns_%' OR proname LIKE 'uuid_generate_%' OR proname = 'uuid_nil' ORDER BY proname;`,
|
||||
Expected: []sql.Row{
|
||||
{"uuid_generate_v1"}, {"uuid_generate_v1mc"}, {"uuid_generate_v3"}, {"uuid_generate_v4"},
|
||||
{"uuid_generate_v5"}, {"uuid_nil"}, {"uuid_ns_dns"}, {"uuid_ns_oid"}, {"uuid_ns_url"},
|
||||
{"uuid_ns_x500"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "uuid-ossp is not available before it is created",
|
||||
Assertions: []ScriptTestAssertion{
|
||||
{
|
||||
Query: `SELECT count(*) FROM pg_catalog.pg_extension;`,
|
||||
Expected: []sql.Row{{0}},
|
||||
},
|
||||
{
|
||||
Query: `SELECT name, installed_version FROM pg_catalog.pg_available_extensions WHERE name = 'uuid-ossp';`,
|
||||
Expected: []sql.Row{
|
||||
{"uuid-ossp", nil},
|
||||
},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_nil();",
|
||||
ExpectedErr: `function: 'uuid_nil' not found`,
|
||||
},
|
||||
{
|
||||
Query: `CREATE EXTENSION "uuid-ossp";`,
|
||||
Expected: []sql.Row{},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_nil();",
|
||||
Expected: []sql.Row{{"00000000-0000-0000-0000-000000000000"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "only emulated extensions may be created",
|
||||
Assertions: []ScriptTestAssertion{
|
||||
{
|
||||
Query: `CREATE EXTENSION "doltgres_no_such_extension";`,
|
||||
ExpectedErr: `extension "doltgres_no_such_extension" is not available`,
|
||||
},
|
||||
{
|
||||
Query: `CREATE EXTENSION IF NOT EXISTS "doltgres_no_such_extension";`,
|
||||
ExpectedErr: `extension "doltgres_no_such_extension" is not available`,
|
||||
},
|
||||
{
|
||||
Query: `CREATE EXTENSION "UUID-OSSP";`,
|
||||
ExpectedErr: `extension "UUID-OSSP" is not available`,
|
||||
},
|
||||
{
|
||||
Query: `CREATE EXTENSION "uuid-ossp";`,
|
||||
Expected: []sql.Row{},
|
||||
},
|
||||
{
|
||||
Query: `CREATE EXTENSION "uuid-ossp";`,
|
||||
ExpectedErr: `extension "uuid-ossp" already exists`,
|
||||
},
|
||||
{
|
||||
Query: `CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`,
|
||||
Expected: []sql.Row{},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_nil();",
|
||||
Expected: []sql.Row{{"00000000-0000-0000-0000-000000000000"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "uuid-ossp options that are not yet supported",
|
||||
Assertions: []ScriptTestAssertion{
|
||||
{
|
||||
Query: `CREATE EXTENSION "uuid-ossp" VERSION oldversion;`,
|
||||
ExpectedErr: "VERSION is not yet supported",
|
||||
},
|
||||
{
|
||||
Query: `CREATE EXTENSION "uuid-ossp" WITH SCHEMA myschema;`,
|
||||
ExpectedErr: "non public SCHEMA is not yet supported",
|
||||
},
|
||||
{
|
||||
Query: `CREATE EXTENSION "uuid-ossp" CASCADE;`,
|
||||
ExpectedErr: "CASCADE is not yet supported",
|
||||
},
|
||||
{
|
||||
Query: `DROP EXTENSION "uuid-ossp";`,
|
||||
ExpectedErr: "DROP EXTENSION is not yet implemented",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "uuid-ossp installation participates in branches",
|
||||
SetUpScript: []string{
|
||||
`SELECT dolt_commit('--allow-empty', '-m', 'initial commit');`,
|
||||
`SELECT dolt_checkout('-b', 'ext');`,
|
||||
`CREATE EXTENSION "uuid-ossp";`,
|
||||
`SELECT dolt_commit('-Am', 'create the extension');`,
|
||||
},
|
||||
Assertions: []ScriptTestAssertion{
|
||||
{
|
||||
Query: `SELECT extname FROM pg_catalog.pg_extension;`,
|
||||
Expected: []sql.Row{{"uuid-ossp"}},
|
||||
},
|
||||
{
|
||||
Query: `SELECT dolt_checkout('main');`,
|
||||
SkipResultsCheck: true,
|
||||
},
|
||||
{
|
||||
Query: `SELECT extname FROM pg_catalog.pg_extension;`,
|
||||
Expected: []sql.Row{},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_nil();",
|
||||
ExpectedErr: `function: 'uuid_nil' not found`,
|
||||
},
|
||||
{
|
||||
Query: `SELECT dolt_merge('ext');`,
|
||||
SkipResultsCheck: true,
|
||||
},
|
||||
{
|
||||
Query: `SELECT extname, extversion FROM pg_catalog.pg_extension;`,
|
||||
Expected: []sql.Row{{"uuid-ossp", "1.1"}},
|
||||
},
|
||||
{
|
||||
Query: "SELECT uuid_nil();",
|
||||
Expected: []sql.Row{{"00000000-0000-0000-0000-000000000000"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,9 +16,7 @@ package _go
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/dolthub/go-mysql-server/sql"
|
||||
@@ -714,8 +712,6 @@ func TestDoltBackup(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
// TODO: Extension loading in Windows CI environments don't work currently
|
||||
if !(runtime.GOOS == "windows" && os.Getenv("CI") != "") {
|
||||
backupUrl = localBackupUrl(t, "backup")
|
||||
runBackupTest(t, ScriptTest{
|
||||
Name: "extension is preserved across backup and restore",
|
||||
@@ -727,14 +723,16 @@ func TestDoltBackup(t *testing.T) {
|
||||
},
|
||||
Assertions: []ScriptTestAssertion{
|
||||
{
|
||||
// pg_extension is an unimplemented stub; calling the extension function is the best available proof.
|
||||
Query: "select extname, extversion from pg_catalog.pg_extension;",
|
||||
Expected: []sql.Row{{"uuid-ossp", "1.1"}},
|
||||
},
|
||||
{
|
||||
// uuid_generate_v4() returns a 36-character UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
|
||||
Query: "select length(uuid_generate_v4()::text) = 36;",
|
||||
Expected: []sql.Row{{"t"}},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
backupUrl = localBackupUrl(t, "backup")
|
||||
runBackupTest(t, ScriptTest{
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -820,8 +819,6 @@ func TestDoltRemote(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
// TODO: Extension loading in Windows CI environments don't work currently
|
||||
if !(runtime.GOOS == "windows" && os.Getenv("CI") != "") {
|
||||
remoteUrl = localRemoteUrl(t, "remote")
|
||||
runRemoteTest(t, ScriptTest{
|
||||
Name: "extension is preserved across push and clone",
|
||||
@@ -835,14 +832,16 @@ func TestDoltRemote(t *testing.T) {
|
||||
},
|
||||
Assertions: []ScriptTestAssertion{
|
||||
{
|
||||
// pg_extension is an unimplemented stub; calling the extension function is the best available proof.
|
||||
Query: "select extname, extversion from pg_catalog.pg_extension;",
|
||||
Expected: []sql.Row{{"uuid-ossp", "1.1"}},
|
||||
},
|
||||
{
|
||||
// uuid_generate_v4() returns a 36-character UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
|
||||
Query: "select length(uuid_generate_v4()::text) = 36;",
|
||||
Expected: []sql.Row{{"t"}},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
remoteUrl = localRemoteUrl(t, "remote")
|
||||
runRemoteTest(t, ScriptTest{
|
||||
|
||||
@@ -439,12 +439,17 @@ func TestPgAvailableExtensionVersions(t *testing.T) {
|
||||
{
|
||||
Name: "pg_available_extension_versions",
|
||||
Assertions: []ScriptTestAssertion{
|
||||
{ // The set of available extensions depends on the local Postgres installation, so we filter on a
|
||||
// name that will never exist to keep the results deterministic across environments.
|
||||
{ // Only the extensions that Doltgres emulates are available
|
||||
Query: `SELECT * FROM "pg_catalog"."pg_available_extension_versions" WHERE name = 'doltgres_no_such_extension';`,
|
||||
Expected: []sql.Row{},
|
||||
ExpectedColNames: []string{"name", "version", "installed", "superuser", "trusted", "relocatable", "schema", "requires", "comment"},
|
||||
},
|
||||
{
|
||||
Query: `SELECT name, version, installed, superuser, trusted, relocatable, schema, requires, comment FROM "pg_catalog"."pg_available_extension_versions" ORDER BY name;`,
|
||||
Expected: []sql.Row{
|
||||
{"uuid-ossp", "1.1", "f", "t", "t", "t", nil, nil, "generate universally unique identifiers (UUIDs)"},
|
||||
},
|
||||
},
|
||||
{ // No extensions are installed by default
|
||||
Query: `SELECT name, version FROM "pg_catalog"."pg_available_extension_versions" WHERE installed = true;`,
|
||||
Expected: []sql.Row{},
|
||||
@@ -463,6 +468,18 @@ func TestPgAvailableExtensionVersions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "pg_available_extension_versions with an installed extension",
|
||||
SetUpScript: []string{
|
||||
`CREATE EXTENSION "uuid-ossp";`,
|
||||
},
|
||||
Assertions: []ScriptTestAssertion{
|
||||
{
|
||||
Query: `SELECT name, version FROM "pg_catalog"."pg_available_extension_versions" WHERE installed = true;`,
|
||||
Expected: []sql.Row{{"uuid-ossp", "1.1"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -471,12 +488,17 @@ func TestPgAvailableExtensions(t *testing.T) {
|
||||
{
|
||||
Name: "pg_available_extensions",
|
||||
Assertions: []ScriptTestAssertion{
|
||||
{ // The set of available extensions depends on the local Postgres installation, so we filter on a
|
||||
// name that will never exist to keep the results deterministic across environments.
|
||||
{ // Only the extensions that Doltgres emulates are available
|
||||
Query: `SELECT * FROM "pg_catalog"."pg_available_extensions" WHERE name = 'doltgres_no_such_extension';`,
|
||||
Expected: []sql.Row{},
|
||||
ExpectedColNames: []string{"name", "default_version", "installed_version", "comment"},
|
||||
},
|
||||
{
|
||||
Query: `SELECT name, default_version, installed_version, comment FROM "pg_catalog"."pg_available_extensions" ORDER BY name;`,
|
||||
Expected: []sql.Row{
|
||||
{"uuid-ossp", "1.1", nil, "generate universally unique identifiers (UUIDs)"},
|
||||
},
|
||||
},
|
||||
{ // No extensions are installed by default
|
||||
Query: `SELECT name, installed_version FROM "pg_catalog"."pg_available_extensions" WHERE installed_version IS NOT NULL;`,
|
||||
Expected: []sql.Row{},
|
||||
@@ -495,6 +517,18 @@ func TestPgAvailableExtensions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "pg_available_extensions with an installed extension",
|
||||
SetUpScript: []string{
|
||||
`CREATE EXTENSION "uuid-ossp";`,
|
||||
},
|
||||
Assertions: []ScriptTestAssertion{
|
||||
{
|
||||
Query: `SELECT name, installed_version FROM "pg_catalog"."pg_available_extensions" WHERE installed_version IS NOT NULL;`,
|
||||
Expected: []sql.Row{{"uuid-ossp", "1.1"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1439,9 +1473,9 @@ func TestPgExtension(t *testing.T) {
|
||||
{
|
||||
Name: "pg_extension",
|
||||
Assertions: []ScriptTestAssertion{
|
||||
{ // No extensions are installed in a fresh database
|
||||
// TODO: install an extension (CREATE EXTENSION requires a local Postgres installation, which is
|
||||
// not available in every test environment) and assert its row here
|
||||
{
|
||||
// TODO: Postgres installs plpgsql automatically, while it's built-in for us
|
||||
// We need to return it as a row as though it were installed
|
||||
Query: `SELECT * FROM "pg_catalog"."pg_extension";`,
|
||||
Expected: []sql.Row{},
|
||||
ExpectedColNames: []string{"oid", "extname", "extowner", "extnamespace", "extrelocatable", "extversion", "extconfig", "extcondition"},
|
||||
@@ -1460,6 +1494,22 @@ func TestPgExtension(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "pg_extension with an installed extension",
|
||||
SetUpScript: []string{
|
||||
`CREATE EXTENSION "uuid-ossp";`,
|
||||
},
|
||||
Assertions: []ScriptTestAssertion{
|
||||
{
|
||||
Query: `SELECT extname, extrelocatable, extversion, extconfig, extcondition FROM "pg_catalog"."pg_extension";`,
|
||||
Expected: []sql.Row{{"uuid-ossp", "t", "1.1", nil, nil}},
|
||||
},
|
||||
{
|
||||
Query: `SELECT e.extname, n.nspname FROM "pg_catalog"."pg_extension" e, "pg_catalog"."pg_namespace" n WHERE n.oid = e.extnamespace;`,
|
||||
Expected: []sql.Row{{"uuid-ossp", "public"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user