• Start

Concepts

Query builder

The Go SDK provides a contrib package for building type-safe SurrealQL queries programmatically with automatic parameter binding.

Warning

This feature is provided by the contrib/surrealql package, which is outside of the backward compatibility guarantees of the core SDK. Its API may change without following semantic versioning.

The surrealql query builder lets you construct SurrealQL queries programmatically using a fluent Go API. It automatically binds values as parameters to prevent injection, and produces a query string and variables map that you can pass directly to surrealdb.Query.

The query builder is part of the SDK module. Import it alongside the main package:

import "github.com/surrealdb/surrealdb.go/contrib/surrealql"

Every query builder type has a .Build() method that returns a SurrealQL string and a map[string]any of parameters. Pass these directly to surrealdb.Query:

q := surrealql.Select("users").Where("age > ?", 18).Limit(10)
sql, vars := q.Build()

results, err := surrealdb.Query[[]User](ctx, db, sql, vars)

Use Select to start a SELECT query. Chain .Fields(), .Where(), .OrderBy(), .Limit(), and other methods to refine it.

surrealql.Select("users")
// SELECT * FROM users

surrealql.Select("users").Fields("name", "email").Where("active = ?", true)
// SELECT name, email FROM users WHERE active = $param_1

surrealql.Select("users").
	Field(surrealql.Expr("count()").As("total")).
	GroupBy("department")
// SELECT count() AS total FROM users GROUP BY department

SelectOnly produces a SELECT ... ONLY query that returns a single record instead of an array. You can pass RecordID and Table values directly as targets:

surrealql.SelectOnly(models.NewRecordID("users", "tobie")).Fields("name")
// SELECT ONLY name FROM $id_1

The .Where() method accepts a condition string with ? placeholders. Values are bound as parameters automatically.

surrealql.Select("products").
	Where("price < ?", 100).
	Where("category = ?", "electronics")
// SELECT * FROM products WHERE price < $param_1 AND category = $param_2
surrealql.Select("users").
	OrderByDesc("created_at").
	Limit(20).
	Start(40)
// SELECT * FROM users ORDER BY created_at DESC LIMIT 20 START 40

Use Create to build a CREATE query. Set fields with .Set() or provide the entire content with .Content().

surrealql.Create("users").
	Set("name", "Alice").
	Set("age", 30)
// CREATE users SET name = $set_1, age = $set_2

surrealql.Create("users").Content(map[string]any{
	"name": "Alice",
	"age":  30,
})
// CREATE users CONTENT $content_1

CreateOnly produces a CREATE ... ONLY query:

surrealql.CreateOnly("users").Set("name", "Alice").ReturnNone()
// CREATE ONLY users SET name = $set_1 RETURN NONE

Use Update to build an UPDATE query with .Set() and .Where():

surrealql.Update("users").
	Set("active", false).
	Where("last_login < ?", "2025-01-01")
// UPDATE users SET active = $set_1 WHERE last_login < $param_1

Compound operations are supported in .Set():

surrealql.Update("products").Set("stock -= ?", 1).Where("id = ?", "products:apple")
// UPDATE products SET stock -= $set_1 WHERE id = $param_1

Use Upsert to build an UPSERT query. This creates a record if it does not exist, or updates it if it does. After calling Upsert, choose one of the data modes: .Set(), .Content(), .Merge(), .Patch(), or .Replace().

surrealql.Upsert("users:tobie").Set("name", "Tobie").Set("active", true)
// UPSERT users:tobie SET name = $set_1, active = $set_2

surrealql.Upsert("users:tobie").Content(map[string]any{
	"name": "Tobie",
	"active": true,
})
// UPSERT users:tobie CONTENT $upsert_content_1

surrealql.Upsert("users:tobie").Merge(map[string]any{"active": false})
// UPSERT users:tobie MERGE $upsert_merge_1

Upsert queries also support .Where(), .Timeout(), .Parallel(), and .Explain().

Use Delete to build a DELETE query:

surrealql.Delete("sessions").Where("expired_at < ?", "2025-01-01")
// DELETE sessions WHERE expired_at < $param_1

Use Insert to build an INSERT query. This is useful for bulk inserts and inserting from subqueries.

surrealql.Insert("users").Value(map[string]any{
	"name": "Alice",
	"age":  30,
})
// INSERT INTO users $insert_data_1

surrealql.Insert("users").
	Fields("name", "age").
	Values("Alice", 30).
	Values("Bob", 25)
// INSERT INTO users (name, age) VALUES ($insert_0_0_1, $insert_0_1_1), ($insert_1_0_1, $insert_1_1_1)

surrealql.Insert("users").Relation().Value(map[string]any{
	"in":  "users:tobie",
	"out": "posts:first",
})
// INSERT RELATION INTO users $insert_data_1

Handle conflicts with ON DUPLICATE KEY UPDATE:

surrealql.Insert("products").
	Fields("id", "stock").
	Values("products:apple", 10).
	OnDuplicateKeyUpdateRaw("stock += $input.stock")

Use Relate to build a RELATE query between two records:

surrealql.Relate(
	models.NewRecordID("users", "tobie"),
	"wrote",
	models.NewRecordID("posts", "first"),
).Set("created_at", "2026-01-01")

The Expr function creates parameterised expressions with ? placeholders. Use it for function calls, graph traversals, or any expression that needs value binding.

surrealql.Select("users").
	Field(surrealql.Expr("math::mean([?, ?, ?])", 1, 2, 3).As("avg"))
// SELECT math::mean([$param_1, $param_2, $param_3]) AS avg FROM users

surrealql.Select(surrealql.Expr("?->knows->users", models.NewRecordID("users", "tobie")))
// SELECT * FROM $from_param_1->knows->users

All mutation queries support return clauses:

surrealql.Create("users").Set("name", "Alice").ReturnNone()
// CREATE users SET name = $set_1 RETURN NONE

surrealql.Update("users").Set("active", true).Return("AFTER")
// UPDATE users SET active = $set_1 RETURN AFTER

surrealql.Delete("sessions").ReturnBefore()
// DELETE sessions RETURN BEFORE

The query builder can compose text-based transactions with Begin:

q := surrealql.Begin().
	Then(surrealql.Create("users").Set("name", "Alice")).
	Then(surrealql.Create("users").Set("name", "Bob"))

sql, vars := q.Build()
// BEGIN TRANSACTION; CREATE users SET name = $set_1; CREATE users SET name = $set_2; COMMIT TRANSACTION;
Note

Text-based transactions built with the query builder are different from interactive transactions. Text-based transactions execute all statements atomically in a single RPC call. Interactive transactions allow inspecting results between statements.

Was this page helpful?