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.
Data Models ¶
The surrealdb package facilitates communication between client and the backend service using the Concise Binary Object Representation (CBOR) format.
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.
Index ¶
- func Create[TResult any, TWhat TableOrRecord](ctx context.Context, db *DB, what TWhat, data any) (*TResult, error)
- func Delete[TResult any, TWhat TableOrRecord](ctx context.Context, db *DB, what TWhat) (*TResult, error)
- func Insert[TResult any](ctx context.Context, db *DB, what models.Table, data any) (*[]TResult, error)
- func InsertRelation[TResult any](ctx context.Context, db *DB, relationship *Relationship) (*TResult, error)
- func Kill(ctx context.Context, db *DB, id string) error
- func Live(ctx context.Context, db *DB, table models.Table, diff bool) (*models.UUID, error)
- func Merge[TResult any, TWhat TableOrRecord](ctx context.Context, db *DB, what TWhat, data any) (*TResult, error)
- func Patch[TWhat TableOrRecord](ctx context.Context, db *DB, what TWhat, patches []PatchData) (*[]PatchData, error)
- func Query[TResult any](ctx context.Context, db *DB, sql string, vars map[string]any) (*[]QueryResult[TResult], error)
- func QueryRaw(ctx context.Context, db *DB, queries *[]QueryStmt) error
- func Relate[TResult any](ctx context.Context, db *DB, rel *Relationship) (*TResult, error)
- func Select[TResult any, TWhat TableOrRecord](ctx context.Context, db *DB, what TWhat) (*TResult, error)
- func Send[Result any](ctx context.Context, db *DB, res *connection.RPCResponse[Result], ...) error
- func Update[TResult any, TWhat TableOrRecord](ctx context.Context, db *DB, what TWhat, data any) (*TResult, error)
- func Upsert[TResult any, TWhat TableOrRecord](ctx context.Context, db *DB, what TWhat, data any) (*TResult, error)
- type Auth
- type DB
- func (db *DB) Authenticate(ctx context.Context, token string) 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) SignUp(ctx context.Context, authData any) (string, 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) *DB
- type Objdeprecated
- type PatchData
- type QueryError
- type QueryResult
- type QueryStmt
- type RPCError
- type Relationship
- type Resultdeprecated
- type TableOrRecord
- type VersionData
Examples ¶
- Create
- Create (Server_unmarshal_error)
- DB (Record_user_auth_struct)
- DB (Record_user_custom_struct)
- DB (Signin_failure)
- DB.Version
- 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)
- 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)
- Query (None_and_null_handling_explicitFields_ints)
- Query (None_and_null_handling_explicitFields_ints_surrealcbor)
- Query (None_and_null_handling_explicitFields_surrealcbor)
- Query (Null_none_customdatetime_roundtrip)
- Query (Null_none_customdatetime_roundtrip_surrealcbor)
- Query (Return)
- 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
- Send (Select)
- Update
- Upsert
- Upsert (Rpc_error)
- Upsert (Unmarshal_error_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](ctx context.Context, db *DB, what TWhat, data any) (*TResult, error)
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 (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 Insert ¶ added in v0.3.0
func Insert[TResult any](ctx context.Context, db *DB, what models.Table, data any) (*[]TResult, error)
Insert creates records with either specified IDs or generated IDs.
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](ctx context.Context, db *DB, relationship *Relationship) (*TResult, error)
InsertRelation inserts a relation between two records in the database.
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 Live ¶ added in v0.3.0
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() 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, while DELETE still returns the deleted record as map[string]any. 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, " ") + "}" } // 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] if k == "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)) } } else { 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() 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() { for notification := range notifications { var resultStr string // With diff=true: // - CREATE and UPDATE return diff operations as []any // - DELETE returns the full deleted record as map[string]any (same as without diff) if notification.Action == connection.DeleteAction { // DELETE always returns a regular record, even with diff=true record, ok := notification.Result.(map[string]any) if !ok { panic(fmt.Sprintf("Expected map[string]any for DELETE result, got %T", notification.Result)) } resultStr = formatRecordResult(record) close(received) } else { // CREATE and UPDATE return an array of diff operations diffs, ok := notification.Result.([]any) if !ok { panic(fmt.Sprintf("Expected []any for diff result, got %T", notification.Result)) } resultStr = formatDiffResult(diffs) } fmt.Printf("Action: %s, Result: %s\n", notification.Action, resultStr) } // 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: {id=inventory:⟨UUID⟩ quantity=45} 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](ctx context.Context, db *DB, what TWhat, data any) (*TResult, error)
Merge a table or record in the database like a PATCH request.
func Patch ¶ added in v0.2.0
func Patch[TWhat TableOrRecord](ctx context.Context, db *DB, what TWhat, patches []PatchData) (*[]PatchData, error)
Patches either all records in a table or a single record with specified patches.
func Query ¶ added in v0.3.0
func Query[TResult any](ctx context.Context, db *DB, sql string, vars map[string]any) (*[]QueryResult[TResult], error)
Query executes a query against the SurrealDB database.
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) } createQueryResults, err := surrealdb.Query[[]Person]( context.Background(), db, `CREATE type::thing($tb, $id) CONTENT $content`, map[string]any{ "tb": "persons", "id": "yusuke", "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:{Latitude:45.65 Longitude:12.34}} {ID:persons:p1 Note:inserted1 Num:1 Loc:{Latitude:46.65 Longitude:13.34}}] # INSERT IGNORE INTO Count : 1 Status : OK Result : [] Selected: [{ID:persons:p0 Note:inserted0 Num:0 Loc:{Latitude:45.65 Longitude:12.34}} {ID:persons:p1 Note:inserted1 Num:1 Loc:{Latitude:46.65 Longitude:13.34}} {ID:persons:p2 Note:inserted2 Num:2 Loc:{Latitude:47.65 Longitude:14.34}}] # UPSERT CONTENT Count : 4 Status : OK Result : [] Selected: [{ID:persons:p0 Note:updated0 Num:0 Loc:{Latitude:45.65 Longitude:12.34}} {ID:persons:p1 Note:updated1 Num:1 Loc:{Latitude:46.65 Longitude:13.34}} {ID:persons:p2 Note:updated2 Num:2 Loc:{Latitude:47.65 Longitude:14.34}} {ID:persons:p3 Note:inserted3 Num:3 Loc:{Latitude:48.65 Longitude:15.34}}]
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) ¶
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.UseSurrealCBOR = false 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.UseSurrealCBOR = true 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) } createQueryResults, err := surrealdb.Query[[]Person]( context.Background(), db, `CREATE type::thing($tb, $id) CONTENT $content`, map[string]any{ "tb": "persons", "id": "yusuke", "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() 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") } err = surrealdb.Kill(ctx, db, liveID) if err != nil { panic(fmt.Sprintf("Failed to kill live query: %v", err)) } fmt.Println("Live query terminated") 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} Live query terminated 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) ¶
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.UseSurrealCBOR = false 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_ints) ¶
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.UseSurrealCBOR = false 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.UseSurrealCBOR = true 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_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.UseSurrealCBOR = true 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) ¶
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.UseSurrealCBOR = false 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_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.UseSurrealCBOR = true 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_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 (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 (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() { db := testenv.MustNew("surrealdbexamples", "query", "t") var err error queryResults, err := surrealdb.Query[any](context.Background(), 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) if len(*queryResults) != 3 { panic(fmt.Errorf("expected 3 query results, got %d", len(*queryResults))) } var records []map[string]any for i, result := range *queryResults { if result.Status != "OK" { panic(fmt.Errorf("expected OK status for query result %d, got %s", i, result.Status)) } if result.Result == nil { panic(fmt.Errorf("expected non-nil result for query 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() { db := testenv.MustNew("surrealdbexamples", "query", "t") var err error // 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](context.Background(), 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) } if len(*queryResults) != 1 { panic(fmt.Errorf("expected 1 query result, got %d", len(*queryResults))) } rs := (*queryResults)[0].Result.([]any) r := rs[0].(map[string]any) fmt.Printf("Status: %v\n", (*queryResults)[0].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() { db := testenv.MustNew("surrealdbexamples", "query", "t") createQueryResults, err := surrealdb.Query[[]any]( context.Background(), 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) } fmt.Printf("Number of query results: %d\n", len(*createQueryResults)) fmt.Printf("First query result's status: %+s\n", (*createQueryResults)[0].Status) fmt.Printf("Names contained in the first query result: %+v\n", (*createQueryResults)[0].Result) }
Output: Number of query results: 1 First query result's status: OK Names contained in the first query 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() { db := testenv.MustNew("surrealdbexamples", "query", "person") var err error var a *[]surrealdb.QueryResult[bool] a, err = surrealdb.Query[bool]( context.Background(), db, `BEGIN; CREATE person:1; CREATE person:2; RETURN true; COMMIT;`, map[string]any{}, ) if err != nil { panic(err) } fmt.Printf("Status: %v\n", (*a)[0].Status) fmt.Printf("Result: %v\n", (*a)[0].Result) }
Output: Status: OK Result: true
Example (Transaction_throw) ¶
package main import ( "context" "errors" "fmt" 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, ) fmt.Printf("# of results: %d\n", len(*queryResults)) fmt.Println("=== Func error ===") fmt.Printf("Error: %v\n", 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 *queryResults { 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", 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 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.
You may want to use Query with github.com/surrealdb/surrealdb.go/contrib/surrealql instead.
func Relate ¶ added in v0.3.0
Relate creates a relationship between two records in the table with a generated relationship ID.
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](ctx context.Context, db *DB, what TWhat) (*TResult, error)
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", "update", "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}}
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", "update", "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](ctx context.Context, db *DB, what TWhat, data any) (*TResult, error)
Update a table or record in the database like a PUT request.
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", "update", "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](ctx context.Context, db *DB, what TWhat, data any) (*TResult, error)
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 either v3.0.0-alpha.7 or v2.3.7") // 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 either v3.0.0-alpha.7 or v2.3.7") 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 either v3.0.0-alpha.7 or v2.3.7 Error is RPCError: true
Example (Unmarshal_error_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.UseSurrealCBOR = false 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.UseSurrealCBOR = true 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"` }
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", }) 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, 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", }) 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: There was a problem with the database: There was a problem with authentication
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_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) // 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:", 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:", 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:", err) err = db.Close(context.Background()) fmt.Println("Close error:", err) }
Output: FromConnection error: <nil> SignIn error: There was a problem with the database: There was a problem with authentication Use error: <nil> SignIn error: There was a problem with the database: There was a problem with authentication 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{ 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) 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"
Example with struct:
db.SignIn(Auth{ Namespace: "app", Database: "app", Access: "user", Username: "yusuke", Password: "VerySecurePassword123!", })
Example with map:
db.SignIn(map[string]any{ "NS": "app", "DB": "app", "AC": "user", "user": "yusuke", "pass": "VerySecurePassword123!", })
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!", })
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:} }
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 ¶
type RPCError = connection.RPCError
type Relationship ¶ added in v0.3.0
type TableOrRecord ¶ added in v0.3.0
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. |
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. |
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. |
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. |