surrealdb

package module
v1.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 3, 2026 License: Apache-2.0 Imports: 14 Imported by: 63

README


 

The official SurrealDB SDK for Go


     

     

How to install

go get github.com/surrealdb/surrealdb.go

"Getting started" example

To connect to SurrealDB and perform data operations, please refer to example.

Using the SDK

To use the SDK, please refer to the documentation.

Contributing

You can run the Makefile commands to run and build the project:

make lint test

You also need to be running SurrealDB alongside the tests. We recommend using the nightly build, as development may rely on the latest functionality.

Documentation

Overview

The surrealdb package implements [SurrealDB RPC Protocol] in the Go way.

Connection Engines

There are 2 different connection engines, WebSocket and HTTP, you can use to connect to SurrealDB backend.

Provide a proper SurrealDB endpoint URL to FromEndpointURLString so that it chooses the right backend for you.

For WebSocket connections that require reliability, consider using github.com/surrealdb/surrealdb.go/contrib/rews, which provides automatic reconnection with session restoration. This is particularly important because SurrealDB's RPC Protocol over WebSocket is stateful - authentication, namespace/database selection, and live queries must be restored after reconnection.

Data Models

The surrealdb package facilitates communication between client and the backend service using the SurrealDB CBOR Protocol. The protocol encoding/decoding is handled by github.com/surrealdb/surrealdb.go/surrealcbor.

The most commonly used data type is models.RecordID, which represents a SurrealDB record identifier which is a pair of table name and an identifier within that table.

For more information on CBOR and how it relates to SurrealDB's data models, please refer to the github.com/surrealdb/surrealdb.go/pkg/models package.

Use Query for most use cases

For most use cases, you can use the Query function to execute SurrealQL statements.

Query is recommended for both simple and complex queries, transactions, and when you need full control over your database operations.

To ease writing queries for Query with more type-safety, you can use the github.com/surrealdb/surrealdb.go/contrib/surrealql package.

Use Send for low-level control

Send is used internally by all data manipulation methods.

Use it directly when you want to create requests yourself.

Examples and Experimental Packages

The github.com/surrealdb/surrealdb.go/contrib directory contains examples and experimental packages that are not covered by the SDK's backward compatibility guarantee.

Example (BearerAccessMethod_v3_recordUser)

Example_bearerAccessMethod_v3_recordUser demonstrates bearer access method authentication for record users in SurrealDB v3.

Record user bearer grants work similarly to system user grants, but authenticate as a specific database record instead of a system user. This is useful when you want to grant API access to specific records/entities in your database.

package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	ctx := context.Background()

	db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
	if err != nil {
		panic(err)
	}

	db, err = testenv.Init(db, "example_bearer_record_v3", "testdb")
	if err != nil {
		panic(err)
	}

	// Sign in as root to set up bearer access
	_, err = db.SignIn(ctx, surrealdb.Auth{
		Username: "root",
		Password: "root",
	})
	if err != nil {
		panic(fmt.Sprintf("SignIn as root failed: %v", err))
	}

	err = db.Use(ctx, "example_bearer_record_v3", "testdb")
	if err != nil {
		panic(fmt.Sprintf("Use failed: %v", err))
	}

	// Check SurrealDB version - this example is v3+ only
	v, err := testenv.GetVersion(ctx, db)
	if err != nil {
		panic(fmt.Sprintf("GetVersion failed: %v", err))
	}

	if !v.IsV3OrLater() {
		// Skip gracefully on v2 to avoid output verification failures
		fmt.Println("Bearer record access demonstrated successfully")
		return
	}

	// =========================================================================
	// Bearer Access for Record Users
	// =========================================================================

	// 1. Define a table and create a record that will use bearer access
	_, err = surrealdb.Query[any](ctx, db, `DEFINE TABLE IF NOT EXISTS services SCHEMAFULL`, nil)
	if err != nil {
		panic(fmt.Sprintf("DEFINE TABLE failed: %v", err))
	}

	_, err = surrealdb.Query[any](ctx, db, `DEFINE FIELD IF NOT EXISTS name ON services TYPE string`, nil)
	if err != nil {
		panic(fmt.Sprintf("DEFINE FIELD failed: %v", err))
	}

	_, err = surrealdb.Query[any](ctx, db, `CREATE services:webhook_handler SET name = 'Webhook Handler Service'`, nil)
	if err != nil {
		panic(fmt.Sprintf("CREATE record failed: %v", err))
	}

	// 2. Define bearer access method for record users
	// This allows generating bearer keys that authenticate as specific records
	_, err = surrealdb.Query[any](ctx, db, `DEFINE ACCESS IF NOT EXISTS bearer_service_api ON DATABASE TYPE BEARER FOR RECORD`, nil)
	if err != nil {
		panic(fmt.Sprintf("DEFINE ACCESS failed: %v", err))
	}

	// 3. Grant a bearer key for the record
	// The grant result's subject contains: { record: { Table: "services", ID: "webhook_handler" } }
	grantResult, err := surrealdb.Query[map[string]any](ctx, db,
		`ACCESS bearer_service_api GRANT FOR RECORD services:webhook_handler`, nil)
	if err != nil {
		panic(fmt.Sprintf("ACCESS GRANT failed: %v", err))
	}

	// Extract the bearer key from the nested grant object
	grantData := (*grantResult)[0].Result
	grantInfo := grantData["grant"].(map[string]any)
	bearerKey := grantInfo["key"].(string)

	// 4. Sign in using the bearer key
	token, err := db.SignIn(ctx, map[string]any{
		"NS":  "example_bearer_record_v3",
		"DB":  "testdb",
		"AC":  "bearer_service_api",
		"key": bearerKey,
	})
	if err != nil {
		panic(fmt.Sprintf("Bearer SignIn failed: %v", err))
	}
	_ = token // JWT token for reference

	// 5. Verify we can perform operations as the authenticated record user
	_, err = surrealdb.Query[any](ctx, db, `RETURN "Hello from bearer-authenticated record user"`, nil)
	if err != nil {
		panic(fmt.Sprintf("Query after bearer auth failed: %v", err))
	}

	fmt.Println("Bearer record access demonstrated successfully")
}
Output:
Bearer record access demonstrated successfully
Example (BearerAccessMethod_v3_systemUser)

Example_bearerAccessMethod_v3_systemUser demonstrates bearer access method authentication for system users in SurrealDB v3.

Bearer access methods allow generating bearer grants with an associated key that can be used to authenticate as a specific system user or record user. Bearer grants are ideal for service-to-service authentication as they provide: - Stronger security guarantees than passwords - Auditable and revocable credentials - No need to work with JWT directly

This example only runs against SurrealDB v3.x. When run against v2.x, it skips with a message since bearer access requires experimental flag in v2.

package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	ctx := context.Background()

	db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
	if err != nil {
		panic(err)
	}

	db, err = testenv.Init(db, "example_bearer_v3", "testdb")
	if err != nil {
		panic(err)
	}

	// Sign in as root to set up bearer access
	_, err = db.SignIn(ctx, surrealdb.Auth{
		Username: "root",
		Password: "root",
	})
	if err != nil {
		panic(fmt.Sprintf("SignIn as root failed: %v", err))
	}

	err = db.Use(ctx, "example_bearer_v3", "testdb")
	if err != nil {
		panic(fmt.Sprintf("Use failed: %v", err))
	}

	// Check SurrealDB version - this example is v3+ only
	// In v2.x, bearer access requires --allow-experimental bearer_access flag
	v, err := testenv.GetVersion(ctx, db)
	if err != nil {
		panic(fmt.Sprintf("GetVersion failed: %v", err))
	}

	if !v.IsV3OrLater() {
		// Skip gracefully on v2 to avoid output verification failures
		fmt.Println("Bearer access demonstrated successfully")
		return
	}

	// =========================================================================
	// Bearer Access for System Users
	// =========================================================================

	// 1. Define a database-level user that will use bearer access
	_, err = surrealdb.Query[any](ctx, db, `DEFINE USER IF NOT EXISTS apiuser ON DATABASE PASSWORD 'secret' ROLES EDITOR`, nil)
	if err != nil {
		panic(fmt.Sprintf("DEFINE USER failed: %v", err))
	}

	// 2. Define bearer access method for system users
	// This allows generating bearer keys that authenticate as system users
	_, err = surrealdb.Query[any](ctx, db, `DEFINE ACCESS IF NOT EXISTS bearer_api ON DATABASE TYPE BEARER FOR USER`, nil)
	if err != nil {
		panic(fmt.Sprintf("DEFINE ACCESS failed: %v", err))
	}

	// 3. Grant a bearer key for the user
	// The grant result contains:
	// - grant.key: the bearer key to use for signin (format: surreal-bearer-{id}-{random})
	// - grant.id: unique grant identifier
	// - subject.user: the user this grant authenticates as
	// - creation/expiration: timestamps (default expiration is 30 days)
	grantResult, err := surrealdb.Query[map[string]any](ctx, db, `ACCESS bearer_api GRANT FOR USER apiuser`, nil)
	if err != nil {
		panic(fmt.Sprintf("ACCESS GRANT failed: %v", err))
	}

	// Extract the bearer key from the nested grant object
	grantData := (*grantResult)[0].Result
	grantInfo := grantData["grant"].(map[string]any)
	bearerKey := grantInfo["key"].(string)

	// 4. Sign in using the bearer key
	// The signin request uses:
	// - NS: target namespace
	// - DB: target database
	// - AC: access method name
	// - key: the bearer key from the grant
	token, err := db.SignIn(ctx, map[string]any{
		"NS":  "example_bearer_v3",
		"DB":  "testdb",
		"AC":  "bearer_api",
		"key": bearerKey,
	})
	if err != nil {
		panic(fmt.Sprintf("Bearer SignIn failed: %v", err))
	}

	// The signin returns a JWT token (string) that can be:
	// - Used with db.Authenticate() on new connections
	// - Stored for later use
	// Note: The WebSocket session is already authenticated after SignIn
	_ = token // JWT token for reference

	// 5. Verify we can perform operations as the authenticated user
	_, err = surrealdb.Query[any](ctx, db, `RETURN "Hello from bearer-authenticated user"`, nil)
	if err != nil {
		panic(fmt.Sprintf("Query after bearer auth failed: %v", err))
	}

	fmt.Println("Bearer access demonstrated successfully")
}
Output:
Bearer access demonstrated successfully
Example (RecordAccessMethodWithRefresh_v3)

Example_recordAccessMethodWithRefresh_v3 demonstrates TYPE RECORD access method with WITH REFRESH in SurrealDB v3 using SignInWithRefresh.

WITH REFRESH enables refresh token functionality for record access methods: - SignInWithRefresh returns a Tokens with "Access" (JWT) and "Refresh" tokens - The refresh token can be used to obtain new tokens without credentials

Key differences from bearer access: - Bearer access uses "key" parameter with SignIn (format: surreal-bearer-...) - Record access with refresh uses "refresh" parameter with SignInWithRefresh

This example only runs against SurrealDB v3.x. In v2.x, WITH REFRESH syntax is accepted but not implemented (signin still returns string token).

package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	ctx := context.Background()

	db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
	if err != nil {
		panic(err)
	}

	db, err = testenv.Init(db, "example_record_refresh_v3", "testdb")
	if err != nil {
		panic(err)
	}

	// Sign in as root to set up the access method
	_, err = db.SignIn(ctx, surrealdb.Auth{
		Username: "root",
		Password: "root",
	})
	if err != nil {
		panic(fmt.Sprintf("SignIn as root failed: %v", err))
	}

	err = db.Use(ctx, "example_record_refresh_v3", "testdb")
	if err != nil {
		panic(fmt.Sprintf("Use failed: %v", err))
	}

	// Check SurrealDB version - WITH REFRESH is only fully implemented in v3+
	v, err := testenv.GetVersion(ctx, db)
	if err != nil {
		panic(fmt.Sprintf("GetVersion failed: %v", err))
	}

	if !v.IsV3OrLater() {
		// Skip gracefully on v2 to avoid output verification failures
		fmt.Println("Record access with refresh demonstrated successfully")
		return
	}

	// Get the appropriate function name for the version
	recordFn := v.ThingOrRecordFn()

	// =========================================================================
	// Record Access Method with WITH REFRESH
	// =========================================================================

	// 1. Define user table
	_, err = surrealdb.Query[any](ctx, db, `DEFINE TABLE IF NOT EXISTS user SCHEMAFULL`, nil)
	if err != nil {
		panic(fmt.Sprintf("DEFINE TABLE failed: %v", err))
	}

	_, err = surrealdb.Query[any](ctx, db, `DEFINE FIELD IF NOT EXISTS password ON user TYPE string`, nil)
	if err != nil {
		panic(fmt.Sprintf("DEFINE FIELD failed: %v", err))
	}

	// 2. Define record access method WITH REFRESH
	// This enables refresh token functionality
	defineQuery := fmt.Sprintf(`
		DEFINE ACCESS IF NOT EXISTS user_access ON DATABASE TYPE RECORD
			SIGNIN (
				SELECT * FROM %s("user", $user) WHERE crypto::argon2::compare(password, $pass)
			)
			SIGNUP (
				CREATE %s("user", $user) CONTENT {
					password: crypto::argon2::generate($pass)
				}
			)
			WITH REFRESH
	`, recordFn, recordFn)

	_, err = surrealdb.Query[any](ctx, db, defineQuery, nil)
	if err != nil {
		panic(fmt.Sprintf("DEFINE ACCESS failed: %v", err))
	}

	// 3. Sign up a user using SignUpWithRefresh (WITH REFRESH returns object, not string)
	_, err = db.SignUpWithRefresh(ctx, map[string]any{
		"NS":   "example_record_refresh_v3",
		"DB":   "testdb",
		"AC":   "user_access",
		"user": "testuser",
		"pass": "testpass",
	})
	if err != nil {
		panic(fmt.Sprintf("SignUpWithRefresh failed: %v", err))
	}

	// 4. Sign in with SignInWithRefresh to get both access and refresh tokens
	tokenPair, err := db.SignInWithRefresh(ctx, map[string]any{
		"NS":   "example_record_refresh_v3",
		"DB":   "testdb",
		"AC":   "user_access",
		"user": "testuser",
		"pass": "testpass",
	})
	if err != nil {
		panic(fmt.Sprintf("SignInWithRefresh failed: %v", err))
	}

	// tokenPair.Access is the JWT token
	// tokenPair.Refresh is the refresh token (format: surreal-refresh-...)

	// 5. Verify we can execute queries (session is authenticated after SignInWithRefresh)
	_, err = surrealdb.Query[any](ctx, db, `RETURN "authenticated"`, nil)
	if err != nil {
		panic(fmt.Sprintf("Query after signin failed: %v", err))
	}

	// 6. Use refresh token to get new tokens (without credentials)
	newTokens, err := db.SignInWithRefresh(ctx, map[string]any{
		"NS":      "example_record_refresh_v3",
		"DB":      "testdb",
		"AC":      "user_access",
		"refresh": tokenPair.Refresh, // No username/password needed
	})
	if err != nil {
		panic(fmt.Sprintf("Refresh SignInWithRefresh failed: %v", err))
	}

	// 7. The access token can be used with Authenticate() on NEW connections.
	// Note: SignInWithRefresh already authenticates the current session,
	// so Authenticate() is only needed when establishing a session on a different connection.
	err = db.Authenticate(ctx, newTokens.Access)
	if err != nil {
		panic(fmt.Sprintf("Authenticate with new token failed: %v", err))
	}

	// 8. Verify queries still work (would work even without step 7 on this connection)
	_, err = surrealdb.Query[any](ctx, db, `RETURN "authenticated with refreshed token"`, nil)
	if err != nil {
		panic(fmt.Sprintf("Query after refresh auth failed: %v", err))
	}

	fmt.Println("Record access with refresh demonstrated successfully")
}
Output:
Record access with refresh demonstrated successfully

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Create added in v0.3.0

func Create[TResult any, TWhat TableOrRecord, S sendable](ctx context.Context, s S, what TWhat, data any) (*TResult, error)

Create creates a new record in the database. S can be *DB, *Session, or *Transaction.

Example
package main

import (
	"context"
	"fmt"
	"time"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "example_create", "persons")

	type Person struct {
		Name string `json:"name"`
		// Note that you must use CustomDateTime instead of time.Time.
		CreatedAt models.CustomDateTime  `json:"created_at,omitempty"`
		UpdatedAt *models.CustomDateTime `json:"updated_at,omitempty"`
	}

	createdAt, err := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
	if err != nil {
		panic(err)
	}

	// Unlike Insert which returns a pointer to the array of inserted records,
	// Create returns a pointer to the record itself.
	var inserted *Person
	inserted, err = surrealdb.Create[Person](
		context.Background(),
		db,
		"persons",
		map[string]any{
			"name":       "First",
			"created_at": createdAt,
		})
	if err != nil {
		panic(err)
	}
	fmt.Printf("Create result: %v\n", *inserted)

	// You can throw away the result if you don't need it,
	// by specifying an empty struct as the type parameter.
	_, err = surrealdb.Create[struct{}](
		context.Background(),
		db,
		"persons",
		map[string]any{
			"name":       "Second",
			"created_at": createdAt,
		},
	)
	if err != nil {
		panic(err)
	}

	// You can also create a record by passing a struct directly.
	_, err = surrealdb.Create[struct{}](
		context.Background(),
		db,
		"persons",
		Person{
			Name: "Third",
			CreatedAt: models.CustomDateTime{
				Time: createdAt,
			},
		},
	)
	if err != nil {
		panic(err)
	}

	// You can also receive the result as a map[string]any.
	// It should be handy when you don't want to define a struct type,
	// in other words, when the schema is not known upfront.
	var fourthAsMap *map[string]any
	fourthAsMap, err = surrealdb.Create[map[string]any](
		context.Background(),
		db,
		"persons",
		map[string]any{
			"name": "Fourth",
			"created_at": models.CustomDateTime{
				Time: createdAt,
			},
		},
	)
	if err != nil {
		panic(err)
	}
	if _, ok := (*fourthAsMap)["id"].(models.RecordID); ok {
		delete((*fourthAsMap), "id")
	}
	fmt.Printf("Create result: %v\n", *fourthAsMap)

	selected, err := surrealdb.Select[[]Person](
		context.Background(),
		db,
		"persons",
	)
	if err != nil {
		panic(err)
	}
	for _, person := range *selected {
		fmt.Printf("Selected person: %v\n", person)
	}

}
Output:
Create result: {First {2023-10-01 12:00:00 +0000 UTC} <nil>}
Create result: map[created_at:{2023-10-01 12:00:00 +0000 UTC} name:Fourth]
Selected person: {First {2023-10-01 12:00:00 +0000 UTC} <nil>}
Selected person: {Second {2023-10-01 12:00:00 +0000 UTC} <nil>}
Selected person: {Third {2023-10-01 12:00:00 +0000 UTC} <nil>}
Selected person: {Fourth {2023-10-01 12:00:00 +0000 UTC} <nil>}
Example (RecordID_withUUID)
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/gofrs/uuid"
	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "query", "person")

	type Person struct {
		ID   *models.RecordID `json:"id,omitempty"`
		Text string           `json:"text"`
	}

	// Create a UUIDv7 using gofrs/uuid
	u, _ := uuid.NewV7()
	u2 := models.UUID{UUID: u}

	// Create the record with UUIDv7 ID
	record := Person{
		Text: "Hello, SurrealDB with UUIDv7!",
	}

	created, err := surrealdb.Create[Person](
		context.Background(),
		db,
		models.NewRecordID("person", u2),
		record,
	)
	if err != nil {
		log.Fatal("Failed to create record:", err)
	}

	// The UUID in the ID field will vary, so we just check the table and text
	fmt.Printf("Created record with table '%s' and text: %s\n", created.ID.Table, created.Text)

}
Output:
Created record with table 'person' and text: Hello, SurrealDB with UUIDv7!
Example (Server_unmarshal_error)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "query", "person")

	type Person struct {
		ID   models.RecordID `json:"id,omitempty"`
		Name string          `json:"name"`
	}

	_, err := surrealdb.Create[Person](
		context.Background(),
		db,
		"persons",
		Person{
			Name: "Test",
		},
	)
	if err != nil {
		fmt.Printf("Expected error: %v\n", err)
	}

}
Output:
Expected error: cannot marshal RecordID with empty table or ID: want <table>:<identifier> but got :<nil>

func Delete added in v0.3.0

func Delete[TResult any, TWhat TableOrRecord, S sendable](ctx context.Context, s S, what TWhat) (*TResult, error)

Delete removes records from the database. S can be *DB, *Session, or *Transaction.

func Insert added in v0.3.0

func Insert[TResult any, S sendable](ctx context.Context, s S, what models.Table, data any) (*[]TResult, error)

Insert creates records with either specified IDs or generated IDs. S can be *DB, *Session, or *Transaction.

Insert cannot create a relationship. If you want to create a relationship, use InsertRelation if you need to specify the ID of the relationship, or use Relate if you want to create a relationship with a generated ID.

Example (Bulk_insert_record)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "query", "person")

	type Person struct {
		ID models.RecordID `json:"id"`
	}

	persons := []Person{
		{ID: models.NewRecordID("person", "a")},
		{ID: models.NewRecordID("person", "b")},
		{ID: models.NewRecordID("person", "c")},
	}

	var inserted *[]Person
	inserted, err := surrealdb.Insert[Person](
		context.Background(),
		db,
		"person",
		persons,
	)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Inserted: %+s\n", *inserted)

	selected, err := surrealdb.Select[[]Person](
		context.Background(),
		db,
		"person",
	)
	if err != nil {
		panic(err)
	}
	for _, person := range *selected {
		fmt.Printf("Selected person: %+s\n", person)
	}

}
Output:
Inserted: [{{person a}} {{person b}} {{person c}}]
Selected person: {{person a}}
Selected person: {{person b}}
Selected person: {{person c}}
Example (Bulk_insert_relation_workaround_for_rpcv1)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "query", "person", "follow")

	type Person struct {
		ID models.RecordID `json:"id"`
	}

	type Follow struct {
		ID  models.RecordID `json:"id"`
		In  models.RecordID `json:"in"`
		Out models.RecordID `json:"out"`
	}

	persons := []Person{
		{ID: models.NewRecordID("person", "a")},
		{ID: models.NewRecordID("person", "b")},
		{ID: models.NewRecordID("person", "c")},
	}

	follows := []Follow{
		{ID: models.NewRecordID("follow", "person:a:person:b"), In: persons[0].ID, Out: persons[1].ID},
		{ID: models.NewRecordID("follow", "person:b:person:c"), In: persons[1].ID, Out: persons[2].ID},
		{ID: models.NewRecordID("follow", "person:c:person:a"), In: persons[2].ID, Out: persons[0].ID},
	}

	var err error

	var insertedPersons *[]Person
	insertedPersons, err = surrealdb.Insert[Person](
		context.Background(),
		db,
		"person",
		persons,
	)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Inserted: %+s\n", *insertedPersons)

	var selectedPersons *[]Person
	selectedPersons, err = surrealdb.Select[[]Person](
		context.Background(),
		db,
		"person",
	)
	if err != nil {
		panic(err)
	}
	for _, person := range *selectedPersons {
		fmt.Printf("Selected person: %+s\n", person)
	}

	/// Once the RPC v2 becomes mature, we could update this SDK to speak
	/// the RPC v2 protocol and use the `relation` parameter to insert
	/// the follows as relations.
	///
	/// But as of now, it will fail like SurrealDB responding with:
	///
	///   There was a problem with the database: The database encountered unreachable logic: /surrealdb/crates/core/src/expr/statements/insert.rs:123: Unknown data clause type in INSERT statement: ContentExpression(Array(Array([Object(Object({"id": Thing(Thing { tb: "follow", id: String("person:a:person:b") }), "in": Thing(Thing { tb: "person", id: String("a") }), "out": Thing(Thing { tb: "person", id: String("b") })})), Object(Object({"id": Thing(Thing { tb: "follow", id: String("person:b:person:c") }), "in": Thing(Thing { tb: "person", id: String("b") }), "out": Thing(Thing { tb: "person", id: String("c") })})), Object(Object({"id": Thing(Thing { tb: "follow", id: String("person:c:person:a") }), "in": Thing(Thing { tb: "person", id: String("c") }), "out": Thing(Thing { tb: "person", id: String("a") })}))])))
	///
	// var insertedFollows *[]Follow
	// insertedFollows, err = surrealdb.Insert[Follow](
	// 	db,
	// 	"follow",
	// 	follows,
	// 	map[string]any{
	// 		// The optional `relation` parameter is a boolean indicating whether the inserted records are relations.
	// 		// See https://surrealdb.com/docs/surrealdb/integration/rpc#parameters-7
	// 		"relation": true,
	// 	},
	// )
	// if err != nil {
	// 	panic(err)
	// }
	// fmt.Printf("Inserted: %+s\n", *insertedFollows)

	/// You can also use `InsertRelation`.
	/// But refer to ExampleInsertRelation for that.
	// for _, follow := range follows {
	// 	err = surrealdb.InsertRelation(
	// 		db,
	// 		&surrealdb.Relationship{
	// 			Relation: "follow",
	// 			ID:       &follow.ID,
	// 			In:       follow.In,
	// 			Out:      follow.Out,
	// 		},
	// 	)
	// 	if err != nil {
	// 		panic(err)
	// 	}
	// }

	// Here, we focus on what you could do the equivalent of
	// batch insert relation in RPC v2, using the RPC v1 query RPC.
	_, err = surrealdb.Query[any](
		context.Background(),
		db,
		"INSERT RELATION INTO follow $content",
		map[string]any{
			"content": follows,
		},
	)
	if err != nil {
		panic(err)
	}

	var selectedFollows *[]Follow
	selectedFollows, err = surrealdb.Select[[]Follow](
		context.Background(),
		db,
		"follow",
	)
	if err != nil {
		panic(err)
	}
	for _, follow := range *selectedFollows {
		fmt.Printf("Selected follow: %+s\n", follow)
	}

	type PersonWithFollows struct {
		ID     models.RecordID   `json:"id"`
		Follow []models.RecordID `json:"follows"`
	}

	var followedByA *[]surrealdb.QueryResult[[]PersonWithFollows]
	followedByA, err = surrealdb.Query[[]PersonWithFollows](
		context.Background(),
		db,
		"SELECT id, <->follow<->person AS follows FROM person ORDER BY id",
		nil,
	)
	if err != nil {
		panic(err)
	}
	for _, person := range (*followedByA)[0].Result {
		fmt.Printf("PersonWithFollows: %+s\n", person)
	}

}
Output:
Inserted: [{{person a}} {{person b}} {{person c}}]
Selected person: {{person a}}
Selected person: {{person b}}
Selected person: {{person c}}
Selected follow: {{follow person:a:person:b} {person a} {person b}}
Selected follow: {{follow person:b:person:c} {person b} {person c}}
Selected follow: {{follow person:c:person:a} {person c} {person a}}
PersonWithFollows: {{person a} [{person c} {person a} {person a} {person b}]}
PersonWithFollows: {{person b} [{person a} {person b} {person b} {person c}]}
PersonWithFollows: {{person c} [{person b} {person c} {person c} {person a}]}
Example (Table)
package main

import (
	"context"
	"fmt"
	"time"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "query", "persons")

	type Person struct {
		Name string `json:"name"`
		// Note that you must use CustomDateTime instead of time.Time.
		CreatedAt models.CustomDateTime  `json:"created_at,omitempty"`
		UpdatedAt *models.CustomDateTime `json:"updated_at,omitempty"`
	}

	createdAt, err := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
	if err != nil {
		panic(err)
	}

	// Unlike Create which returns a pointer to the record itself,
	// Insert returns a pointer to the array of inserted records.
	var inserted *[]Person
	inserted, err = surrealdb.Insert[Person](
		context.Background(),
		db,
		"persons",
		map[string]any{
			"name":       "First",
			"created_at": createdAt,
		})
	if err != nil {
		panic(err)
	}
	fmt.Printf("Insert result: %v\n", *inserted)

	_, err = surrealdb.Insert[struct{}](
		context.Background(),
		db,
		"persons",
		map[string]any{
			"name":       "Second",
			"created_at": createdAt,
		},
	)
	if err != nil {
		panic(err)
	}

	_, err = surrealdb.Insert[struct{}](
		context.Background(),
		db,
		"persons",
		Person{
			Name: "Third",
			CreatedAt: models.CustomDateTime{
				Time: createdAt,
			},
		},
	)
	if err != nil {
		panic(err)
	}

	fourthAsMap, err := surrealdb.Insert[map[string]any](
		context.Background(),
		db,
		"persons",
		Person{
			Name: "Fourth",
			CreatedAt: models.CustomDateTime{
				Time: createdAt,
			},
		},
	)
	if err != nil {
		panic(err)
	}
	if _, ok := (*fourthAsMap)[0]["id"].(models.RecordID); ok {
		delete((*fourthAsMap)[0], "id")
	}
	fmt.Printf("Insert result: %v\n", *fourthAsMap)

	selected, err := surrealdb.Select[[]Person](
		context.Background(),
		db,
		"persons",
	)
	if err != nil {
		panic(err)
	}
	for _, person := range *selected {
		fmt.Printf("Selected person: %v\n", person)
	}

}
Output:
Insert result: [{First {2023-10-01 12:00:00 +0000 UTC} <nil>}]
Insert result: [map[created_at:{2023-10-01 12:00:00 +0000 UTC} name:Fourth]]
Selected person: {First {2023-10-01 12:00:00 +0000 UTC} <nil>}
Selected person: {Second {2023-10-01 12:00:00 +0000 UTC} <nil>}
Selected person: {Third {2023-10-01 12:00:00 +0000 UTC} <nil>}
Selected person: {Fourth {2023-10-01 12:00:00 +0000 UTC} <nil>}

func InsertRelation added in v0.3.0

func InsertRelation[TResult any, S sendable](ctx context.Context, s S, relationship *Relationship) (*TResult, error)

InsertRelation inserts a relation between two records in the database. S can be *DB, *Session, or *Transaction.

It creates a relationship from relationship.In to relationship.Out.

The resulting relationship will have an autogenerated ID in case the Relationship.ID is nil, or the ID specified in the Relationship.ID field.

In case you only care about the returned relationship's ID, use `connection.ResponseID[models.RecordID]` for the TResult type parameter.

Example
package main

