Skip to content

Mental Model

A short orientation before the Quick Start. Five sentences and you have the right shape in your head for everything that follows.

Documents and collections

A document is a Go struct you define. Den serializes it to JSON (via the standard encoding/json rules) and stores it in a SQL table called a collection — one collection per document type, one row per document. The actual storage is a JSONB column plus a small set of metadata columns Den manages, so you get the schema-flexibility of a document store with the durability and tooling of SQLite or PostgreSQL underneath.

type Note struct {
    document.Base                       // ID, CreatedAt, UpdatedAt, Rev — required
    Title string `json:"title"`         // serialized as the "title" key
    Body  string `json:"body"`
}

ID is a 26-character ULID auto-generated on first save; CreatedAt and UpdatedAt are stamped automatically; Rev stays empty unless you opt your type into revision tracking for optimistic-concurrency conflicts.

Save: one verb for insert and update

There is no separate Insert and Update at the top level — den.Save(ctx, db, doc) looks at the document's ID and branches: empty ID → insert (a ULID is generated, BeforeInsert hooks fire), non-empty ID → update (revision check, BeforeUpdate hooks). The same rule applies to SaveAll for batches. Read-modify-write becomes FindByID → mutate → Save. For atomic single-field updates without the read, use NewQuery[T](db, …).UpdateOne(ctx, fields) — see CRUD Operations.

Registration

Before any operation, every type must be registered once with the DB. Registration creates the collection (table) and any secondary indexes the struct's den: tags request. It's idempotent — safe to call on every startup; missing tables get created, existing ones are left alone.

db, _ := den.OpenURL(ctx, "sqlite:///notes.db", den.WithTypes(&Note{}))
// or, after Open:
den.Register(ctx, db, &Note{})

If you query an unregistered type, the operation returns ErrNotRegistered with a message that names the type and tells you which Register call to add. There is no auto-discovery — explicit registration is the Go-idiomatic choice.

document.Base reserves a small set of underscore-prefixed JSON keys (_id, _created_at, _updated_at, _rev) for its standard fields, with document.SoftDelete adding _deleted_at and friends. The Go-side fields keep natural names (doc.ID, doc.CreatedAt); the underscore form only appears when you reference the field by JSON name in where.Field, Sort, or SetFields. Use the den.FieldID, den.FieldCreatedAt, … constants instead of typing the strings — refactor-safe and IDE-discoverable.

Two struct tags

Tag Job
json Serialization. Sets the field's key in JSONB. Standard Go semantics.
den Den-specific metadata. Indexes, uniqueness, full-text search, omitempty — never a field name. The omitempty on the den tag controls index behavior (skip the index when the field is zero), not JSON serialization.
type Note struct {
    document.Base
    Title string   `json:"title"   den:"index"`         // indexed for fast lookup/sort
    Slug  string   `json:"slug"    den:"unique"`        // unique constraint
    Body  string   `json:"body"    den:"fts"`           // full-text search
    Tags  []string `json:"tags"    den:"index,omitempty"`
}

The den: tag also appears on GroupBy().Into() and Project() target structs with a different value set (count, sum:price, from:foo.bar, …). The full inventory lives in Struct Tags Reference.

Backends

The same code runs against either SQLite or PostgreSQL. The choice happens at OpenURL time:

den.OpenURL(ctx, "sqlite:///notes.db")                   // embedded, single binary
den.OpenURL(ctx, "postgres://user:pass@host/db")         // server-based, scales out

Every CRUD, query, transaction, and aggregation works the same on both. Backend-specific features (PostgreSQL GIN indexes, SQLite FTS5) sit behind the same Go API; you don't write SQL.

What's next

That's the whole mental model. From here:

  • Quick Start — define a type, insert, query, iterate
  • Documents — composable embeds for soft-delete, change tracking, attachments
  • Backends — DSN formats, when to pick which