Hanzo

Database Drivers

SQLite, ZAP binary protocol (PostgreSQL, MongoDB, Redis, ClickHouse), and custom driver support.

Database Drivers

This page covers drivers for Hanzo ORM v0.3.0. See the main ORM docs for the full changelog.

Hanzo ORM uses a two-layer interface design. The orm.DB interface is minimal (what Model[T] needs), while db.DB is the full driver interface. The adapter layer bridges them.

Backend Overview

BackendConstructorTransportUse Case
SQLiteorm.OpenSQLiteEmbeddedSingle-node, development, edge
PostgreSQLorm.OpenZap + ZapSQLZAP binaryProduction SQL
MongoDB/FerretDBorm.OpenZap + ZapDocumentDBZAP binaryDocument storage
Redis/Valkeyorm.OpenZap + ZapKVZAP binaryKV storage
ClickHouseorm.OpenZap + ZapDatastoreZAP binaryAnalytics/columnar
Customorm.AdaptDBAnyBring your own driver

SQLite

The built-in SQLite driver uses WAL mode, JSON entity storage, and json_extract() for filtering.

Setup

import (
    "github.com/hanzoai/orm"
    ormdb "github.com/hanzoai/orm/db"
)

db, err := orm.OpenSQLite(&ormdb.SQLiteDBConfig{
    Path: "data/app.db",
    Config: ormdb.SQLiteConfig{
        BusyTimeout: 5000,
        JournalMode: "WAL",
    },
})
if err != nil {
    log.Fatal(err)
}

Configuration

FieldDefaultDescription
PathrequiredPath to the SQLite database file
BusyTimeout5000Milliseconds to wait for locks
JournalMode"WAL"WAL for concurrent reads; DELETE for simpler recovery

Storage Schema

All entities are stored in a single _entities table:

CREATE TABLE _entities (
    id         TEXT PRIMARY KEY,
    kind       TEXT NOT NULL,
    parent_id  TEXT,
    data       JSON NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    deleted    BOOLEAN DEFAULT 0
);

CREATE INDEX idx_entities_kind ON _entities(kind);
CREATE INDEX idx_entities_parent ON _entities(parent_id);

How Queries Work

Filters on struct fields translate to json_extract() calls:

// Go code
q.Filter("Status=", "active").Filter("Credits>", 0)
-- Generated SQL
SELECT data FROM _entities
WHERE kind = 'user'
  AND json_extract(data, '$.status') = ?
  AND COALESCE(json_extract(data, '$.credits'), 0) > ?
  AND deleted = 0

Field names auto-convert: Status (Go PascalCase) → $.status (JSON camelCase).

Boolean and zero-value fields use COALESCE to handle NULL correctly.

Write Concurrency

SQLite uses a single write connection protected by a mutex, ensuring serialized writes. Read connections are separate, allowing concurrent reads even during writes (WAL mode).

┌──────────────┐
│  Write Conn  │ ← mutex-protected, one write at a time
├──────────────┤
│  Read Conn   │ ← concurrent reads via WAL
│  Read Conn   │
│  Read Conn   │
└──────────────┘

Transactions

err := db.RunInTransaction(func(tx orm.DB) {
    user := orm.New[User](tx)
    user.Name = "Alice"
    user.Create()

    account := orm.New[Account](tx)
    account.UserID = user.Id()
    account.Balance = 10000
    account.Create()
})

Transactions use the write connection and hold the write mutex for the duration.

Batch Operations

// Allocate multiple IDs at once
keys, err := db.AllocateIDs("user", 100)

sqlite-vec Support

The SQLite driver is compatible with sqlite-vec for vector similarity search. Load the extension before opening the database if your build includes it.

ZAP Binary Protocol

ZAP eliminates JSON serialization overhead by encoding structs directly into a binary format over RPC. A zap-sidecar process proxies requests to the actual database backend.

┌──────────┐    ZAP binary    ┌──────────────┐    native    ┌────────────┐
│  Your App │ ──────────────▸ │  zap-sidecar  │ ──────────▸ │  Database   │
│  (ORM)    │ ◂────────────── │  (proxy)      │ ◂────────── │  Backend    │
└──────────┘                  └──────────────┘              └────────────┘

Setup

import (
    "github.com/hanzoai/orm"
    ormdb "github.com/hanzoai/orm/db"
)

// PostgreSQL via ZAP
db, err := orm.OpenZap(&ormdb.ZapConfig{
    Addr:    "localhost:9651",
    Backend: ormdb.ZapSQL,
})