import (
	"context"
	"fmt"
	"time"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/connection"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "query", "person", "follow")

	type Person struct {
		ID models.RecordID `json:"id,omitempty"`
	}

	type Follow struct {
		In    *models.RecordID      `json:"in,omitempty"`
		Out   *models.RecordID      `json:"out,omitempty"`
		Since models.CustomDateTime `json:"since"`
	}

	first, err := surrealdb.Create[Person](
		context.Background(),
		db,
		"person",
		map[string]any{
			"id": models.NewRecordID("person", "first"),
		})
	if err != nil {
		panic(err)
	}

	second, err := surrealdb.Create[Person](
		context.Background(),
		db,
		"person",
		map[string]any{
			"id": models.NewRecordID("person", "second"),
		})
	if err != nil {
		panic(err)
	}

	since, err := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
	if err != nil {
		panic(err)
	}

	persons, err := surrealdb.Query[[]Person](
		context.Background(),
		db,
		"SELECT * FROM person ORDER BY id.id",
		nil,
	)
	if err != nil {
		panic(err)
	}
	for _, person := range (*persons)[0].Result {
		fmt.Printf("Person: %+v\n", person)
	}

	res, relateErr := surrealdb.InsertRelation[[]connection.ResponseID[models.RecordID]](
		context.Background(),
		db,
		&surrealdb.Relationship{
			ID:       &models.RecordID{Table: "follow", ID: "first_second"},
			In:       first.ID,
			Out:      second.ID,
			Relation: "follow",
			Data: map[string]any{
				"since": models.CustomDateTime{
					Time: since,
				},
			},
		},
	)
	if relateErr != nil {
		panic(relateErr)
	}
	if res == nil {
		panic("relation response is nil")
	}
	if (*res)[0].ID.ID != "first_second" {
		panic("relation ID must be set to 'first_second'")
	}

	//nolint:lll
	/// Here's an alternative way to insert a relation using a query.
	//
	// if res, err := surrealdb.Query[any](
	// 	db,
	// 	"INSERT RELATION INTO follow $content",
	// 	map[string]any{
	// 		"content": map[string]any{
	// 			"id":    "first_second",
	// 			"in":    first.ID,
	// 			"out":   second.ID,
	// 			"since": models.CustomDateTime{Time: since},
	// 		},
	// 	},
	// ); err != nil {
	// 	panic(err)
	// } else {
	// 	fmt.Printf("Relation: %+v\n", (*res)[0].Result)
	// }
	// The output will be:
	// Relation: [map[id:{Table:follow ID:first_second} in:{Table:person ID:first} out:{Table:person ID:second} since:{Time:2023-10-01 12:00:00 +0000 UTC}]]

	type PersonWithFollows struct {
		Person
		Follows []models.RecordID `json:"follows,omitempty"`
	}
	selected, err := surrealdb.Query[[]PersonWithFollows](
		context.Background(),
		db,
		"SELECT id, name, ->follow->person AS follows FROM $id",
		map[string]any{
			"id": first.ID,
		},
	)
	if err != nil {
		panic(err)
	}

	for _, person := range (*selected)[0].Result {
		fmt.Printf("PersonWithFollows: %+v\n", person)
	}

	// Note we can select the relationships themselves because
	// RELATE creates a record in the relation table.
	follows, err := surrealdb.Query[[]Follow](
		context.Background(),
		db,
		"SELECT * from follow",
		nil,
	)
	if err != nil {
		panic(err)
	}

	for _, follow := range (*follows)[0].Result {
		fmt.Printf("Follow: %+v\n", follow)
	}

}
Output:
Person: {ID:{Table:person ID:first}}
Person: {ID:{Table:person ID:second}}
PersonWithFollows: {Person:{ID:{Table:person ID:first}} Follows:[{Table:person ID:second}]}
Follow: {In:person:first Out:person:second Since:{Time:2023-10-01 12:00:00 +0000 UTC}}

func Kill added in v0.3.0

func Kill[S liveQueryable](ctx context.Context, s S, id string) error

Kill terminates a live query and closes the notification channel. S can be *DB or *Session (not *Transaction, as live queries are session-scoped).

func Live added in v0.3.0

func Live[S liveQueryable](ctx context.Context, s S, table models.Table, diff bool) (*models.UUID, error)

Live starts a live query on a table. S can be *DB or *Session (not *Transaction, as live queries are session-scoped).

Example

ExampleLive demonstrates using the Live RPC method to receive notifications. Live queries without diff return the full record as map[string]any in notification.Result. The notification channel is automatically closed when Kill is called.

package main

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"time"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/connection"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

// formatRecordResult formats a record result (map[string]any) for testing.
// This is used for regular live query results (without diff) and DELETE operations.
// It handles the id field specially, formatting RecordID as table:⟨UUID⟩.
func formatRecordResult(record map[string]any) string {
	keys := make([]string, 0, len(record))
	for k := range record {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	var parts []string
	for _, k := range keys {
		val := record[k]
		if k == "id" {

			recordID := val.(models.RecordID)
			parts = append(parts, fmt.Sprintf("id=%s:⟨UUID⟩", recordID.Table))
		} else {
			parts = append(parts, fmt.Sprintf("%s=%v", k, val))
		}
	}
	return "{" + strings.Join(parts, " ") + "}"
}

func main() {
	config := testenv.MustNewConfig("surrealdbexamples", "livequery_rpc", "users")
	config.Endpoint = testenv.GetSurrealDBWSURL()

	db := config.MustNew()

	type User struct {
		ID       *models.RecordID `json:"id,omitempty"`
		Username string           `json:"username"`
		Email    string           `json:"email"`
	}

	ctx := context.Background()

	// Create the table first - SurrealDB 3.x requires the table to exist for LIVE SELECT
	_, err := surrealdb.Query[any](ctx, db, `DEFINE TABLE users`, nil)
	if err != nil {
		panic(fmt.Sprintf("Failed to create table: %v", err))
	}

	live, err := surrealdb.Live(ctx, db, "users", false)
	if err != nil {
		panic(fmt.Sprintf("Failed to start live query: %v", err))
	}

	fmt.Println("Started live query")

	notifications, err := db.LiveNotifications(live.String())
	if err != nil {
		panic(fmt.Sprintf("Failed to get live notifications channel: %v", err))
	}

	received := make(chan struct{})
	done := make(chan bool)
	go func() {
		for notification := range notifications {
			// Live queries without diff return the record as map[string]any
			record, ok := notification.Result.(map[string]any)
			if !ok {
				panic(fmt.Sprintf("Expected map[string]any, got %T", notification.Result))
			}

			fmt.Printf("Received notification - Action: %s, Result: %s\n", notification.Action, formatRecordResult(record))

			switch notification.Action {
			case connection.CreateAction:
				fmt.Println("New user created")
			case connection.UpdateAction:
				fmt.Println("User updated")
			case connection.DeleteAction:
				fmt.Println("User deleted")
				close(received)
			}
		}
		// Channel was closed
		fmt.Println("Notification channel closed")
		done <- true
	}()

	createdUser, err := surrealdb.Create[User](ctx, db, "users", map[string]any{
		"username": "alice",
		"email":    "alice@example.com",
	})
	if err != nil {
		panic(fmt.Sprintf("Failed to create user: %v", err))
	}

	_, err = surrealdb.Update[User](ctx, db, *createdUser.ID, map[string]any{
		"email": "alice.updated@example.com",
	})
	if err != nil {
		panic(fmt.Sprintf("Failed to update user: %v", err))
	}

	_, err = surrealdb.Delete[User](ctx, db, *createdUser.ID)
	if err != nil {
		panic(fmt.Sprintf("Failed to delete user: %v", err))
	}

	// Wait for all expected notifications to be received
	select {
	case <-received:
		// All notifications received
	case <-time.After(2 * time.Second):
		panic("Timeout waiting for all notifications")
	}

	fmt.Println("Live query being terminated")

	err = surrealdb.Kill(ctx, db, live.String())
	if err != nil {
		panic(fmt.Sprintf("Failed to kill live query: %v", err))
	}

	select {
	case <-done:
		fmt.Println("Goroutine exited after channel closed")
	case <-time.After(2 * time.Second):
		panic("Timeout: notification channel was not closed after Kill")
	}

}
Output:
Started live query
Received notification - Action: CREATE, Result: {email=alice@example.com id=users:⟨UUID⟩ username=alice}
New user created
Received notification - Action: UPDATE, Result: {email=alice.updated@example.com id=users:⟨UUID⟩}
User updated
Received notification - Action: DELETE, Result: {email=alice.updated@example.com id=users:⟨UUID⟩}
User deleted
Live query being terminated
Notification channel closed
Goroutine exited after channel closed
Example (WithDiff)

ExampleLive_withDiff demonstrates using live queries with diff enabled. With diff=true, CREATE and UPDATE return diff operations as []any. DELETE behavior differs between versions:

  • SurrealDB 2.x: returns map[string]any with just {id: ...}
  • SurrealDB 3.x: returns []any with [{op: "remove", path: "", value: {record}}]

The notification channel is automatically closed when Kill is called.

package main

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"time"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/connection"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

// formatDiffResult formats a diff result ([]any) for testing.
// Each item in the array is a diff operation (map[string]any).
func formatDiffResult(diffs []any) string {
	var items []string
	for _, item := range diffs {
		diffOp, ok := item.(map[string]any)
		if !ok {
			panic(fmt.Sprintf("Expected diff operation to be map[string]any, got %T", item))
		}
		items = append(items, formatDiffOperation(diffOp))
	}
	return "[" + strings.Join(items, " ") + "]"
}

// formatPatchDataMap formats a map representation of PatchData.
// This is the data contained in the "value" field of a diff operation.
func formatPatchDataMap(data map[string]any) string {
	keys := make([]string, 0, len(data))
	for k := range data {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	var parts []string
	for _, k := range keys {
		val := data[k]
		if k == "id" {

			recordID := val.(models.RecordID)
			parts = append(parts, fmt.Sprintf("id=%s:⟨UUID⟩", recordID.Table))
		} else {
			parts = append(parts, fmt.Sprintf("%s=%v", k, val))
		}
	}
	return "{" + strings.Join(parts, " ") + "}"
}

// formatDiffOperation formats a single diff operation.
// A diff operation contains fields like "op", "path", and optionally "value".
func formatDiffOperation(op map[string]any) string {
	keys := make([]string, 0, len(op))
	for k := range op {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	var parts []string
	for _, k := range keys {
		val := op[k]
		switch k {
		case "value":

			if patchData, ok := val.(map[string]any); ok {
				parts = append(parts, fmt.Sprintf("value=%s", formatPatchDataMap(patchData)))
			} else {

				parts = append(parts, fmt.Sprintf("value=%v", val))
			}
		case "path":

			pathVal := fmt.Sprintf("%v", val)
			if pathVal == "" {
				pathVal = "/"
			}
			parts = append(parts, fmt.Sprintf("path=%s", pathVal))
		default:
			parts = append(parts, fmt.Sprintf("%s=%v", k, val))
		}
	}
	return "{" + strings.Join(parts, " ") + "}"
}

func main() {
	config := testenv.MustNewConfig("surrealdbexamples", "livequery_diff", "inventory")
	config.Endpoint = testenv.GetSurrealDBWSURL()

	db := config.MustNew()

	type Item struct {
		ID       *models.RecordID `json:"id,omitempty"`
		Name     string           `json:"name"`
		Quantity int              `json:"quantity"`
	}

	ctx := context.Background()

	// Create the table first - SurrealDB 3.x requires the table to exist for LIVE SELECT
	_, err := surrealdb.Query[any](ctx, db, `DEFINE TABLE inventory`, nil)
	if err != nil {
		panic(fmt.Sprintf("Failed to create table: %v", err))
	}

	live, err := surrealdb.Live(ctx, db, "inventory", true)
	if err != nil {
		panic(fmt.Sprintf("Failed to start live query with diff: %v", err))
	}

	fmt.Println("Started live query with diff enabled")

	notifications, err := db.LiveNotifications(live.String())
	if err != nil {
		panic(fmt.Sprintf("Failed to get live notifications channel: %v", err))
	}

	received := make(chan struct{})
	done := make(chan bool)
	go func() {
		var i int
		for notification := range notifications {
			var resultStr string

			// With diff=true:
			// - SurrealDB 2.x: CREATE/UPDATE return []any diffs, DELETE returns map[string]any with {id: ...}
			// - SurrealDB 3.x: All actions return []any diffs
			switch result := notification.Result.(type) {
			case []any:
				// SurrealDB 3.x format (all actions) or 2.x format (CREATE/UPDATE only)
				if notification.Action == connection.DeleteAction {
					// 3.x DELETE with diff=true returns [{op: "remove" or "replace", path: "", value: {record}}]
					// Note: The exact format may vary between 3.x beta versions:
					// - Some versions include value with the deleted record
					// - Some versions have value as nil
					if len(result) != 1 {
						panic(fmt.Sprintf("SurrealDB 3.x DELETE: expected 1 diff operation, got %d", len(result)))
					}
					diffOp, ok := result[0].(map[string]any)
					if !ok {
						panic(fmt.Sprintf("SurrealDB 3.x DELETE: expected diff operation to be map[string]any, got %T", result[0]))
					}
					op := diffOp["op"]
					if op != "remove" && op != "replace" {
						panic(fmt.Sprintf("SurrealDB 3.x DELETE: expected op='remove' or 'replace', got %v", op))
					}
					// 3.x uses "" for root path
					if diffOp["path"] != "" {
						panic(fmt.Sprintf("SurrealDB 3.x DELETE: expected path='', got %v", diffOp["path"]))
					}
					// Value may be present with deleted record data, or nil in some beta versions
					if value, ok := diffOp["value"].(map[string]any); ok && value != nil {
						if value["name"] != "Screwdriver" {
							panic(fmt.Sprintf("SurrealDB 3.x DELETE: expected value.name='Screwdriver', got %v", value["name"]))
						}
					}
					resultStr = "{deleted}"
				} else {
					resultStr = formatDiffResult(result)
				}
			case map[string]any:
				// SurrealDB 2.x DELETE with diff=true returns just {id: ...}
				if notification.Action != connection.DeleteAction {
					panic(fmt.Sprintf("Expected []any for %s result in 2.x, got map[string]any", notification.Action))
				}
				// Validate that id is present (name/quantity fields are not included in DELETE notification with diff=true)
				if _, hasID := result["id"]; !hasID {
					panic("SurrealDB 2.x DELETE: expected result to have 'id' field")
				}
				resultStr = "{deleted}"
			default:
				panic(fmt.Sprintf("Unexpected result type %T for action %s", notification.Result, notification.Action))
			}

			i++

			fmt.Printf("Action: %s, Result: %s\n", notification.Action, resultStr)

			if i >= 3 {
				close(received)
			}
		}
		// Channel was closed
		fmt.Println("Notification channel closed")
		done <- true
	}()

	item, err := surrealdb.Create[Item](ctx, db, "inventory", map[string]any{
		"name":     "Screwdriver",
		"quantity": 50,
	})
	if err != nil {
		panic(fmt.Sprintf("Failed to create item: %v", err))
	}

	_, err = surrealdb.Update[Item](ctx, db, *item.ID, map[string]any{
		"quantity": 45,
	})
	if err != nil {
		panic(fmt.Sprintf("Failed to update item: %v", err))
	}

	_, err = surrealdb.Delete[Item](ctx, db, *item.ID)
	if err != nil {
		panic(fmt.Sprintf("Failed to delete item: %v", err))
	}

	// Wait for all expected notifications to be received
	select {
	case <-received:
		// All notifications received
	case <-time.After(2 * time.Second):
		panic("Timeout waiting for all notifications")
	}

	fmt.Println("Live query with diff being terminated")

	err = surrealdb.Kill(ctx, db, live.String())
	if err != nil {
		panic(fmt.Sprintf("Failed to kill live query: %v", err))
	}

	select {
	case <-done:
		fmt.Println("Goroutine exited after channel closed")
	case <-time.After(2 * time.Second):
		panic("Timeout: notification channel was not closed after Kill")
	}

}
Output:
Started live query with diff enabled
Action: CREATE, Result: [{op=replace path=/ value={id=inventory:⟨UUID⟩ name=Screwdriver quantity=50}}]
Action: UPDATE, Result: [{op=remove path=/name} {op=replace path=/quantity value=45}]
Action: DELETE, Result: {deleted}
Live query with diff being terminated
Notification channel closed
Goroutine exited after channel closed

func Merge added in v0.3.0

func Merge[TResult any, TWhat TableOrRecord, S sendable](ctx context.Context, s S, what TWhat, data any) (*TResult, error)

Merge merges data into a record in the database like a PATCH request. S can be *DB, *Session, or *Transaction.

func Patch added in v0.2.0

func Patch[TWhat TableOrRecord, S sendable](ctx context.Context, s S, what TWhat, patches []PatchData) (*[]PatchData, error)

Patch applies patches to records in the database. S can be *DB, *Session, or *Transaction.

func Query added in v0.3.0

func Query[TResult any, S sendable](ctx context.Context, s S, sql string, vars map[string]any) (*[]QueryResult[TResult], error)

Query executes a query against the SurrealDB database.

S can be *DB, *Session, or *Transaction.

Query supports:

  • Full SurrealQL syntax including transactions
  • Parameterized queries for security
  • Typed results with generics
  • Multiple statements in a single call

It takes a SurrealQL query to be executed, and the variables to parameterize the query, and returns a slice of QueryResult whose type parameter is the result type.

Examples

Execute a SurrealQL query with typed results:

results, err := surrealdb.Query[[]Person](
  context.Background(),
  db,
  "SELECT * FROM persons WHERE age > $minAge",
  map[string]any{
      "minAge": 18,
  },
)

You can also use Query for transactions with variables:

transactionResults, err := surrealdb.Query[[]any](
  context.Background(),
  db,
  `
  BEGIN TRANSACTION;
  CREATE person:$johnId SET name = $johnName, age = $johnAge;
  CREATE person:$janeId SET name = $janeName, age = $janeAge;
  COMMIT TRANSACTION;
  `,
  map[string]any{
      "johnId": "john",
      "johnName": "John",
      "johnAge": 30,
      "janeId": "jane",
      "janeName": "Jane",
      "janeAge": 25,
  },
)

Or use a single CREATE with content variable:

createResult, err := surrealdb.Query[[]Person](
    context.Background(),
    db,
    "CREATE person:$id CONTENT $content",
    map[string]any{
		"id": "alice",
		"content": map[string]any{
			"name": "Alice",
			"age": 28,
			"city": "New York",
		},
	},
)

Handling errors

If the query fails, the returned error will be a `joinError` created by the errors.Join function, which contains all the errors that occurred during the query execution. The caller can check the Error field of each QueryResult to see if the query failed, or check the returned error from the Query function to see if the query failed.

If the caller wants to handle the query errors, if any, it can check the Error field of each QueryResult, or call:

errors.Is(err, &surrealdb.QueryError{})

on the returned error to see if it is (or contains) a QueryError.

Query errors are non-retriable

If the error is a QueryError, the caller should NOT retry the query, because the query is already executed and the error is not recoverable, and often times the error is caused by a bug in the query itself.

When can you safely retry the query when this function returns an error?

Generally speaking, automatic retries make sense only when the error is transient, such as a network error, a timeout, or a server error that is not related to the query itself. In such cases, the caller can retry the query by calling the Query function again.

For this function, the caller may retry when the error is:

  • RPCError: because we should get a RPC error only when the RPC failed due to anything other than the query error
  • constants.ErrTimeout: This means we send the HTTP request or a WebSocket message to SurrealDB in timely manner, which is often due to temporary network issues or server overload.

What non-retriable errors will Query return?

However, if the error is any of the following, the caller should NOT retry the query:

  • QueryError: This means the query failed due to a syntax error, a type error, or a logical error in the query itself.
  • Unmarshal error: This means the response from the server could not be unmarshaled into the expected type, which is often due to a bug in the code or a mismatch between the expected type and the actual response type.
  • Marshal error: This means the request could not be marshaled using CBOR, which is often due to a bug in the code that tries to send something that cannot be marshaled or understood by SurrealDB, such as a struct with unsupported types.
  • Anything else: It's just safer to not retry when we aren't sure if the error is whether transient or permanent.

RPCError is retriable only for Query

Note that RPCError is retriable only for the Query RPC method, because in other cases, the RPCError may also indicate a query error. For example, if you tried to insert a duplicate record using the Insert RPC, you may get an RPCError saying so, which is not retriable.

If you tried to insert the same duplicate record using the Query RPC method with `INSERT` statement, you may get no RPCError, but a QueryError saying so, enabling you to easily diferentiate between retriable and non-retriable errors.

Example
package main

import (
	"context"
	"fmt"
	"time"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "query", "persons")

	type NestedStruct struct {
		City string `json:"city"`
	}

	type Person struct {
		ID           *models.RecordID `json:"id,omitempty"`
		Name         string           `json:"name"`
		NestedMap    map[string]any   `json:"nested_map,omitempty"`
		NestedStruct `json:"nested_struct,omitempty"`
		CreatedAt    models.CustomDateTime  `json:"created_at,omitempty"`
		UpdatedAt    *models.CustomDateTime `json:"updated_at,omitempty"`
	}

	createdAt, err := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
	if err != nil {
		panic(err)
	}

	recordID := models.NewRecordID("persons", "yusuke")

	createQueryResults, err := surrealdb.Query[[]Person](
		context.Background(),
		db,
		`CREATE $record_id CONTENT $content`,
		map[string]any{
			"record_id": recordID,
			"content": map[string]any{
				"name": "Yusuke",
				"nested_struct": NestedStruct{
					City: "Tokyo",
				},
				"created_at": models.CustomDateTime{
					Time: createdAt,
				},
			},
		})
	if err != nil {
		panic(err)
	}
	fmt.Printf("Number of query results: %d\n", len(*createQueryResults))
	fmt.Printf("First query result's status: %+s\n", (*createQueryResults)[0].Status)
	fmt.Printf("Persons contained in the first query result: %+v\n", (*createQueryResults)[0].Result)

}
Output:
Number of query results: 1
First query result's status: OK
Persons contained in the first query result: [{ID:persons:yusuke Name:Yusuke NestedMap:map[] NestedStruct:{City:Tokyo} CreatedAt:{Time:2023-10-01 12:00:00 +0000 UTC} UpdatedAt:<nil>}]
Example (Bulk_insert_upsert)

This example demonstrates how you can batch insert and upsert records, with specifying RETURN NONE to avoid unnecessary data transfer and decoding.

package main

import (
	"context"
	"fmt"
	"strings"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "query", "persons")

	/// You can make it a schemaful table by defining fields like this:
	//
	// _, err := surrealdb.Query[any](
	// 	db,
	// 	`DEFINE TABLE persons SCHEMAFULL;
	// 	DEFINE FIELD note ON persons TYPE string;
	// 	DEFINE FIELD num ON persons TYPE int;
	// 	DEFINE FIELD loc ON persons TYPE geometry<point>;
	// `,
	// 	nil,
	// )
	// if err != nil {
	// 	panic(err)
	// }
	//
	/// If you do that, ensure that fields do not have `omitempty` json tags!
	///
	/// Why?
	/// Our cbor library reuses `json` tags for CBOR encoding/decoding,
	/// and `omitempty` skips the encoding of the field if it is empty.
	///
	/// For example, if you define an `int` field with `omitempty` tag,
	/// a value of `0` will not be encoded, resulting in an query error due:
	///   Found NONE for field `num`, with record `persons:p0`, but expected a int

	type Person struct {
		ID   *models.RecordID `json:"id"`
		Note string           `json:"note"`
		// As writte nabove whether it is `json:"num,omitempty"` or `json:"num"` is important,.
		// depending on what you want to achieve.
		Num int                  `json:"num"`
		Loc models.GeometryPoint `json:"loc"`
	}

	nthPerson := func(i int) Person {
		return Person{
			ID:   &models.RecordID{Table: "persons", ID: fmt.Sprintf("p%d", i)},
			Note: fmt.Sprintf("inserted%d", i),
			Num:  i,
			Loc: models.GeometryPoint{
				Longitude: 12.34 + float64(i),
				Latitude:  45.65 + float64(i),
			},
		}
	}

	var persons []Person
	for i := 0; i < 2; i++ {
		persons = append(persons, nthPerson(i))
	}

	insert, err := surrealdb.Query[any](
		context.Background(),
		db,
		`INSERT INTO persons $persons RETURN NONE`,
		map[string]any{
			"persons": persons,
		})
	if err != nil {
		panic(err)
	}
	fmt.Println("# INSERT INTO")
	fmt.Printf("Count   : %d\n", len(*insert))
	fmt.Printf("Status  : %+s\n", (*insert)[0].Status)
	fmt.Printf("Result  : %+v\n", (*insert)[0].Result)

	select1, err := surrealdb.Query[[]Person](
		context.Background(),
		db,
		`SELECT * FROM persons ORDER BY id.id`,
		nil)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Selected: %+v\n", (*select1)[0].Result)

	persons = append(persons, nthPerson(2))

	insertIgnore, err := surrealdb.Query[any](
		context.Background(),
		db,
		`INSERT IGNORE INTO persons $persons RETURN NONE`,
		map[string]any{
			"persons": persons,
		})
	if err != nil {
		panic(err)
	}
	fmt.Println("# INSERT IGNORE INTO")
	fmt.Printf("Count   : %d\n", len(*insertIgnore))
	fmt.Printf("Status  : %+s\n", (*insertIgnore)[0].Status)
	fmt.Printf("Result  : %+v\n", (*insertIgnore)[0].Result)

	select2, err := surrealdb.Query[[]Person](
		context.Background(),
		db,
		`SELECT * FROM persons ORDER BY id.id`,
		nil)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Selected: %+v\n", (*select2)[0].Result)

	for i := 0; i < 3; i++ {
		persons[i].Note = fmt.Sprintf("updated%d", i)
	}
	persons = append(persons, nthPerson(3))
	var upsertQueries []string
	vars := make(map[string]any)
	for i, p := range persons {
		upsertQueries = append(upsertQueries,
			fmt.Sprintf(`UPSERT persons CONTENT $content%d RETURN NONE`, i),
		)
		vars[fmt.Sprintf("content%d", i)] = p
	}
	upsert, err := surrealdb.Query[any](
		context.Background(),
		db,
		strings.Join(upsertQueries, ";"),
		vars,
	)
	if err != nil {
		panic(err)
	}
	fmt.Println("# UPSERT CONTENT")
	fmt.Printf("Count   : %d\n", len(*upsert))
	fmt.Printf("Status  : %+s\n", (*upsert)[0].Status)
	fmt.Printf("Result  : %+v\n", (*upsert)[0].Result)

	select3, err := surrealdb.Query[[]Person](
		context.Background(),
		db,
		`SELECT * FROM persons ORDER BY id.id`,
		nil)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Selected: %+v\n", (*select3)[0].Result)

}
Output:
# INSERT INTO
Count   : 1
Status  : OK
Result  : []
Selected: [{ID:persons:p0 Note:inserted0 Num:0 Loc:{Longitude:12.34 Latitude:45.65}} {ID:persons:p1 Note:inserted1 Num:1 Loc:{Longitude:13.34 Latitude:46.65}}]
# INSERT IGNORE INTO
Count   : 1
Status  : OK
Result  : []
Selected: [{ID:persons:p0 Note:inserted0 Num:0 Loc:{Longitude:12.34 Latitude:45.65}} {ID:persons:p1 Note:inserted1 Num:1 Loc:{Longitude:13.34 Latitude:46.65}} {ID:persons:p2 Note:inserted2 Num:2 Loc:{Longitude:14.34 Latitude:47.65}}]
# UPSERT CONTENT
Count   : 4
Status  : OK
Result  : []
Selected: [{ID:persons:p0 Note:updated0 Num:0 Loc:{Longitude:12.34 Latitude:45.65}} {ID:persons:p1 Note:updated1 Num:1 Loc:{Longitude:13.34 Latitude:46.65}} {ID:persons:p2 Note:updated2 Num:2 Loc:{Longitude:14.34 Latitude:47.65}} {ID:persons:p3 Note:inserted3 Num:3 Loc:{Longitude:15.34 Latitude:48.65}}]
Example (ChangeFeedSchemafull)

ExampleQuery_changeFeedSchemafull demonstrates how to use Change Feeds with a schemaful table in SurrealDB. This example shows how to define a table with schema enforcement, define required fields, and track changes made to records that must conform to the schema.

// Connect to database
db := testenv.MustNew("surrealdbexamples", "changefeed_schemaful", "inventory")

ctx := context.Background()

// Define a schemaful table with change feed enabled
// SCHEMAFULL enforces that all records must have the defined fields
_, err := surrealdb.Query[any](ctx, db, `
		DEFINE TABLE inventory SCHEMAFULL CHANGEFEED 1h;
	`, nil)
if err != nil {
	panic(err)
}

// Note that DEFINE FIELD statements are not tracked by the change feed,
// although DEFINE TABLE statements are tracked.
//
// In schemaful tables:
// - TYPE string means the field is required (cannot be none or null)
// - TYPE option<string> means the field can be none
// - TYPE string | null means the field can be null
//
// In change feeds:
// - `null` fields appear as `null`
// - `none` fields do not appear at all
_, err = surrealdb.Query[any](ctx, db, `
		DEFINE FIELD sku ON TABLE inventory TYPE string;
		DEFINE FIELD name ON TABLE inventory TYPE string;
		DEFINE FIELD price ON TABLE inventory TYPE number ASSERT $value >= 0;
		DEFINE FIELD quantity ON TABLE inventory TYPE int ASSERT $value >= 0;
		DEFINE FIELD active ON TABLE inventory TYPE bool DEFAULT true;
		DEFINE FIELD notes ON TABLE inventory TYPE option<string>;
	`, nil)
if err != nil {
	panic(err)
}

// Make some changes to generate change feed entries
// All operations must comply with the schema
_, err = surrealdb.Query[any](ctx, db, `
		CREATE inventory:item1 SET
			sku = "SKU001",
			name = "Wireless Mouse",
			price = 29.99,
			quantity = 100,
			active = true,
			notes = "Best seller";

		UPDATE inventory:item1 SET quantity = 95;

		CREATE inventory:item2 SET
			sku = "SKU002",
			name = "USB Cable",
			price = 9.99,
			quantity = 250,
			active = true;
		-- notes is optional, so we can omit it when
		-- creating and updating records

		UPDATE inventory:item2 SET active = false;

		DELETE inventory:item2;
	`, nil)
if err != nil {
	panic(err)
}

type ChangeDefineTable struct {
	Name string `json:"name"`
}

type ChangeDefineField struct {
	Name  string `json:"name"`
	What  string `json:"what"`
	Table string `json:"table"`
}

// Change represents a change to a table in the database.
type Change struct {
	// DefineTable represents the definition of a new table in the database.
	DefineTable *ChangeDefineTable `json:"define_table"`
	// DefineField represents the definition of a new field in the table.
	DefineField *ChangeDefineField `json:"define_field"`
	// Update represents an update to a table in the database.
	// Note that in schemaful tables, all defined fields are present.
	Update map[string]any `json:"update"`
	// Delete represents a deletion from a table in the database.
	Delete map[string]any `json:"delete"`
}

// ChangeSet represents a set of changes in the database.
type ChangeSet struct {
	// Versionstamp is a unique identifier for the change set.
	Versionstamp uint64 `json:"versionstamp"`
	// Changes is a list of changes made in the database.
	Changes []Change `json:"changes"`
}

result, err := surrealdb.Query[[]ChangeSet](ctx, db, "SHOW CHANGES FOR TABLE inventory SINCE 0", nil)
if err != nil {
	panic(err)
}

showChangesResult := (*result)[0].Result

// Verify versionstamps are monotonic
monotonic := true
for i := 1; i < len(showChangesResult); i++ {
	if showChangesResult[i].Versionstamp <= showChangesResult[i-1].Versionstamp {
		monotonic = false
		break
	}
}
fmt.Printf("Versionstamps are monotonic: %v\n", monotonic)

// Count different types of changes
var defineTableCount, defineFieldCount, updateCount, deleteCount int
for _, changeSet := range showChangesResult {
	for _, change := range changeSet.Changes {
		if change.DefineTable != nil {
			defineTableCount++
		}
		if change.DefineField != nil {
			defineFieldCount++
		}
		if change.Update != nil {
			updateCount++
		}
		if change.Delete != nil {
			deleteCount++
		}
	}
}

if defineTableCount > 0 && updateCount > 0 && deleteCount > 0 {
	fmt.Println("Found change entries: table definitions, updates, and deletes")
}
if defineFieldCount > 0 {
	fmt.Printf("Field definitions tracked: %d\n", defineFieldCount)
}

// Show the last few changes with actual data
// Find the last table definition to show only recent changes
lastTableDefIndex := -1
for i := len(showChangesResult) - 1; i >= 0; i-- {
	for _, change := range showChangesResult[i].Changes {
		if change.DefineTable != nil {
			lastTableDefIndex = i
			break
		}
	}
	if lastTableDefIndex >= 0 {
		break
	}
}

if lastTableDefIndex >= 0 {
	// Show changes from the last table definition onwards
	recentChanges := showChangesResult[lastTableDefIndex:]

	// Limit to last 10 changes for readability
	startIndex := 0
	if len(recentChanges) > 10 {
		startIndex = len(recentChanges) - 10
	}

	fmt.Printf("Last %d changes:\n", len(recentChanges[startIndex:]))
	for _, changeSet := range recentChanges[startIndex:] {
		for _, change := range changeSet.Changes {
			if change.DefineTable != nil {
				fmt.Printf("  DefineTable: %s\n", change.DefineTable.Name)
			}
			if change.DefineField != nil {
				fmt.Printf("  DefineField: %s on %s\n", change.DefineField.Name, change.DefineField.Table)
			}
			if change.Update != nil {
				// Extract fields from the update (all fields present in schemaful table)
				if id, ok := change.Update["id"]; ok {
					sku := change.Update["sku"]
					name := change.Update["name"]
					price := change.Update["price"]
					quantity := change.Update["quantity"]
					active := change.Update["active"]
					notes, ok := change.Update["notes"]
					var notesStr string
					if !ok {
						notesStr = "<none>"
					} else if notes == nil {
						notesStr = "<null>"
					} else {
						notesStr = fmt.Sprintf("%v", notes)
					}
					fmt.Printf("  Update: id=%v, sku=%v, name=%v, price=%v, quantity=%v, active=%v, notes=%v\n",
						id, sku, name, price, quantity, active, notesStr)
				} else {
					fmt.Printf("  Update: %v\n", change.Update)
				}
			}
			if change.Delete != nil {
				// Extract id from the delete
				if id, ok := change.Delete["id"]; ok {
					fmt.Printf("  Delete: id=%v\n", id)
				} else {
					fmt.Printf("  Delete: %v\n", change.Delete)
				}
			}
		}
	}
}
Output:
Versionstamps are monotonic: true
Found change entries: table definitions, updates, and deletes
Last 6 changes:
  DefineTable: inventory
  Update: id={inventory item1}, sku=SKU001, name=Wireless Mouse, price=29.99, quantity=100, active=true, notes=Best seller
  Update: id={inventory item1}, sku=SKU001, name=Wireless Mouse, price=29.99, quantity=95, active=true, notes=Best seller
  Update: id={inventory item2}, sku=SKU002, name=USB Cable, price=9.99, quantity=250, active=true, notes=<none>
  Update: id={inventory item2}, sku=SKU002, name=USB Cable, price=9.99, quantity=250, active=false, notes=<none>
  Delete: id={inventory item2}
Example (ChangeFeedSchemaless)

ExampleQuery_changeFeedSchemaless demonstrates how to use Change Feeds in SurrealDB to track changes made to database records.

// Connect to database
db := testenv.MustNew("surrealdbexamples", "changefeed", "product")

ctx := context.Background()

// Enable change feed on the database
_, err := surrealdb.Query[any](ctx, db, "DEFINE TABLE product CHANGEFEED 1h", nil)
if err != nil {
	panic(err)
}

// Make some changes to generate change feed entries
_, err = surrealdb.Query[any](ctx, db, `
		CREATE product:1 SET name = "Laptop", price = 999.99;
		UPDATE product:1 SET price = 899.99;
		CREATE product:2 SET name = "Mouse", price = 29.99;
		DELETE product:2;
	`, nil)
if err != nil {
	panic(err)
}

type ChangeDefineTable struct {
	Name string `json:"name"`
}

// Change represents a change to a table in the database.
type Change struct {
	// DefineTable represents the definition of a new table in the database.
	// It has Name and nothing else.
	DefineTable *ChangeDefineTable `json:"define_table"`
	// Update represents an update to a table in the database.
	// Note that this may represent a new record being created.
	// In case of an update, the "id" field must be present,
	// and all other fields including unchanged fields must be included.
	Update map[string]any `json:"update"`
	// Delete represents a deletion from a table in the database.
	Delete map[string]any `json:"delete"`
}

// ChangeSet represents a set of changes in the database.
type ChangeSet struct {
	// Versionstamp is a unique identifier for the change set.
	// It is unique per database.
	Versionstamp uint64 `json:"versionstamp"`
	// Changes is a list of changes made in the database.
	// It may contain one or more table changes,
	// each represented as a map of field names to their new values.
	Changes []Change `json:"changes"`
}

result, err := surrealdb.Query[[]ChangeSet](ctx, db, "SHOW CHANGES FOR TABLE product SINCE 0", nil)
if err != nil {
	panic(err)
}

showChangesResult := (*result)[0].Result

// Verify versionstamps are monotonic
monotonic := true
for i := 1; i < len(showChangesResult); i++ {
	if showChangesResult[i].Versionstamp <= showChangesResult[i-1].Versionstamp {
		monotonic = false
		break
	}
}
fmt.Printf("Versionstamps are monotonic: %v\n", monotonic)

// Count different types of changes
var defineCount, updateCount, deleteCount int
for _, changeSet := range showChangesResult {
	for _, change := range changeSet.Changes {
		if change.DefineTable != nil {
			defineCount++
		}
		if change.Update != nil {
			updateCount++
		}
		if change.Delete != nil {
			deleteCount++
		}
	}
}

if defineCount > 0 && updateCount > 0 && deleteCount > 0 {
	fmt.Println("Found change entries: defines, updates, and deletes")
}

// Show the pattern of the last few changes with actual data
if len(showChangesResult) >= 5 {
	lastFive := showChangesResult[len(showChangesResult)-5:]
	fmt.Println("Last 5 changes:")
	for _, changeSet := range lastFive {
		for _, change := range changeSet.Changes {
			if change.DefineTable != nil {
				fmt.Printf("  DefineTable: %s\n", change.DefineTable.Name)
			}
			if change.Update != nil {
				// Extract key fields from the update
				if id, ok := change.Update["id"]; ok {
					if name, hasName := change.Update["name"]; hasName {
						if price, hasPrice := change.Update["price"]; hasPrice {
							fmt.Printf("  Update: id=%v, name=%v, price=%v\n", id, name, price)
						} else {
							fmt.Printf("  Update: id=%v, name=%v\n", id, name)
						}
					} else if price, hasPrice := change.Update["price"]; hasPrice {
						fmt.Printf("  Update: id=%v, price=%v\n", id, price)
					} else {
						fmt.Printf("  Update: id=%v\n", id)
					}
				} else {
					fmt.Printf("  Update: %v\n", change.Update)
				}
			}
			if change.Delete != nil {
				// Extract id from the delete
				if id, ok := change.Delete["id"]; ok {
					fmt.Printf("  Delete: id=%v\n", id)
				} else {
					fmt.Printf("  Delete: %v\n", change.Delete)
				}
			}
		}
	}
}
Output:
Versionstamps are monotonic: true
Found change entries: defines, updates, and deletes
Last 5 changes:
  DefineTable: product
  Update: id={product 1}, name=Laptop, price=999.99
  Update: id={product 1}, name=Laptop, price=899.99
  Update: id={product 2}, name=Mouse, price=29.99
  Delete: id={product 2}
Example (Count_groupAll)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "querytest", "product")

	type Product struct {
		ID       models.RecordID `json:"id,omitempty"`
		Name     string          `json:"name,omitempty"`
		Category string          `json:"category,omitempty"`
	}

	a := Product{
		ID:       models.NewRecordID("product", "a"),
		Name:     "A",
		Category: "One",
	}
	b := Product{
		ID:       models.NewRecordID("product", "b"),
		Name:     "B",
		Category: "One",
	}
	c := Product{
		ID:       models.NewRecordID("product", "c"),
		Name:     "C",
		Category: "Two",
	}

	for _, p := range []Product{a, b, c} {
		created, err := surrealdb.Create[Product](
			context.Background(),
			db,
			p.ID,
			p,
		)
		if err != nil {
			panic(err)
		}
		fmt.Printf("Created product: %+v\n", *created)
	}

	type CountResult struct {
		C int `json:"c,omitempty"`
	}

	res, err := surrealdb.Query[[]CountResult](
		context.Background(),
		db,
		"SELECT COUNT() as c FROM product GROUP ALL",
		map[string]any{},
	)
	if err != nil {
		panic(err)
	}

	countResult := (*res)[0].Result[0]

	fmt.Printf("Count: %d\n", countResult.C)

}
Output:
Created product: {ID:{Table:product ID:a} Name:A Category:One}
Created product: {ID:{Table:product ID:b} Name:B Category:One}
Created product: {ID:{Table:product ID:c} Name:C Category:Two}
Count: 3
Example (Count_groupBy)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "querytest", "product")

	type Product struct {
		ID       models.RecordID `json:"id,omitempty"`
		Name     string          `json:"name,omitempty"`
		Category string          `json:"category,omitempty"`
	}

	a := Product{
		ID:       models.NewRecordID("product", "a"),
		Name:     "A",
		Category: "One",
	}
	b := Product{
		ID:       models.NewRecordID("product", "b"),
		Name:     "B",
		Category: "One",
	}
	c := Product{
		ID:       models.NewRecordID("product", "c"),
		Name:     "C",
		Category: "Two",
	}

	for _, p := range []Product{a, b, c} {
		created, err := surrealdb.Create[Product](
			context.Background(),
			db,
			p.ID,
			p,
		)
		if err != nil {
			panic(err)
		}
		fmt.Printf("Created product: %+v\n", *created)
	}

	type ProductCategorySummary struct {
		Category string `json:"category,omitempty"`
		Count    int    `json:"count,omitempty"`
	}

	res, err := surrealdb.Query[[]ProductCategorySummary](
		context.Background(),
		db,
		// Note that there's no `COUNT(*)` in SurrealDB.
		// When counting, you use either `COUNT()` or `COUNT(field)`,
		// with either GROUP BY or GROUP ALL.
		"SELECT category, COUNT() AS count FROM product GROUP BY category",
		map[string]any{},
	)
	if err != nil {
		panic(err)
	}

	summaries := (*res)[0].Result

	for i, summary := range summaries {
		fmt.Printf("Category %d: %s, Count: %d\n", i+1, summary.Category, summary.Count)
	}

}
Output:
Created product: {ID:{Table:product ID:a} Name:A Category:One}
Created product: {ID:{Table:product ID:b} Name:B Category:One}
Created product: {ID:{Table:product ID:c} Name:C Category:Two}
Category 1: One, Count: 2
Category 2: Two, Count: 1
Example (Create_none_null_fields_legacy_fxamackercbor)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

