Documentation
¶
Overview ¶
The surrealdb package implements [SurrealDB RPC Protocol] in the Go way.
Connection Engines ¶
There are 2 different connection engines, WebSocket and HTTP, you can use to connect to SurrealDB backend.
Provide a proper SurrealDB endpoint URL to FromEndpointURLString so that it chooses the right backend for you.
For WebSocket connections that require reliability, consider using github.com/surrealdb/surrealdb.go/contrib/rews, which provides automatic reconnection with session restoration. This is particularly important because SurrealDB's RPC Protocol over WebSocket is stateful - authentication, namespace/database selection, and live queries must be restored after reconnection.
Data Models ¶
The surrealdb package facilitates communication between client and the backend service using the SurrealDB CBOR Protocol. The protocol encoding/decoding is handled by github.com/surrealdb/surrealdb.go/surrealcbor.
The most commonly used data type is models.RecordID, which represents a SurrealDB record identifier which is a pair of table name and an identifier within that table.
For more information on CBOR and how it relates to SurrealDB's data models, please refer to the github.com/surrealdb/surrealdb.go/pkg/models package.
Use Query for most use cases ¶
For most use cases, you can use the Query function to execute SurrealQL statements.
Query is recommended for both simple and complex queries, transactions, and when you need full control over your database operations.
To ease writing queries for Query with more type-safety, you can use the github.com/surrealdb/surrealdb.go/contrib/surrealql package.
Use Send for low-level control ¶
Send is used internally by all data manipulation methods.
Use it directly when you want to create requests yourself.
Examples and Experimental Packages ¶
The github.com/surrealdb/surrealdb.go/contrib directory contains examples and experimental packages that are not covered by the SDK's backward compatibility guarantee.
Example (BearerAccessMethod_v3_recordUser) ¶
Example_bearerAccessMethod_v3_recordUser demonstrates bearer access method authentication for record users in SurrealDB v3.
Record user bearer grants work similarly to system user grants, but authenticate as a specific database record instead of a system user. This is useful when you want to grant API access to specific records/entities in your database.
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
ctx := context.Background()
db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
panic(err)
}
db, err = testenv.Init(db, "example_bearer_record_v3", "testdb")
if err != nil {
panic(err)
}
// Sign in as root to set up bearer access
_, err = db.SignIn(ctx, surrealdb.Auth{
Username: "root",
Password: "root",
})
if err != nil {
panic(fmt.Sprintf("SignIn as root failed: %v", err))
}
err = db.Use(ctx, "example_bearer_record_v3", "testdb")
if err != nil {
panic(fmt.Sprintf("Use failed: %v", err))
}
// Check SurrealDB version - this example is v3+ only
v, err := testenv.GetVersion(ctx, db)
if err != nil {
panic(fmt.Sprintf("GetVersion failed: %v", err))
}
if !v.IsV3OrLater() {
// Skip gracefully on v2 to avoid output verification failures
fmt.Println("Bearer record access demonstrated successfully")
return
}
// =========================================================================
// Bearer Access for Record Users
// =========================================================================
// 1. Define a table and create a record that will use bearer access
_, err = surrealdb.Query[any](ctx, db, `DEFINE TABLE IF NOT EXISTS services SCHEMAFULL`, nil)
if err != nil {
panic(fmt.Sprintf("DEFINE TABLE failed: %v", err))
}
_, err = surrealdb.Query[any](ctx, db, `DEFINE FIELD IF NOT EXISTS name ON services TYPE string`, nil)
if err != nil {
panic(fmt.Sprintf("DEFINE FIELD failed: %v", err))
}
_, err = surrealdb.Query[any](ctx, db, `CREATE services:webhook_handler SET name = 'Webhook Handler Service'`, nil)
if err != nil {
panic(fmt.Sprintf("CREATE record failed: %v", err))
}
// 2. Define bearer access method for record users
// This allows generating bearer keys that authenticate as specific records
_, err = surrealdb.Query[any](ctx, db, `DEFINE ACCESS IF NOT EXISTS bearer_service_api ON DATABASE TYPE BEARER FOR RECORD`, nil)
if err != nil {
panic(fmt.Sprintf("DEFINE ACCESS failed: %v", err))
}
// 3. Grant a bearer key for the record
// The grant result's subject contains: { record: { Table: "services", ID: "webhook_handler" } }
grantResult, err := surrealdb.Query[map[string]any](ctx, db,
`ACCESS bearer_service_api GRANT FOR RECORD services:webhook_handler`, nil)
if err != nil {
panic(fmt.Sprintf("ACCESS GRANT failed: %v", err))
}
// Extract the bearer key from the nested grant object
grantData := (*grantResult)[0].Result
grantInfo := grantData["grant"].(map[string]any)
bearerKey := grantInfo["key"].(string)
// 4. Sign in using the bearer key
token, err := db.SignIn(ctx, map[string]any{
"NS": "example_bearer_record_v3",
"DB": "testdb",
"AC": "bearer_service_api",
"key": bearerKey,
})
if err != nil {
panic(fmt.Sprintf("Bearer SignIn failed: %v", err))
}
_ = token // JWT token for reference
// 5. Verify we can perform operations as the authenticated record user
_, err = surrealdb.Query[any](ctx, db, `RETURN "Hello from bearer-authenticated record user"`, nil)
if err != nil {
panic(fmt.Sprintf("Query after bearer auth failed: %v", err))
}
fmt.Println("Bearer record access demonstrated successfully")
}
Output: Bearer record access demonstrated successfully
Example (BearerAccessMethod_v3_systemUser) ¶
Example_bearerAccessMethod_v3_systemUser demonstrates bearer access method authentication for system users in SurrealDB v3.
Bearer access methods allow generating bearer grants with an associated key that can be used to authenticate as a specific system user or record user. Bearer grants are ideal for service-to-service authentication as they provide: - Stronger security guarantees than passwords - Auditable and revocable credentials - No need to work with JWT directly
This example only runs against SurrealDB v3.x. When run against v2.x, it skips with a message since bearer access requires experimental flag in v2.
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
ctx := context.Background()
db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
panic(err)
}
db, err = testenv.Init(db, "example_bearer_v3", "testdb")
if err != nil {
panic(err)
}
// Sign in as root to set up bearer access
_, err = db.SignIn(ctx, surrealdb.Auth{
Username: "root",
Password: "root",
})
if err != nil {
panic(fmt.Sprintf("SignIn as root failed: %v", err))
}
err = db.Use(ctx, "example_bearer_v3", "testdb")
if err != nil {
panic(fmt.Sprintf("Use failed: %v", err))
}
// Check SurrealDB version - this example is v3+ only
// In v2.x, bearer access requires --allow-experimental bearer_access flag
v, err := testenv.GetVersion(ctx, db)
if err != nil {
panic(fmt.Sprintf("GetVersion failed: %v", err))
}
if !v.IsV3OrLater() {
// Skip gracefully on v2 to avoid output verification failures
fmt.Println("Bearer access demonstrated successfully")
return
}
// =========================================================================
// Bearer Access for System Users
// =========================================================================
// 1. Define a database-level user that will use bearer access
_, err = surrealdb.Query[any](ctx, db, `DEFINE USER IF NOT EXISTS apiuser ON DATABASE PASSWORD 'secret' ROLES EDITOR`, nil)
if err != nil {
panic(fmt.Sprintf("DEFINE USER failed: %v", err))
}
// 2. Define bearer access method for system users
// This allows generating bearer keys that authenticate as system users
_, err = surrealdb.Query[any](ctx, db, `DEFINE ACCESS IF NOT EXISTS bearer_api ON DATABASE TYPE BEARER FOR USER`, nil)
if err != nil {
panic(fmt.Sprintf("DEFINE ACCESS failed: %v", err))
}
// 3. Grant a bearer key for the user
// The grant result contains:
// - grant.key: the bearer key to use for signin (format: surreal-bearer-{id}-{random})
// - grant.id: unique grant identifier
// - subject.user: the user this grant authenticates as
// - creation/expiration: timestamps (default expiration is 30 days)
grantResult, err := surrealdb.Query[map[string]any](ctx, db, `ACCESS bearer_api GRANT FOR USER apiuser`, nil)
if err != nil {
panic(fmt.Sprintf("ACCESS GRANT failed: %v", err))
}
// Extract the bearer key from the nested grant object
grantData := (*grantResult)[0].Result
grantInfo := grantData["grant"].(map[string]any)
bearerKey := grantInfo["key"].(string)
// 4. Sign in using the bearer key
// The signin request uses:
// - NS: target namespace
// - DB: target database
// - AC: access method name
// - key: the bearer key from the grant
token, err := db.SignIn(ctx, map[string]any{
"NS": "example_bearer_v3",
"DB": "testdb",
"AC": "bearer_api",
"key": bearerKey,
})
if err != nil {
panic(fmt.Sprintf("Bearer SignIn failed: %v", err))
}
// The signin returns a JWT token (string) that can be:
// - Used with db.Authenticate() on new connections
// - Stored for later use
// Note: The WebSocket session is already authenticated after SignIn
_ = token // JWT token for reference
// 5. Verify we can perform operations as the authenticated user
_, err = surrealdb.Query[any](ctx, db, `RETURN "Hello from bearer-authenticated user"`, nil)
if err != nil {
panic(fmt.Sprintf("Query after bearer auth failed: %v", err))
}
fmt.Println("Bearer access demonstrated successfully")
}
Output: Bearer access demonstrated successfully
Example (RecordAccessMethodWithRefresh_v3) ¶
Example_recordAccessMethodWithRefresh_v3 demonstrates TYPE RECORD access method with WITH REFRESH in SurrealDB v3 using SignInWithRefresh.
WITH REFRESH enables refresh token functionality for record access methods: - SignInWithRefresh returns a Tokens with "Access" (JWT) and "Refresh" tokens - The refresh token can be used to obtain new tokens without credentials
Key differences from bearer access: - Bearer access uses "key" parameter with SignIn (format: surreal-bearer-...) - Record access with refresh uses "refresh" parameter with SignInWithRefresh
This example only runs against SurrealDB v3.x. In v2.x, WITH REFRESH syntax is accepted but not implemented (signin still returns string token).
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
ctx := context.Background()
db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
panic(err)
}
db, err = testenv.Init(db, "example_record_refresh_v3", "testdb")
if err != nil {
panic(err)
}
// Sign in as root to set up the access method
_, err = db.SignIn(ctx, surrealdb.Auth{
Username: "root",
Password: "root",
})
if err != nil {
panic(fmt.Sprintf("SignIn as root failed: %v", err))
}
err = db.Use(ctx, "example_record_refresh_v3", "testdb")
if err != nil {
panic(fmt.Sprintf("Use failed: %v", err))
}
// Check SurrealDB version - WITH REFRESH is only fully implemented in v3+
v, err := testenv.GetVersion(ctx, db)
if err != nil {
panic(fmt.Sprintf("GetVersion failed: %v", err))
}
if !v.IsV3OrLater() {
// Skip gracefully on v2 to avoid output verification failures
fmt.Println("Record access with refresh demonstrated successfully")
return
}
// Get the appropriate function name for the version
recordFn := v.ThingOrRecordFn()
// =========================================================================
// Record Access Method with WITH REFRESH
// =========================================================================
// 1. Define user table
_, err = surrealdb.Query[any](ctx, db, `DEFINE TABLE IF NOT EXISTS user SCHEMAFULL`, nil)
if err != nil {
panic(fmt.Sprintf("DEFINE TABLE failed: %v", err))
}
_, err = surrealdb.Query[any](ctx, db, `DEFINE FIELD IF NOT EXISTS password ON user TYPE string`, nil)
if err != nil {
panic(fmt.Sprintf("DEFINE FIELD failed: %v", err))
}
// 2. Define record access method WITH REFRESH
// This enables refresh token functionality
defineQuery := fmt.Sprintf(`
DEFINE ACCESS IF NOT EXISTS user_access ON DATABASE TYPE RECORD
SIGNIN (
SELECT * FROM %s("user", $user) WHERE crypto::argon2::compare(password, $pass)
)
SIGNUP (
CREATE %s("user", $user) CONTENT {
password: crypto::argon2::generate($pass)
}
)
WITH REFRESH
`, recordFn, recordFn)
_, err = surrealdb.Query[any](ctx, db, defineQuery, nil)
if err != nil {
panic(fmt.Sprintf("DEFINE ACCESS failed: %v", err))
}
// 3. Sign up a user using SignUpWithRefresh (WITH REFRESH returns object, not string)
_, err = db.SignUpWithRefresh(ctx, map[string]any{
"NS": "example_record_refresh_v3",
"DB": "testdb",
"AC": "user_access",
"user": "testuser",
"pass": "testpass",
})
if err != nil {
panic(fmt.Sprintf("SignUpWithRefresh failed: %v", err))
}
// 4. Sign in with SignInWithRefresh to get both access and refresh tokens
tokenPair, err := db.SignInWithRefresh(ctx, map[string]any{
"NS": "example_record_refresh_v3",
"DB": "testdb",
"AC": "user_access",
"user": "testuser",
"pass": "testpass",
})
if err != nil {
panic(fmt.Sprintf("SignInWithRefresh failed: %v", err))
}
// tokenPair.Access is the JWT token
// tokenPair.Refresh is the refresh token (format: surreal-refresh-...)
// 5. Verify we can execute queries (session is authenticated after SignInWithRefresh)
_, err = surrealdb.Query[any](ctx, db, `RETURN "authenticated"`, nil)
if err != nil {
panic(fmt.Sprintf("Query after signin failed: %v", err))
}
// 6. Use refresh token to get new tokens (without credentials)
newTokens, err := db.SignInWithRefresh(ctx, map[string]any{
"NS": "example_record_refresh_v3",
"DB": "testdb",
"AC": "user_access",
"refresh": tokenPair.Refresh, // No username/password needed
})
if err != nil {
panic(fmt.Sprintf("Refresh SignInWithRefresh failed: %v", err))
}
// 7. The access token can be used with Authenticate() on NEW connections.
// Note: SignInWithRefresh already authenticates the current session,
// so Authenticate() is only needed when establishing a session on a different connection.
err = db.Authenticate(ctx, newTokens.Access)
if err != nil {
panic(fmt.Sprintf("Authenticate with new token failed: %v", err))
}
// 8. Verify queries still work (would work even without step 7 on this connection)
_, err = surrealdb.Query[any](ctx, db, `RETURN "authenticated with refreshed token"`, nil)
if err != nil {
panic(fmt.Sprintf("Query after refresh auth failed: %v", err))
}
fmt.Println("Record access with refresh demonstrated successfully")
}
Output: Record access with refresh demonstrated successfully
Index ¶
- func Create[TResult any, TWhat TableOrRecord, S sendable](ctx context.Context, s S, what TWhat, data any) (*TResult, error)
- func Delete[TResult any, TWhat TableOrRecord, S sendable](ctx context.Context, s S, what TWhat) (*TResult, error)
- func Insert[TResult any, S sendable](ctx context.Context, s S, what models.Table, data any) (*[]TResult, error)
- func InsertRelation[TResult any, S sendable](ctx context.Context, s S, relationship *Relationship) (*TResult, error)
- func Kill[S liveQueryable](ctx context.Context, s S, id string) error
- func Live[S liveQueryable](ctx context.Context, s S, table models.Table, diff bool) (*models.UUID, error)
- func Merge[TResult any, TWhat TableOrRecord, S sendable](ctx context.Context, s S, what TWhat, data any) (*TResult, error)
- func Patch[TWhat TableOrRecord, S sendable](ctx context.Context, s S, what TWhat, patches []PatchData) (*[]PatchData, error)
- func Query[TResult any, S sendable](ctx context.Context, s S, sql string, vars map[string]any) (*[]QueryResult[TResult], error)
- func QueryRaw[S sendable](ctx context.Context, s S, queries *[]QueryStmt) error
- func Relate[TResult any, S sendable](ctx context.Context, s S, rel *Relationship) (*TResult, error)
- func Select[TResult any, TWhat TableOrRecord, S sendable](ctx context.Context, s S, what TWhat) (*TResult, error)
- func Send[Result any](ctx context.Context, db *DB, res *connection.RPCResponse[Result], ...) error
- func Update[TResult any, TWhat TableOrRecord, S sendable](ctx context.Context, s S, what TWhat, data any) (*TResult, error)
- func Upsert[TResult any, TWhat TableOrRecord, S sendable](ctx context.Context, s S, what TWhat, data any) (*TResult, error)
- type Auth
- type DB
- func (db *DB) Attach(ctx context.Context) (*Session, error)
- func (db *DB) Authenticate(ctx context.Context, token string) error
- func (db *DB) Begin(ctx context.Context) (*Transaction, error)
- func (db *DB) Close(ctx context.Context) error
- func (db *DB) CloseLiveNotifications(liveQueryID string) error
- func (db *DB) Info(ctx context.Context) (map[string]any, error)
- func (db *DB) Invalidate(ctx context.Context) error
- func (db *DB) Let(ctx context.Context, key string, val any) error
- func (db *DB) LiveNotifications(liveQueryID string) (chan connection.Notification, error)
- func (db *DB) SignIn(ctx context.Context, authData any) (string, error)
- func (db *DB) SignInWithRefresh(ctx context.Context, authData any) (*Tokens, error)
- func (db *DB) SignUp(ctx context.Context, authData any) (string, error)
- func (db *DB) SignUpWithRefresh(ctx context.Context, authData any) (*Tokens, error)
- func (db *DB) Unset(ctx context.Context, key string) error
- func (db *DB) Use(ctx context.Context, ns, database string) error
- func (db *DB) Version(ctx context.Context) (*VersionData, error)
- func (db *DB) WithContext(ctx context.Context) *DBdeprecated
- type Objdeprecated
- type PatchData
- type QueryError
- type QueryResult
- type QueryStmt
- type RPCErrordeprecated
- type Relationship
- type Resultdeprecated
- type ServerError
- type Session
- func (s *Session) Authenticate(ctx context.Context, token string) error
- func (s *Session) Begin(ctx context.Context) (*Transaction, error)
- func (s *Session) CloseLiveNotifications(liveQueryID string) error
- func (s *Session) Detach(ctx context.Context) error
- func (s *Session) ID() *models.UUID
- func (s *Session) Info(ctx context.Context) (map[string]any, error)
- func (s *Session) Invalidate(ctx context.Context) error
- func (s *Session) Let(ctx context.Context, key string, val any) error
- func (s *Session) LiveNotifications(liveQueryID string) (chan connection.Notification, error)
- func (s *Session) SignIn(ctx context.Context, authData any) (string, error)
- func (s *Session) SignInWithRefresh(ctx context.Context, authData any) (*Tokens, error)
- func (s *Session) SignUp(ctx context.Context, authData any) (string, error)
- func (s *Session) SignUpWithRefresh(ctx context.Context, authData any) (*Tokens, error)
- func (s *Session) Unset(ctx context.Context, key string) error
- func (s *Session) Use(ctx context.Context, ns, database string) error
- func (s *Session) Version(ctx context.Context) (*VersionData, error)
- type TableOrRecord
- type Tokens
- type Transaction
- type VersionData
Examples ¶
- Package (BearerAccessMethod_v3_recordUser)
- Package (BearerAccessMethod_v3_systemUser)
- Package (RecordAccessMethodWithRefresh_v3)
- Create
- Create (RecordID_withUUID)
- Create (Server_unmarshal_error)
- DB (Record_user_auth_struct)
- DB (Record_user_custom_struct)
- DB (Signin_failure)
- DB.Attach
- DB.Authenticate (Jwt_databaseLevelUser)
- DB.Authenticate (Jwt_hs512_databaseLevelUser)
- DB.Authenticate (Jwt_hs512_namespaceLevelUser)
- DB.Authenticate (Jwt_hs512_rootLevelUser)
- DB.Authenticate (Jwt_hs512_rootLevelUser_expired)
- DB.Begin
- DB.SignIn (DatabaseLevelUser)
- DB.SignIn (DatabaseLevelUser_failureDueToMissingNamespace)
- DB.SignIn (NamespaceLevelUser)
- DB.SignIn (NamespaceLevelUser_failureDueToExtraDatabase)
- DB.SignIn (RootLevelUser)
- DB.SignIn (RootLevelUser_invalidAuthLevelDatabase)
- DB.SignIn (RootLevelUser_invalidAuthLevelNamespace)
- DB.SignUp (DatabaseLevelRecordUser)
- DB.Version
- FromConnection (AlternativeCBORImpl_fxamackerCBOR)
- FromConnection (AlternativeCBORImpl_surrealCBOR)
- FromConnection (AlternativeWebSocketLibrary_gws)
- FromConnection (CborUnmarshaler_decOptions_customSmallLimit)
- FromConnection (CborUnmarshaler_decOptions_defaultLimit)
- Insert (Bulk_insert_record)
- Insert (Bulk_insert_relation_workaround_for_rpcv1)
- Insert (Table)
- InsertRelation
- Live
- Live (WithDiff)
- Query
- Query (Bulk_insert_upsert)
- Query (ChangeFeedSchemafull)
- Query (ChangeFeedSchemaless)
- Query (Count_groupAll)
- Query (Count_groupBy)
- Query (Create_none_null_fields_legacy_fxamackercbor)
- Query (Create_none_null_fields_surrealcbor)
- Query (Embedded_struct)
- Query (Issue192)
- Query (Issue291)
- Query (Live)
- Query (None_and_null_handling_allExistingFields)
- Query (None_and_null_handling_explicitFields_ints_legacy_fxamackercbor)
- Query (None_and_null_handling_explicitFields_ints_surrealcbor)
- Query (None_and_null_handling_explicitFields_legacy_fxamackercbor)
- Query (None_and_null_handling_explicitFields_surrealcbor)
- Query (Null_none_customdatetime_roundtrip_legacy_fxamackercbor)
- Query (Null_none_customdatetime_roundtrip_surrealcbor)
- Query (Only)
- Query (Return)
- Query (SelectOnTable)
- Query (TransactionRollback)
- Query (Transaction_issue_177_commit)
- Query (Transaction_issue_177_return_before_commit)
- Query (Transaction_let_return)
- Query (Transaction_return)
- Query (Transaction_throw)
- Relate
- Select
- Select (NonExistentRecord_fxamackercbor)
- Select (NonExistentRecord_surrealcbor)
- Send (Select)
- Session.Begin
- Transaction (ConditionalCommit)
- Transaction (Isolation)
- Update
- Upsert
- Upsert (Rpc_error)
- Upsert (Server_error)
- Upsert (Unmarshal_error_fxamackercbor_legacy_fxamackercbor)
- Upsert (Unmarshal_error_surrealcbor)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Create ¶ added in v0.3.0
func Create[TResult any, TWhat TableOrRecord, S sendable](ctx context.Context, s S, what TWhat, data any) (*TResult, error)
Create creates a new record in the database. S can be *DB, *Session, or *Transaction.
Example ¶
package main
import (
"context"
"fmt"
"time"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "example_create", "persons")
type Person struct {
Name string `json:"name"`
// Note that you must use CustomDateTime instead of time.Time.
CreatedAt models.CustomDateTime `json:"created_at,omitempty"`
UpdatedAt *models.CustomDateTime `json:"updated_at,omitempty"`
}
createdAt, err := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
if err != nil {
panic(err)
}
// Unlike Insert which returns a pointer to the array of inserted records,
// Create returns a pointer to the record itself.
var inserted *Person
inserted, err = surrealdb.Create[Person](
context.Background(),
db,
"persons",
map[string]any{
"name": "First",
"created_at": createdAt,
})
if err != nil {
panic(err)
}
fmt.Printf("Create result: %v\n", *inserted)
// You can throw away the result if you don't need it,
// by specifying an empty struct as the type parameter.
_, err = surrealdb.Create[struct{}](
context.Background(),
db,
"persons",
map[string]any{
"name": "Second",
"created_at": createdAt,
},
)
if err != nil {
panic(err)
}
// You can also create a record by passing a struct directly.
_, err = surrealdb.Create[struct{}](
context.Background(),
db,
"persons",
Person{
Name: "Third",
CreatedAt: models.CustomDateTime{
Time: createdAt,
},
},
)
if err != nil {
panic(err)
}
// You can also receive the result as a map[string]any.
// It should be handy when you don't want to define a struct type,
// in other words, when the schema is not known upfront.
var fourthAsMap *map[string]any
fourthAsMap, err = surrealdb.Create[map[string]any](
context.Background(),
db,
"persons",
map[string]any{
"name": "Fourth",
"created_at": models.CustomDateTime{
Time: createdAt,
},
},
)
if err != nil {
panic(err)
}
if _, ok := (*fourthAsMap)["id"].(models.RecordID); ok {
delete((*fourthAsMap), "id")
}
fmt.Printf("Create result: %v\n", *fourthAsMap)
selected, err := surrealdb.Select[[]Person](
context.Background(),
db,
"persons",
)
if err != nil {
panic(err)
}
for _, person := range *selected {
fmt.Printf("Selected person: %v\n", person)
}
}
Output: Create result: {First {2023-10-01 12:00:00 +0000 UTC} <nil>} Create result: map[created_at:{2023-10-01 12:00:00 +0000 UTC} name:Fourth] Selected person: {First {2023-10-01 12:00:00 +0000 UTC} <nil>} Selected person: {Second {2023-10-01 12:00:00 +0000 UTC} <nil>} Selected person: {Third {2023-10-01 12:00:00 +0000 UTC} <nil>} Selected person: {Fourth {2023-10-01 12:00:00 +0000 UTC} <nil>}
Example (RecordID_withUUID) ¶
package main
import (
"context"
"fmt"
"log"
"github.com/gofrs/uuid"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "query", "person")
type Person struct {
ID *models.RecordID `json:"id,omitempty"`
Text string `json:"text"`
}
// Create a UUIDv7 using gofrs/uuid
u, _ := uuid.NewV7()
u2 := models.UUID{UUID: u}
// Create the record with UUIDv7 ID
record := Person{
Text: "Hello, SurrealDB with UUIDv7!",
}
created, err := surrealdb.Create[Person](
context.Background(),
db,
models.NewRecordID("person", u2),
record,
)
if err != nil {
log.Fatal("Failed to create record:", err)
}
// The UUID in the ID field will vary, so we just check the table and text
fmt.Printf("Created record with table '%s' and text: %s\n", created.ID.Table, created.Text)
}
Output: Created record with table 'person' and text: Hello, SurrealDB with UUIDv7!
Example (Server_unmarshal_error) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "query", "person")
type Person struct {
ID models.RecordID `json:"id,omitempty"`
Name string `json:"name"`
}
_, err := surrealdb.Create[Person](
context.Background(),
db,
"persons",
Person{
Name: "Test",
},
)
if err != nil {
fmt.Printf("Expected error: %v\n", err)
}
}
Output: Expected error: cannot marshal RecordID with empty table or ID: want <table>:<identifier> but got :<nil>
func Delete ¶ added in v0.3.0
func Delete[TResult any, TWhat TableOrRecord, S sendable](ctx context.Context, s S, what TWhat) (*TResult, error)
Delete removes records from the database. S can be *DB, *Session, or *Transaction.
func Insert ¶ added in v0.3.0
func Insert[TResult any, S sendable](ctx context.Context, s S, what models.Table, data any) (*[]TResult, error)
Insert creates records with either specified IDs or generated IDs. S can be *DB, *Session, or *Transaction.
Insert cannot create a relationship. If you want to create a relationship, use InsertRelation if you need to specify the ID of the relationship, or use Relate if you want to create a relationship with a generated ID.
Example (Bulk_insert_record) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "query", "person")
type Person struct {
ID models.RecordID `json:"id"`
}
persons := []Person{
{ID: models.NewRecordID("person", "a")},
{ID: models.NewRecordID("person", "b")},
{ID: models.NewRecordID("person", "c")},
}
var inserted *[]Person
inserted, err := surrealdb.Insert[Person](
context.Background(),
db,
"person",
persons,
)
if err != nil {
panic(err)
}
fmt.Printf("Inserted: %+s\n", *inserted)
selected, err := surrealdb.Select[[]Person](
context.Background(),
db,
"person",
)
if err != nil {
panic(err)
}
for _, person := range *selected {
fmt.Printf("Selected person: %+s\n", person)
}
}
Output: Inserted: [{{person a}} {{person b}} {{person c}}] Selected person: {{person a}} Selected person: {{person b}} Selected person: {{person c}}
Example (Bulk_insert_relation_workaround_for_rpcv1) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "query", "person", "follow")
type Person struct {
ID models.RecordID `json:"id"`
}
type Follow struct {
ID models.RecordID `json:"id"`
In models.RecordID `json:"in"`
Out models.RecordID `json:"out"`
}
persons := []Person{
{ID: models.NewRecordID("person", "a")},
{ID: models.NewRecordID("person", "b")},
{ID: models.NewRecordID("person", "c")},
}
follows := []Follow{
{ID: models.NewRecordID("follow", "person:a:person:b"), In: persons[0].ID, Out: persons[1].ID},
{ID: models.NewRecordID("follow", "person:b:person:c"), In: persons[1].ID, Out: persons[2].ID},
{ID: models.NewRecordID("follow", "person:c:person:a"), In: persons[2].ID, Out: persons[0].ID},
}
var err error
var insertedPersons *[]Person
insertedPersons, err = surrealdb.Insert[Person](
context.Background(),
db,
"person",
persons,
)
if err != nil {
panic(err)
}
fmt.Printf("Inserted: %+s\n", *insertedPersons)
var selectedPersons *[]Person
selectedPersons, err = surrealdb.Select[[]Person](
context.Background(),
db,
"person",
)
if err != nil {
panic(err)
}
for _, person := range *selectedPersons {
fmt.Printf("Selected person: %+s\n", person)
}
/// Once the RPC v2 becomes mature, we could update this SDK to speak
/// the RPC v2 protocol and use the `relation` parameter to insert
/// the follows as relations.
///
/// But as of now, it will fail like SurrealDB responding with:
///
/// There was a problem with the database: The database encountered unreachable logic: /surrealdb/crates/core/src/expr/statements/insert.rs:123: Unknown data clause type in INSERT statement: ContentExpression(Array(Array([Object(Object({"id": Thing(Thing { tb: "follow", id: String("person:a:person:b") }), "in": Thing(Thing { tb: "person", id: String("a") }), "out": Thing(Thing { tb: "person", id: String("b") })})), Object(Object({"id": Thing(Thing { tb: "follow", id: String("person:b:person:c") }), "in": Thing(Thing { tb: "person", id: String("b") }), "out": Thing(Thing { tb: "person", id: String("c") })})), Object(Object({"id": Thing(Thing { tb: "follow", id: String("person:c:person:a") }), "in": Thing(Thing { tb: "person", id: String("c") }), "out": Thing(Thing { tb: "person", id: String("a") })}))])))
///
// var insertedFollows *[]Follow
// insertedFollows, err = surrealdb.Insert[Follow](
// db,
// "follow",
// follows,
// map[string]any{
// // The optional `relation` parameter is a boolean indicating whether the inserted records are relations.
// // See https://surrealdb.com/docs/surrealdb/integration/rpc#parameters-7
// "relation": true,
// },
// )
// if err != nil {
// panic(err)
// }
// fmt.Printf("Inserted: %+s\n", *insertedFollows)
/// You can also use `InsertRelation`.
/// But refer to ExampleInsertRelation for that.
// for _, follow := range follows {
// err = surrealdb.InsertRelation(
// db,
// &surrealdb.Relationship{
// Relation: "follow",
// ID: &follow.ID,
// In: follow.In,
// Out: follow.Out,
// },
// )
// if err != nil {
// panic(err)
// }
// }
// Here, we focus on what you could do the equivalent of
// batch insert relation in RPC v2, using the RPC v1 query RPC.
_, err = surrealdb.Query[any](
context.Background(),
db,
"INSERT RELATION INTO follow $content",
map[string]any{
"content": follows,
},
)
if err != nil {
panic(err)
}
var selectedFollows *[]Follow
selectedFollows, err = surrealdb.Select[[]Follow](
context.Background(),
db,
"follow",
)
if err != nil {
panic(err)
}
for _, follow := range *selectedFollows {
fmt.Printf("Selected follow: %+s\n", follow)
}
type PersonWithFollows struct {
ID models.RecordID `json:"id"`
Follow []models.RecordID `json:"follows"`
}
var followedByA *[]surrealdb.QueryResult[[]PersonWithFollows]
followedByA, err = surrealdb.Query[[]PersonWithFollows](
context.Background(),
db,
"SELECT id, <->follow<->person AS follows FROM person ORDER BY id",
nil,
)
if err != nil {
panic(err)
}
for _, person := range (*followedByA)[0].Result {
fmt.Printf("PersonWithFollows: %+s\n", person)
}
}
Output: Inserted: [{{person a}} {{person b}} {{person c}}] Selected person: {{person a}} Selected person: {{person b}} Selected person: {{person c}} Selected follow: {{follow person:a:person:b} {person a} {person b}} Selected follow: {{follow person:b:person:c} {person b} {person c}} Selected follow: {{follow person:c:person:a} {person c} {person a}} PersonWithFollows: {{person a} [{person c} {person a} {person a} {person b}]} PersonWithFollows: {{person b} [{person a} {person b} {person b} {person c}]} PersonWithFollows: {{person c} [{person b} {person c} {person c} {person a}]}
Example (Table) ¶
package main
import (
"context"
"fmt"
"time"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "query", "persons")
type Person struct {
Name string `json:"name"`
// Note that you must use CustomDateTime instead of time.Time.
CreatedAt models.CustomDateTime `json:"created_at,omitempty"`
UpdatedAt *models.CustomDateTime `json:"updated_at,omitempty"`
}
createdAt, err := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
if err != nil {
panic(err)
}
// Unlike Create which returns a pointer to the record itself,
// Insert returns a pointer to the array of inserted records.
var inserted *[]Person
inserted, err = surrealdb.Insert[Person](
context.Background(),
db,
"persons",
map[string]any{
"name": "First",
"created_at": createdAt,
})
if err != nil {
panic(err)
}
fmt.Printf("Insert result: %v\n", *inserted)
_, err = surrealdb.Insert[struct{}](
context.Background(),
db,
"persons",
map[string]any{
"name": "Second",
"created_at": createdAt,
},
)
if err != nil {
panic(err)
}
_, err = surrealdb.Insert[struct{}](
context.Background(),
db,
"persons",
Person{
Name: "Third",
CreatedAt: models.CustomDateTime{
Time: createdAt,
},
},
)
if err != nil {
panic(err)
}
fourthAsMap, err := surrealdb.Insert[map[string]any](
context.Background(),
db,
"persons",
Person{
Name: "Fourth",
CreatedAt: models.CustomDateTime{
Time: createdAt,
},
},
)
if err != nil {
panic(err)
}
if _, ok := (*fourthAsMap)[0]["id"].(models.RecordID); ok {
delete((*fourthAsMap)[0], "id")
}
fmt.Printf("Insert result: %v\n", *fourthAsMap)
selected, err := surrealdb.Select[[]Person](
context.Background(),
db,
"persons",
)
if err != nil {
panic(err)
}
for _, person := range *selected {
fmt.Printf("Selected person: %v\n", person)
}
}
Output: Insert result: [{First {2023-10-01 12:00:00 +0000 UTC} <nil>}] Insert result: [map[created_at:{2023-10-01 12:00:00 +0000 UTC} name:Fourth]] Selected person: {First {2023-10-01 12:00:00 +0000 UTC} <nil>} Selected person: {Second {2023-10-01 12:00:00 +0000 UTC} <nil>} Selected person: {Third {2023-10-01 12:00:00 +0000 UTC} <nil>} Selected person: {Fourth {2023-10-01 12:00:00 +0000 UTC} <nil>}
func InsertRelation ¶ added in v0.3.0
func InsertRelation[TResult any, S sendable](ctx context.Context, s S, relationship *Relationship) (*TResult, error)
InsertRelation inserts a relation between two records in the database. S can be *DB, *Session, or *Transaction.
It creates a relationship from relationship.In to relationship.Out.
The resulting relationship will have an autogenerated ID in case the Relationship.ID is nil, or the ID specified in the Relationship.ID field.
In case you only care about the returned relationship's ID, use `connection.ResponseID[models.RecordID]` for the TResult type parameter.
Example ¶
package main
import (
"context"
"fmt"
"time"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/connection"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "query", "person", "follow")
type Person struct {
ID models.RecordID `json:"id,omitempty"`
}
type Follow struct {
In *models.RecordID `json:"in,omitempty"`
Out *models.RecordID `json:"out,omitempty"`
Since models.CustomDateTime `json:"since"`
}
first, err := surrealdb.Create[Person](
context.Background(),
db,
"person",
map[string]any{
"id": models.NewRecordID("person", "first"),
})
if err != nil {
panic(err)
}
second, err := surrealdb.Create[Person](
context.Background(),
db,
"person",
map[string]any{
"id": models.NewRecordID("person", "second"),
})
if err != nil {
panic(err)
}
since, err := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
if err != nil {
panic(err)
}
persons, err := surrealdb.Query[[]Person](
context.Background(),
db,
"SELECT * FROM person ORDER BY id.id",
nil,
)
if err != nil {
panic(err)
}
for _, person := range (*persons)[0].Result {
fmt.Printf("Person: %+v\n", person)
}
res, relateErr := surrealdb.InsertRelation[[]connection.ResponseID[models.RecordID]](
context.Background(),
db,
&surrealdb.Relationship{
ID: &models.RecordID{Table: "follow", ID: "first_second"},
In: first.ID,
Out: second.ID,
Relation: "follow",
Data: map[string]any{
"since": models.CustomDateTime{
Time: since,
},
},
},
)
if relateErr != nil {
panic(relateErr)
}
if res == nil {
panic("relation response is nil")
}
if (*res)[0].ID.ID != "first_second" {
panic("relation ID must be set to 'first_second'")
}
//nolint:lll
/// Here's an alternative way to insert a relation using a query.
//
// if res, err := surrealdb.Query[any](
// db,
// "INSERT RELATION INTO follow $content",
// map[string]any{
// "content": map[string]any{
// "id": "first_second",
// "in": first.ID,
// "out": second.ID,
// "since": models.CustomDateTime{Time: since},
// },
// },
// ); err != nil {
// panic(err)
// } else {
// fmt.Printf("Relation: %+v\n", (*res)[0].Result)
// }
// The output will be:
// Relation: [map[id:{Table:follow ID:first_second} in:{Table:person ID:first} out:{Table:person ID:second} since:{Time:2023-10-01 12:00:00 +0000 UTC}]]
type PersonWithFollows struct {
Person
Follows []models.RecordID `json:"follows,omitempty"`
}
selected, err := surrealdb.Query[[]PersonWithFollows](
context.Background(),
db,
"SELECT id, name, ->follow->person AS follows FROM $id",
map[string]any{
"id": first.ID,
},
)
if err != nil {
panic(err)
}
for _, person := range (*selected)[0].Result {
fmt.Printf("PersonWithFollows: %+v\n", person)
}
// Note we can select the relationships themselves because
// RELATE creates a record in the relation table.
follows, err := surrealdb.Query[[]Follow](
context.Background(),
db,
"SELECT * from follow",
nil,
)
if err != nil {
panic(err)
}
for _, follow := range (*follows)[0].Result {
fmt.Printf("Follow: %+v\n", follow)
}
}
Output: Person: {ID:{Table:person ID:first}} Person: {ID:{Table:person ID:second}} PersonWithFollows: {Person:{ID:{Table:person ID:first}} Follows:[{Table:person ID:second}]} Follow: {In:person:first Out:person:second Since:{Time:2023-10-01 12:00:00 +0000 UTC}}
func Kill ¶ added in v0.3.0
Kill terminates a live query and closes the notification channel. S can be *DB or *Session (not *Transaction, as live queries are session-scoped).
func Live ¶ added in v0.3.0
func Live[S liveQueryable](ctx context.Context, s S, table models.Table, diff bool) (*models.UUID, error)
Live starts a live query on a table. S can be *DB or *Session (not *Transaction, as live queries are session-scoped).
Example ¶
ExampleLive demonstrates using the Live RPC method to receive notifications. Live queries without diff return the full record as map[string]any in notification.Result. The notification channel is automatically closed when Kill is called.
package main
import (
"context"
"fmt"
"sort"
"strings"
"time"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/connection"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
// formatRecordResult formats a record result (map[string]any) for testing.
// This is used for regular live query results (without diff) and DELETE operations.
// It handles the id field specially, formatting RecordID as table:⟨UUID⟩.
func formatRecordResult(record map[string]any) string {
keys := make([]string, 0, len(record))
for k := range record {
keys = append(keys, k)
}
sort.Strings(keys)
var parts []string
for _, k := range keys {
val := record[k]
if k == "id" {
recordID := val.(models.RecordID)
parts = append(parts, fmt.Sprintf("id=%s:⟨UUID⟩", recordID.Table))
} else {
parts = append(parts, fmt.Sprintf("%s=%v", k, val))
}
}
return "{" + strings.Join(parts, " ") + "}"
}
func main() {
config := testenv.MustNewConfig("surrealdbexamples", "livequery_rpc", "users")
config.Endpoint = testenv.GetSurrealDBWSURL()
db := config.MustNew()
type User struct {
ID *models.RecordID `json:"id,omitempty"`
Username string `json:"username"`
Email string `json:"email"`
}
ctx := context.Background()
// Create the table first - SurrealDB 3.x requires the table to exist for LIVE SELECT
_, err := surrealdb.Query[any](ctx, db, `DEFINE TABLE users`, nil)
if err != nil {
panic(fmt.Sprintf("Failed to create table: %v", err))
}
live, err := surrealdb.Live(ctx, db, "users", false)
if err != nil {
panic(fmt.Sprintf("Failed to start live query: %v", err))
}
fmt.Println("Started live query")
notifications, err := db.LiveNotifications(live.String())
if err != nil {
panic(fmt.Sprintf("Failed to get live notifications channel: %v", err))
}
received := make(chan struct{})
done := make(chan bool)
go func() {
for notification := range notifications {
// Live queries without diff return the record as map[string]any
record, ok := notification.Result.(map[string]any)
if !ok {
panic(fmt.Sprintf("Expected map[string]any, got %T", notification.Result))
}
fmt.Printf("Received notification - Action: %s, Result: %s\n", notification.Action, formatRecordResult(record))
switch notification.Action {
case connection.CreateAction:
fmt.Println("New user created")
case connection.UpdateAction:
fmt.Println("User updated")
case connection.DeleteAction:
fmt.Println("User deleted")
close(received)
}
}
// Channel was closed
fmt.Println("Notification channel closed")
done <- true
}()
createdUser, err := surrealdb.Create[User](ctx, db, "users", map[string]any{
"username": "alice",
"email": "alice@example.com",
})
if err != nil {
panic(fmt.Sprintf("Failed to create user: %v", err))
}
_, err = surrealdb.Update[User](ctx, db, *createdUser.ID, map[string]any{
"email": "alice.updated@example.com",
})
if err != nil {
panic(fmt.Sprintf("Failed to update user: %v", err))
}
_, err = surrealdb.Delete[User](ctx, db, *createdUser.ID)
if err != nil {
panic(fmt.Sprintf("Failed to delete user: %v", err))
}
// Wait for all expected notifications to be received
select {
case <-received:
// All notifications received
case <-time.After(2 * time.Second):
panic("Timeout waiting for all notifications")
}
fmt.Println("Live query being terminated")
err = surrealdb.Kill(ctx, db, live.String())
if err != nil {
panic(fmt.Sprintf("Failed to kill live query: %v", err))
}
select {
case <-done:
fmt.Println("Goroutine exited after channel closed")
case <-time.After(2 * time.Second):
panic("Timeout: notification channel was not closed after Kill")
}
}
Output: Started live query Received notification - Action: CREATE, Result: {email=alice@example.com id=users:⟨UUID⟩ username=alice} New user created Received notification - Action: UPDATE, Result: {email=alice.updated@example.com id=users:⟨UUID⟩} User updated Received notification - Action: DELETE, Result: {email=alice.updated@example.com id=users:⟨UUID⟩} User deleted Live query being terminated Notification channel closed Goroutine exited after channel closed
Example (WithDiff) ¶
ExampleLive_withDiff demonstrates using live queries with diff enabled. With diff=true, CREATE and UPDATE return diff operations as []any. DELETE behavior differs between versions:
- SurrealDB 2.x: returns map[string]any with just {id: ...}
- SurrealDB 3.x: returns []any with [{op: "remove", path: "", value: {record}}]
The notification channel is automatically closed when Kill is called.
package main
import (
"context"
"fmt"
"sort"
"strings"
"time"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/connection"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
// formatDiffResult formats a diff result ([]any) for testing.
// Each item in the array is a diff operation (map[string]any).
func formatDiffResult(diffs []any) string {
var items []string
for _, item := range diffs {
diffOp, ok := item.(map[string]any)
if !ok {
panic(fmt.Sprintf("Expected diff operation to be map[string]any, got %T", item))
}
items = append(items, formatDiffOperation(diffOp))
}
return "[" + strings.Join(items, " ") + "]"
}
// formatPatchDataMap formats a map representation of PatchData.
// This is the data contained in the "value" field of a diff operation.
func formatPatchDataMap(data map[string]any) string {
keys := make([]string, 0, len(data))
for k := range data {
keys = append(keys, k)
}
sort.Strings(keys)
var parts []string
for _, k := range keys {
val := data[k]
if k == "id" {
recordID := val.(models.RecordID)
parts = append(parts, fmt.Sprintf("id=%s:⟨UUID⟩", recordID.Table))
} else {
parts = append(parts, fmt.Sprintf("%s=%v", k, val))
}
}
return "{" + strings.Join(parts, " ") + "}"
}
// formatDiffOperation formats a single diff operation.
// A diff operation contains fields like "op", "path", and optionally "value".
func formatDiffOperation(op map[string]any) string {
keys := make([]string, 0, len(op))
for k := range op {
keys = append(keys, k)
}
sort.Strings(keys)
var parts []string
for _, k := range keys {
val := op[k]
switch k {
case "value":
if patchData, ok := val.(map[string]any); ok {
parts = append(parts, fmt.Sprintf("value=%s", formatPatchDataMap(patchData)))
} else {
parts = append(parts, fmt.Sprintf("value=%v", val))
}
case "path":
pathVal := fmt.Sprintf("%v", val)
if pathVal == "" {
pathVal = "/"
}
parts = append(parts, fmt.Sprintf("path=%s", pathVal))
default:
parts = append(parts, fmt.Sprintf("%s=%v", k, val))
}
}
return "{" + strings.Join(parts, " ") + "}"
}
func main() {
config := testenv.MustNewConfig("surrealdbexamples", "livequery_diff", "inventory")
config.Endpoint = testenv.GetSurrealDBWSURL()
db := config.MustNew()
type Item struct {
ID *models.RecordID `json:"id,omitempty"`
Name string `json:"name"`
Quantity int `json:"quantity"`
}
ctx := context.Background()
// Create the table first - SurrealDB 3.x requires the table to exist for LIVE SELECT
_, err := surrealdb.Query[any](ctx, db, `DEFINE TABLE inventory`, nil)
if err != nil {
panic(fmt.Sprintf("Failed to create table: %v", err))
}
live, err := surrealdb.Live(ctx, db, "inventory", true)
if err != nil {
panic(fmt.Sprintf("Failed to start live query with diff: %v", err))
}
fmt.Println("Started live query with diff enabled")
notifications, err := db.LiveNotifications(live.String())
if err != nil {
panic(fmt.Sprintf("Failed to get live notifications channel: %v", err))
}
received := make(chan struct{})
done := make(chan bool)
go func() {
var i int
for notification := range notifications {
var resultStr string
// With diff=true:
// - SurrealDB 2.x: CREATE/UPDATE return []any diffs, DELETE returns map[string]any with {id: ...}
// - SurrealDB 3.x: All actions return []any diffs
switch result := notification.Result.(type) {
case []any:
// SurrealDB 3.x format (all actions) or 2.x format (CREATE/UPDATE only)
if notification.Action == connection.DeleteAction {
// 3.x DELETE with diff=true returns [{op: "remove" or "replace", path: "", value: {record}}]
// Note: The exact format may vary between 3.x beta versions:
// - Some versions include value with the deleted record
// - Some versions have value as nil
if len(result) != 1 {
panic(fmt.Sprintf("SurrealDB 3.x DELETE: expected 1 diff operation, got %d", len(result)))
}
diffOp, ok := result[0].(map[string]any)
if !ok {
panic(fmt.Sprintf("SurrealDB 3.x DELETE: expected diff operation to be map[string]any, got %T", result[0]))
}
op := diffOp["op"]
if op != "remove" && op != "replace" {
panic(fmt.Sprintf("SurrealDB 3.x DELETE: expected op='remove' or 'replace', got %v", op))
}
// 3.x uses "" for root path
if diffOp["path"] != "" {
panic(fmt.Sprintf("SurrealDB 3.x DELETE: expected path='', got %v", diffOp["path"]))
}
// Value may be present with deleted record data, or nil in some beta versions
if value, ok := diffOp["value"].(map[string]any); ok && value != nil {
if value["name"] != "Screwdriver" {
panic(fmt.Sprintf("SurrealDB 3.x DELETE: expected value.name='Screwdriver', got %v", value["name"]))
}
}
resultStr = "{deleted}"
} else {
resultStr = formatDiffResult(result)
}
case map[string]any:
// SurrealDB 2.x DELETE with diff=true returns just {id: ...}
if notification.Action != connection.DeleteAction {
panic(fmt.Sprintf("Expected []any for %s result in 2.x, got map[string]any", notification.Action))
}
// Validate that id is present (name/quantity fields are not included in DELETE notification with diff=true)
if _, hasID := result["id"]; !hasID {
panic("SurrealDB 2.x DELETE: expected result to have 'id' field")
}
resultStr = "{deleted}"
default:
panic(fmt.Sprintf("Unexpected result type %T for action %s", notification.Result, notification.Action))
}
i++
fmt.Printf("Action: %s, Result: %s\n", notification.Action, resultStr)
if i >= 3 {
close(received)
}
}
// Channel was closed
fmt.Println("Notification channel closed")
done <- true
}()
item, err := surrealdb.Create[Item](ctx, db, "inventory", map[string]any{
"name": "Screwdriver",
"quantity": 50,
})
if err != nil {
panic(fmt.Sprintf("Failed to create item: %v", err))
}
_, err = surrealdb.Update[Item](ctx, db, *item.ID, map[string]any{
"quantity": 45,
})
if err != nil {
panic(fmt.Sprintf("Failed to update item: %v", err))
}
_, err = surrealdb.Delete[Item](ctx, db, *item.ID)
if err != nil {
panic(fmt.Sprintf("Failed to delete item: %v", err))
}
// Wait for all expected notifications to be received
select {
case <-received:
// All notifications received
case <-time.After(2 * time.Second):
panic("Timeout waiting for all notifications")
}
fmt.Println("Live query with diff being terminated")
err = surrealdb.Kill(ctx, db, live.String())
if err != nil {
panic(fmt.Sprintf("Failed to kill live query: %v", err))
}
select {
case <-done:
fmt.Println("Goroutine exited after channel closed")
case <-time.After(2 * time.Second):
panic("Timeout: notification channel was not closed after Kill")
}
}
Output: Started live query with diff enabled Action: CREATE, Result: [{op=replace path=/ value={id=inventory:⟨UUID⟩ name=Screwdriver quantity=50}}] Action: UPDATE, Result: [{op=remove path=/name} {op=replace path=/quantity value=45}] Action: DELETE, Result: {deleted} Live query with diff being terminated Notification channel closed Goroutine exited after channel closed
func Merge ¶ added in v0.3.0
func Merge[TResult any, TWhat TableOrRecord, S sendable](ctx context.Context, s S, what TWhat, data any) (*TResult, error)
Merge merges data into a record in the database like a PATCH request. S can be *DB, *Session, or *Transaction.
func Patch ¶ added in v0.2.0
func Patch[TWhat TableOrRecord, S sendable](ctx context.Context, s S, what TWhat, patches []PatchData) (*[]PatchData, error)
Patch applies patches to records in the database. S can be *DB, *Session, or *Transaction.
func Query ¶ added in v0.3.0
func Query[TResult any, S sendable](ctx context.Context, s S, sql string, vars map[string]any) (*[]QueryResult[TResult], error)
Query executes a query against the SurrealDB database.
S can be *DB, *Session, or *Transaction.
Query supports:
- Full SurrealQL syntax including transactions
- Parameterized queries for security
- Typed results with generics
- Multiple statements in a single call
It takes a SurrealQL query to be executed, and the variables to parameterize the query, and returns a slice of QueryResult whose type parameter is the result type.
Examples ¶
Execute a SurrealQL query with typed results:
results, err := surrealdb.Query[[]Person](
context.Background(),
db,
"SELECT * FROM persons WHERE age > $minAge",
map[string]any{
"minAge": 18,
},
)
You can also use Query for transactions with variables:
transactionResults, err := surrealdb.Query[[]any](
context.Background(),
db,
`
BEGIN TRANSACTION;
CREATE person:$johnId SET name = $johnName, age = $johnAge;
CREATE person:$janeId SET name = $janeName, age = $janeAge;
COMMIT TRANSACTION;
`,
map[string]any{
"johnId": "john",
"johnName": "John",
"johnAge": 30,
"janeId": "jane",
"janeName": "Jane",
"janeAge": 25,
},
)
Or use a single CREATE with content variable:
createResult, err := surrealdb.Query[[]Person](
context.Background(),
db,
"CREATE person:$id CONTENT $content",
map[string]any{
"id": "alice",
"content": map[string]any{
"name": "Alice",
"age": 28,
"city": "New York",
},
},
)
Handling errors ¶
If the query fails, the returned error will be a `joinError` created by the errors.Join function, which contains all the errors that occurred during the query execution. The caller can check the Error field of each QueryResult to see if the query failed, or check the returned error from the Query function to see if the query failed.
If the caller wants to handle the query errors, if any, it can check the Error field of each QueryResult, or call:
errors.Is(err, &surrealdb.QueryError{})
on the returned error to see if it is (or contains) a QueryError.
Query errors are non-retriable ¶
If the error is a QueryError, the caller should NOT retry the query, because the query is already executed and the error is not recoverable, and often times the error is caused by a bug in the query itself.
When can you safely retry the query when this function returns an error? ¶
Generally speaking, automatic retries make sense only when the error is transient, such as a network error, a timeout, or a server error that is not related to the query itself. In such cases, the caller can retry the query by calling the Query function again.
For this function, the caller may retry when the error is:
- RPCError: because we should get a RPC error only when the RPC failed due to anything other than the query error
- constants.ErrTimeout: This means we send the HTTP request or a WebSocket message to SurrealDB in timely manner, which is often due to temporary network issues or server overload.
What non-retriable errors will Query return? ¶
However, if the error is any of the following, the caller should NOT retry the query:
- QueryError: This means the query failed due to a syntax error, a type error, or a logical error in the query itself.
- Unmarshal error: This means the response from the server could not be unmarshaled into the expected type, which is often due to a bug in the code or a mismatch between the expected type and the actual response type.
- Marshal error: This means the request could not be marshaled using CBOR, which is often due to a bug in the code that tries to send something that cannot be marshaled or understood by SurrealDB, such as a struct with unsupported types.
- Anything else: It's just safer to not retry when we aren't sure if the error is whether transient or permanent.
RPCError is retriable only for Query ¶
Note that RPCError is retriable only for the Query RPC method, because in other cases, the RPCError may also indicate a query error. For example, if you tried to insert a duplicate record using the Insert RPC, you may get an RPCError saying so, which is not retriable.
If you tried to insert the same duplicate record using the Query RPC method with `INSERT` statement, you may get no RPCError, but a QueryError saying so, enabling you to easily diferentiate between retriable and non-retriable errors.
Example ¶
package main
import (
"context"
"fmt"
"time"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "query", "persons")
type NestedStruct struct {
City string `json:"city"`
}
type Person struct {
ID *models.RecordID `json:"id,omitempty"`
Name string `json:"name"`
NestedMap map[string]any `json:"nested_map,omitempty"`
NestedStruct `json:"nested_struct,omitempty"`
CreatedAt models.CustomDateTime `json:"created_at,omitempty"`
UpdatedAt *models.CustomDateTime `json:"updated_at,omitempty"`
}
createdAt, err := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
if err != nil {
panic(err)
}
recordID := models.NewRecordID("persons", "yusuke")
createQueryResults, err := surrealdb.Query[[]Person](
context.Background(),
db,
`CREATE $record_id CONTENT $content`,
map[string]any{
"record_id": recordID,
"content": map[string]any{
"name": "Yusuke",
"nested_struct": NestedStruct{
City: "Tokyo",
},
"created_at": models.CustomDateTime{
Time: createdAt,
},
},
})
if err != nil {
panic(err)
}
fmt.Printf("Number of query results: %d\n", len(*createQueryResults))
fmt.Printf("First query result's status: %+s\n", (*createQueryResults)[0].Status)
fmt.Printf("Persons contained in the first query result: %+v\n", (*createQueryResults)[0].Result)
}
Output: Number of query results: 1 First query result's status: OK Persons contained in the first query result: [{ID:persons:yusuke Name:Yusuke NestedMap:map[] NestedStruct:{City:Tokyo} CreatedAt:{Time:2023-10-01 12:00:00 +0000 UTC} UpdatedAt:<nil>}]
Example (Bulk_insert_upsert) ¶
This example demonstrates how you can batch insert and upsert records, with specifying RETURN NONE to avoid unnecessary data transfer and decoding.
package main
import (
"context"
"fmt"
"strings"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "query", "persons")
/// You can make it a schemaful table by defining fields like this:
//
// _, err := surrealdb.Query[any](
// db,
// `DEFINE TABLE persons SCHEMAFULL;
// DEFINE FIELD note ON persons TYPE string;
// DEFINE FIELD num ON persons TYPE int;
// DEFINE FIELD loc ON persons TYPE geometry<point>;
// `,
// nil,
// )
// if err != nil {
// panic(err)
// }
//
/// If you do that, ensure that fields do not have `omitempty` json tags!
///
/// Why?
/// Our cbor library reuses `json` tags for CBOR encoding/decoding,
/// and `omitempty` skips the encoding of the field if it is empty.
///
/// For example, if you define an `int` field with `omitempty` tag,
/// a value of `0` will not be encoded, resulting in an query error due:
/// Found NONE for field `num`, with record `persons:p0`, but expected a int
type Person struct {
ID *models.RecordID `json:"id"`
Note string `json:"note"`
// As writte nabove whether it is `json:"num,omitempty"` or `json:"num"` is important,.
// depending on what you want to achieve.
Num int `json:"num"`
Loc models.GeometryPoint `json:"loc"`
}
nthPerson := func(i int) Person {
return Person{
ID: &models.RecordID{Table: "persons", ID: fmt.Sprintf("p%d", i)},
Note: fmt.Sprintf("inserted%d", i),
Num: i,
Loc: models.GeometryPoint{
Longitude: 12.34 + float64(i),
Latitude: 45.65 + float64(i),
},
}
}
var persons []Person
for i := 0; i < 2; i++ {
persons = append(persons, nthPerson(i))
}
insert, err := surrealdb.Query[any](
context.Background(),
db,
`INSERT INTO persons $persons RETURN NONE`,
map[string]any{
"persons": persons,
})
if err != nil {
panic(err)
}
fmt.Println("# INSERT INTO")
fmt.Printf("Count : %d\n", len(*insert))
fmt.Printf("Status : %+s\n", (*insert)[0].Status)
fmt.Printf("Result : %+v\n", (*insert)[0].Result)
select1, err := surrealdb.Query[[]Person](
context.Background(),
db,
`SELECT * FROM persons ORDER BY id.id`,
nil)
if err != nil {
panic(err)
}
fmt.Printf("Selected: %+v\n", (*select1)[0].Result)
persons = append(persons, nthPerson(2))
insertIgnore, err := surrealdb.Query[any](
context.Background(),
db,
`INSERT IGNORE INTO persons $persons RETURN NONE`,
map[string]any{
"persons": persons,
})
if err != nil {
panic(err)
}
fmt.Println("# INSERT IGNORE INTO")
fmt.Printf("Count : %d\n", len(*insertIgnore))
fmt.Printf("Status : %+s\n", (*insertIgnore)[0].Status)
fmt.Printf("Result : %+v\n", (*insertIgnore)[0].Result)
select2, err := surrealdb.Query[[]Person](
context.Background(),
db,
`SELECT * FROM persons ORDER BY id.id`,
nil)
if err != nil {
panic(err)
}
fmt.Printf("Selected: %+v\n", (*select2)[0].Result)
for i := 0; i < 3; i++ {
persons[i].Note = fmt.Sprintf("updated%d", i)
}
persons = append(persons, nthPerson(3))
var upsertQueries []string
vars := make(map[string]any)
for i, p := range persons {
upsertQueries = append(upsertQueries,
fmt.Sprintf(`UPSERT persons CONTENT $content%d RETURN NONE`, i),
)
vars[fmt.Sprintf("content%d", i)] = p
}
upsert, err := surrealdb.Query[any](
context.Background(),
db,
strings.Join(upsertQueries, ";"),
vars,
)
if err != nil {
panic(err)
}
fmt.Println("# UPSERT CONTENT")
fmt.Printf("Count : %d\n", len(*upsert))
fmt.Printf("Status : %+s\n", (*upsert)[0].Status)
fmt.Printf("Result : %+v\n", (*upsert)[0].Result)
select3, err := surrealdb.Query[[]Person](
context.Background(),
db,
`SELECT * FROM persons ORDER BY id.id`,
nil)
if err != nil {
panic(err)
}
fmt.Printf("Selected: %+v\n", (*select3)[0].Result)
}
Output: # INSERT INTO Count : 1 Status : OK Result : [] Selected: [{ID:persons:p0 Note:inserted0 Num:0 Loc:{Longitude:12.34 Latitude:45.65}} {ID:persons:p1 Note:inserted1 Num:1 Loc:{Longitude:13.34 Latitude:46.65}}] # INSERT IGNORE INTO Count : 1 Status : OK Result : [] Selected: [{ID:persons:p0 Note:inserted0 Num:0 Loc:{Longitude:12.34 Latitude:45.65}} {ID:persons:p1 Note:inserted1 Num:1 Loc:{Longitude:13.34 Latitude:46.65}} {ID:persons:p2 Note:inserted2 Num:2 Loc:{Longitude:14.34 Latitude:47.65}}] # UPSERT CONTENT Count : 4 Status : OK Result : [] Selected: [{ID:persons:p0 Note:updated0 Num:0 Loc:{Longitude:12.34 Latitude:45.65}} {ID:persons:p1 Note:updated1 Num:1 Loc:{Longitude:13.34 Latitude:46.65}} {ID:persons:p2 Note:updated2 Num:2 Loc:{Longitude:14.34 Latitude:47.65}} {ID:persons:p3 Note:inserted3 Num:3 Loc:{Longitude:15.34 Latitude:48.65}}]
Example (ChangeFeedSchemafull) ¶
ExampleQuery_changeFeedSchemafull demonstrates how to use Change Feeds with a schemaful table in SurrealDB. This example shows how to define a table with schema enforcement, define required fields, and track changes made to records that must conform to the schema.
// Connect to database
db := testenv.MustNew("surrealdbexamples", "changefeed_schemaful", "inventory")
ctx := context.Background()
// Define a schemaful table with change feed enabled
// SCHEMAFULL enforces that all records must have the defined fields
_, err := surrealdb.Query[any](ctx, db, `
DEFINE TABLE inventory SCHEMAFULL CHANGEFEED 1h;
`, nil)
if err != nil {
panic(err)
}
// Note that DEFINE FIELD statements are not tracked by the change feed,
// although DEFINE TABLE statements are tracked.
//
// In schemaful tables:
// - TYPE string means the field is required (cannot be none or null)
// - TYPE option<string> means the field can be none
// - TYPE string | null means the field can be null
//
// In change feeds:
// - `null` fields appear as `null`
// - `none` fields do not appear at all
_, err = surrealdb.Query[any](ctx, db, `
DEFINE FIELD sku ON TABLE inventory TYPE string;
DEFINE FIELD name ON TABLE inventory TYPE string;
DEFINE FIELD price ON TABLE inventory TYPE number ASSERT $value >= 0;
DEFINE FIELD quantity ON TABLE inventory TYPE int ASSERT $value >= 0;
DEFINE FIELD active ON TABLE inventory TYPE bool DEFAULT true;
DEFINE FIELD notes ON TABLE inventory TYPE option<string>;
`, nil)
if err != nil {
panic(err)
}
// Make some changes to generate change feed entries
// All operations must comply with the schema
_, err = surrealdb.Query[any](ctx, db, `
CREATE inventory:item1 SET
sku = "SKU001",
name = "Wireless Mouse",
price = 29.99,
quantity = 100,
active = true,
notes = "Best seller";
UPDATE inventory:item1 SET quantity = 95;
CREATE inventory:item2 SET
sku = "SKU002",
name = "USB Cable",
price = 9.99,
quantity = 250,
active = true;
-- notes is optional, so we can omit it when
-- creating and updating records
UPDATE inventory:item2 SET active = false;
DELETE inventory:item2;
`, nil)
if err != nil {
panic(err)
}
type ChangeDefineTable struct {
Name string `json:"name"`
}
type ChangeDefineField struct {
Name string `json:"name"`
What string `json:"what"`
Table string `json:"table"`
}
// Change represents a change to a table in the database.
type Change struct {
// DefineTable represents the definition of a new table in the database.
DefineTable *ChangeDefineTable `json:"define_table"`
// DefineField represents the definition of a new field in the table.
DefineField *ChangeDefineField `json:"define_field"`
// Update represents an update to a table in the database.
// Note that in schemaful tables, all defined fields are present.
Update map[string]any `json:"update"`
// Delete represents a deletion from a table in the database.
Delete map[string]any `json:"delete"`
}
// ChangeSet represents a set of changes in the database.
type ChangeSet struct {
// Versionstamp is a unique identifier for the change set.
Versionstamp uint64 `json:"versionstamp"`
// Changes is a list of changes made in the database.
Changes []Change `json:"changes"`
}
result, err := surrealdb.Query[[]ChangeSet](ctx, db, "SHOW CHANGES FOR TABLE inventory SINCE 0", nil)
if err != nil {
panic(err)
}
showChangesResult := (*result)[0].Result
// Verify versionstamps are monotonic
monotonic := true
for i := 1; i < len(showChangesResult); i++ {
if showChangesResult[i].Versionstamp <= showChangesResult[i-1].Versionstamp {
monotonic = false
break
}
}
fmt.Printf("Versionstamps are monotonic: %v\n", monotonic)
// Count different types of changes
var defineTableCount, defineFieldCount, updateCount, deleteCount int
for _, changeSet := range showChangesResult {
for _, change := range changeSet.Changes {
if change.DefineTable != nil {
defineTableCount++
}
if change.DefineField != nil {
defineFieldCount++
}
if change.Update != nil {
updateCount++
}
if change.Delete != nil {
deleteCount++
}
}
}
if defineTableCount > 0 && updateCount > 0 && deleteCount > 0 {
fmt.Println("Found change entries: table definitions, updates, and deletes")
}
if defineFieldCount > 0 {
fmt.Printf("Field definitions tracked: %d\n", defineFieldCount)
}
// Show the last few changes with actual data
// Find the last table definition to show only recent changes
lastTableDefIndex := -1
for i := len(showChangesResult) - 1; i >= 0; i-- {
for _, change := range showChangesResult[i].Changes {
if change.DefineTable != nil {
lastTableDefIndex = i
break
}
}
if lastTableDefIndex >= 0 {
break
}
}
if lastTableDefIndex >= 0 {
// Show changes from the last table definition onwards
recentChanges := showChangesResult[lastTableDefIndex:]
// Limit to last 10 changes for readability
startIndex := 0
if len(recentChanges) > 10 {
startIndex = len(recentChanges) - 10
}
fmt.Printf("Last %d changes:\n", len(recentChanges[startIndex:]))
for _, changeSet := range recentChanges[startIndex:] {
for _, change := range changeSet.Changes {
if change.DefineTable != nil {
fmt.Printf(" DefineTable: %s\n", change.DefineTable.Name)
}
if change.DefineField != nil {
fmt.Printf(" DefineField: %s on %s\n", change.DefineField.Name, change.DefineField.Table)
}
if change.Update != nil {
// Extract fields from the update (all fields present in schemaful table)
if id, ok := change.Update["id"]; ok {
sku := change.Update["sku"]
name := change.Update["name"]
price := change.Update["price"]
quantity := change.Update["quantity"]
active := change.Update["active"]
notes, ok := change.Update["notes"]
var notesStr string
if !ok {
notesStr = "<none>"
} else if notes == nil {
notesStr = "<null>"
} else {
notesStr = fmt.Sprintf("%v", notes)
}
fmt.Printf(" Update: id=%v, sku=%v, name=%v, price=%v, quantity=%v, active=%v, notes=%v\n",
id, sku, name, price, quantity, active, notesStr)
} else {
fmt.Printf(" Update: %v\n", change.Update)
}
}
if change.Delete != nil {
// Extract id from the delete
if id, ok := change.Delete["id"]; ok {
fmt.Printf(" Delete: id=%v\n", id)
} else {
fmt.Printf(" Delete: %v\n", change.Delete)
}
}
}
}
}
Output: Versionstamps are monotonic: true Found change entries: table definitions, updates, and deletes Last 6 changes: DefineTable: inventory Update: id={inventory item1}, sku=SKU001, name=Wireless Mouse, price=29.99, quantity=100, active=true, notes=Best seller Update: id={inventory item1}, sku=SKU001, name=Wireless Mouse, price=29.99, quantity=95, active=true, notes=Best seller Update: id={inventory item2}, sku=SKU002, name=USB Cable, price=9.99, quantity=250, active=true, notes=<none> Update: id={inventory item2}, sku=SKU002, name=USB Cable, price=9.99, quantity=250, active=false, notes=<none> Delete: id={inventory item2}
Example (ChangeFeedSchemaless) ¶
ExampleQuery_changeFeedSchemaless demonstrates how to use Change Feeds in SurrealDB to track changes made to database records.
// Connect to database
db := testenv.MustNew("surrealdbexamples", "changefeed", "product")
ctx := context.Background()
// Enable change feed on the database
_, err := surrealdb.Query[any](ctx, db, "DEFINE TABLE product CHANGEFEED 1h", nil)
if err != nil {
panic(err)
}
// Make some changes to generate change feed entries
_, err = surrealdb.Query[any](ctx, db, `
CREATE product:1 SET name = "Laptop", price = 999.99;
UPDATE product:1 SET price = 899.99;
CREATE product:2 SET name = "Mouse", price = 29.99;
DELETE product:2;
`, nil)
if err != nil {
panic(err)
}
type ChangeDefineTable struct {
Name string `json:"name"`
}
// Change represents a change to a table in the database.
type Change struct {
// DefineTable represents the definition of a new table in the database.
// It has Name and nothing else.
DefineTable *ChangeDefineTable `json:"define_table"`
// Update represents an update to a table in the database.
// Note that this may represent a new record being created.
// In case of an update, the "id" field must be present,
// and all other fields including unchanged fields must be included.
Update map[string]any `json:"update"`
// Delete represents a deletion from a table in the database.
Delete map[string]any `json:"delete"`
}
// ChangeSet represents a set of changes in the database.
type ChangeSet struct {
// Versionstamp is a unique identifier for the change set.
// It is unique per database.
Versionstamp uint64 `json:"versionstamp"`
// Changes is a list of changes made in the database.
// It may contain one or more table changes,
// each represented as a map of field names to their new values.
Changes []Change `json:"changes"`
}
result, err := surrealdb.Query[[]ChangeSet](ctx, db, "SHOW CHANGES FOR TABLE product SINCE 0", nil)
if err != nil {
panic(err)
}
showChangesResult := (*result)[0].Result
// Verify versionstamps are monotonic
monotonic := true
for i := 1; i < len(showChangesResult); i++ {
if showChangesResult[i].Versionstamp <= showChangesResult[i-1].Versionstamp {
monotonic = false
break
}
}
fmt.Printf("Versionstamps are monotonic: %v\n", monotonic)
// Count different types of changes
var defineCount, updateCount, deleteCount int
for _, changeSet := range showChangesResult {
for _, change := range changeSet.Changes {
if change.DefineTable != nil {
defineCount++
}
if change.Update != nil {
updateCount++
}
if change.Delete != nil {
deleteCount++
}
}
}
if defineCount > 0 && updateCount > 0 && deleteCount > 0 {
fmt.Println("Found change entries: defines, updates, and deletes")
}
// Show the pattern of the last few changes with actual data
if len(showChangesResult) >= 5 {
lastFive := showChangesResult[len(showChangesResult)-5:]
fmt.Println("Last 5 changes:")
for _, changeSet := range lastFive {
for _, change := range changeSet.Changes {
if change.DefineTable != nil {
fmt.Printf(" DefineTable: %s\n", change.DefineTable.Name)
}
if change.Update != nil {
// Extract key fields from the update
if id, ok := change.Update["id"]; ok {
if name, hasName := change.Update["name"]; hasName {
if price, hasPrice := change.Update["price"]; hasPrice {
fmt.Printf(" Update: id=%v, name=%v, price=%v\n", id, name, price)
} else {
fmt.Printf(" Update: id=%v, name=%v\n", id, name)
}
} else if price, hasPrice := change.Update["price"]; hasPrice {
fmt.Printf(" Update: id=%v, price=%v\n", id, price)
} else {
fmt.Printf(" Update: id=%v\n", id)
}
} else {
fmt.Printf(" Update: %v\n", change.Update)
}
}
if change.Delete != nil {
// Extract id from the delete
if id, ok := change.Delete["id"]; ok {
fmt.Printf(" Delete: id=%v\n", id)
} else {
fmt.Printf(" Delete: %v\n", change.Delete)
}
}
}
}
}
Output: Versionstamps are monotonic: true Found change entries: defines, updates, and deletes Last 5 changes: DefineTable: product Update: id={product 1}, name=Laptop, price=999.99 Update: id={product 1}, name=Laptop, price=899.99 Update: id={product 2}, name=Mouse, price=29.99 Delete: id={product 2}
Example (Count_groupAll) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "querytest", "product")
type Product struct {
ID models.RecordID `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Category string `json:"category,omitempty"`
}
a := Product{
ID: models.NewRecordID("product", "a"),
Name: "A",
Category: "One",
}
b := Product{
ID: models.NewRecordID("product", "b"),
Name: "B",
Category: "One",
}
c := Product{
ID: models.NewRecordID("product", "c"),
Name: "C",
Category: "Two",
}
for _, p := range []Product{a, b, c} {
created, err := surrealdb.Create[Product](
context.Background(),
db,
p.ID,
p,
)
if err != nil {
panic(err)
}
fmt.Printf("Created product: %+v\n", *created)
}
type CountResult struct {
C int `json:"c,omitempty"`
}
res, err := surrealdb.Query[[]CountResult](
context.Background(),
db,
"SELECT COUNT() as c FROM product GROUP ALL",
map[string]any{},
)
if err != nil {
panic(err)
}
countResult := (*res)[0].Result[0]
fmt.Printf("Count: %d\n", countResult.C)
}
Output: Created product: {ID:{Table:product ID:a} Name:A Category:One} Created product: {ID:{Table:product ID:b} Name:B Category:One} Created product: {ID:{Table:product ID:c} Name:C Category:Two} Count: 3
Example (Count_groupBy) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "querytest", "product")
type Product struct {
ID models.RecordID `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Category string `json:"category,omitempty"`
}
a := Product{
ID: models.NewRecordID("product", "a"),
Name: "A",
Category: "One",
}
b := Product{
ID: models.NewRecordID("product", "b"),
Name: "B",
Category: "One",
}
c := Product{
ID: models.NewRecordID("product", "c"),
Name: "C",
Category: "Two",
}
for _, p := range []Product{a, b, c} {
created, err := surrealdb.Create[Product](
context.Background(),
db,
p.ID,
p,
)
if err != nil {
panic(err)
}
fmt.Printf("Created product: %+v\n", *created)
}
type ProductCategorySummary struct {
Category string `json:"category,omitempty"`
Count int `json:"count,omitempty"`
}
res, err := surrealdb.Query[[]ProductCategorySummary](
context.Background(),
db,
// Note that there's no `COUNT(*)` in SurrealDB.
// When counting, you use either `COUNT()` or `COUNT(field)`,
// with either GROUP BY or GROUP ALL.
"SELECT category, COUNT() AS count FROM product GROUP BY category",
map[string]any{},
)
if err != nil {
panic(err)
}
summaries := (*res)[0].Result
for i, summary := range summaries {
fmt.Printf("Category %d: %s, Count: %d\n", i+1, summary.Category, summary.Count)
}
}
Output: Created product: {ID:{Table:product ID:a} Name:A Category:One} Created product: {ID:{Table:product ID:b} Name:B Category:One} Created product: {ID:{Table:product ID:c} Name:C Category:Two} Category 1: One, Count: 2 Category 2: Two, Count: 1
Example (Create_none_null_fields_legacy_fxamackercbor) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
const (
NullString = "null"
NoneString = "none"
)
func main() {
c := testenv.MustNewConfig("example", "query", "t")
c.CBORImpl = testenv.CBORImplFxamackerCBOR
db := c.MustNew()
_, err := surrealdb.Query[[]any](
context.Background(),
db,
`DEFINE TABLE t SCHEMAFULL;
DEFINE FIELD nullable ON t TYPE bool | null;
DEFINE FIELD option ON t TYPE option<bool>;
CREATE t:a SET nullable = $nil;
CREATE t:b SET nullable = true;
CREATE t:c SET nullable = true, option = false;
CREATE t:d SET nullable = false, option = true;
CREATE t:e SET nullable = false;
CREATE t:f SET nullable = false, option = $none;
`,
map[string]any{
"id": models.NewRecordID("t", 1),
"nil": nil,
"none": models.None,
})
if err != nil {
panic(err)
}
fmt.Println("Created records with none and null fields successfully")
type T struct {
ID *models.RecordID `json:"id,omitempty"`
Nulabble *bool `json:"nullable"`
Option *bool `json:"option"`
}
selected, err := surrealdb.Query[[]T](
context.Background(),
db,
`SELECT id, nullable, option FROM t ORDER BY id`,
nil,
)
if err != nil {
panic(err)
}
for _, t := range (*selected)[0].Result {
id := t.ID
var nullable string
if t.Nulabble == nil {
nullable = NullString
} else {
nullable = fmt.Sprintf("%t", *t.Nulabble)
}
var option string
if t.Option == nil {
option = NoneString
} else {
option = fmt.Sprintf("%t", *t.Option)
}
fmt.Printf("ID: %s, Nullable: %s, Option: %s\n", id, nullable, option)
}
}
Output: Created records with none and null fields successfully ID: t:a, Nullable: null, Option: false ID: t:b, Nullable: true, Option: false ID: t:c, Nullable: true, Option: false ID: t:d, Nullable: false, Option: true ID: t:e, Nullable: false, Option: false ID: t:f, Nullable: false, Option: false
Example (Create_none_null_fields_surrealcbor) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
const NilString = "<nil>"
func main() {
c := testenv.MustNewConfig("example", "query", "t")
c.CBORImpl = testenv.CBORImplSurrealCBOR
db := c.MustNew()
_, err := surrealdb.Query[[]any](
context.Background(),
db,
`DEFINE TABLE t SCHEMAFULL;
DEFINE FIELD nullable ON t TYPE bool | null;
DEFINE FIELD option ON t TYPE option<bool>;
CREATE t:a SET nullable = $nil;
CREATE t:b SET nullable = true;
CREATE t:c SET nullable = true, option = false;
CREATE t:d SET nullable = false, option = true;
CREATE t:e SET nullable = false;
CREATE t:f SET nullable = false, option = $none;
`,
map[string]any{
"id": models.NewRecordID("t", 1),
"nil": nil,
"none": models.None,
})
if err != nil {
panic(err)
}
fmt.Println("Created records with none and null fields successfully")
type T struct {
ID *models.RecordID `json:"id,omitempty"`
Nulabble *bool `json:"nullable"`
Option *bool `json:"option"`
}
selected, err := surrealdb.Query[[]T](
context.Background(),
db,
`SELECT id, nullable, option FROM t ORDER BY id`,
nil,
)
if err != nil {
panic(err)
}
for _, t := range (*selected)[0].Result {
id := t.ID
var nullable string
if t.Nulabble == nil {
nullable = NilString
} else {
nullable = fmt.Sprintf("%t", *t.Nulabble)
}
var option string
if t.Option == nil {
option = NilString
} else {
option = fmt.Sprintf("%t", *t.Option)
}
fmt.Printf("ID: %s, Nullable: %s, Option: %s\n", id, nullable, option)
}
}
Output: Created records with none and null fields successfully ID: t:a, Nullable: <nil>, Option: <nil> ID: t:b, Nullable: true, Option: <nil> ID: t:c, Nullable: true, Option: false ID: t:d, Nullable: false, Option: true ID: t:e, Nullable: false, Option: <nil> ID: t:f, Nullable: false, Option: <nil>
Example (Embedded_struct) ¶
package main
import (
"context"
"fmt"
"time"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "query", "persons")
type Base struct {
ID *models.RecordID `json:"id,omitempty"`
}
type Profile struct {
Base
City string `json:"city"`
}
type Person struct {
Base
Name string `json:"name"`
Profile Profile
CreatedAt models.CustomDateTime `json:"created_at,omitempty"`
UpdatedAt *models.CustomDateTime `json:"updated_at,omitempty"`
}
createdAt, err := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
if err != nil {
panic(err)
}
recordID := models.NewRecordID("persons", "yusuke")
createQueryResults, err := surrealdb.Query[[]Person](
context.Background(),
db,
`CREATE $record_id CONTENT $content`,
map[string]any{
"record_id": recordID,
"content": map[string]any{
"name": "Yusuke",
"created_at": models.CustomDateTime{
Time: createdAt,
},
"profile": map[string]any{
"id": models.NewRecordID("profiles", "yusuke"),
"city": "Tokyo",
},
},
})
if err != nil {
panic(err)
}
fmt.Printf("Number of query results: %d\n", len(*createQueryResults))
fmt.Printf("First query result's status: %+s\n", (*createQueryResults)[0].Status)
fmt.Printf("Persons contained in the first query result: %+v\n", (*createQueryResults)[0].Result)
updatedAt, err := time.Parse(time.RFC3339, "2023-10-02T12:00:00Z")
if err != nil {
panic(err)
}
updateQueryResults, err := surrealdb.Query[[]Person](
context.Background(),
db,
`UPDATE $id CONTENT $content`,
map[string]any{
"id": models.NewRecordID("persons", "yusuke"),
"content": map[string]any{
"name": "Yusuke Updated Last",
"created_at": createdAt,
"updated_at": updatedAt,
},
},
)
if err != nil {
panic(err)
}
fmt.Printf("Number of update query results: %d\n", len(*updateQueryResults))
fmt.Printf("Update query result's status: %+s\n", (*updateQueryResults)[0].Status)
fmt.Printf("Persons contained in the update query result: %+v\n", (*updateQueryResults)[0].Result)
selectQueryResults, err := surrealdb.Query[[]Person](
context.Background(),
db,
`SELECT * FROM $id`,
map[string]any{
"id": models.NewRecordID("persons", "yusuke"),
},
)
if err != nil {
panic(err)
}
fmt.Printf("Number of select query results: %d\n", len(*selectQueryResults))
fmt.Printf("Select query result's status: %+s\n", (*selectQueryResults)[0].Status)
fmt.Printf("Persons contained in the select query result: %+v\n", (*selectQueryResults)[0].Result)
}
Output: Number of query results: 1 First query result's status: OK Persons contained in the first query result: [{Base:{ID:persons:yusuke} Name:Yusuke Profile:{Base:{ID:profiles:yusuke} City:Tokyo} CreatedAt:{Time:2023-10-01 12:00:00 +0000 UTC} UpdatedAt:<nil>}] Number of update query results: 1 Update query result's status: OK Persons contained in the update query result: [{Base:{ID:persons:yusuke} Name:Yusuke Updated Last Profile:{Base:{ID:<nil>} City:} CreatedAt:{Time:2023-10-01 12:00:00 +0000 UTC} UpdatedAt:2023-10-02T12:00:00Z}] Number of select query results: 1 Select query result's status: OK Persons contained in the select query result: [{Base:{ID:persons:yusuke} Name:Yusuke Updated Last Profile:{Base:{ID:<nil>} City:} CreatedAt:{Time:2023-10-01 12:00:00 +0000 UTC} UpdatedAt:2023-10-02T12:00:00Z}]
Example (Issue192) ¶
See https://github.com/surrealdb/surrealdb.go/issues/292
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "query_issue192", "t")
_, err := surrealdb.Query[any](
context.Background(),
db,
`DEFINE TABLE IF NOT EXISTS t SCHEMAFULL;
DEFINE FIELD IF NOT EXISTS modified_at2 ON TABLE t TYPE option<datetime>;
CREATE t:s`,
map[string]any{"name": "John Doe"},
)
if err != nil {
panic(err)
}
type ReturnData1 struct {
ID *models.RecordID `json:"id,omitempty"`
ModifiedAt models.CustomDateTime `json:"modified_at,omitempty"`
}
data, err := surrealdb.Query[[]ReturnData1](
context.Background(),
db,
"SELECT id, modified_at FROM t",
nil)
if err != nil {
panic(err)
}
got := (*data)[0].Result[0]
fmt.Printf("ID: %s\n", got.ID)
fmt.Printf("ModifiedAt: %v\n", got.ModifiedAt)
fmt.Printf("ModifiedAt.IsZero(): %v\n", got.ModifiedAt.IsZero())
type ReturnData2 struct {
ID *models.RecordID `json:"id,omitempty"`
ModifiedAt *models.CustomDateTime `json:"modified_at,omitempty"`
}
data2, err := surrealdb.Query[[]ReturnData2](
context.Background(),
db,
"SELECT id, modified_at FROM t",
nil)
if err != nil {
panic(err)
}
got2 := (*data2)[0].Result[0]
fmt.Printf("ID: %s\n", got2.ID)
// With fxamacker/cbor: returns zero-value struct (not nil)
// With surrealcbor: returns nil
// Both should print the same format for consistency
if got2.ModifiedAt == nil || got2.ModifiedAt.IsZero() {
fmt.Printf("ModifiedAt: <nil or zero>\n")
}
fmt.Printf("ModifiedAt.IsZero(): %v\n", got2.ModifiedAt.IsZero())
}
Output: ID: t:s ModifiedAt: {0001-01-01 00:00:00 +0000 UTC} ModifiedAt.IsZero(): true ID: t:s ModifiedAt: <nil or zero> ModifiedAt.IsZero(): true
Example (Issue291) ¶
See https://github.com/surrealdb/surrealdb.go/issues/291
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "query_issue291", "t")
_, err := surrealdb.Query[any](
context.Background(),
db,
`DEFINE TABLE IF NOT EXISTS t SCHEMAFULL;
DEFINE FIELD IF NOT EXISTS i ON TABLE t TYPE option<int>;
DEFINE FIELD IF NOT EXISTS j ON TABLE t TYPE option<string>;
CREATE t:s;`,
map[string]any{"name": "John Doe"},
)
if err != nil {
panic(err)
}
type ReturnData struct {
I *int `json:"i"`
J *string `json:"j"`
}
dataNones, err := surrealdb.Query[[]ReturnData](
context.Background(),
db,
"SELECT i, j FROM t",
nil)
if err != nil {
panic(err)
}
got := (*dataNones)[0].Result[0]
// With fxamacker/cbor: returns zero values (0, "")
// With surrealcbor: returns nil
// We need to handle both cases
if got.I == nil || *got.I == 0 {
fmt.Printf("I: <nil or zero>\n")
} else {
fmt.Printf("I: %+v\n", *got.I)
}
if got.J == nil || *got.J == "" {
fmt.Printf("J: <nil or zero>\n")
} else {
fmt.Printf("J: %q\n", *got.J)
}
dataAll, err := surrealdb.Query[[]ReturnData](
context.Background(),
db,
"SELECT * FROM t",
nil)
if err != nil {
panic(err)
}
gotAll := (*dataAll)[0].Result[0]
fmt.Printf("I: %+v\n", gotAll.I)
fmt.Printf("J: %+v\n", gotAll.J)
}
Output: I: <nil or zero> J: <nil or zero> I: <nil> J: <nil>
Example (Live) ¶
ExampleQuery_live demonstrates using LIVE SELECT via the Query RPC. LIVE SELECT returns matching records as map[string]any in notification.Result. The notification channel is automatically closed when Kill is called.
package main
import (
"context"
"fmt"
"sort"
"strings"
"time"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
// formatRecordResult formats a record result (map[string]any) for testing.
// This is used for regular live query results (without diff) and DELETE operations.
// It handles the id field specially, formatting RecordID as table:⟨UUID⟩.
func formatRecordResult(record map[string]any) string {
keys := make([]string, 0, len(record))
for k := range record {
keys = append(keys, k)
}
sort.Strings(keys)
var parts []string
for _, k := range keys {
val := record[k]
if k == "id" {
recordID := val.(models.RecordID)
parts = append(parts, fmt.Sprintf("id=%s:⟨UUID⟩", recordID.Table))
} else {
parts = append(parts, fmt.Sprintf("%s=%v", k, val))
}
}
return "{" + strings.Join(parts, " ") + "}"
}
func main() {
config := testenv.MustNewConfig("surrealdbexamples", "livequery_query", "products")
config.Endpoint = testenv.GetSurrealDBWSURL()
db := config.MustNew()
type Product struct {
ID *models.RecordID `json:"id,omitempty"`
Name string `json:"name"`
Price float64 `json:"price"`
Stock int `json:"stock"`
}
ctx := context.Background()
// Create the table first - SurrealDB 3.x requires the table to exist for LIVE SELECT
_, err := surrealdb.Query[any](ctx, db, `DEFINE TABLE products`, nil)
if err != nil {
panic(fmt.Sprintf("Failed to create table: %v", err))
}
result, err := surrealdb.Query[models.UUID](ctx, db, "LIVE SELECT * FROM products WHERE stock < 10", map[string]any{})
if err != nil {
panic(fmt.Sprintf("Failed to start live query: %v", err))
}
liveID := (*result)[0].Result.String()
fmt.Println("Started live query")
notifications, err := db.LiveNotifications(liveID)
if err != nil {
panic(fmt.Sprintf("Failed to get live notifications channel: %v", err))
}
received := make(chan struct{})
done := make(chan bool)
notificationCount := 0
go func() {
for notification := range notifications {
notificationCount++
// LIVE SELECT returns matching records as map[string]any
record, ok := notification.Result.(map[string]any)
if !ok {
panic(fmt.Sprintf("Expected map[string]any for LIVE SELECT result, got %T", notification.Result))
}
fmt.Printf("Notification %d - Action: %s, Result: %s\n", notificationCount, notification.Action, formatRecordResult(record))
if notificationCount >= 3 {
close(received)
}
}
// Channel was closed
fmt.Println("Notification channel closed")
done <- true
}()
_, err = surrealdb.Create[Product](ctx, db, "products", map[string]any{
"name": "Widget",
"price": 9.99,
"stock": 5,
})
if err != nil {
panic(fmt.Sprintf("Failed to create product: %v", err))
}
_, err = surrealdb.Create[Product](ctx, db, "products", map[string]any{
"name": "Gadget",
"price": 19.99,
"stock": 3,
})
if err != nil {
panic(fmt.Sprintf("Failed to create second product: %v", err))
}
_, err = surrealdb.Create[Product](ctx, db, "products", map[string]any{
"name": "Abundant Item",
"price": 5.99,
"stock": 100,
})
if err != nil {
panic(fmt.Sprintf("Failed to create third product: %v", err))
}
_, err = surrealdb.Create[Product](ctx, db, "products", map[string]any{
"name": "Rare Item",
"price": 99.99,
"stock": 1,
})
if err != nil {
panic(fmt.Sprintf("Failed to create fourth product: %v", err))
}
// Wait for all expected notifications to be received
select {
case <-received:
// All notifications received
case <-time.After(2 * time.Second):
panic("Timeout waiting for all notifications")
}
fmt.Println("Stopping live query notifications")
err = surrealdb.Kill(ctx, db, liveID)
if err != nil {
panic(fmt.Sprintf("Failed to kill live query: %v", err))
}
select {
case <-done:
fmt.Println("Goroutine exited after channel closed")
case <-time.After(2 * time.Second):
panic("Timeout: notification channel was not closed after Kill")
}
}
Output: Started live query Notification 1 - Action: CREATE, Result: {id=products:⟨UUID⟩ name=Widget price=9.99 stock=5} Notification 2 - Action: CREATE, Result: {id=products:⟨UUID⟩ name=Gadget price=19.99 stock=3} Notification 3 - Action: CREATE, Result: {id=products:⟨UUID⟩ name=Rare Item price=99.99 stock=1} Stopping live query notifications Notification channel closed Goroutine exited after channel closed
Example (None_and_null_handling_allExistingFields) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
const (
NullString = "null"
NoneString = "none"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "query", "t")
_, err := surrealdb.Query[[]any](
context.Background(),
db,
`DEFINE TABLE t SCHEMAFULL;
DEFINE FIELD nullable ON t TYPE bool | null;
DEFINE FIELD option ON t TYPE option<bool>;
CREATE t:a SET nullable = null;
CREATE t:b SET nullable = true;
CREATE t:c SET nullable = true, option = false;
CREATE t:d SET nullable = false, option = true;
CREATE t:e SET nullable = false;
CREATE t:f SET nullable = false, option = NONE;
`,
map[string]any{
"id": models.NewRecordID("t", 1),
})
if err != nil {
panic(err)
}
type T struct {
ID *models.RecordID `json:"id,omitempty"`
Nulabble *bool `json:"nullable"`
Option *bool `json:"option"`
}
selected, err := surrealdb.Query[[]T](
context.Background(),
db,
`SELECT * FROM t ORDER BY id.id`,
nil,
)
if err != nil {
panic(err)
}
for _, t := range (*selected)[0].Result {
id := t.ID
var nullable string
if t.Nulabble == nil {
nullable = NullString
} else {
nullable = fmt.Sprintf("%t", *t.Nulabble)
}
var option string
if t.Option == nil {
option = NoneString
} else {
option = fmt.Sprintf("%t", *t.Option)
}
fmt.Printf("ID: %s, Nullable: %s, Option: %s\n", id, nullable, option)
}
}
Output: ID: t:a, Nullable: null, Option: none ID: t:b, Nullable: true, Option: none ID: t:c, Nullable: true, Option: false ID: t:d, Nullable: false, Option: true ID: t:e, Nullable: false, Option: none ID: t:f, Nullable: false, Option: none
Example (None_and_null_handling_explicitFields_ints_legacy_fxamackercbor) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
const (
NullString = "null"
NoneString = "none"
)
func main() {
c := testenv.MustNewConfig("example", "query", "t")
c.CBORImpl = testenv.CBORImplFxamackerCBOR
db := c.MustNew()
_, err := surrealdb.Query[[]any](
context.Background(),
db,
`DEFINE TABLE t SCHEMAFULL;
DEFINE FIELD nullable ON t TYPE int | null;
DEFINE FIELD option ON t TYPE option<int>;
CREATE t:a SET nullable = null;
CREATE t:b SET nullable = 2;
CREATE t:c SET nullable = 2, option = 1;
CREATE t:d SET nullable = 1, option = 2;
CREATE t:e SET nullable = 1;
CREATE t:f SET nullable = 1, option = NONE;
`,
map[string]any{
"id": models.NewRecordID("t", 1),
})
if err != nil {
panic(err)
}
type T struct {
ID *models.RecordID `json:"id,omitempty"`
Nulabble *int `json:"nullable"`
Option *int `json:"option"`
}
selected, err := surrealdb.Query[[]T](
context.Background(),
db,
`SELECT id, nullable, option FROM t ORDER BY id`,
nil,
)
if err != nil {
panic(err)
}
for _, t := range (*selected)[0].Result {
id := t.ID
var nullable string
if t.Nulabble == nil {
nullable = NullString
} else {
nullable = fmt.Sprintf("%v", *t.Nulabble)
}
var option string
if t.Option == nil {
option = NoneString
} else {
option = fmt.Sprintf("%v", *t.Option)
}
fmt.Printf("ID: %v, Nullable: %v, Option: %s\n", id, nullable, option)
}
}
Output: ID: t:a, Nullable: null, Option: 0 ID: t:b, Nullable: 2, Option: 0 ID: t:c, Nullable: 2, Option: 1 ID: t:d, Nullable: 1, Option: 2 ID: t:e, Nullable: 1, Option: 0 ID: t:f, Nullable: 1, Option: 0
Example (None_and_null_handling_explicitFields_ints_surrealcbor) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
const NilString = "<nil>"
func main() {
c := testenv.MustNewConfig("example", "query", "t")
c.CBORImpl = testenv.CBORImplSurrealCBOR
db := c.MustNew()
_, err := surrealdb.Query[[]any](
context.Background(),
db,
`DEFINE TABLE t SCHEMAFULL;
DEFINE FIELD nullable ON t TYPE int | null;
DEFINE FIELD option ON t TYPE option<int>;
CREATE t:a SET nullable = null;
CREATE t:b SET nullable = 2;
CREATE t:c SET nullable = 2, option = 1;
CREATE t:d SET nullable = 1, option = 2;
CREATE t:e SET nullable = 1;
CREATE t:f SET nullable = 1, option = NONE;
`,
map[string]any{
"id": models.NewRecordID("t", 1),
})
if err != nil {
panic(err)
}
type T struct {
ID *models.RecordID `json:"id,omitempty"`
Nulabble *int `json:"nullable"`
Option *int `json:"option"`
}
selected, err := surrealdb.Query[[]T](
context.Background(),
db,
`SELECT id, nullable, option FROM t ORDER BY id`,
nil,
)
if err != nil {
panic(err)
}
for _, t := range (*selected)[0].Result {
id := t.ID
var nullable string
if t.Nulabble == nil {
nullable = NilString
} else {
nullable = fmt.Sprintf("%v", *t.Nulabble)
}
var option string
if t.Option == nil {
option = NilString
} else {
option = fmt.Sprintf("%v", *t.Option)
}
fmt.Printf("ID: %v, Nullable: %v, Option: %s\n", id, nullable, option)
}
}
Output: ID: t:a, Nullable: <nil>, Option: <nil> ID: t:b, Nullable: 2, Option: <nil> ID: t:c, Nullable: 2, Option: 1 ID: t:d, Nullable: 1, Option: 2 ID: t:e, Nullable: 1, Option: <nil> ID: t:f, Nullable: 1, Option: <nil>
Example (None_and_null_handling_explicitFields_legacy_fxamackercbor) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
const (
NullString = "null"
NoneString = "none"
)
func main() {
c := testenv.MustNewConfig("example", "query", "t")
c.CBORImpl = testenv.CBORImplFxamackerCBOR
db := c.MustNew()
_, err := surrealdb.Query[[]any](
context.Background(),
db,
`DEFINE TABLE t SCHEMAFULL;
DEFINE FIELD nullable ON t TYPE bool | null;
DEFINE FIELD option ON t TYPE option<bool>;
CREATE t:a SET nullable = null;
CREATE t:b SET nullable = true;
CREATE t:c SET nullable = true, option = false;
CREATE t:d SET nullable = false, option = true;
CREATE t:e SET nullable = false;
CREATE t:f SET nullable = false, option = NONE;
`,
map[string]any{
"id": models.NewRecordID("t", 1),
})
if err != nil {
panic(err)
}
type T struct {
ID *models.RecordID `json:"id,omitempty"`
Nulabble *bool `json:"nullable"`
Option *bool `json:"option"`
}
selected, err := surrealdb.Query[[]T](
context.Background(),
db,
`SELECT id, nullable, option FROM t ORDER BY id`,
nil,
)
if err != nil {
panic(err)
}
for _, t := range (*selected)[0].Result {
id := t.ID
var nullable string
if t.Nulabble == nil {
nullable = NullString
} else {
nullable = fmt.Sprintf("%t", *t.Nulabble)
}
var option string
if t.Option == nil {
option = NoneString
} else {
option = fmt.Sprintf("%t", *t.Option)
}
fmt.Printf("ID: %s, Nullable: %s, Option: %s\n", id, nullable, option)
}
}
Output: ID: t:a, Nullable: null, Option: false ID: t:b, Nullable: true, Option: false ID: t:c, Nullable: true, Option: false ID: t:d, Nullable: false, Option: true ID: t:e, Nullable: false, Option: false ID: t:f, Nullable: false, Option: false
Example (None_and_null_handling_explicitFields_surrealcbor) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
const NilString = "<nil>"
func main() {
c := testenv.MustNewConfig("example", "query", "t")
c.CBORImpl = testenv.CBORImplSurrealCBOR
db := c.MustNew()
_, err := surrealdb.Query[[]any](
context.Background(),
db,
`DEFINE TABLE t SCHEMAFULL;
DEFINE FIELD nullable ON t TYPE bool | null;
DEFINE FIELD option ON t TYPE option<bool>;
CREATE t:a SET nullable = null;
CREATE t:b SET nullable = true;
CREATE t:c SET nullable = true, option = false;
CREATE t:d SET nullable = false, option = true;
CREATE t:e SET nullable = false;
CREATE t:f SET nullable = false, option = NONE;
`,
map[string]any{
"id": models.NewRecordID("t", 1),
})
if err != nil {
panic(err)
}
type T struct {
ID *models.RecordID `json:"id,omitempty"`
Nulabble *bool `json:"nullable"`
Option *bool `json:"option"`
}
selected, err := surrealdb.Query[[]T](
context.Background(),
db,
`SELECT id, nullable, option FROM t ORDER BY id`,
nil,
)
if err != nil {
panic(err)
}
for _, t := range (*selected)[0].Result {
id := t.ID
var nullable string
if t.Nulabble == nil {
nullable = NilString
} else {
nullable = fmt.Sprintf("%t", *t.Nulabble)
}
var option string
if t.Option == nil {
option = NilString
} else {
option = fmt.Sprintf("%t", *t.Option)
}
fmt.Printf("ID: %s, Nullable: %v, Option: %v\n", id, nullable, option)
}
}
Output: ID: t:a, Nullable: <nil>, Option: <nil> ID: t:b, Nullable: true, Option: <nil> ID: t:c, Nullable: true, Option: false ID: t:d, Nullable: false, Option: true ID: t:e, Nullable: false, Option: <nil> ID: t:f, Nullable: false, Option: <nil>
Example (Null_none_customdatetime_roundtrip_legacy_fxamackercbor) ¶
package main
import (
"context"
"fmt"
"time"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
c := testenv.MustNewConfig("example", "query", "t")
c.CBORImpl = testenv.CBORImplFxamackerCBOR
db := c.MustNew()
_, err := surrealdb.Query[[]any](
context.Background(),
db,
`DEFINE TABLE t SCHEMAFULL;
// DEFINE FIELD nullable_zero ON t TYPE datetime | null;
DEFINE FIELD nullable_nil ON t TYPE datetime | null;
DEFINE FIELD option_zero ON t TYPE option<datetime>;
DEFINE FIELD option_zero_omitempty ON t TYPE option<datetime>;
DEFINE FIELD option_zero_omitzero ON t TYPE option<datetime>;
DEFINE FIELD option_ptr_zero ON t TYPE option<datetime>;
DEFINE FIELD option_ptr_nil ON t TYPE option<datetime>;
DEFINE FIELD option_ptr_nil_omitempty ON t TYPE option<datetime>;
`,
nil,
)
if err != nil {
panic(err)
}
type T struct {
ID *models.RecordID `json:"id,omitempty"`
// NullableZero tests how a Zero value of CustomDateTime is marshaled into a nullable field
//
// This fails like this:
// Found NONE for field `nullable_zero`, with record `t:1`, but expected a datetime | null
// NullableZero models.CustomDateTime `json:"nullable_zero"`
// NullableNil tests how a Go pointer to nil is marshaled into a nullable field
NullableNil *models.CustomDateTime `json:"nullable_nil"`
OptionZero models.CustomDateTime `json:"option_zero"`
// OptionZeroOmitEmpty tests how a Zero value of CustomDateTime is marshaled into an option field
OptionZeroOmitEmpty models.CustomDateTime `json:"option_zero_omitempty,omitempty"`
// OptionZeroOmitZero tests how a Zero value of CustomDateTime is marshaled into an option field
// when the field is omitzero
OptionZeroOmitZero models.CustomDateTime `json:"option_zero_omitzero,omitzero"`
// OptionPtrZero tests how a Go pointer to a Zero value of CustomDateTime is marshaled
OptionPtrZero *models.CustomDateTime `json:"option_ptr_zero"`
// OptionNil tests how a Go pointer to nil is marshaled
//
// This fails like this:
// Found NULL for field `option_ptr_nil`, with record `t:1`, but expected a option<datetime>
// OptionPtrNil *models.CustomDateTime `json:"option_ptr_nil"`
// OptionPtrNilOmitEmpty tests how a Go pointer to nil is marshaled into an option field
// when the field is omitted if empty.
OptionPtrNilOmitEmpty *models.CustomDateTime `json:"option_ptr_nil_omitempty,omitempty"`
}
// Marshaling rule:
//
// m1. Go `nil` w/o omitempty is SurrealDB `null`
// m2. Go `nil` w/ omitempty is SurrealDB `none`
// m3. Zero value of CustomDateTime is SurrealDB `none`
// Unmarshaling rule:
//
// u1. SurrealDB `null` is Go `nil`
// u2. SurrealDB `none` + Go `any` type is Go `models.CustomNil{}`
// u3. SurrealDB `none` + Go non-pointer type is Go zero value (no primitive type is supported yet, but CustomDateTime is supported)
// u4. SurrealDB `none` + Go pointer type is Go `nil` when `SELECT *` is used
// u5. SurrealDB `none` + Go pointer type is a pointer to a Go zero value when explicit fields are selected
// (e.g., you cannot unmarshal `none` into `nil` when the field is explicitly selected)
// Future plans:
//
// Our plan is to fix u4 and u5 in the future, by unifying the two into:
//
// u4. SurrealDB `none` will unmarshal into Go `nil` if the field IS a pointer type,
// REGARDLESS of whether the field is explicitly selected or not
_, err = surrealdb.Query[[]T](
context.Background(),
db,
`CREATE $id CONTENT $value`,
map[string]any{
"id": models.NewRecordID("t", 1),
"value": T{
// NullableZero: models.CustomDateTime{Time: time.Time{}},
NullableNil: nil,
OptionZero: models.CustomDateTime{Time: time.Time{}},
OptionZeroOmitEmpty: models.CustomDateTime{Time: time.Time{}},
OptionZeroOmitZero: models.CustomDateTime{Time: time.Time{}},
OptionPtrZero: &models.CustomDateTime{Time: time.Time{}},
// OptionPtrNil: nil,
OptionPtrNilOmitEmpty: nil,
},
},
)
if err != nil {
panic(err)
}
selectExplicit, err := surrealdb.Query[[]T](
context.Background(),
db,
`SELECT id,
nullable_nil,
option_zero,
option_zero_omitempty,
option_zero_omitzero,
option_ptr_zero,
option_ptr_nil_omitempty
FROM t ORDER BY id`,
nil,
)
if err != nil {
panic(err)
}
fmt.Println()
fmt.Println("# SELECT with explicit fields")
fmt.Println()
for _, t := range (*selectExplicit)[0].Result {
fmt.Printf("ID: %v\n", t.ID)
// fmt.Printf("NullableZero: %v\n", t.NullableZero)
fmt.Printf("NullableNil: %v\n", t.NullableNil)
fmt.Printf("OptionZero: %v\n", t.OptionZero)
fmt.Printf("OptionZeroOmitEmpty: %v\n", t.OptionZeroOmitEmpty)
fmt.Printf("OptionZeroOmitZero: %v\n", t.OptionZeroOmitZero)
fmt.Printf("OptionPtrZero: %v\n", t.OptionPtrZero)
// fmt.Printf("OptionPtrNil: %v\n", t.OptionPtrNil)
fmt.Printf("OptionPtrNilOmitEmpty: %v\n", t.OptionPtrNilOmitEmpty)
}
selectAll, err := surrealdb.Query[[]T](
context.Background(),
db,
`SELECT * FROM t ORDER BY id`,
nil,
)
if err != nil {
panic(err)
}
fmt.Println()
fmt.Println("# SELECT with all fields")
fmt.Println()
for _, t := range (*selectAll)[0].Result {
fmt.Printf("ID: %v\n", t.ID)
// fmt.Printf("NullableZero: %v\n", t.NullableZero)
fmt.Printf("NullableNil: %v\n", t.NullableNil)
fmt.Printf("OptionZero: %v\n", t.OptionZero)
fmt.Printf("OptionZeroOmitEmpty: %v\n", t.OptionZeroOmitEmpty)
fmt.Printf("OptionZeroOmitZero: %v\n", t.OptionZeroOmitZero)
fmt.Printf("OptionPtrZero: %v\n", t.OptionPtrZero)
// fmt.Printf("OptionPtrNil: %v\n", t.OptionPtrNil)
fmt.Printf("OptionPtrNilOmitEmpty: %v\n", t.OptionPtrNilOmitEmpty)
}
selectExplicitMap, err := surrealdb.Query[[]map[string]any](
context.Background(),
db,
`SELECT id,
nullable_nil,
option_zero,
option_zero_omitempty,
option_zero_omitzero,
option_ptr_zero,
option_ptr_nil_omitempty
FROM t ORDER BY id`,
nil,
)
if err != nil {
panic(err)
}
fmt.Println()
fmt.Println("# SELECT with explicit fields into map[string]any")
fmt.Println()
for _, t := range (*selectExplicitMap)[0].Result {
fmt.Printf("ID: %v\n", t["id"])
// fmt.Printf("NullableZero: %+v\n", t["nullable_zero"])
fmt.Printf("NullableNil: %T%+v\n", t["nullable_nil"], t["nullable_nil"])
fmt.Printf("OptionZero: %T%+v\n", t["option_zero"], t["option_zero"])
fmt.Printf("OptionZeroOmitEmpty: %T%+v\n", t["option_zero_omitempty"], t["option_zero_omitempty"])
fmt.Printf("OptionZeroOmitZero: %T%+v\n", t["option_zero_omitzero"], t["option_zero_omitzero"])
fmt.Printf("OptionPtrZero: %T%+v\n", t["option_ptr_zero"], t["option_ptr_zero"])
// fmt.Printf("OptionPtrNil: %T%+v\n", t["option_ptr_nil"], t["option_ptr_nil"])
fmt.Printf("OptionPtrNilOmitEmpty: %T%+v\n", t["option_ptr_nil_omitempty"], t["option_ptr_nil_omitempty"])
}
}
Output: # SELECT with explicit fields ID: t:1 NullableNil: <nil> OptionZero: {0001-01-01 00:00:00 +0000 UTC} OptionZeroOmitEmpty: {0001-01-01 00:00:00 +0000 UTC} OptionZeroOmitZero: {0001-01-01 00:00:00 +0000 UTC} OptionPtrZero: 0001-01-01T00:00:00Z OptionPtrNilOmitEmpty: 0001-01-01T00:00:00Z # SELECT with all fields ID: t:1 NullableNil: <nil> OptionZero: {0001-01-01 00:00:00 +0000 UTC} OptionZeroOmitEmpty: {0001-01-01 00:00:00 +0000 UTC} OptionZeroOmitZero: {0001-01-01 00:00:00 +0000 UTC} OptionPtrZero: <nil> OptionPtrNilOmitEmpty: <nil> # SELECT with explicit fields into map[string]any ID: {t 1} NullableNil: <nil><nil> OptionZero: models.CustomNil{} OptionZeroOmitEmpty: models.CustomNil{} OptionZeroOmitZero: models.CustomNil{} OptionPtrZero: models.CustomNil{} OptionPtrNilOmitEmpty: models.CustomNil{}
Example (Null_none_customdatetime_roundtrip_surrealcbor) ¶
package main
import (
"context"
"fmt"
"time"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
c := testenv.MustNewConfig("example", "query", "t")
c.CBORImpl = testenv.CBORImplSurrealCBOR
db := c.MustNew()
_, err := surrealdb.Query[[]any](
context.Background(),
db,
`DEFINE TABLE t SCHEMAFULL;
// DEFINE FIELD nullable_zero ON t TYPE datetime | null;
DEFINE FIELD nullable_nil ON t TYPE datetime | null;
DEFINE FIELD option_zero ON t TYPE option<datetime>;
DEFINE FIELD option_zero_omitempty ON t TYPE option<datetime>;
DEFINE FIELD option_zero_omitzero ON t TYPE option<datetime>;
DEFINE FIELD option_ptr_zero ON t TYPE option<datetime>;
DEFINE FIELD option_ptr_nil ON t TYPE option<datetime>;
DEFINE FIELD option_ptr_nil_omitempty ON t TYPE option<datetime>;
`,
nil,
)
if err != nil {
panic(err)
}
type T struct {
ID *models.RecordID `json:"id,omitempty"`
// NullableZero tests how a Zero value of CustomDateTime is marshaled into a nullable field
//
// This fails like this:
// Found NONE for field `nullable_zero`, with record `t:1`, but expected a datetime | null
// NullableZero models.CustomDateTime `json:"nullable_zero"`
// NullableNil tests how a Go pointer to nil is marshaled into a nullable field
NullableNil *models.CustomDateTime `json:"nullable_nil"`
OptionZero models.CustomDateTime `json:"option_zero"`
// OptionZeroOmitEmpty tests how a Zero value of CustomDateTime is marshaled into an option field
OptionZeroOmitEmpty models.CustomDateTime `json:"option_zero_omitempty,omitempty"`
// OptionZeroOmitZero tests how a Zero value of CustomDateTime is marshaled into an option field
// when the field is omitzero
OptionZeroOmitZero models.CustomDateTime `json:"option_zero_omitzero,omitzero"`
// OptionPtrZero tests how a Go pointer to a Zero value of CustomDateTime is marshaled
OptionPtrZero *models.CustomDateTime `json:"option_ptr_zero"`
// OptionNil tests how a Go pointer to nil is marshaled
//
// This fails like this:
// Found NULL for field `option_ptr_nil`, with record `t:1`, but expected a option<datetime>
// OptionPtrNil *models.CustomDateTime `json:"option_ptr_nil"`
// OptionPtrNilOmitEmpty tests how a Go pointer to nil is marshaled into an option field
// when the field is omitted if empty.
OptionPtrNilOmitEmpty *models.CustomDateTime `json:"option_ptr_nil_omitempty,omitempty"`
}
_, err = surrealdb.Query[[]T](
context.Background(),
db,
`CREATE $id CONTENT $value`,
map[string]any{
"id": models.NewRecordID("t", 1),
"value": T{
// NullableZero: models.CustomDateTime{Time: time.Time{}},
NullableNil: nil,
OptionZero: models.CustomDateTime{Time: time.Time{}},
OptionZeroOmitEmpty: models.CustomDateTime{Time: time.Time{}},
OptionZeroOmitZero: models.CustomDateTime{Time: time.Time{}},
OptionPtrZero: &models.CustomDateTime{Time: time.Time{}},
// OptionPtrNil: nil,
OptionPtrNilOmitEmpty: nil,
},
},
)
if err != nil {
panic(err)
}
selectExplicit, err := surrealdb.Query[[]T](
context.Background(),
db,
`SELECT id,
nullable_nil,
option_zero,
option_zero_omitempty,
option_zero_omitzero,
option_ptr_zero,
option_ptr_nil_omitempty
FROM t ORDER BY id`,
nil,
)
if err != nil {
panic(err)
}
fmt.Println()
fmt.Println("# SELECT with explicit fields")
fmt.Println()
for _, t := range (*selectExplicit)[0].Result {
fmt.Printf("ID: %v\n", t.ID)
// fmt.Printf("NullableZero: %v\n", t.NullableZero)
fmt.Printf("NullableNil: %v\n", t.NullableNil)
fmt.Printf("OptionZero: %v\n", t.OptionZero)
fmt.Printf("OptionZeroOmitEmpty: %v\n", t.OptionZeroOmitEmpty)
fmt.Printf("OptionZeroOmitZero: %v\n", t.OptionZeroOmitZero)
fmt.Printf("OptionPtrZero: %v\n", t.OptionPtrZero)
// fmt.Printf("OptionPtrNil: %v\n", t.OptionPtrNil)
fmt.Printf("OptionPtrNilOmitEmpty: %v\n", t.OptionPtrNilOmitEmpty)
}
selectAll, err := surrealdb.Query[[]T](
context.Background(),
db,
`SELECT * FROM t ORDER BY id`,
nil,
)
if err != nil {
panic(err)
}
fmt.Println()
fmt.Println("# SELECT with all fields")
fmt.Println()
for _, t := range (*selectAll)[0].Result {
fmt.Printf("ID: %v\n", t.ID)
// fmt.Printf("NullableZero: %v\n", t.NullableZero)
fmt.Printf("NullableNil: %v\n", t.NullableNil)
fmt.Printf("OptionZero: %v\n", t.OptionZero)
fmt.Printf("OptionZeroOmitEmpty: %v\n", t.OptionZeroOmitEmpty)
fmt.Printf("OptionZeroOmitZero: %v\n", t.OptionZeroOmitZero)
fmt.Printf("OptionPtrZero: %v\n", t.OptionPtrZero)
// fmt.Printf("OptionPtrNil: %v\n", t.OptionPtrNil)
fmt.Printf("OptionPtrNilOmitEmpty: %v\n", t.OptionPtrNilOmitEmpty)
}
selectExplicitMap, err := surrealdb.Query[[]map[string]any](
context.Background(),
db,
`SELECT id,
nullable_nil,
option_zero,
option_zero_omitempty,
option_zero_omitzero,
option_ptr_zero,
option_ptr_nil_omitempty
FROM t ORDER BY id`,
nil,
)
if err != nil {
panic(err)
}
fmt.Println()
fmt.Println("# SELECT with explicit fields into map[string]any")
fmt.Println()
for _, t := range (*selectExplicitMap)[0].Result {
fmt.Printf("ID: %v\n", t["id"])
// fmt.Printf("NullableZero: %+v\n", t["nullable_zero"])
fmt.Printf("NullableNil: %T%+v\n", t["nullable_nil"], t["nullable_nil"])
fmt.Printf("OptionZero: %T%+v\n", t["option_zero"], t["option_zero"])
fmt.Printf("OptionZeroOmitEmpty: %T%+v\n", t["option_zero_omitempty"], t["option_zero_omitempty"])
fmt.Printf("OptionZeroOmitZero: %T%+v\n", t["option_zero_omitzero"], t["option_zero_omitzero"])
fmt.Printf("OptionPtrZero: %T%+v\n", t["option_ptr_zero"], t["option_ptr_zero"])
// fmt.Printf("OptionPtrNil: %T%+v\n", t["option_ptr_nil"], t["option_ptr_nil"])
fmt.Printf("OptionPtrNilOmitEmpty: %T%+v\n", t["option_ptr_nil_omitempty"], t["option_ptr_nil_omitempty"])
}
}
Output: # SELECT with explicit fields ID: t:1 NullableNil: <nil> OptionZero: {0001-01-01 00:00:00 +0000 UTC} OptionZeroOmitEmpty: {0001-01-01 00:00:00 +0000 UTC} OptionZeroOmitZero: {0001-01-01 00:00:00 +0000 UTC} OptionPtrZero: <nil> OptionPtrNilOmitEmpty: <nil> # SELECT with all fields ID: t:1 NullableNil: <nil> OptionZero: {0001-01-01 00:00:00 +0000 UTC} OptionZeroOmitEmpty: {0001-01-01 00:00:00 +0000 UTC} OptionZeroOmitZero: {0001-01-01 00:00:00 +0000 UTC} OptionPtrZero: <nil> OptionPtrNilOmitEmpty: <nil> # SELECT with explicit fields into map[string]any ID: {t 1} NullableNil: <nil><nil> OptionZero: <nil><nil> OptionZeroOmitEmpty: <nil><nil> OptionZeroOmitZero: <nil><nil> OptionPtrZero: <nil><nil> OptionPtrNilOmitEmpty: <nil><nil>
Example (Only) ¶
The Query function's result type parameter should be varied according to the query. For example, SELECT ONLY returns a single record, not an array of records, and therefore the result type parameter should be a single type, not a slice type.
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "query_only", "persons")
type Person struct {
ID *models.RecordID `json:"id,omitempty"`
Name string `json:"name"`
}
recordID := models.NewRecordID("persons", "yusuke")
// Note the type parameter is []Person
createQueryResults, err := surrealdb.Query[[]Person](
context.Background(),
db,
`CREATE $record_id CONTENT {name: "Yusuke"}`,
map[string]any{
"record_id": recordID,
},
)
if err != nil {
panic(err)
}
var persons []Person = (*createQueryResults)[0].Result
fmt.Printf("Persons contained in the first query result: %+v\n", persons)
// Note the type parameter is Person, not []Person,
// due to the ONLY keyword
queryOnlyResults, err := surrealdb.Query[Person](
context.Background(),
db,
`SELECT * FROM ONLY $record_id`,
map[string]any{
"record_id": recordID,
},
)
if err != nil {
panic(err)
}
var person Person = (*queryOnlyResults)[0].Result
fmt.Printf("Person contained in the query only result: %+v\n", person)
}
Output: Persons contained in the first query result: [{ID:persons:yusuke Name:Yusuke}] Person contained in the query only result: {ID:persons:yusuke Name:Yusuke}
Example (Return) ¶
ExampleQueryReturn demonstrates how to use the RETURN NONE clause in a query. See https://github.com/surrealdb/surrealdb.go/issues/203 for more context.
package main
import (
"context"
"fmt"
"time"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "query", "persons")
type NestedStruct struct {
City string `json:"city"`
}
type Person struct {
ID *models.RecordID `json:"id,omitempty"`
Name string `json:"name"`
NestedMap map[string]any `json:"nested_map,omitempty"`
NestedStruct `json:"nested_struct,omitempty"`
CreatedAt models.CustomDateTime `json:"created_at,omitempty"`
UpdatedAt *models.CustomDateTime `json:"updated_at,omitempty"`
}
createdAt, err := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
if err != nil {
panic(err)
}
insertQueryResults, err := surrealdb.Query[any](
context.Background(),
db,
`INSERT INTO persons [$content] RETURN NONE`,
map[string]any{
"content": map[string]any{
"id": "yusuke",
"name": "Yusuke",
"nested_struct": NestedStruct{
City: "Tokyo",
},
"created_at": models.CustomDateTime{
Time: createdAt,
},
},
})
if err != nil {
panic(err)
}
fmt.Printf("Number of insert query results: %d\n", len(*insertQueryResults))
fmt.Printf("First insert query result's status: %+s\n", (*insertQueryResults)[0].Status)
fmt.Printf("Results contained in the first query result: %+v\n", (*insertQueryResults)[0].Result)
selectQueryResults, err := surrealdb.Query[[]Person](
context.Background(),
db,
`SELECT * FROM $id`, map[string]any{
"id": models.NewRecordID("persons", "yusuke"),
},
)
if err != nil {
panic(err)
}
fmt.Printf("Number of select query results: %d\n", len(*selectQueryResults))
fmt.Printf("First select query result's status: %+s\n", (*selectQueryResults)[0].Status)
fmt.Printf("Persons contained in the first select query result: %+v\n", (*selectQueryResults)[0].Result)
}
Output: Number of insert query results: 1 First insert query result's status: OK Results contained in the first query result: [] Number of select query results: 1 First select query result's status: OK Persons contained in the first select query result: [{ID:persons:yusuke Name:Yusuke NestedMap:map[] NestedStruct:{City:Tokyo} CreatedAt:{Time:2023-10-01 12:00:00 +0000 UTC} UpdatedAt:<nil>}]
Example (SelectOnTable) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "query_select_on_table", "persons")
type Person struct {
ID *models.RecordID `json:"id,omitempty"`
Name string `json:"name"`
}
// Seed two records
_, err := surrealdb.Query[[]Person](
context.Background(),
db,
`CREATE persons:alice CONTENT {name: "Alice"}; CREATE persons:bob CONTENT {name: "Bob"}`,
nil,
)
if err != nil {
panic(err)
}
// Use models.Table as a query variable to select all records in a table
// Note: Directly embedding table names in query strings is prone to injection attacks.
results, err := surrealdb.Query[[]Person](
context.Background(),
db,
`SELECT * FROM $table ORDER BY name`,
map[string]any{
"table": models.Table("persons"),
},
)
if err != nil {
panic(err)
}
fmt.Printf("Number of query results: %d\n", len(*results))
fmt.Printf("First query result's status: %s\n", (*results)[0].Status)
for _, p := range (*results)[0].Result {
fmt.Printf("Person: %s (ID: %s)\n", p.Name, p.ID)
}
}
Output: Number of query results: 1 First query result's status: OK Person: Alice (ID: persons:alice) Person: Bob (ID: persons:bob)
Example (TransactionRollback) ¶
ExampleQuery_transactionRollback demonstrates that mutations within a rolled back transaction don't persist. The CANCEL statement rolls back all changes made within the transaction.
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
config := testenv.MustNewConfig("surrealdbexamples", "transaction_rollback", "accounts")
config.Endpoint = testenv.GetSurrealDBURL()
db := config.MustNew()
type Account struct {
ID *models.RecordID `json:"id,omitempty"`
Name string `json:"name"`
Balance float64 `json:"balance"`
}
ctx := context.Background()
// First, create an initial account outside of any transaction
initialAccount, err := surrealdb.Create[Account](ctx, db, "accounts", map[string]any{
"name": "Savings Account",
"balance": 1000.00,
})
if err != nil {
panic(fmt.Sprintf("Failed to create initial account: %v", err))
}
fmt.Printf("Initial account created: %s with balance %.2f\n", initialAccount.Name, initialAccount.Balance)
// Now start a transaction that will be rolled back
transactionQuery := `
BEGIN TRANSACTION;
-- Create a new account within the transaction
CREATE accounts SET name = $checkingName, balance = $checkingBalance;
-- Update the existing account within the transaction
UPDATE $accountID SET balance = $newBalance;
-- Create another account
CREATE accounts SET name = $investmentName, balance = $investmentBalance;
-- Roll back all changes made in this transaction
CANCEL TRANSACTION;
`
_, err = surrealdb.Query[any](ctx, db, transactionQuery, map[string]any{
"accountID": initialAccount.ID,
"checkingName": "Checking Account",
"checkingBalance": 500.00,
"newBalance": 2000.00,
"investmentName": "Investment Account",
"investmentBalance": 5000.00,
})
// When a transaction is canceled, SurrealDB returns an error
if err != nil {
fmt.Println("Transaction was rolled back (as expected)")
} else {
panic("Expected an error from canceled transaction, but got none")
}
// Verify that no new accounts were created
allAccounts, err := surrealdb.Select[[]Account](ctx, db, "accounts")
if err != nil {
panic(fmt.Sprintf("Failed to select accounts: %v", err))
}
fmt.Printf("Number of accounts after rollback: %d\n", len(*allAccounts))
// Verify that the original account balance wasn't changed
updatedAccount, err := surrealdb.Select[Account](ctx, db, *initialAccount.ID)
if err != nil {
panic(fmt.Sprintf("Failed to select account: %v", err))
}
fmt.Printf("Original account balance after rollback: %.2f\n", updatedAccount.Balance)
// Now demonstrate a committed transaction for comparison
committedTransactionQuery := `
BEGIN TRANSACTION;
-- Create a new account within the transaction
CREATE accounts SET name = $businessName, balance = $businessBalance;
-- Commit the transaction
COMMIT TRANSACTION;
`
_, err = surrealdb.Query[any](ctx, db, committedTransactionQuery, map[string]any{
"businessName": "Business Account",
"businessBalance": 3000.00,
})
if err != nil {
panic(fmt.Sprintf("Failed to execute committed transaction: %v", err))
}
fmt.Println("Second transaction executed and committed")
// Verify that the new account was created
finalAccounts, err := surrealdb.Select[[]Account](ctx, db, "accounts")
if err != nil {
panic(fmt.Sprintf("Failed to select accounts: %v", err))
}
fmt.Printf("Number of accounts after commit: %d\n", len(*finalAccounts))
}
Output: Initial account created: Savings Account with balance 1000.00 Transaction was rolled back (as expected) Number of accounts after rollback: 1 Original account balance after rollback: 1000.00 Second transaction executed and committed Number of accounts after commit: 2
Example (Transaction_issue_177_commit) ¶
See https://github.com/surrealdb/surrealdb.go/issues/177
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
config := testenv.MustNewConfig("surrealdbexamples", "query", "t")
db := config.MustNew()
ctx := context.Background()
// Detect version to handle result format differences
v, err := testenv.GetVersion(ctx, db)
if err != nil {
panic(err)
}
queryResults, err := surrealdb.Query[any](ctx, db,
`BEGIN;
CREATE t:s SET name = 'test1';
CREATE t:t SET name = 'test2';
SELECT * FROM $id;
COMMIT;`,
map[string]any{
"id": models.RecordID{Table: "t", ID: "s"},
})
if err != nil {
panic(err)
}
fmt.Printf("Status: %v\n", (*queryResults)[0].Status)
// Transaction result format changed between v2 and v3:
// - v2.x: Returns only statement results (3 results: CREATE, CREATE, SELECT)
// - v3.x: Returns results for all statements (5 results: BEGIN, CREATE, CREATE, SELECT, COMMIT)
// Extract only the statement results (CREATE, CREATE, SELECT)
var statementResults []surrealdb.QueryResult[any]
if v.IsV3OrLater() {
// In v3, skip BEGIN (index 0) and COMMIT (last index)
statementResults = (*queryResults)[1:4]
} else {
// In v2, all results are statement results
statementResults = *queryResults
}
if len(statementResults) != 3 {
panic(fmt.Errorf("expected 3 statement results, got %d", len(statementResults)))
}
var records []map[string]any
for i, result := range statementResults {
if result.Status != "OK" {
panic(fmt.Errorf("expected OK status for statement result %d, got %s", i, result.Status))
}
if result.Result == nil {
panic(fmt.Errorf("expected non-nil result for statement result %d", i))
}
if record, ok := result.Result.([]any); ok && len(record) > 0 {
records = append(records, record[0].(map[string]any))
} else {
panic(fmt.Errorf("expected result to be a slice of maps, got %T", result.Result))
}
}
fmt.Printf("result[0].id: %v\n", records[0]["id"])
fmt.Printf("result[0].name: %v\n", records[0]["name"])
fmt.Printf("result[1].id: %v\n", records[1]["id"])
fmt.Printf("result[1].name: %v\n", records[1]["name"])
if id := records[2]["id"]; id != nil && id != (models.RecordID{Table: "t", ID: "s"}) {
panic(fmt.Errorf("expected id to be empty for SurrealDB v3.0.0-alpha.7, or 's' for v2.3.7, got %v", id))
}
fmt.Printf("result[2].name: %v\n", records[2]["name"])
}
Output: Status: OK result[0].id: {t s} result[0].name: test1 result[1].id: {t t} result[1].name: test2 result[2].name: test1
Example (Transaction_issue_177_return_before_commit) ¶
See https://github.com/surrealdb/surrealdb.go/issues/177
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
config := testenv.MustNewConfig("surrealdbexamples", "query", "t")
db := config.MustNew()
ctx := context.Background()
// Detect version to handle result format differences
v, err := testenv.GetVersion(ctx, db)
if err != nil {
panic(err)
}
// Note that you are returning before committing the transaction.
// In this case, you get the uncommitted result of the CREATE,
// which lacks the ID field becase we aren't sure if the ID is committed or not
// at that point.
// SurrealDB may be enhanced to handle this, but for now,
// you should commit the transaction before returning the result.
// See the ExampleQuery_transaction_issue_177_commit function for the correct way to do this.
queryResults, err := surrealdb.Query[any](ctx, db,
`BEGIN;
CREATE t:s SET name = 'test';
LET $i = SELECT * FROM $id;
RETURN $i;
COMMIT;`,
map[string]any{
"id": models.RecordID{Table: "t", ID: "s"},
})
if err != nil {
panic(err)
}
// Transaction result format changed between v2 and v3:
// - v2.x: Returns only the RETURN result (1 result)
// - v3.x: Returns results for all statements (5 results: BEGIN, CREATE, LET, RETURN, COMMIT)
var returnResult surrealdb.QueryResult[any]
if v.IsV3OrLater() {
// In v3, the RETURN result is at index 3 (after BEGIN, CREATE, LET)
returnResult = (*queryResults)[3]
} else {
// In v2, only the RETURN result is returned
returnResult = (*queryResults)[0]
}
rs := returnResult.Result.([]any)
r := rs[0].(map[string]any)
fmt.Printf("Status: %v\n", returnResult.Status)
fmt.Printf("r.name: %v\n", r["name"])
if id := r["id"]; id != nil && id != (models.RecordID{Table: "t", ID: "s"}) {
panic(fmt.Errorf("expected id to be empty for SurrealDB v3.0.0-alpha.7, or 's' for v2.3.7, got %v", id))
}
}
Output: Status: OK r.name: test
Example (Transaction_let_return) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
config := testenv.MustNewConfig("surrealdbexamples", "query", "t")
db := config.MustNew()
ctx := context.Background()
// Detect version to handle result format differences
v, err := testenv.GetVersion(ctx, db)
if err != nil {
panic(err)
}
createQueryResults, err := surrealdb.Query[[]any](
ctx,
db,
`BEGIN;
CREATE t:1 SET name = 'test';
LET $i = SELECT * FROM $id;
RETURN $i.name;
COMMIT
`,
map[string]any{
"id": models.NewRecordID("t", 1),
})
if err != nil {
panic(err)
}
// Transaction result format changed between v2 and v3:
// - v2.x: Returns only the RETURN result (1 result)
// - v3.x: Returns results for all statements (5 results)
var returnResult any
if v.IsV3OrLater() {
// In v3, the RETURN result is at index 3 (after BEGIN, CREATE, LET)
returnResult = (*createQueryResults)[3].Result
} else {
// In v2, only the RETURN result is returned
returnResult = (*createQueryResults)[0].Result
}
fmt.Printf("First query result's status: %+s\n", (*createQueryResults)[0].Status)
fmt.Printf("Names contained in the RETURN result: %+v\n", returnResult)
}
Output: First query result's status: OK Names contained in the RETURN result: [test]
Example (Transaction_return) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
config := testenv.MustNewConfig("surrealdbexamples", "query", "person")
db := config.MustNew()
ctx := context.Background()
// Detect version to handle result format differences
v, err := testenv.GetVersion(ctx, db)
if err != nil {
panic(err)
}
// Transaction result format changed between v2 and v3:
// - v2.x: Returns only the RETURN result (1 result)
// - v3.x: Returns results for all statements (5 results: BEGIN, CREATE, CREATE, RETURN, COMMIT)
// For v3.x, use []any to avoid decode error when the type varies per result
results, err := surrealdb.Query[any](
ctx,
db,
`BEGIN; CREATE person:1; CREATE person:2; RETURN true; COMMIT;`,
map[string]any{},
)
if err != nil {
panic(err)
}
var resultBool bool
if v.IsV3OrLater() {
// In v3, the RETURN result is at index 3 (after BEGIN, CREATE, CREATE)
resultBool = (*results)[3].Result.(bool)
} else {
// In v2, only the RETURN result is returned
resultBool = (*results)[0].Result.(bool)
}
fmt.Printf("Status: %v\n", (*results)[0].Status)
fmt.Printf("Result: %v\n", resultBool)
}
Output: Status: OK Result: true
Example (Transaction_throw) ¶
package main
import (
"context"
"errors"
"fmt"
"strings"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "query", "person")
var (
queryResults *[]surrealdb.QueryResult[*int]
err error
)
// Up until v0.4.3, making QueryResult[T] parameterized with anything other than `any`
// or `string` failed with:
// cannot unmarshal UTF-8 text string into Go struct field
// in case the query was executed on the database but failed with an error.
//
// It was due to a mismatch between the expected type and the actual type-
// The actual query result was a string, which provides the error message sent
// from the database, regardless of the type parameter specified to the Query function.
//
// Since v0.4.4, the QueryResult was enhanced to set the Error field
// to a QueryError if the query failed, allowing the caller to handle the error.
// In that case, the Result field will be empty(or nil if it is a pointer type),
// and the Status field will be set to "ERR".
//
// It's also worth noting that the returned error from the Query function
// will be nil if the query was executed successfully, in which case all the results
// have no Error field set.
//
// If the query failed, the returned error will be a `joinError` created by the `errors.Join` function,
// which contains all the errors that occurred during the query execution.
// The caller can check the Error field of each QueryResult to see if the query failed,
// or check the returned error from the Query function to see if the query failed.
queryResults, err = surrealdb.Query[*int](
context.Background(),
db,
`BEGIN; THROW "test"; RETURN 1; COMMIT;`,
nil,
)
// Normalize error messages for version compatibility
// v2.x: "failed transaction"
// v3.x: uses British spelling in error messages
normalizeTransactionError := func(err error) string {
if err == nil {
return "<nil>"
}
s := err.Error()
s = strings.ReplaceAll(s, "cancelled transaction", "failed transaction") //nolint:misspell
s = strings.ReplaceAll(s, "canceled transaction", "failed transaction")
return s
}
// Filter to only show ERR results (v3 adds OK results for BEGIN)
var errResults []surrealdb.QueryResult[*int]
for _, r := range *queryResults {
if r.Status == "ERR" {
errResults = append(errResults, r)
}
}
fmt.Printf("# of ERR results: %d\n", len(errResults))
fmt.Println("=== Func error ===")
fmt.Printf("Error: %v\n", normalizeTransactionError(err))
fmt.Printf("Error is RPCError: %v\n", errors.Is(err, &surrealdb.RPCError{}))
fmt.Printf("Error is QueryError: %v\n", errors.Is(err, &surrealdb.QueryError{}))
for i, r := range errResults {
fmt.Printf("=== QueryResult[%d] ===\n", i)
fmt.Printf("Status: %v\n", r.Status)
fmt.Printf("Result: %v\n", r.Result)
fmt.Printf("Error: %v\n", normalizeTransactionError(r.Error))
fmt.Printf("Error is RPCError: %v\n", errors.Is(r.Error, &surrealdb.RPCError{}))
fmt.Printf("Error is QueryError: %v\n", errors.Is(r.Error, &surrealdb.QueryError{}))
}
}
Output: # of ERR results: 2 === Func error === Error: An error occurred: test The query was not executed due to a failed transaction Error is RPCError: false Error is QueryError: true === QueryResult[0] === Status: ERR Result: <nil> Error: An error occurred: test Error is RPCError: false Error is QueryError: true === QueryResult[1] === Status: ERR Result: <nil> Error: The query was not executed due to a failed transaction Error is RPCError: false Error is QueryError: true
func QueryRaw ¶ added in v0.3.0
QueryRaw composes a query from the provided QueryStmt objects, and execute it using the query RPC method. S can be *DB, *Session, or *Transaction.
You may want to use Query with github.com/surrealdb/surrealdb.go/contrib/surrealql instead.
func Relate ¶ added in v0.3.0
func Relate[TResult any, S sendable](ctx context.Context, s S, rel *Relationship) (*TResult, error)
Relate creates a relationship between two records in the table with a generated relationship ID. S can be *DB, *Session, or *Transaction.
The relation needs to be specified via the `Relation` field of the Relationship struct.
A relation is basically a table, so you can query it directly using SELECT if needed.
Although the Relationship struct allows you to specify the ID, it is ignored when you use Relate, and the ID is generated by SurrealDB.
In other words, Relationship.ID is meant for unmarshaling the relation from the database to the Relationship struct, in which case the ID is set to the ID of the relation record generated by SurrealDB.
In case you only care about the returned relationship's ID, use `connection.ResponseID[models.RecordID]` for the TResult type parameter.
Example ¶
package main
import (
"context"
"fmt"
"time"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/connection"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "query", "person", "follow")
type Person struct {
ID models.RecordID `json:"id,omitempty"`
}
type Follow struct {
In *models.RecordID `json:"in,omitempty"`
Out *models.RecordID `json:"out,omitempty"`
Since models.CustomDateTime `json:"since"`
}
first, err := surrealdb.Create[Person](
context.Background(),
db,
"person",
map[string]any{
"id": models.NewRecordID("person", "first"),
})
if err != nil {
panic(err)
}
second, err := surrealdb.Create[Person](
context.Background(),
db,
"person",
map[string]any{
"id": models.NewRecordID("person", "second"),
})
if err != nil {
panic(err)
}
since, err := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
if err != nil {
panic(err)
}
persons, err := surrealdb.Query[[]Person](
context.Background(),
db,
"SELECT * FROM person ORDER BY id.id",
nil,
)
if err != nil {
panic(err)
}
for _, person := range (*persons)[0].Result {
fmt.Printf("Person: %+v\n", person)
}
res, relateErr := surrealdb.Relate[connection.ResponseID[models.RecordID]](
context.Background(),
db,
&surrealdb.Relationship{
// ID is currently ignored, and the relation will have a generated ID.
// If you want to set the ID, use InsertRelation, or use
// Query with `RELATE` statement.
ID: &models.RecordID{Table: "follow", ID: "first_second"},
In: first.ID,
Out: second.ID,
Relation: "follow",
Data: map[string]any{
"since": models.CustomDateTime{
Time: since,
},
},
},
)
if relateErr != nil {
panic(relateErr)
}
if res == nil {
panic("relation response is nil")
}
if res.ID.ID == "first_second" {
panic("relation ID should not be set to 'first_second'")
}
//nolint:lll
/// Here's an alternative way to create a relation using a query.
//
// if res, err := surrealdb.Query[any](
// db,
// "RELATE $in->follow:first_second->$out SET since = $since",
// map[string]any{
// // `RELATE $in->follow->$out` with "id" below is ignored,
// // and the id becomes a generated one.
// // If you want to set the id, use `RELATE $in->follow:the_id->$out` like above.
// // "id": models.NewRecordID("follow", "first_second"),
// "in": first.ID,
// "out": second.ID,
// "since": models.CustomDateTime{Time: since},
// },
// ); err != nil {
// panic(err)
// } else {
// fmt.Printf("Relation: %+v\n", (*res)[0].Result)
// }
// The output will be:
// Relation: [map[id:{Table:follow ID:first_second} in:{Table:person ID:first} out:{Table:person ID:second} since:{Time:2023-10-01 12:00:00 +0000 UTC}]]
type PersonWithFollows struct {
Person
Follows []models.RecordID `json:"follows,omitempty"`
}
selected, err := surrealdb.Query[[]PersonWithFollows](
context.Background(),
db,
"SELECT id, name, ->follow->person AS follows FROM $id",
map[string]any{
"id": first.ID,
},
)
if err != nil {
panic(err)
}
for _, person := range (*selected)[0].Result {
fmt.Printf("PersonWithFollows: %+v\n", person)
}
// Note we can select the relationships themselves because
// RELATE creates a record in the relation table.
follows, err := surrealdb.Query[[]Follow](
context.Background(),
db,
"SELECT * from follow",
nil,
)
if err != nil {
panic(err)
}
for _, follow := range (*follows)[0].Result {
fmt.Printf("Follow: %+v\n", follow)
}
}
Output: Person: {ID:{Table:person ID:first}} Person: {ID:{Table:person ID:second}} PersonWithFollows: {Person:{ID:{Table:person ID:first}} Follows:[{Table:person ID:second}]} Follow: {In:person:first Out:person:second Since:{Time:2023-10-01 12:00:00 +0000 UTC}}
func Select ¶ added in v0.3.0
func Select[TResult any, TWhat TableOrRecord, S sendable](ctx context.Context, s S, what TWhat) (*TResult, error)
Select retrieves records from the database. S can be *DB, *Session, or *Transaction.
Example ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "updatedb", "person")
type Person struct {
ID models.RecordID `json:"id,omitempty"`
}
a := Person{ID: models.NewRecordID("person", "a")}
b := Person{ID: models.NewRecordID("person", "b")}
for _, p := range []Person{a, b} {
created, err := surrealdb.Create[Person](
context.Background(),
db,
p.ID,
map[string]any{},
)
if err != nil {
panic(err)
}
fmt.Printf("Created person: %+v\n", *created)
}
selectedOneUsingSelect, err := surrealdb.Select[Person](
context.Background(),
db,
a.ID,
)
if err != nil {
panic(err)
}
fmt.Printf("selectedOneUsingSelect: %+v\n", *selectedOneUsingSelect)
selectedMultiUsingSelect, err := surrealdb.Select[[]Person](
context.Background(),
db,
"person",
)
if err != nil {
panic(err)
}
for _, p := range *selectedMultiUsingSelect {
fmt.Printf("selectedMultiUsingSelect: %+v\n", p)
}
}
Output: Created person: {ID:{Table:person ID:a}} Created person: {ID:{Table:person ID:b}} selectedOneUsingSelect: {ID:{Table:person ID:a}} selectedMultiUsingSelect: {ID:{Table:person ID:a}} selectedMultiUsingSelect: {ID:{Table:person ID:b}}
Example (NonExistentRecord_fxamackercbor) ¶
ExampleSelect_nonExistentRecord_fxamackercbor demonstrates how fxamacker/cbor handles non-existent records - it returns a struct with non-nil CustomNil{} for missing pointer fields
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
c := testenv.MustNewConfig("example", "selectdb", "user")
c.CBORImpl = testenv.CBORImplFxamackerCBOR
db := c.MustNew()
ctx := context.Background()
// Create the table first - SurrealDB 3.x requires the table to exist
_, err := surrealdb.Query[any](ctx, db, `DEFINE TABLE user`, nil)
if err != nil {
panic(err)
}
type User struct {
ID *models.RecordID `json:"id,omitempty"`
Username string `json:"username"`
Email string `json:"email"`
}
// Try to select a record that doesn't exist
user, err := surrealdb.Select[User](ctx, db, models.NewRecordID("user", "does_not_exist"))
if err != nil {
panic(err)
}
// With fxamacker/cbor, non-existent records return a struct where:
// - Pointer fields that would be NONE become nil (behavior changed after v1.0.0)
// - This example shows the current behavior with fxamacker
fmt.Printf("User found: %t\n", user != nil)
fmt.Printf("User.ID is nil: %t\n", user.ID == nil)
fmt.Printf("User.ID type: %T\n", user.ID)
fmt.Printf("User.Username: %q\n", user.Username)
fmt.Printf("User.Email: %q\n", user.Email)
}
Output: User found: true User.ID is nil: true User.ID type: *models.RecordID User.Username: "" User.Email: ""
Example (NonExistentRecord_surrealcbor) ¶
ExampleSelect_nonExistentRecord_surrealcbor demonstrates how surrealcbor handles non-existent records - it returns nil
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
c := testenv.MustNewConfig("example", "selectdb", "user")
c.CBORImpl = testenv.CBORImplSurrealCBOR
db := c.MustNew()
ctx := context.Background()
// Create the table first - SurrealDB 3.x requires the table to exist
_, err := surrealdb.Query[any](ctx, db, `DEFINE TABLE user`, nil)
if err != nil {
panic(err)
}
type User struct {
ID *models.RecordID `json:"id,omitempty"`
Username string `json:"username"`
Email string `json:"email"`
}
// Try to select a record that doesn't exist
user, err := surrealdb.Select[User](ctx, db, models.NewRecordID("user", "does_not_exist"))
if err != nil {
panic(err)
}
// With surrealcbor, non-existent records return nil
// - This is why tests like s.Require().Nil(user) pass
fmt.Printf("User found: %t\n", user != nil)
}
Output: User found: false
func Send ¶ added in v0.7.0
func Send[Result any](ctx context.Context, db *DB, res *connection.RPCResponse[Result], method string, params ...any) error
Send sends a request to the SurrealDB server.
It is a wrapper around connection.Send, which is used by various RPC methods like Query, Insert and so on.
Compared to the original connection.Send, Send is smarter about methods that are allowed to be sent. You usually want to use this function than using connection.Send directly.
This function is limited to a selected set of RPC methods listed below:
- select - create - insert - insert_relation - kill - live - merge - relate - update - upsert - patch - delete - query
The `res` needs to be of type `*connection.RPCResponse[T]`.
It returns an error in the following cases: - Error if the method is not allowed to be sent, which means that the request was not even sent. - Transport error like WebSocket message write timeout, connection closed, etc. - Unmarshal error if the response cannot be unmarshaled into the provided res parameter. - RPCError if the request was processed by SurrealDB but it failed there.
Example (Select) ¶
Send can be used to any SurrealDB RPC method including "select".
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/connection"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
// Send can be used to any SurrealDB RPC method including "select".
func main() {
db := testenv.MustNew("surrealdbexamples", "updatedb", "person")
type Person struct {
ID models.RecordID `json:"id,omitempty"`
}
a := Person{ID: models.NewRecordID("person", "a")}
b := Person{ID: models.NewRecordID("person", "b")}
for _, p := range []Person{a, b} {
created, err := surrealdb.Create[Person](
context.Background(),
db,
p.ID,
map[string]any{},
)
if err != nil {
panic(err)
}
fmt.Printf("Created person: %+v\n", *created)
}
var selectedUsingSendSelect connection.RPCResponse[Person]
err := surrealdb.Send(
context.Background(),
db,
&selectedUsingSendSelect,
"select",
a.ID,
)
if err != nil {
panic(err)
}
fmt.Printf("selectedUsingSendSelect: %+v\n", *selectedUsingSendSelect.Result)
var selectedMultiUsingSendSelect connection.RPCResponse[[]Person]
err = surrealdb.Send(
context.Background(),
db,
&selectedMultiUsingSendSelect,
"select",
"person",
)
if err != nil {
panic(err)
}
for _, p := range *selectedMultiUsingSendSelect.Result {
fmt.Printf("selectedMultiUsingSendSelect: %+v\n", p)
}
var selectedOneUsingCustomSelect *Person
selectedOneUsingCustomSelect, err = customSelect[Person](db, a.ID)
if err != nil {
panic(err)
}
fmt.Printf("selectedOneUsingCustomSelect: %+v\n", *selectedOneUsingCustomSelect)
var selectedMultiUsingCustomSelect *[]Person
selectedMultiUsingCustomSelect, err = customSelect[[]Person](db, "person")
if err != nil {
panic(err)
}
for _, p := range *selectedMultiUsingCustomSelect {
fmt.Printf("selectedMultiUsingCustomSelect: %+v\n", p)
}
}
func customSelect[TResult any, TWhat surrealdb.TableOrRecord](db *surrealdb.DB, what TWhat) (*TResult, error) {
var res connection.RPCResponse[TResult]
if err := surrealdb.Send(context.Background(), db, &res, "select", what); err != nil {
return nil, err
}
return res.Result, nil
}
Output: Created person: {ID:{Table:person ID:a}} Created person: {ID:{Table:person ID:b}} selectedUsingSendSelect: {ID:{Table:person ID:a}} selectedMultiUsingSendSelect: {ID:{Table:person ID:a}} selectedMultiUsingSendSelect: {ID:{Table:person ID:b}} selectedOneUsingCustomSelect: {ID:{Table:person ID:a}} selectedMultiUsingCustomSelect: {ID:{Table:person ID:a}} selectedMultiUsingCustomSelect: {ID:{Table:person ID:b}}
func Update ¶ added in v0.3.0
func Update[TResult any, TWhat TableOrRecord, S sendable](ctx context.Context, s S, what TWhat, data any) (*TResult, error)
Update replaces a record in the database like a PUT request. S can be *DB, *Session, or *Transaction.
Example ¶
package main
import (
"context"
"fmt"
"time"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "updatedb", "persons")
type NestedStruct struct {
City string `json:"city"`
}
type Person struct {
ID *models.RecordID `json:"id,omitempty"`
Name string `json:"name"`
NestedMap map[string]any `json:"nested_map,omitempty"`
NestedStruct `json:"nested_struct,omitempty"`
CreatedAt models.CustomDateTime `json:"created_at,omitempty"`
UpdatedAt *models.CustomDateTime `json:"updated_at,omitempty"`
}
createdAt, err := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
if err != nil {
panic(err)
}
recordID := models.NewRecordID("persons", "yusuke")
created, err := surrealdb.Create[Person](context.Background(), db, recordID, map[string]any{
"name": "Yusuke",
"nested_struct": NestedStruct{
City: "Tokyo",
},
"created_at": models.CustomDateTime{
Time: createdAt,
},
})
if err != nil {
panic(err)
}
fmt.Printf("Created persons: %+v\n", *created)
updatedAt, err := time.Parse(time.RFC3339, "2023-10-02T12:00:00Z")
if err != nil {
panic(err)
}
updated, err := surrealdb.Update[Person](context.Background(), db, recordID, map[string]any{
"name": "Yusuke",
"nested_map": map[string]any{
"key1": "value1",
},
"nested_struct": NestedStruct{
City: "Kagawa",
},
"updated_at": models.CustomDateTime{
Time: updatedAt,
},
})
if err != nil {
panic(err)
}
fmt.Printf("Updated persons: %+v\n", *updated)
}
Output: Created persons: {ID:persons:yusuke Name:Yusuke NestedMap:map[] NestedStruct:{City:Tokyo} CreatedAt:{Time:2023-10-01 12:00:00 +0000 UTC} UpdatedAt:<nil>} Updated persons: {ID:persons:yusuke Name:Yusuke NestedMap:map[key1:value1] NestedStruct:{City:Kagawa} CreatedAt:{Time:0001-01-01 00:00:00 +0000 UTC} UpdatedAt:2023-10-02T12:00:00Z}
func Upsert ¶ added in v0.3.0
func Upsert[TResult any, TWhat TableOrRecord, S sendable](ctx context.Context, s S, what TWhat, data any) (*TResult, error)
Upsert creates or updates a record in the database. S can be *DB, *Session, or *Transaction.
Example ¶
package main
import (
"context"
"fmt"
"time"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "query", "persons")
type Person struct {
ID *models.RecordID `json:"id,omitempty"`
Name string `json:"name"`
// Note that you must use CustomDateTime instead of time.Time.
// See
CreatedAt models.CustomDateTime `json:"created_at,omitempty"`
UpdatedAt *models.CustomDateTime `json:"updated_at,omitempty"`
}
createdAt, err := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
if err != nil {
panic(err)
}
inserted, err := surrealdb.Upsert[Person](
context.Background(),
db,
models.NewRecordID("persons", "yusuke"),
map[string]any{
"name": "Yusuke",
"created_at": createdAt,
})
if err != nil {
panic(err)
}
fmt.Printf("Insert via upsert result: %v\n", *inserted)
updated, err := surrealdb.Upsert[Person](
context.Background(),
db,
models.NewRecordID("persons", "yusuke"),
map[string]any{
"name": "Yusuke Updated",
// because the upsert RPC is like UPSERT ~ CONTENT rather than UPSERT ~ MERGE,
// the created_at field becomes None, which results in the returned created_at field being zero value.
"updated_at": createdAt,
},
)
if err != nil {
panic(err)
}
fmt.Printf("Update via upsert result: %v\n", *updated)
udpatedAt, err := time.Parse(time.RFC3339, "2023-10-02T12:00:00Z")
if err != nil {
panic(err)
}
updatedFurther, err := surrealdb.Upsert[Person](
context.Background(),
db,
models.NewRecordID("persons", "yusuke"),
map[string]any{
"name": "Yusuke Updated Further",
"created_at": createdAt,
"updated_at": models.CustomDateTime{
Time: udpatedAt,
},
},
)
if err != nil {
panic(err)
}
fmt.Printf("Update further via upsert result: %v\n", *updatedFurther)
_, err = surrealdb.Upsert[struct{}](
context.Background(),
db,
models.NewRecordID("persons", "yusuke"),
map[string]any{
"name": "Yusuke Updated Last",
},
)
if err != nil {
panic(err)
}
selected, err := surrealdb.Select[Person](
context.Background(),
db,
models.NewRecordID("persons", "yusuke"),
)
if err != nil {
panic(err)
}
fmt.Printf("Selected person: %v\n", *selected)
}
Output: Insert via upsert result: {persons:yusuke Yusuke {2023-10-01 12:00:00 +0000 UTC} <nil>} Update via upsert result: {persons:yusuke Yusuke Updated {0001-01-01 00:00:00 +0000 UTC} 2023-10-01T12:00:00Z} Update further via upsert result: {persons:yusuke Yusuke Updated Further {2023-10-01 12:00:00 +0000 UTC} 2023-10-02T12:00:00Z} Selected person: {persons:yusuke Yusuke Updated Last {0001-01-01 00:00:00 +0000 UTC} <nil>}
Example (Rpc_error) ¶
package main
import (
"context"
"errors"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "query", "person")
type Person struct {
Name string `json:"name"`
}
// For this example, we will define a SCHEMAFUL table
// with a name field that is a string.
// Trying to set the name field to a number
// will result in an error from the database.
if _, err := surrealdb.Query[any](
context.Background(),
db,
`DEFINE TABLE person SCHEMAFUL;
DEFINE FIELD name ON person TYPE string;`,
nil,
); err != nil {
panic(err)
}
// This will fail because the record ID is not valid.
_, err := surrealdb.Upsert[Person](
context.Background(),
db,
models.Table("person"),
map[string]any{
"id": models.NewRecordID("person", "a"),
// Unlike ExampleUpsert_unmarshal_error,
// this will fail on the database side
// because the name field is defined as a string,
// and we are trying to set it to a number.
"name": 123,
},
)
if err != nil {
switch err.Error() {
// As of v3.0.0-alpha.7
case "There was a problem with the database: Couldn't coerce value for field `name` of `person:a`: Expected `string` but found `123`":
fmt.Println("Encountered expected error for SurrealDB 2.x or 3.x")
// As of v3.0.0-beta.2 (format changed)
case "Couldn't coerce value for field `name` of `person:a`: Expected `string` but found `123`":
fmt.Println("Encountered expected error for SurrealDB 2.x or 3.x")
// As of v2.3.7
case "There was a problem with the database: Found 123 for field `name`, with record `person:a`, but expected a string":
fmt.Println("Encountered expected error for SurrealDB 2.x or 3.x")
default:
fmt.Printf("Unknown Error: %v\n", err)
}
fmt.Printf("Error is RPCError: %v\n", errors.Is(err, &surrealdb.RPCError{}))
}
}
Output: Encountered expected error for SurrealDB 2.x or 3.x Error is RPCError: true
Example (Server_error) ¶
ExampleUpsert_server_error demonstrates extracting a *ServerError from an RPC error on SurrealDB v3 using errors.As.
On SurrealDB v2, the error is still an *RPCError (backward compatible), but errors.As(err, &se) also works because RPCError.Unwrap() returns a *ServerError. On v2 servers, se.Kind will be empty.
package main
import (
"context"
"errors"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
db := testenv.MustNew("surrealdbexamples", "server_error", "person")
type Person struct {
Name string `json:"name"`
}
if _, err := surrealdb.Query[any](
context.Background(),
db,
`DEFINE TABLE person SCHEMAFUL;
DEFINE FIELD name ON person TYPE string;`,
nil,
); err != nil {
panic(err)
}
_, err := surrealdb.Upsert[Person](
context.Background(),
db,
models.Table("person"),
map[string]any{
"id": models.NewRecordID("person", "a"),
"name": 123,
},
)
if err != nil {
// v2 backward compat: RPCError is still matchable
fmt.Printf("Error is RPCError: %v\n", errors.Is(err, &surrealdb.RPCError{}))
// v3 migration path: extract ServerError for structured info
fmt.Printf("Error is ServerError: %v\n", errors.Is(err, surrealdb.ServerError{}))
}
}
Output: Error is RPCError: true Error is ServerError: true
Example (Unmarshal_error_fxamackercbor_legacy_fxamackercbor) ¶
package main
import (
"context"
"errors"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
c := testenv.MustNewConfig("example", "query", "person")
c.CBORImpl = testenv.CBORImplFxamackerCBOR
db := c.MustNew()
type Person struct {
Name string `json:"name"`
}
// This will fail because the record ID is not valid.
_, err := surrealdb.Upsert[Person](
context.Background(),
db,
models.Table("person"),
map[string]any{
// We are trying to set the name field to a number,
// which is OK from the database's perspective,
// because the table is schemaless for this example.
//
// However, we are trying to unmarshal the result into a struct
// that expects the name field to be a string,
// which will fail when the result is unmarshaled.
"name": 123,
},
)
if err != nil {
fmt.Printf("Error: %v\n", err)
fmt.Printf("Error is RPCError: %v\n", errors.Is(err, &surrealdb.RPCError{}))
}
}
Output: Error: Send: error unmarshaling result: cbor: cannot unmarshal array into Go value of type surrealdb_test.Person (cannot decode CBOR array to struct without toarray option) Error is RPCError: false
Example (Unmarshal_error_surrealcbor) ¶
ExampleUpsert_unmarshal_error_surrealcbor demonstrates that surrealcbor fails unmarshaling CBOR array into Go struct with similar but different error message compared to fxamacker/cbor
package main
import (
"context"
"errors"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/models"
)
func main() {
c := testenv.MustNewConfig("example", "query", "person")
c.CBORImpl = testenv.CBORImplSurrealCBOR
db := c.MustNew()
type Person struct {
Name string `json:"name"`
}
// This will fail because the record ID is not valid.
_, err := surrealdb.Upsert[Person](
context.Background(),
db,
models.Table("person"),
map[string]any{
// We are trying to set the name field to a number,
// which is OK from the database's perspective,
// because the table is schemaless for this example.
//
// However, we are trying to unmarshal the result into a struct
// that expects the name field to be a string,
// which will fail when the result is unmarshaled.
"name": 123,
},
)
if err != nil {
fmt.Printf("Error: %v\n", err)
fmt.Printf("Error is RPCError: %v\n", errors.Is(err, &surrealdb.RPCError{}))
}
}
Output: Error: Send: error unmarshaling result: cannot decode array into surrealdb_test.Person Error is RPCError: false
Types ¶
type Auth ¶ added in v0.3.0
type Auth struct {
Namespace string `json:"NS,omitempty"`
Database string `json:"DB,omitempty"`
Scope string `json:"SC,omitempty"`
Access string `json:"AC,omitempty"`
Username string `json:"user,omitempty"`
Password string `json:"pass,omitempty"` //nolint:gosec // G117: user-supplied auth credential
}
Auth is a struct that holds surrealdb auth data for login.
type DB ¶
type DB struct {
// contains filtered or unexported fields
}
DB is a client for the SurrealDB database that holds the connection.
Example (Record_user_auth_struct) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
ns := "surrealdbexamples"
db := testenv.MustNew(ns, "record_auth_demo", "user")
setupQuery := `
-- Define the user table with schema
DEFINE TABLE user SCHEMAFULL
PERMISSIONS
FOR select, update, delete WHERE id = $auth.id;
-- Define fields
DEFINE FIELD name ON user TYPE string;
DEFINE FIELD password ON user TYPE string;
-- Define unique index on email
REMOVE INDEX IF EXISTS name ON user;
DEFINE INDEX name ON user FIELDS name UNIQUE;
-- Define access method for record authentication
REMOVE ACCESS IF EXISTS user ON DATABASE;
DEFINE ACCESS user ON DATABASE TYPE RECORD
SIGNIN (
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(password, $pass)
)
SIGNUP (
CREATE user CONTENT {
name: $user,
password: crypto::argon2::generate($pass)
}
);
`
if _, err := surrealdb.Query[any](context.Background(), db, setupQuery, nil); err != nil {
panic(err)
}
fmt.Println("Database schema setup complete")
// Refer to the next example, `ExampleDB_record_user_custom_struct`,
// when you need to use fields other than `user` and `pass` in the query specified for SIGNUP.
_, err := db.SignUp(context.Background(), &surrealdb.Auth{
Namespace: ns,
Database: "record_auth_demo",
Access: "user",
Username: "yusuke",
Password: "VerySecurePassword123!",
})
if err != nil {
panic(err)
}
fmt.Println("User signed up successfully")
// Refer to the next example, `ExampleDB_record_user_custom_struct`,
// when you need to use fields other than `user` and `pass` in the query specified for SIGNIN.
//
// For example, you might want to use `email` and `password` instead of `user` and `pass`.
// In that case, you need to something that encodes to a cbor map containing those keys.
_, err = db.SignIn(context.Background(), &surrealdb.Auth{
Namespace: ns,
Database: "record_auth_demo",
Access: "user",
Username: "yusuke",
Password: "VerySecurePassword123!",
})
if err != nil {
panic(err)
}
fmt.Println("User signed in successfully")
info, err := db.Info(context.Background())
if err != nil {
panic(err)
}
fmt.Printf("Authenticated user name: %v\n", info["name"])
}
Output: Database schema setup complete User signed up successfully User signed in successfully Authenticated user name: yusuke
Example (Record_user_custom_struct) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
ns := "surrealdbexamples"
db := testenv.MustNew(ns, "record_user_custom", "user")
setupQuery := `
-- Define the user table with schema
DEFINE TABLE user SCHEMAFULL
PERMISSIONS
FOR select, update, delete WHERE id = $auth.id;
-- Define fields
DEFINE FIELD name ON user TYPE string;
DEFINE FIELD email ON user TYPE string;
DEFINE FIELD password ON user TYPE string;
-- Define unique index on email
REMOVE INDEX IF EXISTS email ON user;
DEFINE INDEX email ON user FIELDS email UNIQUE;
-- Define access method for record authentication
REMOVE ACCESS IF EXISTS user ON DATABASE;
DEFINE ACCESS user ON DATABASE TYPE RECORD
SIGNIN (
SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(password, $password)
)
SIGNUP (
CREATE user CONTENT {
name: $name,
email: $email,
password: crypto::argon2::generate($password)
}
);
`
if _, err := surrealdb.Query[any](context.Background(), db, setupQuery, nil); err != nil {
panic(err)
}
fmt.Println("Database schema setup complete")
type User struct {
Namespace string `json:"NS"`
Database string `json:"DB"`
Access string `json:"AC"`
Name string `json:"name"`
Password string `json:"password"`
Email string `json:"email"`
}
type LoginRequest struct {
Namespace string `json:"NS"`
Database string `json:"DB"`
Access string `json:"AC"`
Email string `json:"email"`
Password string `json:"password"`
}
_, err := db.SignUp(context.Background(), &User{
// Corresponds to the SurrealDB namespace
Namespace: ns,
// Corresponds to the SurrealDB database
Database: "record_user_custom",
// Corresponds to `user` in `DEFINE ACCESS USER ON ...`
Access: "user",
// Corresponds to the $name in the SIGNUP query and `name` in `DEFINE FIELD name ON user`
Name: "yusuke",
// Corresponds to the $password in the SIGNUP query and `password` in `DEFINE FIELD password ON user`
Password: "VerySecurePassword123!",
// Corresponds to the $email in the SIGNUP query and `email` in `DEFINE FIELD email ON user`
Email: "yusuke@example.com",
})
if err != nil {
panic(err)
}
fmt.Println("User signed up successfully")
_, err = db.SignIn(context.Background(), &LoginRequest{
Namespace: ns,
Database: "record_user_custom",
Access: "user",
// Corresponds to the $email in the SIGNIN query and `email` in `DEFINE FIELD email ON user`
Email: "yusuke@example.com",
// Corresponds to the $password in the SIGNIN query and `password` in `DEFINE FIELD password ON user`
Password: "VerySecurePassword123!",
})
if err != nil {
panic(err)
}
fmt.Println("User signed in successfully")
info, err := db.Info(context.Background())
if err != nil {
panic(err)
}
fmt.Printf("Authenticated user name: %v\n", info["name"])
}
Output: Database schema setup complete User signed up successfully User signed in successfully Authenticated user name: yusuke
Example (Signin_failure) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
db, err := surrealdb.FromEndpointURLString(
context.Background(),
testenv.GetSurrealDBWSURL(),
)
if err != nil {
panic(err)
}
// Attempt to sign in without setting namespace or database
// This should fail with an error, whose message will depend on the connection type.
_, err = db.SignIn(context.Background(), surrealdb.Auth{
Username: "root",
Password: "invalid",
})
//nolint:goconst // Keeping error messages inline for readability in examples
switch err.Error() {
case "namespace or database or both are not set":
// In case the connection is over HTTP, this error is expected
case "There was a problem with the database: There was a problem with authentication":
// In case the connection is over WebSocket on SurrealDB 2.x, this error is expected
case "There was a problem with authentication":
// In case the connection is over WebSocket on SurrealDB 3.x, this error is expected
default:
panic(fmt.Sprintf("Unexpected error: %v", err))
}
err = db.Use(context.Background(), "testNS", "testDB")
if err != nil {
fmt.Println("Use error:", err)
}
// Even though the ns/db is set, the SignIn should still fail
// when the credentials are invalid.
_, err = db.SignIn(context.Background(), surrealdb.Auth{
Username: "root",
Password: "invalid",
})
// Normalize error message for version compatibility
// SurrealDB 2.x: "There was a problem with the database: There was a problem with authentication"
// SurrealDB 3.x: "There was a problem with authentication"
errMsg := err.Error()
//nolint:goconst // Keeping error messages inline for readability in examples
switch errMsg {
case "There was a problem with the database: There was a problem with authentication":
fmt.Println("SignIn error: authentication failed")
case "There was a problem with authentication":
fmt.Println("SignIn error: authentication failed")
default:
fmt.Println("SignIn error:", err)
}
// Now let's try with the correct credentials
// This should succeed if the database is set up correctly.
_, err = db.SignIn(context.Background(), surrealdb.Auth{
Username: "root",
Password: "root",
})
if err != nil {
panic(fmt.Sprintf("SignIn failed: %v", err))
}
if err := db.Close(context.Background()); err != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", err))
}
}
Output: SignIn error: authentication failed
func FromConnection ¶ added in v0.7.0
func FromConnection(ctx context.Context, conn connection.Connection) (*DB, error)
FromConnection creates a new SurrealDB client using the provided connection.
Note that this function calls `conn.Connect(ctx)` for you, so you don't need to call it manually.
Example (AlternativeCBORImpl_fxamackerCBOR) ¶
FromConnection can take any connection.Connection implementation with a custom connection.Config that can be used to specify a CBOR marshaler and unmarshaler. This example demonstrates how to explicitly use the legacy fxamacker/cbor implementation instead of the default surrealcbor implementation.
conf := connection.NewConfig(testenv.MustParseSurrealDBWSURL())
// To explicitly use the legacy fxamacker/cbor implementation,
// override the default surrealcbor with fxamacker-based marshalers.
// Note: fxamacker/cbor is deprecated in favor of surrealcbor.
conf.Marshaler = &models.CborMarshaler{} //nolint:staticcheck // Intentional use of deprecated type for example
conf.Unmarshaler = &models.CborUnmarshaler{} //nolint:staticcheck // Intentional use of deprecated type for example
conn := gws.New(conf)
db, err := surrealdb.FromConnection(context.Background(), conn)
if err != nil {
panic(err)
}
db, err = testenv.Init(db, "surrealdbexamples", "fxamackercbor", "user")
if err != nil {
panic(err)
}
// Define a sample struct
type User struct {
ID *models.RecordID `json:"id,omitempty"`
Name string `json:"name"`
Email string `json:"email"`
// Note that with fxamacker/cbor you need to use models.CustomDateTime
// instead of time.Time for proper datetime handling
CreatedAt models.CustomDateTime `json:"created_at"`
}
// Create a user
createdAt, _ := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
user := User{
Name: "Bob",
Email: "bob@example.com",
CreatedAt: models.CustomDateTime{Time: createdAt},
}
// Insert the user
created, err := surrealdb.Insert[User](context.Background(), db, "user", user)
if err != nil {
panic(err)
}
if created != nil && len(*created) > 0 {
fmt.Printf("Created user: %s with email: %s\n", (*created)[0].Name, (*created)[0].Email)
}
Output: Created user: Bob with email: bob@example.com
Example (AlternativeCBORImpl_surrealCBOR) ¶
FromConnection can take any connection.Connection implementation with a custom connection.Config that can be used to specify a CBOR marshaler and unmarshaler. This SDK has two built-in CBOR implementations: fxamacker/cbor-based one and the newer surrealcbor. surrealcbor is a more efficient and feature-rich implementation that is recommended for new projects.
conf := connection.NewConfig(testenv.MustParseSurrealDBWSURL())
// To enable surrealcbor, instantiate the codec
// and set it as the marshaler and unmarshaler.
codec := surrealcbor.New()
conf.Marshaler = codec
conf.Unmarshaler = codec
conn := gws.New(conf)
db, err := surrealdb.FromConnection(context.Background(), conn)
if err != nil {
panic(err)
}
db, err = testenv.Init(db, "surrealdbexamples", "surrealcbor", "user")
if err != nil {
panic(err)
}
// Define a sample struct
type User struct {
ID *models.RecordID `json:"id,omitempty"`
Name string `json:"name"`
Email string `json:"email"`
// Note that this had to be `CreatedAt models.CustomDateTime`
// with the previous fxamacker/cbor-based implementation.
CreatedAt time.Time `json:"created_at"`
}
// Create a user
createdAt, _ := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
user := User{
Name: "Alice",
Email: "alice@example.com",
CreatedAt: createdAt,
}
// Insert the user
created, err := surrealdb.Insert[User](context.Background(), db, "user", user)
if err != nil {
panic(err)
}
if created != nil && len(*created) > 0 {
fmt.Printf("Created user: %s with email: %s\n", (*created)[0].Name, (*created)[0].Email)
}
Output: Created user: Alice with email: alice@example.com
Example (AlternativeWebSocketLibrary_gws) ¶
FromConnection can take any connection.Connection implementation, including gws.Connection which is based on https://github.com/lxzan/gws.
package main
import (
"context"
"fmt"
"net/url"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/connection"
"github.com/surrealdb/surrealdb.go/pkg/connection/gws"
)
func main() {
u, err := url.ParseRequestURI(testenv.GetSurrealDBWSURL())
if err != nil {
panic(fmt.Sprintf("Failed to parse URL: %v", err))
}
conf := connection.NewConfig(u)
conf.Logger = nil // Disable logging for this example
conn := gws.New(conf)
db, err := surrealdb.FromConnection(context.Background(), conn)
fmt.Println("FromConnection error:", err)
// normalizeAuthError normalizes authentication error messages for version compatibility
// SurrealDB 2.x: "There was a problem with the database: There was a problem with authentication"
// SurrealDB 3.x: "There was a problem with authentication"
normalizeAuthError := func(err error) string {
if err == nil {
return "<nil>"
}
errMsg := err.Error()
//nolint:goconst // Keeping error messages inline for readability in examples
switch errMsg {
case "There was a problem with the database: There was a problem with authentication":
return "authentication failed"
case "There was a problem with authentication":
return "authentication failed"
}
return errMsg
}
// Attempt to sign in without setting namespace or database
// This should fail with an error, whose message will depend on the connection type.
_, err = db.SignIn(context.Background(), surrealdb.Auth{
Username: "root",
Password: "invalid",
})
fmt.Println("SignIn error:", normalizeAuthError(err))
err = db.Use(context.Background(), "testNS", "testDB")
fmt.Println("Use error:", err)
// Even though the ns/db is set, the SignIn should still fail
// when the credentials are invalid.
_, err = db.SignIn(context.Background(), surrealdb.Auth{
Username: "root",
Password: "invalid",
})
fmt.Println("SignIn error:", normalizeAuthError(err))
// Now let's try with the correct credentials
// This should succeed if the database is set up correctly.
_, err = db.SignIn(context.Background(), surrealdb.Auth{
Username: "root",
Password: "root",
})
fmt.Println("SignIn error:", normalizeAuthError(err))
err = db.Close(context.Background())
fmt.Println("Close error:", err)
}
Output: FromConnection error: <nil> SignIn error: authentication failed Use error: <nil> SignIn error: authentication failed SignIn error: <nil> Close error: <nil>
Example (CborUnmarshaler_decOptions_customSmallLimit) ¶
ExampleFromConnection_cborUnmarshaler_decOptions_customSmallLimit demonstrates what happens when a custom MaxArrayElements limit is set too low and the actual data exceeds that limit. The unmarshal operation fails with a clear error message.
// Parse the SurrealDB WebSocket URL
u, err := url.ParseRequestURI(testenv.GetSurrealDBWSURL())
if err != nil {
panic(fmt.Sprintf("Failed to parse URL: %v", err))
}
// First, create the record using default connection settings
{
conf := connection.NewConfig(u)
conf.Logger = nil
conn := gws.New(conf)
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
db, err := surrealdb.FromConnection(ctx, conn)
if err != nil {
panic(fmt.Sprintf("Failed to connect: %v", err))
}
defer db.Close(context.Background())
err = db.Use(ctx, "example", "test")
if err != nil {
panic(fmt.Sprintf("Failed to use namespace/database: %v", err))
}
_, err = db.SignIn(ctx, surrealdb.Auth{
Username: "root",
Password: "root",
})
if err != nil {
panic(fmt.Sprintf("SignIn failed: %v", err))
}
// Setup table and ensure it's clean before test
tableName := "test_small_limit"
setupTable(db, tableName)
createRecords(db, tableName, 20)
}
// Now try to retrieve with a connection that has a small array limit
{
conf := connection.NewConfig(u)
// Use TestLogHandler to see unmarshal errors but ignore debug and close errors
handler := testenv.NewTestLogHandlerWithOptions(
testenv.WithIgnoreErrorPrefixes("failed to close"),
testenv.WithIgnoreDebug(),
)
conf.Logger = logger.New(handler)
// Set a custom small limit that will be exceeded
// Note: fxamacker/cbor requires MaxArrayElements to be at least 16
conf.Unmarshaler = &models.CborUnmarshaler{ //nolint:staticcheck // Example demonstrating fxamacker/cbor DecOptions
DecOptions: cbor.DecOptions{
MaxArrayElements: 16, // Set to minimum allowed value
},
}
conn := gws.New(conf)
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
db, err := surrealdb.FromConnection(ctx, conn)
if err != nil {
panic(fmt.Sprintf("Failed to connect: %v", err))
}
defer db.Close(context.Background())
err = db.Use(ctx, "example", "test")
if err != nil {
panic(fmt.Sprintf("Failed to use namespace/database: %v", err))
}
_, err = db.SignIn(ctx, surrealdb.Auth{
Username: "root",
Password: "root",
})
if err != nil {
panic(fmt.Sprintf("SignIn failed: %v", err))
}
// This should fail due to array limit
tableName := "test_small_limit"
selectRecords(db, tableName)
}
Output: Table test_small_limit cleaned up Successfully created record with 20 items [0] ERROR: Failed to unmarshal response error=cbor: exceeded max number of elements 16 for CBOR array Error retrieving record: context deadline exceeded
Example (CborUnmarshaler_decOptions_defaultLimit) ¶
ExampleFromConnection_cborUnmarshaler_decOptions_defaultLimit demonstrates that the default CBOR decoder configuration works fine with small arrays that are well within the default limit of 131,072 elements.
package main
import (
"context"
"fmt"
"net/url"
"time"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
"github.com/surrealdb/surrealdb.go/pkg/connection"
"github.com/surrealdb/surrealdb.go/pkg/connection/gorillaws"
)
func main() {
// Parse the SurrealDB WebSocket URL
u, err := url.ParseRequestURI(testenv.GetSurrealDBWSURL())
if err != nil {
panic(fmt.Sprintf("Failed to parse URL: %v", err))
}
// Setup connection with default configuration
conf := connection.NewConfig(u)
conf.Logger = nil
conn := gorillaws.New(conf)
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
db, err := surrealdb.FromConnection(ctx, conn)
if err != nil {
panic(fmt.Sprintf("Failed to connect: %v", err))
}
defer db.Close(context.Background())
err = db.Use(ctx, "example", "test")
if err != nil {
panic(fmt.Sprintf("Failed to use namespace/database: %v", err))
}
_, err = db.SignIn(ctx, surrealdb.Auth{
Username: "root",
Password: "root",
})
if err != nil {
panic(fmt.Sprintf("SignIn failed: %v", err))
}
// Setup table and ensure it's clean before test
tableName := "test_default_limit"
setupTable(db, tableName)
// Default settings work with small arrays
createRecords(db, tableName, 10)
selectRecords(db, tableName)
}
// setupTable prepares a clean table for testing by deleting any existing records
func setupTable(db *surrealdb.DB, tableName string) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
_, _ = surrealdb.Query[any](ctx, db, fmt.Sprintf("DELETE %s", tableName), nil)
fmt.Printf("Table %s cleaned up\n", tableName)
}
// createRecords creates a test record in the specified table
func createRecords(db *surrealdb.DB, tableName string, arraySize int) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
items := make([]string, arraySize)
for i := range arraySize {
items[i] = fmt.Sprintf("item_%d", i)
}
_, err := surrealdb.Query[any](ctx, db, fmt.Sprintf("CREATE %s SET items = $items", tableName), map[string]any{
"items": items,
})
if err != nil {
fmt.Printf("Error creating record: %v\n", err)
} else {
fmt.Printf("Successfully created record with %d items\n", arraySize)
}
}
func selectRecords(db *surrealdb.DB, tableName string) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
type TestRecord struct {
ID any `json:"id"`
Items []string `json:"items"`
}
result, err := surrealdb.Query[[]TestRecord](ctx, db, fmt.Sprintf("SELECT * FROM %s", tableName), nil)
if err != nil {
fmt.Printf("Error retrieving record: %v\n", err)
return
}
if result != nil && len(*result) > 0 && len((*result)[0].Result) > 0 {
recordCount := len((*result)[0].Result)
if recordCount > 0 && (*result)[0].Result[0].Items != nil {
fmt.Printf("Successfully retrieved record with %d items\n", len((*result)[0].Result[0].Items))
}
}
}
Output: Table test_default_limit cleaned up Successfully created record with 10 items Successfully retrieved record with 10 items
func FromEndpointURLString ¶ added in v0.7.0
FromEndpointURLString creates a new SurrealDB client and connects to the database.
This function incurs a network call (currently HTTP request) to the SurrealDB server to check the health of the connection in case of HTTP, or to establish a WebSocket connection in case of WebSocket.
The provided `ctx` is used to cancel the connection attempt if needed, so that you control how long you want to block in case the network is not reliable or any other issues like OS network stack issues/settings/etc.
Connection Engines ¶
There are 2 different connection engines you can use to connect to SurrealDb backend. You can do so via Websocket or through HTTP connections
Via WebSocket ¶
WebSocket is required when using live queries.
db, err := surrealdb.FromEndpointURLString(ctx, "ws://localhost:8000")
or for a secure connection
db, err := surrealdb.FromEndpointURLString(ctx, "wss://localhost:8000")
Via HTTP ¶
There are some functions that are not available on RPC when using HTTP but on WebSocket.
All these except the "live" endpoint are effectively implemented in the HTTP library and provides the same result as though it is natively available on HTTP.
db, err := surrealdb.FromEndpointURLString(ctx, "http://localhost:8000")
or for a secure connection
db, err := surrealdb.FromEndpointURLString(ctx, "https://localhost:8000")
func (*DB) Attach ¶ added in v1.3.0
Attach creates a new session on the WebSocket connection. Sessions are only supported on WebSocket connections (SurrealDB v3+).
The new session starts unauthenticated and without a selected namespace/database. You must call SignIn/Authenticate and Use on the session before making queries.
Example:
session, err := db.Attach(ctx)
if err != nil {
return err
}
defer session.Detach(ctx)
// Authenticate the session
_, err = session.SignIn(ctx, Auth{Username: "root", Password: "root"})
if err != nil {
return err
}
// Select namespace and database
err = session.Use(ctx, "test", "test")
if err != nil {
return err
}
// Now the session is ready for queries
results, err := surrealdb.Query[[]User](ctx, session, "SELECT * FROM users", nil)
Example ¶
ExampleDB_Attach demonstrates creating and using an additional session. Sessions allow independent authentication, namespace selection, and variable scope. This feature requires SurrealDB v3+ and WebSocket connections.
package main
import (
"context"
"fmt"
"log"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
// Skip if not v3+ (this is for documentation purposes)
ctx := context.Background()
// Connect using WebSocket (sessions require WebSocket)
db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
log.Fatal(err)
}
defer func() {
if closeErr := db.Close(ctx); closeErr != nil {
log.Printf("Failed to close db: %v", closeErr)
}
}()
// Sign in as root on the main connection
_, err = db.SignIn(ctx, map[string]any{"user": "root", "pass": "root"})
if err != nil {
log.Fatal(err) //nolint:gocritic // Example code - log.Fatal is acceptable
}
// Create an additional session
session, err := db.Attach(ctx)
if err != nil {
log.Fatal(err)
}
defer func() { _ = session.Detach(ctx) }()
fmt.Printf("Session created with ID: %s\n", session.ID())
// The session starts unauthenticated - sign in and select namespace/database
_, err = session.SignIn(ctx, map[string]any{"user": "root", "pass": "root"})
if err != nil {
log.Fatal(err)
}
err = session.Use(ctx, "test", "test")
if err != nil {
log.Fatal(err)
}
// Set a session-scoped variable
err = session.Let(ctx, "user_id", "user123")
if err != nil {
log.Fatal(err)
}
// Query using the session - the variable is available
type Result struct {
UserID string `json:"user_id"`
}
results, err := surrealdb.Query[Result](ctx, session, "RETURN $user_id", nil)
if err != nil {
log.Fatal(err)
}
if len(*results) > 0 {
fmt.Printf("Session variable $user_id: %s\n", (*results)[0].Result)
}
// Note: This example requires SurrealDB v3+ and will fail on earlier versions.
// Output is not verified because session IDs are dynamic.
}
Output:
func (*DB) Authenticate ¶
Authenticate authenticates the current connection with the provided token.
This is mostly useful when you created a JWT authentication method on SurrealDB using `DEFINE ACCESS ... TYPE JWT` query, so that SurrealDB can verify the token provided via this method for authentication.
After calling this method, all subsequent requests will be authenticated. How the authentication is maintained depends on the connection type:
For WebSocket connections, the token is kept in the session on the server side. This means connecting to the server again or creating a new connection to another server will require calling this method again to authenticate.
For HTTP connections, the token is sent with every request via the `Authorization` header. This means that even if you create a new connection to another server, as long as you call this method on the new connection, the requests will be authenticated.
Example (Jwt_databaseLevelUser) ¶
nolint:gocyclo // Example covers end-to-end JWT setup; splitting would reduce readability for docs
ctx := context.Background()
// Generate ECDSA key pair using Go standard library
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(fmt.Sprintf("Failed to generate private key: %v", err))
}
// Extract public key and encode it to PEM format
publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
if err != nil {
panic(fmt.Sprintf("Failed to marshal public key: %v", err))
}
publicKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: publicKeyBytes,
})
// Connect to SurrealDB and authenticate as root
db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
panic(err)
}
db, err = testenv.Init(db, "exampledb_authenticate_jwt", "testdb", "user")
if err != nil {
panic(err)
}
// Sign in as root to set up the JWT access method
_, err = db.SignIn(ctx, surrealdb.Auth{
Username: "root",
Password: "root",
})
if err != nil {
panic(fmt.Sprintf("SignIn as root failed: %v", err))
}
err = db.Use(ctx, "exampledb_authenticate_jwt", "testdb")
if err != nil {
panic(fmt.Sprintf("Use failed: %v", err))
}
// Remove any existing access method first
_, err = surrealdb.Query[any](ctx, db, `REMOVE ACCESS IF EXISTS jwt_access ON DATABASE`, nil)
if err != nil {
panic(fmt.Sprintf("Failed to remove existing JWT access: %v", err))
}
// Define a JWT access method with the public key
defineAccessQuery := fmt.Sprintf(`
DEFINE ACCESS jwt_access ON DATABASE TYPE JWT
ALGORITHM ES256 KEY '%s'
`, string(publicKeyPEM))
_, err = surrealdb.Query[any](ctx, db, defineAccessQuery, nil)
if err != nil {
panic(fmt.Sprintf("Failed to define JWT access: %v", err))
}
// Create the user table and a test user record for SurrealDB 3.x
// SurrealDB 3.x requires the table to exist before querying,
// while SurrealDB 2.x does not.
_, err = surrealdb.Query[any](ctx, db, `
DEFINE TABLE user SCHEMAFULL;
DEFINE FIELD name ON user TYPE string;
CREATE user:test SET name = "test_user"
`, nil)
if err != nil {
panic(fmt.Sprintf("Failed to create test user: %v", err))
}
// Create a signed JWT token using the private key
// Only the required claims for database-level JWT access
// See: https://surrealdb.com/docs/surrealql/statements/define/access/jwt#using-tokens
claims := jwt.MapClaims{
"exp": time.Now().Add(1 * time.Hour).Unix(), // Token expiration
"ac": "jwt_access", // Access method name
"ns": "exampledb_authenticate_jwt", // Namespace
"db": "testdb", // Database
}
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
signedToken, err := token.SignedString(privateKey)
if err != nil {
panic(fmt.Sprintf("Failed to sign JWT token: %v", err))
}
// Close the root connection
if closeErr := db.Close(ctx); closeErr != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}
// Create a new connection and authenticate with the JWT token
db, err = surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
panic(err)
}
// Authenticate using the JWT token via the Authenticate method
// The JWT contains ns and db claims, so we don't need to call Use() first
err = db.Authenticate(ctx, signedToken)
if err != nil {
panic(fmt.Sprintf("Authenticate with JWT failed: %v", err))
}
// Verify authentication by performing a query
results, err := surrealdb.Query[any](ctx, db, `SELECT * FROM $id`, map[string]any{
"id": models.NewRecordID("user", "test"),
})
if err != nil {
panic(fmt.Sprintf("Query after JWT authentication failed: %v", err))
}
if results == nil || len(*results) == 0 {
panic("Expected query results after JWT authentication")
}
if closeErr := db.Close(ctx); closeErr != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}
fmt.Println("JWT-based authentication completed successfully")
Output: JWT-based authentication completed successfully
Example (Jwt_hs512_databaseLevelUser) ¶
ctx := context.Background()
// Generate a symmetric key for HS512 (HMAC-SHA512)
// Use a strong random string as the symmetric key
symmetricKeyString := "sNSYneezcr8kqphfOC6NwwraUHJCVAt0XjsRSNmssBaBRh3WyMa9TRfq8ST7fsU2H2kGiOpU4GbAF1bCiXmM1b3JGgleBzz7rsrz6VvYEM4q3CLkcO8CMBIlhwhzWmy8" //nolint:goconst // duplicated across examples intentionally for self-contained docs
// Connect to SurrealDB and authenticate as root
db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
panic(err)
}
db, err = testenv.Init(db, "exampledb_authenticate_jwt_hs512", "testdb", "user")
if err != nil {
panic(err)
}
// Sign in as root to set up the JWT access method
_, err = db.SignIn(ctx, surrealdb.Auth{
Username: "root",
Password: "root",
})
if err != nil {
panic(fmt.Sprintf("SignIn as root failed: %v", err))
}
err = db.Use(ctx, "exampledb_authenticate_jwt_hs512", "testdb")
if err != nil {
panic(fmt.Sprintf("Use failed: %v", err))
}
// Remove any existing access method first
_, err = surrealdb.Query[any](ctx, db, `REMOVE ACCESS IF EXISTS jwt_hs512 ON DATABASE`, nil)
if err != nil {
panic(fmt.Sprintf("Failed to remove existing JWT access: %v", err))
}
// Define a JWT access method with HS512 and the symmetric key
// See: https://surrealdb.com/docs/surrealql/statements/define/access/jwt#database
defineAccessQuery := fmt.Sprintf(`
DEFINE ACCESS jwt_hs512 ON DATABASE TYPE JWT
ALGORITHM HS512 KEY '%s'
`, symmetricKeyString)
_, err = surrealdb.Query[any](ctx, db, defineAccessQuery, nil)
if err != nil {
panic(fmt.Sprintf("Failed to define JWT access: %v", err))
}
// Create the user table and a test user record for SurrealDB 3.x
// SurrealDB 3.x requires the table to exist before querying,
// while SurrealDB 2.x does not.
_, err = surrealdb.Query[any](ctx, db, `
DEFINE TABLE user SCHEMAFULL;
DEFINE FIELD name ON user TYPE string;
CREATE user:test SET name = "test_user"
`, nil)
if err != nil {
panic(fmt.Sprintf("Failed to create test user: %v", err))
}
// Create a signed JWT token using the symmetric key
// Only the required claims for database-level JWT access
claims := jwt.MapClaims{
"exp": time.Now().Add(1 * time.Hour).Unix(), // Token expiration
"ac": "jwt_hs512", // Access method name
"ns": "exampledb_authenticate_jwt_hs512", // Namespace
"db": "testdb", // Database
}
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
signedToken, err := token.SignedString([]byte(symmetricKeyString))
if err != nil {
panic(fmt.Sprintf("Failed to sign JWT token: %v", err))
}
// Close the root connection
if closeErr := db.Close(ctx); closeErr != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}
// Create a new connection and authenticate with the JWT token
db, err = surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
panic(err)
}
// Authenticate using the JWT token via the Authenticate method
// The JWT contains ns and db claims, so we don't need to call Use() first
err = db.Authenticate(ctx, signedToken)
if err != nil {
panic(fmt.Sprintf("Authenticate with JWT failed: %v", err))
}
// Verify authentication by performing a query
results, err := surrealdb.Query[any](ctx, db, `SELECT * FROM $id`, map[string]any{
"id": models.NewRecordID("user", "test"),
})
if err != nil {
panic(fmt.Sprintf("Query after JWT authentication failed: %v", err))
}
if results == nil || len(*results) == 0 {
panic("Expected query results after JWT authentication")
}
if closeErr := db.Close(ctx); closeErr != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}
fmt.Println("JWT HS512 authentication completed successfully")
Output: JWT HS512 authentication completed successfully
Example (Jwt_hs512_namespaceLevelUser) ¶
nolint:gocyclo // Example shows full flow for namespace-level JWT auth
ctx := context.Background()
// Symmetric key for HS512 (HMAC-SHA512)
symmetricKeyString := "sNSYneezcr8kqphfOC6NwwraUHJCVAt0XjsRSNmssBaBRh3WyMa9TRfq8ST7fsU2H2kGiOpU4GbAF1bCiXmM1b3JGgleBzz7rsrz6VvYEM4q3CLkcO8CMBIlhwhzWmy8" //nolint:goconst // duplicated across examples intentionally for self-contained docs
// Names for this test
ns := "exampledb_authenticate_jwt_hs512_ns"
accessName := "jwt_hs512_ns"
// Connect to SurrealDB and authenticate as root
db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
panic(err)
}
db, err = testenv.Init(db, ns, "testdb")
if err != nil {
panic(err)
}
// Sign in as root to set up the JWT access method
_, err = db.SignIn(ctx, surrealdb.Auth{
Username: "root",
Password: "root",
})
if err != nil {
panic(fmt.Sprintf("SignIn as root failed: %v", err))
}
// Select namespace (database may be required by helper; safe to set anyway)
err = db.Use(ctx, ns, "testdb")
if err != nil {
panic(fmt.Sprintf("Use failed: %v", err))
}
// Remove any existing access method first (namespace level)
_, err = surrealdb.Query[any](ctx, db, `REMOVE ACCESS IF EXISTS jwt_hs512_ns ON NAMESPACE`, nil)
if err != nil {
panic(fmt.Sprintf("Failed to remove existing JWT access: %v", err))
}
// Define a JWT access method for namespace level with HS512
defineAccessQuery := fmt.Sprintf(`
DEFINE ACCESS %s ON NAMESPACE TYPE JWT
ALGORITHM HS512 KEY '%s'
`, accessName, symmetricKeyString)
_, err = surrealdb.Query[any](ctx, db, defineAccessQuery, nil)
if err != nil {
panic(fmt.Sprintf("Failed to define JWT access: %v", err))
}
// Create a database and user table/record for the namespace-level auth test
// SurrealDB 3.x requires the table to exist before querying,
// while SurrealDB 2.x does not.
// First remove the database if it exists to ensure clean state
_, _ = surrealdb.Query[any](ctx, db, `REMOVE DATABASE IF EXISTS testdb`, nil)
_, err = surrealdb.Query[any](ctx, db, `
DEFINE DATABASE testdb;
USE DB testdb;
DEFINE TABLE user SCHEMAFULL;
DEFINE FIELD name ON user TYPE string;
CREATE user:test SET name = "test_user"
`, nil)
if err != nil {
panic(fmt.Sprintf("Failed to create test user: %v", err))
}
// Create a signed JWT token using the symmetric key (namespace-level claims)
claims := jwt.MapClaims{
"exp": time.Now().Add(1 * time.Hour).Unix(),
"ac": accessName,
"ns": ns,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
signedToken, err := token.SignedString([]byte(symmetricKeyString))
if err != nil {
panic(fmt.Sprintf("Failed to sign JWT token: %v", err))
}
// Close the root connection
if closeErr := db.Close(ctx); closeErr != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}
// Create a new connection and authenticate with the JWT token
db, err = surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
panic(err)
}
// Authenticate using the JWT token via the Authenticate method
err = db.Authenticate(ctx, signedToken)
if err != nil {
panic(fmt.Sprintf("Authenticate with JWT failed: %v", err))
}
// For namespace-level tokens, select a database to run queries
err = db.Use(ctx, ns, "testdb")
if err != nil {
panic(fmt.Sprintf("Use failed after JWT auth: %v", err))
}
// Verify authentication by performing a query
results, err := surrealdb.Query[any](ctx, db, `SELECT * FROM $id`, map[string]any{
"id": models.NewRecordID("user", "test"),
})
if err != nil {
panic(fmt.Sprintf("Query after JWT authentication failed: %v", err))
}
if results == nil || len(*results) == 0 {
panic("Expected query results after JWT authentication")
}
if closeErr := db.Close(ctx); closeErr != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}
fmt.Println("JWT HS512 namespace-level authentication completed successfully")
Output: JWT HS512 namespace-level authentication completed successfully
Example (Jwt_hs512_rootLevelUser) ¶
nolint:gocyclo // Example shows full flow for root-level JWT auth
ctx := context.Background()
symmetricKeyString := "sNSYneezcr8kqphfOC6NwwraUHJCVAt0XjsRSNmssBaBRh3WyMa9TRfq8ST7fsU2H2kGiOpU4GbAF1bCiXmM1b3JGgleBzz7rsrz6VvYEM4q3CLkcO8CMBIlhwhzWmy8" //nolint:goconst // duplicated across examples intentionally for self-contained docs
accessName := "jwt_hs512_root"
ns := "exampledb_authenticate_jwt_hs512_root"
// Admin connection
db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
panic(err)
}
db, err = testenv.Init(db, ns, "testdb")
if err != nil {
panic(err)
}
if _, err = db.SignIn(ctx, surrealdb.Auth{Username: "root", Password: "root"}); err != nil {
panic(fmt.Sprintf("SignIn as root failed: %v", err))
}
err = db.Use(ctx, ns, "testdb")
if err != nil {
panic(fmt.Sprintf("Use failed: %v", err))
}
_, err = surrealdb.Query[any](ctx, db, `REMOVE ACCESS IF EXISTS jwt_hs512_root ON ROOT`, nil)
if err != nil {
panic(fmt.Sprintf("Failed to remove existing JWT access: %v", err))
}
defineAccessQuery := fmt.Sprintf(`
DEFINE ACCESS %s ON ROOT TYPE JWT
ALGORITHM HS512 KEY '%s'
`, accessName, symmetricKeyString)
_, err = surrealdb.Query[any](ctx, db, defineAccessQuery, nil)
if err != nil {
panic(fmt.Sprintf("Failed to define JWT access: %v", err))
}
// Create the namespace, database, table, and test user for root-level auth test
// SurrealDB 3.x requires the table to exist before querying,
// while SurrealDB 2.x does not.
// First remove namespace if it exists to ensure clean state
_, _ = surrealdb.Query[any](ctx, db, fmt.Sprintf(`REMOVE NAMESPACE IF EXISTS %s`, ns), nil)
_, err = surrealdb.Query[any](ctx, db, fmt.Sprintf(`
DEFINE NAMESPACE %s;
USE NS %s;
DEFINE DATABASE testdb;
USE DB testdb;
DEFINE TABLE user SCHEMAFULL;
DEFINE FIELD name ON user TYPE string;
CREATE user:test SET name = "test_user"
`, ns, ns), nil)
if err != nil {
panic(fmt.Sprintf("Failed to create test user: %v", err))
}
// Root-level token claims (no ns/db)
claims := jwt.MapClaims{
"exp": time.Now().Add(1 * time.Hour).Unix(),
"ac": accessName,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
signedToken, err := token.SignedString([]byte(symmetricKeyString))
if err != nil {
panic(fmt.Sprintf("Failed to sign JWT token: %v", err))
}
if closeErr := db.Close(ctx); closeErr != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}
// Authenticate with root-level token
db, err = surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
panic(err)
}
err = db.Authenticate(ctx, signedToken)
if err != nil {
panic(fmt.Sprintf("Authenticate with JWT failed: %v", err))
}
// Choose ns/db for subsequent queries
err = db.Use(ctx, ns, "testdb")
if err != nil {
panic(fmt.Sprintf("Use failed after JWT auth: %v", err))
}
results, err := surrealdb.Query[any](ctx, db, `SELECT * FROM $id`, map[string]any{
"id": models.NewRecordID("user", "test"),
})
if err != nil {
panic(fmt.Sprintf("Query after JWT authentication failed: %v", err))
}
if results == nil || len(*results) == 0 {
panic("Expected query results after JWT authentication")
}
if closeErr := db.Close(ctx); closeErr != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}
fmt.Println("JWT HS512 root-level authentication completed successfully")
Output: JWT HS512 root-level authentication completed successfully
Example (Jwt_hs512_rootLevelUser_expired) ¶
ctx := context.Background()
symmetricKeyString := "sNSYneezcr8kqphfOC6NwwraUHJCVAt0XjsRSNmssBaBRh3WyMa9TRfq8ST7fsU2H2kGiOpU4GbAF1bCiXmM1b3JGgleBzz7rsrz6VvYEM4q3CLkcO8CMBIlhwhzWmy8" //nolint:goconst // duplicated across examples intentionally for self-contained docs
accessName := "jwt_hs512_root_expired"
// Admin connection
db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
panic(err)
}
// Use a dedicated namespace for this test
ns := "exampledb_authenticate_jwt_hs512_root_expired"
db, err = testenv.Init(db, ns, "testdb")
if err != nil {
panic(err)
}
if _, err = db.SignIn(ctx, surrealdb.Auth{Username: "root", Password: "root"}); err != nil {
panic(fmt.Sprintf("SignIn as root failed: %v", err))
}
err = db.Use(ctx, ns, "testdb")
if err != nil {
panic(fmt.Sprintf("Use failed: %v", err))
}
_, err = surrealdb.Query[any](ctx, db, `REMOVE ACCESS IF EXISTS jwt_hs512_root_expired ON ROOT`, nil)
if err != nil {
panic(fmt.Sprintf("Failed to remove existing JWT access: %v", err))
}
defineAccessQuery := fmt.Sprintf(`
DEFINE ACCESS %s ON ROOT TYPE JWT
ALGORITHM HS512 KEY '%s'
`, accessName, symmetricKeyString)
_, err = surrealdb.Query[any](ctx, db, defineAccessQuery, nil)
if err != nil {
panic(fmt.Sprintf("Failed to define JWT access: %v", err))
}
// Expired token (exp in the past)
claims := jwt.MapClaims{
"exp": time.Now().Add(-1 * time.Hour).Unix(),
"ac": accessName,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
signedToken, err := token.SignedString([]byte(symmetricKeyString))
if err != nil {
panic(fmt.Sprintf("Failed to sign JWT token: %v", err))
}
if closeErr := db.Close(ctx); closeErr != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}
// Try authenticating with expired token - should fail
db, err = surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
panic(err)
}
err = db.Authenticate(ctx, signedToken)
if err != nil {
// Expected failure path
fmt.Println("JWT HS512 root-level expired authentication failed as expected")
return
}
// If we reached here, authentication incorrectly succeeded
panic("Expected Authenticate to fail with expired token, but it succeeded")
Output: JWT HS512 root-level expired authentication failed as expected
func (*DB) Begin ¶ added in v1.3.0
func (db *DB) Begin(ctx context.Context) (*Transaction, error)
Begin starts a new interactive transaction on the default session. Interactive transactions are only supported on WebSocket connections (SurrealDB v3+).
Example:
tx, err := db.Begin(ctx)
if err != nil {
return err
}
defer tx.Cancel(ctx) // Cancel if not committed
// Execute queries within the transaction
_, err = surrealdb.Query[[]any](ctx, tx, "CREATE user:1 SET name = 'Alice'", nil)
if err != nil {
return err
}
// Commit the transaction
return tx.Commit(ctx)
Example ¶
ExampleDB_Begin demonstrates starting an interactive transaction. Interactive transactions allow executing statements one at a time and conditionally committing or canceling based on results. This feature requires SurrealDB v3+ and WebSocket connections.
package main
import (
"context"
"fmt"
"log"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
ctx := context.Background()
// Connect using WebSocket (transactions require WebSocket)
db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
log.Fatal(err)
}
defer func() {
if closeErr := db.Close(ctx); closeErr != nil {
log.Printf("Failed to close db: %v", closeErr)
}
}()
// Sign in and select namespace/database
_, err = db.SignIn(ctx, map[string]any{"user": "root", "pass": "root"})
if err != nil {
log.Fatal(err) //nolint:gocritic // Example code - log.Fatal is acceptable
}
err = db.Use(ctx, "test", "test")
if err != nil {
log.Fatal(err) //nolint:gocritic // Example code - log.Fatal is acceptable
}
// Start an interactive transaction
tx, err := db.Begin(ctx)
if err != nil {
log.Fatal(err)
}
// Always clean up if not committed
defer func() {
if !tx.IsClosed() {
_ = tx.Cancel(ctx)
}
}()
fmt.Printf("Transaction started with ID: %s\n", tx.ID())
// Perform operations within the transaction
type Product struct {
ID string `json:"id"`
Name string `json:"name"`
Stock int `json:"stock"`
}
// Create a product
_, err = surrealdb.Query[[]Product](ctx, tx,
"CREATE products:widget SET name = 'Widget', stock = 100", nil)
if err != nil {
log.Fatal(err)
}
// Query within the same transaction - changes are visible
results, err := surrealdb.Query[[]Product](ctx, tx,
"SELECT * FROM products:widget", nil)
if err != nil {
log.Fatal(err)
}
if len(*results) > 0 && len((*results)[0].Result) > 0 {
fmt.Printf("Product in transaction: %s (stock: %d)\n",
(*results)[0].Result[0].Name,
(*results)[0].Result[0].Stock)
}
// Commit the transaction to persist changes
err = tx.Commit(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println("Transaction committed")
// Note: This example requires SurrealDB v3+ and will fail on earlier versions.
// Output is not verified because transaction IDs are dynamic.
}
Output:
func (*DB) CloseLiveNotifications ¶ added in v0.10.0
func (*DB) LiveNotifications ¶ added in v0.3.0
func (db *DB) LiveNotifications(liveQueryID string) (chan connection.Notification, error)
func (*DB) SignIn ¶ added in v0.3.0
SignIn signs in an existing user.
The authData parameter can be either:
- An Auth struct
- A map[string]any with keys like: "namespace", "database", "scope", "user", "pass"
In either case, the username and the password are mandatory. Depending on whether namespace and database are provided or not, the user is signed in as a database-level user, a namespace-level user, or a root-level user.
Moreover, the Access field in the Auth struct or the "AC" key in the map[string]any is optional, and is only needed when signing in as a record user, which is like a database user that requires the namespace and database to be specified too.
The following examples illustrate the different cases.
The most complex case is signing in as a record user, which requires specifying the Access field to indicate which access method to use for authentication.
db.SignIn(Auth{
Access: "user",
Namespace: "app",
Database: "app",
Username: "yusuke",
Password: "VerySecurePassword123!",
})
If namespace and database are provided, the user is signed in as a database-level user.
db.SignIn(Auth{
Namespace: "app",
Database: "app",
Username: "yusuke",
Password: "VerySecurePassword123!",
})
db.SignIn(map[string]any{
"NS": "app",
"DB": "app",
"user": "yusuke",
"pass": "VerySecurePassword123!",
})
If namespace is provided but database is omitted, the user is signed in as a namespace-level user.
db.SignIn(Auth{
Namespace: "app",
Username: "yusuke",
Password: "VerySecurePassword123!",
})
db.SignIn(map[string]any{
"NS": "app",
"user": "yusuke",
"pass": "VerySecurePassword123!",
})
If both namespace and database are omitted, the user is signed in as a root-level user.
db.SignIn(Auth{
Username: "yusuke",
Password: "VerySecurePassword123!",
})
db.SignIn(map[string]any{
"user": "yusuke",
"pass": "VerySecurePassword123!",
})
Bearer Access Method ¶
For TYPE BEARER access methods (SurrealDB v3+), use the "key" parameter with a bearer key obtained from ACCESS ... GRANT. Bearer keys have the format "surreal-bearer-...". No username/password is needed:
db.SignIn(map[string]any{
"NS": "app",
"DB": "app",
"AC": "bearer_api",
"key": bearerKey, // from ACCESS bearer_api GRANT FOR USER/RECORD ...
})
Note: The "key" parameter is exclusively for bearer access grants. For TYPE RECORD access methods with WITH REFRESH, use SignInWithRefresh instead.
Example (DatabaseLevelUser) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
db, err := surrealdb.FromEndpointURLString(
context.Background(),
testenv.GetSurrealDBWSURL(),
)
if err != nil {
panic(err)
}
db, err = testenv.Init(db, "exampledb_signin_databaselevel", "testdb", "testtable")
if err != nil {
panic(err)
}
// Login at the root level to set up the namespace-level user
_, err = db.SignIn(context.Background(), surrealdb.Auth{
Username: "root",
Password: "root",
})
if err != nil {
panic(fmt.Sprintf("SignIn failed: %v", err))
}
err = db.Use(context.Background(), "exampledb_signin_databaselevel", "testdb")
if err != nil {
panic(fmt.Sprintf("Use failed: %v", err))
}
// Clean up any existing database-level user
_, err = surrealdb.Query[any](context.Background(), db, `REMOVE USER IF EXISTS myuser ON DATABASE`, nil)
if err != nil {
panic(fmt.Sprintf("Failed to remove existing database-level user: %v", err))
}
// Create a database-level user
_, err = surrealdb.Query[any](context.Background(), db, `DEFINE USER myuser ON DATABASE PASSWORD 'mypassword' ROLES OWNER`, nil)
if err != nil {
panic(fmt.Sprintf("Failed to create database-level user: %v", err))
}
err = db.Close(context.Background())
if err != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", err))
}
// Reconnect to ensure a fresh session
db, err = surrealdb.FromEndpointURLString(
context.Background(),
testenv.GetSurrealDBWSURL(),
)
if err != nil {
panic(err)
}
// Now sign in as the database-level user
_, err = db.SignIn(context.Background(), surrealdb.Auth{
Namespace: "exampledb_signin_databaselevel",
Database: "testdb",
Username: "myuser",
Password: "mypassword",
})
if err != nil {
panic(fmt.Sprintf("SignIn failed: %v", err))
}
err = db.Use(context.Background(), "exampledb_signin_databaselevel", "testdb")
if err != nil {
panic(fmt.Sprintf("Use failed: %v", err))
}
// Create table and query - SurrealDB 3.x requires table to exist before SELECT
_, err = surrealdb.Query[any](context.Background(), db, `DEFINE TABLE testtable; SELECT * FROM testtable`, nil)
if err != nil {
panic(fmt.Sprintf("Query failed: %v", err))
}
if closeErr := db.Close(context.Background()); closeErr != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}
fmt.Println("Database-level user SignIn tests completed successfully")
}
Output: Database-level user SignIn tests completed successfully
Example (DatabaseLevelUser_failureDueToMissingNamespace) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
db, err := surrealdb.FromEndpointURLString(
context.Background(),
testenv.GetSurrealDBWSURL(),
)
if err != nil {
panic(err)
}
db, err = testenv.Init(db, "exampledb_signin_databaselevel_failure", "testdb", "testtable")
if err != nil {
panic(err)
}
// Login at the root level to set up the database-level user
_, err = db.SignIn(context.Background(), surrealdb.Auth{
Username: "root",
Password: "root",
})
if err != nil {
panic(fmt.Sprintf("SignIn failed: %v", err))
}
err = db.Use(context.Background(), "exampledb_signin_databaselevel_failure", "testdb")
if err != nil {
panic(fmt.Sprintf("Use failed: %v", err))
}
// Clean up any existing database-level user
_, err = surrealdb.Query[any](context.Background(), db, `REMOVE USER IF EXISTS myuser ON DATABASE`, nil)
if err != nil {
panic(fmt.Sprintf("Failed to remove existing database-level user: %v", err))
}
// Create a database-level user
_, err = surrealdb.Query[any](context.Background(), db, `DEFINE USER myuser ON DATABASE PASSWORD 'mypassword' ROLES OWNER`, nil)
if err != nil {
panic(fmt.Sprintf("Failed to create database-level user: %v", err))
}
err = db.Close(context.Background())
if err != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", err))
}
// Reconnect to ensure a fresh session
db, err = surrealdb.FromEndpointURLString(
context.Background(),
testenv.GetSurrealDBWSURL(),
)
if err != nil {
panic(err)
}
_, err = db.SignIn(context.Background(), surrealdb.Auth{
// Note the omission of the Database field here.
// This is invalid for database-level users, because
// the database is present in a namespace.
// Namespace: "",
Database: "testdb",
Username: "myuser",
Password: "mypassword",
})
if err == nil {
panic("Expected SignIn to fail, but it succeeded")
}
if err := db.Close(context.Background()); err != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", err))
}
fmt.Println("Database-level user SignIn tests completed successfully")
}
Output: Database-level user SignIn tests completed successfully
Example (NamespaceLevelUser) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
db, err := surrealdb.FromEndpointURLString(
context.Background(),
testenv.GetSurrealDBWSURL(),
)
if err != nil {
panic(err)
}
db, err = testenv.Init(db, "exampledb_signin_namespacelevel", "testdb", "testtable")
if err != nil {
panic(err)
}
// Login at the root level to set up the namespace-level user
_, err = db.SignIn(context.Background(), surrealdb.Auth{
Username: "root",
Password: "root",
})
if err != nil {
panic(fmt.Sprintf("SignIn failed: %v", err))
}
err = db.Use(context.Background(), "exampledb_signin_namespacelevel", "")
if err != nil {
panic(fmt.Sprintf("Use failed: %v", err))
}
// Clean up any existing namespace-level user
_, err = surrealdb.Query[any](context.Background(), db, `REMOVE USER IF EXISTS myuser ON NAMESPACE`, nil)
if err != nil {
panic(fmt.Sprintf("Failed to remove existing namespace-level user: %v", err))
}
// Create a namespace-level user
_, err = surrealdb.Query[any](context.Background(), db, `DEFINE USER myuser ON NAMESPACE PASSWORD 'mypassword' ROLES OWNER`, nil)
if err != nil {
panic(fmt.Sprintf("Failed to create namespace-level user: %v", err))
}
err = db.Close(context.Background())
if err != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", err))
}
// Reconnect to ensure a fresh session
db, err = surrealdb.FromEndpointURLString(
context.Background(),
testenv.GetSurrealDBWSURL(),
)
if err != nil {
panic(err)
}
// Now sign in as the namespace-level user
_, err = db.SignIn(context.Background(), surrealdb.Auth{
Namespace: "exampledb_signin_namespacelevel",
Username: "myuser",
Password: "mypassword",
})
if err != nil {
panic(fmt.Sprintf("SignIn failed: %v", err))
}
err = db.Use(context.Background(), "exampledb_signin_namespacelevel", "testdb")
if err != nil {
panic(fmt.Sprintf("Use failed: %v", err))
}
// Create table and query - SurrealDB 3.x requires table to exist before SELECT
_, err = surrealdb.Query[any](context.Background(), db, `DEFINE TABLE testtable; SELECT * FROM testtable`, nil)
if err != nil {
panic(fmt.Sprintf("Query failed: %v", err))
}
if closeErr := db.Close(context.Background()); closeErr != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}
fmt.Println("Namespace-level user SignIn tests completed successfully")
}
Output: Namespace-level user SignIn tests completed successfully
Example (NamespaceLevelUser_failureDueToExtraDatabase) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
db, err := surrealdb.FromEndpointURLString(
context.Background(),
testenv.GetSurrealDBWSURL(),
)
if err != nil {
panic(err)
}
db, err = testenv.Init(db, "exampledb_signin_namespacelevel", "testdb", "testtable")
if err != nil {
panic(err)
}
// Login at the root level to set up the namespace-level user
_, err = db.SignIn(context.Background(), surrealdb.Auth{
Username: "root",
Password: "root",
})
if err != nil {
panic(fmt.Sprintf("SignIn failed: %v", err))
}
err = db.Use(context.Background(), "exampledb_signin_namespacelevel", "")
if err != nil {
panic(fmt.Sprintf("Use failed: %v", err))
}
// Clean up any existing namespace-level user
_, err = surrealdb.Query[any](context.Background(), db, `REMOVE USER IF EXISTS myuser ON NAMESPACE`, nil)
if err != nil {
panic(fmt.Sprintf("Failed to remove existing namespace-level user: %v", err))
}
// Create a namespace-level user
_, err = surrealdb.Query[any](context.Background(), db, `DEFINE USER myuser ON NAMESPACE PASSWORD 'mypassword' ROLES OWNER`, nil)
if err != nil {
panic(fmt.Sprintf("Failed to create namespace-level user: %v", err))
}
err = db.Close(context.Background())
if err != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", err))
}
// Reconnect to ensure a fresh session
db, err = surrealdb.FromEndpointURLString(
context.Background(),
testenv.GetSurrealDBWSURL(),
)
if err != nil {
panic(err)
}
_, err = db.SignIn(context.Background(), surrealdb.Auth{
Namespace: "exampledb_signin_namespacelevel",
// Note the extra Database field here.
// This is invalid for namespace-level users, because
// the existence of a database signals SurrealDB to authenticate you as a database-level user,
// which we didn't create for this test.
Database: "testdb",
Username: "myuser",
Password: "mypassword",
})
if err == nil {
panic("Expected SignIn to fail, but it succeeded")
}
if err := db.Close(context.Background()); err != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", err))
}
fmt.Println("Namespace-level user SignIn tests completed successfully")
}
Output: Namespace-level user SignIn tests completed successfully
Example (RootLevelUser) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
db, err := surrealdb.FromEndpointURLString(
context.Background(),
testenv.GetSurrealDBWSURL(),
)
if err != nil {
panic(err)
}
db, err = testenv.Init(db, "exampledb_signin_rootlevel", "testdb", "testtable")
if err != nil {
panic(err)
}
// Sign in as the root user
_, err = db.SignIn(context.Background(), surrealdb.Auth{
Username: "root",
Password: "root",
})
if err != nil {
panic(fmt.Sprintf("SignIn failed: %v", err))
}
err = db.Use(context.Background(), "exampledb_signin_rootlevel", "testdb")
if err != nil {
panic(fmt.Sprintf("Use failed: %v", err))
}
_, err = surrealdb.Query[any](context.Background(), db, `REMOVE USER IF EXISTS myuser ON ROOT`, nil)
if err != nil {
panic(fmt.Sprintf("Query failed: %v", err))
}
_, err = surrealdb.Query[any](context.Background(), db, `DEFINE USER myuser ON ROOT PASSWORD 'mypassword' ROLES OWNER`, nil)
if err != nil {
panic(fmt.Sprintf("Query failed: %v", err))
}
if closeErr := db.Close(context.Background()); closeErr != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}
db, err = surrealdb.FromEndpointURLString(
context.Background(),
testenv.GetSurrealDBWSURL(),
)
if err != nil {
panic(err)
}
// Now sign in to the SurrealDB instance as the new root level user.
// Omitting namespace and database indicates that we want to authenticate
// as a root-level user.
_, err = db.SignIn(context.Background(), surrealdb.Auth{
Username: "myuser",
Password: "mypassword",
})
if err != nil {
panic(fmt.Sprintf("SignIn failed: %v", err))
}
err = db.Use(context.Background(), "exampledb_signin_rootlevel", "testdb")
if err != nil {
panic(fmt.Sprintf("Use failed: %v", err))
}
// Create table and query - SurrealDB 3.x requires table to exist before SELECT
_, err = surrealdb.Query[any](context.Background(), db, `DEFINE TABLE testtable; SELECT * FROM testtable`, nil)
if err != nil {
panic(fmt.Sprintf("Query failed: %v", err))
}
if closeErr := db.Close(context.Background()); closeErr != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}
fmt.Println("Root-level user SignIn tests completed successfully")
}
Output: Root-level user SignIn tests completed successfully
Example (RootLevelUser_invalidAuthLevelDatabase) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
db, err := surrealdb.FromEndpointURLString(
context.Background(),
testenv.GetSurrealDBWSURL(),
)
if err != nil {
panic(err)
}
db, err = testenv.Init(db, "exampledb_signin_rootlevel", "testdb", "testtable")
if err != nil {
panic(err)
}
// Sign in as the root user
_, err = db.SignIn(context.Background(), surrealdb.Auth{
Username: "root",
Password: "root",
})
if err != nil {
panic(fmt.Sprintf("SignIn failed: %v", err))
}
err = db.Use(context.Background(), "exampledb_signin_rootlevel", "testdb")
if err != nil {
panic(fmt.Sprintf("Use failed: %v", err))
}
_, err = surrealdb.Query[any](context.Background(), db, `REMOVE USER IF EXISTS myuser ON ROOT`, nil)
if err != nil {
panic(fmt.Sprintf("Query failed: %v", err))
}
_, err = surrealdb.Query[any](context.Background(), db, `DEFINE USER myuser ON ROOT PASSWORD 'mypassword' ROLES OWNER`, nil)
if err != nil {
panic(fmt.Sprintf("Query failed: %v", err))
}
if closeErr := db.Close(context.Background()); closeErr != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}
db, err = surrealdb.FromEndpointURLString(
context.Background(),
testenv.GetSurrealDBWSURL(),
)
if err != nil {
panic(err)
}
// Try to sign in to the namespace/database using the new root level user
_, err = db.SignIn(context.Background(), surrealdb.Auth{
Namespace: "exampledb_signin_rootlevel",
Database: "testdb",
Username: "myuser",
Password: "mypassword",
})
// This should fail, because specifying both namespace and database
// indicates that we want to authenticate as a database-level user,
// which this user is not.
if err == nil {
panic("Expected SignIn to fail, but it succeeded")
}
fmt.Println("Root-level user SignIn tests completed successfully")
}
Output: Root-level user SignIn tests completed successfully
Example (RootLevelUser_invalidAuthLevelNamespace) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
db, err := surrealdb.FromEndpointURLString(
context.Background(),
testenv.GetSurrealDBWSURL(),
)
if err != nil {
panic(err)
}
db, err = testenv.Init(db, "exampledb_signin_rootlevel", "testdb", "testtable")
if err != nil {
panic(err)
}
// Sign in as the root user
_, err = db.SignIn(context.Background(), surrealdb.Auth{
Username: "root",
Password: "root",
})
if err != nil {
panic(fmt.Sprintf("SignIn failed: %v", err))
}
err = db.Use(context.Background(), "exampledb_signin_rootlevel", "testdb")
if err != nil {
panic(fmt.Sprintf("Use failed: %v", err))
}
_, err = surrealdb.Query[any](context.Background(), db, `REMOVE USER IF EXISTS myuser ON ROOT`, nil)
if err != nil {
panic(fmt.Sprintf("Query failed: %v", err))
}
_, err = surrealdb.Query[any](context.Background(), db, `DEFINE USER myuser ON ROOT PASSWORD 'mypassword' ROLES OWNER`, nil)
if err != nil {
panic(fmt.Sprintf("Query failed: %v", err))
}
if closeErr := db.Close(context.Background()); closeErr != nil {
panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}
db, err = surrealdb.FromEndpointURLString(
context.Background(),
testenv.GetSurrealDBWSURL(),
)
if err != nil {
panic(err)
}
// Try to sign in to the namespace as the new root level user
_, err = db.SignIn(context.Background(), surrealdb.Auth{
Namespace: "exampledb_signin_rootlevel",
Username: "myuser",
Password: "mypassword",
})
// This should fail because "myuser" is a root-level user, not a namespace-level user.
// Specifying the namespace indicates that we want to authenticate
// as a namespace-level user.
if err == nil {
panic("Expected SignIn to fail, but it succeeded")
}
fmt.Println("Root-level user SignIn tests completed successfully")
}
Output: Root-level user SignIn tests completed successfully
func (*DB) SignInWithRefresh ¶ added in v1.2.0
SignInWithRefresh signs in using a TYPE RECORD access method with WITH REFRESH enabled. This is only supported in SurrealDB v3+ and returns both an access token and a refresh token.
The authData parameter should be a map[string]any with the signin credentials:
// Initial signin with username/password
pair, err := db.SignInWithRefresh(ctx, map[string]any{
"NS": "app",
"DB": "app",
"AC": "user_access",
"user": "yusuke",
"pass": "VerySecurePassword123!",
})
The returned Tokens contains:
- Access: JWT token (use with Authenticate() on new connections)
- Refresh: Refresh token (format: "surreal-refresh-...")
To obtain new tokens using the refresh token (no credentials needed):
newPair, err := db.SignInWithRefresh(ctx, map[string]any{
"NS": "app",
"DB": "app",
"AC": "user_access",
"refresh": pair.Refresh, // no username/password needed
})
Note: The "refresh" parameter is for record access refresh tokens only. For bearer access methods, use SignIn with the "key" parameter. For other access methods (system users, record users without refresh), use SignIn.
func (*DB) SignUp ¶ added in v0.3.0
SignUp signs up a new user.
The authData parameter can be either:
- An Auth struct
- A map[string]any with keys like: "namespace", "database", "scope", "user", "pass"
Example with struct:
db.SignUp(Auth{
Namespace: "app",
Database: "app",
Access: "user",
Username: "yusuke",
Password: "VerySecurePassword123!",
})
Example with map:
db.SignUp(map[string]any{
"NS": "app",
"DB": "app",
"AC": "user",
"user": "yusuke",
"pass": "VerySecurePassword123!",
})
Example (DatabaseLevelRecordUser) ¶
package main
import (
"context"
"fmt"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
// SignUp's sole purpose is to create a new record user in a database
// that has been configured to use RECORD access method type at the database level.
//
// # SignIn with and without ACCESS field
//
// The only difference between signing in as a database user and signing in as a record user
// is that you need to specify the Access field to indicate which access method to use for authentication.
//
// Like logging in as a database user defined using DEFINE USER ON DATABASE,
// signing in as a record user also requires specifying the target namespace and database.
db, err := surrealdb.FromEndpointURLString(
context.Background(),
testenv.GetSurrealDBWSURL(),
)
if err != nil {
panic(err)
}
db, err = testenv.Init(db, "exampledb_signup_rootlevel", "testdb", "user")
if err != nil {
panic(err)
}
// Sign in as the root user
_, err = db.SignIn(context.Background(), surrealdb.Auth{
Username: "root",
Password: "root",
})
if err != nil {
panic(fmt.Sprintf("SignIn failed: %v", err))
}
err = db.Use(context.Background(), "exampledb_signup_rootlevel", "testdb")
if err != nil {
panic(fmt.Sprintf("Use failed: %v", err))
}
// Detect SurrealDB version to use the correct function name
// SurrealDB 2.x uses type::thing(), SurrealDB 3.x uses type::record()
v, err := testenv.GetVersion(context.Background(), db)
if err != nil {
panic(fmt.Sprintf("GetVersion failed: %v", err))
}
recordFn := v.ThingOrRecordFn()
setupQuery := fmt.Sprintf(`
-- Define the user table with schema
DEFINE TABLE user SCHEMAFULL
PERMISSIONS
FOR select, update, delete WHERE id = $auth.id;
-- Define fields
DEFINE FIELD password ON user TYPE string;
-- Define access method for record authentication
REMOVE ACCESS IF EXISTS user ON DATABASE;
DEFINE ACCESS user ON DATABASE TYPE RECORD
SIGNIN (
SELECT * FROM %s("user", $user) WHERE crypto::argon2::compare(password, $pass)
)
SIGNUP (
CREATE %s("user", $user) CONTENT {
password: crypto::argon2::generate($pass)
}
);
`, recordFn, recordFn)
_, err = surrealdb.Query[any](context.Background(), db, setupQuery, nil)
if err != nil {
panic(fmt.Sprintf("Query failed: %v", err))
}
_, err = db.SignUp(context.Background(), surrealdb.Auth{
Access: "user",
Namespace: "exampledb_signup_rootlevel",
Database: "testdb",
Username: "myuser",
Password: "mypassword",
})
if err != nil {
panic(fmt.Sprintf("SignUp failed: %v", err))
}
_, err = db.SignIn(context.Background(), surrealdb.Auth{
Access: "user",
Namespace: "exampledb_signup_rootlevel",
Database: "testdb",
Username: "myuser",
Password: "mypassword",
})
if err != nil {
panic(fmt.Sprintf("SignIn failed: %v", err))
}
fmt.Println("User signed up and signed in successfully")
}
Output: User signed up and signed in successfully
func (*DB) SignUpWithRefresh ¶ added in v1.2.0
SignUpWithRefresh signs up a new user using a TYPE RECORD access method with WITH REFRESH enabled. This is only supported in SurrealDB v3+ and returns both an access token and a refresh token.
The authData parameter should be a map[string]any with the signup credentials:
tokens, err := db.SignUpWithRefresh(ctx, map[string]any{
"NS": "app",
"DB": "app",
"AC": "user_access",
"user": "yusuke",
"pass": "VerySecurePassword123!",
})
The returned Tokens contains:
- Access: JWT token (use with Authenticate() on new connections)
- Refresh: Refresh token (format: "surreal-refresh-...")
Note: Use this method instead of SignUp when the access method has WITH REFRESH enabled. For access methods without WITH REFRESH, use SignUp instead.
func (*DB) Version ¶ added in v0.3.0
func (db *DB) Version(ctx context.Context) (*VersionData, error)
Example ¶
package main
import (
"context"
"fmt"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
ws := testenv.MustNew("surrealdbexamples", "version")
v, err := ws.Version(context.Background())
if err != nil {
panic(err)
}
fmt.Printf("VersionData (WebSocket): %+v\n", v)
http := testenv.MustNew("surrealdbexamples", "version")
v, err = http.Version(context.Background())
if err != nil {
panic(err)
}
fmt.Printf("VersionData (HTTP): %+v\n", v)
// You get something like below depending on your SurrealDB version:
//
// VersionData (WebSocket): &{Version:2.3.7 Build: Timestamp:}
// VersionData (HTTP): &{Version:2.3.7 Build: Timestamp:}
}
Output:
func (*DB) WithContext
deprecated
added in
v0.3.0
type QueryError ¶ added in v0.5.0
type QueryError struct {
Message string
}
QueryError represents an error that occurred during a query execution.
The caller can type-assert the return errror to QueryError to see if the error is a query error or not.
func (*QueryError) Error ¶ added in v0.5.0
func (e *QueryError) Error() string
func (*QueryError) Is ¶ added in v0.5.0
func (e *QueryError) Is(target error) bool
type QueryResult ¶ added in v0.3.0
type QueryResult[T any] struct { Status string `json:"status"` Time string `json:"time"` Result T `json:"result"` Error *QueryError `json:"-"` }
QueryResult is a struct that represents one of the results of a SurrealDB query RPC method call, made via Query, for example.
type QueryStmt ¶ added in v0.3.0
type QueryStmt struct {
SQL string
Vars map[string]any
Result QueryResult[cbor.RawMessage]
// contains filtered or unexported fields
}
type RPCError
deprecated
type RPCError = connection.RPCError
Deprecated: Use ServerError instead on SurrealDB v3 for richer error information. TODO(v2-compat): Remove in next major release.
type Relationship ¶ added in v0.3.0
type ServerError ¶ added in v1.4.0
type ServerError = connection.ServerError
ServerError represents a structured error from SurrealDB v3. Only use this when you know you are running against a SurrealDB v3 server.
Extract from RPC errors using errors.As:
var se *surrealdb.ServerError
if errors.As(err, &se) {
fmt.Println(se.Kind, se.Details)
}
type Session ¶ added in v1.3.0
type Session struct {
// contains filtered or unexported fields
}
Session represents an additional SurrealDB session on a WebSocket connection. Sessions scope live notifications and can have their own transactions.
Sessions are only supported on WebSocket connections (SurrealDB v3+). Each session starts unauthenticated and without a selected namespace/database, so you must call SignIn/Authenticate and Use after creating a session.
Session satisfies the sendable constraint, so all surrealdb.Query, surrealdb.Create, etc. functions work with sessions directly.
func (*Session) Authenticate ¶ added in v1.3.0
Authenticate authenticates the session with the provided token.
func (*Session) Begin ¶ added in v1.3.0
func (s *Session) Begin(ctx context.Context) (*Transaction, error)
Begin starts a new interactive transaction in this session. Interactive transactions are only supported on WebSocket connections (SurrealDB v3+).
Example ¶
ExampleSession_Begin demonstrates starting a transaction within a session. Transactions within sessions are isolated and can be committed or canceled.
package main
import (
"context"
"fmt"
"log"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
ctx := context.Background()
// Connect using WebSocket
db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
log.Fatal(err)
}
defer func() {
if closeErr := db.Close(ctx); closeErr != nil {
log.Printf("Failed to close db: %v", closeErr)
}
}()
// Sign in and set up
_, err = db.SignIn(ctx, map[string]any{"user": "root", "pass": "root"})
if err != nil {
log.Fatal(err) //nolint:gocritic // Example code - log.Fatal is acceptable
}
err = db.Use(ctx, "test", "test")
if err != nil {
log.Fatal(err) //nolint:gocritic // Example code - log.Fatal is acceptable
}
// Create a session
session, err := db.Attach(ctx)
if err != nil {
log.Fatal(err)
}
defer func() { _ = session.Detach(ctx) }()
// Authenticate and configure the session
_, err = session.SignIn(ctx, map[string]any{"user": "root", "pass": "root"})
if err != nil {
log.Fatal(err)
}
err = session.Use(ctx, "test", "test")
if err != nil {
log.Fatal(err)
}
// Start a transaction within the session
tx, err := session.Begin(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Transaction started with ID: %s\n", tx.ID())
fmt.Printf("Transaction is in session: %s\n", tx.SessionID())
// Perform operations in the transaction
// ... your operations here ...
// Commit or cancel
err = tx.Commit(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println("Transaction committed successfully")
// Note: This example requires SurrealDB v3+ and will fail on earlier versions.
// Output is not verified because transaction IDs are dynamic.
}
Output:
func (*Session) CloseLiveNotifications ¶ added in v1.3.0
CloseLiveNotifications closes the notification channel for a live query.
func (*Session) Detach ¶ added in v1.3.0
Detach deletes the session from the server. After calling Detach, the session cannot be used anymore.
func (*Session) Invalidate ¶ added in v1.3.0
Invalidate invalidates the authentication for this session.
func (*Session) LiveNotifications ¶ added in v1.3.0
func (s *Session) LiveNotifications(liveQueryID string) (chan connection.Notification, error)
LiveNotifications returns a channel for receiving live query notifications.
func (*Session) SignInWithRefresh ¶ added in v1.3.0
SignInWithRefresh signs in using a TYPE RECORD access method with WITH REFRESH enabled.
func (*Session) SignUpWithRefresh ¶ added in v1.3.0
SignUpWithRefresh signs up a new user using a TYPE RECORD access method with WITH REFRESH enabled.
type TableOrRecord ¶ added in v0.3.0
type Tokens ¶ added in v1.2.0
type Tokens = connection.Tokens
Tokens contains the access token and refresh token returned by SignInWithRefresh. Access is the JWT token used for authentication. Use this with Authenticate() to establish a session on a new connection. Refresh is the refresh token used to obtain new tokens without credentials. Use this with SignInWithRefresh to get a new Tokens.
type Transaction ¶ added in v1.3.0
type Transaction struct {
// contains filtered or unexported fields
}
Transaction represents an interactive SurrealDB transaction on a WebSocket connection. Unlike text-based transactions (BEGIN TRANSACTION; ... COMMIT;), interactive transactions allow executing statements one at a time and conditionally committing or canceling.
Transactions are only supported on WebSocket connections (SurrealDB v3+).
Transaction satisfies the sendable constraint, so all surrealdb.Query, surrealdb.Create, etc. functions work with transactions directly.
Note: Transactions do NOT support session state changes like SignIn, Use, Let, etc. The namespace/database and authentication are inherited from the session or connection that started the transaction.
Example (ConditionalCommit) ¶
ExampleTransaction_conditionalCommit demonstrates conditional commit/cancel. Based on query results, you can decide whether to commit or rollback.
package main
import (
"context"
"fmt"
"log"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
ctx := context.Background()
// Connect using WebSocket
db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
log.Fatal(err)
}
defer func() {
if closeErr := db.Close(ctx); closeErr != nil {
log.Printf("Failed to close db: %v", closeErr)
}
}()
// Sign in and configure
_, err = db.SignIn(ctx, map[string]any{"user": "root", "pass": "root"})
if err != nil {
log.Fatal(err) //nolint:gocritic // Example code - log.Fatal is acceptable
}
err = db.Use(ctx, "test", "test")
if err != nil {
log.Fatal(err) //nolint:gocritic // Example code - log.Fatal is acceptable
}
// Start transaction
tx, err := db.Begin(ctx)
if err != nil {
log.Fatal(err)
}
// Simulate a business operation: deduct from inventory
type Inventory struct {
Stock int `json:"stock"`
}
// Check current stock
results, err := surrealdb.Query[[]Inventory](ctx, tx,
"SELECT stock FROM inventory:item1", nil)
if err != nil {
_ = tx.Cancel(ctx)
log.Fatal(err)
}
var currentStock int
if len(*results) > 0 && len((*results)[0].Result) > 0 {
currentStock = (*results)[0].Result[0].Stock
}
requestedQuantity := 5
// Conditional logic based on query results
if currentStock >= requestedQuantity {
// Update stock
_, err = surrealdb.Query[any](ctx, tx,
"UPDATE inventory:item1 SET stock -= $qty",
map[string]any{"qty": requestedQuantity})
if err != nil {
_ = tx.Cancel(ctx)
log.Fatal(err)
}
// Commit the transaction
err = tx.Commit(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println("Transaction committed: inventory updated")
} else {
// Not enough stock - cancel transaction
err = tx.Cancel(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println("Transaction canceled: insufficient stock")
}
// Output depends on inventory state
}
Output:
Example (Isolation) ¶
ExampleTransaction_isolation demonstrates transaction isolation. Changes made in a transaction are not visible to other connections until the transaction is committed.
package main
import (
"context"
"fmt"
"log"
surrealdb "github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/contrib/testenv"
)
func main() {
ctx := context.Background()
// Create two connections
db1, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
log.Fatal(err)
}
defer func() {
if closeErr := db1.Close(ctx); closeErr != nil {
log.Printf("Failed to close db1: %v", closeErr)
}
}()
db2, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
log.Fatal(err) //nolint:gocritic // Example code - log.Fatal is acceptable
}
defer func() {
if closeErr := db2.Close(ctx); closeErr != nil {
log.Printf("Failed to close db2: %v", closeErr)
}
}()
// Configure both connections
for _, db := range []*surrealdb.DB{db1, db2} {
_, signInErr := db.SignIn(ctx, map[string]any{"user": "root", "pass": "root"})
if signInErr != nil {
log.Fatal(signInErr)
}
useErr := db.Use(ctx, "test", "test")
if useErr != nil {
log.Fatal(useErr)
}
}
// Start transaction on db1
tx, err := db1.Begin(ctx)
if err != nil {
log.Fatal(err)
}
defer func() {
if !tx.IsClosed() {
_ = tx.Cancel(ctx)
}
}()
// Create record in transaction
_, err = surrealdb.Query[any](ctx, tx,
"CREATE items:isolated SET value = 'hidden'", nil)
if err != nil {
log.Fatal(err)
}
// Query from db2 - should NOT see uncommitted data
type Item struct {
Value string `json:"value"`
}
results, err := surrealdb.Query[[]Item](ctx, db2,
"SELECT * FROM items:isolated", nil)
if err != nil {
log.Fatal(err)
}
if len(*results) > 0 && len((*results)[0].Result) == 0 {
fmt.Println("Before commit: db2 cannot see uncommitted data")
}
// Commit
err = tx.Commit(ctx)
if err != nil {
log.Fatal(err)
}
// Now db2 should see the data
results, err = surrealdb.Query[[]Item](ctx, db2,
"SELECT * FROM items:isolated", nil)
if err != nil {
log.Fatal(err)
}
if len(*results) > 0 && len((*results)[0].Result) > 0 {
fmt.Println("After commit: db2 can see committed data")
}
// Note: This example requires SurrealDB v3+ and will fail on earlier versions.
}
Output:
func (*Transaction) Cancel ¶ added in v1.3.0
func (tx *Transaction) Cancel(ctx context.Context) error
Cancel cancels the transaction, discarding all changes. After calling Cancel, the transaction cannot be used anymore.
It's safe to call Cancel on an already committed or canceled transaction; it will return ErrTransactionClosed but won't cause any harm.
func (*Transaction) Commit ¶ added in v1.3.0
func (tx *Transaction) Commit(ctx context.Context) error
Commit commits the transaction, making all changes permanent. After calling Commit, the transaction cannot be used anymore.
func (*Transaction) ID ¶ added in v1.3.0
func (tx *Transaction) ID() *models.UUID
ID returns the transaction's UUID.
func (*Transaction) IsClosed ¶ added in v1.3.0
func (tx *Transaction) IsClosed() bool
IsClosed returns whether the transaction has been committed or canceled.
func (*Transaction) SessionID ¶ added in v1.3.0
func (tx *Transaction) SessionID() *models.UUID
SessionID returns the session UUID if the transaction was started within a session. Returns nil if the transaction was started on the default session.
type VersionData ¶ added in v0.3.0
Directories
¶
| Path | Synopsis |
|---|---|
|
Package contrib provides additional functionality and utilities for the SurrealDB Go SDK.
|
Package contrib provides additional functionality and utilities for the SurrealDB Go SDK. |
|
rews
Package rews provides a reliable, auto-reconnecting WebSocket connection for SurrealDB with support for session restoration, live query persistence, and customizable retry strategies.
|
Package rews provides a reliable, auto-reconnecting WebSocket connection for SurrealDB with support for session restoration, live query persistence, and customizable retry strategies. |
|
surrealdump/cmd/surrealdump
command
|
|
|
surrealql
Package surrealql provides a type-safe query builder for SurrealDB's SurrealQL language.
|
Package surrealql provides a type-safe query builder for SurrealDB's SurrealQL language. |
|
surrealrestore/cmd/surrealexec
command
|
|
|
testenv
Package testenv provides utilities for testing the SurrealDB Go SDK and SurrealDB.
|
Package testenv provides utilities for testing the SurrealDB Go SDK and SurrealDB. |
|
surrealnote
module
|
|
|
cmd
command
|
|
|
internal
|
|
|
fakesdb
Package fakesdb provides a fake SurrealDB WebSocket server for testing purposes.
|
Package fakesdb provides a fake SurrealDB WebSocket server for testing purposes. |
|
pkg
|
|
|
Package surrealcbor provides CBOR (Concise Binary Object Representation) encoding and decoding for SurrealDB.
|
Package surrealcbor provides CBOR (Concise Binary Object Representation) encoding and decoding for SurrealDB. |