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/sql driver - 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

OptionDescriptionDefault
page_sizeResults page size1000
hello_nameClient name for HELLOgeode-go
hello_verClient version for HELLO0.1
conformanceGQL conformance levelmin
caPath to CA certificate
certPath to client certificate (mTLS)
keyPath to client key (mTLS)
insecure_tls_skip_verifySkip TLS verification (testing only)false
tlsEnable/disable TLS (gRPC only)true
user / usernameAuthentication username
pass / passwordAuthentication password

Environment Variables

VariableDescription
GEODE_HOSTDefault host
GEODE_PORTDefault port
GEODE_TLS_CADefault CA certificate path
GEODE_TRANSPORTDefault transport (quic or grpc)
GEODE_USERNAMEDefault username
GEODE_PASSWORDDefault 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

CodeDescription
00000Success
42000Syntax error
42001Undefined object
42002Duplicate object
22000Data exception
23000Integrity 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

  1. Use connection pooling with appropriate limits
  2. Reuse prepared statements for repeated queries
  3. Batch operations in transactions
  4. Use context timeouts to prevent hanging queries
  5. Close rows/statements to prevent resource leaks
  6. 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