const (
	NullString = "null"
	NoneString = "none"
)

func main() {
	c := testenv.MustNewConfig("example", "query", "t")
	c.CBORImpl = testenv.CBORImplFxamackerCBOR

	db := c.MustNew()

	_, err := surrealdb.Query[[]any](
		context.Background(),
		db,
		`DEFINE TABLE t SCHEMAFULL;
		 DEFINE FIELD nullable ON t TYPE bool | null;
		 DEFINE FIELD option ON t TYPE option<bool>;
		 CREATE t:a SET nullable = $nil;
		 CREATE t:b SET nullable = true;
		 CREATE t:c SET nullable = true, option = false;
		 CREATE t:d SET nullable = false, option = true;
		 CREATE t:e SET nullable = false;
		 CREATE t:f SET nullable = false, option = $none;
		`,
		map[string]any{
			"id":   models.NewRecordID("t", 1),
			"nil":  nil,
			"none": models.None,
		})
	if err != nil {
		panic(err)
	}

	fmt.Println("Created records with none and null fields successfully")

	type T struct {
		ID       *models.RecordID `json:"id,omitempty"`
		Nulabble *bool            `json:"nullable"`
		Option   *bool            `json:"option"`
	}

	selected, err := surrealdb.Query[[]T](
		context.Background(),
		db,
		`SELECT id, nullable, option FROM t ORDER BY id`,
		nil,
	)
	if err != nil {
		panic(err)
	}
	for _, t := range (*selected)[0].Result {
		id := t.ID

		var nullable string
		if t.Nulabble == nil {
			nullable = NullString
		} else {
			nullable = fmt.Sprintf("%t", *t.Nulabble)
		}

		var option string
		if t.Option == nil {
			option = NoneString
		} else {
			option = fmt.Sprintf("%t", *t.Option)
		}

		fmt.Printf("ID: %s, Nullable: %s, Option: %s\n", id, nullable, option)
	}
}
Output:
Created records with none and null fields successfully
ID: t:a, Nullable: null, Option: false
ID: t:b, Nullable: true, Option: false
ID: t:c, Nullable: true, Option: false
ID: t:d, Nullable: false, Option: true
ID: t:e, Nullable: false, Option: false
ID: t:f, Nullable: false, Option: false
Example (Create_none_null_fields_surrealcbor)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

const NilString = "<nil>"

func main() {
	c := testenv.MustNewConfig("example", "query", "t")
	c.CBORImpl = testenv.CBORImplSurrealCBOR

	db := c.MustNew()

	_, err := surrealdb.Query[[]any](
		context.Background(),
		db,
		`DEFINE TABLE t SCHEMAFULL;
		 DEFINE FIELD nullable ON t TYPE bool | null;
		 DEFINE FIELD option ON t TYPE option<bool>;
		 CREATE t:a SET nullable = $nil;
		 CREATE t:b SET nullable = true;
		 CREATE t:c SET nullable = true, option = false;
		 CREATE t:d SET nullable = false, option = true;
		 CREATE t:e SET nullable = false;
		 CREATE t:f SET nullable = false, option = $none;
		`,
		map[string]any{
			"id":   models.NewRecordID("t", 1),
			"nil":  nil,
			"none": models.None,
		})
	if err != nil {
		panic(err)
	}

	fmt.Println("Created records with none and null fields successfully")

	type T struct {
		ID       *models.RecordID `json:"id,omitempty"`
		Nulabble *bool            `json:"nullable"`
		Option   *bool            `json:"option"`
	}

	selected, err := surrealdb.Query[[]T](
		context.Background(),
		db,
		`SELECT id, nullable, option FROM t ORDER BY id`,
		nil,
	)
	if err != nil {
		panic(err)
	}
	for _, t := range (*selected)[0].Result {
		id := t.ID

		var nullable string
		if t.Nulabble == nil {
			nullable = NilString
		} else {
			nullable = fmt.Sprintf("%t", *t.Nulabble)
		}

		var option string
		if t.Option == nil {
			option = NilString
		} else {
			option = fmt.Sprintf("%t", *t.Option)
		}

		fmt.Printf("ID: %s, Nullable: %s, Option: %s\n", id, nullable, option)
	}
}
Output:
Created records with none and null fields successfully
ID: t:a, Nullable: <nil>, Option: <nil>
ID: t:b, Nullable: true, Option: <nil>
ID: t:c, Nullable: true, Option: false
ID: t:d, Nullable: false, Option: true
ID: t:e, Nullable: false, Option: <nil>
ID: t:f, Nullable: false, Option: <nil>
Example (Embedded_struct)
package main

import (
	"context"
	"fmt"
	"time"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "query", "persons")

	type Base struct {
		ID *models.RecordID `json:"id,omitempty"`
	}

	type Profile struct {
		Base
		City string `json:"city"`
	}

	type Person struct {
		Base
		Name      string `json:"name"`
		Profile   Profile
		CreatedAt models.CustomDateTime  `json:"created_at,omitempty"`
		UpdatedAt *models.CustomDateTime `json:"updated_at,omitempty"`
	}

	createdAt, err := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
	if err != nil {
		panic(err)
	}

	recordID := models.NewRecordID("persons", "yusuke")

	createQueryResults, err := surrealdb.Query[[]Person](
		context.Background(),
		db,
		`CREATE $record_id CONTENT $content`,
		map[string]any{
			"record_id": recordID,
			"content": map[string]any{
				"name": "Yusuke",
				"created_at": models.CustomDateTime{
					Time: createdAt,
				},
				"profile": map[string]any{
					"id":   models.NewRecordID("profiles", "yusuke"),
					"city": "Tokyo",
				},
			},
		})
	if err != nil {
		panic(err)
	}
	fmt.Printf("Number of query results: %d\n", len(*createQueryResults))
	fmt.Printf("First query result's status: %+s\n", (*createQueryResults)[0].Status)
	fmt.Printf("Persons contained in the first query result: %+v\n", (*createQueryResults)[0].Result)

	updatedAt, err := time.Parse(time.RFC3339, "2023-10-02T12:00:00Z")
	if err != nil {
		panic(err)
	}
	updateQueryResults, err := surrealdb.Query[[]Person](
		context.Background(),
		db,
		`UPDATE $id CONTENT $content`,
		map[string]any{
			"id": models.NewRecordID("persons", "yusuke"),
			"content": map[string]any{
				"name":       "Yusuke Updated Last",
				"created_at": createdAt,
				"updated_at": updatedAt,
			},
		},
	)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Number of update query results: %d\n", len(*updateQueryResults))
	fmt.Printf("Update query result's status: %+s\n", (*updateQueryResults)[0].Status)
	fmt.Printf("Persons contained in the update query result: %+v\n", (*updateQueryResults)[0].Result)

	selectQueryResults, err := surrealdb.Query[[]Person](
		context.Background(),
		db,
		`SELECT * FROM $id`,
		map[string]any{
			"id": models.NewRecordID("persons", "yusuke"),
		},
	)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Number of select query results: %d\n", len(*selectQueryResults))
	fmt.Printf("Select query result's status: %+s\n", (*selectQueryResults)[0].Status)
	fmt.Printf("Persons contained in the select query result: %+v\n", (*selectQueryResults)[0].Result)

}
Output:
Number of query results: 1
First query result's status: OK
Persons contained in the first query result: [{Base:{ID:persons:yusuke} Name:Yusuke Profile:{Base:{ID:profiles:yusuke} City:Tokyo} CreatedAt:{Time:2023-10-01 12:00:00 +0000 UTC} UpdatedAt:<nil>}]
Number of update query results: 1
Update query result's status: OK
Persons contained in the update query result: [{Base:{ID:persons:yusuke} Name:Yusuke Updated Last Profile:{Base:{ID:<nil>} City:} CreatedAt:{Time:2023-10-01 12:00:00 +0000 UTC} UpdatedAt:2023-10-02T12:00:00Z}]
Number of select query results: 1
Select query result's status: OK
Persons contained in the select query result: [{Base:{ID:persons:yusuke} Name:Yusuke Updated Last Profile:{Base:{ID:<nil>} City:} CreatedAt:{Time:2023-10-01 12:00:00 +0000 UTC} UpdatedAt:2023-10-02T12:00:00Z}]
Example (Issue192)

See https://github.com/surrealdb/surrealdb.go/issues/292

package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "query_issue192", "t")

	_, err := surrealdb.Query[any](
		context.Background(),
		db,
		`DEFINE TABLE IF NOT EXISTS t SCHEMAFULL;
DEFINE FIELD IF NOT EXISTS modified_at2 ON TABLE t TYPE option<datetime>;
CREATE t:s`,
		map[string]any{"name": "John Doe"},
	)
	if err != nil {
		panic(err)
	}

	type ReturnData1 struct {
		ID         *models.RecordID      `json:"id,omitempty"`
		ModifiedAt models.CustomDateTime `json:"modified_at,omitempty"`
	}

	data, err := surrealdb.Query[[]ReturnData1](
		context.Background(),
		db,
		"SELECT id, modified_at FROM t",
		nil)
	if err != nil {
		panic(err)
	}

	got := (*data)[0].Result[0]

	fmt.Printf("ID: %s\n", got.ID)
	fmt.Printf("ModifiedAt: %v\n", got.ModifiedAt)
	fmt.Printf("ModifiedAt.IsZero(): %v\n", got.ModifiedAt.IsZero())

	type ReturnData2 struct {
		ID         *models.RecordID       `json:"id,omitempty"`
		ModifiedAt *models.CustomDateTime `json:"modified_at,omitempty"`
	}

	data2, err := surrealdb.Query[[]ReturnData2](
		context.Background(),
		db,
		"SELECT id, modified_at FROM t",
		nil)
	if err != nil {
		panic(err)
	}

	got2 := (*data2)[0].Result[0]

	fmt.Printf("ID: %s\n", got2.ID)
	// With fxamacker/cbor: returns zero-value struct (not nil)
	// With surrealcbor: returns nil
	// Both should print the same format for consistency
	if got2.ModifiedAt == nil || got2.ModifiedAt.IsZero() {
		fmt.Printf("ModifiedAt: <nil or zero>\n")
	}
	fmt.Printf("ModifiedAt.IsZero(): %v\n", got2.ModifiedAt.IsZero())

}
Output:
ID: t:s
ModifiedAt: {0001-01-01 00:00:00 +0000 UTC}
ModifiedAt.IsZero(): true
ID: t:s
ModifiedAt: <nil or zero>
ModifiedAt.IsZero(): true
Example (Issue291)

See https://github.com/surrealdb/surrealdb.go/issues/291

package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "query_issue291", "t")

	_, err := surrealdb.Query[any](
		context.Background(),
		db,
		`DEFINE TABLE IF NOT EXISTS t SCHEMAFULL;
DEFINE FIELD IF NOT EXISTS i ON TABLE t TYPE option<int>;
DEFINE FIELD IF NOT EXISTS j ON TABLE t TYPE option<string>;
CREATE t:s;`,
		map[string]any{"name": "John Doe"},
	)
	if err != nil {
		panic(err)
	}

	type ReturnData struct {
		I *int    `json:"i"`
		J *string `json:"j"`
	}

	dataNones, err := surrealdb.Query[[]ReturnData](
		context.Background(),
		db,
		"SELECT i, j FROM t",
		nil)
	if err != nil {
		panic(err)
	}

	got := (*dataNones)[0].Result[0]

	// With fxamacker/cbor: returns zero values (0, "")
	// With surrealcbor: returns nil
	// We need to handle both cases
	if got.I == nil || *got.I == 0 {
		fmt.Printf("I: <nil or zero>\n")
	} else {
		fmt.Printf("I: %+v\n", *got.I)
	}

	if got.J == nil || *got.J == "" {
		fmt.Printf("J: <nil or zero>\n")
	} else {
		fmt.Printf("J: %q\n", *got.J)
	}

	dataAll, err := surrealdb.Query[[]ReturnData](
		context.Background(),
		db,
		"SELECT * FROM t",
		nil)
	if err != nil {
		panic(err)
	}

	gotAll := (*dataAll)[0].Result[0]

	fmt.Printf("I: %+v\n", gotAll.I)
	fmt.Printf("J: %+v\n", gotAll.J)

}
Output:
I: <nil or zero>
J: <nil or zero>
I: <nil>
J: <nil>
Example (Live)

ExampleQuery_live demonstrates using LIVE SELECT via the Query RPC. LIVE SELECT returns matching records as map[string]any in notification.Result. The notification channel is automatically closed when Kill is called.

package main

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"time"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

// formatRecordResult formats a record result (map[string]any) for testing.
// This is used for regular live query results (without diff) and DELETE operations.
// It handles the id field specially, formatting RecordID as table:⟨UUID⟩.
func formatRecordResult(record map[string]any) string {
	keys := make([]string, 0, len(record))
	for k := range record {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	var parts []string
	for _, k := range keys {
		val := record[k]
		if k == "id" {

			recordID := val.(models.RecordID)
			parts = append(parts, fmt.Sprintf("id=%s:⟨UUID⟩", recordID.Table))
		} else {
			parts = append(parts, fmt.Sprintf("%s=%v", k, val))
		}
	}
	return "{" + strings.Join(parts, " ") + "}"
}

func main() {
	config := testenv.MustNewConfig("surrealdbexamples", "livequery_query", "products")
	config.Endpoint = testenv.GetSurrealDBWSURL()

	db := config.MustNew()

	type Product struct {
		ID    *models.RecordID `json:"id,omitempty"`
		Name  string           `json:"name"`
		Price float64          `json:"price"`
		Stock int              `json:"stock"`
	}

	ctx := context.Background()

	// Create the table first - SurrealDB 3.x requires the table to exist for LIVE SELECT
	_, err := surrealdb.Query[any](ctx, db, `DEFINE TABLE products`, nil)
	if err != nil {
		panic(fmt.Sprintf("Failed to create table: %v", err))
	}

	result, err := surrealdb.Query[models.UUID](ctx, db, "LIVE SELECT * FROM products WHERE stock < 10", map[string]any{})
	if err != nil {
		panic(fmt.Sprintf("Failed to start live query: %v", err))
	}

	liveID := (*result)[0].Result.String()
	fmt.Println("Started live query")

	notifications, err := db.LiveNotifications(liveID)
	if err != nil {
		panic(fmt.Sprintf("Failed to get live notifications channel: %v", err))
	}

	received := make(chan struct{})
	done := make(chan bool)
	notificationCount := 0
	go func() {
		for notification := range notifications {
			notificationCount++

			// LIVE SELECT returns matching records as map[string]any
			record, ok := notification.Result.(map[string]any)
			if !ok {
				panic(fmt.Sprintf("Expected map[string]any for LIVE SELECT result, got %T", notification.Result))
			}

			fmt.Printf("Notification %d - Action: %s, Result: %s\n", notificationCount, notification.Action, formatRecordResult(record))

			if notificationCount >= 3 {
				close(received)
			}
		}
		// Channel was closed
		fmt.Println("Notification channel closed")
		done <- true
	}()

	_, err = surrealdb.Create[Product](ctx, db, "products", map[string]any{
		"name":  "Widget",
		"price": 9.99,
		"stock": 5,
	})
	if err != nil {
		panic(fmt.Sprintf("Failed to create product: %v", err))
	}

	_, err = surrealdb.Create[Product](ctx, db, "products", map[string]any{
		"name":  "Gadget",
		"price": 19.99,
		"stock": 3,
	})
	if err != nil {
		panic(fmt.Sprintf("Failed to create second product: %v", err))
	}

	_, err = surrealdb.Create[Product](ctx, db, "products", map[string]any{
		"name":  "Abundant Item",
		"price": 5.99,
		"stock": 100,
	})
	if err != nil {
		panic(fmt.Sprintf("Failed to create third product: %v", err))
	}

	_, err = surrealdb.Create[Product](ctx, db, "products", map[string]any{
		"name":  "Rare Item",
		"price": 99.99,
		"stock": 1,
	})
	if err != nil {
		panic(fmt.Sprintf("Failed to create fourth product: %v", err))
	}

	// Wait for all expected notifications to be received
	select {
	case <-received:
		// All notifications received
	case <-time.After(2 * time.Second):
		panic("Timeout waiting for all notifications")
	}

	fmt.Println("Stopping live query notifications")

	err = surrealdb.Kill(ctx, db, liveID)
	if err != nil {
		panic(fmt.Sprintf("Failed to kill live query: %v", err))
	}

	select {
	case <-done:
		fmt.Println("Goroutine exited after channel closed")
	case <-time.After(2 * time.Second):
		panic("Timeout: notification channel was not closed after Kill")
	}

}
Output:
Started live query
Notification 1 - Action: CREATE, Result: {id=products:⟨UUID⟩ name=Widget price=9.99 stock=5}
Notification 2 - Action: CREATE, Result: {id=products:⟨UUID⟩ name=Gadget price=19.99 stock=3}
Notification 3 - Action: CREATE, Result: {id=products:⟨UUID⟩ name=Rare Item price=99.99 stock=1}
Stopping live query notifications
Notification channel closed
Goroutine exited after channel closed
Example (None_and_null_handling_allExistingFields)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

const (
	NullString = "null"
	NoneString = "none"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "query", "t")

	_, err := surrealdb.Query[[]any](
		context.Background(),
		db,
		`DEFINE TABLE t SCHEMAFULL;
		 DEFINE FIELD nullable ON t TYPE bool | null;
		 DEFINE FIELD option ON t TYPE option<bool>;
		 CREATE t:a SET nullable = null;
		 CREATE t:b SET nullable = true;
		 CREATE t:c SET nullable = true, option = false;
		 CREATE t:d SET nullable = false, option = true;
		 CREATE t:e SET nullable = false;
		 CREATE t:f SET nullable = false, option = NONE;
		`,
		map[string]any{
			"id": models.NewRecordID("t", 1),
		})
	if err != nil {
		panic(err)
	}

	type T struct {
		ID       *models.RecordID `json:"id,omitempty"`
		Nulabble *bool            `json:"nullable"`
		Option   *bool            `json:"option"`
	}

	selected, err := surrealdb.Query[[]T](
		context.Background(),
		db,
		`SELECT * FROM t ORDER BY id.id`,
		nil,
	)
	if err != nil {
		panic(err)
	}
	for _, t := range (*selected)[0].Result {
		id := t.ID

		var nullable string
		if t.Nulabble == nil {
			nullable = NullString
		} else {
			nullable = fmt.Sprintf("%t", *t.Nulabble)
		}

		var option string
		if t.Option == nil {
			option = NoneString
		} else {
			option = fmt.Sprintf("%t", *t.Option)
		}

		fmt.Printf("ID: %s, Nullable: %s, Option: %s\n", id, nullable, option)
	}

}
Output:
ID: t:a, Nullable: null, Option: none
ID: t:b, Nullable: true, Option: none
ID: t:c, Nullable: true, Option: false
ID: t:d, Nullable: false, Option: true
ID: t:e, Nullable: false, Option: none
ID: t:f, Nullable: false, Option: none
Example (None_and_null_handling_explicitFields_ints_legacy_fxamackercbor)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

const (
	NullString = "null"
	NoneString = "none"
)

func main() {
	c := testenv.MustNewConfig("example", "query", "t")
	c.CBORImpl = testenv.CBORImplFxamackerCBOR

	db := c.MustNew()

	_, err := surrealdb.Query[[]any](
		context.Background(),
		db,
		`DEFINE TABLE t SCHEMAFULL;
		 DEFINE FIELD nullable ON t TYPE int | null;
		 DEFINE FIELD option ON t TYPE option<int>;
		 CREATE t:a SET nullable = null;
		 CREATE t:b SET nullable = 2;
		 CREATE t:c SET nullable = 2, option = 1;
		 CREATE t:d SET nullable = 1, option = 2;
		 CREATE t:e SET nullable = 1;
		 CREATE t:f SET nullable = 1, option = NONE;
		`,
		map[string]any{
			"id": models.NewRecordID("t", 1),
		})
	if err != nil {
		panic(err)
	}

	type T struct {
		ID       *models.RecordID `json:"id,omitempty"`
		Nulabble *int             `json:"nullable"`
		Option   *int             `json:"option"`
	}

	selected, err := surrealdb.Query[[]T](
		context.Background(),
		db,
		`SELECT id, nullable, option FROM t ORDER BY id`,
		nil,
	)
	if err != nil {
		panic(err)
	}
	for _, t := range (*selected)[0].Result {
		id := t.ID

		var nullable string
		if t.Nulabble == nil {
			nullable = NullString
		} else {
			nullable = fmt.Sprintf("%v", *t.Nulabble)
		}

		var option string
		if t.Option == nil {
			option = NoneString
		} else {
			option = fmt.Sprintf("%v", *t.Option)
		}

		fmt.Printf("ID: %v, Nullable: %v, Option: %s\n", id, nullable, option)
	}

}
Output:
ID: t:a, Nullable: null, Option: 0
ID: t:b, Nullable: 2, Option: 0
ID: t:c, Nullable: 2, Option: 1
ID: t:d, Nullable: 1, Option: 2
ID: t:e, Nullable: 1, Option: 0
ID: t:f, Nullable: 1, Option: 0
Example (None_and_null_handling_explicitFields_ints_surrealcbor)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

