Quick Start¶
This guide walks through a complete working example: defining a document, storing it, and querying it back.
Create a Project¶
Define a Document¶
Every document struct embeds document.Base, which provides ID, Rev, CreatedAt, and UpdatedAt fields. Use json tags for field names and den tags for index metadata.
type Product struct {
document.Base
Name string `json:"name" den:"index"`
Price float64 `json:"price" den:"index"`
}
Register before use
Every type must be registered — either via den.Register(ctx, db, ...) after Open or via den.WithTypes(...) during Open — before any Save, query, or other operation. Registration creates the backing collection and indexes; unregistered types return ErrNotRegistered. Registration is idempotent: calling Register on every startup is safe — it creates missing tables and indexes and is a no-op for ones already in place.
Save handles both insert and update
den.Save(ctx, db, doc) inspects the document's ID. Empty ID → insert path (a ULID is generated, BeforeInsert hooks fire). Non-empty ID → update path (revision check, BeforeUpdate hooks). One call, two branches; the same applies to SaveAll for batches.
About Base.ID
document.Base.ID is a 26-character ULID stored as a string. ULIDs are sortable in chronological order, so WHERE id > ? cursor pagination over _id produces the natural insert order without a separate timestamp column.
Full Example¶
package main
import (
"context"
"fmt"
"log"
"github.com/oliverandrich/den"
_ "github.com/oliverandrich/den/backend/sqlite" // register sqlite:// scheme
"github.com/oliverandrich/den/document"
"github.com/oliverandrich/den/where"
)
type Product struct {
document.Base
Name string `json:"name" den:"index"`
Price float64 `json:"price" den:"index"`
}
func main() {
ctx := context.Background()
// Open a SQLite database
db, err := den.OpenURL(ctx, "sqlite:///products.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Register document types — creates collections and indexes
if err := den.Register(ctx, db, &Product{}); err != nil {
log.Fatal(err)
}
// Insert a document
p := &Product{Name: "Widget", Price: 9.99}
if err := den.Save(ctx, db, p); err != nil {
log.Fatal(err)
}
fmt.Printf("Inserted: %s (ID: %s)\n", p.Name, p.ID)
// Query with conditions. den.Asc and den.Desc are the sort-direction
// constants accepted by Sort.
products, err := den.NewQuery[Product](db,
where.Field("price").Lt(20.0),
).Sort("name", den.Asc).All(ctx)
if err != nil {
log.Fatal(err)
}
for _, prod := range products {
fmt.Printf(" %s — $%.2f\n", prod.Name, prod.Price)
}
// Iterate (streaming, memory-efficient)
for doc, err := range den.NewQuery[Product](db).Iter(ctx) {
if err != nil {
log.Fatal(err)
}
fmt.Printf(" %s\n", doc.Name)
}
}
One-Expression Setup¶
den.WithTypes(...) registers document types at Open time so the whole setup reads as a single expression. Registration errors abort OpenURL and surface as its return value.
db, err := den.OpenURL(ctx, "sqlite:///products.db", den.WithTypes(&Product{}))
if err != nil {
log.Fatal(err)
}
defer db.Close()
Use explicit den.Register(ctx, db, ...) (as in the full example above) when you need a different context for registration than for Open, or when types become known only after Open.
Switching to PostgreSQL¶
Change the import and the DSN — the rest of your code stays identical.
import _ "github.com/oliverandrich/den/backend/postgres" // instead of sqlite
db, err := den.OpenURL(ctx, "postgres://user:pass@localhost/mydb")
Same API, different engine
Every Den operation — Save, NewQuery, Delete, RunInTransaction — works the same on both backends. Choose SQLite for embedded single-binary deployments and PostgreSQL when you need replication or scale.
Next Steps¶
- Backends — DSN formats, comparison, and when to use which
- Documents — Base types, struct tags, and lifecycle hooks
- API Reference — Complete API overview