Documentation
¶
Overview ¶
Package dynago is the runtime for code generated by the dynago tool.
dynago turns a schema file describing a DynamoDB table (its entities, keys, indexes, uniqueness claims, counters, and every read and write) into a typed Go store, a data model document, Terraform and a cost report. The generated store calls this package; application code uses it for the few things that cross stores:
- sentinel errors such as ErrNotFound, ErrTaken, ErrLimit and ErrVersionMismatch, which every generated error wraps;
- Page, to page through queries;
- Limit and Max, for counter limits supplied by the caller;
- From and IfVersion, for optimistic concurrency.
The remaining exported identifiers (Op, Diff, Run, QuerySpec and so on) are building blocks for generated code. They are stable, but not meant to be called by hand.
Documentation: https://github.com/nicklanng/dynago/tree/main/docs
Example ¶
A store is created from a guregu/dynamo DB and a table name; its fields hold one store per entity.
package main
import (
"context"
"errors"
"fmt"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/guregu/dynamo/v2"
"github.com/nicklanng/dynago/examples/toollibrary"
)
// These examples use the store generated from examples/toollibrary/toollibrary.dynago.yaml.
var ctx = context.Background()
func main() {
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
panic(err)
}
st := toollibrary.New(dynamo.New(cfg), "toollibrary-prod")
tool, err := st.Tools.Get(ctx, toollibrary.ToolKey{LibraryID: "lib_1", ToolID: "t_42"})
switch {
case errors.Is(err, toollibrary.ErrToolNotFound):
fmt.Println("no such tool")
case err != nil:
panic(err)
default:
fmt.Println(tool.Name, tool.Status)
}
}
Output:
Index ¶
- Constants
- Variables
- func CheckAbsent(t dynamo.Table, key Key, ttlAttr string) *dynamo.ConditionCheck
- func CheckCounter(t dynamo.Table, key Key, values []Cond) *dynamo.ConditionCheck
- func CheckKeyPart(field, value, separators string) error
- func CheckRequirement(t dynamo.Table, r Requirement) *dynamo.ConditionCheck
- func CondUpdate(u *dynamo.Update, c Cond)
- func ConsumeRequirement(t dynamo.Table, r Requirement) *dynamo.Delete
- func DeleteIfExists(ctx context.Context, t dynamo.Table, key Key, g Guard) error
- func Distinct(s []string) []string
- func EnsureTable(ctx context.Context, db *dynamo.DB, name string, spec TableSpec) error
- func Expired(ttl, now int64) bool
- func ExpiredItem(raw dynamo.Item, ttlAttr string) bool
- func FmtBool(b bool) string
- func FmtInt(i int64) string
- func FmtStamp(t time.Time) string
- func FmtString[S ~string](s S) string
- func FmtTime(t time.Time) string
- func FormatVersion(rev int64) string
- func GetMany(ctx context.Context, t dynamo.Table, keys []Key, consistent bool, out any) error
- func GetOne(ctx context.Context, t dynamo.Table, key Key, consistent bool, out any) (found bool, err error)
- func GuardUpdate(u *dynamo.Update, g Guard, now int64)
- func ItemRev(raw dynamo.Item) int64
- func ItemType(raw dynamo.Item) string
- func KeepUnknown(raw dynamo.Item, known map[string]bool, item any) (dynamo.Item, error)
- func Lower(s string) string
- func MigratedFrom(raw dynamo.Item, src Key, srcRev int64) (same, other bool)
- func MigrationItem(item any, src Key, srcRev int64) (dynamo.Item, error)
- func NeedsRead(err error) bool
- func NewRev() int64
- func NewStamp() string
- func Now() int64
- func ParseMigrate(args []string, base string) (command, table string, workers, passes int, rate float64, err error)
- func ParseStamp(s string) time.Time
- func ParseVersion(s string) (int64, error)
- func Path(attr string) string
- func Ptr[T any](v T) *T
- func Query(ctx context.Context, t dynamo.Table, spec QuerySpec, page Page, out any) (string, error)
- func ReadIfNeeded(fn func(read bool) error) error
- func RequireLimit(name string, l Limit) error
- func Retry(ctx context.Context, fn func() error) error
- func Run(ctx context.Context, db *dynamo.DB, ops []Op) error
- func Scan(ctx context.Context, t dynamo.Table, spec ScanSpec, page Page, out any) (string, error)
- func SetFields(u *dynamo.Update, sets []Set)
- func SetRetries(p RetryPolicy)
- func ShardPK(pk string, owner Key, shards int) string
- func ShardPKs(pk string, shards int) []string
- func SignCursors(key []byte, previous ...[]byte)
- func StaleAs(versioned bool, err error) error
- func UnixTTL(t time.Time) int64
- func UpdateFields(ctx context.Context, t dynamo.Table, key Key, sets []Set, when []Cond, g Guard, ...) (int64, error)
- type Change
- type Claim
- type Cond
- type Derived
- type DerivedKind
- type GSISpec
- type Guard
- type Key
- type Limit
- type Migration
- type Op
- func CheckOp(key Key, c *dynamo.ConditionCheck, condErr error) Op
- func CreateOp(key Key, p *dynamo.Put, condErr error, rev int64) Op
- func DeleteOp(key Key, d *dynamo.Delete, condErr error) Op
- func Diff(t dynamo.Table, owner Key, before, after []Derived) ([]Op, error)
- func DiffAll(t dynamo.Table, changes []Change) ([]Op, error)
- func PutOp(key Key, p *dynamo.Put, condErr error) Op
- func UpdateOp(key Key, u *dynamo.Update, condErr error) Op
- type OpError
- type Page
- type QuerySpec
- type Requirement
- type RetryPolicy
- type ScanSpec
- type Set
- type TableSpec
- type Timestamps
- type WriteOption
- type WriteOptions
Examples ¶
Constants ¶
const ( AttrPK = "PK" AttrSK = "SK" AttrType = "_t" AttrVer = "_v" AttrRev = "_rev" // AttrCreated and AttrUpdated hold when dynago first wrote an item and when it last changed // it, in TimeLayout. Items written before dynago kept them lack them until rewritten. AttrCreated = "_created" AttrUpdated = "_updated" )
Attribute names written on every item.
const MigrateUsage = `` /* 475-byte string literal not displayed */
MigrateUsage describes the migration command's arguments, for generated commands.
const RangeEnd = "\U0010FFFF"
RangeEnd is appended to a sort key prefix to form an inclusive upper bound that sorts after every key starting with that prefix.
const TimeLayout = "2006-01-02T15:04:05.000000000Z"
TimeLayout is the fixed-width UTC layout used for times in keys, so they sort chronologically.
Variables ¶
var ( // ErrNotFound means the item does not exist. ErrNotFound = errors.New("dynago: not found") // ErrExists means a create found an item already at that key. ErrExists = errors.New("dynago: already exists") // ErrPrecondition means a write's declared condition did not hold: an update's `when`, or // something it `requires` of another item. ErrPrecondition = errors.New("dynago: precondition failed") // ErrTaken means a unique value is already claimed by another item. ErrTaken = errors.New("dynago: unique value already taken") // ErrLimit means a write would take a limited counter past its limit. ErrLimit = errors.New("dynago: counter limit reached") // ErrConflict means the item kept changing under a read-modify-write until retries ran out. ErrConflict = errors.New("dynago: concurrent modification") // ErrInvalidCursor means a page cursor was malformed or belongs to a different query. ErrInvalidCursor = errors.New("dynago: invalid cursor") // ErrInvalidKey means a key field was empty, or contained a character its key uses as a // separator. ErrInvalidKey = errors.New("dynago: invalid key") // ErrFieldRequired means a write left a field declared `required: true` at its zero value. ErrFieldRequired = errors.New("dynago: required field is empty") // ErrLimitRequired means a write that takes a caller-supplied limit was not given one. ErrLimitRequired = errors.New("dynago: limit required") // ErrSameItemTwice means a write would touch one item twice in a transaction, which DynamoDB // rejects. It is a schema shape dynago should have refused when generating: please report it. ErrSameItemTwice = errors.New("dynago: a transaction would touch the same item twice") // ErrTooManyItems means a write would touch more than DynamoDB's 100-item transaction limit. ErrTooManyItems = errors.New("dynago: write touches more than 100 items") // ErrNeedsRead is returned internally when a write tried without reading the item found that // the state it assumed did not hold. Generated writes then read the item and try again; callers // never see it. ErrNeedsRead = errors.New("dynago: write must read the item first") // ErrStale is returned internally when an item changed between a read and the conditional // write that followed it. Generated writes retry on it; callers see ErrConflict if retries run out. ErrStale = errors.New("dynago: item changed since it was read") // ErrContention is returned when DynamoDB cancels a transaction because another transaction // touched the same item. Generated writes retry on it. ErrContention = errors.New("dynago: transaction conflict") )
Sentinel errors. Generated code wraps these in entity-specific errors, so both errors.Is(err, dynago.ErrNotFound) and errors.Is(err, mytable.ErrEventNotFound) work.
var ( // ErrVersionMismatch means the item changed since the version the caller supplied: someone // else wrote it in between. Writes never retry it; the caller decides (reload, merge, report). ErrVersionMismatch = errors.New("dynago: item changed since the version supplied") // ErrVersionRequired means the write is declared `versioned: required` and no version was // supplied, or the entity passed with From was not read from the store. ErrVersionRequired = errors.New("dynago: this write requires the version that was read") )
var DefaultRetries = RetryPolicy{Attempts: 8, BaseDelay: 5 * time.Millisecond, MaxDelay: 250 * time.Millisecond}
DefaultRetries is the policy generated writes use unless SetRetries changes it: 8 attempts, waiting 5 ms, 10 ms, ... up to 250 ms between them, about half a second in total. That rides out bursts of contention on a busy counter.
var ErrLeaseLost = errors.New("dynago: another migration job holds the lease")
ErrLeaseLost means another migration job took over: this one stops without writing more.
var ErrMigrationConflict = errors.New("dynago: item can't be copied")
ErrMigrationConflict is wrapped by errors a migration reports as conflicts: items it can't copy until a person fixes the data in the old table (two old items converting to one new key, a value a conversion rejects). Claims taken, invalid keys and empty required fields are conflicts too. Other errors stop the job.
var ErrUnchanged = errors.New("dynago: already copied at this revision")
ErrUnchanged is returned by a migration's Copy when the new table already holds the item at the source's revision, or when the item isn't copied at all (it has expired).
Functions ¶
func CheckAbsent ¶
CheckAbsent builds a condition check that an item is absent or expired. A write that read an optional item and found none uses it to fail if one appeared before the write commits.
func CheckCounter ¶
CheckCounter builds a condition check on a counter item's values. A missing value (or item) counts as zero.
func CheckKeyPart ¶
CheckKeyPart rejects a value that contains a character the key template uses to separate its parts: "a#b" + "c" and "a" + "b#c" would otherwise render the same key. The value is checked as given and as {field|lower} renders it, since lower-casing (and Unicode normalisation) can produce a separator: "AX" before a literal "x", or the Kelvin sign becoming "k".
func CheckRequirement ¶
func CheckRequirement(t dynamo.Table, r Requirement) *dynamo.ConditionCheck
CheckRequirement builds a condition check that another item meets a requirement.
func CondUpdate ¶
CondUpdate adds an equality precondition to an update.
func ConsumeRequirement ¶
func ConsumeRequirement(t dynamo.Table, r Requirement) *dynamo.Delete
ConsumeRequirement builds the deletion of another item that meets a requirement.
func DeleteIfExists ¶
DeleteIfExists deletes an item that has no derived items, returning the Guard's errors if it is absent, expired or at another revision.
func Distinct ¶
Distinct returns s without repeated values, keeping the first of each: a list converted to a set, which can't hold duplicates.
func EnsureTable ¶
EnsureTable creates the table if it does not exist, waits for it to become active and enables TTL. It is meant for local development and tests; production tables belong in infrastructure code (dynago generates Terraform for them).
func Expired ¶
Expired reports whether a TTL attribute value (epoch seconds, 0 for none) has passed. DynamoDB deletes expired items lazily, often hours later, so reads must check.
func ExpiredItem ¶
ExpiredItem reports whether a stored item's TTL attribute has passed.
func FormatVersion ¶
FormatVersion renders a revision as an opaque version string, suitable for an ETag.
func GetMany ¶
GetMany reads items by key with one BatchGetItem (chunked by the library at 100 keys) and appends the ones that exist to out, which must be a pointer to a slice.
func GetOne ¶
func GetOne(ctx context.Context, t dynamo.Table, key Key, consistent bool, out any) (found bool, err error)
GetOne reads one item into out. It reports found=false rather than an error when the item does not exist.
func GuardUpdate ¶
GuardUpdate adds a Guard's conditions to an update.
func KeepUnknown ¶
KeepUnknown returns item, marshalled, with every attribute of raw (the stored item it replaces) that known doesn't name. Code at an older compatible version rewriting an item that newer code wrote must not drop the fields it doesn't know: within a table generation, versions only add or remove plain fields, so keeping what it doesn't know is always safe.
func Lower ¶
Lower is the `lower` key template transform: {name|lower} sorts and matches case-insensitively. It normalises to NFC first, so an "é" typed as one code point and as "e" plus a combining accent render the same key. Keys still sort by bytes, not by any language's alphabet: "émile" sorts after "zoe".
func MigratedFrom ¶
MigratedFrom inspects an item of the new table: same reports whether it was copied from src at srcRev; other reports whether it was copied from a different source item (two old items converting to one new key).
func MigrationItem ¶
MigrationItem returns item, marshalled, with the source item's key and revision recorded on it.
func NeedsRead ¶
NeedsRead reports whether a write attempted without reading the item found that its assumptions did not hold, so it must be retried by reading first.
func NewRev ¶
func NewRev() int64
NewRev returns the starting revision for a new item. It is random rather than 1 so that a read-modify-write guarded by a revision read before a delete and re-create cannot mistake the new item for the one it read. It stays below 2^52 so it survives a trip through a float64 (JSON, JavaScript) exactly.
func NewStamp ¶
func NewStamp() string
NewStamp returns the current time as dynago stores it in _created and _updated.
func ParseMigrate ¶
func ParseMigrate(args []string, base string) (command, table string, workers, passes int, rate float64, err error)
ParseMigrate reads the migration command's arguments.
func ParseStamp ¶
ParseStamp reads a stored timestamp; "" (or anything unreadable) is the zero time.
func ParseVersion ¶
ParseVersion parses a version string made by FormatVersion.
func Path ¶
Path quotes an attribute name for guregu's update builders unless it is a plain identifier. Names such as "_rev" are not valid bare identifiers in DynamoDB expressions; quoting makes guregu substitute a placeholder for them.
func Ptr ¶
func Ptr[T any](v T) *T
Ptr returns a pointer to v, for optional (patch) update fields: dynago.Ptr("new title").
Example ¶
Ptr makes the pointers that optional (patch) fields take: nil leaves a field unchanged.
package main
import (
"context"
"github.com/nicklanng/dynago"
"github.com/nicklanng/dynago/examples/toollibrary"
)
// These examples use the store generated from examples/toollibrary/toollibrary.dynago.yaml.
var (
ctx = context.Background()
st *toollibrary.Store
)
func main() {
key := toollibrary.MemberKey{LibraryID: "lib_1", MemberID: "m_7"}
// Only the phone number changes; the name is left as it is.
_ = st.Members.UpdateProfile(ctx, key, toollibrary.MemberUpdateProfile{Phone: dynago.Ptr("555-0100")})
}
Output:
func Query ¶
Query runs exactly one Query request for one page and appends the items to out, which must be a pointer to a slice. It returns the cursor for the next page, or "" when there are no more.
func ReadIfNeeded ¶
ReadIfNeeded runs a write that also changes other items. It first builds the changes assuming those items are in the state the schema requires, without reading them; if that assumption fails (ErrNeedsRead), it runs again reading them, which reports precisely what was wrong.
func RequireLimit ¶
RequireLimit returns ErrLimitRequired if l was not given. Generated writes call it for every limit they take.
func Retry ¶
Retry runs a write, retrying on ErrStale and ErrContention under the retry policy (see SetRetries). After the last attempt it returns ErrConflict.
func Run ¶
Run applies ops atomically: a single op runs as a plain write (half the cost of a transaction), several run as one TransactWriteItems. A failed condition is reported as the error registered for the op that failed.
func Scan ¶
Scan runs exactly one Scan request for one page and appends the entity's items to out, which must be a pointer to a slice. A page evaluates up to its size of the table's items, of every kind, and keeps the entity's: it can hold few items, or none, and still have a next cursor. It returns the cursor for the next page, or "" when the whole table has been read.
func SetFields ¶
SetFields applies attribute changes to an update, bumps the item's revision and stamps it as updated now.
func SetRetries ¶
func SetRetries(p RetryPolicy)
SetRetries sets the policy generated writes use. It is safe to call while writes run.
func ShardPK ¶
ShardPK appends the shard an owner's contributions land on to a counter partition key.
func SignCursors ¶
SignCursors makes page cursors carry an HMAC-SHA256 under key, so a client cannot forge or edit one. Every instance must use the same key. Cursors signed with any of previous are still accepted, so the key can be rotated: deploy the new key with the old one as previous, then drop the old one once its cursors have expired from clients. SignCursors(nil) turns signing off. It is safe to call while queries run.
Without signing, cursors are only checked to belong to the query, partition and range bounds that produced them.
Example ¶
SignCursors makes page cursors tamper-proof. Call it once at startup with a secret shared by every instance.
package main
import (
"github.com/nicklanng/dynago"
)
func main() {
dynago.SignCursors([]byte("a secret from your secret store"))
}
Output:
func StaleAs ¶
StaleAs reports a write that lost a race with another writer as ErrVersionMismatch when the caller supplied a version (the caller must decide what to do), and leaves it as ErrStale (which Retry retries) otherwise.
func UnixTTL ¶
UnixTTL converts a time to the epoch seconds DynamoDB's TTL expects; zero stays zero.
func UpdateFields ¶
func UpdateFields(ctx context.Context, t dynamo.Table, key Key, sets []Set, when []Cond, g Guard, precondition error) (int64, error)
UpdateFields changes attributes of an existing item in one UpdateItem, bumping its revision, and returns the new revision. It is used for updates that feed no counters, claims, copies or index keys, so no read is needed. Besides the Guard's errors it returns precondition if a Cond fails.
Types ¶
type Change ¶
Change is one item's derived items before and after a write. A write that also changes other entities (through requires) has one Change per entity it changes.
type Cond ¶
Cond is an equality precondition. Zero marks the field's zero value, which may be stored as an absent attribute.
type Derived ¶
type Derived struct {
Kind DerivedKind
Key Key
// Type is the _t value stored on the derived item.
Type string
// Counter contribution.
Attr string
Amount int64
Limit Limit
LimitErr error
// LimitRequired marks a value whose limit the caller must supply: growing it without one fails.
LimitRequired bool
// Min is a lower bound on the counter value; MinErr is returned when a write would cross it.
Min Limit
MinErr error
// Claim.
TakenErr error
// Copy: the full item to store, including its PK, SK, _t and _v attributes. Created is the
// owner's creation time (TimeLayout), stamped on the copy; "" for a new owner, stamped now.
Item any
Created string
}
Derived describes an item that exists because of an entity's state. Generated code computes the derived items of an entity before and after a write; Diff turns the difference into writes.
type DerivedKind ¶
type DerivedKind int
DerivedKind is the kind of item maintained alongside an entity item.
const ( // KindCounter is one entity's contribution to one attribute of a counter item. KindCounter DerivedKind = iota + 1 // KindClaim is a uniqueness claim owned by the entity item. KindClaim // KindCopy is a copy item holding a projection of the entity, used as a transactional index. KindCopy )
Kinds of derived items.
type GSISpec ¶
type GSISpec struct {
Name string `json:"name"`
PKAttr string `json:"pk"`
// SKAttr is "" for an index without a sort key.
SKAttr string `json:"sk,omitempty"`
// Projection is "ALL" or "INCLUDE".
Projection string `json:"projection"`
NonKeyAttrs []string `json:"non_key_attributes,omitempty"`
}
GSISpec is one global secondary index.
type Guard ¶
type Guard struct {
// ExpectRev, if non-zero, is the revision the caller read (a document version).
ExpectRev int64
// TTLAttr, if set, makes an expired item count as absent.
TTLAttr string
// NotFound is returned if the item is absent (or expired).
NotFound error
}
Guard is the conditions every write to an existing item carries.
type Limit ¶
Limit is a caller-supplied cap for a counter value declared with `limit: arg`. Build it with Max or Unlimited: the zero Limit is "not given", which generated writes reject with ErrLimitRequired, so a forgotten limit can never silently switch enforcement off.
func Max ¶
Max returns a Limit of n.
Example ¶
Max caps a counter value declared with `limit: arg`, here how many tools a member may have out. The counter update is conditioned on the limit, so concurrent borrows can never exceed it. A limit must always be given: pass Unlimited to switch it off on purpose.
package main
import (
"context"
"errors"
"fmt"
"time"
"github.com/nicklanng/dynago"
"github.com/nicklanng/dynago/examples/toollibrary"
)
// These examples use the store generated from examples/toollibrary/toollibrary.dynago.yaml.
var (
ctx = context.Background()
st *toollibrary.Store
)
func main() {
member, err := st.Members.Get(ctx, toollibrary.MemberKey{LibraryID: "lib_1", MemberID: "m_7"})
if err != nil {
panic(err)
}
err = st.Loans.Borrow(ctx, &toollibrary.Loan{
LibraryID: "lib_1", ToolID: "t_42", LoanID: "ln_9", MemberID: "m_7",
BorrowedAt: time.Now(), DueAt: time.Now().Add(7 * 24 * time.Hour),
}, toollibrary.LoanBorrowLimits{MemberLoansActive: dynago.Max(member.MaxLoans)})
switch {
case errors.Is(err, toollibrary.ErrMemberLoansActiveLimit):
fmt.Println("return something first")
case errors.Is(err, toollibrary.ErrLoanBorrowRequiresTool):
fmt.Println("that tool is out")
case errors.Is(err, toollibrary.ErrLoanBorrowRequiresHold):
fmt.Println("that tool is reserved for someone else")
}
}
Output:
type Migration ¶
type Migration struct {
DB *dynamo.DB
From, To string
// Types lists the entity types (their _t values) to copy. Other items of the old table
// (claims, copies, counters, and entities this generation dropped) are not copied: the new
// table's derived items are rebuilt from the entities.
Types map[string]bool
// Copy writes the entity converted from one item of the old table into the new one, with its
// derived items and fence (an op the write must include in its transaction). It returns
// ErrUnchanged if the new table already has that revision, or the item isn't copied.
Copy func(ctx context.Context, raw dynamo.Item, fence Op) error
// KeyOf returns the key an item of the old table converts to in the new one, or ok false if
// it isn't copied (expired, or not an entity of this generation).
KeyOf func(raw dynamo.Item) (key Key, ok bool, err error)
// Remove deletes an entity of the new table, with its derived items and fence, because the
// item it was copied from is gone or now converts to another key. It returns ErrUnchanged if
// the entity is gone already, or has been rewritten since raw was read (a fresher copy).
Remove func(ctx context.Context, raw dynamo.Item, fence Op) error
// Check reports why the migration can't run at all (a conversion function not provided), or nil.
Check func() error
// TTLAttr is the TTL attribute: expired items of the old table are not copied.
TTLAttr string
Workers int // segments scanned at once (and segments per new pass); default 8
Rate float64 // items per second across workers; 0 for no limit
MaxPasses int // catch-up passes after the first, for copy; default 3
Out io.Writer
// LeaseFor is how long a job holds the lease without renewing it; default 2 minutes. The
// lease is renewed every third of it.
LeaseFor time.Duration
}
Migration copies one table generation into the next.
type Op ¶
type Op struct {
// contains filtered or unexported fields
}
Op is one item write, with the error to report if its condition is what failed.
func CheckOp ¶
func CheckOp(key Key, c *dynamo.ConditionCheck, condErr error) Op
CheckOp wraps a condition check on the item at key. condErr is returned when the check fails.
func CreateOp ¶
CreateOp wraps the put that creates the item at key with revision rev. condErr is returned when the item already exists, unless it is this very write repeated by an SDK retry.
func DeleteOp ¶
DeleteOp wraps a delete of the item at key. condErr is returned when its condition fails.
func Diff ¶
Diff returns the writes that move the derived items of owner from before to after. See DiffAll.
func DiffAll ¶
DiffAll returns the writes that apply every change of one transaction:
- counters get one ADD per changed item, conditioned on their limits when they grow. Changes to the same counter item from different owners are merged, since a transaction may touch an item only once (two accounts of one tenant moving between states in one write);
- claims are created (conditioned on being free or already the owner's) or released;
- copies are put when new or changed and deleted when gone.
type OpError ¶
OpError is a write's failed condition, carrying the key of the item whose condition failed. It unwraps to the error registered for that item, so errors.Is works as with the bare error; errors.As gives the key, which the migration job uses to find who holds a claim.
type Page ¶
type Page struct {
// Size is the maximum number of items to evaluate. Zero means the access pattern's default;
// sizes above its maximum are clamped.
Size int
// Cursor is the Next value returned by the previous page, or "" for the first page.
Cursor string
}
Page selects one page of a query. The zero Page asks for the first page at the default size.
Example ¶
Page selects one page of a query. Every query method makes exactly one request per call and returns the cursor for the next page, "" at the end.
package main
import (
"context"
"fmt"
"github.com/nicklanng/dynago"
"github.com/nicklanng/dynago/examples/toollibrary"
)
// These examples use the store generated from examples/toollibrary/toollibrary.dynago.yaml.
var (
ctx = context.Background()
st *toollibrary.Store
)
func main() {
q := toollibrary.ToolCatalogueQuery{LibraryID: "lib_1", Category: toollibrary.ToolCategoryPower}
page := dynago.Page{Size: 25}
for {
tools, next, err := st.Tools.Catalogue(ctx, q, page)
if err != nil {
panic(err)
}
for _, t := range tools {
fmt.Println(t.Name, t.Status)
}
if next == "" {
break
}
page.Cursor = next
}
}
Output:
type QuerySpec ¶
type QuerySpec struct {
// Scope identifies the access pattern and partition; a cursor is only valid for the scope
// that produced it.
Scope string
// Index is the GSI name, or "" for the base table.
Index string
PKAttr string
PK string
SKAttr string
// Prefix restricts the query to sort keys starting with it (the item family's prefix).
Prefix string
// From and To, when set, bound the sort key inclusively. They already include Prefix.
From, To *string
Desc bool
Consistent bool
PageSize int
MaxPage int
// TTLAttr, if set, leaves out items whose TTL has passed but that DynamoDB has not yet deleted.
// Such a page may hold fewer items than its size and still have a next cursor.
TTLAttr string
// Project, if set, reads only these attributes. It saves bandwidth and keeps other attributes
// from callers; DynamoDB still charges for the whole item.
Project []string
}
QuerySpec describes one generated query.
type Requirement ¶
type Requirement struct {
Key Key
// When lists values the item must have.
When []Cond
// TTLAttr, if set, makes an expired item count as absent.
TTLAttr string
// Optional lets an absent (or expired) item pass: When applies only to an item that is there.
Optional bool
}
Requirement is the state a write requires of another item, checked in the write's transaction.
type RetryPolicy ¶
type RetryPolicy struct {
Attempts int
BaseDelay time.Duration
// MaxDelay caps each delay; zero means DefaultRetries.MaxDelay.
MaxDelay time.Duration
}
RetryPolicy controls how generated writes retry when the item changed under them (ErrStale) or another transaction held one of their items (ErrContention). Delays grow exponentially from BaseDelay up to MaxDelay, with jitter: each wait is between half and one and a half times the delay.
type ScanSpec ¶
type ScanSpec struct {
// Scope identifies the access pattern; a cursor is only valid for the scope that produced it.
Scope string
// Type is the entity's type attribute (_t): the scan returns only its items.
Type string
Consistent bool
PageSize int
MaxPage int
// TTLAttr, if set, leaves out items whose TTL has passed.
TTLAttr string
}
ScanSpec describes one generated scan: a declared read of every item of one entity.
type Set ¶
type Set struct {
Attr string
Value any
Remove bool
// StringSet stores Value as a DynamoDB string set rather than a list.
StringSet bool
}
Set is one attribute change in an update. Zero values are removed rather than stored, which matches how items are written (every non-key attribute is omitted when empty).
type TableSpec ¶
type TableSpec struct {
TTLAttr string `json:"ttl_attribute,omitempty"`
GSIs []GSISpec `json:"gsis,omitempty"`
}
TableSpec is the physical shape of a table: its GSIs and TTL attribute. The base key is always PK (hash) and SK (range), both strings. Billing is on-demand.
func (TableSpec) CreateTableInput ¶
func (s TableSpec) CreateTableInput(name string) *dynamodb.CreateTableInput
CreateTableInput builds the CreateTable request for a table named name.
type Timestamps ¶
Timestamps say when an item was first written and last changed. They come from the clock of the server that wrote it, so across servers they are ordered only as well as those clocks agree. A zero time means unknown: the item was written before dynago kept timestamps.
func StampsOf ¶
func StampsOf(raw dynamo.Item) Timestamps
StampsOf reads the timestamps of a stored item.
type WriteOption ¶
type WriteOption func(*WriteOptions)
WriteOption modifies an update or delete.
func From ¶
func From(entity any) WriteOption
From makes the write start from an entity the store returned earlier (by Get, a query, or a unique lookup) instead of reading the item again. The write fails with ErrVersionMismatch if the item has changed since that entity was read, so no update can be lost.
Example ¶
From starts a write from an entity the store returned: the write skips its own read, and fails with ErrVersionMismatch if the item changed since, so decisions made on the entity still hold.
package main
import (
"context"
"errors"
"fmt"
"time"
"github.com/nicklanng/dynago"
"github.com/nicklanng/dynago/examples/toollibrary"
)
// These examples use the store generated from examples/toollibrary/toollibrary.dynago.yaml.
var (
ctx = context.Background()
st *toollibrary.Store
)
func main() {
key := toollibrary.LoanKey{LibraryID: "lib_1", ToolID: "t_42", LoanID: "ln_9"}
loan, err := st.Loans.Get(ctx, key)
if err != nil {
panic(err)
}
if loan.DueAt.Sub(loan.BorrowedAt) > 28*24*time.Hour {
return // business rule decided on the loaded entity: no more extensions
}
err = st.Loans.Extend(ctx, key, toollibrary.LoanExtend{DueAt: loan.DueAt.Add(7 * 24 * time.Hour)}, dynago.From(loan))
if errors.Is(err, dynago.ErrVersionMismatch) {
fmt.Println("the loan changed; reload and decide again")
}
}
Output:
func IfVersion ¶
func IfVersion(v string) WriteOption
IfVersion makes the write fail with ErrVersionMismatch unless the item is still at version v (from the entity's Version method, typically round-tripped through an ETag). An empty v means no check, so a missing If-Match header can be passed straight through.
Writes that change nothing derived stay a single conditional UpdateItem; writes that maintain counters, claims or copies still read the item to compute their changes, but fail instead of retrying when the version differs.
Example ¶
IfVersion carries a version across requests, typically as an ETag, so a stale edit form cannot overwrite a newer change. ReturnVersion hands back the new version for the response.
package main
import (
"context"
"errors"
"net/http"
"strconv"
"strings"
"github.com/nicklanng/dynago"
"github.com/nicklanng/dynago/examples/toollibrary"
)
// These examples use the store generated from examples/toollibrary/toollibrary.dynago.yaml.
var st *toollibrary.Store
func main() {
key := toollibrary.ToolKey{LibraryID: "lib_1", ToolID: "t_42"}
get := func(w http.ResponseWriter, r *http.Request) {
tool, _ := st.Tools.Get(r.Context(), key)
w.Header().Set("ETag", strconv.Quote(tool.Version())) // ETags are quoted (RFC 9110)
}
put := func(w http.ResponseWriter, r *http.Request) {
var version string
err := st.Tools.EditDetails(r.Context(), key, toollibrary.ToolEditDetails{Name: dynago.Ptr(r.FormValue("name"))},
dynago.IfVersion(strings.Trim(strings.TrimPrefix(r.Header.Get("If-Match"), "W/"), `"`)), dynago.ReturnVersion(&version))
if errors.Is(err, dynago.ErrVersionMismatch) {
http.Error(w, "changed by someone else; reload", http.StatusPreconditionFailed)
return
}
w.Header().Set("ETag", strconv.Quote(version))
}
_, _ = get, put
}
Output:
func ReturnVersion ¶
func ReturnVersion(dst *string) WriteOption
ReturnVersion stores the item's new version in dst after a successful write, so a handler can return it as an ETag without reading the item again.
type WriteOptions ¶
type WriteOptions struct {
// Version is an expected version from IfVersion, or "" for none.
Version string
// From is an entity previously returned by the store, or nil.
From any
// NewVersion, if set, receives the item's version after a successful write.
NewVersion *string
}
WriteOptions are the resolved options of a write. Generated code reads them.
func ApplyOptions ¶
func ApplyOptions(opts []WriteOption) WriteOptions
ApplyOptions resolves write options.
func (WriteOptions) ExpectedRev ¶
func (o WriteOptions) ExpectedRev() (int64, error)
ExpectedRev returns the revision the caller expects from IfVersion, 0 for none.
func (WriteOptions) Written ¶
func (o WriteOptions) Written(rev int64)
Written records a successful write's new revision for ReturnVersion.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
dynago
command
Command dynago generates typed DynamoDB access code, documentation and infrastructure from a schema file, analyses the design, compares designs for review, and finds DynamoDB calls that bypass the schema.
|
Command dynago generates typed DynamoDB access code, documentation and infrastructure from a schema file, analyses the design, compares designs for review, and finds DynamoDB calls that bypass the schema. |
|
Package dynagotest connects tests to DynamoDB Local and gives each test its own table.
|
Package dynagotest connects tests to DynamoDB Local and gives each test its own table. |
|
examples
|
|
|
toollibrary
Package toollibrary is the dynago example: a fictional neighbourhood tool library.
|
Package toollibrary is the dynago example: a fictional neighbourhood tool library. |
|
toollibrary/cmd/migrate-toollibrary
command
Command migrate-toollibrary copies the previous generation of the toollibrary table into the current one.
|
Command migrate-toollibrary copies the previous generation of the toollibrary table into the current one. |
|
internal
|
|
|
analysis
Package analysis reasons about a schema as a design: how its items group into partitions, how big and busy each partition gets under the declared workload, what the design guarantees, and which choices are risky.
|
Package analysis reasons about a schema as a design: how its items group into partitions, how big and busy each partition gets under the declared workload, what the design guarantees, and which choices are risky. |
|
arch
Package arch captures a schema's analysed design as a plain snapshot (what `dynago check -json` prints), and compares two snapshots as an architecture diff for review.
|
Package arch captures a schema's analysed design as a plain snapshot (what `dynago check -json` prints), and compares two snapshots as an architecture diff for review. |
|
cost
Package cost estimates item sizes, capacity units and monthly cost for a schema.
|
Package cost estimates item sizes, capacity units and monthly cost for a schema. |
|
e2e/fixture
Package fixture is a test-only store for shapes the example doesn't use.
|
Package fixture is a test-only store for shapes the example doesn't use. |
|
e2e/rekey
Package rekey is a test-only store whose second table generation changes an entity's key: people were keyed by id, and are keyed by email.
|
Package rekey is a test-only store whose second table generation changes an entity's key: people were keyed by id, and are keyed by email. |
|
gen/docs
Package docs renders a schema as a Markdown data model document with Mermaid diagrams.
|
Package docs renders a schema as a Markdown data model document with Mermaid diagrams. |
|
gen/gocode
Package gocode generates the typed Go store for a schema.
|
Package gocode generates the typed Go store for a schema. |
|
gen/infra
Package infra renders the table definition as Terraform JSON and as a CreateTable request.
|
Package infra renders the table definition as Terraform JSON and as a CreateTable request. |
|
keytmpl
Package keytmpl parses key templates such as "LIB#{libraryId}#TOOL#{toolId}".
|
Package keytmpl parses key templates such as "LIB#{libraryId}#TOOL#{toolId}". |
|
lock
Package lock keeps the history of each entity's storage shape, and of the table generations, in a checked-in lock file.
|
Package lock keeps the history of each entity's storage shape, and of the table generations, in a checked-in lock file. |
|
schema
Package schema parses a dynago schema file, validates it, and resolves it into the model the generators and the cost report work from.
|
Package schema parses a dynago schema file, validates it, and resolves it into the model the generators and the cost report work from. |
|
testdb
Package testdb gives dynago's own tests a DynamoDB: DynamoDB Local by default (through dynagotest), or real tables in an AWS account for the maintainer's release checks.
|
Package testdb gives dynago's own tests a DynamoDB: DynamoDB Local by default (through dynagotest), or real tables in an AWS account for the maintainer's release checks. |
|
testdb/sweep
command
Command sweep deletes the tables dynago's AWS test runs left behind: tables named dynagotest-… and tagged with a creation time more than an hour ago (-age changes it).
|
Command sweep deletes the tables dynago's AWS test runs left behind: tables named dynagotest-… and tagged with a creation time more than an hour ago (-age changes it). |
|
vet
Package vet finds DynamoDB calls made outside generated code: requests through the AWS SDK's DynamoDB client, guregu/dynamo, or dynago's runtime, which bypass the schema's declared reads and writes, and function values that would (`retry(c.PutItem)`).
|
Package vet finds DynamoDB calls made outside generated code: requests through the AWS SDK's DynamoDB client, guregu/dynamo, or dynago's runtime, which bypass the schema's declared reads and writes, and function values that would (`retry(c.PutItem)`). |