const NilString = "<nil>"

func main() {
	c := testenv.MustNewConfig("example", "query", "t")
	c.CBORImpl = testenv.CBORImplSurrealCBOR

	db := c.MustNew()

	_, err := surrealdb.Query[[]any](
		context.Background(),
		db,
		`DEFINE TABLE t SCHEMAFULL;
		 DEFINE FIELD nullable ON t TYPE int | null;
		 DEFINE FIELD option ON t TYPE option<int>;
		 CREATE t:a SET nullable = null;
		 CREATE t:b SET nullable = 2;
		 CREATE t:c SET nullable = 2, option = 1;
		 CREATE t:d SET nullable = 1, option = 2;
		 CREATE t:e SET nullable = 1;
		 CREATE t:f SET nullable = 1, option = NONE;
		`,
		map[string]any{
			"id": models.NewRecordID("t", 1),
		})
	if err != nil {
		panic(err)
	}

	type T struct {
		ID       *models.RecordID `json:"id,omitempty"`
		Nulabble *int             `json:"nullable"`
		Option   *int             `json:"option"`
	}

	selected, err := surrealdb.Query[[]T](
		context.Background(),
		db,
		`SELECT id, nullable, option FROM t ORDER BY id`,
		nil,
	)
	if err != nil {
		panic(err)
	}
	for _, t := range (*selected)[0].Result {
		id := t.ID

		var nullable string
		if t.Nulabble == nil {
			nullable = NilString
		} else {
			nullable = fmt.Sprintf("%v", *t.Nulabble)
		}

		var option string
		if t.Option == nil {
			option = NilString
		} else {
			option = fmt.Sprintf("%v", *t.Option)
		}

		fmt.Printf("ID: %v, Nullable: %v, Option: %s\n", id, nullable, option)
	}

}
Output:
ID: t:a, Nullable: <nil>, Option: <nil>
ID: t:b, Nullable: 2, Option: <nil>
ID: t:c, Nullable: 2, Option: 1
ID: t:d, Nullable: 1, Option: 2
ID: t:e, Nullable: 1, Option: <nil>
ID: t:f, Nullable: 1, Option: <nil>
Example (None_and_null_handling_explicitFields_legacy_fxamackercbor)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

const (
	NullString = "null"
	NoneString = "none"
)

func main() {
	c := testenv.MustNewConfig("example", "query", "t")
	c.CBORImpl = testenv.CBORImplFxamackerCBOR

	db := c.MustNew()

	_, err := surrealdb.Query[[]any](
		context.Background(),
		db,
		`DEFINE TABLE t SCHEMAFULL;
		 DEFINE FIELD nullable ON t TYPE bool | null;
		 DEFINE FIELD option ON t TYPE option<bool>;
		 CREATE t:a SET nullable = null;
		 CREATE t:b SET nullable = true;
		 CREATE t:c SET nullable = true, option = false;
		 CREATE t:d SET nullable = false, option = true;
		 CREATE t:e SET nullable = false;
		 CREATE t:f SET nullable = false, option = NONE;
		`,
		map[string]any{
			"id": models.NewRecordID("t", 1),
		})
	if err != nil {
		panic(err)
	}

	type T struct {
		ID       *models.RecordID `json:"id,omitempty"`
		Nulabble *bool            `json:"nullable"`
		Option   *bool            `json:"option"`
	}

	selected, err := surrealdb.Query[[]T](
		context.Background(),
		db,
		`SELECT id, nullable, option FROM t ORDER BY id`,
		nil,
	)
	if err != nil {
		panic(err)
	}
	for _, t := range (*selected)[0].Result {
		id := t.ID

		var nullable string
		if t.Nulabble == nil {
			nullable = NullString
		} else {
			nullable = fmt.Sprintf("%t", *t.Nulabble)
		}

		var option string
		if t.Option == nil {
			option = NoneString
		} else {
			option = fmt.Sprintf("%t", *t.Option)
		}

		fmt.Printf("ID: %s, Nullable: %s, Option: %s\n", id, nullable, option)
	}

}
Output:
ID: t:a, Nullable: null, Option: false
ID: t:b, Nullable: true, Option: false
ID: t:c, Nullable: true, Option: false
ID: t:d, Nullable: false, Option: true
ID: t:e, Nullable: false, Option: false
ID: t:f, Nullable: false, Option: false
Example (None_and_null_handling_explicitFields_surrealcbor)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

const NilString = "<nil>"

func main() {
	c := testenv.MustNewConfig("example", "query", "t")
	c.CBORImpl = testenv.CBORImplSurrealCBOR

	db := c.MustNew()

	_, err := surrealdb.Query[[]any](
		context.Background(),
		db,
		`DEFINE TABLE t SCHEMAFULL;
		 DEFINE FIELD nullable ON t TYPE bool | null;
		 DEFINE FIELD option ON t TYPE option<bool>;
		 CREATE t:a SET nullable = null;
		 CREATE t:b SET nullable = true;
		 CREATE t:c SET nullable = true, option = false;
		 CREATE t:d SET nullable = false, option = true;
		 CREATE t:e SET nullable = false;
		 CREATE t:f SET nullable = false, option = NONE;
		`,
		map[string]any{
			"id": models.NewRecordID("t", 1),
		})
	if err != nil {
		panic(err)
	}

	type T struct {
		ID       *models.RecordID `json:"id,omitempty"`
		Nulabble *bool            `json:"nullable"`
		Option   *bool            `json:"option"`
	}

	selected, err := surrealdb.Query[[]T](
		context.Background(),
		db,
		`SELECT id, nullable, option FROM t ORDER BY id`,
		nil,
	)
	if err != nil {
		panic(err)
	}
	for _, t := range (*selected)[0].Result {
		id := t.ID

		var nullable string
		if t.Nulabble == nil {
			nullable = NilString
		} else {
			nullable = fmt.Sprintf("%t", *t.Nulabble)
		}

		var option string
		if t.Option == nil {
			option = NilString
		} else {
			option = fmt.Sprintf("%t", *t.Option)
		}

		fmt.Printf("ID: %s, Nullable: %v, Option: %v\n", id, nullable, option)
	}

}
Output:
ID: t:a, Nullable: <nil>, Option: <nil>
ID: t:b, Nullable: true, Option: <nil>
ID: t:c, Nullable: true, Option: false
ID: t:d, Nullable: false, Option: true
ID: t:e, Nullable: false, Option: <nil>
ID: t:f, Nullable: false, Option: <nil>
Example (Null_none_customdatetime_roundtrip_legacy_fxamackercbor)
package main

import (
	"context"
	"fmt"
	"time"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	c := testenv.MustNewConfig("example", "query", "t")
	c.CBORImpl = testenv.CBORImplFxamackerCBOR

	db := c.MustNew()

	_, err := surrealdb.Query[[]any](
		context.Background(),
		db,
		`DEFINE TABLE t SCHEMAFULL;
		//  DEFINE FIELD nullable_zero ON t TYPE datetime | null;
		 DEFINE FIELD nullable_nil ON t TYPE datetime | null;
		 DEFINE FIELD option_zero ON t TYPE option<datetime>;
		 DEFINE FIELD option_zero_omitempty ON t TYPE option<datetime>;
		 DEFINE FIELD option_zero_omitzero ON t TYPE option<datetime>;
		 DEFINE FIELD option_ptr_zero ON t TYPE option<datetime>;
		 DEFINE FIELD option_ptr_nil ON t TYPE option<datetime>;
		 DEFINE FIELD option_ptr_nil_omitempty ON t TYPE option<datetime>;
		`,
		nil,
	)
	if err != nil {
		panic(err)
	}

	type T struct {
		ID *models.RecordID `json:"id,omitempty"`
		// NullableZero tests how a Zero value of CustomDateTime is marshaled into a nullable field
		//
		// This fails like this:
		//   Found NONE for field `nullable_zero`, with record `t:1`, but expected a datetime | null
		// NullableZero  models.CustomDateTime  `json:"nullable_zero"`

		// NullableNil tests how a Go pointer to nil is marshaled into a nullable field
		NullableNil *models.CustomDateTime `json:"nullable_nil"`

		OptionZero models.CustomDateTime `json:"option_zero"`

		// OptionZeroOmitEmpty tests how a Zero value of CustomDateTime is marshaled into an option field
		OptionZeroOmitEmpty models.CustomDateTime `json:"option_zero_omitempty,omitempty"`

		// OptionZeroOmitZero tests how a Zero value of CustomDateTime is marshaled into an option field
		// when the field is omitzero
		OptionZeroOmitZero models.CustomDateTime `json:"option_zero_omitzero,omitzero"`

		// OptionPtrZero tests how a Go pointer to a Zero value of CustomDateTime is marshaled
		OptionPtrZero *models.CustomDateTime `json:"option_ptr_zero"`

		// OptionNil tests how a Go pointer to nil is marshaled
		//
		// This fails like this:
		//   Found NULL for field `option_ptr_nil`, with record `t:1`, but expected a option<datetime>
		// OptionPtrNil *models.CustomDateTime `json:"option_ptr_nil"`

		// OptionPtrNilOmitEmpty tests how a Go pointer to nil is marshaled into an option field
		// when the field is omitted if empty.
		OptionPtrNilOmitEmpty *models.CustomDateTime `json:"option_ptr_nil_omitempty,omitempty"`
	}

	// Marshaling rule:
	//
	// m1. Go `nil` w/o omitempty is SurrealDB `null`
	// m2. Go `nil` w/  omitempty is SurrealDB `none`
	// m3. Zero value of CustomDateTime is SurrealDB `none`

	// Unmarshaling rule:
	//
	// u1. SurrealDB `null`                       is Go `nil`
	// u2. SurrealDB `none` + Go       `any` type is Go `models.CustomNil{}`
	// u3. SurrealDB `none` + Go non-pointer type is Go zero value (no primitive type is supported yet, but CustomDateTime is supported)
	// u4. SurrealDB `none` + Go     pointer type is Go `nil`                     when `SELECT *` is used
	// u5. SurrealDB `none` + Go     pointer type is a pointer to a Go zero value when explicit fields are selected
	//     (e.g., you cannot unmarshal `none` into `nil` when the field is explicitly selected)

	// Future plans:
	//
	// Our plan is to fix u4 and u5 in the future, by unifying the two into:
	//
	// u4. SurrealDB `none` will unmarshal into Go `nil` if the field IS a pointer type,
	//   REGARDLESS of whether the field is explicitly selected or not

	_, err = surrealdb.Query[[]T](
		context.Background(),
		db,
		`CREATE $id CONTENT $value`,
		map[string]any{
			"id": models.NewRecordID("t", 1),
			"value": T{
				// NullableZero:  models.CustomDateTime{Time: time.Time{}},
				NullableNil:         nil,
				OptionZero:          models.CustomDateTime{Time: time.Time{}},
				OptionZeroOmitEmpty: models.CustomDateTime{Time: time.Time{}},
				OptionZeroOmitZero:  models.CustomDateTime{Time: time.Time{}},
				OptionPtrZero:       &models.CustomDateTime{Time: time.Time{}},
				// OptionPtrNil:  nil,
				OptionPtrNilOmitEmpty: nil,
			},
		},
	)
	if err != nil {
		panic(err)
	}

	selectExplicit, err := surrealdb.Query[[]T](
		context.Background(),
		db,
		`SELECT id,
		        nullable_nil,
				option_zero,
				option_zero_omitempty,
				option_zero_omitzero,
				option_ptr_zero,
				option_ptr_nil_omitempty
		FROM t ORDER BY id`,
		nil,
	)
	if err != nil {
		panic(err)
	}

	fmt.Println()
	fmt.Println("# SELECT with explicit fields")
	fmt.Println()

	for _, t := range (*selectExplicit)[0].Result {
		fmt.Printf("ID: %v\n", t.ID)
		// fmt.Printf("NullableZero: %v\n", t.NullableZero)
		fmt.Printf("NullableNil: %v\n", t.NullableNil)
		fmt.Printf("OptionZero: %v\n", t.OptionZero)
		fmt.Printf("OptionZeroOmitEmpty: %v\n", t.OptionZeroOmitEmpty)
		fmt.Printf("OptionZeroOmitZero: %v\n", t.OptionZeroOmitZero)
		fmt.Printf("OptionPtrZero: %v\n", t.OptionPtrZero)
		// fmt.Printf("OptionPtrNil: %v\n", t.OptionPtrNil)
		fmt.Printf("OptionPtrNilOmitEmpty: %v\n", t.OptionPtrNilOmitEmpty)
	}

	selectAll, err := surrealdb.Query[[]T](
		context.Background(),
		db,
		`SELECT * FROM t ORDER BY id`,
		nil,
	)
	if err != nil {
		panic(err)
	}

	fmt.Println()
	fmt.Println("# SELECT with all fields")
	fmt.Println()

	for _, t := range (*selectAll)[0].Result {
		fmt.Printf("ID: %v\n", t.ID)
		// fmt.Printf("NullableZero: %v\n", t.NullableZero)
		fmt.Printf("NullableNil: %v\n", t.NullableNil)
		fmt.Printf("OptionZero: %v\n", t.OptionZero)
		fmt.Printf("OptionZeroOmitEmpty: %v\n", t.OptionZeroOmitEmpty)
		fmt.Printf("OptionZeroOmitZero: %v\n", t.OptionZeroOmitZero)
		fmt.Printf("OptionPtrZero: %v\n", t.OptionPtrZero)
		// fmt.Printf("OptionPtrNil: %v\n", t.OptionPtrNil)
		fmt.Printf("OptionPtrNilOmitEmpty: %v\n", t.OptionPtrNilOmitEmpty)
	}

	selectExplicitMap, err := surrealdb.Query[[]map[string]any](
		context.Background(),
		db,
		`SELECT id,
		        nullable_nil,
				option_zero,
				option_zero_omitempty,
				option_zero_omitzero,
				option_ptr_zero,
				option_ptr_nil_omitempty
		FROM t ORDER BY id`,
		nil,
	)
	if err != nil {
		panic(err)
	}

	fmt.Println()
	fmt.Println("# SELECT with explicit fields into map[string]any")
	fmt.Println()

	for _, t := range (*selectExplicitMap)[0].Result {
		fmt.Printf("ID: %v\n", t["id"])
		// fmt.Printf("NullableZero: %+v\n", t["nullable_zero"])
		fmt.Printf("NullableNil: %T%+v\n", t["nullable_nil"], t["nullable_nil"])
		fmt.Printf("OptionZero: %T%+v\n", t["option_zero"], t["option_zero"])
		fmt.Printf("OptionZeroOmitEmpty: %T%+v\n", t["option_zero_omitempty"], t["option_zero_omitempty"])
		fmt.Printf("OptionZeroOmitZero: %T%+v\n", t["option_zero_omitzero"], t["option_zero_omitzero"])
		fmt.Printf("OptionPtrZero: %T%+v\n", t["option_ptr_zero"], t["option_ptr_zero"])
		// fmt.Printf("OptionPtrNil: %T%+v\n", t["option_ptr_nil"], t["option_ptr_nil"])
		fmt.Printf("OptionPtrNilOmitEmpty: %T%+v\n", t["option_ptr_nil_omitempty"], t["option_ptr_nil_omitempty"])
	}

}
Output:

# SELECT with explicit fields

ID: t:1
NullableNil: <nil>
OptionZero: {0001-01-01 00:00:00 +0000 UTC}
OptionZeroOmitEmpty: {0001-01-01 00:00:00 +0000 UTC}
OptionZeroOmitZero: {0001-01-01 00:00:00 +0000 UTC}
OptionPtrZero: 0001-01-01T00:00:00Z
OptionPtrNilOmitEmpty: 0001-01-01T00:00:00Z

# SELECT with all fields

ID: t:1
NullableNil: <nil>
OptionZero: {0001-01-01 00:00:00 +0000 UTC}
OptionZeroOmitEmpty: {0001-01-01 00:00:00 +0000 UTC}
OptionZeroOmitZero: {0001-01-01 00:00:00 +0000 UTC}
OptionPtrZero: <nil>
OptionPtrNilOmitEmpty: <nil>

# SELECT with explicit fields into map[string]any

ID: {t 1}
NullableNil: <nil><nil>
OptionZero: models.CustomNil{}
OptionZeroOmitEmpty: models.CustomNil{}
OptionZeroOmitZero: models.CustomNil{}
OptionPtrZero: models.CustomNil{}
OptionPtrNilOmitEmpty: models.CustomNil{}
Example (Null_none_customdatetime_roundtrip_surrealcbor)
package main

import (
	"context"
	"fmt"
	"time"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	c := testenv.MustNewConfig("example", "query", "t")
	c.CBORImpl = testenv.CBORImplSurrealCBOR

	db := c.MustNew()

	_, err := surrealdb.Query[[]any](
		context.Background(),
		db,
		`DEFINE TABLE t SCHEMAFULL;
		//  DEFINE FIELD nullable_zero ON t TYPE datetime | null;
		 DEFINE FIELD nullable_nil ON t TYPE datetime | null;
		 DEFINE FIELD option_zero ON t TYPE option<datetime>;
		 DEFINE FIELD option_zero_omitempty ON t TYPE option<datetime>;
		 DEFINE FIELD option_zero_omitzero ON t TYPE option<datetime>;
		 DEFINE FIELD option_ptr_zero ON t TYPE option<datetime>;
		 DEFINE FIELD option_ptr_nil ON t TYPE option<datetime>;
		 DEFINE FIELD option_ptr_nil_omitempty ON t TYPE option<datetime>;
		`,
		nil,
	)
	if err != nil {
		panic(err)
	}

	type T struct {
		ID *models.RecordID `json:"id,omitempty"`
		// NullableZero tests how a Zero value of CustomDateTime is marshaled into a nullable field
		//
		// This fails like this:
		//   Found NONE for field `nullable_zero`, with record `t:1`, but expected a datetime | null
		// NullableZero  models.CustomDateTime  `json:"nullable_zero"`

		// NullableNil tests how a Go pointer to nil is marshaled into a nullable field
		NullableNil *models.CustomDateTime `json:"nullable_nil"`

		OptionZero models.CustomDateTime `json:"option_zero"`

		// OptionZeroOmitEmpty tests how a Zero value of CustomDateTime is marshaled into an option field
		OptionZeroOmitEmpty models.CustomDateTime `json:"option_zero_omitempty,omitempty"`

		// OptionZeroOmitZero tests how a Zero value of CustomDateTime is marshaled into an option field
		// when the field is omitzero
		OptionZeroOmitZero models.CustomDateTime `json:"option_zero_omitzero,omitzero"`

		// OptionPtrZero tests how a Go pointer to a Zero value of CustomDateTime is marshaled
		OptionPtrZero *models.CustomDateTime `json:"option_ptr_zero"`

		// OptionNil tests how a Go pointer to nil is marshaled
		//
		// This fails like this:
		//   Found NULL for field `option_ptr_nil`, with record `t:1`, but expected a option<datetime>
		// OptionPtrNil *models.CustomDateTime `json:"option_ptr_nil"`

		// OptionPtrNilOmitEmpty tests how a Go pointer to nil is marshaled into an option field
		// when the field is omitted if empty.
		OptionPtrNilOmitEmpty *models.CustomDateTime `json:"option_ptr_nil_omitempty,omitempty"`
	}

	_, err = surrealdb.Query[[]T](
		context.Background(),
		db,
		`CREATE $id CONTENT $value`,
		map[string]any{
			"id": models.NewRecordID("t", 1),
			"value": T{
				// NullableZero:  models.CustomDateTime{Time: time.Time{}},
				NullableNil:         nil,
				OptionZero:          models.CustomDateTime{Time: time.Time{}},
				OptionZeroOmitEmpty: models.CustomDateTime{Time: time.Time{}},
				OptionZeroOmitZero:  models.CustomDateTime{Time: time.Time{}},
				OptionPtrZero:       &models.CustomDateTime{Time: time.Time{}},
				// OptionPtrNil:  nil,
				OptionPtrNilOmitEmpty: nil,
			},
		},
	)
	if err != nil {
		panic(err)
	}

	selectExplicit, err := surrealdb.Query[[]T](
		context.Background(),
		db,
		`SELECT id,
		        nullable_nil,
				option_zero,
				option_zero_omitempty,
				option_zero_omitzero,
				option_ptr_zero,
				option_ptr_nil_omitempty
		FROM t ORDER BY id`,
		nil,
	)
	if err != nil {
		panic(err)
	}

	fmt.Println()
	fmt.Println("# SELECT with explicit fields")
	fmt.Println()

	for _, t := range (*selectExplicit)[0].Result {
		fmt.Printf("ID: %v\n", t.ID)
		// fmt.Printf("NullableZero: %v\n", t.NullableZero)
		fmt.Printf("NullableNil: %v\n", t.NullableNil)
		fmt.Printf("OptionZero: %v\n", t.OptionZero)
		fmt.Printf("OptionZeroOmitEmpty: %v\n", t.OptionZeroOmitEmpty)
		fmt.Printf("OptionZeroOmitZero: %v\n", t.OptionZeroOmitZero)
		fmt.Printf("OptionPtrZero: %v\n", t.OptionPtrZero)
		// fmt.Printf("OptionPtrNil: %v\n", t.OptionPtrNil)
		fmt.Printf("OptionPtrNilOmitEmpty: %v\n", t.OptionPtrNilOmitEmpty)
	}

	selectAll, err := surrealdb.Query[[]T](
		context.Background(),
		db,
		`SELECT * FROM t ORDER BY id`,
		nil,
	)
	if err != nil {
		panic(err)
	}

	fmt.Println()
	fmt.Println("# SELECT with all fields")
	fmt.Println()

	for _, t := range (*selectAll)[0].Result {
		fmt.Printf("ID: %v\n", t.ID)
		// fmt.Printf("NullableZero: %v\n", t.NullableZero)
		fmt.Printf("NullableNil: %v\n", t.NullableNil)
		fmt.Printf("OptionZero: %v\n", t.OptionZero)
		fmt.Printf("OptionZeroOmitEmpty: %v\n", t.OptionZeroOmitEmpty)
		fmt.Printf("OptionZeroOmitZero: %v\n", t.OptionZeroOmitZero)
		fmt.Printf("OptionPtrZero: %v\n", t.OptionPtrZero)
		// fmt.Printf("OptionPtrNil: %v\n", t.OptionPtrNil)
		fmt.Printf("OptionPtrNilOmitEmpty: %v\n", t.OptionPtrNilOmitEmpty)
	}

	selectExplicitMap, err := surrealdb.Query[[]map[string]any](
		context.Background(),
		db,
		`SELECT id,
		        nullable_nil,
				option_zero,
				option_zero_omitempty,
				option_zero_omitzero,
				option_ptr_zero,
				option_ptr_nil_omitempty
		FROM t ORDER BY id`,
		nil,
	)
	if err != nil {
		panic(err)
	}

	fmt.Println()
	fmt.Println("# SELECT with explicit fields into map[string]any")
	fmt.Println()

	for _, t := range (*selectExplicitMap)[0].Result {
		fmt.Printf("ID: %v\n", t["id"])
		// fmt.Printf("NullableZero: %+v\n", t["nullable_zero"])
		fmt.Printf("NullableNil: %T%+v\n", t["nullable_nil"], t["nullable_nil"])
		fmt.Printf("OptionZero: %T%+v\n", t["option_zero"], t["option_zero"])
		fmt.Printf("OptionZeroOmitEmpty: %T%+v\n", t["option_zero_omitempty"], t["option_zero_omitempty"])
		fmt.Printf("OptionZeroOmitZero: %T%+v\n", t["option_zero_omitzero"], t["option_zero_omitzero"])
		fmt.Printf("OptionPtrZero: %T%+v\n", t["option_ptr_zero"], t["option_ptr_zero"])
		// fmt.Printf("OptionPtrNil: %T%+v\n", t["option_ptr_nil"], t["option_ptr_nil"])
		fmt.Printf("OptionPtrNilOmitEmpty: %T%+v\n", t["option_ptr_nil_omitempty"], t["option_ptr_nil_omitempty"])
	}

}
Output:

# SELECT with explicit fields

ID: t:1
NullableNil: <nil>
OptionZero: {0001-01-01 00:00:00 +0000 UTC}
OptionZeroOmitEmpty: {0001-01-01 00:00:00 +0000 UTC}
OptionZeroOmitZero: {0001-01-01 00:00:00 +0000 UTC}
OptionPtrZero: <nil>
OptionPtrNilOmitEmpty: <nil>

# SELECT with all fields

ID: t:1
NullableNil: <nil>
OptionZero: {0001-01-01 00:00:00 +0000 UTC}
OptionZeroOmitEmpty: {0001-01-01 00:00:00 +0000 UTC}
OptionZeroOmitZero: {0001-01-01 00:00:00 +0000 UTC}
OptionPtrZero: <nil>
OptionPtrNilOmitEmpty: <nil>

# SELECT with explicit fields into map[string]any

ID: {t 1}
NullableNil: <nil><nil>
OptionZero: <nil><nil>
OptionZeroOmitEmpty: <nil><nil>
OptionZeroOmitZero: <nil><nil>
OptionPtrZero: <nil><nil>
OptionPtrNilOmitEmpty: <nil><nil>
Example (Only)

The Query function's result type parameter should be varied according to the query. For example, SELECT ONLY returns a single record, not an array of records, and therefore the result type parameter should be a single type, not a slice type.

package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "query_only", "persons")

	type Person struct {
		ID   *models.RecordID `json:"id,omitempty"`
		Name string           `json:"name"`
	}

	recordID := models.NewRecordID("persons", "yusuke")

	// Note the type parameter is []Person
	createQueryResults, err := surrealdb.Query[[]Person](
		context.Background(),
		db,
		`CREATE $record_id CONTENT {name: "Yusuke"}`,
		map[string]any{
			"record_id": recordID,
		},
	)
	if err != nil {
		panic(err)
	}
	var persons []Person = (*createQueryResults)[0].Result
	fmt.Printf("Persons contained in the first query result: %+v\n", persons)

	// Note the type parameter is Person, not []Person,
	// due to the ONLY keyword
	queryOnlyResults, err := surrealdb.Query[Person](
		context.Background(),
		db,
		`SELECT * FROM ONLY $record_id`,
		map[string]any{
			"record_id": recordID,
		},
	)
	if err != nil {
		panic(err)
	}
	var person Person = (*queryOnlyResults)[0].Result
	fmt.Printf("Person contained in the query only result: %+v\n", person)

}
Output:
Persons contained in the first query result: [{ID:persons:yusuke Name:Yusuke}]
Person contained in the query only result: {ID:persons:yusuke Name:Yusuke}
Example (Return)

ExampleQueryReturn demonstrates how to use the RETURN NONE clause in a query. See https://github.com/surrealdb/surrealdb.go/issues/203 for more context.

package main

import (
	"context"
	"fmt"
	"time"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "query", "persons")

	type NestedStruct struct {
		City string `json:"city"`
	}

	type Person struct {
		ID           *models.RecordID `json:"id,omitempty"`
		Name         string           `json:"name"`
		NestedMap    map[string]any   `json:"nested_map,omitempty"`
		NestedStruct `json:"nested_struct,omitempty"`
		CreatedAt    models.CustomDateTime  `json:"created_at,omitempty"`
		UpdatedAt    *models.CustomDateTime `json:"updated_at,omitempty"`
	}

	createdAt, err := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
	if err != nil {
		panic(err)
	}

	insertQueryResults, err := surrealdb.Query[any](
		context.Background(),
		db,
		`INSERT INTO persons [$content] RETURN NONE`,
		map[string]any{
			"content": map[string]any{
				"id":   "yusuke",
				"name": "Yusuke",
				"nested_struct": NestedStruct{
					City: "Tokyo",
				},
				"created_at": models.CustomDateTime{
					Time: createdAt,
				},
			},
		})
	if err != nil {
		panic(err)
	}
	fmt.Printf("Number of insert query results: %d\n", len(*insertQueryResults))
	fmt.Printf("First insert query result's status: %+s\n", (*insertQueryResults)[0].Status)
	fmt.Printf("Results contained in the first query result: %+v\n", (*insertQueryResults)[0].Result)

	selectQueryResults, err := surrealdb.Query[[]Person](
		context.Background(),
		db,
		`SELECT * FROM $id`, map[string]any{
			"id": models.NewRecordID("persons", "yusuke"),
		},
	)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Number of select query results: %d\n", len(*selectQueryResults))
	fmt.Printf("First select query result's status: %+s\n", (*selectQueryResults)[0].Status)
	fmt.Printf("Persons contained in the first select query result: %+v\n", (*selectQueryResults)[0].Result)

}
Output:
Number of insert query results: 1
First insert query result's status: OK
Results contained in the first query result: []
Number of select query results: 1
First select query result's status: OK
Persons contained in the first select query result: [{ID:persons:yusuke Name:Yusuke NestedMap:map[] NestedStruct:{City:Tokyo} CreatedAt:{Time:2023-10-01 12:00:00 +0000 UTC} UpdatedAt:<nil>}]
Example (SelectOnTable)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "query_select_on_table", "persons")

	type Person struct {
		ID   *models.RecordID `json:"id,omitempty"`
		Name string           `json:"name"`
	}

	// Seed two records
	_, err := surrealdb.Query[[]Person](
		context.Background(),
		db,
		`CREATE persons:alice CONTENT {name: "Alice"}; CREATE persons:bob CONTENT {name: "Bob"}`,
		nil,
	)
	if err != nil {
		panic(err)
	}

	// Use models.Table as a query variable to select all records in a table
	// Note: Directly embedding table names in query strings is prone to injection attacks.
	results, err := surrealdb.Query[[]Person](
		context.Background(),
		db,
		`SELECT * FROM $table ORDER BY name`,
		map[string]any{
			"table": models.Table("persons"),
		},
	)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Number of query results: %d\n", len(*results))
	fmt.Printf("First query result's status: %s\n", (*results)[0].Status)
	for _, p := range (*results)[0].Result {
		fmt.Printf("Person: %s (ID: %s)\n", p.Name, p.ID)
	}

}
Output:
Number of query results: 1
First query result's status: OK
Person: Alice (ID: persons:alice)
Person: Bob (ID: persons:bob)
Example (TransactionRollback)

ExampleQuery_transactionRollback demonstrates that mutations within a rolled back transaction don't persist. The CANCEL statement rolls back all changes made within the transaction.

