Documentation
¶
Overview ¶
Package engine is the den ODM implementation: DB and Tx, the chainable QuerySet, CRUD entry points, link hydration, lifecycle hooks, revision tracking, and soft-delete handling. Backend implementations under den/backend implement Backend and are registered via RegisterBackend.
Most application code should import github.com/oliverandrich/den instead. The den root re-exports every type and function this package exports as aliases or one-line wrappers, so `den.QuerySet[T]` IS `engine.QuerySet[T]` (same type, same method set). Import engine directly only when you need a type or function the root does not surface — for example when building admin tooling, custom backends, or maintenance scripts.
Index ¶
- Constants
- Variables
- func AdvisoryLock(ctx context.Context, tx *Tx, key int64) error
- func Collections(db *DB) []string
- func Delete[T any](ctx context.Context, s Scope, document *T, opts ...CRUDOption) error
- func DeleteAll[T any](ctx context.Context, s Scope, docs []*T, opts ...CRUDOption) error
- func FetchAllLinks[T any](ctx context.Context, s Scope, doc *T) error
- func FetchLink[T any](ctx context.Context, s Scope, doc *T, fieldName string) error
- func FetchLinkField[T any](ctx context.Context, s Scope, link *Link[T]) error
- func FindByID[T any](ctx context.Context, s Scope, id string, opts ...CRUDOption) (*T, error)
- func FindByIDs[T any](ctx context.Context, s Scope, ids []string, opts ...CRUDOption) ([]*T, error)
- func GetChanges[T any](db *DB, doc *T) (map[string]FieldChange, error)
- func IsChanged[T any](db *DB, doc *T) (bool, error)
- func LockByID[T any](ctx context.Context, tx *Tx, id string, opts ...LockOption) (*T, error)
- func Marshal(v any) ([]byte, error)
- func NewID() string
- func PreserveServerFields[T any](db *DB, dst, src *T) error
- func Refresh[T any](ctx context.Context, s Scope, document *T, opts ...CRUDOption) error
- func RefreshAll[T any](ctx context.Context, s Scope, docs []*T, opts ...CRUDOption) error
- func Register(ctx context.Context, db *DB, types ...document.Document) error
- func RegisterBackend(scheme string, opener func(ctx context.Context, dsn string) (Backend, error))
- func Replace[T any](ctx context.Context, s Scope, fresh *T, opts ...CRUDOption) error
- func Revert[T any](db *DB, doc *T) error
- func RunInTransaction(ctx context.Context, db *DB, fn func(tx *Tx) error) error
- func Save[T any](ctx context.Context, s Scope, document *T, opts ...CRUDOption) error
- func SaveAll[T any](ctx context.Context, s Scope, docs []*T, opts ...CRUDOption) error
- type AfterDeleter
- type AfterInserter
- type AfterSaver
- type AfterSoftDeleter
- type AfterUpdater
- type AggregateOp
- type Backend
- type BeforeDeleter
- type BeforeInserter
- type BeforeSaver
- type BeforeSoftDeleter
- type BeforeUpdater
- type CRUDOption
- type CollectionMeta
- type DB
- type DanglingLinkError
- type DenSettable
- type DropStaleOption
- type DropStaleResult
- type FTSProvider
- type FTSSearcher
- type FieldChange
- type FieldMeta
- type GroupByAgg
- type GroupByBuilder
- type GroupByRow
- type GroupBySortEntry
- type IndexDefinition
- type Iterator
- type Link
- type LinkFieldMeta
- type LinkRule
- type LockMode
- type LockOption
- type Option
- type Query
- type QuerySet
- func (qs QuerySet[T]) After(id string) QuerySet[T]
- func (qs QuerySet[T]) All(ctx context.Context) ([]*T, error)
- func (qs QuerySet[T]) AllWithCount(ctx context.Context) ([]*T, int64, error)
- func (qs QuerySet[T]) Avg(ctx context.Context, field string) (float64, error)
- func (qs QuerySet[T]) BackLinks(fieldName string, targetID string) QuerySet[T]
- func (qs QuerySet[T]) Before(id string) QuerySet[T]
- func (qs QuerySet[T]) Count(ctx context.Context) (int64, error)
- func (qs QuerySet[T]) Delete(ctx context.Context, opts ...CRUDOption) (int64, error)
- func (qs QuerySet[T]) Exists(ctx context.Context) (bool, error)
- func (qs QuerySet[T]) First(ctx context.Context) (*T, error)
- func (qs QuerySet[T]) ForUpdate(opts ...LockOption) QuerySet[T]
- func (qs QuerySet[T]) GetOrCreate(ctx context.Context, defaults *T) (*T, bool, error)
- func (qs QuerySet[T]) GroupBy(fields ...string) GroupByBuilder[T]
- func (qs QuerySet[T]) IncludeDeleted() QuerySet[T]
- func (qs QuerySet[T]) Iter(ctx context.Context) iter.Seq2[*T, error]
- func (qs QuerySet[T]) Limit(n int) QuerySet[T]
- func (qs QuerySet[T]) Max(ctx context.Context, field string) (float64, error)
- func (qs QuerySet[T]) Min(ctx context.Context, field string) (float64, error)
- func (qs QuerySet[T]) Project(ctx context.Context, target any) error
- func (qs QuerySet[T]) Search(ctx context.Context, term string) ([]*T, error)
- func (qs QuerySet[T]) SearchRaw(ctx context.Context, term string) ([]*T, error)
- func (qs QuerySet[T]) Skip(n int) QuerySet[T]
- func (qs QuerySet[T]) Sort(field string, dir SortDirection) QuerySet[T]
- func (qs QuerySet[T]) Sum(ctx context.Context, field string) (float64, error)
- func (qs QuerySet[T]) Update(ctx context.Context, fields SetFields) (int64, error)
- func (qs QuerySet[T]) UpdateOne(ctx context.Context, fields SetFields) (*T, error)
- func (qs QuerySet[T]) UpsertOne(ctx context.Context, defaults *T, fields SetFields) (*T, bool, error)
- func (qs QuerySet[T]) Where(conditions ...where.Condition) QuerySet[T]
- func (qs QuerySet[T]) WithFetchLinks(fields ...string) QuerySet[T]
- func (qs QuerySet[T]) WithNestingDepth(depth int) QuerySet[T]
- func (qs QuerySet[T]) WithoutFetchLinks() QuerySet[T]
- type ReadWriter
- type RecordedIndex
- type Scope
- type SeekableStorage
- type SetFields
- type Settings
- type SortDirection
- type SortEntry
- type StaleIndex
- type Storage
- type Transaction
- type Tx
- type Validator
Constants ¶
const ( Asc = backend.Asc Desc = backend.Desc OpSum = backend.OpSum OpAvg = backend.OpAvg OpMin = backend.OpMin OpMax = backend.OpMax OpCount = backend.OpCount LockDefault = backend.LockDefault LockSkipLocked = backend.LockSkipLocked LockNoWait = backend.LockNoWait )
const ( // FieldID is the document.Base.ID JSON field name. Maps to a // 26-character ULID string; sortable chronologically. FieldID = "_id" // FieldCreatedAt is the document.Base.CreatedAt JSON field name. // Set on Insert, never touched afterwards. FieldCreatedAt = "_created_at" // FieldUpdatedAt is the document.Base.UpdatedAt JSON field name. // Refreshed by Insert and Update. FieldUpdatedAt = "_updated_at" // FieldRev is the document.Base.Rev JSON field name. Present // only when the type opts into revision tracking via // DenSettings().UseRevision; absent (omitempty) otherwise. FieldRev = "_rev" // FieldDeletedAt is the document.SoftDelete.DeletedAt JSON field // name. Available only on types that embed document.SoftDelete. // Default queries auto-filter rows where this is non-nil; opt // back in via QuerySet.IncludeDeleted or den.IncludeDeleted as // a CRUDOption. FieldDeletedAt = "_deleted_at" // FieldDeletedBy is the document.SoftDelete.DeletedBy JSON field // name. Optional audit value populated via the SoftDeleteBy // CRUDOption on the soft-delete path. FieldDeletedBy = "_deleted_by" // FieldDeleteReason is the document.SoftDelete.DeleteReason JSON // field name. Optional audit value populated via the // SoftDeleteReason CRUDOption on the soft-delete path. FieldDeleteReason = "_delete_reason" )
Reserved JSON field names that Den's standard embeds (document.Base and document.SoftDelete) install on every registered type. The underscore prefix namespaces these away from user-defined fields and matches the MongoDB convention.
Use the constants whenever you need the JSON name in code that takes a string — `where.Field`, `Sort`, `SetFields`, `After` / `Before`, `Project`'s `den:"from:..."` tag — so a refactor stays compile-safe instead of relying on string literals scattered across the codebase.
The Go-side struct fields (Base.ID, Base.CreatedAt, …) keep their natural names; only the JSON tag (and therefore the SQL column access path) uses the underscore form. Storage is independent of these constants — renaming would be a breaking storage change, not a source rename.
Variables ¶
var ( ErrNotFound = errors.New("den: document not found") ErrMultipleMatches = errors.New("den: more than one document matched") ErrDuplicate = errors.New("den: duplicate key") ErrRevisionConflict = errors.New("den: revision conflict") ErrNotRegistered = errors.New("den: document type not registered") ErrValidation = errors.New("den: validation failed") ErrTransactionFailed = errors.New("den: transaction failed") ErrNoSnapshot = errors.New("den: no snapshot — document was never loaded from database") ErrMigrationFailed = errors.New("den: migration failed") ErrLocked = errors.New("den: row is locked by another transaction") ErrDeadlock = errors.New("den: deadlock detected") ErrSerialization = errors.New("den: serialization failure") ErrFTSNotSupported = errors.New("den: backend does not support full-text search") // ErrLockRequiresTransaction is returned when a terminal method runs on a // QuerySet whose ForUpdate was set but whose scope is a *DB. Row locking // is only meaningful inside a transaction because the lock is released // when the enclosing statement commits. ErrLockRequiresTransaction = errors.New("den: ForUpdate requires a transaction scope (*Tx)") // ErrIncompatiblePagination is returned by terminal QuerySet methods when // the caller mixed cursor pagination (After/Before) with offset pagination // (Skip). The two styles have no defined interaction — pick one. ErrIncompatiblePagination = errors.New("den: cursor pagination (After/Before) cannot be combined with offset pagination (Skip)") // ErrUnsupportedScheme is returned by OpenURL when no backend opener is // registered for the DSN's scheme — typically because the caller forgot // the side-effect import (e.g. `_ "github.com/oliverandrich/den/backend/sqlite"`). // Wrapped with the actual scheme via fmt.Errorf so callers can use // errors.Is to detect this case without scraping error strings. ErrUnsupportedScheme = errors.New("den: unsupported database scheme") )
Functions ¶
func AdvisoryLock ¶
AdvisoryLock acquires an application-defined lock on key that persists until the transaction commits or rolls back. Concurrent transactions attempting to lock the same key block until the holder ends. See the Transaction interface for backend-specific behavior.
func Collections ¶
Collections returns the names of all registered collections in sorted order.
func Delete ¶
Delete removes a document from the database. Options: WithLinkRule to cascade deletes to linked documents.
func DeleteAll ¶
DeleteAll removes every doc in docs by routing each through Delete. All deletes run inside a single transaction when bound to a *DB; when bound to a *Tx they run inline in the caller's transaction. Fail-fast: any per-doc error rolls back the batch.
Empty input is a no-op.
func FetchAllLinks ¶
FetchAllLinks resolves the direct link fields on doc — single-level, the loaded targets' own links stay untouched. See FetchLink for the scope semantics. The eager / lazy tag on each field is ignored; calling FetchAllLinks is itself the explicit ask for hydration.
For transitive hydration use a QuerySet terminal (All / AllWithCount / First / Search), which honors WithNestingDepth. Internally this routes through the same batched resolver — a one-element batch — so depth recursion is available; FetchAllLinks fixes it at one hop because the API has no place to thread a depth knob.
func FetchLink ¶
FetchLink resolves a single named link field on a document. The scope parameter accepts either a *DB (read from the backend directly) or a *Tx (read from the enclosing transaction).
The fieldName is the JSON tag on the parent's link field. Renaming the JSON tag silently breaks every FetchLink call against this collection. Prefer FetchLinkField when you can pass the typed link pointer directly — it's compile-checked.
func FetchLinkField ¶
FetchLinkField resolves the link by typed pointer instead of a stringly-named field on the parent. Use it when you have the Link[T] in hand directly — refactor-safe and immune to JSON-tag renames on the parent struct.
No-op when the link's ID is empty (cascade-write input) or when Loaded is already true (idempotent — matches FetchLink).
func FindByID ¶
FindByID retrieves a document by its ID. Returns ErrNotFound if no row matches.
`den:"eager"`-tagged link fields on T are hydrated by default; pass WithoutFetchLinks to suppress hydration. Soft-deleted documents are returned: explicit-by-ID lookups bypass the soft-delete filter that QuerySet read terminals apply on filtered queries — callers can check Value.IsDeleted() to react.
Top-level shorthand for `NewQuery[T](s).Where(where.Field("_id").Eq(id)).IncludeDeleted().First(ctx)` — discoverable next to Save / Delete / Refresh.
func FindByIDs ¶
FindByIDs retrieves multiple documents by their IDs in a single query. Missing IDs are silently skipped. Order is not guaranteed.
`den:"eager"`-tagged link fields on T are batch-resolved by default; pass WithoutFetchLinks to suppress hydration. Soft-deleted documents are returned (see FindByID for the rationale).
Top-level shorthand for `NewQuery[T](s).Where(where.Field("_id").In(ids...)).IncludeDeleted().All(ctx)`.
func GetChanges ¶
func GetChanges[T any](db *DB, doc *T) (map[string]FieldChange, error)
GetChanges returns a map of field names to their before/after values for all fields that changed since the document was loaded. Returns nil if nothing changed or no snapshot exists.
func IsChanged ¶
IsChanged reports whether the document has changed since it was loaded. Returns false if the document has no snapshot (never loaded or not Trackable).
func LockByID ¶
LockByID retrieves a document by ID and acquires a row-level lock that persists for the lifetime of the transaction. Without options, concurrent transactions attempting to lock the same row block until this transaction commits or rolls back. Pass SkipLocked or NoWait to change that behavior.
On PostgreSQL this maps to SELECT ... FOR UPDATE; on SQLite it is a no-op because IMMEDIATE transactions already serialize writers.
The *Tx parameter enforces transaction scope at compile time — a lock outside a transaction releases immediately and would be meaningless. Returns ErrNotFound if the document does not exist. Returns ErrLocked when NoWait is set and the row is held by another transaction.
func Marshal ¶ added in v0.17.0
Marshal is Den's JSON marshaller for output: it behaves exactly like encoding/json.Marshal, except that any hydrated (Loaded) Link[T] / []Link[T] anywhere in the value graph is emitted as its resolved Value object instead of the bare id. Unloaded links, and every non-link field, marshal identically to the standard library.
This is additive — the default wire format produced by json.Marshal (and therefore Den's storage encoding) is unchanged; only Marshal opts into expansion. Hydrate the links you want expanded with QuerySet.WithFetchLinks (optionally naming specific fields), then call Marshal for the response: the set of loaded links is exactly the set that expands.
Note: objects that actually contain an expanded link are re-encoded from a map, so their JSON keys come out in sorted order rather than struct- declaration order (JSON objects are unordered). Values with no loaded link are returned byte-for-byte as json.Marshal would produce them.
func NewID ¶
func NewID() string
NewID generates a new ULID-format document ID — 26-character Crockford base32, lexicographically sortable, strictly monotonic within the same millisecond.
func PreserveServerFields ¶ added in v0.17.0
PreserveServerFields copies Den's server-owned fields (_id, _created_at, _updated_at, _rev, and the soft-delete audit fields _deleted_at / _deleted_by / _delete_reason) from src onto dst, leaving every client-owned field untouched. The transient document.Tracked snapshot is not copied — Save recaptures it.
It is the building block behind Replace; reach for it directly when you load and persist the existing record yourself. Returns ErrNotRegistered if T was never registered.
func Refresh ¶
Refresh re-reads a document from the database by its ID, overwriting all fields on the provided struct.
`den:"eager"`-tagged link fields on T are hydrated by default; pass WithoutFetchLinks to suppress hydration.
func RefreshAll ¶
RefreshAll re-reads every doc in docs by routing each through Refresh. All refreshes run inside a single transaction when bound to a *DB; when bound to a *Tx they run inline in the caller's transaction. Fail-fast: any per-doc error rolls back the batch.
Empty input is a no-op.
func Register ¶
Register analyzes the given document types and registers their collections with the database. Must be called before any CRUD operations.
func RegisterBackend ¶
RegisterBackend registers a backend opener for a URL scheme. The opener receives the context supplied to OpenURL so that expensive setup work (dialing, metadata table creation) can honor deadlines and cancellation. Called by backend packages in their init() functions.
The scheme is normalized to lowercase so registration and lookup stay case-insensitive, matching URL-scheme semantics: "sqlite", "SQLite", and "SQLITE" all address the same backend.
Panics if scheme is empty, opener is nil, or a different opener is already registered for scheme — mirrors storage.Register semantics. Duplicate registrations surface mis-wiring (two backend packages claiming the same scheme, a replace-directive fork, or a manual call after a side-effect import) at process startup instead of at first lookup.
func Replace ¶ added in v0.17.0
Replace performs a full-content replace (PUT semantics): the client-owned fields of fresh overwrite the stored row — fields omitted from fresh reset to their zero value — while Den's server-owned identity, audit, and soft-delete fields are preserved from the existing record (see PreserveServerFields).
Replace is last-writer-wins: it adopts the stored _rev, so a revisioned type round-trips without conflict and Save bumps the revision. For optimistic concurrency, load the document, mutate it, and Save it directly instead. Replace does NOT resurrect soft-deleted documents — replacing a soft-deleted row leaves it soft-deleted.
The load and the save run in one transaction (a new one when bound to a *DB, the caller's when bound to a *Tx). Fires the update hook chain (BeforeUpdate/BeforeSave → AfterUpdate/AfterSave). Returns ErrNotFound if no row matches fresh's _id, and ErrValidation if fresh has no _id.
func Revert ¶
Revert restores the document to its state at load time by decoding the stored snapshot back over its fields. Returns ErrNoSnapshot if the document was never loaded from the database or does not embed document.Tracked.
Named Revert rather than Rollback to avoid name collision with the backend transaction's Rollback method — this operation is purely an in-memory restore against the document snapshot and has nothing to do with transactions.
func RunInTransaction ¶
RunInTransaction executes fn within a transaction. If fn returns nil, the transaction is committed. If fn returns an error, the transaction is rolled back.
The *Tx passed to fn does not itself carry the context; entry points inside fn take ctx explicitly. Use the ctx closed over from the caller.
func Save ¶
Save inserts the document if its ID is empty, otherwise updates it. The single doc-in-hand persistence entry point: callers don't pick branches, Save inspects the ID and routes accordingly.
Empty-ID docs follow the insert path (ULID assigned, BeforeInsert hooks fire). ID-bearing docs follow the update path (revision check, BeforeUpdate hooks fire). Exactly one branch runs.
Options pass through to whichever underlying path runs.
func SaveAll ¶
SaveAll persists every doc in docs by routing each through Save: empty-ID docs take the Insert path, ID-bearing docs take the Update path. Mixed batches are supported — every doc gets the right branch.
All saves run inside a single transaction when bound to a *DB; when bound to a *Tx they run inline in the caller's transaction. Fail-fast: any per-doc error rolls back the batch.
Empty input is a no-op (returns nil without opening a transaction).
Types ¶
type AfterDeleter ¶
AfterDeleter fires after any deletion completes — both soft and hard. See BeforeDeleter for the full hook ordering on each path.
type AfterInserter ¶
type AfterSaver ¶
type AfterSoftDeleter ¶
AfterSoftDeleter fires only on the soft-delete path — after the write, before AfterDelete. HardDelete() bypasses this hook. See BeforeDeleter for the full hook ordering.
type AfterUpdater ¶
type AggregateOp ¶
type AggregateOp = backend.AggregateOp
type BeforeDeleter ¶
BeforeDeleter fires before any deletion — both soft and hard. The hook runs before the soft-delete flip OR the physical row removal, whichever the call resolves to. Use BeforeSoftDeleter for soft-only logic.
Ordering on the soft path: BeforeDelete → BeforeSoftDelete → [write] → AfterSoftDelete → AfterDelete.
Ordering on the hard path (HardDelete() option, or no SoftDelete embed): BeforeDelete → [write] → AfterDelete; the soft-only hooks are skipped.
type BeforeInserter ¶
type BeforeSaver ¶
type BeforeSoftDeleter ¶
BeforeSoftDeleter fires only on the soft-delete path — after BeforeDelete, before the write. HardDelete() bypasses this hook. Use it for audit-log side effects that should not fire on permanent deletion.
Full ordering: BeforeDelete → BeforeSoftDelete → [write] → AfterSoftDelete → AfterDelete.
type BeforeUpdater ¶
type CRUDOption ¶
type CRUDOption func(*crudOpts)
CRUDOption configures CRUD operations.
func HardDelete ¶
func HardDelete() CRUDOption
HardDelete returns a CRUDOption that makes Delete permanently remove a document from storage, bypassing soft-delete. Hooks and link cascade are still applied. Compose with other CRUDOptions such as WithLinkRule:
den.Delete(ctx, db, doc, den.HardDelete()) den.Delete(ctx, db, doc, den.HardDelete(), den.WithLinkRule(den.LinkDelete))
func IgnoreRevision ¶
func IgnoreRevision() CRUDOption
IgnoreRevision returns a CRUDOption that skips revision checking.
func IncludeDeleted ¶
func IncludeDeleted() CRUDOption
IncludeDeleted returns a CRUDOption that makes lookup-style operations consider soft-deleted documents. Currently honored by FindOneAndUpdate and FindOneAndUpsert: without it, soft-deleted matches are skipped (Upsert then inserts a fresh document); with it, the soft-deleted document is updated in place and DeletedAt is left untouched.
Mirrors the QuerySet.IncludeDeleted modifier so the same name covers both query-driven reads and CRUD-style lookups.
func SoftDeleteBy ¶
func SoftDeleteBy(actor string) CRUDOption
SoftDeleteBy returns a CRUDOption that records an actor identifier (user ID, service name, etc.) on the document's DeletedBy field during a soft-delete. Silently ignored on the hard-delete path or on documents that do not embed document.SoftDelete — there is nowhere to store the value.
func SoftDeleteReason ¶
func SoftDeleteReason(reason string) CRUDOption
SoftDeleteReason returns a CRUDOption that records a free-form reason on the document's DeleteReason field during a soft-delete. Silently ignored on the hard-delete path or on documents that do not embed document.SoftDelete.
func WithLinkRule ¶
func WithLinkRule(rule LinkRule) CRUDOption
WithLinkRule sets the link cascading rule for Save / Delete and the QuerySet write terminals.
func WithoutFetchLinks ¶
func WithoutFetchLinks() CRUDOption
WithoutFetchLinks suppresses link hydration on a doc-in-hand read, including fields tagged `den:"eager"`. Mirrors the QuerySet modifier of the same name; honored by FindByID, FindByIDs, and Refresh. On a type with no eager-tagged links it's a no-op.
type CollectionMeta ¶
type CollectionMeta = backend.CollectionMeta
type DB ¶
type DB struct {
// contains filtered or unexported fields
}
DB is the main entry point for Den operations. It wraps a Backend and holds the collection registry.
func Open ¶
Open creates a new DB using the given backend directly. The context governs any registration work triggered by WithTypes (collection table creation, index provisioning); callers with long-running startup work can pass a timeout or cancellable context to abort it cleanly.
Use OpenURL for URL-based opening with automatic backend selection.
func OpenURL ¶
OpenURL opens a database connection using a URL-style DSN. The context governs the backend's connection setup (metadata table creation, server version checks) and any registration work triggered by WithTypes.
Supported schemes depend on which backend packages are imported:
- sqlite:///path/to/db — import _ "github.com/oliverandrich/den/backend/sqlite"
- sqlite://:memory: — SQLite in-memory database
- postgres://user:pass@host:5432/db — import _ "github.com/oliverandrich/den/backend/postgres"
- postgresql://user:pass@host/db — PostgreSQL (alias)
Backend packages register themselves automatically via init().
func (*DB) Backend ¶
Backend returns the underlying backend. Useful for advanced use cases or backend-specific type assertions.
type DanglingLinkError ¶
DanglingLinkError describes a Link[T] whose ID does not resolve to any row in the target collection. Returned by the batched link-resolver when a parent references a deleted or never-existed target. Wraps ErrNotFound so callers can keep the simple `errors.Is(err, ErrNotFound)` check, but also exposes Collection and ID for callers that need to surface "which link broke" without parsing the error message.
func (*DanglingLinkError) Error ¶
func (e *DanglingLinkError) Error() string
func (*DanglingLinkError) Unwrap ¶
func (e *DanglingLinkError) Unwrap() error
type DenSettable ¶
type DenSettable interface {
DenSettings() Settings
}
DenSettable is implemented by document types that provide custom settings.
type DropStaleOption ¶
type DropStaleOption = maintenance.Option
func DryRun ¶
func DryRun() DropStaleOption
DryRun causes DropStaleIndexes to report the indexes that would be dropped without actually dropping them. Thin wrapper over maintenance.DryRun.
type DropStaleResult ¶
type DropStaleResult = maintenance.DropStaleResult
func DropStaleIndexes ¶
func DropStaleIndexes(ctx context.Context, db *DB, opts ...DropStaleOption) (DropStaleResult, error)
DropStaleIndexes removes indexes previously created by Register() that no longer correspond to a registered IndexDefinition. Managed indexes (for example the PostgreSQL GIN index, FTS triggers, or tables) are not tracked and therefore cannot be dropped by this function.
Typically invoked from a migration or deployment script after a struct has changed. Pass DryRun() to inspect what would be dropped without making changes.
type FTSProvider ¶
type FTSProvider = search.FTSProvider
type FTSSearcher ¶
type FTSSearcher = search.FTSSearcher
type FieldChange ¶
FieldChange holds the before and after values for a changed field.
type GroupByAgg ¶
type GroupByAgg = backend.GroupByAgg
type GroupByBuilder ¶
type GroupByBuilder[T any] struct { // contains filtered or unexported fields }
GroupByBuilder allows specifying group-by fields. The builder is typically obtained from QuerySet.GroupBy.
func (GroupByBuilder[T]) Into ¶
func (gb GroupByBuilder[T]) Into(ctx context.Context, target any) error
Into executes the group-by aggregation and maps results into the target slice. The query is pushed down to the database as a SQL GROUP BY statement.
func (GroupByBuilder[T]) OrderByAgg ¶
func (gb GroupByBuilder[T]) OrderByAgg(op AggregateOp, field string, dir SortDirection) GroupByBuilder[T]
OrderByAgg appends an ORDER BY entry that sorts grouped results by an aggregate expression. Op selects the aggregate column; field names its source field (ignored for OpCount, which sorts by COUNT(*)). Multiple calls define tie-breakers in the order they were added.
To order by a group key, use the ordinary QuerySet.Sort chain on the underlying query set — Sort fields that match a group key translate to ORDER BY the group-key expression. Sort fields that are neither a group key nor an aggregate error out at Into.
type GroupByRow ¶
type GroupByRow = backend.GroupByRow
type GroupBySortEntry ¶
type GroupBySortEntry = backend.GroupBySortEntry
type IndexDefinition ¶
type IndexDefinition = backend.IndexDefinition
type Link ¶
Link represents a reference to a document in another collection. Only the ID is persisted; Value is populated on fetch.
func NewLink ¶
NewLink creates a Link from an existing document, extracting its ID from the embedded document.Base.
The doc must contain a document.Base anywhere in its struct tree — directly embedded (the standard pattern), embedded via a wrapper, or even as a named field. NewLink panics if no document.Base is found, because a Link without an ID is silently broken downstream and always indicates a programmer error.
An empty Base.ID (i.e. the doc has not been inserted yet) is fine and expected on the LinkWrite cascade path — the cascaded Insert will populate the ID and propagate it back into the parent's Link.
func (Link[T]) MarshalJSON ¶
MarshalJSON serializes the link as a JSON string (the ID).
Symmetric fast path to UnmarshalJSON: ULID-shaped IDs need no escaping, so re-entering the JSON encoder is wasted work. The byte- for-byte contract with json.Marshal(l.ID) is preserved — anything that would force an escape falls through.
func (*Link[T]) UnmarshalJSON ¶
UnmarshalJSON deserializes a JSON string into the link.
Link bodies in persisted JSON are almost always escape-free IDs (ULIDs are pure ASCII). The fast path takes the quoted bytes directly instead of re-entering the JSON decoder for what is structurally one string field — the round-trip dominated alloc profiles on read paths with many Link fields per row. Anything with escapes or unusual shape falls through to json.Unmarshal so the error and unescaping contract stays identical.
type LinkFieldMeta ¶ added in v0.17.0
type LinkFieldMeta = backend.LinkFieldMeta
func LinkFields ¶ added in v0.17.0
func LinkFields[T any](db *DB) ([]LinkFieldMeta, error)
LinkFields enumerates the Link[T] / []Link[T] relation fields of the registered document type T. Use it to validate or allowlist expandable relations (e.g. an `?expand=` API) without re-implementing reflection over Link[T].
Returns ErrNotRegistered if T is not registered. A link whose target type is itself unregistered is still reported, with an empty TargetCollection (the field is valid; only its collection name is unknown).
type LinkRule ¶
type LinkRule int
LinkRule controls cascading behavior for write and delete operations.
LinkDelete cascades a Delete to the immediate link targets only — it does not recurse into the targets' own links. Callers that need transitive cleanup must walk the graph themselves. This keeps a mis-configured delete from wiping an unbounded subgraph.
type LockOption ¶
LockOption configures a lock acquisition (LockByID, QuerySet.ForUpdate).
func NoWait ¶
func NoWait() LockOption
NoWait makes a lock acquisition return ErrLocked immediately if another transaction already holds the row lock, instead of blocking. Maps to PostgreSQL's FOR UPDATE NOWAIT; on SQLite this option is a no-op. Passing both SkipLocked and NoWait is an error — they are mutually exclusive in PostgreSQL. Thin wrapper over lock.NoWait.
func SkipLocked ¶
func SkipLocked() LockOption
SkipLocked makes a lock acquisition return ErrNotFound immediately if another transaction already holds the row lock, instead of blocking. Maps to PostgreSQL's FOR UPDATE SKIP LOCKED; on SQLite this option is a no-op. Passing both SkipLocked and NoWait is an error — they are mutually exclusive in PostgreSQL. Thin wrapper over lock.SkipLocked.
type Option ¶
type Option func(*DB)
Option configures a DB during Open.
func WithStorage ¶
WithStorage installs a Storage on the DB. Storage is DB-scoped — all document types that embed or contain document.Attachment use the same backend. Install at Open:
fs, err := file.New("./uploads", "/media")
// handle err
db, err := den.OpenURL(ctx, dsn, den.WithStorage(fs))
Without a Storage, Den refuses to hard-delete documents that carry attachments — orphan bytes are worse than a clear error.
func WithTypes ¶
WithTypes queues document types to be registered at the end of Open. Equivalent to calling Register(ctx, db, types...) after Open returns, where ctx is the same context passed to Open / OpenURL — there is no silent context.Background() substitution. Lets the whole setup read as a single expression:
db, err := den.OpenURL(ctx, dsn, den.WithTypes(&Note{}, &Tag{}))
Registration runs after every other Option has been applied. Any registration error aborts Open and is surfaced as its error.
type QuerySet ¶
type QuerySet[T any] struct { // contains filtered or unexported fields }
QuerySet is a lazy, immutable query builder. Chain methods return copies; the query is only executed when a terminal method (All, First, Count, etc.) is called.
QuerySet binds to a Scope — either a *DB (operating outside a transaction) or a *Tx (operating inside RunInTransaction). Row-level locking via ForUpdate is only valid on a *Tx scope; calling it on a *DB-bound QuerySet defers an error that surfaces on the terminal method.
The zero value is not usable — always obtain a QuerySet via NewQuery. Calling terminal methods on a zero-value QuerySet panics because the scope reference is nil.
func NewQuery ¶
NewQuery creates a new QuerySet bound to the given scope. Conditions can optionally be passed directly. The context is supplied later when a terminal method (All, First, Iter, …) runs, so the same QuerySet can be executed against different contexts.
Pass a *DB for queries outside a transaction, or a *Tx from within a RunInTransaction closure for a query that sees the transaction's view of the data. Use ForUpdate only on a *Tx-bound QuerySet.
func (QuerySet[T]) After ¶
After sets the cursor for forward pagination.
Cannot be combined with Skip (offset pagination) — terminal methods return ErrIncompatiblePagination when both styles are set.
func (QuerySet[T]) All ¶
All executes the query and returns all matching documents.
With WithFetchLinks enabled, All drains the result set first and then resolves every link field in batched IN-queries (one per target type per nesting level) instead of the per-row Get that streaming .Iter() does. For N parents sharing a small set of linked targets this collapses N round-trips into one — at the cost of buffering the full result set, which is already implicit in .All()'s contract. Callers who need true streaming with link resolution should keep using .Iter().
func (QuerySet[T]) AllWithCount ¶
AllWithCount returns matching documents and the total unpaginated count.
When the QuerySet is bound to a *DB, the count+query run in a read transaction for consistency. When bound to a *Tx, they run through the existing transaction and no nested tx is opened.
func (QuerySet[T]) Avg ¶
Avg returns the average of the given field across matching documents.
Scalar aggregates ignore Limit, Skip, Sort, After, and Before — they always operate on the full WHERE-filtered set.
func (QuerySet[T]) BackLinks ¶
BackLinks adds a WHERE filter that matches documents whose link field references the given target ID. Equivalent to Where(where.Field(fieldName).Eq(targetID)) but reads more clearly when the intent is a backwards-link lookup. Chainable: combine with Sort / Limit / etc as needed, then call a terminal like All / First / Count.
houses, err := den.NewQuery[House](db).BackLinks("door", doorID).All(ctx)
func (QuerySet[T]) Before ¶
Before sets the cursor for backward pagination.
Cannot be combined with Skip (offset pagination) — terminal methods return ErrIncompatiblePagination when both styles are set.
func (QuerySet[T]) Count ¶
Count returns the number of matching documents.
Limit, Skip, and Sort are ignored — Count always operates on the full WHERE-filtered set. After / Before cursor modifiers are honored.
func (QuerySet[T]) Delete ¶
Delete removes every document matching the QuerySet's conditions and returns the number of rows affected. The scan + writes run in one transaction (or inline in the caller's tx when bound to a *Tx); each chunk is drained from the iterator before its writes fire, which is required on PostgreSQL because pgx pins the connection while a cursor is open and an in-loop write would surface "conn busy". The loop repeats LIMIT-bounded chunks until the match set is empty, so memory stays bounded regardless of total match-set size.
Per-row hooks fire: BeforeDelete, then on the soft path BeforeSoftDelete → AfterSoftDelete → AfterDelete, on the hard path just AfterDelete. Soft-delete docs route through the soft path unless HardDelete() is passed via opts.
Fail-fast: a per-row error stops the loop, rolls back the transaction, and returns (0, err). No partial commit.
Skip is honoured on the first chunk only — subsequent chunks see fresh LIMIT windows over the still-matching rows. After/Before cursor pagination is honoured every chunk because deleted rows naturally drop out of the next chunk's match set.
func (QuerySet[T]) Exists ¶
Exists returns true if at least one document matches.
Limit, Skip, and Sort are ignored — the backend emits its own LIMIT 1 internally. After / Before cursor modifiers are honored.
func (QuerySet[T]) First ¶
First returns the first matching document. Returns ErrNotFound if none match.
func (QuerySet[T]) ForUpdate ¶
func (qs QuerySet[T]) ForUpdate(opts ...LockOption) QuerySet[T]
ForUpdate acquires a row-level lock on every matching document, held until the enclosing transaction commits or rolls back. Only valid on a QuerySet bound to a *Tx — on a *DB-bound QuerySet the call is accepted but the terminal method will return ErrLockRequiresTransaction.
Pass SkipLocked to omit locked rows from the result set (queue-consumer pattern) or NoWait to fail immediately with ErrLocked when a row is held by another transaction. On SQLite these options are no-ops because IMMEDIATE transactions already serialize writers.
Passing both SkipLocked and NoWait is a programmer error (PG allows only one); ForUpdate captures the error on the query set and surfaces it when a terminal method runs.
func (QuerySet[T]) GetOrCreate ¶
GetOrCreate is the fetch-this-row-or-insert-defaults shorthand: returns the existing document if conditions match exactly one row, otherwise inserts defaults as a new row. Existing rows are not modified.
Equivalent to UpsertOne(ctx, defaults, SetFields{}); reach for it when the typical "fetch by unique key, create with defaults if missing, leave the rest alone" pattern doesn't need post-find field updates.
func (QuerySet[T]) GroupBy ¶
func (qs QuerySet[T]) GroupBy(fields ...string) GroupByBuilder[T]
GroupBy starts a group-by aggregation on one or more fields.
The target struct passed to Into must carry one field tagged `den:"group_key:N"` for each field listed here, with N running 0..len(fields)-1. The legacy unindexed `den:"group_key"` is accepted when exactly one field is requested and is treated as slot 0; mixing the unindexed form with positional tags returns an error.
func (QuerySet[T]) IncludeDeleted ¶
IncludeDeleted includes soft-deleted documents in the results.
func (QuerySet[T]) Iter ¶
Iter returns an iterator over matching documents for use with range. Documents are streamed one at a time via the backend's Iterator, not collected in memory.
for doc, err := range den.NewQuery[Product](db).Iter(ctx) {
if err != nil { return err }
fmt.Println(doc.Name)
}
Cancelling ctx stops the iteration: the per-row prologue checks ctx.Err() and surfaces it through the seq2 error path, so at most one further document is yielded to the consumer after cancellation. With WithFetchLinks, an in-flight link fetch may still complete its current backend round-trip before the next prologue check fires; the link resolver passes ctx through, so the round-trip after that observes the cancellation.
func (QuerySet[T]) Limit ¶
Limit sets the maximum number of results.
Honored by the same row-returning terminals as Sort, plus GroupBy.Into (caps the number of group rows returned): All, AllWithCount (data slice only; the count path runs unpaginated), First (which rewrites Limit to 1 internally), Iter, Search, Update, Project, and GroupBy.Into. Ignored by Count, Exists, and scalar aggregates — those always operate on the full WHERE-filtered set.
func (QuerySet[T]) Max ¶
Max returns the maximum value of the given field across matching documents. See Avg for the modifier-applicability rules.
func (QuerySet[T]) Min ¶
Min returns the minimum value of the given field across matching documents. See Avg for the modifier-applicability rules.
func (QuerySet[T]) Project ¶
Project executes the query and decodes results into the projection type. Target must be a pointer to a slice of structs with json/den tags.
func (QuerySet[T]) Search ¶
Search performs a literal-terms full-text search on the QuerySet: the raw term is treated as a set of words ANDed together, with FTS5 operators and punctuation neutralised (via search.LiteralFTS5), giving plainto_tsquery- like semantics on every backend. This makes raw user input safe to pass straight through on both SQLite and PostgreSQL. A blank/whitespace-only term returns no rows without touching the backend.
For raw backend-native query syntax (FTS5 query expressions on SQLite) use QuerySet.SearchRaw. Both honor the QuerySet's scope identically.
func (QuerySet[T]) SearchRaw ¶ added in v0.17.0
SearchRaw performs a full-text search passing the term straight to the backend's native FTS query mechanism. On SQLite this is an FTS5 query expression (operators, column filters, prefix * all honored — and raw user input unsafe); on PostgreSQL it is still plainto_tsquery, which normalises the term and ignores operators, so the "raw" extra power is effectively SQLite-only. Most callers want QuerySet.Search, which neutralises operators for safe user-supplied input identically on both backends.
func (QuerySet[T]) Skip ¶
Skip sets the number of results to skip (offset pagination).
Honored by the same terminals as Limit (including GroupBy.Into). Ignored by Count, Exists, and scalar aggregates.
Cannot be combined with After or Before (cursor pagination) — terminal methods return ErrIncompatiblePagination when both styles are set.
func (QuerySet[T]) Sort ¶
func (qs QuerySet[T]) Sort(field string, dir SortDirection) QuerySet[T]
Sort adds a sort criterion. Multiple calls define tie-breakers.
Honored by terminals that return ordered rows: All, AllWithCount, First, Iter, Search, Update, and Project. On GroupBy.Into, Sort is honored when the referenced field matches a group key; a non-key field returns an error — use GroupByBuilder.OrderByAgg for aggregate ordering. Ignored by Count, Exists, and the scalar aggregates (Avg / Sum / Min / Max) — those operate on unordered sets where sort order has no effect on the result.
func (QuerySet[T]) Sum ¶
Sum returns the sum of the given field across matching documents. See Avg for the modifier-applicability rules.
func (QuerySet[T]) Update ¶
Update applies field updates to every matching document. Returns the number of updated documents.
When bound to a *DB, the scan + writes run in a new transaction so the batch is atomic. When bound to a *Tx, they run inline in the caller's transaction — a per-row failure rolls back the caller's transaction too.
Update is fail-fast: any per-row error (BeforeUpdate hook, validation, revision conflict, backend write) stops the loop, rolls back the transaction, and returns (0, err). There is no partial commit; no AfterUpdate / AfterSave hooks fire for rows that would have come after the failure.
Field names in fields (as they appear in the `json` struct tag) are validated against the registered struct before the write transaction opens — an unknown name returns immediately without opening the tx. Callers that want to validate field names at application start can iterate Meta[T].Fields.
WithFetchLinks and WithNestingDepth have no effect on Update. The loaded docs are loop-local and discarded after the per-row write, so resolving links would only be visible to BeforeUpdate / Validate hooks — Update keeps that path lean and Link.Value remains unresolved (nil). Hooks that need linked data should call FetchLink or FetchAllLinks themselves.
func (QuerySet[T]) UpdateOne ¶
UpdateOne atomically finds the single document matching the QuerySet's conditions, applies fields, and returns the updated document hydrated per the QuerySet's fetchMode / nestDepth settings.
Conditions must identify the document uniquely: returns ErrMultipleMatches if more than one matches, ErrNotFound if none match. Field names are validated against the registered struct before the transaction opens.
Sort / Limit / Skip / After / Before are ignored — UpdateOne is single-doc-strict-by-conditions. IncludeDeleted is honored.
func (QuerySet[T]) UpsertOne ¶
func (qs QuerySet[T]) UpsertOne(ctx context.Context, defaults *T, fields SetFields) (*T, bool, error)
UpsertOne atomically finds the single document matching the QuerySet's conditions and applies fields, or inserts a new document built from defaults with fields applied on top. The second return value reports which path ran: true means a new document was inserted, false means an existing one was updated.
Conditions must identify the document uniquely: ErrMultipleMatches is returned if more than one matches. Concurrent upserts on the same missing row rely on a unique constraint to fail one of the inserters with ErrDuplicate — there is no internal retry, and no row lock is taken on the lookup.
On the miss path, defaults is mutated by the insert path (ID, CreatedAt, UpdatedAt are populated) and returned as the result. Callers reusing a shared defaults template across upserts should pass a fresh value each call — a stale ID would otherwise be carried into the next insert.
Hooks follow the standard Insert / Update order. Exactly one path runs. IncludeDeleted is honored on the lookup; Sort/Limit/Skip/After/ Before are ignored.
func (QuerySet[T]) WithFetchLinks ¶
WithFetchLinks hydrates Link[T] fields on the returned documents.
Called with no arguments it hydrates every link field, regardless of the `den:"eager"` tag. Called with one or more JSON field names it hydrates only those named top-level link fields, leaving the rest unloaded — useful for an `?expand=` API that resolves only the relations the client asked for. Names are matched as the field's JSON key (falling back to the lowercased Go name for untagged fields). Unknown names are ignored.
Honored only by terminals that return *T values: All, AllWithCount, First, Iter, and Search. Every other terminal — counts, aggregates, projections, GroupBy.Into, and bulk Update — ignores it because it has no documents to attach the resolved links to. See Update's godoc for the hook-visibility caveat that follows from this rule.
func (QuerySet[T]) WithNestingDepth ¶
WithNestingDepth caps recursive link resolution. Meaningful for any query that hydrates links — `den:"eager"`-tagged fields under the default mode, or every link field under WithFetchLinks.
Honored uniformly by every terminal that returns *T values: All, AllWithCount, First, Search, and Iter all route through the same batched resolver, which recurses level-by-level up to depth. Terminals that don't return *T values (Count, Exists, scalar aggregates, GroupBy.Into, bulk Update) ignore the setting because they have no documents to attach resolved links to.
depth <= 0 disables link hydration entirely, regardless of fetchMode. Prefer WithoutFetchLinks if that's the intent.
func (QuerySet[T]) WithoutFetchLinks ¶
WithoutFetchLinks suppresses link hydration on this query, including fields tagged `den:"eager"`. Use it when the eager tags would otherwise pay a per-link round-trip cost the caller does not need (bulk export, IDs-only sweep, count-by-link). Returned `Link[T]` values carry their ID but `Value` stays `nil`.
type ReadWriter ¶
type ReadWriter = backend.ReadWriter
type RecordedIndex ¶
type RecordedIndex = backend.RecordedIndex
type Scope ¶
type Scope interface {
// contains filtered or unexported methods
}
Scope is the common parameter type for every CRUD entry point that works both outside and inside a transaction. It is sealed to *DB and *Tx — the gateway methods are unexported so external types cannot implement it, and callers can only obtain a Scope by passing one of the two concrete types.
The idiom mirrors the implicit DBTX pattern used around database/sql (where *sql.DB and *sql.Tx share the query surface) but is explicit here so the compiler can document and enforce which operations accept either.
type SeekableStorage ¶
type SeekableStorage = storage.SeekableStorage
type SetFields ¶
SetFields is a map of field names (as they appear in the `json` struct tag) to new values for partial updates via QuerySet.UpdateOne, QuerySet.UpsertOne, and QuerySet.Update.
Names are validated against the registered struct before the write transaction opens; an unknown name aborts the call without touching storage. Callers that want to validate names at application start can iterate Meta[T].Fields and compare against a known set.
type Settings ¶
type Settings struct {
CollectionName string
UseRevision bool
Indexes []IndexDefinition
}
Settings configures per-collection behavior.
type SortDirection ¶
type SortDirection = backend.SortDirection
type StaleIndex ¶
type StaleIndex = maintenance.StaleIndex
type Transaction ¶
type Transaction = backend.Transaction
type Tx ¶
type Tx struct {
// contains filtered or unexported fields
}
Tx wraps a backend Transaction for use in RunInTransaction.
The zero value is not usable — construct a Tx only indirectly by passing a closure to RunInTransaction. Calling transaction-scoped functions on a zero-value Tx panics.
func (*Tx) Transaction ¶
func (t *Tx) Transaction() Transaction
Transaction returns the underlying backend Transaction so infrastructure code can issue raw Get / Put / Delete calls on unregistered collections. This is a low-level escape hatch — normal code should use Insert, Update, Delete, FindByID, NewQuery, and friends, all of which honor the registry, encoding, validation, and hook contracts. The only legitimate consumer today is den/migrate (the migration-log collection is deliberately not registered with Den).
Mirrors DB.Backend() in spirit: both are low-level accessors you reach for only when the high-level API does not cover the case.
type Validator ¶
Validator is the custom-validation hook. Implement it on a document to enforce invariants beyond what struct tag validation can express. Returning an error rolls back the surrounding Insert / Update without touching storage.
The passed ctx is the same one threaded through the surrounding Insert / Update call — use it for cancellation, deadlines, DB lookups inside the validator, outbound HTTP calls that need to participate in the request, or tracing spans. Matches the signature of every other Den hook.
Source Files
¶
- aggregate.go
- aggregate_project.go
- aggregate_scalar.go
- aliases.go
- attachments.go
- collection.go
- crud.go
- crud_delete.go
- crud_find.go
- crud_refresh.go
- crud_save.go
- crud_setfields.go
- den.go
- errors.go
- fields.go
- hooks.go
- iter.go
- link.go
- link_batch.go
- link_cascade.go
- link_fetch.go
- link_introspect.go
- link_options.go
- link_reflect.go
- marshal.go
- queryset.go
- replace.go
- revision.go
- scope.go
- search.go
- settings.go
- soft_delete.go
- stale_indexes.go
- storage.go
- track.go
- tx.go
- url.go