Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SurrealDB #917

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
SOURCE ?= file go_bindata github github_ee bitbucket aws_s3 google_cloud_storage godoc_vfs gitlab
DATABASE ?= postgres mysql redshift cassandra spanner cockroachdb yugabytedb clickhouse mongodb sqlserver firebird neo4j pgx pgx5 rqlite
DATABASE ?= postgres mysql redshift cassandra spanner cockroachdb yugabytedb clickhouse mongodb sqlserver firebird neo4j pgx pgx5 rqlite surrealdb
DATABASE_TEST ?= $(DATABASE) sqlite sqlite3 sqlcipher
VERSION ?= $(shell git describe --tags 2>/dev/null | cut -c 2-)
TEST_FLAGS ?=
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Database drivers run migrations. [Add a new database?](database/driver.go)
* [Firebird](database/firebird)
* [MS SQL Server](database/sqlserver)
* [rqlite](database/rqlite)
* [SurrealDB](database/surrealdb)

### Database URLs

Expand Down
18 changes: 18 additions & 0 deletions database/surrealdb/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# SurrealDB

`surrealdb://username:password@host:port/namespace/database` (`surreal://` works, too)

| URL Query | WithInstance Config | Description |
|------------|---------------------|-------------|
| `x-migrations-table` | `MigrationsTable` | Name of the migrations table. Defaults to `schema_migrations`. |
| `namespace` | `Namespace` | The namespace to connect to |
| `database` | `DatabaseName` | The name of the database to connect to |
| `user` | | The user to sign in as |
| `password` | | The user's password |
| `host` | | The host to connect to. |
| `port` | | The port to bind to. (optional) |
| `sslmode` | | Whether or not to use SSL (disable\|require) |

## Notes

* Uses the `github.com/surrealdb/surrealdb.go` surrealdb driver
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
REMOVE TABLE user;
15 changes: 15 additions & 0 deletions database/surrealdb/examples/migrations/33_create_table.up.surql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
BEGIN;

-- Create schemafull user table.
DEFINE TABLE user SCHEMAFULL;

-- Define some fields.
DEFINE FIELD firstName ON TABLE user TYPE string
ASSERT $value != NONE;
DEFINE FIELD lastName ON TABLE user TYPE string
ASSERT $value != NONE;
DEFINE FIELD email ON TABLE user TYPE string
ASSERT $value != NONE AND string::is::email($value);
DEFINE INDEX userEmailIndex ON TABLE user COLUMNS email UNIQUE;

COMMIT;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
REMOVE INDEX userEmailIndex ON TABLE user;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Make sure that email addresses in the user table are always unique
DEFINE INDEX userEmailIndex ON TABLE user COLUMNS email UNIQUE;
299 changes: 299 additions & 0 deletions database/surrealdb/surrealdb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
package surrealdb