package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	config := testenv.MustNewConfig("surrealdbexamples", "transaction_rollback", "accounts")
	config.Endpoint = testenv.GetSurrealDBURL()

	db := config.MustNew()

	type Account struct {
		ID      *models.RecordID `json:"id,omitempty"`
		Name    string           `json:"name"`
		Balance float64          `json:"balance"`
	}

	ctx := context.Background()

	// First, create an initial account outside of any transaction
	initialAccount, err := surrealdb.Create[Account](ctx, db, "accounts", map[string]any{
		"name":    "Savings Account",
		"balance": 1000.00,
	})
	if err != nil {
		panic(fmt.Sprintf("Failed to create initial account: %v", err))
	}

	fmt.Printf("Initial account created: %s with balance %.2f\n", initialAccount.Name, initialAccount.Balance)

	// Now start a transaction that will be rolled back
	transactionQuery := `
		BEGIN TRANSACTION;

		-- Create a new account within the transaction
		CREATE accounts SET name = $checkingName, balance = $checkingBalance;

		-- Update the existing account within the transaction
		UPDATE $accountID SET balance = $newBalance;

		-- Create another account
		CREATE accounts SET name = $investmentName, balance = $investmentBalance;

		-- Roll back all changes made in this transaction
		CANCEL TRANSACTION;
	`

	_, err = surrealdb.Query[any](ctx, db, transactionQuery, map[string]any{
		"accountID":         initialAccount.ID,
		"checkingName":      "Checking Account",
		"checkingBalance":   500.00,
		"newBalance":        2000.00,
		"investmentName":    "Investment Account",
		"investmentBalance": 5000.00,
	})
	// When a transaction is canceled, SurrealDB returns an error
	if err != nil {
		fmt.Println("Transaction was rolled back (as expected)")
	} else {
		panic("Expected an error from canceled transaction, but got none")
	}

	// Verify that no new accounts were created
	allAccounts, err := surrealdb.Select[[]Account](ctx, db, "accounts")
	if err != nil {
		panic(fmt.Sprintf("Failed to select accounts: %v", err))
	}

	fmt.Printf("Number of accounts after rollback: %d\n", len(*allAccounts))

	// Verify that the original account balance wasn't changed
	updatedAccount, err := surrealdb.Select[Account](ctx, db, *initialAccount.ID)
	if err != nil {
		panic(fmt.Sprintf("Failed to select account: %v", err))
	}

	fmt.Printf("Original account balance after rollback: %.2f\n", updatedAccount.Balance)

	// Now demonstrate a committed transaction for comparison
	committedTransactionQuery := `
		BEGIN TRANSACTION;

		-- Create a new account within the transaction
		CREATE accounts SET name = $businessName, balance = $businessBalance;

		-- Commit the transaction
		COMMIT TRANSACTION;
	`

	_, err = surrealdb.Query[any](ctx, db, committedTransactionQuery, map[string]any{
		"businessName":    "Business Account",
		"businessBalance": 3000.00,
	})
	if err != nil {
		panic(fmt.Sprintf("Failed to execute committed transaction: %v", err))
	}

	fmt.Println("Second transaction executed and committed")

	// Verify that the new account was created
	finalAccounts, err := surrealdb.Select[[]Account](ctx, db, "accounts")
	if err != nil {
		panic(fmt.Sprintf("Failed to select accounts: %v", err))
	}

	fmt.Printf("Number of accounts after commit: %d\n", len(*finalAccounts))

}
Output:
Initial account created: Savings Account with balance 1000.00
Transaction was rolled back (as expected)
Number of accounts after rollback: 1
Original account balance after rollback: 1000.00
Second transaction executed and committed
Number of accounts after commit: 2
Example (Transaction_issue_177_commit)

See https://github.com/surrealdb/surrealdb.go/issues/177

package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	config := testenv.MustNewConfig("surrealdbexamples", "query", "t")
	db := config.MustNew()
	ctx := context.Background()

	// Detect version to handle result format differences
	v, err := testenv.GetVersion(ctx, db)
	if err != nil {
		panic(err)
	}

	queryResults, err := surrealdb.Query[any](ctx, db,
		`BEGIN;
		CREATE t:s SET name = 'test1';
		CREATE t:t SET name = 'test2';
		SELECT * FROM $id;
		COMMIT;`,
		map[string]any{
			"id": models.RecordID{Table: "t", ID: "s"},
		})
	if err != nil {
		panic(err)
	}

	fmt.Printf("Status: %v\n", (*queryResults)[0].Status)

	// Transaction result format changed between v2 and v3:
	// - v2.x: Returns only statement results (3 results: CREATE, CREATE, SELECT)
	// - v3.x: Returns results for all statements (5 results: BEGIN, CREATE, CREATE, SELECT, COMMIT)
	// Extract only the statement results (CREATE, CREATE, SELECT)
	var statementResults []surrealdb.QueryResult[any]
	if v.IsV3OrLater() {
		// In v3, skip BEGIN (index 0) and COMMIT (last index)
		statementResults = (*queryResults)[1:4]
	} else {
		// In v2, all results are statement results
		statementResults = *queryResults
	}

	if len(statementResults) != 3 {
		panic(fmt.Errorf("expected 3 statement results, got %d", len(statementResults)))
	}

	var records []map[string]any
	for i, result := range statementResults {
		if result.Status != "OK" {
			panic(fmt.Errorf("expected OK status for statement result %d, got %s", i, result.Status))
		}
		if result.Result == nil {
			panic(fmt.Errorf("expected non-nil result for statement result %d", i))
		}
		if record, ok := result.Result.([]any); ok && len(record) > 0 {
			records = append(records, record[0].(map[string]any))
		} else {
			panic(fmt.Errorf("expected result to be a slice of maps, got %T", result.Result))
		}
	}

	fmt.Printf("result[0].id: %v\n", records[0]["id"])
	fmt.Printf("result[0].name: %v\n", records[0]["name"])
	fmt.Printf("result[1].id: %v\n", records[1]["id"])
	fmt.Printf("result[1].name: %v\n", records[1]["name"])
	if id := records[2]["id"]; id != nil && id != (models.RecordID{Table: "t", ID: "s"}) {
		panic(fmt.Errorf("expected id to be empty for SurrealDB v3.0.0-alpha.7, or 's' for v2.3.7, got %v", id))
	}
	fmt.Printf("result[2].name: %v\n", records[2]["name"])

}
Output:
Status: OK
result[0].id: {t s}
result[0].name: test1
result[1].id: {t t}
result[1].name: test2
result[2].name: test1
Example (Transaction_issue_177_return_before_commit)

See https://github.com/surrealdb/surrealdb.go/issues/177

package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	config := testenv.MustNewConfig("surrealdbexamples", "query", "t")
	db := config.MustNew()
	ctx := context.Background()

	// Detect version to handle result format differences
	v, err := testenv.GetVersion(ctx, db)
	if err != nil {
		panic(err)
	}

	// Note that you are returning before committing the transaction.
	// In this case, you get the uncommitted result of the CREATE,
	// which lacks the ID field becase we aren't sure if the ID is committed or not
	// at that point.
	// SurrealDB may be enhanced to handle this, but for now,
	// you should commit the transaction before returning the result.
	// See the ExampleQuery_transaction_issue_177_commit function for the correct way to do this.
	queryResults, err := surrealdb.Query[any](ctx, db,
		`BEGIN;
		CREATE t:s SET name = 'test';
		LET $i = SELECT * FROM $id;
		RETURN $i;
		COMMIT;`,
		map[string]any{
			"id": models.RecordID{Table: "t", ID: "s"},
		})
	if err != nil {
		panic(err)
	}

	// Transaction result format changed between v2 and v3:
	// - v2.x: Returns only the RETURN result (1 result)
	// - v3.x: Returns results for all statements (5 results: BEGIN, CREATE, LET, RETURN, COMMIT)
	var returnResult surrealdb.QueryResult[any]
	if v.IsV3OrLater() {
		// In v3, the RETURN result is at index 3 (after BEGIN, CREATE, LET)
		returnResult = (*queryResults)[3]
	} else {
		// In v2, only the RETURN result is returned
		returnResult = (*queryResults)[0]
	}

	rs := returnResult.Result.([]any)
	r := rs[0].(map[string]any)

	fmt.Printf("Status: %v\n", returnResult.Status)
	fmt.Printf("r.name: %v\n", r["name"])
	if id := r["id"]; id != nil && id != (models.RecordID{Table: "t", ID: "s"}) {
		panic(fmt.Errorf("expected id to be empty for SurrealDB v3.0.0-alpha.7, or 's' for v2.3.7, got %v", id))
	}

}
Output:
Status: OK
r.name: test
Example (Transaction_let_return)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	config := testenv.MustNewConfig("surrealdbexamples", "query", "t")
	db := config.MustNew()
	ctx := context.Background()

	// Detect version to handle result format differences
	v, err := testenv.GetVersion(ctx, db)
	if err != nil {
		panic(err)
	}

	createQueryResults, err := surrealdb.Query[[]any](
		ctx,
		db,
		`BEGIN;
		 CREATE t:1 SET name = 'test';
		 LET $i = SELECT * FROM $id;
		 RETURN $i.name;
		 COMMIT
		`,
		map[string]any{
			"id": models.NewRecordID("t", 1),
		})
	if err != nil {
		panic(err)
	}

	// Transaction result format changed between v2 and v3:
	// - v2.x: Returns only the RETURN result (1 result)
	// - v3.x: Returns results for all statements (5 results)
	var returnResult any
	if v.IsV3OrLater() {
		// In v3, the RETURN result is at index 3 (after BEGIN, CREATE, LET)
		returnResult = (*createQueryResults)[3].Result
	} else {
		// In v2, only the RETURN result is returned
		returnResult = (*createQueryResults)[0].Result
	}
	fmt.Printf("First query result's status: %+s\n", (*createQueryResults)[0].Status)
	fmt.Printf("Names contained in the RETURN result: %+v\n", returnResult)

}
Output:
First query result's status: OK
Names contained in the RETURN result: [test]
Example (Transaction_return)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	config := testenv.MustNewConfig("surrealdbexamples", "query", "person")
	db := config.MustNew()
	ctx := context.Background()

	// Detect version to handle result format differences
	v, err := testenv.GetVersion(ctx, db)
	if err != nil {
		panic(err)
	}

	// Transaction result format changed between v2 and v3:
	// - v2.x: Returns only the RETURN result (1 result)
	// - v3.x: Returns results for all statements (5 results: BEGIN, CREATE, CREATE, RETURN, COMMIT)
	// For v3.x, use []any to avoid decode error when the type varies per result
	results, err := surrealdb.Query[any](
		ctx,
		db,
		`BEGIN; CREATE person:1; CREATE person:2; RETURN true; COMMIT;`,
		map[string]any{},
	)
	if err != nil {
		panic(err)
	}

	var resultBool bool
	if v.IsV3OrLater() {
		// In v3, the RETURN result is at index 3 (after BEGIN, CREATE, CREATE)
		resultBool = (*results)[3].Result.(bool)
	} else {
		// In v2, only the RETURN result is returned
		resultBool = (*results)[0].Result.(bool)
	}
	fmt.Printf("Status: %v\n", (*results)[0].Status)
	fmt.Printf("Result: %v\n", resultBool)

}
Output:
Status: OK
Result: true
Example (Transaction_throw)
package main

import (
	"context"
	"errors"
	"fmt"
	"strings"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "query", "person")

	var (
		queryResults *[]surrealdb.QueryResult[*int]
		err          error
	)

	// Up until v0.4.3, making QueryResult[T] parameterized with anything other than `any`
	// or `string` failed with:
	//   cannot unmarshal UTF-8 text string into Go struct field
	// in case the query was executed on the database but failed with an error.
	//
	// It was due to a mismatch between the expected type and the actual type-
	// The actual query result was a string, which provides the error message sent
	// from the database, regardless of the type parameter specified to the Query function.
	//
	// Since v0.4.4, the QueryResult was enhanced to set the Error field
	// to a QueryError if the query failed, allowing the caller to handle the error.
	// In that case, the Result field will be empty(or nil if it is a pointer type),
	// and the Status field will be set to "ERR".
	//
	// It's also worth noting that the returned error from the Query function
	// will be nil if the query was executed successfully, in which case all the results
	// have no Error field set.
	//
	// If the query failed, the returned error will be a `joinError` created by the `errors.Join` function,
	// which contains all the errors that occurred during the query execution.
	// The caller can check the Error field of each QueryResult to see if the query failed,
	// or check the returned error from the Query function to see if the query failed.
	queryResults, err = surrealdb.Query[*int](
		context.Background(),
		db,
		`BEGIN; THROW "test"; RETURN 1; COMMIT;`,
		nil,
	)

	// Normalize error messages for version compatibility
	// v2.x: "failed transaction"
	// v3.x: uses British spelling in error messages
	normalizeTransactionError := func(err error) string {
		if err == nil {
			return "<nil>"
		}
		s := err.Error()
		s = strings.ReplaceAll(s, "cancelled transaction", "failed transaction") //nolint:misspell
		s = strings.ReplaceAll(s, "canceled transaction", "failed transaction")
		return s
	}

	// Filter to only show ERR results (v3 adds OK results for BEGIN)
	var errResults []surrealdb.QueryResult[*int]
	for _, r := range *queryResults {
		if r.Status == "ERR" {
			errResults = append(errResults, r)
		}
	}

	fmt.Printf("# of ERR results: %d\n", len(errResults))
	fmt.Println("=== Func error ===")
	fmt.Printf("Error: %v\n", normalizeTransactionError(err))
	fmt.Printf("Error is RPCError: %v\n", errors.Is(err, &surrealdb.RPCError{}))
	fmt.Printf("Error is QueryError: %v\n", errors.Is(err, &surrealdb.QueryError{}))
	for i, r := range errResults {
		fmt.Printf("=== QueryResult[%d] ===\n", i)
		fmt.Printf("Status: %v\n", r.Status)
		fmt.Printf("Result: %v\n", r.Result)
		fmt.Printf("Error: %v\n", normalizeTransactionError(r.Error))
		fmt.Printf("Error is RPCError: %v\n", errors.Is(r.Error, &surrealdb.RPCError{}))
		fmt.Printf("Error is QueryError: %v\n", errors.Is(r.Error, &surrealdb.QueryError{}))
	}

}
Output:
# of ERR results: 2
=== Func error ===
Error: An error occurred: test
The query was not executed due to a failed transaction
Error is RPCError: false
Error is QueryError: true
=== QueryResult[0] ===
Status: ERR
Result: <nil>
Error: An error occurred: test
Error is RPCError: false
Error is QueryError: true
=== QueryResult[1] ===
Status: ERR
Result: <nil>
Error: The query was not executed due to a failed transaction
Error is RPCError: false
Error is QueryError: true

func QueryRaw added in v0.3.0

func QueryRaw[S sendable](ctx context.Context, s S, queries *[]QueryStmt) error

QueryRaw composes a query from the provided QueryStmt objects, and execute it using the query RPC method. S can be *DB, *Session, or *Transaction.

You may want to use Query with github.com/surrealdb/surrealdb.go/contrib/surrealql instead.

func Relate added in v0.3.0

func Relate[TResult any, S sendable](ctx context.Context, s S, rel *Relationship) (*TResult, error)

Relate creates a relationship between two records in the table with a generated relationship ID. S can be *DB, *Session, or *Transaction.

The relation needs to be specified via the `Relation` field of the Relationship struct.

A relation is basically a table, so you can query it directly using SELECT if needed.

Although the Relationship struct allows you to specify the ID, it is ignored when you use Relate, and the ID is generated by SurrealDB.

In other words, Relationship.ID is meant for unmarshaling the relation from the database to the Relationship struct, in which case the ID is set to the ID of the relation record generated by SurrealDB.

In case you only care about the returned relationship's ID, use `connection.ResponseID[models.RecordID]` for the TResult type parameter.

Example
package main

import (
	"context"
	"fmt"
	"time"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/connection"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "query", "person", "follow")

	type Person struct {
		ID models.RecordID `json:"id,omitempty"`
	}

	type Follow struct {
		In    *models.RecordID      `json:"in,omitempty"`
		Out   *models.RecordID      `json:"out,omitempty"`
		Since models.CustomDateTime `json:"since"`
	}

	first, err := surrealdb.Create[Person](
		context.Background(),
		db,
		"person",
		map[string]any{
			"id": models.NewRecordID("person", "first"),
		})
	if err != nil {
		panic(err)
	}

	second, err := surrealdb.Create[Person](
		context.Background(),
		db,
		"person",
		map[string]any{
			"id": models.NewRecordID("person", "second"),
		})
	if err != nil {
		panic(err)
	}

	since, err := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
	if err != nil {
		panic(err)
	}

	persons, err := surrealdb.Query[[]Person](
		context.Background(),
		db,
		"SELECT * FROM person ORDER BY id.id",
		nil,
	)
	if err != nil {
		panic(err)
	}
	for _, person := range (*persons)[0].Result {
		fmt.Printf("Person: %+v\n", person)
	}

	res, relateErr := surrealdb.Relate[connection.ResponseID[models.RecordID]](
		context.Background(),
		db,
		&surrealdb.Relationship{
			// ID is currently ignored, and the relation will have a generated ID.
			// If you want to set the ID, use InsertRelation, or use
			// Query with `RELATE` statement.
			ID:       &models.RecordID{Table: "follow", ID: "first_second"},
			In:       first.ID,
			Out:      second.ID,
			Relation: "follow",
			Data: map[string]any{
				"since": models.CustomDateTime{
					Time: since,
				},
			},
		},
	)
	if relateErr != nil {
		panic(relateErr)
	}
	if res == nil {
		panic("relation response is nil")
	}
	if res.ID.ID == "first_second" {
		panic("relation ID should not be set to 'first_second'")
	}

	//nolint:lll
	/// Here's an alternative way to create a relation using a query.
	//
	// if res, err := surrealdb.Query[any](
	// 	db,
	// 	"RELATE $in->follow:first_second->$out SET since = $since",
	// 	map[string]any{
	// 		// `RELATE $in->follow->$out` with "id" below is ignored,
	// 		// and the id becomes a generated one.
	// 		// If you want to set the id, use `RELATE $in->follow:the_id->$out` like above.
	// 		// "id":    models.NewRecordID("follow", "first_second"),
	// 		"in":    first.ID,
	// 		"out":   second.ID,
	// 		"since": models.CustomDateTime{Time: since},
	// 	},
	// ); err != nil {
	// 	panic(err)
	// } else {
	// 	fmt.Printf("Relation: %+v\n", (*res)[0].Result)
	// }
	// The output will be:
	// Relation: [map[id:{Table:follow ID:first_second} in:{Table:person ID:first} out:{Table:person ID:second} since:{Time:2023-10-01 12:00:00 +0000 UTC}]]

	type PersonWithFollows struct {
		Person
		Follows []models.RecordID `json:"follows,omitempty"`
	}
	selected, err := surrealdb.Query[[]PersonWithFollows](
		context.Background(),
		db,
		"SELECT id, name, ->follow->person AS follows FROM $id",
		map[string]any{
			"id": first.ID,
		},
	)
	if err != nil {
		panic(err)
	}

	for _, person := range (*selected)[0].Result {
		fmt.Printf("PersonWithFollows: %+v\n", person)
	}

	// Note we can select the relationships themselves because
	// RELATE creates a record in the relation table.
	follows, err := surrealdb.Query[[]Follow](
		context.Background(),
		db,
		"SELECT * from follow",
		nil,
	)
	if err != nil {
		panic(err)
	}

	for _, follow := range (*follows)[0].Result {
		fmt.Printf("Follow: %+v\n", follow)
	}

}
Output:
Person: {ID:{Table:person ID:first}}
Person: {ID:{Table:person ID:second}}
PersonWithFollows: {Person:{ID:{Table:person ID:first}} Follows:[{Table:person ID:second}]}
Follow: {In:person:first Out:person:second Since:{Time:2023-10-01 12:00:00 +0000 UTC}}

func Select added in v0.3.0

func Select[TResult any, TWhat TableOrRecord, S sendable](ctx context.Context, s S, what TWhat) (*TResult, error)

Select retrieves records from the database. S can be *DB, *Session, or *Transaction.

Example
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "updatedb", "person")

	type Person struct {
		ID models.RecordID `json:"id,omitempty"`
	}

	a := Person{ID: models.NewRecordID("person", "a")}
	b := Person{ID: models.NewRecordID("person", "b")}

	for _, p := range []Person{a, b} {
		created, err := surrealdb.Create[Person](
			context.Background(),
			db,
			p.ID,
			map[string]any{},
		)
		if err != nil {
			panic(err)
		}
		fmt.Printf("Created person: %+v\n", *created)
	}

	selectedOneUsingSelect, err := surrealdb.Select[Person](
		context.Background(),
		db,
		a.ID,
	)
	if err != nil {
		panic(err)
	}
	fmt.Printf("selectedOneUsingSelect: %+v\n", *selectedOneUsingSelect)

	selectedMultiUsingSelect, err := surrealdb.Select[[]Person](
		context.Background(),
		db,
		"person",
	)
	if err != nil {
		panic(err)
	}
	for _, p := range *selectedMultiUsingSelect {
		fmt.Printf("selectedMultiUsingSelect: %+v\n", p)
	}

}
Output:
Created person: {ID:{Table:person ID:a}}
Created person: {ID:{Table:person ID:b}}
selectedOneUsingSelect: {ID:{Table:person ID:a}}
selectedMultiUsingSelect: {ID:{Table:person ID:a}}
selectedMultiUsingSelect: {ID:{Table:person ID:b}}
Example (NonExistentRecord_fxamackercbor)

ExampleSelect_nonExistentRecord_fxamackercbor demonstrates how fxamacker/cbor handles non-existent records - it returns a struct with non-nil CustomNil{} for missing pointer fields

package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	c := testenv.MustNewConfig("example", "selectdb", "user")
	c.CBORImpl = testenv.CBORImplFxamackerCBOR

	db := c.MustNew()
	ctx := context.Background()

	// Create the table first - SurrealDB 3.x requires the table to exist
	_, err := surrealdb.Query[any](ctx, db, `DEFINE TABLE user`, nil)
	if err != nil {
		panic(err)
	}

	type User struct {
		ID       *models.RecordID `json:"id,omitempty"`
		Username string           `json:"username"`
		Email    string           `json:"email"`
	}

	// Try to select a record that doesn't exist
	user, err := surrealdb.Select[User](ctx, db, models.NewRecordID("user", "does_not_exist"))
	if err != nil {
		panic(err)
	}

	// With fxamacker/cbor, non-existent records return a struct where:
	// - Pointer fields that would be NONE become nil (behavior changed after v1.0.0)
	// - This example shows the current behavior with fxamacker
	fmt.Printf("User found: %t\n", user != nil)
	fmt.Printf("User.ID is nil: %t\n", user.ID == nil)
	fmt.Printf("User.ID type: %T\n", user.ID)
	fmt.Printf("User.Username: %q\n", user.Username)
	fmt.Printf("User.Email: %q\n", user.Email)

}
Output:
User found: true
User.ID is nil: true
User.ID type: *models.RecordID
User.Username: ""
User.Email: ""
Example (NonExistentRecord_surrealcbor)

ExampleSelect_nonExistentRecord_surrealcbor demonstrates how surrealcbor handles non-existent records - it returns nil

package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	c := testenv.MustNewConfig("example", "selectdb", "user")
	c.CBORImpl = testenv.CBORImplSurrealCBOR

	db := c.MustNew()
	ctx := context.Background()

	// Create the table first - SurrealDB 3.x requires the table to exist
	_, err := surrealdb.Query[any](ctx, db, `DEFINE TABLE user`, nil)
	if err != nil {
		panic(err)
	}

	type User struct {
		ID       *models.RecordID `json:"id,omitempty"`
		Username string           `json:"username"`
		Email    string           `json:"email"`
	}

	// Try to select a record that doesn't exist
	user, err := surrealdb.Select[User](ctx, db, models.NewRecordID("user", "does_not_exist"))
	if err != nil {
		panic(err)
	}

	// With surrealcbor, non-existent records return nil
	// - This is why tests like s.Require().Nil(user) pass
	fmt.Printf("User found: %t\n", user != nil)

}
Output:
User found: false

func Send added in v0.7.0

func Send[Result any](ctx context.Context, db *DB, res *connection.RPCResponse[Result], method string, params ...any) error

Send sends a request to the SurrealDB server.

It is a wrapper around connection.Send, which is used by various RPC methods like Query, Insert and so on.

Compared to the original connection.Send, Send is smarter about methods that are allowed to be sent. You usually want to use this function than using connection.Send directly.

This function is limited to a selected set of RPC methods listed below:

- select - create - insert - insert_relation - kill - live - merge - relate - update - upsert - patch - delete - query

The `res` needs to be of type `*connection.RPCResponse[T]`.

It returns an error in the following cases: - Error if the method is not allowed to be sent, which means that the request was not even sent. - Transport error like WebSocket message write timeout, connection closed, etc. - Unmarshal error if the response cannot be unmarshaled into the provided res parameter. - RPCError if the request was processed by SurrealDB but it failed there.

Example (Select)

Send can be used to any SurrealDB RPC method including "select".

package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/connection"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

// Send can be used to any SurrealDB RPC method including "select".
func main() {
	db := testenv.MustNew("surrealdbexamples", "updatedb", "person")

	type Person struct {
		ID models.RecordID `json:"id,omitempty"`
	}

	a := Person{ID: models.NewRecordID("person", "a")}
	b := Person{ID: models.NewRecordID("person", "b")}

	for _, p := range []Person{a, b} {
		created, err := surrealdb.Create[Person](
			context.Background(),
			db,
			p.ID,
			map[string]any{},
		)
		if err != nil {
			panic(err)
		}
		fmt.Printf("Created person: %+v\n", *created)
	}

	var selectedUsingSendSelect connection.RPCResponse[Person]
	err := surrealdb.Send(
		context.Background(),
		db,
		&selectedUsingSendSelect,
		"select",
		a.ID,
	)
	if err != nil {
		panic(err)
	}
	fmt.Printf("selectedUsingSendSelect: %+v\n", *selectedUsingSendSelect.Result)

	var selectedMultiUsingSendSelect connection.RPCResponse[[]Person]
	err = surrealdb.Send(
		context.Background(),
		db,
		&selectedMultiUsingSendSelect,
		"select",
		"person",
	)
	if err != nil {
		panic(err)
	}
	for _, p := range *selectedMultiUsingSendSelect.Result {
		fmt.Printf("selectedMultiUsingSendSelect: %+v\n", p)
	}

	var selectedOneUsingCustomSelect *Person
	selectedOneUsingCustomSelect, err = customSelect[Person](db, a.ID)
	if err != nil {
		panic(err)
	}
	fmt.Printf("selectedOneUsingCustomSelect: %+v\n", *selectedOneUsingCustomSelect)

	var selectedMultiUsingCustomSelect *[]Person
	selectedMultiUsingCustomSelect, err = customSelect[[]Person](db, "person")
	if err != nil {
		panic(err)
	}
	for _, p := range *selectedMultiUsingCustomSelect {
		fmt.Printf("selectedMultiUsingCustomSelect: %+v\n", p)
	}

}

func customSelect[TResult any, TWhat surrealdb.TableOrRecord](db *surrealdb.DB, what TWhat) (*TResult, error) {
	var res connection.RPCResponse[TResult]

	if err := surrealdb.Send(context.Background(), db, &res, "select", what); err != nil {
		return nil, err
	}

	return res.Result, nil
}
Output:
Created person: {ID:{Table:person ID:a}}
Created person: {ID:{Table:person ID:b}}
selectedUsingSendSelect: {ID:{Table:person ID:a}}
selectedMultiUsingSendSelect: {ID:{Table:person ID:a}}
selectedMultiUsingSendSelect: {ID:{Table:person ID:b}}
selectedOneUsingCustomSelect: {ID:{Table:person ID:a}}
selectedMultiUsingCustomSelect: {ID:{Table:person ID:a}}
selectedMultiUsingCustomSelect: {ID:{Table:person ID:b}}

func Update added in v0.3.0

func Update[TResult any, TWhat TableOrRecord, S sendable](ctx context.Context, s S, what TWhat, data any) (*TResult, error)

Update replaces a record in the database like a PUT request. S can be *DB, *Session, or *Transaction.

Example
package main

import (
	"context"
	"fmt"
	"time"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "updatedb", "persons")

	type NestedStruct struct {
		City string `json:"city"`
	}

	type Person struct {
		ID           *models.RecordID `json:"id,omitempty"`
		Name         string           `json:"name"`
		NestedMap    map[string]any   `json:"nested_map,omitempty"`
		NestedStruct `json:"nested_struct,omitempty"`
		CreatedAt    models.CustomDateTime  `json:"created_at,omitempty"`
		UpdatedAt    *models.CustomDateTime `json:"updated_at,omitempty"`
	}

	createdAt, err := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
	if err != nil {
		panic(err)
	}

	recordID := models.NewRecordID("persons", "yusuke")
	created, err := surrealdb.Create[Person](context.Background(), db, recordID, map[string]any{
		"name": "Yusuke",
		"nested_struct": NestedStruct{
			City: "Tokyo",
		},
		"created_at": models.CustomDateTime{
			Time: createdAt,
		},
	})
	if err != nil {
		panic(err)
	}
	fmt.Printf("Created persons: %+v\n", *created)

	updatedAt, err := time.Parse(time.RFC3339, "2023-10-02T12:00:00Z")
	if err != nil {
		panic(err)
	}

	updated, err := surrealdb.Update[Person](context.Background(), db, recordID, map[string]any{
		"name": "Yusuke",
		"nested_map": map[string]any{
			"key1": "value1",
		},
		"nested_struct": NestedStruct{
			City: "Kagawa",
		},
		"updated_at": models.CustomDateTime{
			Time: updatedAt,
		},
	})
	if err != nil {
		panic(err)
	}

	fmt.Printf("Updated persons: %+v\n", *updated)

}
Output:
Created persons: {ID:persons:yusuke Name:Yusuke NestedMap:map[] NestedStruct:{City:Tokyo} CreatedAt:{Time:2023-10-01 12:00:00 +0000 UTC} UpdatedAt:<nil>}
Updated persons: {ID:persons:yusuke Name:Yusuke NestedMap:map[key1:value1] NestedStruct:{City:Kagawa} CreatedAt:{Time:0001-01-01 00:00:00 +0000 UTC} UpdatedAt:2023-10-02T12:00:00Z}

func Upsert added in v0.3.0

func Upsert[TResult any, TWhat TableOrRecord, S sendable](ctx context.Context, s S, what TWhat, data any) (*TResult, error)

Upsert creates or updates a record in the database. S can be *DB, *Session, or *Transaction.

Example
package main

