Documentation
¶
Overview ¶
Package r3 provides a universal, backend-agnostic CRUD repository abstraction for Go: the same CRUD interface over PostgreSQL, MySQL, SQLite, MongoDB, and JSON/YAML/TOML files. Business code depends on r3, never on a concrete backend.
CRUD is generic over entity type T and primary key ID, composing Querier (Get, List, Count) and Commander (Create, Update, Patch, Delete). Depend on the narrower sub-interface when that is all you need (a read-only store needs only Querier).
Queries are immutable value types — Filters, Sorts, PaginationSpec, CursorSpec, Fields, Preloads — assembled into one Query. Query.MergeWith returns a new value, so queries from different sources (defaults, request, permission scope) combine without mutation. Build filters with the short-form helpers (Eq, Gt, In, Like, Between, ...) or, for table hints and nested paths, the FieldSpec forms (F, Fop).
Schema and capabilities ¶
SchemaOf reflects an entity's struct tags into a Schema of capability-bearing [Attribute]s. Five capabilities gate what a field may do: Filterable, Sortable, Queryable, Creatable, Mutable. Defaults are permissive (a plain scalar gets all five); tags only tighten them (no-filter, no-sort, no-output, readonly, immutable, enum). The PK, created_at/updated_at, and the soft-delete column are read-only by default.
Capabilities are the public ceiling. Schema.ValidateQuery rejects an unknown or disallowed field with a typed error (ErrUnknownField, ErrFieldNotFilterable, ErrFieldNotSortable, ErrFieldNotQueryable) before any SQL is built, and the engine shapes writes to honor Creatable/Mutable so a full Update cannot clobber created_at or resurrect a soft-deleted row. created_at/updated_at are system-managed: read-only to callers but stamped with server time by the engine. The permissions feature only narrows the ceiling per-actor.
WithoutWriteGuard (on the context) and the SystemWriter wrapper are the audited system/worker door: they skip the capability check but not the structural floor (the PK and computed attributes stay unwritable), and history/metrics still record the write.
Errors and pagination ¶
Get normalizes every backend's not-found condition to ErrNotFound, so errors.Is works the same across drivers. List paginates by default (PageSizeDefault items) unless passed Unpaginated; compare the returned total against the slice length to detect truncation. Count answers "how many match?" without materializing rows (only Filters and IncludeTrashed apply).
Aggregation ¶
Aggregator is the opt-in grouped-aggregation capability: GROUP BY plus COUNT/COUNT DISTINCT/SUM/AVG/MIN/MAX over the matching records, returning AggregateRow values. Declare the shape on the Query (Query.GroupBy, Query.Aggregates, Query.Having) via GroupBy, AggCount, AggSum, AggMin, AggMax, AggAvg, AggCountDistinct, and reach it with AggregateOf. Every engine implements it and every decorator forwards it, so it survives decoration.
rows, err := r3.AggregateOf(ctx, raidRepo, r3.Query{
GroupBy: r3.GroupBy("location_id"),
Aggregates: r3.Aggregates{r3.AggCount("raids"), r3.AggMax("date", "last_raid")},
})
Query.Buckets adds closed-form time-bucket group keys (Bucket with a BucketUnit: hour/day/week/month/year) for time-series grouping ("counts per day"). A bucket truncates the field's stored wall-clock as-is (no timezone conversion; ISO-Monday weeks) and returns under its alias, the aggregation-side analog of WeekdayIn/TimeOfDayBetween. The file, Mongo, and SQL engines lower it (SQL via a per-flavor date-trunc hook); a backend without the hook returns ErrBucketNotSupported.
Relationships ¶
Relations (has-many, belongs-to, many-to-many) are declared either by struct tag (`r3:"rel:has-many,fk:city_id"`) or physically by table and column via WithRelations with HasManyRelation, BelongsToRelation, and ManyToManyRelation. The physical form lets an entity relate to a table it does not import as a Go type, avoiding domain import cycles. A declared relation supports three operations through the normal Query helpers:
- Has — relationship filter (EXISTS): owners with at least one related row matching the inner filters.
- HasNo — its negation (NOT EXISTS / anti-join), correctly including owners with a NULL foreign key.
- AggregateThroughRelation — grouped aggregation over the related rows, via RelationAggregator. Owner filters (including a Scoper's) restrict which owners fold in, and soft-deleted related rows are excluded.
Relation resolution is GORM-only today; other SQL drivers reject relationship filters and the mongo/file engines ignore them (tracked in docs/backend-parity.md).
Project layout ¶
Five layers, each depending only on the layers above:
- r3 (this package): interfaces and the immutable query model. Zero deps.
- dialects/: pure, stateless converters between r3 types and a format — data-store (sql, bson), serialization (json, yaml, toml, url, schema), and the when bridge that compiles human time vocabulary into recurring time-pattern filters. No I/O.
- engine/: complete CRUD per storage category (sql, mongo, file); reflection, query building, execution.
- drivers/: ready-to-use constructors for a specific library.
- features/: decorators (permissions, history, metrics, validation, i18n, softdelete, transactor) that wrap any r3.CRUD regardless of backend.
Features compose across backends: the same permission checker, audit log, or metrics collector works whether the repo is PostgreSQL, MongoDB, or a YAML file — the behavioral layer is written once and applied everywhere.
Index ¶
- Constants
- Variables
- func As[C any, T any, ID comparable](repo CRUD[T, ID]) (C, bool)
- func DecodeAggregateCodecs(s Schema, q Query, rows []AggregateRow) error
- func EncodeCursor(cv CursorValues) (string, error)
- func EvalTimeOfDayBetween(t time.Time, value any) (bool, error)
- func EvalWeekdayIn(t time.Time, value any) (bool, error)
- func ExtractBetweenBounds(value any) (any, any, error)
- func FieldsToStrings(fields Fields) []string
- func FinalizeCount[T any](entities []T, paginatedCount int64, isPaginated bool) ([]T, int64)
- func FinalizeCountCursor[T any](entities []T) ([]T, int64)
- func GetLocale(ctx context.Context) string
- func InTx[T any, ID comparable](ctx context.Context, repo CRUD[T, ID], fn func(tx CRUD[T, ID]) error) error
- func PatchWhereOf[T any, ID comparable](ctx context.Context, repo Commander[T, ID], filters Filters, entity T, ...) (int64, error)
- func RegisterCodec(name string, c Codec)
- func RequireCodecSupport(s Schema, backend string)
- func SupportsTx[T any, ID comparable](repo CRUD[T, ID]) bool
- func TimeOfDayBounds(value any) (int, int, error)
- func TruncateToBucket(t time.Time, u BucketUnit) time.Time
- func UpsertOf[T any, ID comparable](ctx context.Context, repo Commander[T, ID], entity T, opts ...UpsertOption) (T, error)
- func ValidateIdentifier(s string) error
- func WeekdaysValue(value any) ([]time.Weekday, error)
- func WithActor(ctx context.Context, actor Actor) context.Context
- func WithLocale(ctx context.Context, locale string) context.Context
- func WithoutWriteGuard(ctx context.Context) context.Context
- func WriteGuardBypassed(ctx context.Context) bool
- type Actor
- type AggregateFunc
- type AggregateRow
- func (r AggregateRow) Bool(key string) (bool, bool)
- func (r AggregateRow) Float64(key string) (float64, bool)
- func (r AggregateRow) Int64(key string) (int64, bool)
- func (r AggregateRow) String(key string) (string, bool)
- func (r AggregateRow) Time(key string) (time.Time, bool)
- func (r AggregateRow) Value(key string) any
- type AggregateSpec
- type Aggregates
- type Aggregator
- type Attribute
- type BucketSpec
- type BucketUnit
- type Buckets
- type BulkPatcher
- type CRUD
- type Capability
- type Codec
- type Commander
- type Config
- type CursorDirection
- type CursorSpec
- type CursorValues
- type DataType
- type Defaults
- type DefaultsConfig
- type DefaultsManager
- func (dm *DefaultsManager) GetDefaultGetQuery() Query
- func (dm *DefaultsManager) GetDefaultListQuery() Query
- func (dm *DefaultsManager) MergeGetQuery(qarg ...Query) Query
- func (dm *DefaultsManager) MergeListQuery(qarg ...Query) Query
- func (dm *DefaultsManager) SetDefaultGetQuery(q Query)
- func (dm *DefaultsManager) SetDefaultListQuery(q Query)
- type FieldSpec
- type Fields
- type FilterOperatorSpec
- type FilterSpec
- func And(filters ...*FilterSpec) *FilterSpec
- func Between(field string, lo, hi any) *FilterSpec
- func Eq(field string, value any) *FilterSpec
- func Exists(field string, value any) *FilterSpec
- func F(field *FieldSpec, value any) *FilterSpec
- func FILike(field *FieldSpec, value any) *FilterSpec
- func FLike(field *FieldSpec, value any) *FilterSpec
- func Fop(field *FieldSpec, operator FilterOperatorSpec, value any) *FilterSpec
- func Gt(field string, value any) *FilterSpec
- func Gte(field string, value any) *FilterSpec
- func Has(relation string, inner ...*FilterSpec) *FilterSpec
- func HasNo(relation string, inner ...*FilterSpec) *FilterSpec
- func ILike(field string, value any) *FilterSpec
- func In(field string, value any) *FilterSpec
- func Like(field string, value any) *FilterSpec
- func Lt(field string, value any) *FilterSpec
- func Lte(field string, value any) *FilterSpec
- func Ne(field string, value any) *FilterSpec
- func NewFilterSpec(field *FieldSpec, operator FilterOperatorSpec, value any) *FilterSpec
- func NewFilterSpecAndGroup(filters ...*FilterSpec) *FilterSpec
- func NewFilterSpecOrGroup(filters ...*FilterSpec) *FilterSpec
- func NotIn(field string, value any) *FilterSpec
- func NotLike(field string, value any) *FilterSpec
- func Or(filters ...*FilterSpec) *FilterSpec
- func TimeOfDayBetween(field string, loMin, hiMin int) *FilterSpec
- func WeekdayIn(field string, days ...time.Weekday) *FilterSpec
- type Filters
- type JSONColumn
- type NamingConfig
- type Option
- type Options
- type PaginationSpec
- func DefaultPagination() *PaginationSpec
- func NewOffsetPagination(offset int, limit ...int) *PaginationSpec
- func NewPaginationSpec(pageNum int, pageSize ...int) *PaginationSpec
- func NewPaginationSpecWithSize(pageSize int) *PaginationSpec
- func NoPagination() *PaginationSpec
- func Unpaginated() *PaginationSpec
- func (p *PaginationSpec) Clone() *PaginationSpec
- func (p *PaginationSpec) GetOffset() (int, bool)
- func (p *PaginationSpec) GetPageNum() int
- func (p *PaginationSpec) GetPageSize() int
- func (p *PaginationSpec) IsPaginated() bool
- func (p *PaginationSpec) MergeWith(other *PaginationSpec) *PaginationSpec
- func (p *PaginationSpec) String() string
- func (p *PaginationSpec) ToLimitOffset() (int, int)
- type PreloadSpec
- type Preloads
- type Querier
- type Query
- type RelationAggregator
- type RelationKind
- type RelationOption
- type RelationRef
- type RelationSpec
- type Rewrapper
- type Schema
- func (s Schema) Attributes() []Attribute
- func (s Schema) Filterable(name string) bool
- func (s Schema) IsZero() bool
- func (s Schema) Lookup(name string) (Attribute, bool)
- func (s Schema) Queryable(name string) bool
- func (s Schema) Sortable(name string) bool
- func (s Schema) ValidateAggregateQuery(q Query) error
- func (s Schema) ValidateQuery(q Query) error
- func (s Schema) With(overrides ...Attribute) Schema
- func (s Schema) Writable(name string, op WriteOp) bool
- type SchemaOption
- type SortDirection
- type SortNullsPosition
- type SortSpec
- type Sorts
- type Transactor
- type TxCRUD
- type Unwrapper
- type UpsertOption
- type UpsertSpec
- type Upserter
- type WriteOp
Constants ¶
const (
// PageSizeDefault is the package-wide default page size when none is specified.
PageSizeDefault = 100
)
Variables ¶
var ( // ErrInvalidCursor is returned when a cursor token cannot be decoded. ErrInvalidCursor = errors.New("invalid cursor token") // ErrCursorRequiresSort is returned for cursor pagination without any sort // columns: keyset paging needs a stable order to page against. ErrCursorRequiresSort = errors.New("cursor pagination requires at least one sort column") )
Sentinel errors for cursor pagination.
var ( // ErrUnknownField is returned when a referenced field is not declared by the schema. ErrUnknownField = errors.New("unknown field") // ErrFieldNotFilterable is returned when a non-filterable field appears in Query.Filters. ErrFieldNotFilterable = errors.New("field is not filterable") // ErrFieldNotSortable is returned when a non-sortable field appears in Query.Sorts. ErrFieldNotSortable = errors.New("field is not sortable") // ErrFieldNotQueryable is returned when a non-queryable field appears in Query.Fields. ErrFieldNotQueryable = errors.New("field is not queryable") )
Schema validation errors. Schema.ValidateQuery wraps the offending field name (fmt.Errorf("%w: %q", err, name)) so a consumer can surface a 400-class message instead of leaking a backend driver error (a 500) once SQL is built.
var ErrAggregateNotSupported = errors.New("r3: aggregate not supported")
ErrAggregateNotSupported is returned by AggregateOf when the repository (or a decorator in its chain) does not implement Aggregator.
var ErrBucketNotSupported = errors.New("r3: time-bucket group key not supported by this backend")
ErrBucketNotSupported is returned when a backend cannot lower a time-bucket group key (e.g. a SQL flavor with no date-truncation hook wired). It is a loud failure by design: a backend never silently returns un-bucketed rows.
var ErrBulkPatchNotSupported = errors.New("r3: bulk patch not supported")
ErrBulkPatchNotSupported is returned by PatchWhereOf when the repository (or a decorator in its chain) does not implement BulkPatcher.
var ErrCodecNotSupported = errors.New("r3: value codec not supported by this backend")
ErrCodecNotSupported wraps a construction-time panic when an entity declares a codec that the target backend does not yet apply. Silently ignoring the codec would store the un-encoded value - data corruption, not graceful degradation - so such backends panic here instead (see RequireCodecSupport).
var ErrInvalidAggregate = errors.New("r3: invalid aggregate query")
ErrInvalidAggregate is returned when an aggregate query is structurally invalid: no aggregates declared, a missing/duplicate/invalid alias, an aggregate function that requires a field called without one, an alias colliding with a group field, a Having filter referencing an undeclared alias, or SUM/AVG over an attribute the schema knows to be non-numeric.
var ErrInvalidBucket = errors.New("r3: invalid time-bucket group key")
ErrInvalidBucket is returned when a time-bucket group key (BucketSpec) is invalid: a missing/invalid field, an unknown unit, an alias colliding with a group field or aggregate, a source field the schema knows to be non-temporal, or a codec'd field (bucketing a value stored in a non-temporal form, e.g. codec:unixtime, is not yet supported - see docs/plan-aggregate-buckets.md).
var ErrInvalidIdentifier = errors.New("invalid identifier")
ErrInvalidIdentifier is returned when a field name contains characters that are not valid SQL identifiers.
var ErrInvalidPatchField = errors.New("invalid patch field")
ErrInvalidPatchField is returned by a write (Patch, or full Update SET-shaping) when a field name does not match any attribute in the schema, or names an attribute that is not mutable (e.g. PK, created_at, soft-delete, immutable).
var ErrNoPatchFields = errors.New("patch requires at least one field")
ErrNoPatchFields is returned by Patch when the Fields list is empty or nil.
var ErrNotFound = errors.New("r3: record not found")
ErrNotFound is returned by Get (and other single-record ops) when no record matches. Every backend normalizes its native "no rows"/"no documents" error to this sentinel - sql.ErrNoRows, gorm.ErrRecordNotFound, mongo.ErrNoDocuments, the file engine's internal not-found - so errors.Is detects a missing record identically across drivers.
var ErrRelationAggregateNotSupported = errors.New("relation aggregation not supported by this repository")
ErrRelationAggregateNotSupported is returned by AggregateThroughRelation when the repository does not implement RelationAggregator (relation aggregation is GORM-only today).
var ErrTransactionsNotSupported = errors.New("r3: transactions not supported by this repository")
ErrTransactionsNotSupported is returned when no layer in a repo's chain implements Transactor.
var ErrUnknownCodec = errors.New("r3: unknown codec")
ErrUnknownCodec wraps a panic from SchemaOf when a r3:"...,codec:<name>" tag names an unregistered codec. A tag typo is a deterministic developer error, so it fails loudly at derivation rather than silently leaving the field un-encoded.
var ErrUpsertNotSupported = errors.New("r3: upsert not supported")
ErrUpsertNotSupported is returned by UpsertOf when the repository (or a decorator in its chain) does not implement Upserter.
var SystemActor = Actor{ID: "", Type: "system"}
SystemActor is the actor used when the context carries none.
Functions ¶
func As ¶
func As[C any, T any, ID comparable](repo CRUD[T, ID]) (C, bool)
As finds the first layer in the decorator chain (starting at repo and following Unwrapper.Unwrap) that implements C, enabling capability detection through decorators. For example, to reach a backend's soft-delete support regardless of how many decorators wrap it:
sd, ok := r3.As[SoftDeleter[ID]](repo)
It returns the zero value of C and false if no layer implements C.
func DecodeAggregateCodecs ¶ added in v0.0.8
func DecodeAggregateCodecs(s Schema, q Query, rows []AggregateRow) error
DecodeAggregateCodecs decodes, in place, the AggregateRow values that still carry a codec'd attribute's domain meaning: a group-by column mapping to a codec'd attribute, and a MIN/MAX over a codec'd attribute (both are real field values). SUM/AVG/COUNT are never decoded — a sum of unix seconds is not a time.Time. A NULL (e.g. MIN/MAX over an empty group) is left nil so AggregateRow.Time still reports ok=false rather than the codec's zero value.
No-op when the schema declares no codecs. Codec-aware Aggregator backends call it once before returning rows, so the "which columns decode" rule lives in one place.
func EncodeCursor ¶
func EncodeCursor(cv CursorValues) (string, error)
EncodeCursor serializes cursor values into an opaque base64 token.
func EvalTimeOfDayBetween ¶ added in v0.1.5
EvalTimeOfDayBetween reports whether t's wall-clock minute-of-day falls in the TimeOfDayBetween window [lo, hi). lo > hi wraps midnight (match m >= lo OR m < hi); lo == hi matches nothing.
func EvalWeekdayIn ¶ added in v0.1.5
EvalWeekdayIn reports whether t's wall-clock weekday is in the WeekdayIn value. In-memory engines (the file engine, the permissions matcher) call it so the membership and value-normalization rules live in exactly one place.
func ExtractBetweenBounds ¶
ExtractBetweenBounds returns the low and high bounds from a between value, which must be a slice or array of exactly 2 elements.
func FieldsToStrings ¶
FieldsToStrings converts Fields to a []string of field names (nils skipped).
func FinalizeCount ¶
FinalizeCount returns (entities, totalCount): the backend's paginatedCount when pagination was active, otherwise len(entities). Shared by every engine.
func FinalizeCountCursor ¶
FinalizeCountCursor returns (entities, -1): keyset pagination has no total count.
func GetLocale ¶ added in v0.0.2
GetLocale returns the locale tag from the context, or "" if none is set.
func InTx ¶
func InTx[T any, ID comparable]( ctx context.Context, repo CRUD[T, ID], fn func(tx CRUD[T, ID]) error, ) error
InTx runs fn inside a transaction, committing if it returns nil and rolling back on error or panic. It walks repo's decorator chain to the backend Transactor, begins there, and re-applies the same decorator stack on top of the tx-bound CRUD - so the CRUD passed to fn stays fully decorated and writes inside the transaction still run every decorator. Returns ErrTransactionsNotSupported if no layer in the chain implements Transactor.
Example:
err := r3.InTx(ctx, userRepo, func(tx r3.CRUD[User, int64]) error {
user, err := tx.Create(ctx, newUser)
if err != nil {
return err
}
return nil
})
func PatchWhereOf ¶ added in v0.0.4
func PatchWhereOf[T any, ID comparable]( ctx context.Context, repo Commander[T, ID], filters Filters, entity T, fields Fields, ) (int64, error)
PatchWhereOf runs a bulk conditional update against repo if it (including any decorators, which forward the capability) implements BulkPatcher, and returns ErrBulkPatchNotSupported otherwise. Like UpsertOf it asserts only the outermost value so decorator concerns — the permissions scope confinement in particular — always apply.
func RegisterCodec ¶ added in v0.0.7
RegisterCodec registers a value codec under name for use in a struct tag (r3:"...,codec:<name>"). Call it during setup, before deriving schemas; it overwrites any existing registration and panics on an empty name or nil codec.
func RequireCodecSupport ¶ added in v0.0.7
RequireCodecSupport panics if s declares any value codec, naming the attribute and backend. A backend that does not yet apply codecs calls this at construction so a declared codec fails loudly instead of silently storing the un-encoded value (corrupting data and breaking portability). backend is a short id for the message, e.g. "r3/mongo".
func SupportsTx ¶
func SupportsTx[T any, ID comparable](repo CRUD[T, ID]) bool
SupportsTx reports whether repo's chain reaches a backend implementing Transactor. It sees through decorators, and unlike As only the backend counts - an intermediate decorator merely exposing BeginTx is not proof.
func TimeOfDayBounds ¶ added in v0.1.5
TimeOfDayBounds normalizes a TimeOfDayBetween value into its [lo, hi) minute bounds. The value is the 2-element [lo, hi] the sugar produces; bounds may arrive as int or float64 (JSON). Each must be a whole minute in 0..1440 (1440 is the exclusive end-of-day upper bound). Reusing ExtractBetweenBounds keeps the shape identical to Between.
func TruncateToBucket ¶ added in v0.1.7
func TruncateToBucket(t time.Time, u BucketUnit) time.Time
TruncateToBucket truncates t to the start of the calendar unit, in t's own location (no timezone conversion). Week starts on ISO-8601 Monday. An unknown unit returns t unchanged (structural validation rejects it before any engine reaches this). In-memory engines (the file engine) call it so the truncation rule lives in exactly one place - the analog of EvalWeekdayIn.
func UpsertOf ¶ added in v0.0.4
func UpsertOf[T any, ID comparable]( ctx context.Context, repo Commander[T, ID], entity T, opts ...UpsertOption, ) (T, error)
UpsertOf runs an upsert against repo, or returns ErrUpsertNotSupported if it does not implement Upserter. Like AggregateOf, it asserts only the outermost value - never the decorator chain - so permission checks always apply.
func ValidateIdentifier ¶
ValidateIdentifier checks that s is a safe SQL identifier or dotted path; each dot-separated segment must match [a-zA-Z_][a-zA-Z0-9_]*. Valid: "id", "user.profile", "orders.items.product_name". Invalid: "1col", "a b", "x;y", "table.*".
func WeekdaysValue ¶ added in v0.1.5
WeekdaysValue normalizes a WeekdayIn filter value into a []time.Weekday. It accepts the shapes that survive a serialization round-trip: []time.Weekday (the native sugar), []int, and []float64 (JSON decodes numbers as float64). Each element must be in 0..6 (Go's Sunday..Saturday). Every engine and dialect that lowers OperatorWeekdayIn goes through this one door so the accepted wire forms stay consistent.
func WithActor ¶
WithActor attaches an Actor to the context, typically in auth middleware. Authorization data can ride along on Actor's Claims:
ctx := r3.WithActor(r.Context(), r3.Actor{ID: "42", Type: "user", Claims: principal})
func WithLocale ¶ added in v0.0.2
WithLocale attaches a locale tag to the context, typically after language negotiation in HTTP middleware:
ctx := r3.WithLocale(r.Context(), "ru")
func WithoutWriteGuard ¶
WithoutWriteGuard returns a context that skips the engine's write-capability enforcement for writes made with it. Use it for audited system/worker writes of a user-immutable column; prefer the SystemWriter wrapper for ergonomics.
It does not bypass the structural floor (computed/PK remain unwritable), and it does not bypass any decorator (history/metrics still record the write).
func WriteGuardBypassed ¶
WriteGuardBypassed reports whether the context carries the write-guard bypass. Engines consult it before enforcing Creatable/Mutable.
Types ¶
type Actor ¶
type Actor struct {
// ID identifies who performed the action (e.g. user ID, API key ID, service name).
ID string
// Type categorizes the actor (e.g. "user", "service", "system", "cron").
Type string
// Claims carries optional application-defined authorization data (roles,
// tenant memberships, scopes, ...). R3 never inspects it - it only rides the
// context so policies can read it back; the permissions feature forwards it to
// Checker/Scoper via AccessRequest.Actor. Intentionally untyped: the
// application owns the shape and type-asserts it. Nil for system/service actors
// and the attribution-only features, which ignore it.
Claims any
}
Actor is the identity performing a CRUD operation, carried in context and picked up by the metrics and history features for attribution. The zero value is the system/anonymous actor.
type AggregateFunc ¶ added in v0.0.3
type AggregateFunc int8
AggregateFunc identifies an aggregate function applied over a group of rows.
const ( // AggregateCount counts rows in the group (COUNT(*) when Field is nil, // COUNT(field) otherwise — NULLs excluded). AggregateCount AggregateFunc = iota + 1 // AggregateCountDistinct counts distinct non-NULL values of Field. AggregateCountDistinct // AggregateSum sums Field over the group. AggregateSum // AggregateAvg averages Field over the group. AggregateAvg // AggregateMin takes the minimum of Field over the group. AggregateMin // AggregateMax takes the maximum of Field over the group. AggregateMax )
func (AggregateFunc) String ¶ added in v0.0.3
func (f AggregateFunc) String() string
String returns the canonical lowercase name of the aggregate function.
type AggregateRow ¶ added in v0.0.3
AggregateRow is one result row of an Aggregate call: group-field values plus one entry per declared aggregate alias. Raw values are backend-native (SQLite returns MAX over a timestamp as TEXT, MySQL returns SUM as a decimal string), so prefer the typed accessors, which coerce the common representations.
func AggregateOf ¶ added in v0.0.3
func AggregateOf[T any, ID comparable]( ctx context.Context, repo Querier[T, ID], qarg ...Query, ) ([]AggregateRow, error)
AggregateOf runs an aggregate query against repo if it implements Aggregator (decorators all forward it), else returns ErrAggregateNotSupported. It asserts only the outermost value - never walking the chain - so feature concerns, permission scoping above all, always apply.
func AggregateThroughRelation ¶ added in v0.0.5
func AggregateThroughRelation[T any, ID comparable]( ctx context.Context, repo Querier[T, ID], relation string, qarg ...Query, ) ([]AggregateRow, error)
AggregateThroughRelation folds repo's related rows through the named relation, or returns ErrRelationAggregateNotSupported if repo does not implement RelationAggregator. Like AggregateOf, it asserts only the outermost value - never the decorator chain - so permission scoping is never bypassed.
func (AggregateRow) Bool ¶ added in v0.0.3
func (r AggregateRow) Bool(key string) (bool, bool)
Bool returns the value as bool, coercing the 0/1 integers SQL backends return for boolean columns.
func (AggregateRow) Float64 ¶ added in v0.0.3
func (r AggregateRow) Float64(key string) (float64, bool)
Float64 returns the value as float64, coercing integer widths and numeric strings (MySQL decimals arrive as []byte).
func (AggregateRow) Int64 ¶ added in v0.0.3
func (r AggregateRow) Int64(key string) (int64, bool)
Int64 returns the value as int64, coercing integer widths, integral floats, and numeric strings (MySQL decimals arrive as []byte).
func (AggregateRow) String ¶ added in v0.0.3
func (r AggregateRow) String(key string) (string, bool)
String returns the value as a string. []byte is converted; other types report ok=false.
func (AggregateRow) Time ¶ added in v0.0.3
func (r AggregateRow) Time(key string) (time.Time, bool)
Time returns the value as time.Time, parsing the textual forms backends fall back to when an aggregate loses the column's declared type.
func (AggregateRow) Value ¶ added in v0.0.3
func (r AggregateRow) Value(key string) any
Value returns the raw backend value for key (nil if absent).
type AggregateSpec ¶ added in v0.0.3
type AggregateSpec struct {
Func AggregateFunc
Field *FieldSpec
Alias string
}
AggregateSpec declares one aggregated value to compute per group: the function, the field it aggregates (nil only for AggregateCount = COUNT(*)), and the alias under which the value appears in each AggregateRow. The alias is also the name Sorts and Having refer to.
func AggAvg ¶ added in v0.0.3
func AggAvg(field, alias string) *AggregateSpec
AggAvg builds AVG(field) AS alias.
func AggCount ¶ added in v0.0.3
func AggCount(alias string) *AggregateSpec
AggCount counts rows per group: COUNT(*) AS alias.
func AggCountDistinct ¶ added in v0.0.3
func AggCountDistinct(field, alias string) *AggregateSpec
AggCountDistinct counts distinct values: COUNT(DISTINCT field) AS alias.
func AggMax ¶ added in v0.0.3
func AggMax(field, alias string) *AggregateSpec
AggMax builds MAX(field) AS alias.
func AggMin ¶ added in v0.0.3
func AggMin(field, alias string) *AggregateSpec
AggMin builds MIN(field) AS alias.
func AggSum ¶ added in v0.0.3
func AggSum(field, alias string) *AggregateSpec
AggSum builds SUM(field) AS alias.
func (*AggregateSpec) Clone ¶ added in v0.0.3
func (a *AggregateSpec) Clone() *AggregateSpec
Clone returns a deep copy of the spec.
type Aggregates ¶ added in v0.0.3
type Aggregates []*AggregateSpec
Aggregates is a slice of *AggregateSpec.
func (Aggregates) Clone ¶ added in v0.0.3
func (as Aggregates) Clone() Aggregates
Clone returns a safe full-clone of the aggregates list.
type Aggregator ¶ added in v0.0.3
type Aggregator interface {
// Aggregate computes the declared aggregates per group and returns one
// row per group.
Aggregate(ctx context.Context, qarg ...Query) ([]AggregateRow, error)
}
Aggregator is the opt-in grouped-aggregation capability (see AggregateOf and the package doc). Of the merged Query, Aggregate honors Filters, IncludeTrashed, GroupBy, Buckets, Aggregates, Having, Sorts (over group fields, bucket aliases, and aggregate aliases only - other sorts, e.g. inherited defaults, are dropped), and Pagination (limits grouped rows); Fields, Preloads, and Cursor are ignored. Empty GroupBy and Buckets with non-empty Aggregates yields a single whole-set row; empty Aggregates is ErrInvalidAggregate.
Reach it via AggregateOf, never by asserting an inner repo - unwrapping the decorator chain would silently bypass permission scoping.
type Attribute ¶
type Attribute struct {
// Name is the public/wire name (snake_case). Queries reference attributes by
// this name; the engine translates it to a physical column/path.
Name string
// Type is the logical data type, used to pick default operators and drive
// frontend filter widgets.
Type DataType
// Caps is the bitset of capabilities.
Caps Capability
// Ops are the allowed filter operators. nil means "the defaults for Type"
// (or none, when not filterable).
Ops []FilterOperatorSpec
// Enum holds the allowed values when Type == TypeEnum.
Enum []string
// Relation describes the target when Type == TypeRel.
Relation *RelationRef
// Codec, when non-nil, transforms this attribute's value to and from its
// stored form (see [Codec]). Type stays the domain type (e.g. TypeTime) so
// callers filter with domain values; the codec bridges to storage
// ([Codec.Stored], e.g. TypeInt) at the engine boundary. nil is the identity.
Codec Codec
// Computed marks an attribute with no backing column (reserved; computed
// execution is out of scope - see the schema design doc, §8). A computed
// attribute can never be written: the structural floor has nowhere to put a
// value.
Computed bool
}
Attribute is one declared, capability-bearing member of an entity.
func (Attribute) Has ¶
func (a Attribute) Has(c Capability) bool
Has reports whether the attribute carries every capability in c.
type BucketSpec ¶ added in v0.1.7
type BucketSpec struct {
Field *FieldSpec
Unit BucketUnit
Alias string
}
BucketSpec is a derived group key: the truncation of a time-valued field to Unit, appearing in each AggregateRow under Alias (also the name Sorts and Having refer to). It participates in GROUP BY alongside Query.GroupBy fields.
func Bucket ¶ added in v0.1.7
func Bucket(field string, unit BucketUnit, alias string) *BucketSpec
Bucket builds a time-bucket group key: the value of field truncated to unit, returned under alias.
r3.Query{Buckets: r3.Buckets{r3.Bucket("created_at", r3.BucketDay, "day")}}
func (*BucketSpec) Clone ¶ added in v0.1.7
func (b *BucketSpec) Clone() *BucketSpec
Clone returns a deep copy of the spec.
type BucketUnit ¶ added in v0.1.7
type BucketUnit int8
BucketUnit is the closed set of calendar truncation units for a time-bucket group key (see BucketSpec).
const ( // BucketHour truncates to the top of the hour. BucketHour BucketUnit = iota + 1 // BucketDay truncates to midnight of the day. BucketDay // BucketWeek truncates to midnight of the ISO-8601 week start (Monday). BucketWeek // BucketMonth truncates to midnight of the first day of the month. BucketMonth // BucketYear truncates to midnight of January 1st. BucketYear )
func (BucketUnit) String ¶ added in v0.1.7
func (u BucketUnit) String() string
String returns the canonical lowercase name of the unit (also the Mongo $dateTrunc unit name).
func (BucketUnit) Valid ¶ added in v0.1.7
func (u BucketUnit) Valid() bool
Valid reports whether u is one of the defined bucket units.
type BulkPatcher ¶ added in v0.0.4
type BulkPatcher[T any, ID comparable] interface { // PatchWhere writes fields (taken from entity) on every row matching // filters and returns the number of rows affected. PatchWhere(ctx context.Context, filters Filters, entity T, fields Fields) (int64, error) }
BulkPatcher is the optional bulk-update capability of a repository: set the named fields (to the entity's values) on every row matching filters, returning the affected-row count. It is the multi-row analogue of Patch — same partial, by-Fields semantics — but selects rows by Filters instead of by primary key.
Like Upserter it is opt-in and reached through PatchWhereOf, so feature decorators (permission scoping in particular) always apply. A backend that does not implement it surfaces ErrBulkPatchNotSupported from PatchWhereOf rather than panicking.
type CRUD ¶
type CRUD[T any, ID comparable] interface { Querier[T, ID] Commander[T, ID] }
CRUD is the full read+write repository interface, composing Querier and Commander.
func SystemWriter ¶
func SystemWriter[T any, ID comparable](repo CRUD[T, ID]) CRUD[T, ID]
SystemWriter wraps the top of a repository (decorator) chain so its write methods inject WithoutWriteGuard into the context before delegating. The full chain still runs — history/metrics/soft-delete all see the write — but the engine skips the Creatable/Mutable check. Reads and the structural floor are unaffected.
r3.SystemWriter(repo).Update(ctx, feedRow) // writes a user-immutable column, audited
type Capability ¶
type Capability uint8
Capability is a bitset of what an Attribute is allowed to do. The five capabilities are the public contract — the ceiling of what any API caller may do. The permissions feature can only narrow this per-actor/row, never widen it (see the schema design doc, §2.3).
const ( // Filterable means the attribute may appear in Query.Filters. Filterable Capability = 1 << iota // Sortable means the attribute may appear in Query.Sorts. Sortable // Queryable means the attribute may appear in Query.Fields (SELECT) and in serialized output. Queryable // Creatable means the attribute may be set by Create. Creatable // Mutable means the attribute may be changed after creation — gates both Update and Patch. Mutable )
type Codec ¶ added in v0.0.7
type Codec interface {
// Encode converts a Go field value into the value handed to the backend. It
// returns (nil, nil) to store NULL — e.g. the zero time.Time or a nil pointer.
Encode(goValue any) (any, error)
// Decode converts a stored value back into the Go field of type target (a
// pointer type for a nullable field), tolerating the representation variance
// across backends (int64/int32/float64/[]byte/string) and mapping NULL to the
// zero value or nil pointer. A nil target means no destination shape is known
// (e.g. an [AggregateRow] has no struct field): decode to the codec's natural
// domain type, which [DecodeAggregateCodecs] relies on.
Decode(stored any, target reflect.Type) (any, error)
// Stored reports the logical DataType of the stored form (e.g. [TypeInt] for
// unixtime) — a hint for bind typing and cursor encoding, not a substitute for
// the attribute's domain [DataType].
Stored() DataType
}
Codec transforms a Go field value to and from its stored representation. It is declared once per attribute via the r3:"...,codec:<name>" struct tag, then applied uniformly by every codec-aware backend: on write (Go → stored), on read (stored → Go), and on filter/cursor arguments (a domain value is encoded before it reaches the query). The flagship is time.Time ⇄ unix int ("unixtime"), but the abstraction is general — money as cents, enums as ints, a struct as JSON.
A Codec operates on plain Go values, so it is backend-neutral: an engine feeds it whatever it scanned (int64, int32, float64, []byte, string) and takes back a value to bind or marshal. Implementations MUST be pure and stateless — one instance is shared across all repositories and cached on the Schema. A codec never owns the physical column type (r3 does no DDL); the column must already be the type Codec.Stored reports.
func LookupCodec ¶ added in v0.0.9
LookupCodec resolves a registered value codec by its tag name (e.g. "unixtime"). It lets an engine that derives field metadata reflectively - rather than through the generic SchemaOf - build its own codec map: schema derivation resolves the tag name to a Codec, but a backend keyed on a physical field name (Mongo's bson name, say) needs to resolve it independently. Returns false for an unregistered name.
type Commander ¶
type Commander[T any, ID comparable] interface { // Create inserts a new record. Create(context.Context, T) (T, error) // Update modifies an existing record and returns it as persisted. Columns the // backend does not write (immutable ones like created_at) come back with // their stored values, not the caller's; relation fields are returned as // passed in, since a read-back does not load them. Update(context.Context, T) (T, error) // Patch partially updates the row (PK must be set), writing only the columns // named in Fields and returning the full entity. Patch(context.Context, T, Fields) (T, error) // Delete removes a record by ID (soft-deletes if enabled on the repository). Delete(context.Context, ID) error }
Commander is the write-only subset of repository operations.
type Config ¶
type Config struct {
// Naming maps well-known fields to storage column names.
Naming NamingConfig
// Defaults controls default query behavior (e.g. page size).
Defaults DefaultsConfig
}
Config holds framework-level configuration (naming, default query behavior) shared across models. Build it with DefaultConfig, override fields, then pass it to a constructor via WithConfig. Treat as read-only after construction.
func DefaultConfig ¶
func DefaultConfig() Config
DefaultConfig returns a Config with all defaults applied.
type CursorDirection ¶
type CursorDirection int8
CursorDirection indicates whether cursor pagination goes forward or backward.
const ( // CursorForward pages forward from the "after" position. CursorForward CursorDirection = iota // CursorBackward pages backward from the "before" position. CursorBackward )
func (CursorDirection) String ¶
func (d CursorDirection) String() string
String returns "forward" or "backward".
type CursorSpec ¶
type CursorSpec struct {
// After is the opaque cursor token for forward pagination.
After string
// Before is the opaque cursor token for backward pagination.
Before string
// Limit is the max results to return; 0 means the default.
Limit int
}
CursorSpec specifies cursor/keyset pagination. Set at most one of After or Before; if both are set, After wins.
func NewCursorAfter ¶
func NewCursorAfter(after string, limit int) *CursorSpec
NewCursorAfter creates a CursorSpec for forward pagination after the token.
func NewCursorBefore ¶
func NewCursorBefore(before string, limit int) *CursorSpec
NewCursorBefore creates a CursorSpec for backward pagination before the token.
func NewCursorFirst ¶
func NewCursorFirst(limit int) *CursorSpec
NewCursorFirst creates a CursorSpec for the first page (limit only, no cursor).
func (*CursorSpec) Clone ¶
func (c *CursorSpec) Clone() *CursorSpec
Clone returns a deep copy of the CursorSpec.
func (*CursorSpec) Direction ¶
func (c *CursorSpec) Direction() CursorDirection
Direction returns CursorForward or CursorBackward based on which token is set.
func (*CursorSpec) GetLimit ¶
func (c *CursorSpec) GetLimit() int
GetLimit returns the limit, defaulting to PageSizeDefault if unset.
func (*CursorSpec) MergeWith ¶
func (c *CursorSpec) MergeWith(other *CursorSpec) *CursorSpec
MergeWith merges this cursor spec with another, with other taking precedence.
func (*CursorSpec) String ¶
func (c *CursorSpec) String() string
String returns a debug-friendly representation.
func (*CursorSpec) Token ¶
func (c *CursorSpec) Token() string
Token returns the active cursor token (After takes precedence over Before).
type CursorValues ¶
CursorValues holds the sort-column values (column name -> value) that define a cursor position.
func DecodeCursor ¶
func DecodeCursor(token string) (CursorValues, error)
DecodeCursor deserializes an opaque base64 token back into CursorValues. Returns empty CursorValues (not nil) for an empty token.
func EncodeCursorCodecs ¶ added in v0.0.7
func EncodeCursorCodecs(s Schema, values CursorValues) (CursorValues, error)
EncodeCursorCodecs clones the decoded cursor values with every codec'd key converted to stored form, so keyset predicates compare against stored column values. No-op when the schema declares no codecs.
type DataType ¶
type DataType string
DataType is the logical type of an attribute. It is engine-agnostic — it drives the default filter operators and (later) a frontend's filter widgets, not any storage representation.
type Defaults ¶
Defaults holds the default queries for List and Get, shared across all drivers.
func NewDefaults ¶
func NewDefaults() Defaults
NewDefaults returns Defaults with reasonable default queries.
type DefaultsConfig ¶
type DefaultsConfig struct {
// PageSize is the default page size when pagination is active but no explicit
// size is given. Default: [PageSizeDefault].
PageSize int
// Unpaginated, when true, makes List return ALL matching rows by default (no
// page-size cap); a per-query Query.Pagination still opts back in. Takes
// precedence over PageSize.
//
// Use with care on large tables; prefer the per-query [Unpaginated] escape
// hatch when only some call sites need everything.
Unpaginated bool
}
DefaultsConfig controls default query behavior.
type DefaultsManager ¶
type DefaultsManager struct {
// contains filtered or unexported fields
}
DefaultsManager is thread-safe access to Defaults; embed it in a CRUD struct to get the Set/Get default-query methods for free.
func NewDefaultsManager ¶
func NewDefaultsManager() DefaultsManager
NewDefaultsManager creates a DefaultsManager with reasonable defaults.
func NewDefaultsManagerWithConfig ¶
func NewDefaultsManagerWithConfig(cfg Config) DefaultsManager
NewDefaultsManagerWithConfig creates a DefaultsManager honoring cfg: Unpaginated makes the default list query unbounded; otherwise a non-default PageSize seeds the default list query's page size.
func (*DefaultsManager) GetDefaultGetQuery ¶
func (dm *DefaultsManager) GetDefaultGetQuery() Query
GetDefaultGetQuery returns the default GetQuery (thread-safe).
func (*DefaultsManager) GetDefaultListQuery ¶
func (dm *DefaultsManager) GetDefaultListQuery() Query
GetDefaultListQuery returns the default ListQuery (thread-safe).
func (*DefaultsManager) MergeGetQuery ¶
func (dm *DefaultsManager) MergeGetQuery(qarg ...Query) Query
MergeGetQuery merges the given query args with the default get query.
func (*DefaultsManager) MergeListQuery ¶
func (dm *DefaultsManager) MergeListQuery(qarg ...Query) Query
MergeListQuery merges the given query args with the default list query.
func (*DefaultsManager) SetDefaultGetQuery ¶
func (dm *DefaultsManager) SetDefaultGetQuery(q Query)
SetDefaultGetQuery sets the default GetQuery (thread-safe).
func (*DefaultsManager) SetDefaultListQuery ¶
func (dm *DefaultsManager) SetDefaultListQuery(q Query)
SetDefaultListQuery sets the default ListQuery (thread-safe).
type FieldSpec ¶
type FieldSpec string
FieldSpec names a field: a plain string (a column/attribute name, possibly a dotted path).
type Fields ¶
type Fields []*FieldSpec
Fields is a slice of *FieldSpec.
func GroupBy ¶ added in v0.0.3
GroupBy builds the Fields list for Query.GroupBy from plain field names:
r3.Query{GroupBy: r3.GroupBy("location_id", "squad_id"), ...}
type FilterOperatorSpec ¶
type FilterOperatorSpec int8
FilterOperatorSpec is a filter operator. This is the full set r3 aims to support; not every dialect supports every one. TODO: might be refactored into a more complex struct
const ( OperatorUnspecified FilterOperatorSpec = iota OperatorEq // = OperatorNe // != OperatorExists // exists OperatorGt // > OperatorGte // >= OperatorLt // < OperatorLte // <= OperatorBetween // between_inc meaning [] OperatorBetweenEx // between_exc meaning () OperatorBetweenExInc // between_exc_inc meaning (] OperatorBetweenIncEx // between_inc_exc meaning [) OperatorIn // in OperatorNotIn // not in OperatorLike // like OperatorNotLike // not like OperatorILike // ilike (like + case insensitive) OperatorWeekdayIn // weekday_in: weekday-of(field) ∈ value (see r3_filter_timepattern.go) OperatorTimeOfDayBetween // tod_between: minute-of-day-of(field) ∈ [lo, hi) )
func (*FilterOperatorSpec) String ¶
func (op *FilterOperatorSpec) String() string
String returns a debug label for the operator. Note: Protect with the `exhausted` linter.
type FilterSpec ¶
type FilterSpec struct {
Field *FieldSpec `json:"Field"`
Operator FilterOperatorSpec `json:"Operator"`
Value any `json:"Value"`
And Filters `json:"And"`
Or Filters `json:"Or"`
// Relation, when non-empty, makes this a relationship ("has") filter matching
// rows whose declared relation (by preload struct-field name) has at least one
// related row satisfying RelationFilter. Field/Operator/Value/And/Or are then
// ignored. The driver lowers it to a key-set In filter before SQL translation,
// so it works on any backend regardless of native subquery support.
// omitempty keeps a non-relationship filter serializing exactly as before.
Relation string `json:"Relation,omitempty"`
RelationFilter Filters `json:"RelationFilter,omitempty"`
// RelationNegate inverts a relationship filter (see [HasNo]): match rows whose
// relation has NO related row satisfying RelationFilter - an anti-join /
// NOT EXISTS. Only meaningful with Relation set; the driver lowers it to a
// NOT-IN key set.
RelationNegate bool `json:"RelationNegate,omitempty"`
}
FilterSpec represents a filtering criteria with a field, an operator, and a value.
func And ¶
func And(filters ...*FilterSpec) *FilterSpec
And is a shortcut for NewFilterSpecAndGroup.
func Between ¶
func Between(field string, lo, hi any) *FilterSpec
Between builds an inclusive `field BETWEEN lo AND hi` filter.
func Exists ¶
func Exists(field string, value any) *FilterSpec
Exists builds a `field exists` filter (presence check). The value is the expected existence as a bool where the backend supports it.
func F ¶
func F(field *FieldSpec, value any) *FilterSpec
F is a shorthand for NewFilterSpec(field, OperatorEq, value).
func FILike ¶
func FILike(field *FieldSpec, value any) *FilterSpec
func FLike ¶
func FLike(field *FieldSpec, value any) *FilterSpec
func Fop ¶
func Fop(field *FieldSpec, operator FilterOperatorSpec, value any) *FilterSpec
Fop is shorthand for NewFilterSpec ("F" for filter, "op" for operator).
func Has ¶
func Has(relation string, inner ...*FilterSpec) *FilterSpec
Has builds a relationship ("has") filter: it matches rows whose declared relation (by preload struct-field name) has at least one related row satisfying all of inner, evaluated against the related entity. Example:
r3.Has("Squads", r3.In("id", []int64{1, 3})) // rows linked to squad 1 or 3
The driver resolves the relation to a key set and rewrites this to an In filter, so it works on every backend regardless of native subquery support. It does not round-trip through the serialization dialects - build it in Go. As a permission scope, enforcing it on Get requires permissions.WithIDFunc.
func HasNo ¶ added in v0.0.5
func HasNo(relation string, inner ...*FilterSpec) *FilterSpec
HasNo is the negated (anti-join) counterpart of Has: it matches rows whose declared relation has NO related row satisfying inner - a NOT EXISTS. With no inner filters it matches rows with no related row at all. Example:
r3.HasNo("Translations") // rows with no translation yet
r3.HasNo("Squads", r3.Eq("archived", true)) // rows in no archived squad
Like Has, the driver resolves it to a key set (a NOT-IN filter); it works on every backend and does not round-trip through the serialization dialects.
func ILike ¶
func ILike(field string, value any) *FilterSpec
ILike builds a case-insensitive `field ILIKE value` filter.
func In ¶
func In(field string, value any) *FilterSpec
In builds a `field IN (values...)` filter. The value is typically a slice.
func Like ¶
func Like(field string, value any) *FilterSpec
Like builds a case-sensitive `field LIKE value` filter.
func NewFilterSpec ¶
func NewFilterSpec(field *FieldSpec, operator FilterOperatorSpec, value any) *FilterSpec
NewFilterSpec constructs a FilterSpec (not an AND/OR group).
func NewFilterSpecAndGroup ¶
func NewFilterSpecAndGroup(filters ...*FilterSpec) *FilterSpec
NewFilterSpecAndGroup wraps filters in an AND group.
func NewFilterSpecOrGroup ¶
func NewFilterSpecOrGroup(filters ...*FilterSpec) *FilterSpec
NewFilterSpecOrGroup wraps filters in an OR group.
func NotIn ¶
func NotIn(field string, value any) *FilterSpec
NotIn builds a `field NOT IN (values...)` filter. The value is typically a slice.
func NotLike ¶
func NotLike(field string, value any) *FilterSpec
NotLike builds a case-sensitive `field NOT LIKE value` filter.
func TimeOfDayBetween ¶ added in v0.1.5
func TimeOfDayBetween(field string, loMin, hiMin int) *FilterSpec
TimeOfDayBetween matches rows whose time field's minute-of-day lies in [loMin, hiMin). Bounds are minutes from midnight. loMin > hiMin means the window wraps midnight (e.g. 1320, 300 = 22:00-05:00). Minute precision matches schedule.TimeOfDay and sidesteps seconds edge-cases. The value mirrors Between ([]any{lo, hi}) so it round-trips through every serialization dialect as a plain 2-element array.
func WeekdayIn ¶ added in v0.1.5
func WeekdayIn(field string, days ...time.Weekday) *FilterSpec
WeekdayIn matches rows whose time field's weekday is one of days. Weekdays use Go's numbering (time.Sunday=0 .. time.Saturday=6); every lowering converts to its backend's convention at the edge (e.g. Mongo's $dayOfWeek is 1..7).
func (*FilterSpec) Clone ¶
func (f *FilterSpec) Clone() *FilterSpec
Clone returns a deep clone of the filter.
type Filters ¶
type Filters []*FilterSpec
Filters represents a list of *FilterSpec.
func EncodeFilterCodecs ¶ added in v0.0.7
EncodeFilterCodecs clones filters with every codec'd argument converted to stored form, so a domain value compares against stored column values (e.g. r3.Lt("started_at", someTime) against an int column). Scalar, In/NotIn slice, and Between-pair arguments are handled; operators carrying no comparable value (Exists, Like), non-codec'd fields, relationship filters, and dotted field names pass through. No-op when the schema declares no codecs.
type JSONColumn ¶
type JSONColumn[T any] struct { Val T }
JSONColumn wraps a value of type T so it persists as a JSON string in a single SQL column while staying transparent to JSON APIs: it implements sql.Scanner, driver.Valuer, json.Marshaler, and json.Unmarshaler. Use it for fields stored as a JSON blob (e.g. []FieldChange, metadata maps, nested config).
func NewJSONColumn ¶
func NewJSONColumn[T any](v T) JSONColumn[T]
NewJSONColumn creates a JSONColumn wrapping the given value.
func (JSONColumn[T]) MarshalJSON ¶
func (j JSONColumn[T]) MarshalJSON() ([]byte, error)
MarshalJSON implements json.Marshaler — transparent JSON serialization of the wrapped value.
func (*JSONColumn[T]) Scan ¶
func (j *JSONColumn[T]) Scan(src any) error
Scan implements sql.Scanner — reads a JSON string (or []byte) from SQL and unmarshals into T.
func (*JSONColumn[T]) UnmarshalJSON ¶
func (j *JSONColumn[T]) UnmarshalJSON(data []byte) error
UnmarshalJSON implements json.Unmarshaler — transparent JSON deserialization into the wrapped value.
type NamingConfig ¶
type NamingConfig struct {
// CreatedAtField is the creation-timestamp column. Default: "created_at".
CreatedAtField string
// UpdatedAtField is the update-timestamp column. Default: "updated_at".
UpdatedAtField string
// DeletedAtField is the soft-delete-timestamp column. Default: "deleted_at".
DeletedAtField string
}
NamingConfig maps well-known fields to storage column names. Empty means "use the default".
type Option ¶
type Option func(*Options)
Option is a functional option passed to engine/driver constructors.
func WithConfig ¶
WithConfig sets the R3 framework-level configuration.
func WithRelations ¶ added in v0.0.5
func WithRelations(rels ...RelationSpec) Option
WithRelations registers relations by table and column name (see RelationSpec), letting a repository resolve relations to tables it does not import as Go types. These supplement the relations reflected from `r3:"rel:..."` tags, and a declared relation whose name matches a reflected one takes precedence. Resolvable through Has/HasNo filters and AggregateThroughRelation.
repo := r3gorm.NewGormCRUD[Photo, int64](db,
r3.WithRelations(
r3.ManyToManyRelation("locations",
"photo_locations", "photo_id", "location_id", "locations"),
),
)
Relation support is currently implemented by the GORM and Mongo drivers.
type Options ¶
type Options struct {
// Config is the framework-level configuration.
Config Config
// Relations holds relations declared explicitly (by table + column names)
// via [WithRelations], to supplement or override the relations reflected
// from `r3:"rel:..."` struct tags. See [RelationSpec].
Relations []RelationSpec
}
Options is the resolved repository configuration; constructors build it from functional options via ResolveOptions.
func DefaultOptions ¶
func DefaultOptions() Options
DefaultOptions returns Options with sensible defaults.
func ResolveOptions ¶
ResolveOptions applies functional options over DefaultOptions.
type PaginationSpec ¶
type PaginationSpec struct {
// PageNum is 1-indexed page number (1 = first page).
PageNum maybe.Int
// PageSize is the number of items per page.
PageSize maybe.Int
// Offset, when set, is the raw 0-based row offset - an alternative to PageNum
// for consumers that natively think in offsets (REST ?offset=, infinite-scroll
// loaders). Unlike a page number it can express a starting row that is not a
// multiple of PageSize. When set it takes precedence over PageNum in
// ToLimitOffset and is carried to the driver verbatim (no page rounding).
// PageNum and Offset are mutually exclusive position encodings (see MergeWith).
Offset maybe.Int
}
PaginationSpec is the core pagination type using PageNum/PageSize.
func DefaultPagination ¶
func DefaultPagination() *PaginationSpec
DefaultPagination returns a PaginationSpec with default page size.
func NewOffsetPagination ¶ added in v0.1.4
func NewOffsetPagination(offset int, limit ...int) *PaginationSpec
NewOffsetPagination creates a PaginationSpec from a raw 0-based row offset (and an optional limit), for consumers that paginate by offset rather than page number (REST ?offset=, infinite-scroll loaders). Unlike a page number, an offset can start at a row that is not a multiple of the limit: NewOffsetPagination(100, 30) fetches exactly rows 100-129. The offset is carried to the driver verbatim, so no page rounding can silently duplicate or skip rows. A negative offset is treated as 0; an omitted limit defaults to PageSizeDefault (mirrors NewPaginationSpec).
func NewPaginationSpec ¶
func NewPaginationSpec(pageNum int, pageSize ...int) *PaginationSpec
NewPaginationSpec creates a new PaginationSpec with given page number and page size.
func NewPaginationSpecWithSize ¶
func NewPaginationSpecWithSize(pageSize int) *PaginationSpec
NewPaginationSpecWithSize creates a new PaginationSpec with only page size specified.
func NoPagination ¶
func NoPagination() *PaginationSpec
NoPagination returns a PaginationSpec with no pagination limits.
func Unpaginated ¶
func Unpaginated() *PaginationSpec
Unpaginated disables the default page-size cap so List returns every matching record - otherwise a "give me everything" caller silently gets a truncated slice. It overrides any configured default page size on merge:
all, total, err := repo.List(ctx, r3.Query{Pagination: r3.Unpaginated()})
To make a whole repo unpaginated by default, set Config.Defaults.Unpaginated. It is an intention-revealing alias for NoPagination.
func (*PaginationSpec) Clone ¶
func (p *PaginationSpec) Clone() *PaginationSpec
Clone creates a deep copy of the PaginationSpec.
func (*PaginationSpec) GetOffset ¶ added in v0.1.4
func (p *PaginationSpec) GetOffset() (int, bool)
GetOffset returns the raw row offset when one was set explicitly (negative clamped to 0). The bool reports whether an explicit offset is present; when false the caller should derive the offset from PageNum instead.
func (*PaginationSpec) GetPageNum ¶
func (p *PaginationSpec) GetPageNum() int
GetPageNum returns the page number (1-indexed), defaulting to 1 if not set.
func (*PaginationSpec) GetPageSize ¶
func (p *PaginationSpec) GetPageSize() int
GetPageSize returns the page size, defaulting to PageSizeDefault if not set.
func (*PaginationSpec) IsPaginated ¶
func (p *PaginationSpec) IsPaginated() bool
IsPaginated returns true if pagination is configured.
func (*PaginationSpec) MergeWith ¶
func (p *PaginationSpec) MergeWith(other *PaginationSpec) *PaginationSpec
MergeWith merges this pagination with another, with other taking precedence.
func (*PaginationSpec) String ¶
func (p *PaginationSpec) String() string
String returns string representation of pagination.
func (*PaginationSpec) ToLimitOffset ¶
func (p *PaginationSpec) ToLimitOffset() (int, int)
ToLimitOffset converts the spec to Limit/Offset for database queries.
type PreloadSpec ¶
type PreloadSpec struct {
Name string
}
PreloadSpec names a relation to preload (a table/collection name).
func NewPreloadSpec ¶
func NewPreloadSpec(name string) *PreloadSpec
NewPreloadSpec creates a PreloadSpec.
func (*PreloadSpec) Clone ¶
func (t *PreloadSpec) Clone() *PreloadSpec
Clone returns a copy of the preload.
func (*PreloadSpec) GetName ¶
func (t *PreloadSpec) GetName() string
GetName returns the preload name.
type Preloads ¶
type Preloads []*PreloadSpec
Preloads is a slice of *PreloadSpec.
type Querier ¶
type Querier[T any, ID comparable] interface { // Get retrieves a record by its ID, normalizing not-found to [ErrNotFound]. Get(context.Context, ID, ...Query) (T, error) // List retrieves records matching the query. List(context.Context, ...Query) ([]T, int64, error) // Count returns how many records match, without materializing rows. Only // Filters and IncludeTrashed apply; pagination/sorts/fields/preloads are // ignored, and no query counts every non-trashed record. Count(context.Context, ...Query) (int64, error) }
Querier is the read-only subset of repository operations. Depend on it when read-only access is all you need.
type Query ¶
type Query struct {
Pagination *PaginationSpec
// Cursor enables keyset/cursor pagination instead of offset-based. When set it
// takes precedence over Pagination; the two are mutually exclusive.
Cursor *CursorSpec
Fields Fields // Specific fields to retrieve.
Filters Filters // []*FilterSpec
Sorts Sorts // []*SortSpec
Preloads Preloads // []*PreloadSpec
// GroupBy, Buckets, Aggregates, and Having describe an aggregation honored
// only by Aggregate (see [Aggregator]); Get/List/Count ignore them, as Count
// ignores pagination. GroupBy names the plain grouping fields (empty = one
// whole-set row when Buckets is also empty), Buckets adds derived time-bucket
// group keys (see [BucketSpec]), Aggregates the computed values (required for
// Aggregate), and Having filters grouped rows by aggregate alias, group field,
// or bucket alias.
GroupBy Fields
Buckets Buckets
Aggregates Aggregates
Having Filters
// IncludeTrashed, when true, also returns soft-deleted records.
IncludeTrashed maybe.Bool
}
Query is the immutable read model: filters, sorts, projection, pagination, and (for Aggregator) grouping. Compose with Query.MergeWith; never mutate a shared value.
func DefaultQuery ¶
func DefaultQuery() Query
DefaultQuery returns a Query with the default pagination applied.
func (Query) AggregateSorts ¶ added in v0.0.3
AggregateSorts restricts the query's Sorts to what a grouped result can be ordered by - group fields and aggregate aliases. Anything else (typically a default sort over entity columns absent from grouped rows) is dropped. Engines call this instead of Query.Sorts directly.
type RelationAggregator ¶ added in v0.0.5
type RelationAggregator interface {
// AggregateThroughRelation folds the declared relation's related rows into
// one AggregateRow per group.
AggregateThroughRelation(ctx context.Context, relation string, qarg ...Query) ([]AggregateRow, error)
}
RelationAggregator is the opt-in capability of aggregating an entity's RELATED rows through a declared relation: grouped COUNT/SUM/AVG/MIN/MAX like Aggregator, but folded over the related rows (a has-many child table or a many-to-many join table) rather than the entity's own - e.g. "members per group" over a membership join table, without dropping to raw SQL.
Query semantics (of the merged Query):
- GroupBy and Aggregates resolve against the RELATED base table: the child table for has-many, the join table for many-to-many. An AggCount with no field counts related rows; empty Aggregates is ErrInvalidAggregate.
- Filters are interpreted as filters on the OWNER entity and restrict which owners' related rows are aggregated (so a permissions Scoper's owner filters apply). Empty Filters aggregates across all owners.
- Related rows soft-deleted in the target are excluded when the relation declares a soft-delete column (see RelationTargetSoftDelete or a tag-derived soft-delete on the target).
- Having, Sorts (group fields + aggregate aliases only), and Pagination behave as in Aggregator. IncludeTrashed, Fields, Preloads, and Cursor are ignored.
Only has-many and many-to-many relations are aggregatable; belongs-to is not (each owner has at most one related row). Reach it via AggregateThroughRelation rather than asserting inner repos, so permission scoping is never bypassed.
type RelationKind ¶ added in v0.0.5
type RelationKind uint8
RelationKind identifies the shape of a relation between two entities.
const ( // RelationHasMany is one-to-many: the child rows carry an FK to the owner. RelationHasMany RelationKind = iota + 1 // RelationBelongsTo is many-to-one: the owner row carries an FK to the related row. RelationBelongsTo // RelationManyToMany is resolved through a join table. RelationManyToMany )
func (RelationKind) String ¶ added in v0.0.5
func (k RelationKind) String() string
String returns the canonical lowercase name matching the `r3:"rel:..."` tag vocabulary ("has-many" | "belongs-to" | "many-to-many").
type RelationOption ¶ added in v0.0.5
type RelationOption func(*RelationSpec)
RelationOption customizes an optional field of a RelationSpec.
func RelationTargetPK ¶ added in v0.0.5
func RelationTargetPK(column string) RelationOption
RelationTargetPK overrides the related table's primary-key column (default "id").
func RelationTargetSoftDelete ¶ added in v0.0.5
func RelationTargetSoftDelete(column string) RelationOption
RelationTargetSoftDelete declares the related table's soft-delete column so AggregateThroughRelation excludes soft-deleted related rows.
type RelationRef ¶
type RelationRef struct {
// Target is the related entity's logical name (snake_case of its type).
Target string
// Kind is the relationship kind ("has-many", "belongs-to", "many-to-many").
Kind string
// Label is the target attribute used as a human-facing label (optional;
// populated via With for introspection).
Label string
}
RelationRef points an Attribute (Type == TypeRel) at its target entity.
type RelationSpec ¶ added in v0.0.5
type RelationSpec struct {
// Name is the logical name used by Has/HasNo and AggregateThroughRelation;
// it need not correspond to any struct field.
Name string
// Kind is the relation shape.
Kind RelationKind
// TargetTable is the related rows' table (e.g. "locations").
TargetTable string
// TargetPK is the related table's primary-key column. Defaults to "id".
TargetPK string
// FKColumn is the foreign-key column:
// - has-many: the FK on the related (child) table pointing at the owner
// - belongs-to: the FK on the owner table pointing at the related row
// - many-to-many: the owner-side FK column in the join table
FKColumn string
// RefColumn is the related-side FK column in the join table (many-to-many only).
RefColumn string
// JoinTable is the join table name (many-to-many only).
JoinTable string
// TargetSoftDeleteColumn, when set, is the related table's soft-delete
// column: [AggregateThroughRelation] excludes related rows whose value is
// non-NULL, so soft-deleted related rows are not counted.
TargetSoftDeleteColumn string
}
RelationSpec physically describes a relation by table and column names, so a driver can resolve it WITHOUT importing the related Go type - the explicit counterpart to a tag-declared relation (`r3:"rel:..."`). This lets an entity relate to a table it cannot import (e.g. when the packages already reference each other, so importing would create a domain cycle); being a value rather than a struct field, it needs no Go field on the entity.
Register specs with WithRelations; they then resolve by name through Has/HasNo and AggregateThroughRelation like tag-declared relations, but are not preloadable (no struct field to load into). Build one with HasManyRelation, BelongsToRelation, or ManyToManyRelation.
func BelongsToRelation ¶ added in v0.0.5
func BelongsToRelation(name, targetTable, fkColumn string, opts ...RelationOption) RelationSpec
BelongsToRelation declares a many-to-one relation: the owner's `fkColumn` points at `targetTable`'s primary key.
func HasManyRelation ¶ added in v0.0.5
func HasManyRelation(name, targetTable, fkColumn string, opts ...RelationOption) RelationSpec
HasManyRelation declares a one-to-many relation: `fkColumn` on `targetTable` points back at the owner's primary key.
func ManyToManyRelation ¶ added in v0.0.5
func ManyToManyRelation( name, joinTable, fkColumn, refColumn, targetTable string, opts ...RelationOption, ) RelationSpec
ManyToManyRelation declares a many-to-many relation through `joinTable`: `fkColumn` is the owner-side FK and `refColumn` the related-side FK.
type Rewrapper ¶
type Rewrapper[T any, ID comparable] interface { Rewrap(inner CRUD[T, ID]) CRUD[T, ID] }
Rewrapper is implemented by decorators that can rebuild themselves around a different inner CRUD. It lets InTx/BeginTxChain re-apply the decorator stack on top of a transaction-bound CRUD, so decorated behaviour (validation, history, permissions, ...) still runs inside the transaction instead of being bypassed.
Rewrap must return a decorator equivalent to the receiver but delegating to inner. Stateful decorators should share their backing state (stores, locks) with the rebuilt instance.
type Schema ¶
type Schema struct {
// contains filtered or unexported fields
}
Schema is the logical, engine-agnostic descriptor of an entity: an ordered set of capability-bearing [Attribute]s. It carries no storage details (no column names, no BSON paths) - each engine owns the physical binding, keyed by attribute name.
A Schema is immutable: derive with SchemaOf, layer overrides with Schema.With, both returning a fresh value. The zero Schema has no attributes and validates nothing (back-compat for schema-less callers); see Schema.IsZero.
Vocabulary: an Attribute is a declared member of an entity; a FieldSpec is a reference to one inside a Query. The Schema is the set of valid fields plus what each may do.
func SchemaOf ¶
func SchemaOf[T any](opts ...SchemaOption) Schema
SchemaOf reflects T's struct tags into a Schema with permissive default capabilities (see the schema design doc, §2.7): a plain scalar is queryable/filterable/sortable/creatable/mutable; PK, created_at/updated_at, and the soft-delete column are read-only; relations are queryable (preload) only. Tags only ever tighten these. The no-option result is cached per T.
func (Schema) Attributes ¶
Attributes returns the attributes in declaration order (a copy).
func (Schema) Filterable ¶
Filterable reports whether the named attribute may appear in Query.Filters.
func (Schema) IsZero ¶
IsZero reports whether the schema declares no attributes. A zero Schema disables validation at the engine seam (back-compat for schema-less callers).
func (Schema) ValidateAggregateQuery ¶ added in v0.0.3
ValidateAggregateQuery is the aggregate counterpart of Schema.ValidateQuery, called by engines before building an aggregate. Structural checks (aggregates declared, aliases valid and unique, Having references declared names) run even for a zero Schema; capability checks (group/aggregated fields must be Filterable, SUM/AVG need a numeric attribute) apply only when a schema is present, keeping the permissive-defaults philosophy.
It does not check Sorts: a sort referencing neither a group field nor an alias is dropped by Query.AggregateSorts, not rejected - it is usually an inherited repo default, invisible to the caller.
func (Schema) ValidateQuery ¶
ValidateQuery is the source of typed, 400-class errors: it checks every field in Filters, Sorts, and Fields against the schema's capabilities and returns a typed error (wrapping the field name) on the first violation. A zero Schema validates nothing; see Schema.IsZero.
Dotted ("relation.path") names are skipped: they reference another entity, validated by the engine against the target schema (TODO), not the root. Relationship ("has") filters are likewise skipped.
type SchemaOption ¶
type SchemaOption func(*schemaConfig)
SchemaOption customizes schema derivation.
func WithSchemaNaming ¶
func WithSchemaNaming(n NamingConfig) SchemaOption
WithSchemaNaming overrides the well-known field names (created_at, updated_at, deleted_at) that drive the read-only timestamp/soft-delete defaults.
type SortDirection ¶
type SortDirection int8
SortDirection is a sort direction.
const ( // SortDirectionUnspecified falls back to the default direction. SortDirectionUnspecified SortDirection = iota // SortDirectionAsc sorts ascending. SortDirectionAsc // SortDirectionDesc sorts descending. SortDirectionDesc )
func (SortDirection) String ¶
func (s SortDirection) String() string
String returns a debug label ("asc"/"desc"/unspecified).
type SortNullsPosition ¶
type SortNullsPosition int8
SortNullsPosition is where NULLs sort (first or last).
const ( // NullsPositionNotSpecified leaves NULLs position out of the query. NullsPositionNotSpecified SortNullsPosition = iota // NullsPositionFirst places NULLs first. NullsPositionFirst // NullsPositionLast places NULLs last. NullsPositionLast )
func (SortNullsPosition) String ¶
func (s SortNullsPosition) String() string
String returns a debug label ("nulls first"/"nulls last"/"").
type SortSpec ¶
type SortSpec struct {
Column *FieldSpec
Direction SortDirection // Asc | Desc | Unspecified (default)
NullsPosition SortNullsPosition // First | Last | Unspecified
}
SortSpec is a single sort criterion.
func NewSortAscSpec ¶
NewSortAscSpec sorts by col ascending.
func NewSortDescSpec ¶
NewSortDescSpec sorts by col descending.
func NewSortSpec ¶
func NewSortSpec(col *FieldSpec, direction SortDirection, nullsPositionArg ...SortNullsPosition) *SortSpec
NewSortSpec is the primary SortSpec constructor.
func (*SortSpec) GetCriteria ¶
GetCriteria returns the sort criteria.
func (*SortSpec) GetDirection ¶
func (s *SortSpec) GetDirection() SortDirection
GetDirection returns the sort direction.
type Transactor ¶
type Transactor[T any, ID comparable] interface { // BeginTx starts a transaction and returns a [TxCRUD] scoped to it. The // caller MUST call either Commit or Rollback on it. BeginTx(ctx context.Context) (TxCRUD[T, ID], error) }
Transactor is the opt-in transaction capability a CRUD may satisfy; many backends (in-memory, YAML files, some NoSQL) do not. Type-assert to detect support, or use InTx for automatic commit/rollback.
type TxCRUD ¶
type TxCRUD[T any, ID comparable] interface { CRUD[T, ID] // Commit commits the transaction; the TxCRUD must not be used afterward. Commit() error // Rollback aborts the transaction (a no-op if Commit already ran); the // TxCRUD must not be used afterward. Rollback() error }
TxCRUD is a CRUD bound to a single transaction, plus Commit/Rollback. It must not be used after either is called.
func BeginTxChain ¶
func BeginTxChain[T any, ID comparable](ctx context.Context, repo CRUD[T, ID]) (TxCRUD[T, ID], error)
BeginTxChain is the chain-aware counterpart to Transactor.BeginTx: it walks repo's decorator chain to the backend, begins there, and returns a TxCRUD with the same decorator stack re-applied on top. Decorators delegate to it so transactions compose with decoration. Returns ErrTransactionsNotSupported if no layer implements Transactor.
type Unwrapper ¶
type Unwrapper[T any, ID comparable] interface { Unwrap() CRUD[T, ID] }
Unwrapper is implemented by decorators that wrap an inner CRUD. Exposing the wrapped CRUD lets capability detection (As) and transaction propagation (InTx, BeginTxChain) walk the decorator chain down to the backend.
Every r3 feature decorator implements Unwrapper.
type UpsertOption ¶ added in v0.0.4
type UpsertOption func(*UpsertSpec)
UpsertOption configures an UpsertSpec.
func OnConflict ¶ added in v0.0.4
func OnConflict(cols ...string) UpsertOption
OnConflict sets the conflict-target columns. With no option the target is the primary key.
func UpdateOnConflict ¶ added in v0.0.4
func UpdateOnConflict(fields ...*FieldSpec) UpsertOption
UpdateOnConflict restricts which columns are written on conflict. With no option every mutable column is overwritten.
type UpsertSpec ¶ added in v0.0.4
type UpsertSpec struct {
// ConflictColumns is the conflict target — the columns whose collision
// triggers the update branch. Empty means the primary key.
ConflictColumns []string
// UpdateFields are the columns overwritten on conflict. Empty means all
// mutable columns of the entity (a full "replace").
UpdateFields Fields
}
UpsertSpec is the resolved configuration of an Upsert: the conflict target and the columns overwritten on conflict. Build it from [UpsertOption]s via NewUpsertSpec; engines and decorators read it to shape the write.
func NewUpsertSpec ¶ added in v0.0.4
func NewUpsertSpec(opts ...UpsertOption) UpsertSpec
NewUpsertSpec resolves the given options into a concrete UpsertSpec. Engines implementing Upserter call this to interpret the caller's options.
type Upserter ¶ added in v0.0.4
type Upserter[T any, ID comparable] interface { // Upsert inserts entity, or updates the colliding row when it conflicts on // the conflict target (the primary key by default; see [OnConflict]). It // returns the stored row after the write. Upsert(ctx context.Context, entity T, opts ...UpsertOption) (T, error) }
Upserter is the opt-in upsert capability: insert the entity, or update it in place on conflict. The write analogue of Aggregator - reached through UpsertOf (which returns ErrUpsertNotSupported for a backend that lacks it) so decorators always apply, and kept out of core Commander so adding it breaks no existing backend.
Source Files
¶
- crud.go
- doc.go
- helpers.go
- r3_actor.go
- r3_aggregate.go
- r3_aggregate_bucket.go
- r3_bulkpatch.go
- r3_codec.go
- r3_config.go
- r3_cursor.go
- r3_defaults.go
- r3_errors.go
- r3_field.go
- r3_filter.go
- r3_filter_operator.go
- r3_filter_timepattern.go
- r3_json_column.go
- r3_locale.go
- r3_option.go
- r3_pagination.go
- r3_preload.go
- r3_query.go
- r3_relation.go
- r3_relation_aggregate.go
- r3_schema_bypass.go
- r3_sort.go
- r3_tx.go
- r3_unwrap.go
- r3_upsert.go
- schema.go
- schema_caps.go
- schema_derive.go
- schema_validate.go
Directories
¶
| Path | Synopsis |
|---|---|
|
dialects
|
|
|
bson
Package r3bson is the data-store dialect that translates r3 query types (r3.FilterSpec, r3.SortSpec, and friends) into MongoDB bson.D documents.
|
Package r3bson is the data-store dialect that translates r3 query types (r3.FilterSpec, r3.SortSpec, and friends) into MongoDB bson.D documents. |
|
canonical
Package canonical is the single source of the string vocabulary the serialization dialects (JSON, YAML, TOML, URL) all map through: operators ("eq", "ne", "gt", ...), sort directions ("asc", "desc"), nulls positions ("first", "last"), and aggregate functions.
|
Package canonical is the single source of the string vocabulary the serialization dialects (JSON, YAML, TOML, URL) all map through: operators ("eq", "ne", "gt", ...), sort directions ("asc", "desc"), nulls positions ("first", "last"), and aggregate functions. |
|
json
Package r3json converts r3 query types (r3.Filters, r3.Sorts, r3.PaginationSpec, r3.Fields) to and from JSON for REST request/response bodies.
|
Package r3json converts r3 query types (r3.Filters, r3.Sorts, r3.PaginationSpec, r3.Fields) to and from JSON for REST request/response bodies. |
|
schema
Package r3schema serializes an r3.Schema to a stable, versioned, public-only JSON shape for introspection.
|
Package r3schema serializes an r3.Schema to a stable, versioned, public-only JSON shape for introspection. |
|
sql
Package r3sql translates r3 query types into SQL clauses: r3.Filters to WHERE with positional placeholders, r3.Sorts to ORDER BY, r3.PaginationSpec to LIMIT/OFFSET.
|
Package r3sql translates r3 query types into SQL clauses: r3.Filters to WHERE with positional placeholders, r3.Sorts to ORDER BY, r3.PaginationSpec to LIMIT/OFFSET. |
|
toml
Package r3toml converts r3 query types (r3.Filters, r3.Sorts, r3.PaginationSpec, r3.Fields) to and from TOML for configuration files, using human-readable names (field/operator/value) like the YAML dialect.
|
Package r3toml converts r3 query types (r3.Filters, r3.Sorts, r3.PaginationSpec, r3.Fields) to and from TOML for configuration files, using human-readable names (field/operator/value) like the YAML dialect. |
|
url
Package r3url is the serialization dialect between r3 query types and URL query parameters.
|
Package r3url is the serialization dialect between r3 query types and URL query parameters. |
|
when
Package r3when is the bridge between the years schedule vocabulary and r3 filters.
|
Package r3when is the bridge between the years schedule vocabulary and r3 filters. |
|
yaml
Package r3yaml converts r3 query types (r3.Filters, r3.Sorts, r3.PaginationSpec, r3.Fields) to and from YAML for configuration files.
|
Package r3yaml converts r3 query types (r3.Filters, r3.Sorts, r3.PaginationSpec, r3.Fields) to and from YAML for configuration files. |
|
drivers
|
|
|
bun
Package r3bun is an r3.CRUD[T, ID] driver backed by Bun (github.com/uptrace/bun), a SQL-first ORM for PostgreSQL, MySQL, MSSQL, and SQLite.
|
Package r3bun is an r3.CRUD[T, ID] driver backed by Bun (github.com/uptrace/bun), a SQL-first ORM for PostgreSQL, MySQL, MSSQL, and SQLite. |
|
gopg
Package r3gopg is an r3.CRUD[T, ID] driver backed by go-pg v10 (github.com/go-pg/pg/v10), a PostgreSQL ORM.
|
Package r3gopg is an r3.CRUD[T, ID] driver backed by go-pg v10 (github.com/go-pg/pg/v10), a PostgreSQL ORM. |
|
gorm
Package r3gorm is an r3.CRUD[T, ID] driver backed by GORM (gorm.io/gorm).
|
Package r3gorm is an r3.CRUD[T, ID] driver backed by GORM (gorm.io/gorm). |
|
mongo
Package r3mongo is an r3.CRUD[T, ID] driver backed by the official MongoDB Go driver v2 (go.mongodb.org/mongo-driver/v2).
|
Package r3mongo is an r3.CRUD[T, ID] driver backed by the official MongoDB Go driver v2 (go.mongodb.org/mongo-driver/v2). |
|
mysql
Package r3mysql is a raw-SQL r3.CRUD[T, ID] driver backed by go-sql-driver/mysql (github.com/go-sql-driver/mysql) over database/sql.
|
Package r3mysql is a raw-SQL r3.CRUD[T, ID] driver backed by go-sql-driver/mysql (github.com/go-sql-driver/mysql) over database/sql. |
|
pgx
Package r3pgx is a raw-SQL r3.CRUD[T, ID] driver backed by jackc/pgx (github.com/jackc/pgx/v5) in database/sql compatibility mode (pgx/v5/stdlib), so it runs through database/sql while keeping pgx's connection and type support.
|
Package r3pgx is a raw-SQL r3.CRUD[T, ID] driver backed by jackc/pgx (github.com/jackc/pgx/v5) in database/sql compatibility mode (pgx/v5/stdlib), so it runs through database/sql while keeping pgx's connection and type support. |
|
pq
Package r3pq is a raw-SQL r3.CRUD[T, ID] driver backed by lib/pq (github.com/lib/pq) over database/sql.
|
Package r3pq is a raw-SQL r3.CRUD[T, ID] driver backed by lib/pq (github.com/lib/pq) over database/sql. |
|
sqlite3
Package r3sqlite3 is a raw-SQL r3.CRUD[T, ID] driver backed by mattn/go-sqlite3 (github.com/mattn/go-sqlite3, CGo) over database/sql.
|
Package r3sqlite3 is a raw-SQL r3.CRUD[T, ID] driver backed by mattn/go-sqlite3 (github.com/mattn/go-sqlite3, CGo) over database/sql. |
|
engine
|
|
|
file
Package enginefile is the filesystem CRUD engine: it reads and writes entities to JSON or YAML files and evaluates every query feature (filters, sorts, pagination, field selection, soft-delete) in memory.
|
Package enginefile is the filesystem CRUD engine: it reads and writes entities to JSON or YAML files and evaluates every query feature (filters, sorts, pagination, field selection, soft-delete) in memory. |
|
mongo
Package enginemongo is the MongoDB engine for r3: a full r3.CRUD backed by the official Go driver v2, using reflection-based struct metadata and BSON query building.
|
Package enginemongo is the MongoDB engine for r3: a full r3.CRUD backed by the official Go driver v2, using reflection-based struct metadata and BSON query building. |
|
sql
Package enginesql is r3's SQL engine, serving two roles:
|
Package enginesql is r3's SQL engine, serving two roles: |
|
examples
|
|
|
02petstore
Package petstore demonstrates a full CRUD JSON API server using r3 with GORM and PostgreSQL.
|
Package petstore demonstrates a full CRUD JSON API server using r3 with GORM and PostgreSQL. |
|
02petstore/cmd
command
Pet Store example server.
|
Pet Store example server. |
|
features
|
|
|
history
Package history is a transparent decorator around any r3.CRUD[T, ID] that records every Create, Update, Patch, and Delete as a ChangeRecord.
|
Package history is a transparent decorator around any r3.CRUD[T, ID] that records every Create, Update, Patch, and Delete as a ChangeRecord. |
|
i18n
Package i18n adds entity-content translations to any r3.CRUD[T, ID].
|
Package i18n adds entity-content translations to any r3.CRUD[T, ID]. |
|
metrics
Package metrics decorates any r3.CRUD[T, ID] with domain-level analytics, recording configurable metrics for every CRUD operation.
|
Package metrics decorates any r3.CRUD[T, ID] with domain-level analytics, recording configurable metrics for every CRUD operation. |
|
permissions
Package permissions gates every CRUD operation through a caller-supplied authorization policy.
|
Package permissions gates every CRUD operation through a caller-supplied authorization policy. |
|
softdelete
Package softdelete decorates any r3.CRUD[T, ID] with Restore and HardDelete, transparently satisfying the same interface and passing all standard methods through to the inner repo.
|
Package softdelete decorates any r3.CRUD[T, ID] with Restore and HardDelete, transparently satisfying the same interface and passing all standard methods through to the inner repo. |
|
transactor
Package transactor decorates any r3.CRUD[T, ID] to surface the driver's transaction support, transparently satisfying the same interface and passing standard methods through to the inner repo.
|
Package transactor decorates any r3.CRUD[T, ID] to surface the driver's transaction support, transparently satisfying the same interface and passing standard methods through to the inner repo. |
|
validation
Package validation validates entities before mutations (Create, Update, Patch, Upsert, PatchWhere) on any r3.CRUD[T, ID].
|
Package validation validates entities before mutations (Create, Update, Patch, Upsert, PatchWhere) on any r3.CRUD[T, ID]. |
|
internal
|
|
|
tag
Package r3tag provides struct tag parsing for r3 entities.
|
Package r3tag provides struct tag parsing for r3 entities. |
|
utils
Package r3utils provides shared utility functions used across r3 packages.
|
Package r3utils provides shared utility functions used across r3 packages. |