Rust FFI Driver

May 27, 2026 ยท View on GitHub

import "github.com/CaliLuke/go-typeql/driver" (requires build tags: cgo,typedb) -- pkg.go.dev

The driver package provides Go bindings to the official TypeDB typedb-driver 3.x Rust crate via CGo. All files are gated with //go:build cgo && typedb so they don't affect builds that don't need the driver.

The bundled Rust FFI crate currently depends on typedb-driver 3.11.5, paired with typeql 3.11.0 and the typedb/typedb:3.11.5 integration-test image.

go get only downloads the module source. It does not build the Rust static library automatically. If you import driver/, you must either run make build-rust in the module tree that Go is compiling, or provide a prebuilt libtypedb_go_ffi.a and build with the typedb_prebuilt tag. Release archives are published for linux-amd64, linux-arm64, darwin-amd64, and darwin-arm64.

Prerequisites

  • Rust toolchain -- install via rustup
  • TypeDB 3.x server -- for integration tests
  • CGo -- enabled by default in Go

Building

# Build the Rust FFI static library
make build-rust

# Build Go code with driver support
go build -tags "cgo,typedb" ./...

The Rust crate lives in driver/rust/ and compiles to driver/rust/target/release/libtypedb_go_ffi.a, which the default CGo build links via driver/ffi.go.

Connecting

import "github.com/CaliLuke/go-typeql/driver"

// Basic connection
drv, err := driver.Open("localhost:1729", "admin", "password")
if err != nil {
    log.Fatal(err)
}
defer drv.Close()

// With TLS
drv, err := driver.OpenWithTLS("localhost:1729", "admin", "password", true, "/path/to/ca.crt")

// With TypeDB 3.11 driver-level options
drv, err := driver.OpenWithOptions("localhost:1729", "admin", "password", driver.DriverOptions{
    RequestTimeoutMillis:   5000,
    PrimaryFailoverRetries: 1,
})

// Inspect the connected server version
version, err := drv.ServerVersion()
if err == nil {
    log.Printf("connected to %s %s", version.Distribution, version.Version)
}

// Multiple public addresses
drv, err = driver.OpenWithAddresses([]string{
    "typedb-1.example.com:1729",
    "typedb-2.example.com:1729",
}, "admin", "password", driver.DriverOptions{})

// Public-to-private address translation for clusters or mapped containers
drv, err = driver.OpenWithAddressTranslation(map[string]string{
    "localhost:1730": "127.0.0.1:1729",
}, "admin", "password", driver.DriverOptions{})

Open and single-address OpenWithAddresses preserve the repo compose mapping (localhost:1730 on the host to 127.0.0.1:1729 as advertised by TypeDB CE). Use OpenWithAddressTranslation for explicit public-to-private mappings.

TypeDB 3.11 Connection Features

The 3.11 Rust driver adds a small set of connection-level controls that are exposed through DriverOptions:

OptionApplies to
RequestTimeoutMillisUnary RPCs such as database create/list, schema fetch, and transaction open
PrimaryFailoverRetriesFinding or re-routing to a primary server in clustered deployments
TLSEnabled/TLSRootCATLS setup, equivalent to OpenWithTLS

These options do not replace QueryOptions; query result prefetch and instance-type inclusion are still configured per query.

ServerVersion is useful at process startup to make protocol mismatches obvious before application code begins opening transactions:

version, err := drv.ServerVersion()
if err != nil {
    log.Fatal(err)
}
log.Printf("connected to %s %s", version.Distribution, version.Version)

For clusters and containerized deployments, use:

  • OpenWithAddresses when several public server addresses are directly reachable.
  • OpenWithAddressTranslation when TypeDB advertises private addresses that differ from the addresses clients must dial.

Transactions

txn, err := drv.Transaction("my_db", driver.Write)
if err != nil {
    log.Fatal(err)
}
defer txn.Close()

results, err := txn.Query(`insert $p isa person, has name "Alice";`)
if err != nil {
    log.Fatal(err)
}

err = txn.Commit()

Transaction types: Read (0), Write (1), Schema (2).

Close() is caller-fast for uncommitted transactions: it detaches the Go handle immediately and completes the checked TypeDB close on a bounded background worker. Close failures are logged because the gotype.Tx interface cannot return a close error.

Use CloseChecked() when you deliberately want to wait for the TypeDB close result:

if err := txn.CloseChecked(); err != nil {
    log.Printf("close failed: %v", err)
}

Use CloseAsync when you need a completion callback without blocking the caller:

txn.CloseAsync(func(err error) {
    if err != nil {
        log.Printf("close failed: %v", err)
    }
})

Commit() and Rollback() remain synchronous. A deferred Close() after Commit() is a no-op because Commit() consumes the transaction handle.

Long-running applications and integration tests can drain accepted background close work before shutdown:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := driver.WaitForPendingCloses(ctx); err != nil {
    log.Printf("transaction close drain timed out: %v", err)
}

Database Management

dbs := drv.Databases()

exists, _ := dbs.Contains("my_db")
if !exists {
    dbs.Create("my_db")
}

// Get schema for migration
schema, _ := dbs.Schema("my_db")

// List all databases
names, _ := dbs.All()

// Delete a database
dbs.Delete("my_db")

Interface Compatibility

The driver.Driver type satisfies the gotype.Conn interface and driver.Transaction satisfies gotype.Tx. This is the key decoupling that lets the ORM layer work without CGo:

  • The gotype package compiles without CGo or the typedb build tag.
  • Unit tests use mock implementations of Conn/Tx (see Testing Guide).
  • Any compatible TypeDB client can be used as a backend.

The Conn interface includes database lifecycle methods (DatabaseCreate, DatabaseDelete, DatabaseContains, DatabaseAll) and schema introspection (Schema), all of which the driver satisfies.

Architecture Notes

JSON at the boundary: Query results cross the FFI boundary as JSON strings. The Rust layer serializes each result row to JSON; the Go layer deserializes them to []map[string]any. This avoids complex C struct marshalling while keeping the API clean.

Thread-local error pattern: The Rust FFI uses typedb_check_error() / typedb_get_last_error() for error reporting. The Go side checks after each FFI call.

Build tags: All driver source files use //go:build cgo && typedb. Integration tests additionally use the integration tag. This means:

  • go test ./gotype/... works without Rust or CGo
  • go build -tags "cgo,typedb" ./driver/... compiles the driver
  • go test -tags "cgo,typedb,integration" ./driver/... runs integration tests

If you use the repo docker-compose.yml for integration tests, set TEST_DB_ADDRESS=localhost:1730 because the compose stack maps host port 1730 to the server's internal 1729.

Error Handling

Driver-specific errors:

  • ErrNotConnected -- driver or transaction handle is nil
  • ErrNilPointer -- FFI returned a nil pointer without setting an error
  • DriverError -- error message from the Rust driver