import (
	"context"
	"fmt"
	"time"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "query", "persons")

	type Person struct {
		ID   *models.RecordID `json:"id,omitempty"`
		Name string           `json:"name"`
		// Note that you must use CustomDateTime instead of time.Time.
		// See
		CreatedAt models.CustomDateTime  `json:"created_at,omitempty"`
		UpdatedAt *models.CustomDateTime `json:"updated_at,omitempty"`
	}

	createdAt, err := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
	if err != nil {
		panic(err)
	}

	inserted, err := surrealdb.Upsert[Person](
		context.Background(),
		db,
		models.NewRecordID("persons", "yusuke"),
		map[string]any{
			"name":       "Yusuke",
			"created_at": createdAt,
		})
	if err != nil {
		panic(err)
	}
	fmt.Printf("Insert via upsert result: %v\n", *inserted)

	updated, err := surrealdb.Upsert[Person](
		context.Background(),
		db,
		models.NewRecordID("persons", "yusuke"),
		map[string]any{
			"name": "Yusuke Updated",
			// because the upsert RPC is like UPSERT ~ CONTENT rather than UPSERT ~ MERGE,
			// the created_at field becomes None, which results in the returned created_at field being zero value.
			"updated_at": createdAt,
		},
	)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Update via upsert result: %v\n", *updated)

	udpatedAt, err := time.Parse(time.RFC3339, "2023-10-02T12:00:00Z")
	if err != nil {
		panic(err)
	}
	updatedFurther, err := surrealdb.Upsert[Person](
		context.Background(),
		db,
		models.NewRecordID("persons", "yusuke"),
		map[string]any{
			"name":       "Yusuke Updated Further",
			"created_at": createdAt,
			"updated_at": models.CustomDateTime{
				Time: udpatedAt,
			},
		},
	)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Update further via upsert result: %v\n", *updatedFurther)

	_, err = surrealdb.Upsert[struct{}](
		context.Background(),
		db,
		models.NewRecordID("persons", "yusuke"),
		map[string]any{
			"name": "Yusuke Updated Last",
		},
	)
	if err != nil {
		panic(err)
	}

	selected, err := surrealdb.Select[Person](
		context.Background(),
		db,
		models.NewRecordID("persons", "yusuke"),
	)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Selected person: %v\n", *selected)

}
Output:
Insert via upsert result: {persons:yusuke Yusuke {2023-10-01 12:00:00 +0000 UTC} <nil>}
Update via upsert result: {persons:yusuke Yusuke Updated {0001-01-01 00:00:00 +0000 UTC} 2023-10-01T12:00:00Z}
Update further via upsert result: {persons:yusuke Yusuke Updated Further {2023-10-01 12:00:00 +0000 UTC} 2023-10-02T12:00:00Z}
Selected person: {persons:yusuke Yusuke Updated Last {0001-01-01 00:00:00 +0000 UTC} <nil>}
Example (Rpc_error)
package main

import (
	"context"
	"errors"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "query", "person")

	type Person struct {
		Name string `json:"name"`
	}

	// For this example, we will define a SCHEMAFUL table
	// with a name field that is a string.
	// Trying to set the name field to a number
	// will result in an error from the database.

	if _, err := surrealdb.Query[any](
		context.Background(),
		db,
		`DEFINE TABLE person SCHEMAFUL;
		 DEFINE FIELD name ON person TYPE string;`,
		nil,
	); err != nil {
		panic(err)
	}

	// This will fail because the record ID is not valid.
	_, err := surrealdb.Upsert[Person](
		context.Background(),
		db,
		models.Table("person"),
		map[string]any{
			"id": models.NewRecordID("person", "a"),
			// Unlike ExampleUpsert_unmarshal_error,
			// this will fail on the database side
			// because the name field is defined as a string,
			// and we are trying to set it to a number.
			"name": 123,
		},
	)
	if err != nil {
		switch err.Error() {
		// As of v3.0.0-alpha.7
		case "There was a problem with the database: Couldn't coerce value for field `name` of `person:a`: Expected `string` but found `123`":
			fmt.Println("Encountered expected error for SurrealDB 2.x or 3.x")
		// As of v3.0.0-beta.2 (format changed)
		case "Couldn't coerce value for field `name` of `person:a`: Expected `string` but found `123`":
			fmt.Println("Encountered expected error for SurrealDB 2.x or 3.x")
		// As of v2.3.7
		case "There was a problem with the database: Found 123 for field `name`, with record `person:a`, but expected a string":
			fmt.Println("Encountered expected error for SurrealDB 2.x or 3.x")
		default:
			fmt.Printf("Unknown Error: %v\n", err)
		}
		fmt.Printf("Error is RPCError: %v\n", errors.Is(err, &surrealdb.RPCError{}))
	}

}
Output:
Encountered expected error for SurrealDB 2.x or 3.x
Error is RPCError: true
Example (Server_error)

ExampleUpsert_server_error demonstrates extracting a *ServerError from an RPC error on SurrealDB v3 using errors.As.

On SurrealDB v2, the error is still an *RPCError (backward compatible), but errors.As(err, &se) also works because RPCError.Unwrap() returns a *ServerError. On v2 servers, se.Kind will be empty.

package main

import (
	"context"
	"errors"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	db := testenv.MustNew("surrealdbexamples", "server_error", "person")

	type Person struct {
		Name string `json:"name"`
	}

	if _, err := surrealdb.Query[any](
		context.Background(),
		db,
		`DEFINE TABLE person SCHEMAFUL;
		 DEFINE FIELD name ON person TYPE string;`,
		nil,
	); err != nil {
		panic(err)
	}

	_, err := surrealdb.Upsert[Person](
		context.Background(),
		db,
		models.Table("person"),
		map[string]any{
			"id":   models.NewRecordID("person", "a"),
			"name": 123,
		},
	)
	if err != nil {
		// v2 backward compat: RPCError is still matchable
		fmt.Printf("Error is RPCError: %v\n", errors.Is(err, &surrealdb.RPCError{}))

		// v3 migration path: extract ServerError for structured info
		fmt.Printf("Error is ServerError: %v\n", errors.Is(err, surrealdb.ServerError{}))
	}

}
Output:
Error is RPCError: true
Error is ServerError: true
Example (Unmarshal_error_fxamackercbor_legacy_fxamackercbor)
package main

import (
	"context"
	"errors"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	c := testenv.MustNewConfig("example", "query", "person")
	c.CBORImpl = testenv.CBORImplFxamackerCBOR

	db := c.MustNew()

	type Person struct {
		Name string `json:"name"`
	}

	// This will fail because the record ID is not valid.
	_, err := surrealdb.Upsert[Person](
		context.Background(),
		db,
		models.Table("person"),
		map[string]any{
			// We are trying to set the name field to a number,
			// which is OK from the database's perspective,
			// because the table is schemaless for this example.
			//
			// However, we are trying to unmarshal the result into a struct
			// that expects the name field to be a string,
			// which will fail when the result is unmarshaled.
			"name": 123,
		},
	)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		fmt.Printf("Error is RPCError: %v\n", errors.Is(err, &surrealdb.RPCError{}))
	}

}
Output:
Error: Send: error unmarshaling result: cbor: cannot unmarshal array into Go value of type surrealdb_test.Person (cannot decode CBOR array to struct without toarray option)
Error is RPCError: false
Example (Unmarshal_error_surrealcbor)

ExampleUpsert_unmarshal_error_surrealcbor demonstrates that surrealcbor fails unmarshaling CBOR array into Go struct with similar but different error message compared to fxamacker/cbor

package main

import (
	"context"
	"errors"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/models"
)

func main() {
	c := testenv.MustNewConfig("example", "query", "person")
	c.CBORImpl = testenv.CBORImplSurrealCBOR

	db := c.MustNew()

	type Person struct {
		Name string `json:"name"`
	}

	// This will fail because the record ID is not valid.
	_, err := surrealdb.Upsert[Person](
		context.Background(),
		db,
		models.Table("person"),
		map[string]any{
			// We are trying to set the name field to a number,
			// which is OK from the database's perspective,
			// because the table is schemaless for this example.
			//
			// However, we are trying to unmarshal the result into a struct
			// that expects the name field to be a string,
			// which will fail when the result is unmarshaled.
			"name": 123,
		},
	)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		fmt.Printf("Error is RPCError: %v\n", errors.Is(err, &surrealdb.RPCError{}))
	}

}
Output:
Error: Send: error unmarshaling result: cannot decode array into surrealdb_test.Person
Error is RPCError: false

Types

type Auth added in v0.3.0

type Auth struct {
	Namespace string `json:"NS,omitempty"`
	Database  string `json:"DB,omitempty"`
	Scope     string `json:"SC,omitempty"`
	Access    string `json:"AC,omitempty"`
	Username  string `json:"user,omitempty"`
	Password  string `json:"pass,omitempty"` //nolint:gosec // G117: user-supplied auth credential
}

Auth is a struct that holds surrealdb auth data for login.

type DB

type DB struct {
	// contains filtered or unexported fields
}

DB is a client for the SurrealDB database that holds the connection.

Example (Record_user_auth_struct)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	ns := "surrealdbexamples"
	db := testenv.MustNew(ns, "record_auth_demo", "user")

	setupQuery := `
		-- Define the user table with schema
		DEFINE TABLE user SCHEMAFULL
			PERMISSIONS
				FOR select, update, delete WHERE id = $auth.id;

		-- Define fields
		DEFINE FIELD name ON user TYPE string;
		DEFINE FIELD password ON user TYPE string;

		-- Define unique index on email
		REMOVE INDEX IF EXISTS name ON user;
		DEFINE INDEX name ON user FIELDS name UNIQUE;

		-- Define access method for record authentication
		REMOVE ACCESS IF EXISTS user ON DATABASE;
		DEFINE ACCESS user ON DATABASE TYPE RECORD
			SIGNIN (
				SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(password, $pass)
			)
			SIGNUP (
				CREATE user CONTENT {
					name: $user,
					password: crypto::argon2::generate($pass)
				}
			);
	`

	if _, err := surrealdb.Query[any](context.Background(), db, setupQuery, nil); err != nil {
		panic(err)
	}

	fmt.Println("Database schema setup complete")

	// Refer to the next example, `ExampleDB_record_user_custom_struct`,
	// when you need to use fields other than `user` and `pass` in the query specified for SIGNUP.
	_, err := db.SignUp(context.Background(), &surrealdb.Auth{
		Namespace: ns,
		Database:  "record_auth_demo",
		Access:    "user",
		Username:  "yusuke",
		Password:  "VerySecurePassword123!",
	})
	if err != nil {
		panic(err)
	}
	fmt.Println("User signed up successfully")

	// Refer to the next example, `ExampleDB_record_user_custom_struct`,
	// when you need to use fields other than `user` and `pass` in the query specified for SIGNIN.
	//
	// For example, you might want to use `email` and `password` instead of `user` and `pass`.
	// In that case, you need to something that encodes to a cbor map containing those keys.
	_, err = db.SignIn(context.Background(), &surrealdb.Auth{
		Namespace: ns,
		Database:  "record_auth_demo",
		Access:    "user",
		Username:  "yusuke",
		Password:  "VerySecurePassword123!",
	})
	if err != nil {
		panic(err)
	}
	fmt.Println("User signed in successfully")

	info, err := db.Info(context.Background())
	if err != nil {
		panic(err)
	}
	fmt.Printf("Authenticated user name: %v\n", info["name"])

}
Output:
Database schema setup complete
User signed up successfully
User signed in successfully
Authenticated user name: yusuke
Example (Record_user_custom_struct)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	ns := "surrealdbexamples"
	db := testenv.MustNew(ns, "record_user_custom", "user")

	setupQuery := `
		-- Define the user table with schema
		DEFINE TABLE user SCHEMAFULL
			PERMISSIONS
				FOR select, update, delete WHERE id = $auth.id;

		-- Define fields
		DEFINE FIELD name ON user TYPE string;
		DEFINE FIELD email ON user TYPE string;
		DEFINE FIELD password ON user TYPE string;

		-- Define unique index on email
		REMOVE INDEX IF EXISTS email ON user;
		DEFINE INDEX email ON user FIELDS email UNIQUE;

		-- Define access method for record authentication
		REMOVE ACCESS IF EXISTS user ON DATABASE;
		DEFINE ACCESS user ON DATABASE TYPE RECORD
			SIGNIN (
				SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(password, $password)
			)
			SIGNUP (
				CREATE user CONTENT {
					name: $name,
					email: $email,
					password: crypto::argon2::generate($password)
				}
			);
	`

	if _, err := surrealdb.Query[any](context.Background(), db, setupQuery, nil); err != nil {
		panic(err)
	}

	fmt.Println("Database schema setup complete")

	type User struct {
		Namespace string `json:"NS"`
		Database  string `json:"DB"`
		Access    string `json:"AC"`
		Name      string `json:"name"`
		Password  string `json:"password"`
		Email     string `json:"email"`
	}

	type LoginRequest struct {
		Namespace string `json:"NS"`
		Database  string `json:"DB"`
		Access    string `json:"AC"`
		Email     string `json:"email"`
		Password  string `json:"password"`
	}

	_, err := db.SignUp(context.Background(), &User{
		// Corresponds to the SurrealDB namespace
		Namespace: ns,
		// Corresponds to the SurrealDB database
		Database: "record_user_custom",
		// Corresponds to `user` in `DEFINE ACCESS USER ON ...`
		Access: "user",
		// Corresponds to the $name in the SIGNUP query and `name` in `DEFINE FIELD name ON user`
		Name: "yusuke",
		// Corresponds to the $password in the SIGNUP query and `password` in `DEFINE FIELD password ON user`
		Password: "VerySecurePassword123!",
		// Corresponds to the $email in the SIGNUP query and `email` in `DEFINE FIELD email ON user`
		Email: "yusuke@example.com",
	})
	if err != nil {
		panic(err)
	}
	fmt.Println("User signed up successfully")

	_, err = db.SignIn(context.Background(), &LoginRequest{
		Namespace: ns,
		Database:  "record_user_custom",
		Access:    "user",
		// Corresponds to the $email in the SIGNIN query and `email` in `DEFINE FIELD email ON user`
		Email: "yusuke@example.com",
		// Corresponds to the $password in the SIGNIN query and `password` in `DEFINE FIELD password ON user`
		Password: "VerySecurePassword123!",
	})
	if err != nil {
		panic(err)
	}
	fmt.Println("User signed in successfully")

	info, err := db.Info(context.Background())
	if err != nil {
		panic(err)
	}
	fmt.Printf("Authenticated user name: %v\n", info["name"])

}
Output:
Database schema setup complete
User signed up successfully
User signed in successfully
Authenticated user name: yusuke
Example (Signin_failure)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	db, err := surrealdb.FromEndpointURLString(
		context.Background(),
		testenv.GetSurrealDBWSURL(),
	)
	if err != nil {
		panic(err)
	}

	// Attempt to sign in without setting namespace or database
	// This should fail with an error, whose message will depend on the connection type.
	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		Username: "root",
		Password: "invalid",
	})
	//nolint:goconst // Keeping error messages inline for readability in examples
	switch err.Error() {
	case "namespace or database or both are not set":
		// In case the connection is over HTTP, this error is expected
	case "There was a problem with the database: There was a problem with authentication":
		// In case the connection is over WebSocket on SurrealDB 2.x, this error is expected
	case "There was a problem with authentication":
		// In case the connection is over WebSocket on SurrealDB 3.x, this error is expected
	default:
		panic(fmt.Sprintf("Unexpected error: %v", err))
	}

	err = db.Use(context.Background(), "testNS", "testDB")
	if err != nil {
		fmt.Println("Use error:", err)
	}

	// Even though the ns/db is set, the SignIn should still fail
	// when the credentials are invalid.
	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		Username: "root",
		Password: "invalid",
	})
	// Normalize error message for version compatibility
	// SurrealDB 2.x: "There was a problem with the database: There was a problem with authentication"
	// SurrealDB 3.x: "There was a problem with authentication"
	errMsg := err.Error()
	//nolint:goconst // Keeping error messages inline for readability in examples
	switch errMsg {
	case "There was a problem with the database: There was a problem with authentication":
		fmt.Println("SignIn error: authentication failed")
	case "There was a problem with authentication":
		fmt.Println("SignIn error: authentication failed")
	default:
		fmt.Println("SignIn error:", err)
	}

	// Now let's try with the correct credentials
	// This should succeed if the database is set up correctly.
	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		Username: "root",
		Password: "root",
	})
	if err != nil {
		panic(fmt.Sprintf("SignIn failed: %v", err))
	}

	if err := db.Close(context.Background()); err != nil {
		panic(fmt.Sprintf("Failed to close the database connection: %v", err))
	}

}
Output:
SignIn error: authentication failed

func Connect deprecated added in v0.6.0

func Connect(ctx context.Context, connectionURL string) (*DB, error)

Deprecated: Use FromEndpointURLString instead.

func FromConnection added in v0.7.0

func FromConnection(ctx context.Context, conn connection.Connection) (*DB, error)

FromConnection creates a new SurrealDB client using the provided connection.

Note that this function calls `conn.Connect(ctx)` for you, so you don't need to call it manually.

Example (AlternativeCBORImpl_fxamackerCBOR)

FromConnection can take any connection.Connection implementation with a custom connection.Config that can be used to specify a CBOR marshaler and unmarshaler. This example demonstrates how to explicitly use the legacy fxamacker/cbor implementation instead of the default surrealcbor implementation.

conf := connection.NewConfig(testenv.MustParseSurrealDBWSURL())
// To explicitly use the legacy fxamacker/cbor implementation,
// override the default surrealcbor with fxamacker-based marshalers.
// Note: fxamacker/cbor is deprecated in favor of surrealcbor.
conf.Marshaler = &models.CborMarshaler{}     //nolint:staticcheck // Intentional use of deprecated type for example
conf.Unmarshaler = &models.CborUnmarshaler{} //nolint:staticcheck // Intentional use of deprecated type for example

conn := gws.New(conf)
db, err := surrealdb.FromConnection(context.Background(), conn)
if err != nil {
	panic(err)
}

db, err = testenv.Init(db, "surrealdbexamples", "fxamackercbor", "user")
if err != nil {
	panic(err)
}

// Define a sample struct
type User struct {
	ID    *models.RecordID `json:"id,omitempty"`
	Name  string           `json:"name"`
	Email string           `json:"email"`
	// Note that with fxamacker/cbor you need to use models.CustomDateTime
	// instead of time.Time for proper datetime handling
	CreatedAt models.CustomDateTime `json:"created_at"`
}

// Create a user
createdAt, _ := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
user := User{
	Name:      "Bob",
	Email:     "bob@example.com",
	CreatedAt: models.CustomDateTime{Time: createdAt},
}

// Insert the user
created, err := surrealdb.Insert[User](context.Background(), db, "user", user)
if err != nil {
	panic(err)
}

if created != nil && len(*created) > 0 {
	fmt.Printf("Created user: %s with email: %s\n", (*created)[0].Name, (*created)[0].Email)
}
Output:
Created user: Bob with email: bob@example.com
Example (AlternativeCBORImpl_surrealCBOR)

FromConnection can take any connection.Connection implementation with a custom connection.Config that can be used to specify a CBOR marshaler and unmarshaler. This SDK has two built-in CBOR implementations: fxamacker/cbor-based one and the newer surrealcbor. surrealcbor is a more efficient and feature-rich implementation that is recommended for new projects.

conf := connection.NewConfig(testenv.MustParseSurrealDBWSURL())
// To enable surrealcbor, instantiate the codec
// and set it as the marshaler and unmarshaler.
codec := surrealcbor.New()
conf.Marshaler = codec
conf.Unmarshaler = codec
conn := gws.New(conf)
db, err := surrealdb.FromConnection(context.Background(), conn)
if err != nil {
	panic(err)
}

db, err = testenv.Init(db, "surrealdbexamples", "surrealcbor", "user")
if err != nil {
	panic(err)
}

// Define a sample struct
type User struct {
	ID    *models.RecordID `json:"id,omitempty"`
	Name  string           `json:"name"`
	Email string           `json:"email"`
	// Note that this had to be `CreatedAt models.CustomDateTime`
	// with the previous fxamacker/cbor-based implementation.
	CreatedAt time.Time `json:"created_at"`
}

// Create a user
createdAt, _ := time.Parse(time.RFC3339, "2023-10-01T12:00:00Z")
user := User{
	Name:      "Alice",
	Email:     "alice@example.com",
	CreatedAt: createdAt,
}

// Insert the user
created, err := surrealdb.Insert[User](context.Background(), db, "user", user)
if err != nil {
	panic(err)
}

if created != nil && len(*created) > 0 {
	fmt.Printf("Created user: %s with email: %s\n", (*created)[0].Name, (*created)[0].Email)
}
Output:
Created user: Alice with email: alice@example.com
Example (AlternativeWebSocketLibrary_gws)

FromConnection can take any connection.Connection implementation, including gws.Connection which is based on https://github.com/lxzan/gws.

package main

import (
	"context"
	"fmt"
	"net/url"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/connection"
	"github.com/surrealdb/surrealdb.go/pkg/connection/gws"
)

func main() {
	u, err := url.ParseRequestURI(testenv.GetSurrealDBWSURL())
	if err != nil {
		panic(fmt.Sprintf("Failed to parse URL: %v", err))
	}

	conf := connection.NewConfig(u)
	conf.Logger = nil // Disable logging for this example

	conn := gws.New(conf)

	db, err := surrealdb.FromConnection(context.Background(), conn)
	fmt.Println("FromConnection error:", err)

	// normalizeAuthError normalizes authentication error messages for version compatibility
	// SurrealDB 2.x: "There was a problem with the database: There was a problem with authentication"
	// SurrealDB 3.x: "There was a problem with authentication"
	normalizeAuthError := func(err error) string {
		if err == nil {
			return "<nil>"
		}
		errMsg := err.Error()
		//nolint:goconst // Keeping error messages inline for readability in examples
		switch errMsg {
		case "There was a problem with the database: There was a problem with authentication":
			return "authentication failed"
		case "There was a problem with authentication":
			return "authentication failed"
		}
		return errMsg
	}

	// Attempt to sign in without setting namespace or database
	// This should fail with an error, whose message will depend on the connection type.
	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		Username: "root",
		Password: "invalid",
	})
	fmt.Println("SignIn error:", normalizeAuthError(err))

	err = db.Use(context.Background(), "testNS", "testDB")
	fmt.Println("Use error:", err)

	// Even though the ns/db is set, the SignIn should still fail
	// when the credentials are invalid.
	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		Username: "root",
		Password: "invalid",
	})
	fmt.Println("SignIn error:", normalizeAuthError(err))

	// Now let's try with the correct credentials
	// This should succeed if the database is set up correctly.
	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		Username: "root",
		Password: "root",
	})
	fmt.Println("SignIn error:", normalizeAuthError(err))

	err = db.Close(context.Background())
	fmt.Println("Close error:", err)

}
Output:
FromConnection error: <nil>
SignIn error: authentication failed
Use error: <nil>
SignIn error: authentication failed
SignIn error: <nil>
Close error: <nil>
Example (CborUnmarshaler_decOptions_customSmallLimit)

ExampleFromConnection_cborUnmarshaler_decOptions_customSmallLimit demonstrates what happens when a custom MaxArrayElements limit is set too low and the actual data exceeds that limit. The unmarshal operation fails with a clear error message.

// Parse the SurrealDB WebSocket URL
u, err := url.ParseRequestURI(testenv.GetSurrealDBWSURL())
if err != nil {
	panic(fmt.Sprintf("Failed to parse URL: %v", err))
}

// First, create the record using default connection settings
{
	conf := connection.NewConfig(u)
	conf.Logger = nil
	conn := gws.New(conf)

	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	db, err := surrealdb.FromConnection(ctx, conn)
	if err != nil {
		panic(fmt.Sprintf("Failed to connect: %v", err))
	}
	defer db.Close(context.Background())

	err = db.Use(ctx, "example", "test")
	if err != nil {
		panic(fmt.Sprintf("Failed to use namespace/database: %v", err))
	}

	_, err = db.SignIn(ctx, surrealdb.Auth{
		Username: "root",
		Password: "root",
	})
	if err != nil {
		panic(fmt.Sprintf("SignIn failed: %v", err))
	}

	// Setup table and ensure it's clean before test
	tableName := "test_small_limit"
	setupTable(db, tableName)

	createRecords(db, tableName, 20)
}

// Now try to retrieve with a connection that has a small array limit
{
	conf := connection.NewConfig(u)
	// Use TestLogHandler to see unmarshal errors but ignore debug and close errors
	handler := testenv.NewTestLogHandlerWithOptions(
		testenv.WithIgnoreErrorPrefixes("failed to close"),
		testenv.WithIgnoreDebug(),
	)
	conf.Logger = logger.New(handler)
	// Set a custom small limit that will be exceeded
	// Note: fxamacker/cbor requires MaxArrayElements to be at least 16
	conf.Unmarshaler = &models.CborUnmarshaler{ //nolint:staticcheck // Example demonstrating fxamacker/cbor DecOptions
		DecOptions: cbor.DecOptions{
			MaxArrayElements: 16, // Set to minimum allowed value
		},
	}
	conn := gws.New(conf)

	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	db, err := surrealdb.FromConnection(ctx, conn)
	if err != nil {
		panic(fmt.Sprintf("Failed to connect: %v", err))
	}
	defer db.Close(context.Background())

	err = db.Use(ctx, "example", "test")
	if err != nil {
		panic(fmt.Sprintf("Failed to use namespace/database: %v", err))
	}

	_, err = db.SignIn(ctx, surrealdb.Auth{
		Username: "root",
		Password: "root",
	})
	if err != nil {
		panic(fmt.Sprintf("SignIn failed: %v", err))
	}

	// This should fail due to array limit
	tableName := "test_small_limit"
	selectRecords(db, tableName)
}
Output:
Table test_small_limit cleaned up
Successfully created record with 20 items
[0] ERROR: Failed to unmarshal response error=cbor: exceeded max number of elements 16 for CBOR array
Error retrieving record: context deadline exceeded
Example (CborUnmarshaler_decOptions_defaultLimit)

ExampleFromConnection_cborUnmarshaler_decOptions_defaultLimit demonstrates that the default CBOR decoder configuration works fine with small arrays that are well within the default limit of 131,072 elements.

package main

import (
	"context"
	"fmt"
	"net/url"
	"time"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
	"github.com/surrealdb/surrealdb.go/pkg/connection"
	"github.com/surrealdb/surrealdb.go/pkg/connection/gorillaws"
)

func main() {
	// Parse the SurrealDB WebSocket URL
	u, err := url.ParseRequestURI(testenv.GetSurrealDBWSURL())
	if err != nil {
		panic(fmt.Sprintf("Failed to parse URL: %v", err))
	}

	// Setup connection with default configuration
	conf := connection.NewConfig(u)
	conf.Logger = nil
	conn := gorillaws.New(conf)

	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	db, err := surrealdb.FromConnection(ctx, conn)
	if err != nil {
		panic(fmt.Sprintf("Failed to connect: %v", err))
	}
	defer db.Close(context.Background())

	err = db.Use(ctx, "example", "test")
	if err != nil {
		panic(fmt.Sprintf("Failed to use namespace/database: %v", err))
	}

	_, err = db.SignIn(ctx, surrealdb.Auth{
		Username: "root",
		Password: "root",
	})
	if err != nil {
		panic(fmt.Sprintf("SignIn failed: %v", err))
	}

	// Setup table and ensure it's clean before test
	tableName := "test_default_limit"
	setupTable(db, tableName)

	// Default settings work with small arrays
	createRecords(db, tableName, 10)
	selectRecords(db, tableName)

}

// setupTable prepares a clean table for testing by deleting any existing records
func setupTable(db *surrealdb.DB, tableName string) {
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	_, _ = surrealdb.Query[any](ctx, db, fmt.Sprintf("DELETE %s", tableName), nil)
	fmt.Printf("Table %s cleaned up\n", tableName)
}

// createRecords creates a test record in the specified table
func createRecords(db *surrealdb.DB, tableName string, arraySize int) {
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	items := make([]string, arraySize)
	for i := range arraySize {
		items[i] = fmt.Sprintf("item_%d", i)
	}

	_, err := surrealdb.Query[any](ctx, db, fmt.Sprintf("CREATE %s SET items = $items", tableName), map[string]any{
		"items": items,
	})

	if err != nil {
		fmt.Printf("Error creating record: %v\n", err)
	} else {
		fmt.Printf("Successfully created record with %d items\n", arraySize)
	}
}

func selectRecords(db *surrealdb.DB, tableName string) {
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	type TestRecord struct {
		ID    any      `json:"id"`
		Items []string `json:"items"`
	}

	result, err := surrealdb.Query[[]TestRecord](ctx, db, fmt.Sprintf("SELECT * FROM %s", tableName), nil)

	if err != nil {

		fmt.Printf("Error retrieving record: %v\n", err)
		return
	}

	if result != nil && len(*result) > 0 && len((*result)[0].Result) > 0 {
		recordCount := len((*result)[0].Result)
		if recordCount > 0 && (*result)[0].Result[0].Items != nil {
			fmt.Printf("Successfully retrieved record with %d items\n", len((*result)[0].Result[0].Items))
		}
	}
}
Output:
Table test_default_limit cleaned up
Successfully created record with 10 items
Successfully retrieved record with 10 items

func FromEndpointURLString added in v0.7.0

func FromEndpointURLString(ctx context.Context, connectionURL string) (*DB, error)

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 New deprecated

func New(connectionURL string) (*DB, error)

New creates a new SurrealDB client.

Deprecated: New is deprecated. Use FromEndpointURLString instead.

func (*DB) Attach added in v1.3.0

func (db *DB) Attach(ctx context.Context) (*Session, error)

Attach creates a new session on the WebSocket connection. Sessions are only supported on WebSocket connections (SurrealDB v3+).

The new session starts unauthenticated and without a selected namespace/database. You must call SignIn/Authenticate and Use on the session before making queries.

Example:

session, err := db.Attach(ctx)
if err != nil {
    return err
}
defer session.Detach(ctx)

// Authenticate the session
_, err = session.SignIn(ctx, Auth{Username: "root", Password: "root"})
if err != nil {
    return err
}

// Select namespace and database
err = session.Use(ctx, "test", "test")
if err != nil {
    return err
}

// Now the session is ready for queries
results, err := surrealdb.Query[[]User](ctx, session, "SELECT * FROM users", nil)
Example

ExampleDB_Attach demonstrates creating and using an additional session. Sessions allow independent authentication, namespace selection, and variable scope. This feature requires SurrealDB v3+ and WebSocket connections.

package main

import (
	"context"
	"fmt"
	"log"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	// Skip if not v3+ (this is for documentation purposes)
	ctx := context.Background()

	// Connect using WebSocket (sessions require WebSocket)
	db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
	if err != nil {
		log.Fatal(err)
	}
	defer func() {
		if closeErr := db.Close(ctx); closeErr != nil {
			log.Printf("Failed to close db: %v", closeErr)
		}
	}()

	// Sign in as root on the main connection
	_, err = db.SignIn(ctx, map[string]any{"user": "root", "pass": "root"})
	if err != nil {
		log.Fatal(err) //nolint:gocritic // Example code - log.Fatal is acceptable
	}

	// Create an additional session
	session, err := db.Attach(ctx)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = session.Detach(ctx) }()

	fmt.Printf("Session created with ID: %s\n", session.ID())

	// The session starts unauthenticated - sign in and select namespace/database
	_, err = session.SignIn(ctx, map[string]any{"user": "root", "pass": "root"})
	if err != nil {
		log.Fatal(err)
	}

	err = session.Use(ctx, "test", "test")
	if err != nil {
		log.Fatal(err)
	}

	// Set a session-scoped variable
	err = session.Let(ctx, "user_id", "user123")
	if err != nil {
		log.Fatal(err)
	}

	// Query using the session - the variable is available
	type Result struct {
		UserID string `json:"user_id"`
	}
	results, err := surrealdb.Query[Result](ctx, session, "RETURN $user_id", nil)
	if err != nil {
		log.Fatal(err)
	}

	if len(*results) > 0 {
		fmt.Printf("Session variable $user_id: %s\n", (*results)[0].Result)
	}

	// Note: This example requires SurrealDB v3+ and will fail on earlier versions.
	// Output is not verified because session IDs are dynamic.
}