// MongoDB via ZAP
db, err := orm.OpenZap(&ormdb.ZapConfig{
    Addr:    "localhost:9651",
    Backend: ormdb.ZapDocumentDB,
})

// Redis/Valkey via ZAP
db, err := orm.OpenZap(&ormdb.ZapConfig{
    Addr:    "localhost:9651",
    Backend: ormdb.ZapKV,
})

// ClickHouse via ZAP
db, err := orm.OpenZap(&ormdb.ZapConfig{
    Addr:    "localhost:9651",
    Backend: ormdb.ZapDatastore,
})

Configuration

FieldDefaultDescription
AddrrequiredAddress of the zap-sidecar (e.g., "localhost:9651")
BackendZapSQLTarget backend: ZapSQL, ZapDocumentDB, ZapKV, ZapDatastore
Database""Target database name (SQL/DocumentDB)
CollectionautoEntity table/collection name (_entities for SQL, entities for DocumentDB, orm for KV)
NodeIDautoClient node ID for ZAP peer identification
QueryTimeout30sPer-query timeout
Loggerslog.Default()Logger for ZAP node

How It Works

The ZAP driver translates ORM operations into backend-specific requests:

SQL backends (PostgreSQL via ZapSQL):

  • GetSELECT data FROM _entities WHERE id = $1 AND kind = $2
  • PutINSERT ... ON CONFLICT DO UPDATE
  • QuerySELECT with json_extract filters
  • Delete → Soft delete (UPDATE SET deleted = true)

Document backends (MongoDB via ZapDocumentDB):

  • Getfind with {_id, kind} filter
  • Putupdate with $set (upsert) or insert
  • Queryfind with MongoDB query operators
  • Deleteupdate with {deleted: true}

KV backends (Redis/Valkey via ZapKV):

  • GetGET orm:kind:id
  • PutSET orm:kind:id
  • DeleteDEL orm:kind:id

Benefits

  • Zero JSON overhead: Binary encoding instead of json.Marshal/json.Unmarshal for transport
  • Unified interface: Same orm.DB across SQL, document, KV, and columnar backends
  • Native types: Complex Go types (slices, maps, nested structs) transmitted natively
  • Backend flexibility: Switch databases by changing one config line

When to Use ZAP vs SQLite

SQLiteZAP
DeploymentEmbedded, single binaryRequires zap-sidecar process
ScalingSingle nodeMulti-node, distributed
Use caseDev, edge, CLI toolsProduction services
PerformanceFast for small datasetsBetter for large/concurrent workloads
BackendsSQLite onlyPostgreSQL, MongoDB, Redis, ClickHouse

Custom Drivers

Implement the db.DB interface to add support for any backend:

type DB interface {
    Get(ctx context.Context, key Key, dst interface{}) error
    Put(ctx context.Context, key Key, src interface{}) (Key, error)
    Delete(ctx context.Context, key Key) error
    Query(kind string) Query
    NewKey(kind string, id string, intID int64, parent Key) Key
    NewIncompleteKey(kind string, parent Key) Key
    AllocateIDs(kind string, parent Key, n int) ([]Key, error)
    RunInTransaction(ctx context.Context, fn func(tx Transaction) error, opts *TransactionOptions) error
    Close() error
}

Then wrap it with orm.AdaptDB():

customDB := NewMyCustomDB(config)
ormDB := orm.AdaptDB(customDB)

// Now use with Model[T]
user := orm.New[User](ormDB)

Interface Layers

┌─────────────────────────────────────────────────────┐
│  orm.DB (minimal)                                    │
│  Get, Put, Delete, Query, NewKey,                   │
│  AllocateIDs, RunInTransaction, Close               │
│                                                      │
│  Used by: Model[T], New[T], Get[T]                  │
├─────────────────────────────────────────────────────┤
│  adapter.go                                          │
│  OpenSQLite() → orm.DB                               │
│  OpenZap()    → orm.DB                               │
│  AdaptDB(db.DB) → orm.DB                             │
│  bridgeKey / reverseKey wrappers                     │
├─────────────────────────────────────────────────────┤
│  db.DB (full)                                        │
│  FilterField, Run, Start/End, Cursor,               │
│  Transaction with Commit/Rollback                    │
│                                                      │
│  Implemented by: SQLiteDB, ZapDB                     │
└─────────────────────────────────────────────────────┘

How is this guide?

Last updated on

On this page