import (
"fmt"
"io"
nurl "net/url"
"os"
"strings"
"time"

"github.com/golang-migrate/migrate/v4/database"
"github.com/hashicorp/go-multierror"
"github.com/surrealdb/surrealdb.go"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if the surrealdb go client supports multiple platforms?
My guess is that it does since it seems to be implemented in pure Go.
You can test this by running make build-cli or more comprehensively with DATABASE=$(make echo-database) SOURCE=$(make echo-source) goreleaser release --clean --snapshot --skip-docker

Copy link
Author

@sam-kleiner sam-kleiner Feb 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that yes it supports multiple platforms here is the output of DATABASE=$(make echo-database) SOURCE=$(make echo-source) goreleaser release --clean --snapshot --skip-docker. That looks to have succeeded

  • starting release...
  • loading                                          path=.goreleaser.yml
  • DEPRECATED: --skip-docker was deprecated in favor of --skip=docker, check https://goreleaser.com/deprecations#-skip for more details
  • skipping announce, docker, publish and validate...
  • loading environment variables
  • getting and validating git state
    • ignoring errors because this is a snapshot     error=git doesn't contain any tags. Either add a tag or use --snapshot
    • git state                                      commit=bcdd2f120462bec5a83b31c7f8a611d44e963be2 branch=surrealdb current_tag=v0.0.0 previous_tag=<unknown> dirty=false
    • pipe skipped                                   reason=disabled during snapshot mode
  • parsing tag
  • setting defaults
  • snapshotting
    • building snapshot...                           version=v0.0.0-next
  • running before hooks
    • running                                        hook=go mod tidy
    • took: 4s
  • checking distribution directory
  • loading go mod information
  • build prerequisites
  • writing effective config file
    • writing                                        config=dist/config.yaml
  • building binaries
    • building                                       binary=dist/migrate_darwin_arm64/migrate
    • building                                       binary=dist/migrate_linux_amd64_v1/migrate
    • building                                       binary=dist/migrate_windows_arm_7/migrate.exe
    • building                                       binary=dist/migrate_linux_arm64/migrate
    • building                                       binary=dist/migrate_darwin_amd64_v1/migrate
    • building                                       binary=dist/migrate_windows_arm64/migrate.exe
    • building                                       binary=dist/migrate_windows_amd64_v1/migrate.exe
    • building                                       binary=dist/migrate_windows_386/migrate.exe
    • building                                       binary=dist/migrate_linux_386/migrate
    • building                                       binary=dist/migrate_linux_arm_7/migrate
    • took: 1m11s
  • archives
    • creating                                       archive=dist/migrate.linux-386.tar.gz
    • creating                                       archive=dist/migrate.linux-armv7.tar.gz
    • creating                                       archive=dist/migrate.windows-arm64.zip
    • creating                                       archive=dist/migrate.linux-arm64.tar.gz
    • creating                                       archive=dist/migrate.windows-amd64.zip
    • creating                                       archive=dist/migrate.windows-armv7.zip
    • creating                                       archive=dist/migrate.windows-386.zip
    • creating                                       archive=dist/migrate.darwin-arm64.tar.gz
    • creating                                       archive=dist/migrate.linux-amd64.tar.gz
    • creating                                       archive=dist/migrate.darwin-amd64.tar.gz
    • took: 6s
  • creating source archive
    • creating source archive                        file=migrate-v0.0.0-next.zip
  • linux packages
    • creating                                       package=migrate format=deb arch=arm64 file=dist/migrate.linux-arm64.deb
    • creating                                       package=migrate format=deb arch=386 file=dist/migrate.linux-386.deb
    • creating                                       package=migrate format=deb arch=arm7 file=dist/migrate.linux-armv7.deb
    • creating                                       package=migrate format=deb arch=amd64v1 file=dist/migrate.linux-amd64.deb
    • took: 1s
  • calculating checksums
  • storing release metadata
    • writing                                        file=dist/artifacts.json
    • writing                                        file=dist/metadata.json
  • you are using deprecated options, check the output above for details
  • release succeeded after 1m21s
  • thanks for using goreleaser!

)

func init() {
database.Register("surreal", &SurrealDB{})
database.Register("surrealdb", &SurrealDB{})
}

var DefaultMigrationsTable = "schema_migrations"
var (
ErrNilConfig = fmt.Errorf("no config")
)

type Config struct {
MigrationsTable string
Namespace string
DatabaseName string
}

func (c *Config) GetVersionDocumentId() (docId string) {
return fmt.Sprintf("%s:version", c.MigrationsTable)
}

func (c *Config) GetLockDocumentId() (docId string) {
return fmt.Sprintf("%s:lock", c.MigrationsTable)
}

type SurrealDB struct {
db *surrealdb.DB
config *Config
}

type DBInfo struct {
DL map[string]string `json:"dl"`
DT map[string]string `json:"dt"`
FC map[string]string `json:"fc"`
PA map[string]string `json:"pa"`
SC map[string]string `json:"sc"`
TB map[string]string `json:"tb"`
}

type VersionInfo struct {
ID string `json:"id,omitempty"`
Version int `json:"version,omitempty"`
Dirty bool `json:"dirty,omitempty"`
}