func (*DB) Authenticate

func (db *DB) Authenticate(ctx context.Context, token string) error

Authenticate authenticates the current connection with the provided token.

This is mostly useful when you created a JWT authentication method on SurrealDB using `DEFINE ACCESS ... TYPE JWT` query, so that SurrealDB can verify the token provided via this method for authentication.

After calling this method, all subsequent requests will be authenticated. How the authentication is maintained depends on the connection type:

  • For WebSocket connections, the token is kept in the session on the server side. This means connecting to the server again or creating a new connection to another server will require calling this method again to authenticate.

  • For HTTP connections, the token is sent with every request via the `Authorization` header. This means that even if you create a new connection to another server, as long as you call this method on the new connection, the requests will be authenticated.

Example (Jwt_databaseLevelUser)

nolint:gocyclo // Example covers end-to-end JWT setup; splitting would reduce readability for docs

ctx := context.Background()

// Generate ECDSA key pair using Go standard library
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
	panic(fmt.Sprintf("Failed to generate private key: %v", err))
}

// Extract public key and encode it to PEM format
publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
if err != nil {
	panic(fmt.Sprintf("Failed to marshal public key: %v", err))
}

publicKeyPEM := pem.EncodeToMemory(&pem.Block{
	Type:  "PUBLIC KEY",
	Bytes: publicKeyBytes,
})

// Connect to SurrealDB and authenticate as root
db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
	panic(err)
}

db, err = testenv.Init(db, "exampledb_authenticate_jwt", "testdb", "user")
if err != nil {
	panic(err)
}

// Sign in as root to set up the JWT access method
_, err = db.SignIn(ctx, surrealdb.Auth{
	Username: "root",
	Password: "root",
})
if err != nil {
	panic(fmt.Sprintf("SignIn as root failed: %v", err))
}

err = db.Use(ctx, "exampledb_authenticate_jwt", "testdb")
if err != nil {
	panic(fmt.Sprintf("Use failed: %v", err))
}

// Remove any existing access method first
_, err = surrealdb.Query[any](ctx, db, `REMOVE ACCESS IF EXISTS jwt_access ON DATABASE`, nil)
if err != nil {
	panic(fmt.Sprintf("Failed to remove existing JWT access: %v", err))
}

// Define a JWT access method with the public key
defineAccessQuery := fmt.Sprintf(`
		DEFINE ACCESS jwt_access ON DATABASE TYPE JWT
		ALGORITHM ES256 KEY '%s'
	`, string(publicKeyPEM))

_, err = surrealdb.Query[any](ctx, db, defineAccessQuery, nil)
if err != nil {
	panic(fmt.Sprintf("Failed to define JWT access: %v", err))
}

// Create the user table and a test user record for SurrealDB 3.x
// SurrealDB 3.x requires the table to exist before querying,
// while SurrealDB 2.x does not.
_, err = surrealdb.Query[any](ctx, db, `
		DEFINE TABLE user SCHEMAFULL;
		DEFINE FIELD name ON user TYPE string;
		CREATE user:test SET name = "test_user"
	`, nil)
if err != nil {
	panic(fmt.Sprintf("Failed to create test user: %v", err))
}

// Create a signed JWT token using the private key
// Only the required claims for database-level JWT access
// See: https://surrealdb.com/docs/surrealql/statements/define/access/jwt#using-tokens
claims := jwt.MapClaims{
	"exp": time.Now().Add(1 * time.Hour).Unix(), // Token expiration
	"ac":  "jwt_access",                         // Access method name
	"ns":  "exampledb_authenticate_jwt",         // Namespace
	"db":  "testdb",                             // Database
}

token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
signedToken, err := token.SignedString(privateKey)
if err != nil {
	panic(fmt.Sprintf("Failed to sign JWT token: %v", err))
}

// Close the root connection
if closeErr := db.Close(ctx); closeErr != nil {
	panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}

// Create a new connection and authenticate with the JWT token
db, err = surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
	panic(err)
}

// Authenticate using the JWT token via the Authenticate method
// The JWT contains ns and db claims, so we don't need to call Use() first
err = db.Authenticate(ctx, signedToken)
if err != nil {
	panic(fmt.Sprintf("Authenticate with JWT failed: %v", err))
}

// Verify authentication by performing a query
results, err := surrealdb.Query[any](ctx, db, `SELECT * FROM $id`, map[string]any{
	"id": models.NewRecordID("user", "test"),
})
if err != nil {
	panic(fmt.Sprintf("Query after JWT authentication failed: %v", err))
}

if results == nil || len(*results) == 0 {
	panic("Expected query results after JWT authentication")
}

if closeErr := db.Close(ctx); closeErr != nil {
	panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}

fmt.Println("JWT-based authentication completed successfully")
Output:
JWT-based authentication completed successfully
Example (Jwt_hs512_databaseLevelUser)
ctx := context.Background()

// Generate a symmetric key for HS512 (HMAC-SHA512)
// Use a strong random string as the symmetric key
symmetricKeyString := "sNSYneezcr8kqphfOC6NwwraUHJCVAt0XjsRSNmssBaBRh3WyMa9TRfq8ST7fsU2H2kGiOpU4GbAF1bCiXmM1b3JGgleBzz7rsrz6VvYEM4q3CLkcO8CMBIlhwhzWmy8" //nolint:goconst // duplicated across examples intentionally for self-contained docs

// Connect to SurrealDB and authenticate as root
db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
	panic(err)
}

db, err = testenv.Init(db, "exampledb_authenticate_jwt_hs512", "testdb", "user")
if err != nil {
	panic(err)
}

// Sign in as root to set up the JWT access method
_, err = db.SignIn(ctx, surrealdb.Auth{
	Username: "root",
	Password: "root",
})
if err != nil {
	panic(fmt.Sprintf("SignIn as root failed: %v", err))
}

err = db.Use(ctx, "exampledb_authenticate_jwt_hs512", "testdb")
if err != nil {
	panic(fmt.Sprintf("Use failed: %v", err))
}

// Remove any existing access method first
_, err = surrealdb.Query[any](ctx, db, `REMOVE ACCESS IF EXISTS jwt_hs512 ON DATABASE`, nil)
if err != nil {
	panic(fmt.Sprintf("Failed to remove existing JWT access: %v", err))
}

// Define a JWT access method with HS512 and the symmetric key
// See: https://surrealdb.com/docs/surrealql/statements/define/access/jwt#database
defineAccessQuery := fmt.Sprintf(`
		DEFINE ACCESS jwt_hs512 ON DATABASE TYPE JWT
		ALGORITHM HS512 KEY '%s'
	`, symmetricKeyString)

_, err = surrealdb.Query[any](ctx, db, defineAccessQuery, nil)
if err != nil {
	panic(fmt.Sprintf("Failed to define JWT access: %v", err))
}

// Create the user table and a test user record for SurrealDB 3.x
// SurrealDB 3.x requires the table to exist before querying,
// while SurrealDB 2.x does not.
_, err = surrealdb.Query[any](ctx, db, `
		DEFINE TABLE user SCHEMAFULL;
		DEFINE FIELD name ON user TYPE string;
		CREATE user:test SET name = "test_user"
	`, nil)
if err != nil {
	panic(fmt.Sprintf("Failed to create test user: %v", err))
}

// Create a signed JWT token using the symmetric key
// Only the required claims for database-level JWT access
claims := jwt.MapClaims{
	"exp": time.Now().Add(1 * time.Hour).Unix(), // Token expiration
	"ac":  "jwt_hs512",                          // Access method name
	"ns":  "exampledb_authenticate_jwt_hs512",   // Namespace
	"db":  "testdb",                             // Database
}

token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
signedToken, err := token.SignedString([]byte(symmetricKeyString))
if err != nil {
	panic(fmt.Sprintf("Failed to sign JWT token: %v", err))
}

// Close the root connection
if closeErr := db.Close(ctx); closeErr != nil {
	panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}

// Create a new connection and authenticate with the JWT token
db, err = surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
	panic(err)
}

// Authenticate using the JWT token via the Authenticate method
// The JWT contains ns and db claims, so we don't need to call Use() first
err = db.Authenticate(ctx, signedToken)
if err != nil {
	panic(fmt.Sprintf("Authenticate with JWT failed: %v", err))
}

// Verify authentication by performing a query
results, err := surrealdb.Query[any](ctx, db, `SELECT * FROM $id`, map[string]any{
	"id": models.NewRecordID("user", "test"),
})
if err != nil {
	panic(fmt.Sprintf("Query after JWT authentication failed: %v", err))
}

if results == nil || len(*results) == 0 {
	panic("Expected query results after JWT authentication")
}

if closeErr := db.Close(ctx); closeErr != nil {
	panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}

fmt.Println("JWT HS512 authentication completed successfully")
Output:
JWT HS512 authentication completed successfully
Example (Jwt_hs512_namespaceLevelUser)

nolint:gocyclo // Example shows full flow for namespace-level JWT auth

ctx := context.Background()

// Symmetric key for HS512 (HMAC-SHA512)
symmetricKeyString := "sNSYneezcr8kqphfOC6NwwraUHJCVAt0XjsRSNmssBaBRh3WyMa9TRfq8ST7fsU2H2kGiOpU4GbAF1bCiXmM1b3JGgleBzz7rsrz6VvYEM4q3CLkcO8CMBIlhwhzWmy8" //nolint:goconst // duplicated across examples intentionally for self-contained docs

// Names for this test
ns := "exampledb_authenticate_jwt_hs512_ns"
accessName := "jwt_hs512_ns"

// Connect to SurrealDB and authenticate as root
db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
	panic(err)
}

db, err = testenv.Init(db, ns, "testdb")
if err != nil {
	panic(err)
}

// Sign in as root to set up the JWT access method
_, err = db.SignIn(ctx, surrealdb.Auth{
	Username: "root",
	Password: "root",
})
if err != nil {
	panic(fmt.Sprintf("SignIn as root failed: %v", err))
}

// Select namespace (database may be required by helper; safe to set anyway)
err = db.Use(ctx, ns, "testdb")
if err != nil {
	panic(fmt.Sprintf("Use failed: %v", err))
}

// Remove any existing access method first (namespace level)
_, err = surrealdb.Query[any](ctx, db, `REMOVE ACCESS IF EXISTS jwt_hs512_ns ON NAMESPACE`, nil)
if err != nil {
	panic(fmt.Sprintf("Failed to remove existing JWT access: %v", err))
}

// Define a JWT access method for namespace level with HS512
defineAccessQuery := fmt.Sprintf(`
		DEFINE ACCESS %s ON NAMESPACE TYPE JWT
		ALGORITHM HS512 KEY '%s'
	`, accessName, symmetricKeyString)

_, err = surrealdb.Query[any](ctx, db, defineAccessQuery, nil)
if err != nil {
	panic(fmt.Sprintf("Failed to define JWT access: %v", err))
}

// Create a database and user table/record for the namespace-level auth test
// SurrealDB 3.x requires the table to exist before querying,
// while SurrealDB 2.x does not.
// First remove the database if it exists to ensure clean state
_, _ = surrealdb.Query[any](ctx, db, `REMOVE DATABASE IF EXISTS testdb`, nil)
_, err = surrealdb.Query[any](ctx, db, `
		DEFINE DATABASE testdb;
		USE DB testdb;
		DEFINE TABLE user SCHEMAFULL;
		DEFINE FIELD name ON user TYPE string;
		CREATE user:test SET name = "test_user"
	`, nil)
if err != nil {
	panic(fmt.Sprintf("Failed to create test user: %v", err))
}

// Create a signed JWT token using the symmetric key (namespace-level claims)
claims := jwt.MapClaims{
	"exp": time.Now().Add(1 * time.Hour).Unix(),
	"ac":  accessName,
	"ns":  ns,
}

token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
signedToken, err := token.SignedString([]byte(symmetricKeyString))
if err != nil {
	panic(fmt.Sprintf("Failed to sign JWT token: %v", err))
}

// Close the root connection
if closeErr := db.Close(ctx); closeErr != nil {
	panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}

// Create a new connection and authenticate with the JWT token
db, err = surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
	panic(err)
}

// Authenticate using the JWT token via the Authenticate method
err = db.Authenticate(ctx, signedToken)
if err != nil {
	panic(fmt.Sprintf("Authenticate with JWT failed: %v", err))
}

// For namespace-level tokens, select a database to run queries
err = db.Use(ctx, ns, "testdb")
if err != nil {
	panic(fmt.Sprintf("Use failed after JWT auth: %v", err))
}

// Verify authentication by performing a query
results, err := surrealdb.Query[any](ctx, db, `SELECT * FROM $id`, map[string]any{
	"id": models.NewRecordID("user", "test"),
})
if err != nil {
	panic(fmt.Sprintf("Query after JWT authentication failed: %v", err))
}
if results == nil || len(*results) == 0 {
	panic("Expected query results after JWT authentication")
}

if closeErr := db.Close(ctx); closeErr != nil {
	panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}

fmt.Println("JWT HS512 namespace-level authentication completed successfully")
Output:
JWT HS512 namespace-level authentication completed successfully
Example (Jwt_hs512_rootLevelUser)

nolint:gocyclo // Example shows full flow for root-level JWT auth

ctx := context.Background()

symmetricKeyString := "sNSYneezcr8kqphfOC6NwwraUHJCVAt0XjsRSNmssBaBRh3WyMa9TRfq8ST7fsU2H2kGiOpU4GbAF1bCiXmM1b3JGgleBzz7rsrz6VvYEM4q3CLkcO8CMBIlhwhzWmy8" //nolint:goconst // duplicated across examples intentionally for self-contained docs
accessName := "jwt_hs512_root"
ns := "exampledb_authenticate_jwt_hs512_root"

// Admin connection
db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
	panic(err)
}
db, err = testenv.Init(db, ns, "testdb")
if err != nil {
	panic(err)
}
if _, err = db.SignIn(ctx, surrealdb.Auth{Username: "root", Password: "root"}); err != nil {
	panic(fmt.Sprintf("SignIn as root failed: %v", err))
}
err = db.Use(ctx, ns, "testdb")
if err != nil {
	panic(fmt.Sprintf("Use failed: %v", err))
}
_, err = surrealdb.Query[any](ctx, db, `REMOVE ACCESS IF EXISTS jwt_hs512_root ON ROOT`, nil)
if err != nil {
	panic(fmt.Sprintf("Failed to remove existing JWT access: %v", err))
}

defineAccessQuery := fmt.Sprintf(`
		DEFINE ACCESS %s ON ROOT TYPE JWT
		ALGORITHM HS512 KEY '%s'
	`, accessName, symmetricKeyString)
_, err = surrealdb.Query[any](ctx, db, defineAccessQuery, nil)
if err != nil {
	panic(fmt.Sprintf("Failed to define JWT access: %v", err))
}

// Create the namespace, database, table, and test user for root-level auth test
// SurrealDB 3.x requires the table to exist before querying,
// while SurrealDB 2.x does not.
// First remove namespace if it exists to ensure clean state
_, _ = surrealdb.Query[any](ctx, db, fmt.Sprintf(`REMOVE NAMESPACE IF EXISTS %s`, ns), nil)
_, err = surrealdb.Query[any](ctx, db, fmt.Sprintf(`
		DEFINE NAMESPACE %s;
		USE NS %s;
		DEFINE DATABASE testdb;
		USE DB testdb;
		DEFINE TABLE user SCHEMAFULL;
		DEFINE FIELD name ON user TYPE string;
		CREATE user:test SET name = "test_user"
	`, ns, ns), nil)
if err != nil {
	panic(fmt.Sprintf("Failed to create test user: %v", err))
}

// Root-level token claims (no ns/db)
claims := jwt.MapClaims{
	"exp": time.Now().Add(1 * time.Hour).Unix(),
	"ac":  accessName,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
signedToken, err := token.SignedString([]byte(symmetricKeyString))
if err != nil {
	panic(fmt.Sprintf("Failed to sign JWT token: %v", err))
}

if closeErr := db.Close(ctx); closeErr != nil {
	panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}

// Authenticate with root-level token
db, err = surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
	panic(err)
}
err = db.Authenticate(ctx, signedToken)
if err != nil {
	panic(fmt.Sprintf("Authenticate with JWT failed: %v", err))
}
// Choose ns/db for subsequent queries
err = db.Use(ctx, ns, "testdb")
if err != nil {
	panic(fmt.Sprintf("Use failed after JWT auth: %v", err))
}
results, err := surrealdb.Query[any](ctx, db, `SELECT * FROM $id`, map[string]any{
	"id": models.NewRecordID("user", "test"),
})
if err != nil {
	panic(fmt.Sprintf("Query after JWT authentication failed: %v", err))
}
if results == nil || len(*results) == 0 {
	panic("Expected query results after JWT authentication")
}
if closeErr := db.Close(ctx); closeErr != nil {
	panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}

fmt.Println("JWT HS512 root-level authentication completed successfully")
Output:
JWT HS512 root-level authentication completed successfully
Example (Jwt_hs512_rootLevelUser_expired)
ctx := context.Background()

symmetricKeyString := "sNSYneezcr8kqphfOC6NwwraUHJCVAt0XjsRSNmssBaBRh3WyMa9TRfq8ST7fsU2H2kGiOpU4GbAF1bCiXmM1b3JGgleBzz7rsrz6VvYEM4q3CLkcO8CMBIlhwhzWmy8" //nolint:goconst // duplicated across examples intentionally for self-contained docs
accessName := "jwt_hs512_root_expired"

// Admin connection
db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
	panic(err)
}
// Use a dedicated namespace for this test
ns := "exampledb_authenticate_jwt_hs512_root_expired"
db, err = testenv.Init(db, ns, "testdb")
if err != nil {
	panic(err)
}
if _, err = db.SignIn(ctx, surrealdb.Auth{Username: "root", Password: "root"}); err != nil {
	panic(fmt.Sprintf("SignIn as root failed: %v", err))
}
err = db.Use(ctx, ns, "testdb")
if err != nil {
	panic(fmt.Sprintf("Use failed: %v", err))
}
_, err = surrealdb.Query[any](ctx, db, `REMOVE ACCESS IF EXISTS jwt_hs512_root_expired ON ROOT`, nil)
if err != nil {
	panic(fmt.Sprintf("Failed to remove existing JWT access: %v", err))
}
defineAccessQuery := fmt.Sprintf(`
		DEFINE ACCESS %s ON ROOT TYPE JWT
		ALGORITHM HS512 KEY '%s'
	`, accessName, symmetricKeyString)
_, err = surrealdb.Query[any](ctx, db, defineAccessQuery, nil)
if err != nil {
	panic(fmt.Sprintf("Failed to define JWT access: %v", err))
}

// Expired token (exp in the past)
claims := jwt.MapClaims{
	"exp": time.Now().Add(-1 * time.Hour).Unix(),
	"ac":  accessName,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
signedToken, err := token.SignedString([]byte(symmetricKeyString))
if err != nil {
	panic(fmt.Sprintf("Failed to sign JWT token: %v", err))
}

if closeErr := db.Close(ctx); closeErr != nil {
	panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
}

// Try authenticating with expired token - should fail
db, err = surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
if err != nil {
	panic(err)
}
err = db.Authenticate(ctx, signedToken)
if err != nil {
	// Expected failure path
	fmt.Println("JWT HS512 root-level expired authentication failed as expected")
	return
}

// If we reached here, authentication incorrectly succeeded
panic("Expected Authenticate to fail with expired token, but it succeeded")
Output:
JWT HS512 root-level expired authentication failed as expected

func (*DB) Begin added in v1.3.0

func (db *DB) Begin(ctx context.Context) (*Transaction, error)

Begin starts a new interactive transaction on the default session. Interactive transactions are only supported on WebSocket connections (SurrealDB v3+).

Example:

tx, err := db.Begin(ctx)
if err != nil {
    return err
}
defer tx.Cancel(ctx) // Cancel if not committed

// Execute queries within the transaction
_, err = surrealdb.Query[[]any](ctx, tx, "CREATE user:1 SET name = 'Alice'", nil)
if err != nil {
    return err
}

// Commit the transaction
return tx.Commit(ctx)
Example

ExampleDB_Begin demonstrates starting an interactive transaction. Interactive transactions allow executing statements one at a time and conditionally committing or canceling based on results. This feature requires SurrealDB v3+ and WebSocket connections.

package main

import (
	"context"
	"fmt"
	"log"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	ctx := context.Background()

	// Connect using WebSocket (transactions require WebSocket)
	db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
	if err != nil {
		log.Fatal(err)
	}
	defer func() {
		if closeErr := db.Close(ctx); closeErr != nil {
			log.Printf("Failed to close db: %v", closeErr)
		}
	}()

	// Sign in and select namespace/database
	_, err = db.SignIn(ctx, map[string]any{"user": "root", "pass": "root"})
	if err != nil {
		log.Fatal(err) //nolint:gocritic // Example code - log.Fatal is acceptable
	}
	err = db.Use(ctx, "test", "test")
	if err != nil {
		log.Fatal(err) //nolint:gocritic // Example code - log.Fatal is acceptable
	}

	// Start an interactive transaction
	tx, err := db.Begin(ctx)
	if err != nil {
		log.Fatal(err)
	}
	// Always clean up if not committed
	defer func() {
		if !tx.IsClosed() {
			_ = tx.Cancel(ctx)
		}
	}()

	fmt.Printf("Transaction started with ID: %s\n", tx.ID())

	// Perform operations within the transaction
	type Product struct {
		ID    string `json:"id"`
		Name  string `json:"name"`
		Stock int    `json:"stock"`
	}

	// Create a product
	_, err = surrealdb.Query[[]Product](ctx, tx,
		"CREATE products:widget SET name = 'Widget', stock = 100", nil)
	if err != nil {
		log.Fatal(err)
	}

	// Query within the same transaction - changes are visible
	results, err := surrealdb.Query[[]Product](ctx, tx,
		"SELECT * FROM products:widget", nil)
	if err != nil {
		log.Fatal(err)
	}

	if len(*results) > 0 && len((*results)[0].Result) > 0 {
		fmt.Printf("Product in transaction: %s (stock: %d)\n",
			(*results)[0].Result[0].Name,
			(*results)[0].Result[0].Stock)
	}

	// Commit the transaction to persist changes
	err = tx.Commit(ctx)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Transaction committed")

	// Note: This example requires SurrealDB v3+ and will fail on earlier versions.
	// Output is not verified because transaction IDs are dynamic.
}

func (*DB) Close

func (db *DB) Close(ctx context.Context) error

Close closes the underlying WebSocket connection.

func (*DB) CloseLiveNotifications added in v0.10.0

func (db *DB) CloseLiveNotifications(liveQueryID string) error

func (*DB) Info

func (db *DB) Info(ctx context.Context) (map[string]any, error)

func (*DB) Invalidate

func (db *DB) Invalidate(ctx context.Context) error

func (*DB) Let

func (db *DB) Let(ctx context.Context, key string, val any) error

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

func (db *DB) SignIn(ctx context.Context, authData any) (string, error)

SignIn signs in an existing user.

The authData parameter can be either:

  • An Auth struct
  • A map[string]any with keys like: "namespace", "database", "scope", "user", "pass"

In either case, the username and the password are mandatory. Depending on whether namespace and database are provided or not, the user is signed in as a database-level user, a namespace-level user, or a root-level user.

Moreover, the Access field in the Auth struct or the "AC" key in the map[string]any is optional, and is only needed when signing in as a record user, which is like a database user that requires the namespace and database to be specified too.

The following examples illustrate the different cases.

The most complex case is signing in as a record user, which requires specifying the Access field to indicate which access method to use for authentication.

db.SignIn(Auth{
  Access:    "user",
  Namespace: "app",
  Database:  "app",
  Username:  "yusuke",
  Password:  "VerySecurePassword123!",
})

If namespace and database are provided, the user is signed in as a database-level user.

db.SignIn(Auth{
  Namespace: "app",
  Database: "app",
  Username: "yusuke",
  Password: "VerySecurePassword123!",
})

db.SignIn(map[string]any{
  "NS": "app",
  "DB": "app",
  "user": "yusuke",
  "pass": "VerySecurePassword123!",
})

If namespace is provided but database is omitted, the user is signed in as a namespace-level user.

db.SignIn(Auth{
  Namespace: "app",
  Username: "yusuke",
  Password: "VerySecurePassword123!",
})

db.SignIn(map[string]any{
  "NS": "app",
  "user": "yusuke",
  "pass": "VerySecurePassword123!",
})

If both namespace and database are omitted, the user is signed in as a root-level user.

db.SignIn(Auth{
  Username: "yusuke",
  Password: "VerySecurePassword123!",
})

db.SignIn(map[string]any{
  "user": "yusuke",
  "pass": "VerySecurePassword123!",
})

Bearer Access Method

For TYPE BEARER access methods (SurrealDB v3+), use the "key" parameter with a bearer key obtained from ACCESS ... GRANT. Bearer keys have the format "surreal-bearer-...". No username/password is needed:

db.SignIn(map[string]any{
  "NS":  "app",
  "DB":  "app",
  "AC":  "bearer_api",
  "key": bearerKey,  // from ACCESS bearer_api GRANT FOR USER/RECORD ...
})

Note: The "key" parameter is exclusively for bearer access grants. For TYPE RECORD access methods with WITH REFRESH, use SignInWithRefresh instead.

Example (DatabaseLevelUser)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	db, err := surrealdb.FromEndpointURLString(
		context.Background(),
		testenv.GetSurrealDBWSURL(),
	)
	if err != nil {
		panic(err)
	}

	db, err = testenv.Init(db, "exampledb_signin_databaselevel", "testdb", "testtable")
	if err != nil {
		panic(err)
	}

	// Login at the root level to set up the namespace-level user
	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		Username: "root",
		Password: "root",
	})
	if err != nil {
		panic(fmt.Sprintf("SignIn failed: %v", err))
	}

	err = db.Use(context.Background(), "exampledb_signin_databaselevel", "testdb")
	if err != nil {
		panic(fmt.Sprintf("Use failed: %v", err))
	}

	// Clean up any existing database-level user
	_, err = surrealdb.Query[any](context.Background(), db, `REMOVE USER IF EXISTS myuser ON DATABASE`, nil)
	if err != nil {
		panic(fmt.Sprintf("Failed to remove existing database-level user: %v", err))
	}

	// Create a database-level user
	_, err = surrealdb.Query[any](context.Background(), db, `DEFINE USER myuser ON DATABASE PASSWORD 'mypassword' ROLES OWNER`, nil)
	if err != nil {
		panic(fmt.Sprintf("Failed to create database-level user: %v", err))
	}

	err = db.Close(context.Background())
	if err != nil {
		panic(fmt.Sprintf("Failed to close the database connection: %v", err))
	}

	// Reconnect to ensure a fresh session
	db, err = surrealdb.FromEndpointURLString(
		context.Background(),
		testenv.GetSurrealDBWSURL(),
	)
	if err != nil {
		panic(err)
	}

	// Now sign in as the database-level user
	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		Namespace: "exampledb_signin_databaselevel",
		Database:  "testdb",
		Username:  "myuser",
		Password:  "mypassword",
	})
	if err != nil {
		panic(fmt.Sprintf("SignIn failed: %v", err))
	}

	err = db.Use(context.Background(), "exampledb_signin_databaselevel", "testdb")
	if err != nil {
		panic(fmt.Sprintf("Use failed: %v", err))
	}

	// Create table and query - SurrealDB 3.x requires table to exist before SELECT
	_, err = surrealdb.Query[any](context.Background(), db, `DEFINE TABLE testtable; SELECT * FROM testtable`, nil)
	if err != nil {
		panic(fmt.Sprintf("Query failed: %v", err))
	}

	if closeErr := db.Close(context.Background()); closeErr != nil {
		panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
	}

	fmt.Println("Database-level user SignIn tests completed successfully")

}
Output:
Database-level user SignIn tests completed successfully
Example (DatabaseLevelUser_failureDueToMissingNamespace)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	db, err := surrealdb.FromEndpointURLString(
		context.Background(),
		testenv.GetSurrealDBWSURL(),
	)
	if err != nil {
		panic(err)
	}

	db, err = testenv.Init(db, "exampledb_signin_databaselevel_failure", "testdb", "testtable")
	if err != nil {
		panic(err)
	}

	// Login at the root level to set up the database-level user
	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		Username: "root",
		Password: "root",
	})
	if err != nil {
		panic(fmt.Sprintf("SignIn failed: %v", err))
	}

	err = db.Use(context.Background(), "exampledb_signin_databaselevel_failure", "testdb")
	if err != nil {
		panic(fmt.Sprintf("Use failed: %v", err))
	}

	// Clean up any existing database-level user
	_, err = surrealdb.Query[any](context.Background(), db, `REMOVE USER IF EXISTS myuser ON DATABASE`, nil)
	if err != nil {
		panic(fmt.Sprintf("Failed to remove existing database-level user: %v", err))
	}

	// Create a database-level user
	_, err = surrealdb.Query[any](context.Background(), db, `DEFINE USER myuser ON DATABASE PASSWORD 'mypassword' ROLES OWNER`, nil)
	if err != nil {
		panic(fmt.Sprintf("Failed to create database-level user: %v", err))
	}

	err = db.Close(context.Background())
	if err != nil {
		panic(fmt.Sprintf("Failed to close the database connection: %v", err))
	}

	// Reconnect to ensure a fresh session
	db, err = surrealdb.FromEndpointURLString(
		context.Background(),
		testenv.GetSurrealDBWSURL(),
	)
	if err != nil {
		panic(err)
	}

	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		// Note the omission of the Database field here.
		// This is invalid for database-level users, because
		// the database is present in a namespace.
		// Namespace: "",
		Database: "testdb",
		Username: "myuser",
		Password: "mypassword",
	})
	if err == nil {
		panic("Expected SignIn to fail, but it succeeded")
	}

	if err := db.Close(context.Background()); err != nil {
		panic(fmt.Sprintf("Failed to close the database connection: %v", err))
	}

	fmt.Println("Database-level user SignIn tests completed successfully")

}
Output:
Database-level user SignIn tests completed successfully
Example (NamespaceLevelUser)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	db, err := surrealdb.FromEndpointURLString(
		context.Background(),
		testenv.GetSurrealDBWSURL(),
	)
	if err != nil {
		panic(err)
	}

	db, err = testenv.Init(db, "exampledb_signin_namespacelevel", "testdb", "testtable")
	if err != nil {
		panic(err)
	}

	// Login at the root level to set up the namespace-level user
	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		Username: "root",
		Password: "root",
	})
	if err != nil {
		panic(fmt.Sprintf("SignIn failed: %v", err))
	}

	err = db.Use(context.Background(), "exampledb_signin_namespacelevel", "")
	if err != nil {
		panic(fmt.Sprintf("Use failed: %v", err))
	}

	// Clean up any existing namespace-level user
	_, err = surrealdb.Query[any](context.Background(), db, `REMOVE USER IF EXISTS myuser ON NAMESPACE`, nil)
	if err != nil {
		panic(fmt.Sprintf("Failed to remove existing namespace-level user: %v", err))
	}

	// Create a namespace-level user
	_, err = surrealdb.Query[any](context.Background(), db, `DEFINE USER myuser ON NAMESPACE PASSWORD 'mypassword' ROLES OWNER`, nil)
	if err != nil {
		panic(fmt.Sprintf("Failed to create namespace-level user: %v", err))
	}

	err = db.Close(context.Background())
	if err != nil {
		panic(fmt.Sprintf("Failed to close the database connection: %v", err))
	}

	// Reconnect to ensure a fresh session
	db, err = surrealdb.FromEndpointURLString(
		context.Background(),
		testenv.GetSurrealDBWSURL(),
	)
	if err != nil {
		panic(err)
	}

	// Now sign in as the namespace-level user
	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		Namespace: "exampledb_signin_namespacelevel",
		Username:  "myuser",
		Password:  "mypassword",
	})
	if err != nil {
		panic(fmt.Sprintf("SignIn failed: %v", err))
	}

	err = db.Use(context.Background(), "exampledb_signin_namespacelevel", "testdb")
	if err != nil {
		panic(fmt.Sprintf("Use failed: %v", err))
	}

	// Create table and query - SurrealDB 3.x requires table to exist before SELECT
	_, err = surrealdb.Query[any](context.Background(), db, `DEFINE TABLE testtable; SELECT * FROM testtable`, nil)
	if err != nil {
		panic(fmt.Sprintf("Query failed: %v", err))
	}

	if closeErr := db.Close(context.Background()); closeErr != nil {
		panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
	}

	fmt.Println("Namespace-level user SignIn tests completed successfully")

}
Output:
Namespace-level user SignIn tests completed successfully
Example (NamespaceLevelUser_failureDueToExtraDatabase)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	db, err := surrealdb.FromEndpointURLString(
		context.Background(),
		testenv.GetSurrealDBWSURL(),
	)
	if err != nil {
		panic(err)
	}

	db, err = testenv.Init(db, "exampledb_signin_namespacelevel", "testdb", "testtable")
	if err != nil {
		panic(err)
	}

	// Login at the root level to set up the namespace-level user
	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		Username: "root",
		Password: "root",
	})
	if err != nil {
		panic(fmt.Sprintf("SignIn failed: %v", err))
	}

	err = db.Use(context.Background(), "exampledb_signin_namespacelevel", "")
	if err != nil {
		panic(fmt.Sprintf("Use failed: %v", err))
	}

	// Clean up any existing namespace-level user
	_, err = surrealdb.Query[any](context.Background(), db, `REMOVE USER IF EXISTS myuser ON NAMESPACE`, nil)
	if err != nil {
		panic(fmt.Sprintf("Failed to remove existing namespace-level user: %v", err))
	}

	// Create a namespace-level user
	_, err = surrealdb.Query[any](context.Background(), db, `DEFINE USER myuser ON NAMESPACE PASSWORD 'mypassword' ROLES OWNER`, nil)
	if err != nil {
		panic(fmt.Sprintf("Failed to create namespace-level user: %v", err))
	}

	err = db.Close(context.Background())
	if err != nil {
		panic(fmt.Sprintf("Failed to close the database connection: %v", err))
	}

	// Reconnect to ensure a fresh session
	db, err = surrealdb.FromEndpointURLString(
		context.Background(),
		testenv.GetSurrealDBWSURL(),
	)
	if err != nil {
		panic(err)
	}

	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		Namespace: "exampledb_signin_namespacelevel",
		// Note the extra Database field here.
		// This is invalid for namespace-level users, because
		// the existence of a database signals SurrealDB to authenticate you as a database-level user,
		// which we didn't create for this test.
		Database: "testdb",
		Username: "myuser",
		Password: "mypassword",
	})
	if err == nil {
		panic("Expected SignIn to fail, but it succeeded")
	}

	if err := db.Close(context.Background()); err != nil {
		panic(fmt.Sprintf("Failed to close the database connection: %v", err))
	}

	fmt.Println("Namespace-level user SignIn tests completed successfully")

}
Output:
Namespace-level user SignIn tests completed successfully
Example (RootLevelUser)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	db, err := surrealdb.FromEndpointURLString(
		context.Background(),
		testenv.GetSurrealDBWSURL(),
	)
	if err != nil {
		panic(err)
	}

	db, err = testenv.Init(db, "exampledb_signin_rootlevel", "testdb", "testtable")
	if err != nil {
		panic(err)
	}

	// Sign in as the root user
	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		Username: "root",
		Password: "root",
	})
	if err != nil {
		panic(fmt.Sprintf("SignIn failed: %v", err))
	}

	err = db.Use(context.Background(), "exampledb_signin_rootlevel", "testdb")
	if err != nil {
		panic(fmt.Sprintf("Use failed: %v", err))
	}

	_, err = surrealdb.Query[any](context.Background(), db, `REMOVE USER IF EXISTS myuser ON ROOT`, nil)
	if err != nil {
		panic(fmt.Sprintf("Query failed: %v", err))
	}

	_, err = surrealdb.Query[any](context.Background(), db, `DEFINE USER myuser ON ROOT PASSWORD 'mypassword' ROLES OWNER`, nil)
	if err != nil {
		panic(fmt.Sprintf("Query failed: %v", err))
	}

	if closeErr := db.Close(context.Background()); closeErr != nil {
		panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
	}

	db, err = surrealdb.FromEndpointURLString(
		context.Background(),
		testenv.GetSurrealDBWSURL(),
	)
	if err != nil {
		panic(err)
	}

	// Now sign in to the SurrealDB instance as the new root level user.
	// Omitting namespace and database indicates that we want to authenticate
	// as a root-level user.
	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		Username: "myuser",
		Password: "mypassword",
	})
	if err != nil {
		panic(fmt.Sprintf("SignIn failed: %v", err))
	}

	err = db.Use(context.Background(), "exampledb_signin_rootlevel", "testdb")
	if err != nil {
		panic(fmt.Sprintf("Use failed: %v", err))
	}

	// Create table and query - SurrealDB 3.x requires table to exist before SELECT
	_, err = surrealdb.Query[any](context.Background(), db, `DEFINE TABLE testtable; SELECT * FROM testtable`, nil)
	if err != nil {
		panic(fmt.Sprintf("Query failed: %v", err))
	}

	if closeErr := db.Close(context.Background()); closeErr != nil {
		panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
	}

	fmt.Println("Root-level user SignIn tests completed successfully")

}
Output:
Root-level user SignIn tests completed successfully
Example (RootLevelUser_invalidAuthLevelDatabase)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	db, err := surrealdb.FromEndpointURLString(
		context.Background(),
		testenv.GetSurrealDBWSURL(),
	)
	if err != nil {
		panic(err)
	}

	db, err = testenv.Init(db, "exampledb_signin_rootlevel", "testdb", "testtable")
	if err != nil {
		panic(err)
	}

	// Sign in as the root user
	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		Username: "root",
		Password: "root",
	})
	if err != nil {
		panic(fmt.Sprintf("SignIn failed: %v", err))
	}

	err = db.Use(context.Background(), "exampledb_signin_rootlevel", "testdb")
	if err != nil {
		panic(fmt.Sprintf("Use failed: %v", err))
	}

	_, err = surrealdb.Query[any](context.Background(), db, `REMOVE USER IF EXISTS myuser ON ROOT`, nil)
	if err != nil {
		panic(fmt.Sprintf("Query failed: %v", err))
	}

	_, err = surrealdb.Query[any](context.Background(), db, `DEFINE USER myuser ON ROOT PASSWORD 'mypassword' ROLES OWNER`, nil)
	if err != nil {
		panic(fmt.Sprintf("Query failed: %v", err))
	}

	if closeErr := db.Close(context.Background()); closeErr != nil {
		panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
	}

	db, err = surrealdb.FromEndpointURLString(
		context.Background(),
		testenv.GetSurrealDBWSURL(),
	)
	if err != nil {
		panic(err)
	}

	// Try to sign in to the namespace/database using the new root level user
	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		Namespace: "exampledb_signin_rootlevel",
		Database:  "testdb",
		Username:  "myuser",
		Password:  "mypassword",
	})
	// This should fail, because specifying both namespace and database
	// indicates that we want to authenticate as a database-level user,
	// which this user is not.
	if err == nil {
		panic("Expected SignIn to fail, but it succeeded")
	}

	fmt.Println("Root-level user SignIn tests completed successfully")

}
Output:
Root-level user SignIn tests completed successfully
Example (RootLevelUser_invalidAuthLevelNamespace)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	db, err := surrealdb.FromEndpointURLString(
		context.Background(),
		testenv.GetSurrealDBWSURL(),
	)
	if err != nil {
		panic(err)
	}

	db, err = testenv.Init(db, "exampledb_signin_rootlevel", "testdb", "testtable")
	if err != nil {
		panic(err)
	}

	// Sign in as the root user
	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		Username: "root",
		Password: "root",
	})
	if err != nil {
		panic(fmt.Sprintf("SignIn failed: %v", err))
	}

	err = db.Use(context.Background(), "exampledb_signin_rootlevel", "testdb")
	if err != nil {
		panic(fmt.Sprintf("Use failed: %v", err))
	}

	_, err = surrealdb.Query[any](context.Background(), db, `REMOVE USER IF EXISTS myuser ON ROOT`, nil)
	if err != nil {
		panic(fmt.Sprintf("Query failed: %v", err))
	}

	_, err = surrealdb.Query[any](context.Background(), db, `DEFINE USER myuser ON ROOT PASSWORD 'mypassword' ROLES OWNER`, nil)
	if err != nil {
		panic(fmt.Sprintf("Query failed: %v", err))
	}

	if closeErr := db.Close(context.Background()); closeErr != nil {
		panic(fmt.Sprintf("Failed to close the database connection: %v", closeErr))
	}

	db, err = surrealdb.FromEndpointURLString(
		context.Background(),
		testenv.GetSurrealDBWSURL(),
	)
	if err != nil {
		panic(err)
	}

	// Try to sign in to the namespace as the new root level user
	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		Namespace: "exampledb_signin_rootlevel",
		Username:  "myuser",
		Password:  "mypassword",
	})
	// This should fail because "myuser" is a root-level user, not a namespace-level user.
	// Specifying the namespace indicates that we want to authenticate
	// as a namespace-level user.
	if err == nil {
		panic("Expected SignIn to fail, but it succeeded")
	}

	fmt.Println("Root-level user SignIn tests completed successfully")

}
Output:
Root-level user SignIn tests completed successfully

