Geode Go Client Library
A production-ready Go database/sql driver for the Geode graph database with dual transport support (QUIC default, gRPC optional) and Protobuf wire protocol.
Installation
go get geodedb.com/geode
Features
- ✅ Full
database/sqldriver - Standard Go database interface - ✅ Dual transport - QUIC (default) and gRPC
- ✅ Protobuf wire protocol - Efficient binary messaging
- ✅ TLS 1.3 security - QUIC always uses TLS
- ✅ Connection pooling - Built into
database/sql - ✅ Prepared statements - Query parameter support
- ✅ Transaction support - ACID transactions with savepoints
- ✅ Context cancellation - Graceful query cancellation
- ✅ Rich error types - ISO/IEC 39075 status codes
- ✅ Unicode utilities - UTF-8/UTF-16 conversion helpers
Quick Start
Basic Connection
package main
import (
"context"
"database/sql"
"log"
"time"
_ "geodedb.com/geode"
)
func main() {
// Connect to Geode server (QUIC default)
db, err := sql.Open("geode", "quic://localhost:3141?insecure_tls_skip_verify=true")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Or connect via gRPC
// db, err := sql.Open("geode", "grpc://localhost:50051?tls=0")
// Configure connection pool
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Execute a query
rows, err := db.QueryContext(ctx, "MATCH (n:Person) RETURN n.name LIMIT 10")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
log.Fatal(err)
}
log.Println(name)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
}
DSN Format
Note: See the official DSN specification for complete details.
The Data Source Name (DSN) supports multiple formats:
quic://host:port?options # QUIC transport (recommended)
grpc://host:port?options # gRPC transport
host:port?options # Scheme-less defaults to QUIC
host:port
host
DSN Examples
// Development (skip TLS verification)
db, _ := sql.Open("geode", "quic://localhost:3141?insecure_tls_skip_verify=true")
// Production with CA certificate
db, _ := sql.Open("geode", "quic://localhost:3141?ca=/path/to/ca.crt")
// Mutual TLS (mTLS)
db, _ := sql.Open("geode", "quic://localhost:3141?ca=/path/to/ca.crt&cert=/path/to/client.crt&key=/path/to/client.key")
// Custom page size
db, _ := sql.Open("geode", "quic://localhost:3141?page_size=5000")
DSN Options
| Option | Description | Default |
|---|---|---|
page_size | Results page size | 1000 |
hello_name | Client name for HELLO | geode-go |
hello_ver | Client version for HELLO | 0.1 |
conformance | GQL conformance level | min |
ca | Path to CA certificate | |
cert | Path to client certificate (mTLS) | |
key | Path to client key (mTLS) | |
insecure_tls_skip_verify | Skip TLS verification (testing only) | false |
tls | Enable/disable TLS (gRPC only) | true |
user / username | Authentication username | |
pass / password | Authentication password |
Environment Variables
| Variable | Description |
|---|---|
GEODE_HOST | Default host |
GEODE_PORT | Default port |
GEODE_TLS_CA | Default CA certificate path |
GEODE_TRANSPORT | Default transport (quic or grpc) |
GEODE_USERNAME | Default username |
GEODE_PASSWORD | Default password |
Querying
Simple Queries
ctx := context.Background()
// Execute query returning rows
rows, err := db.QueryContext(ctx, "MATCH (n:Person) RETURN n.name, n.age")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var name string
var age int
if err := rows.Scan(&name, &age); err != nil {
log.Fatal(err)
}
fmt.Printf("%s is %d years old\n", name, age)
}
Parameterized Queries
// Using ? placeholders
rows, err := db.QueryContext(ctx,
"MATCH (p:Person {name: ?}) RETURN p.age",
"Alice")
// Multiple parameters
rows, err := db.QueryContext(ctx,
"MATCH (p:Person) WHERE p.age > ? AND p.city = ? RETURN p",
30, "Seattle")
Executing Non-Query Statements
// Create node
result, err := db.ExecContext(ctx,
"CREATE (p:Person {name: ?, age: ?})",
"Bob", 25)
if err != nil {
log.Fatal(err)
}
rowsAffected, _ := result.RowsAffected()
fmt.Printf("Created %d node(s)\n", rowsAffected)
Query with Timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "MATCH (n) RETURN n LIMIT 1000000")
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
log.Println("Query timed out")
}
log.Fatal(err)
}
Prepared Statements
Prepared statements improve performance for frequently executed queries:
// Prepare statement
stmt, err := db.PrepareContext(ctx, "MATCH (n:Person {name: ?}) RETURN n.age")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
// Execute multiple times
for _, name := range []string{"Alice", "Bob", "Charlie"} {
var age int
err := stmt.QueryRowContext(ctx, name).Scan(&age)
if err != nil {
log.Printf("No age found for %s\n", name)
continue
}
fmt.Printf("%s is %d years old\n", name, age)
}
Transactions
Basic Transactions
// Begin transaction
tx, err := db.BeginTx(ctx, nil)
if err != nil {
log.Fatal(err)
}
// Execute operations
_, err = tx.ExecContext(ctx, "CREATE (n:Person {name: 'Alice', age: 30})")
if err != nil {
tx.Rollback()
log.Fatal(err)
}
_, err = tx.ExecContext(ctx, "CREATE (n:Person {name: 'Bob', age: 25})")
if err != nil {
tx.Rollback()
log.Fatal(err)
}
// Commit transaction
if err := tx.Commit(); err != nil {
log.Fatal(err)
}
Transaction Isolation Levels
import "database/sql"
// Serializable isolation
tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelSerializable,
ReadOnly: false,
})
// Read-only transaction
tx, err := db.BeginTx(ctx, &sql.TxOptions{
ReadOnly: true,
})
Savepoints
tx, _ := db.BeginTx(ctx, nil)
// Create initial data
tx.ExecContext(ctx, "CREATE (n:Person {name: 'Alice', age: 30})")
// Create savepoint
_, err := tx.ExecContext(ctx, "SAVEPOINT before_update")
// Make changes
tx.ExecContext(ctx, "MATCH (n:Person {name: 'Alice'}) SET n.age = 40")
// Rollback to savepoint
_, err = tx.ExecContext(ctx, "ROLLBACK TO before_update")
// Alice's age is still 30
tx.Commit()
Error Handling
The driver provides rich error types with diagnostic information:
import (
"errors"
"geodedb.com/geode"
)
rows, err := db.QueryContext(ctx, "INVALID QUERY")
if err != nil {
var derr *geode.DriverError
if errors.As(err, &derr) {
log.Printf("Code: %s", derr.Code)
log.Printf("Message: %s", derr.Message)
log.Printf("Diagnostic: %s", derr.Diagnostic)
if derr.IsRetryable() {
log.Println("Error is retryable")
}
}
}
Error Types
- DriverError - Server errors with ISO/IEC 39075 status codes
- TransportError - Network/QUIC failures
- ConfigError - DSN parsing errors
- SecurityError - TLS/validation failures
- StateError - Invalid connection state transitions
Common Error Codes
| Code | Description |
|---|---|
00000 | Success |
42000 | Syntax error |
42001 | Undefined object |
42002 | Duplicate object |
22000 | Data exception |
23000 | Integrity constraint violation |
Connection Pooling
Configure the connection pool for optimal performance:
// Maximum open connections
db.SetMaxOpenConns(100)
// Maximum idle connections
db.SetMaxIdleConns(25)
// Maximum connection lifetime
db.SetConnMaxLifetime(5 * time.Minute)
// Maximum idle time
db.SetConnMaxIdleTime(1 * time.Minute)
Guidelines:
- Read-heavy: Higher
MaxOpenConns(100-200) - Write-heavy: Lower
MaxOpenConns(10-50) - Mixed: Medium
MaxOpenConns(50-100)
Unicode Utilities
Helper functions for Unicode conversion:
import "geodedb.com/geode"
// UTF-8 to UTF-16
utf16 := geode.Utf8ToUtf16("Hello 🌍")
// UTF-16 to UTF-8
utf8 := geode.Utf16ToUtf8(utf16)
// WTF-8 lossy decode (replaces invalid sequences with U+FFFD)
safe := geode.Wtf8Lossy([]byte{0x61, 0x80, 0x62})
Testing
Unit Tests (No Server Required)
go test -v -short ./...
Integration Tests with Docker
# Automatic Docker integration tests
go test -v -tags=integration ./...
# With custom Geode image
GEODE_IMAGE="myregistry/geode:tag" go test -v -tags=integration ./...
Integration Tests with Manual Server
# Start Geode server
./geode serve --listen 0.0.0.0:3141
# Run tests
GEODE_TEST_DSN="localhost:3141?insecure_tls_skip_verify=true" go test -v ./...
Benchmarks
# Unit benchmarks (no server required)
go test -bench=. -benchmem ./...
# Docker-based benchmarks
go test -tags=integration -bench=BenchmarkDocker -benchmem ./...
Fuzzing
# Fuzz DSN parsing
go test -fuzz=FuzzParseDSN -fuzztime=30s
# Fuzz protocol frame parsing
go test -fuzz=FuzzParseFrame -fuzztime=30s
Examples
Social Network Query
rows, err := db.QueryContext(ctx, `
MATCH (person:Person {name: ?})-[:KNOWS*1..2]->(friend:Person)
WHERE friend <> person
RETURN DISTINCT friend.name AS name, friend.age AS age
ORDER BY friend.age DESC
LIMIT 10
`, "Alice")
for rows.Next() {
var name string
var age int
rows.Scan(&name, &age)
fmt.Printf("%s (age %d)\n", name, age)
}
Aggregation Query
rows, err := db.QueryContext(ctx, `
MATCH (p:Person)
RETURN p.city AS city, count(*) AS population
GROUP BY p.city
ORDER BY population DESC
LIMIT 5
`)
for rows.Next() {
var city string
var population int
rows.Scan(&city, &population)
fmt.Printf("%s: %d people\n", city, population)
}
Batch Insert
tx, _ := db.BeginTx(ctx, nil)
stmt, _ := tx.PrepareContext(ctx, "CREATE (p:Person {name: ?, age: ?})")
defer stmt.Close()
people := []struct{ name string; age int }{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
for _, p := range people {
_, err := stmt.ExecContext(ctx, p.name, p.age)
if err != nil {
tx.Rollback()
log.Fatal(err)
}
}
tx.Commit()
Performance Tips
- Use connection pooling with appropriate limits
- Reuse prepared statements for repeated queries
- Batch operations in transactions
- Use context timeouts to prevent hanging queries
- Close rows/statements to prevent resource leaks
- Monitor pool metrics with
db.Stats()
Troubleshooting
Connection Refused
Ensure Geode server is running:
./geode serve --listen 0.0.0.0:3141
TLS Verification Errors
For development, skip verification:
db, _ := sql.Open("geode", "localhost:3141?insecure_tls_skip_verify=true")
For production, provide CA certificate:
db, _ := sql.Open("geode", "localhost:3141?ca=/path/to/ca.crt")
Connection Pool Exhaustion
Monitor pool stats:
stats := db.Stats()
fmt.Printf("Open: %d, Idle: %d, InUse: %d\n",
stats.OpenConnections,
stats.Idle,
stats.InUse)
Increase pool size if needed:
db.SetMaxOpenConns(200)
Next Steps
- GQL Reference - Query language guide
- Transaction Patterns - Advanced transaction features
- Error Codes - Complete error reference
- Performance Tuning - Optimize performance