type LockDoc struct {
ID string `json:"id,omitempty"`
Pid int `json:"pid,omitempty"`
Hostname string `json:"hostname,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
}

func WithInstance(instance *surrealdb.DB, config *Config) (database.Driver, error) {
if config == nil {
return nil, ErrNilConfig
}

if _, err := instance.Info(); err != nil {
return nil, err
}

if len(config.MigrationsTable) == 0 {
config.MigrationsTable = DefaultMigrationsTable
}

mx := &SurrealDB{
db: instance,
config: config,
}
if err := mx.ensureVersionTable(); err != nil {
return nil, err
}
return mx, nil
}

// ensureVersionTable checks if versions table exists and, if not, creates it.
// Note that this function locks the database, which deviates from the usual
// convention of "caller locks" in the SurrealDB type.
func (m *SurrealDB) ensureVersionTable() (err error) {
if err = m.Lock(); err != nil {
return err
}

defer func() {
if e := m.Unlock(); e != nil {
if err == nil {
err = e
} else {
err = multierror.Append(err, e)
}
}
}()

if err != nil {
return err
}
if _, _, err = m.Version(); err != nil {
return err
}
return nil
}

func (m *SurrealDB) Open(url string) (database.Driver, error) {
purl, err := nurl.Parse(url)
if err != nil {
return nil, err
}

qv := purl.Query()

migrationsTable := qv.Get("x-migrations-table")
if len(migrationsTable) == 0 {
migrationsTable = DefaultMigrationsTable
}

scheme := "wss"
host := purl.Host
path := strings.TrimPrefix(strings.TrimPrefix(purl.Path, "/rpc/"), "/")
username := purl.User.Username()
password, _ := purl.User.Password()

if len(purl.Query().Get("sslmode")) > 0 {
if purl.Query().Get("sslmode") == "disable" {
scheme = "ws"
}
}

split_path := strings.SplitN(path, "/", 2)
namespace, database_name := split_path[0], split_path[1]

if len(namespace) < 1 {
return nil, fmt.Errorf("missing namespace in path: %s", path)
} else if len(database_name) < 1 {
return nil, fmt.Errorf("missing dataspace name in path: %s", path)
} else if strings.Contains(namespace, "/") {
return nil, fmt.Errorf("bad path: %s. Path should be in format '/namespace/database'", path)
} else if strings.Contains(database_name, "/") {
return nil, fmt.Errorf("bad path: %s. Path should be in format '/namespace/database'", path)
}

connUrl := fmt.Sprintf("%s://%s/rpc", scheme, host)

db, err := surrealdb.New(connUrl)
if err != nil {
return nil, err
}

_, err = db.Signin(map[string]interface{}{
"user": username,
"pass": password,
})
if err != nil {
return nil, err
}

_, err = db.Use(namespace, database_name)
if err != nil {
return nil, err
}

mx, err := WithInstance(db, &Config{
Namespace: namespace,
DatabaseName: database_name,
MigrationsTable: migrationsTable,
})
if err != nil {
return nil, err
}

return mx, nil
}

func (m *SurrealDB) Close() error {
m.db.Close()
return nil
}

func (m *SurrealDB) Drop() (err error) {
query := `INFO FOR DB;`
result, err := surrealdb.SmartUnmarshal[DBInfo](m.db.Query(query, map[string]interface{}{}))
if err != nil {
return err
}

for tableName := range result.TB {
query := fmt.Sprintf(`REMOVE TABLE %s;`, tableName)
_, err := m.db.Query(query, map[string]interface{}{})
if err != nil {
return err
}
}

return nil
}

func (m *SurrealDB) Lock() error {
pid := os.Getpid()
hostname, err := os.Hostname()
if err != nil {
hostname = fmt.Sprintf("Could not determine hostname. Error: %s", err.Error())
}

lock_doc_id := m.config.GetLockDocumentId()
query := `BEGIN; CREATE $lock_doc_id SET pid = $pid, hostname = $hostname, created_at = $created_at; RETURN AFTER; COMMIT;`

// using m.db.Query looks to prevent a race condition that can occur when using m.db.Create
// if you use m.db.Create its possible for a second lock call shortly after first to not error as it should
_, err = surrealdb.SmartUnmarshal[[]LockDoc](
m.db.Query(query, map[string]interface{}{
"lock_doc_id": lock_doc_id,
"pid": pid,
"hostname": hostname,
"created_at": time.Now().Format(time.RFC3339),
}),
)
if err != nil {
return err
}

return nil
}

func (m *SurrealDB) Unlock() error {
lock_doc_id := m.config.GetLockDocumentId()
query := `BEGIN; LET $lock = SELECT * FROM $lock_doc_id; DELETE $lock; COMMIT;`

// Delete will error if lock_doc_id does not exist because $lock ends up as NONE
_, err := m.db.Query(query, map[string]interface{}{"lock_doc_id": lock_doc_id})
return err
}

func (m *SurrealDB) Run(migration io.Reader) error {
mig, err := io.ReadAll(migration)
if err != nil {
return err
}

query := string(mig[:])
_, err = m.db.Query(query, map[string]interface{}{})
return err
}

func (m *SurrealDB) SetVersion(version int, dirty bool) error {
version_document_id := m.config.GetVersionDocumentId()
params := map[string]interface{}{"version_document_id": version_document_id}
query := `BEGIN; DELETE $version_document_id; `

// Also re-write the schema version for nil dirty versions to prevent
// empty schema version for failed down migration on the first migration
// See: https://github.com/golang-migrate/migrate/issues/330
if version >= 0 || (version == database.NilVersion && dirty) {
params = map[string]interface{}{
"version_document_id": version_document_id,
"version": version,
"dirty": dirty,
}
query += `CREATE $version_document_id CONTENT {
version: $version,
dirty: $dirty
}; `
}

query += `COMMIT;`

_, err := m.db.Query(query, params)
if err != nil {
return err
}

return nil
}

func (m *SurrealDB) Version() (version int, dirty bool, err error) {
version_document_id := m.config.GetVersionDocumentId()

query := fmt.Sprintf("SELECT * FROM %s;", version_document_id)
versionInfo, err := surrealdb.SmartUnmarshal[[]VersionInfo](m.db.Query(query, map[string]interface{}{}))
if err != nil {
return database.NilVersion, false, err
} else if len(versionInfo) == 0 {
return database.NilVersion, false, nil
}

return versionInfo[0].Version, versionInfo[0].Dirty, nil
}
Loading
Loading