func (*DB) SignInWithRefresh added in v1.2.0

func (db *DB) SignInWithRefresh(ctx context.Context, authData any) (*Tokens, error)

SignInWithRefresh signs in using a TYPE RECORD access method with WITH REFRESH enabled. This is only supported in SurrealDB v3+ and returns both an access token and a refresh token.

The authData parameter should be a map[string]any with the signin credentials:

// Initial signin with username/password
pair, err := db.SignInWithRefresh(ctx, map[string]any{
  "NS":   "app",
  "DB":   "app",
  "AC":   "user_access",
  "user": "yusuke",
  "pass": "VerySecurePassword123!",
})

The returned Tokens contains:

  • Access: JWT token (use with Authenticate() on new connections)
  • Refresh: Refresh token (format: "surreal-refresh-...")

To obtain new tokens using the refresh token (no credentials needed):

newPair, err := db.SignInWithRefresh(ctx, map[string]any{
  "NS":      "app",
  "DB":      "app",
  "AC":      "user_access",
  "refresh": pair.Refresh,  // no username/password needed
})

Note: The "refresh" parameter is for record access refresh tokens only. For bearer access methods, use SignIn with the "key" parameter. For other access methods (system users, record users without refresh), use SignIn.

func (*DB) SignUp added in v0.3.0

func (db *DB) SignUp(ctx context.Context, authData any) (string, error)

SignUp signs up a new user.

The authData parameter can be either:

  • An Auth struct
  • A map[string]any with keys like: "namespace", "database", "scope", "user", "pass"

Example with struct:

db.SignUp(Auth{
  Namespace: "app",
  Database: "app",
  Access: "user",
  Username: "yusuke",
  Password: "VerySecurePassword123!",
})

Example with map:

db.SignUp(map[string]any{
  "NS": "app",
  "DB": "app",
  "AC": "user",
  "user": "yusuke",
  "pass": "VerySecurePassword123!",
})
Example (DatabaseLevelRecordUser)
package main

import (
	"context"
	"fmt"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	// SignUp's sole purpose is to create a new record user in a database
	// that has been configured to use RECORD access method type at the database level.
	//
	// # SignIn with and without ACCESS field
	//
	// The only difference between signing in as a database user and signing in as a record user
	// is that you need to specify the Access field to indicate which access method to use for authentication.
	//
	// Like logging in as a database user defined using DEFINE USER ON DATABASE,
	// signing in as a record user also requires specifying the target namespace and database.

	db, err := surrealdb.FromEndpointURLString(
		context.Background(),
		testenv.GetSurrealDBWSURL(),
	)
	if err != nil {
		panic(err)
	}

	db, err = testenv.Init(db, "exampledb_signup_rootlevel", "testdb", "user")
	if err != nil {
		panic(err)
	}

	// Sign in as the root user
	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		Username: "root",
		Password: "root",
	})
	if err != nil {
		panic(fmt.Sprintf("SignIn failed: %v", err))
	}

	err = db.Use(context.Background(), "exampledb_signup_rootlevel", "testdb")
	if err != nil {
		panic(fmt.Sprintf("Use failed: %v", err))
	}

	// Detect SurrealDB version to use the correct function name
	// SurrealDB 2.x uses type::thing(), SurrealDB 3.x uses type::record()
	v, err := testenv.GetVersion(context.Background(), db)
	if err != nil {
		panic(fmt.Sprintf("GetVersion failed: %v", err))
	}
	recordFn := v.ThingOrRecordFn()

	setupQuery := fmt.Sprintf(`
		-- Define the user table with schema
		DEFINE TABLE user SCHEMAFULL
			PERMISSIONS
				FOR select, update, delete WHERE id = $auth.id;

		-- Define fields
		DEFINE FIELD password ON user TYPE string;

		-- Define access method for record authentication
		REMOVE ACCESS IF EXISTS user ON DATABASE;
		DEFINE ACCESS user ON DATABASE TYPE RECORD
			SIGNIN (
				SELECT * FROM %s("user", $user) WHERE crypto::argon2::compare(password, $pass)
			)
			SIGNUP (
				CREATE %s("user", $user) CONTENT {
					password: crypto::argon2::generate($pass)
				}
			);
	`, recordFn, recordFn)

	_, err = surrealdb.Query[any](context.Background(), db, setupQuery, nil)
	if err != nil {
		panic(fmt.Sprintf("Query failed: %v", err))
	}

	_, err = db.SignUp(context.Background(), surrealdb.Auth{
		Access:    "user",
		Namespace: "exampledb_signup_rootlevel",
		Database:  "testdb",
		Username:  "myuser",
		Password:  "mypassword",
	})
	if err != nil {
		panic(fmt.Sprintf("SignUp failed: %v", err))
	}

	_, err = db.SignIn(context.Background(), surrealdb.Auth{
		Access:    "user",
		Namespace: "exampledb_signup_rootlevel",
		Database:  "testdb",
		Username:  "myuser",
		Password:  "mypassword",
	})
	if err != nil {
		panic(fmt.Sprintf("SignIn failed: %v", err))
	}
	fmt.Println("User signed up and signed in successfully")

}
Output:
User signed up and signed in successfully

func (*DB) SignUpWithRefresh added in v1.2.0

func (db *DB) SignUpWithRefresh(ctx context.Context, authData any) (*Tokens, error)

SignUpWithRefresh signs up a new user using a TYPE RECORD access method with WITH REFRESH enabled. This is only supported in SurrealDB v3+ and returns both an access token and a refresh token.

The authData parameter should be a map[string]any with the signup credentials:

tokens, err := db.SignUpWithRefresh(ctx, map[string]any{
  "NS":   "app",
  "DB":   "app",
  "AC":   "user_access",
  "user": "yusuke",
  "pass": "VerySecurePassword123!",
})

The returned Tokens contains:

  • Access: JWT token (use with Authenticate() on new connections)
  • Refresh: Refresh token (format: "surreal-refresh-...")

Note: Use this method instead of SignUp when the access method has WITH REFRESH enabled. For access methods without WITH REFRESH, use SignUp instead.

func (*DB) Unset added in v0.3.0

func (db *DB) Unset(ctx context.Context, key string) error

func (*DB) Use

func (db *DB) Use(ctx context.Context, ns, database string) error

Use is a method to select the namespace and table to use.

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:}
}

func (*DB) WithContext deprecated added in v0.3.0

func (db *DB) WithContext(ctx context.Context) *DB

Deprecated: WithContext is deprecated and does nothing. Use context parameters in individual method calls instead.

type Obj deprecated added in v0.3.0

type Obj map[any]any

Deprecated: Use map[string]any instead

type PatchData added in v0.3.0

type PatchData struct {
	Op    string `json:"op"`
	Path  string `json:"path"`
	Value any    `json:"value"`
}

Patch represents a patch object set to MODIFY a record

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
}

func (*QueryStmt) GetResult added in v0.3.0

func (q *QueryStmt) GetResult(dest any) error

type RPCError deprecated

type RPCError = connection.RPCError

Deprecated: Use ServerError instead on SurrealDB v3 for richer error information. TODO(v2-compat): Remove in next major release.

type Relationship added in v0.3.0

type Relationship struct {
	ID       *models.RecordID `json:"id"`
	In       models.RecordID  `json:"in"`
	Out      models.RecordID  `json:"out"`
	Relation models.Table     `json:"relation"`
	Data     map[string]any   `json:"data"`
}

type Result deprecated added in v0.3.0

type Result[T any] struct {
	T any
}

Deprecated: Use [RPCResponse] instead.

type ServerError added in v1.4.0

type ServerError = connection.ServerError

ServerError represents a structured error from SurrealDB v3. Only use this when you know you are running against a SurrealDB v3 server.

Extract from RPC errors using errors.As:

var se *surrealdb.ServerError
if errors.As(err, &se) {
    fmt.Println(se.Kind, se.Details)
}

type Session added in v1.3.0

type Session struct {
	// contains filtered or unexported fields
}

Session represents an additional SurrealDB session on a WebSocket connection. Sessions scope live notifications and can have their own transactions.

Sessions are only supported on WebSocket connections (SurrealDB v3+). Each session starts unauthenticated and without a selected namespace/database, so you must call SignIn/Authenticate and Use after creating a session.

Session satisfies the sendable constraint, so all surrealdb.Query, surrealdb.Create, etc. functions work with sessions directly.

func (*Session) Authenticate added in v1.3.0

func (s *Session) Authenticate(ctx context.Context, token string) error

Authenticate authenticates the session with the provided token.

func (*Session) Begin added in v1.3.0

func (s *Session) Begin(ctx context.Context) (*Transaction, error)

Begin starts a new interactive transaction in this session. Interactive transactions are only supported on WebSocket connections (SurrealDB v3+).

Example

ExampleSession_Begin demonstrates starting a transaction within a session. Transactions within sessions are isolated and can be committed or canceled.

package main

import (
	"context"
	"fmt"
	"log"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	ctx := context.Background()

	// Connect using WebSocket
	db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
	if err != nil {
		log.Fatal(err)
	}
	defer func() {
		if closeErr := db.Close(ctx); closeErr != nil {
			log.Printf("Failed to close db: %v", closeErr)
		}
	}()

	// Sign in and set up
	_, err = db.SignIn(ctx, map[string]any{"user": "root", "pass": "root"})
	if err != nil {
		log.Fatal(err) //nolint:gocritic // Example code - log.Fatal is acceptable
	}
	err = db.Use(ctx, "test", "test")
	if err != nil {
		log.Fatal(err) //nolint:gocritic // Example code - log.Fatal is acceptable
	}

	// Create a session
	session, err := db.Attach(ctx)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = session.Detach(ctx) }()

	// Authenticate and configure the session
	_, err = session.SignIn(ctx, map[string]any{"user": "root", "pass": "root"})
	if err != nil {
		log.Fatal(err)
	}
	err = session.Use(ctx, "test", "test")
	if err != nil {
		log.Fatal(err)
	}

	// Start a transaction within the session
	tx, err := session.Begin(ctx)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Transaction started with ID: %s\n", tx.ID())
	fmt.Printf("Transaction is in session: %s\n", tx.SessionID())

	// Perform operations in the transaction
	// ... your operations here ...

	// Commit or cancel
	err = tx.Commit(ctx)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Transaction committed successfully")

	// Note: This example requires SurrealDB v3+ and will fail on earlier versions.
	// Output is not verified because transaction IDs are dynamic.
}

func (*Session) CloseLiveNotifications added in v1.3.0

func (s *Session) CloseLiveNotifications(liveQueryID string) error

CloseLiveNotifications closes the notification channel for a live query.

func (*Session) Detach added in v1.3.0

func (s *Session) Detach(ctx context.Context) error

Detach deletes the session from the server. After calling Detach, the session cannot be used anymore.

func (*Session) ID added in v1.3.0

func (s *Session) ID() *models.UUID

ID returns the session's UUID.

func (*Session) Info added in v1.3.0

func (s *Session) Info(ctx context.Context) (map[string]any, error)

Info returns information about the current session state.

func (*Session) Invalidate added in v1.3.0

func (s *Session) Invalidate(ctx context.Context) error

Invalidate invalidates the authentication for this session.

func (*Session) Let added in v1.3.0

func (s *Session) Let(ctx context.Context, key string, val any) error

Let sets a variable in this session.

func (*Session) LiveNotifications added in v1.3.0

func (s *Session) LiveNotifications(liveQueryID string) (chan connection.Notification, error)

LiveNotifications returns a channel for receiving live query notifications.

func (*Session) SignIn added in v1.3.0

func (s *Session) SignIn(ctx context.Context, authData any) (string, error)

SignIn signs in an existing user in this session.

func (*Session) SignInWithRefresh added in v1.3.0

func (s *Session) SignInWithRefresh(ctx context.Context, authData any) (*Tokens, error)

SignInWithRefresh signs in using a TYPE RECORD access method with WITH REFRESH enabled.

func (*Session) SignUp added in v1.3.0

func (s *Session) SignUp(ctx context.Context, authData any) (string, error)

SignUp signs up a new user in this session.

func (*Session) SignUpWithRefresh added in v1.3.0

func (s *Session) SignUpWithRefresh(ctx context.Context, authData any) (*Tokens, error)

SignUpWithRefresh signs up a new user using a TYPE RECORD access method with WITH REFRESH enabled.

func (*Session) Unset added in v1.3.0

func (s *Session) Unset(ctx context.Context, key string) error

Unset removes a variable from this session.

func (*Session) Use added in v1.3.0

func (s *Session) Use(ctx context.Context, ns, database string) error

Use selects the namespace and database for this session.

func (*Session) Version added in v1.3.0

func (s *Session) Version(ctx context.Context) (*VersionData, error)

Version returns the SurrealDB version information.

type TableOrRecord added in v0.3.0

type TableOrRecord interface {
	string | models.Table | models.RecordID | []models.Table | []models.RecordID
}

type Tokens added in v1.2.0

type Tokens = connection.Tokens

Tokens contains the access token and refresh token returned by SignInWithRefresh. Access is the JWT token used for authentication. Use this with Authenticate() to establish a session on a new connection. Refresh is the refresh token used to obtain new tokens without credentials. Use this with SignInWithRefresh to get a new Tokens.

type Transaction added in v1.3.0

type Transaction struct {
	// contains filtered or unexported fields
}

Transaction represents an interactive SurrealDB transaction on a WebSocket connection. Unlike text-based transactions (BEGIN TRANSACTION; ... COMMIT;), interactive transactions allow executing statements one at a time and conditionally committing or canceling.

Transactions are only supported on WebSocket connections (SurrealDB v3+).

Transaction satisfies the sendable constraint, so all surrealdb.Query, surrealdb.Create, etc. functions work with transactions directly.

Note: Transactions do NOT support session state changes like SignIn, Use, Let, etc. The namespace/database and authentication are inherited from the session or connection that started the transaction.

Example (ConditionalCommit)

ExampleTransaction_conditionalCommit demonstrates conditional commit/cancel. Based on query results, you can decide whether to commit or rollback.

package main

import (
	"context"
	"fmt"
	"log"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	ctx := context.Background()

	// Connect using WebSocket
	db, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
	if err != nil {
		log.Fatal(err)
	}
	defer func() {
		if closeErr := db.Close(ctx); closeErr != nil {
			log.Printf("Failed to close db: %v", closeErr)
		}
	}()

	// Sign in and configure
	_, err = db.SignIn(ctx, map[string]any{"user": "root", "pass": "root"})
	if err != nil {
		log.Fatal(err) //nolint:gocritic // Example code - log.Fatal is acceptable
	}
	err = db.Use(ctx, "test", "test")
	if err != nil {
		log.Fatal(err) //nolint:gocritic // Example code - log.Fatal is acceptable
	}

	// Start transaction
	tx, err := db.Begin(ctx)
	if err != nil {
		log.Fatal(err)
	}

	// Simulate a business operation: deduct from inventory
	type Inventory struct {
		Stock int `json:"stock"`
	}

	// Check current stock
	results, err := surrealdb.Query[[]Inventory](ctx, tx,
		"SELECT stock FROM inventory:item1", nil)
	if err != nil {
		_ = tx.Cancel(ctx)
		log.Fatal(err)
	}

	var currentStock int
	if len(*results) > 0 && len((*results)[0].Result) > 0 {
		currentStock = (*results)[0].Result[0].Stock
	}

	requestedQuantity := 5

	// Conditional logic based on query results
	if currentStock >= requestedQuantity {
		// Update stock
		_, err = surrealdb.Query[any](ctx, tx,
			"UPDATE inventory:item1 SET stock -= $qty",
			map[string]any{"qty": requestedQuantity})
		if err != nil {
			_ = tx.Cancel(ctx)
			log.Fatal(err)
		}

		// Commit the transaction
		err = tx.Commit(ctx)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println("Transaction committed: inventory updated")
	} else {
		// Not enough stock - cancel transaction
		err = tx.Cancel(ctx)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println("Transaction canceled: insufficient stock")
	}

	// Output depends on inventory state
}
Example (Isolation)

ExampleTransaction_isolation demonstrates transaction isolation. Changes made in a transaction are not visible to other connections until the transaction is committed.

package main

import (
	"context"
	"fmt"
	"log"

	surrealdb "github.com/surrealdb/surrealdb.go"
	"github.com/surrealdb/surrealdb.go/contrib/testenv"
)

func main() {
	ctx := context.Background()

	// Create two connections
	db1, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
	if err != nil {
		log.Fatal(err)
	}
	defer func() {
		if closeErr := db1.Close(ctx); closeErr != nil {
			log.Printf("Failed to close db1: %v", closeErr)
		}
	}()

	db2, err := surrealdb.FromEndpointURLString(ctx, testenv.GetSurrealDBWSURL())
	if err != nil {
		log.Fatal(err) //nolint:gocritic // Example code - log.Fatal is acceptable
	}
	defer func() {
		if closeErr := db2.Close(ctx); closeErr != nil {
			log.Printf("Failed to close db2: %v", closeErr)
		}
	}()

	// Configure both connections
	for _, db := range []*surrealdb.DB{db1, db2} {
		_, signInErr := db.SignIn(ctx, map[string]any{"user": "root", "pass": "root"})
		if signInErr != nil {
			log.Fatal(signInErr)
		}
		useErr := db.Use(ctx, "test", "test")
		if useErr != nil {
			log.Fatal(useErr)
		}
	}

	// Start transaction on db1
	tx, err := db1.Begin(ctx)
	if err != nil {
		log.Fatal(err)
	}
	defer func() {
		if !tx.IsClosed() {
			_ = tx.Cancel(ctx)
		}
	}()

	// Create record in transaction
	_, err = surrealdb.Query[any](ctx, tx,
		"CREATE items:isolated SET value = 'hidden'", nil)
	if err != nil {
		log.Fatal(err)
	}

	// Query from db2 - should NOT see uncommitted data
	type Item struct {
		Value string `json:"value"`
	}
	results, err := surrealdb.Query[[]Item](ctx, db2,
		"SELECT * FROM items:isolated", nil)
	if err != nil {
		log.Fatal(err)
	}

	if len(*results) > 0 && len((*results)[0].Result) == 0 {
		fmt.Println("Before commit: db2 cannot see uncommitted data")
	}

	// Commit
	err = tx.Commit(ctx)
	if err != nil {
		log.Fatal(err)
	}

	// Now db2 should see the data
	results, err = surrealdb.Query[[]Item](ctx, db2,
		"SELECT * FROM items:isolated", nil)
	if err != nil {
		log.Fatal(err)
	}

	if len(*results) > 0 && len((*results)[0].Result) > 0 {
		fmt.Println("After commit: db2 can see committed data")
	}

	// Note: This example requires SurrealDB v3+ and will fail on earlier versions.
}

func (*Transaction) Cancel added in v1.3.0

func (tx *Transaction) Cancel(ctx context.Context) error

Cancel cancels the transaction, discarding all changes. After calling Cancel, the transaction cannot be used anymore.

It's safe to call Cancel on an already committed or canceled transaction; it will return ErrTransactionClosed but won't cause any harm.

func (*Transaction) Commit added in v1.3.0

func (tx *Transaction) Commit(ctx context.Context) error

Commit commits the transaction, making all changes permanent. After calling Commit, the transaction cannot be used anymore.

func (*Transaction) ID added in v1.3.0

func (tx *Transaction) ID() *models.UUID

ID returns the transaction's UUID.

func (*Transaction) IsClosed added in v1.3.0

func (tx *Transaction) IsClosed() bool

IsClosed returns whether the transaction has been committed or canceled.

func (*Transaction) SessionID added in v1.3.0

func (tx *Transaction) SessionID() *models.UUID

SessionID returns the session UUID if the transaction was started within a session. Returns nil if the transaction was started on the default session.

type VersionData added in v0.3.0

type VersionData struct {
	Version   string `json:"version"`
	Build     string `json:"build"`
	Timestamp string `json:"timestamp"`
}

Directories

Path Synopsis
Package contrib provides additional functionality and utilities for the SurrealDB Go SDK.
Package contrib provides additional functionality and utilities for the SurrealDB Go SDK.
rews
Package rews provides a reliable, auto-reconnecting WebSocket connection for SurrealDB with support for session restoration, live query persistence, and customizable retry strategies.
Package rews provides a reliable, auto-reconnecting WebSocket connection for SurrealDB with support for session restoration, live query persistence, and customizable retry strategies.
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.
surrealnote module
cmd command
internal
fakesdb
Package fakesdb provides a fake SurrealDB WebSocket server for testing purposes.
Package fakesdb provides a fake SurrealDB WebSocket server for testing purposes.
pkg
Package surrealcbor provides CBOR (Concise Binary Object Representation) encoding and decoding for SurrealDB.
Package surrealcbor provides CBOR (Concise Binary Object Representation) encoding and decoding for SurrealDB.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL