Go Driver
Go driver for Stoolap built on the Rust engine via C FFI (cgo). Provides two ways to use Stoolap from Go:
- Direct API for maximum performance and control
database/sqldriver for standard Go database access
Requirements
- Go 1.24+
- CGO enabled (
CGO_ENABLED=1, the default)
Installation
go get github.com/stoolap/stoolap-go
Prebuilt shared libraries for macOS (arm64), Linux (x64), and Windows (x64) are bundled
in the module. No extra downloads or environment variables needed, just go get and build.
The compiled Go binary dynamically links against libstoolap. For deployment, place the
shared library next to your executable or in a system library path.
Other Platforms
For platforms without a bundled library (e.g. Linux arm64, macOS x64), download from the
releases page or build from source,
then build with the stoolap_use_lib tag:
export LIBRARY_PATH=/path/to/stoolap/target/release
go build -tags stoolap_use_lib ./...
Quick Start
Direct API
package main
import (
"context"
"fmt"
stoolap "github.com/stoolap/stoolap-go"
)
func main() {
db, err := stoolap.Open("memory://")
if err != nil {
panic(err)
}
defer db.Close()
ctx := context.Background()
db.Exec(ctx, "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)")
db.Exec(ctx, "INSERT INTO users VALUES (1, 'Alice', 30), (2, 'Bob', 25)")
rows, _ := db.Query(ctx, "SELECT id, name, age FROM users ORDER BY id")
defer rows.Close()
for rows.Next() {
var id int64
var name string
var age int64
rows.Scan(&id, &name, &age)
fmt.Printf("id=%d name=%s age=%d\n", id, name, age)
}
}
database/sql Driver
package main
import (
"context"
"database/sql"
"fmt"
_ "github.com/stoolap/stoolap-go/pkg/driver"
)
func main() {
db, err := sql.Open("stoolap", "memory://")
if err != nil {
panic(err)
}
defer db.Close()
ctx := context.Background()
db.ExecContext(ctx, "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)")
db.ExecContext(ctx, "INSERT INTO users VALUES (1, 'Alice', 30), (2, 'Bob', 25)")
rows, _ := db.QueryContext(ctx, "SELECT id, name, age FROM users ORDER BY id")
defer rows.Close()
for rows.Next() {
var id int64
var name string
var age int64
rows.Scan(&id, &name, &age)
fmt.Printf("id=%d name=%s age=%d\n", id, name, age)
}
}
Connection Strings
| DSN | Description |
|---|---|
memory:// |
In-memory database (unique, isolated instance) |
memory://mydb |
Named in-memory database (same name shares the engine) |
file:///path/to/db |
File-based persistent database |
file:///path/to/db?sync_mode=full |
File-based with configuration options |
See Connection String Reference for all configuration options.
Parameters
Use positional parameters $1, $2, etc. with driver.NamedValue:
import "database/sql/driver"
ctx := context.Background()
db.ExecContext(ctx, "INSERT INTO users VALUES ($1, $2, $3)",
driver.NamedValue{Ordinal: 1, Value: int64(1)},
driver.NamedValue{Ordinal: 2, Value: "Alice"},
driver.NamedValue{Ordinal: 3, Value: int64(30)},
)
row := db.QueryRow(ctx, "SELECT name FROM users WHERE id = $1",
driver.NamedValue{Ordinal: 1, Value: int64(1)},
)
var name string
row.Scan(&name)
With database/sql, use standard positional arguments:
db.ExecContext(ctx, "INSERT INTO users VALUES ($1, $2, $3)", 1, "Alice", 30)
rows, _ := db.QueryContext(ctx, "SELECT name FROM users WHERE id = $1", 1)
Transactions
Default Isolation (Read Committed)
tx, err := db.Begin()
if err != nil {
panic(err)
}
tx.ExecContext(ctx, "INSERT INTO users VALUES ($1, $2)",
driver.NamedValue{Ordinal: 1, Value: int64(1)},
driver.NamedValue{Ordinal: 2, Value: "Alice"},
)
if err := tx.Commit(); err != nil {
panic(err)
}
Snapshot Isolation
tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelSnapshot,
})
if err != nil {
panic(err)
}
defer tx.Rollback()
// All reads within this transaction see the same snapshot
rows, _ := tx.QueryContext(ctx, "SELECT * FROM users")
// ...
tx.Commit()
| Level | Description |
|---|---|
| Read Committed (default) | Each statement sees data committed before it started |
| Snapshot | The transaction sees a consistent snapshot from when it began |
Prepared Statements
Parse once, execute many times:
stmt, err := db.Prepare("INSERT INTO users VALUES ($1, $2)")
if err != nil {
panic(err)
}
defer stmt.Close()
for i := int64(1); i <= 1000; i++ {
stmt.ExecContext(ctx,
driver.NamedValue{Ordinal: 1, Value: i},
driver.NamedValue{Ordinal: 2, Value: "User"},
)
}
Prepared Statements in Transactions
For transactional atomicity with parse-once performance, prepare statements via Tx.Prepare(). This uses stoolap_tx_stmt_exec/stoolap_tx_stmt_query internally, ensuring all operations participate in the transaction’s commit/rollback.
stmt, err := db.Prepare("INSERT INTO orders VALUES ($1, $2, $3)")
if err != nil {
panic(err)
}
defer stmt.Close()
tx, err := db.Begin()
if err != nil {
panic(err)
}
txStmt, err := tx.Prepare("INSERT INTO orders VALUES ($1, $2, $3)")
if err != nil {
tx.Rollback()
panic(err)
}
defer txStmt.Close()
for i := int64(0); i < 1000; i++ {
txStmt.ExecContext(ctx,
driver.NamedValue{Ordinal: 1, Value: i},
driver.NamedValue{Ordinal: 2, Value: int64(1)},
driver.NamedValue{Ordinal: 3, Value: 99.99},
)
}
tx.Commit() // all 1000 rows committed atomically
Important: Do not use stmt.ExecContext() (DB-level prepared statement) inside a transaction block. It creates its own standalone auto-committing transaction per call, so rollback will not undo those operations. Always use tx.Prepare() for transaction-bound statements.
NULL Handling
Use sql.Null* types for nullable columns:
var (
name sql.NullString
age sql.NullInt64
score sql.NullFloat64
active sql.NullBool
ts sql.NullTime
)
row := db.QueryRow(ctx, "SELECT name, age, score, active, created_at FROM users WHERE id = $1",
driver.NamedValue{Ordinal: 1, Value: int64(1)},
)
row.Scan(&name, &age, &score, &active, &ts)
if name.Valid {
fmt.Println("Name:", name.String)
} else {
fmt.Println("Name is NULL")
}
Scanning into any
rows, _ := db.Query(ctx, "SELECT id, name, age FROM users")
defer rows.Close()
for rows.Next() {
var id, name, age any
rows.Scan(&id, &name, &age)
fmt.Printf("id=%v name=%v age=%v\n", id, name, age)
}
JSON
JSON values are stored and retrieved as strings:
db.Exec(ctx, "CREATE TABLE docs (id INTEGER PRIMARY KEY, data JSON)")
db.Exec(ctx, `INSERT INTO docs VALUES (1, '{"name":"Alice","age":30}')`)
var data string
db.QueryRow(ctx, "SELECT data FROM docs WHERE id = 1").Scan(&data)
// data = `{"name":"Alice","age":30}`
Vector Search
Vectors are stored as packed little-endian f32 bytes:
import (
"encoding/binary"
"math"
)
db.Exec(ctx, "CREATE TABLE vectors (id INTEGER PRIMARY KEY, embedding VECTOR(3))")
// Encode a float32 vector to bytes
vec := []float32{1.0, 2.0, 3.0}
buf := make([]byte, len(vec)*4)
for i, f := range vec {
binary.LittleEndian.PutUint32(buf[i*4:], math.Float32bits(f))
}
db.ExecContext(ctx, "INSERT INTO vectors VALUES ($1, $2)",
driver.NamedValue{Ordinal: 1, Value: int64(1)},
driver.NamedValue{Ordinal: 2, Value: buf},
)
// Read back
var blob []byte
db.QueryRow(ctx, "SELECT embedding FROM vectors WHERE id = 1").Scan(&blob)
// Decode packed f32 bytes back to float32 slice
result := make([]float32, len(blob)/4)
for i := range result {
result[i] = math.Float32frombits(binary.LittleEndian.Uint32(blob[i*4:]))
}
Bulk Fetch
FetchAll() fetches all remaining rows into a single packed binary buffer, avoiding per-row FFI overhead:
rows, _ := db.Query(ctx, "SELECT id, name, age FROM users")
defer rows.Close()
buf, err := rows.FetchAll()
if err != nil {
panic(err)
}
// buf contains all rows in packed binary format
// See the C API docs for the binary format specification
Cloning for Concurrency
A single DB handle must not be used from multiple goroutines simultaneously. Use Clone() to create per-goroutine handles that share the underlying engine:
db, _ := stoolap.Open("memory://mydb")
defer db.Close()
db.Exec(ctx, "CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT)")
var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
clone, _ := db.Clone()
defer clone.Close()
clone.Exec(ctx, fmt.Sprintf("INSERT INTO t VALUES (%d, 'worker-%d')", workerID, workerID))
}(i)
}
wg.Wait()
The database/sql driver handles this automatically. Each connection in the pool gets its own cloned handle.
Type Mapping
| SQL Type | Go Type | Nullable Go Type |
|---|---|---|
| INTEGER | int64, int, int32 |
sql.NullInt64 |
| FLOAT | float64, float32 |
sql.NullFloat64 |
| TEXT | string |
sql.NullString |
| BOOLEAN | bool |
sql.NullBool |
| TIMESTAMP | time.Time |
sql.NullTime |
| JSON | string |
sql.NullString |
| VECTOR/BLOB | []byte |
[]byte (nil for NULL) |
Scan supports type coercion: INTEGER columns can scan into *string, FLOAT into *int64, etc.
Thread Safety
- Direct API: A single
DBhandle must not be shared across goroutines. UseClone()for per-goroutine handles. database/sql: Thread-safe by default. The connection pool creates cloned handles automatically.- Tx, Stmt, Rows: Must remain on the goroutine that created them.
Direct API Reference
Package Functions
| Function | Returns | Description |
|---|---|---|
Version() |
string |
Stoolap library version |
Open(dsn) |
*DB, error |
Open a database connection |
DB
| Method | Returns | Description |
|---|---|---|
Close() |
error |
Close the connection |
Clone() |
*DB, error |
Clone handle for multi-goroutine use |
Exec(ctx, query) |
sql.Result, error |
Execute without parameters |
ExecContext(ctx, query, args...) |
sql.Result, error |
Execute with parameters |
Query(ctx, query) |
Rows, error |
Query without parameters |
QueryContext(ctx, query, args...) |
Rows, error |
Query with parameters |
QueryRow(ctx, query, args...) |
Row |
Query expecting at most one row |
Begin() |
Tx, error |
Begin transaction (Read Committed) |
BeginTx(ctx, opts) |
Tx, error |
Begin transaction with options |
Prepare(query) |
Stmt, error |
Create a prepared statement |
PrepareContext(ctx, query) |
Stmt, error |
Create a prepared statement with context |
Rows
| Method | Returns | Description |
|---|---|---|
Next() |
bool |
Advance to next row |
Scan(dest...) |
error |
Read current row columns |
Close() |
error |
Close result set |
Columns() |
[]string |
Get column names |
FetchAll() |
[]byte, error |
Fetch all remaining rows as packed binary |
Row
| Method | Returns | Description |
|---|---|---|
Scan(dest...) |
error |
Read the row columns (sql.ErrNoRows if empty) |
Tx
| Method | Returns | Description |
|---|---|---|
Commit() |
error |
Commit the transaction |
Rollback() |
error |
Rollback the transaction |
ExecContext(ctx, query, args...) |
sql.Result, error |
Execute within the transaction |
QueryContext(ctx, query, args...) |
Rows, error |
Query within the transaction |
Prepare(query) |
Stmt, error |
Prepare statement bound to the transaction |
ID() |
int64 |
Get the transaction ID |
Stmt
| Method | Returns | Description |
|---|---|---|
ExecContext(ctx, args...) |
sql.Result, error |
Execute the prepared statement |
QueryContext(ctx, args...) |
Rows, error |
Query with the prepared statement |
SQL() |
string |
Get the SQL text |
Close() |
error |
Destroy the prepared statement |
database/sql Driver
The driver is registered as "stoolap" and implements the following database/sql/driver interfaces:
| Interface | Description |
|---|---|
driver.Driver |
Basic driver |
driver.DriverContext |
Connector-based driver |
driver.Connector |
Connection factory with pooling |
driver.Conn |
Connection |
driver.ConnBeginTx |
Transaction with isolation levels |
driver.ExecerContext |
Direct exec (bypasses prepare) |
driver.QueryerContext |
Direct query (bypasses prepare) |
driver.ConnPrepareContext |
Prepared statements |
driver.Pinger |
Connection health check |
driver.SessionResetter |
Session reset on pool return |
driver.Validator |
Connection validation |
driver.Tx |
Transaction commit/rollback |
driver.Stmt |
Prepared statement |
driver.StmtExecContext |
Prepared exec with context |
driver.StmtQueryContext |
Prepared query with context |
driver.Rows |
Result set iteration |
Building from Source
git clone https://github.com/stoolap/stoolap-go.git
cd stoolap-go
go test -v ./...