diff --git a/Makefile b/Makefile index 8e23a43c7..74da622ee 100644 --- a/Makefile +++ b/Makefile @@ -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 ?= diff --git a/README.md b/README.md index 975348685..3d76d8cd8 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/database/surrealdb/README.md b/database/surrealdb/README.md new file mode 100644 index 000000000..9e1a6d8fb --- /dev/null +++ b/database/surrealdb/README.md @@ -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 diff --git a/database/surrealdb/examples/migrations/33_create_table.down.surql b/database/surrealdb/examples/migrations/33_create_table.down.surql new file mode 100644 index 000000000..dc99905db --- /dev/null +++ b/database/surrealdb/examples/migrations/33_create_table.down.surql @@ -0,0 +1 @@ +REMOVE TABLE user; diff --git a/database/surrealdb/examples/migrations/33_create_table.up.surql b/database/surrealdb/examples/migrations/33_create_table.up.surql new file mode 100644 index 000000000..f3502f355 --- /dev/null +++ b/database/surrealdb/examples/migrations/33_create_table.up.surql @@ -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; diff --git a/database/surrealdb/examples/migrations/44_alter_table.down.surql b/database/surrealdb/examples/migrations/44_alter_table.down.surql new file mode 100644 index 000000000..33f354381 --- /dev/null +++ b/database/surrealdb/examples/migrations/44_alter_table.down.surql @@ -0,0 +1 @@ +REMOVE INDEX userEmailIndex ON TABLE user; diff --git a/database/surrealdb/examples/migrations/44_alter_table.up.surql b/database/surrealdb/examples/migrations/44_alter_table.up.surql new file mode 100644 index 000000000..84ea43ed9 --- /dev/null +++ b/database/surrealdb/examples/migrations/44_alter_table.up.surql @@ -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; diff --git a/database/surrealdb/surrealdb.go b/database/surrealdb/surrealdb.go new file mode 100644 index 000000000..5b1111c30 --- /dev/null +++ b/database/surrealdb/surrealdb.go @@ -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" +) + +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 +} diff --git a/database/surrealdb/surrealdb_test.go b/database/surrealdb/surrealdb_test.go new file mode 100644 index 000000000..cedba8eb9 --- /dev/null +++ b/database/surrealdb/surrealdb_test.go @@ -0,0 +1,367 @@ +package surrealdb + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/docker/go-connections/nat" + "github.com/golang-migrate/migrate/v4" + + "github.com/dhui/dktest" + + dt "github.com/golang-migrate/migrate/v4/database/testing" + "github.com/golang-migrate/migrate/v4/dktesting" + _ "github.com/golang-migrate/migrate/v4/source/file" + "github.com/surrealdb/surrealdb.go" +) + +type ConnInfo struct { + User string + Pass string + Host string + Port string + NS string + DB string +} + +func (c *ConnInfo) getUrl() string { + return fmt.Sprintf("ws://%s:%s/rpc", c.Host, c.Port) +} + +func (c *ConnInfo) connString(options ...string) string { + options = append(options, "sslmode=disable") + return fmt.Sprintf("surrealdb://%s:%s@%s:%s/%s/%s?%s", c.User, c.Pass, c.Host, c.Port, c.NS, c.DB, strings.Join(options, "&")) +} + +func getPortBindings() map[nat.Port][]nat.PortBinding { + _, portBindings, err := nat.ParsePortSpecs([]string{"8000/tcp"}) + if err != nil { + panic("Error setting up port bindings") + } + return portBindings +} + +const user = "user" +const pass = "pass" + +var ( + opts = dktest.Options{ + Entrypoint: []string{""}, + Cmd: []string{"/surreal", "start", "--user", user, "--pass", pass, "memory"}, + PortBindings: getPortBindings(), + PortRequired: true, + ReadyFunc: isReady, + } + specs = []dktesting.ContainerSpec{ + {ImageName: "surrealdb/surrealdb:v1.4.2", Options: opts}, + } +) + +func Test(t *testing.T) { + dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { + ip, port, err := c.FirstPort() + if err != nil { + t.Fatal(err) + } + + connInfo := getConnInfo(ip, port) + sur := &SurrealDB{} + d, err := sur.Open(connInfo.connString()) + defer func() { + if err := d.Close(); err != nil { + t.Error(err) + } + }() + if err != nil { + t.Fatal(err) + } + dt.Test(t, d, []byte("SELECT * FROM 1;")) + }) +} + +func TestMigrate(t *testing.T) { + dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { + ip, port, err := c.FirstPort() + if err != nil { + t.Fatal(err) + } + + connInfo := getConnInfo(ip, port) + sur := &SurrealDB{} + d, err := sur.Open(connInfo.connString()) + defer func() { + if err := d.Close(); err != nil { + t.Error(err) + } + }() + if err != nil { + t.Fatal(err) + } + + m, err := migrate.NewWithDatabaseInstance("file://./examples/migrations", "surrealdb", d) + if err != nil { + t.Fatal(err) + } + dt.TestMigrate(t, m) + }) +} + +func TestErrorParsing(t *testing.T) { + dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { + ip, port, err := c.FirstPort() + if err != nil { + t.Fatal(err) + } + + connInfo := getConnInfo(ip, port) + sur := &SurrealDB{} + d, err := sur.Open(connInfo.connString()) + defer func() { + if err := d.Close(); err != nil { + t.Error(err) + } + }() + if err != nil { + t.Fatal(err) + } + + badQuery := "DEFINE TABLEE user SCHEMALESS;" + wantErr := `sending request failed for method 'query': There was a problem with the database: Parse error: Failed to parse query at line 1 column 8 expected query to end` + if err := d.Run(strings.NewReader(badQuery)); err == nil { + t.Fatal("expected err but got nil") + } else if !strings.HasPrefix(err.Error(), wantErr) { + t.Fatalf("expected '%s' to start with '%s'", err.Error(), wantErr) + } else if !strings.Contains(err.Error(), badQuery) { + t.Fatalf("expected err to contain %s", badQuery) + } + }) +} + +func TestFilterCustomQuery(t *testing.T) { + dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { + ip, port, err := c.FirstPort() + if err != nil { + t.Fatal(err) + } + + connInfo := getConnInfo(ip, port) + sur := &SurrealDB{} + d, err := sur.Open(connInfo.connString("x-custom=foobar")) + defer func() { + if err := d.Close(); err != nil { + t.Error(err) + } + }() + if err != nil { + t.Fatal(err) + } + }) +} + +func TestMigrationTable(t *testing.T) { + dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { + ip, port, err := c.FirstPort() + if err != nil { + t.Fatal(err) + } + + connInfo := getConnInfo(ip, port) + sur := &SurrealDB{} + d, err := sur.Open(connInfo.connString()) + defer func() { + if err := d.Close(); err != nil { + t.Error(err) + } + }() + if err != nil { + t.Fatal(err) + } + + m, err := migrate.NewWithDatabaseInstance("file://./examples/migrations", "surrealdb", d) + if err != nil { + t.Fatal(err) + } + + err = m.Up() + if err != nil { + t.Fatal(err) + } + + db, err := surrealdb.New(connInfo.getUrl()) + if err != nil { + t.Fatal(err) + } + defer db.Close() + + _, err = db.Signin(map[string]interface{}{ + "user": connInfo.User, + "pass": connInfo.Pass, + }) + if err != nil { + t.Fatal(err) + } + + _, err = db.Use(connInfo.NS, connInfo.DB) + if err != nil { + t.Fatal(err) + } + + _, err = db.Query("SELECT * FROM schema_migrations:version", map[string]interface{}{}) + if err != nil { + t.Fatal(err) + } + }) +} + +func TestParallelNamespace(t *testing.T) { + dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { + ip, port, err := c.FirstPort() + if err != nil { + t.Fatal(err) + } + + connInfoFoo := getConnInfoForDB(ip, port, "foo", "foo") + surFoo := &SurrealDB{} + dfoo, err := surFoo.Open(connInfoFoo.connString()) + defer func() { + err = dfoo.Close() + if err != nil { + t.Fatal(err) + } + }() + if err != nil { + t.Fatal(err) + } + + connInfoBar := getConnInfoForDB(ip, port, "bar", "bar") + surBar := &SurrealDB{} + dbar, err := surBar.Open(connInfoBar.connString()) + defer func() { + err = dbar.Close() + if err != nil { + t.Fatal(err) + } + }() + if err != nil { + t.Fatal(err) + } + + if err := dfoo.Lock(); err != nil { + t.Fatal(err) + } + + if err := dbar.Lock(); err != nil { + t.Fatal(err) + } + + if err := dbar.Unlock(); err != nil { + t.Fatal(err) + } + + if err := dfoo.Unlock(); err != nil { + t.Fatal(err) + } + }) +} + +func TestParallelDatabase(t *testing.T) { + dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { + ip, port, err := c.FirstPort() + if err != nil { + t.Fatal(err) + } + + connInfoFoo := getConnInfoForDB(ip, port, "foo", "foo") + surFoo := &SurrealDB{} + dfoo, err := surFoo.Open(connInfoFoo.connString()) + defer func() { + err = dfoo.Close() + if err != nil { + t.Fatal(err) + } + }() + if err != nil { + t.Fatal(err) + } + + connInfoBar := getConnInfoForDB(ip, port, "foo", "bar") + surBar := &SurrealDB{} + dbar, err := surBar.Open(connInfoBar.connString()) + defer func() { + err = dbar.Close() + if err != nil { + t.Fatal(err) + } + }() + if err != nil { + t.Fatal(err) + } + + if err := dfoo.Lock(); err != nil { + t.Fatal(err) + } + + if err := dbar.Lock(); err != nil { + t.Fatal(err) + } + + if err := dbar.Unlock(); err != nil { + t.Fatal(err) + } + + if err := dfoo.Unlock(); err != nil { + t.Fatal(err) + } + }) +} + +/////////////////////////////////////// +////////// Test Helper Funcs ////////// +/////////////////////////////////////// + +func getConnInfoForDB(host string, port string, ns string, db string) ConnInfo { + return ConnInfo{ + User: user, + Pass: pass, + Host: host, + Port: port, + NS: ns, + DB: db, + } +} + +func getConnInfo(host string, port string) ConnInfo { + return getConnInfoForDB(host, port, "test_ns", "test_db") +} + +func isReady(ctx context.Context, c dktest.ContainerInfo) bool { + ip, port, err := c.FirstPort() + if err != nil { + return false + } + + connInfo := getConnInfo(ip, port) + db, err := surrealdb.New(connInfo.getUrl()) + if err != nil { + return false + } + defer db.Close() + + _, err = db.Signin(map[string]interface{}{ + "user": connInfo.User, + "pass": connInfo.Pass, + }) + if err != nil { + return false + } + + _, err = db.Use(connInfo.NS, connInfo.DB) + if err != nil { + return false + } + + _, err = db.Query(`SELECT * FROM 1;`, map[string]interface{}{}) + return err == nil +} diff --git a/go.mod b/go.mod index 8cc85c554..5ff5082b3 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba github.com/snowflakedb/gosnowflake v1.6.19 github.com/stretchr/testify v1.8.3 + github.com/surrealdb/surrealdb.go v0.2.1 github.com/xanzy/go-gitlab v0.15.0 go.mongodb.org/mongo-driver v1.7.5 go.uber.org/atomic v1.7.0 @@ -112,6 +113,7 @@ require ( github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/gorilla/handlers v1.4.2 // indirect github.com/gorilla/mux v1.7.4 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect github.com/hashicorp/errwrap v1.1.0 // indirect diff --git a/go.sum b/go.sum index 918342a16..f41704b13 100644 --- a/go.sum +++ b/go.sum @@ -299,6 +299,8 @@ github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= @@ -551,6 +553,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/surrealdb/surrealdb.go v0.2.1 h1:E4rCnD75Ftq8/wTgbQ9kJgMACi3xMziXtMlRkm6Jh1g= +github.com/surrealdb/surrealdb.go v0.2.1/go.mod h1:CloW70O49xyVO/rGO9cAZ62FEbl0/hreRHEJuamnndQ= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/xanzy/go-gitlab v0.15.0 h1:rWtwKTgEnXyNUGrOArN7yyc3THRkpYcKXIXia9abywQ= diff --git a/internal/cli/build_surrealdb.go b/internal/cli/build_surrealdb.go new file mode 100644 index 000000000..e5849e4f3 --- /dev/null +++ b/internal/cli/build_surrealdb.go @@ -0,0 +1,8 @@ +//go:build surrealdb +// +build surrealdb + +package cli + +import ( + _ "github.com/golang-migrate/migrate/v4/database/surrealdb" +)