Documentation
¶
Overview ¶
Package seed is a generic, idempotent, row-asserting seed runner. It replaces hand-rolled seed scripts with declarative, ordered seed steps that verify their own effect: applying a step is never trusted on its own, because a clean exit is not proof that any row was written.
An app declares Steps, each with an idempotent Apply hook and an Assert hook that checks the expected rows/state exist, then runs them in order with a Runner. The core is storage-agnostic: the target is a type parameter, so a step can run against a *sql.DB, a repository, or an in-memory store. For the common relational case, RowSpec builds a Step from a declarative write plus a count assertion against the database/sql Executor surface.
Logging uses the standard library log/slog; there is no third-party logging dependency.
Example ¶
Example declares two ordered seed steps and runs them. Each step applies an idempotent write and then asserts the expected state exists.
package main
import (
"context"
"fmt"
"io"
"log/slog"
seed "github.com/Bugs5382/go-seed"
)
// roleStore is a tiny in-memory target, standing in for whatever storage an app
// seeds. Because the runner is generic over the target type, no database or
// driver is needed to demonstrate it.
type roleStore struct{ roles map[string]bool }
// quiet discards the runner's own log output so example output is deterministic.
func quiet() seed.Option {
return seed.WithLogger(slog.New(slog.NewTextHandler(io.Discard, nil)))
}
func main() {
db := &roleStore{roles: map[string]bool{}}
runner := seed.New(db, quiet()).Add(
seed.Step[*roleStore]{
Name: "admin-role",
Apply: func(_ context.Context, s *roleStore) error { s.roles["admin"] = true; return nil },
Assert: assertRole("admin"),
},
seed.Step[*roleStore]{
Name: "auditor-role",
Apply: func(_ context.Context, s *roleStore) error { s.roles["auditor"] = true; return nil },
Assert: assertRole("auditor"),
},
)
if err := runner.Run(context.Background()); err != nil {
fmt.Println("seed failed:", err)
return
}
fmt.Println("seeded and verified")
}
func assertRole(name string) func(context.Context, *roleStore) error {
return func(_ context.Context, s *roleStore) error {
if !s.roles[name] {
return fmt.Errorf("role %q not present", name)
}
return nil
}
}
Output: seeded and verified
Example (AssertionCatchesUnseeded) ¶
Example_assertionCatchesUnseeded shows the core guarantee: a step whose Apply exits cleanly but writes nothing is caught by its assertion, instead of being mistaken for a successful seed.
package main
import (
"context"
"errors"
"fmt"
"io"
"log/slog"
seed "github.com/Bugs5382/go-seed"
)
// roleStore is a tiny in-memory target, standing in for whatever storage an app
// seeds. Because the runner is generic over the target type, no database or
// driver is needed to demonstrate it.
type roleStore struct{ roles map[string]bool }
// quiet discards the runner's own log output so example output is deterministic.
func quiet() seed.Option {
return seed.WithLogger(slog.New(slog.NewTextHandler(io.Discard, nil)))
}
func main() {
db := &roleStore{roles: map[string]bool{}}
runner := seed.New(db, quiet()).Add(seed.Step[*roleStore]{
Name: "admin-role",
Apply: func(_ context.Context, _ *roleStore) error { return nil }, // exits 0, seeds nothing
Assert: assertRole("admin"),
})
err := runner.Run(context.Background())
var stepErr *seed.StepError
if errors.As(err, &stepErr) {
fmt.Printf("caught: step %q failed during %s\n", stepErr.Step, stepErr.Phase)
}
}
func assertRole(name string) func(context.Context, *roleStore) error {
return func(_ context.Context, s *roleStore) error {
if !s.roles[name] {
return fmt.Errorf("role %q not present", name)
}
return nil
}
}
Output: caught: step "admin-role" failed during assert
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrEmptyStepName is returned when a step has no name. ErrEmptyStepName = errors.New("seed: step name must not be empty") // ErrDuplicateStepName is returned when two steps share a name. ErrDuplicateStepName = errors.New("seed: duplicate step name") // ErrNilApply is returned when a step has a nil Apply hook. ErrNilApply = errors.New("seed: step Apply must not be nil") )
Configuration errors returned by Run before any step executes.
Functions ¶
Types ¶
type Executor ¶
type Executor interface {
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}
Executor is the minimal database surface a SQL seed step needs. It binds only to database/sql, never to a driver; both *sql.DB and *sql.Tx satisfy it, so a step may run against a pool or inside a transaction the app controls.
type Option ¶
type Option func(*options)
Option configures a Runner at construction time.
func WithLogger ¶
WithLogger sets the slog.Logger the Runner uses. When unset, the Runner uses slog.Default().
type RowSpec ¶
type RowSpec struct {
// Name identifies the step. Required.
Name string
// Apply is the idempotent write statement, e.g. an
// INSERT ... ON CONFLICT DO NOTHING or an upsert. Required.
Apply string
// ApplyArgs are the bound arguments for Apply.
ApplyArgs []any
// Count is a query returning a single integer row count that proves the
// seed exists, e.g. SELECT count(*) FROM roles WHERE name = $1. Required.
Count string
// CountArgs are the bound arguments for Count.
CountArgs []any
// WantAtLeast is the minimum row count the assertion requires. A value of
// zero is treated as 1, so the default assertion is "at least one row".
WantAtLeast int
}
RowSpec declares a SQL seed step: an idempotent write plus the count query that proves it took effect. RowSpec.Step turns it into a Step[Executor].
type Runner ¶
type Runner[T any] struct { // contains filtered or unexported fields }
Runner applies an ordered set of Steps against a single target and asserts each one. It is not safe for concurrent use.
func New ¶
New returns a Runner bound to target. Steps are added with Add and applied in the order they were added.
func (*Runner[T]) Add ¶
Add appends steps to the runner in order and returns the runner for chaining.
func (*Runner[T]) Run ¶
Run validates the step set, then applies and asserts each step in order.
Validation runs first and rejects an empty or duplicate step name or a nil Apply hook, returning a configuration error before any step executes. Each step then runs Apply and, if set, Assert. A step with no Assert is applied but logged as unverified, because a clean Apply is not proof of seeding.
Run is fail-fast: the first failing step stops the run and returns a *StepError naming the step and the phase (PhaseApply or PhaseAssert) that failed. Because every Apply must be idempotent, Run is safe to call repeatedly.
type Step ¶
type Step[T any] struct { // Name identifies the step in logs and errors. It must be non-empty and // unique within a Runner. Name string // Apply performs the idempotent write. It must be non-nil. Apply func(ctx context.Context, target T) error // Assert verifies the expected rows/state exist after Apply. It may be nil, // in which case the Runner logs a warning that the step is unverified. Assert func(ctx context.Context, target T) error }
Step is one named unit of seeding. Both hooks receive the app-supplied target T, so the core binds to no particular storage.
Apply performs the write and must be idempotent: running it a second time must not duplicate rows or fail. Assert verifies the expected state exists after Apply and returns an error if it does not; a zero-error Apply is never trusted on its own. Assert is optional but strongly recommended, because a step with no assertion cannot prove it seeded anything.