Revamped extensions to use emulation

This commit is contained in:
Daylon Wilkins
2026-08-04 07:18:59 -07:00
committed by Daylon Wilkins
parent 8e44f2abd9
commit af0b3a8f1c
51 changed files with 1029 additions and 2244 deletions
+1 -27
View File
@@ -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:
-3
View File
@@ -26,6 +26,3 @@ scripts/mini_sysbench
# ignore doltgres db created
doltgres
auth.db
# ignore extensions output
core/extensions/pg_extension/output
+7 -10
View File
@@ -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
ExtName id.Extension
Namespace id.Namespace
Relocatable bool
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.
-219
View File
@@ -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())
}
-12
View File
@@ -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
```
-56
View File
@@ -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
-88
View File
@@ -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))
}
+9 -4
View File
@@ -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")
}
+10 -8
View File
@@ -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
View File
@@ -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
}
+1 -4
View File
@@ -51,7 +51,4 @@ gofmt -s -w parser/help_messages.go
cat parser/gen/sql.go.tmp | \
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
go run golang.org/x/tools/cmd/goimports -local github.com/dolthub/doltgresql -w parser/sql.go
-9
View File
@@ -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 \
+2 -3
View File
@@ -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
+42
View File
@@ -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
}
+84
View File
@@ -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
}
+68
View File
@@ -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;
+142
View File
@@ -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:
@@ -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) {}
+3 -3
View File
@@ -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
+6 -1
View File
@@ -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
},
}
+2
View File
@@ -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()
+36 -60
View File
@@ -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,80 +90,55 @@ 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 != "" {
defer func() {
_ = ctx.SetSessionVariable(ctx, "search_path", originalSchema)
}()
spErr := ctx.SetSessionVariable(ctx, "search_path", c.SchemaName)
if spErr != nil {
return nil, spErr
}
}
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)
// save the current search_path
originalSearchPath, err := ctx.GetSessionVariable(ctx, "search_path")
if err != nil {
return nil, err
}
for _, statement := range statements {
statementSQL := statement.SQL
if _, ok := statement.AST.(*tree.CreateFunction); ok {
statementSQL = strings.ReplaceAll(statementSQL, `'MODULE_PATHNAME'`, fmt.Sprintf(`'%s'`, c.Name))
}
_, err = sql.RunInterpreted(ctx, func(subCtx *sql.Context) ([]sql.Row, error) {
_, rowIter, _, err := c.Runner.Runner.QueryWithBindings(subCtx, statementSQL, nil, nil, nil)
if err != nil {
return nil, err
}
return sql.RowIterToRows(subCtx, rowIter)
})
defer func() {
_ = ctx.SetSessionVariable(ctx, "search_path", originalSearchPath)
}()
if err = ctx.SetSessionVariable(ctx, "search_path", c.SchemaName); err != nil {
return nil, err
}
}
statements, err := parser.Parse(ext.Script)
if err != nil {
return nil, err
}
for _, statement := range statements {
statementSQL := statement.SQL
if _, ok := statement.AST.(*tree.CreateFunction); ok {
statementSQL = strings.ReplaceAll(statementSQL, `'MODULE_PATHNAME'`, fmt.Sprintf(`'%s'`, c.Name))
}
_, err = sql.RunInterpreted(ctx, func(subCtx *sql.Context) ([]sql.Row, error) {
_, rowIter, _, err := c.Runner.Runner.QueryWithBindings(subCtx, statementSQL, nil, nil, nil)
if err != nil {
return nil, err
}
return sql.RowIterToRows(subCtx, rowIter)
})
if err != nil {
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{
ExtName: id.NewExtension(c.Name),
Namespace: namespace,
Relocatable: ext.Control.Relocatable,
LibIdentifier: extensions.CreateLibraryIdentifier(c.Name, ext.Control.DefaultVersion),
err = extCollection.AddLoadedExtension(ctx, coreextensions.Extension{
ExtName: id.NewExtension(c.Name),
Namespace: id.NewNamespace(schemaName),
Relocatable: ext.Control.Relocatable,
Version: ext.Control.DefaultVersion,
})
if err != nil {
return nil, err
+3 -11
View File
@@ -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,
+3 -11
View File
@@ -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)
}
+8 -8
View File
@@ -122,14 +122,14 @@ func (iter *pgExtensionRowIter) Next(ctx *sql.Context) (sql.Row, error) {
iter.idx++
ext := iter.extensions[iter.idx-1]
return sql.Row{
ext.ExtName.AsId(), // oid
ext.ExtName.Name(), // extname
id.Null, // extowner // TODO: extension owner is not yet tracked
ext.Namespace.AsId(), // extnamespace
ext.Relocatable, // extrelocatable
ext.LibIdentifier.Version().String(), // extversion
nil, // extconfig
nil, // extcondition
ext.ExtName.AsId(), // oid
ext.ExtName.Name(), // extname
id.Null, // extowner // TODO: extension owner is not yet tracked
ext.Namespace.AsId(), // extnamespace
ext.Relocatable, // extrelocatable
ext.Version, // extversion
nil, // extconfig
nil, // extcondition
}, nil
}
+390 -11
View File
@@ -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"}},
},
@@ -89,8 +81,7 @@ func TestCreateExtension(t *testing.T) {
Expected: []sql.Row{},
},
{
Query: "SELECT uuid_nil();",
// TODO: error message should be "function uuid_nil() does not exist"
Query: "SELECT uuid_nil();",
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"}},
},
},
},
})
}
+19 -21
View File
@@ -16,9 +16,7 @@ package _go
import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/dolthub/go-mysql-server/sql"
@@ -714,27 +712,27 @@ 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",
SetUpScript: []string{
`create extension "uuid-ossp";`,
fmt.Sprintf("select dolt_backup('sync-url', '%s');", backupUrl),
fmt.Sprintf("select dolt_backup('restore', '%s', 'restored_ext');", backupUrl),
"USE restored_ext",
backupUrl = localBackupUrl(t, "backup")
runBackupTest(t, ScriptTest{
Name: "extension is preserved across backup and restore",
SetUpScript: []string{
`create extension "uuid-ossp";`,
fmt.Sprintf("select dolt_backup('sync-url', '%s');", backupUrl),
fmt.Sprintf("select dolt_backup('restore', '%s', 'restored_ext');", backupUrl),
"USE restored_ext",
},
Assertions: []ScriptTestAssertion{
{
Query: "select extname, extversion from pg_catalog.pg_extension;",
Expected: []sql.Row{{"uuid-ossp", "1.1"}},
},
Assertions: []ScriptTestAssertion{
{
// pg_extension is an unimplemented stub; calling the extension function is the best available proof.
// 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"}},
},
{
// 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{
+21 -22
View File
@@ -18,7 +18,6 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
@@ -820,29 +819,29 @@ 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",
SetUpScript: []string{
`create extension "uuid-ossp";`,
"select dolt_commit('-Am', 'seed extension');",
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
"select dolt_push('origin', 'main');",
fmt.Sprintf("select dolt_clone('%s', 'cloned_ext');", remoteUrl),
"USE cloned_ext",
remoteUrl = localRemoteUrl(t, "remote")
runRemoteTest(t, ScriptTest{
Name: "extension is preserved across push and clone",
SetUpScript: []string{
`create extension "uuid-ossp";`,
"select dolt_commit('-Am', 'seed extension');",
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
"select dolt_push('origin', 'main');",
fmt.Sprintf("select dolt_clone('%s', 'cloned_ext');", remoteUrl),
"USE cloned_ext",
},
Assertions: []ScriptTestAssertion{
{
Query: "select extname, extversion from pg_catalog.pg_extension;",
Expected: []sql.Row{{"uuid-ossp", "1.1"}},
},
Assertions: []ScriptTestAssertion{
{
// pg_extension is an unimplemented stub; calling the extension function is the best available proof.
// 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"}},
},
{
// 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{
+57 -7
View File
@@ -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"}},
},
},
},
})
}