Documentation
¶
Overview ¶
Package seedling is a dependency-aware test data builder for Go and SQL databases.
seedling is designed for tests that need real inserted rows, but do not want to manually wire foreign keys across multiple tables. You define explicit blueprints in Go, provide the insert callbacks for your own DB layer, and seedling plans the dependency graph, fills foreign keys, and executes inserts in the correct order.
Before And After ¶
Without seedling, tests often manually create each parent row in dependency order:
func TestCreateTask(t *testing.T) {
company, err := db.InsertCompany(ctx, InsertCompanyParams{Name: "acme"})
if err != nil {
t.Fatal(err)
}
user, err := db.InsertUser(ctx, InsertUserParams{
Name: "alice", CompanyID: company.ID,
})
if err != nil {
t.Fatal(err)
}
project, err := db.InsertProject(ctx, InsertProjectParams{
Name: "renewal", CompanyID: company.ID,
})
if err != nil {
t.Fatal(err)
}
task, err := db.InsertTask(ctx, InsertTaskParams{
Title: "design", ProjectID: project.ID, AssigneeUserID: user.ID,
})
if err != nil {
t.Fatal(err)
}
_ = task
}
With seedling, the same test can focus on the row it actually cares about:
func TestCreateTask(t *testing.T) {
result := seedling.InsertOne[Task](t, db)
task := result.Root()
_ = task
}
Quick Start ¶
Register a Blueprint for each model that seedling should create:
seedling.MustRegister(seedling.Blueprint[Company]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() Company {
return Company{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v Company) (Company, error) {
return insertCompany(ctx, db, v)
},
})
seedling.MustRegister(seedling.Blueprint[User]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() User {
return User{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v User) (User, error) {
return insertUser(ctx, db, v)
},
})
DBTX is intentionally opaque. Your insert callback and your call sites must agree on the concrete handle type passed as db.
Then create rows directly in your tests:
func TestUser(t *testing.T) {
result := seedling.InsertOne[User](t, db)
user := result.Root()
// user.ID and user.CompanyID are populated.
}
If you want to inspect the graph before executing inserts, use Build or BuildE, then call Plan.Validate, Plan.DebugString, Plan.DryRunString, Plan.Insert, or Plan.InsertE.
Core Concepts ¶
A blueprint defines how to create one model type: default field values, primary-key metadata, relations, and insert/delete callbacks.
Relations describe graph edges such as belongs-to, has-many, and many-to-many. seedling uses them to expand the graph and bind keys.
Options customize a single insert/build call. Common examples are Set, Use, Ref, Omit, When, and With.
Build returns a plan for inspection or validation before execution. Reusing a Plan reuses its AfterInsert callbacks too, so state captured by those closures carries across executions. InsertOne returns a Result so you can access the root record and any related inserted nodes. InsertManyE returns a BatchResult with the batch roots plus cleanup/debug helpers for the full execution graph, including root-scoped lookup helpers such as BatchResult.NodeAt and BatchResult.NodesForRoot.
A session can bind a registry or database handle across repeated calls. When you use database/sql, NewTestSession can open a transaction and roll it back automatically at test cleanup time. For a lighter alternative, WithTx returns a *sql.Tx directly with automatic rollback on cleanup:
tx := seedling.WithTx(t, db) result := seedling.InsertOne[User](t, tx)
For pgx users, the companion package github.com/mhiro2/seedling/seedlingpgx provides the same pattern for pgx transactions:
tx := seedlingpgx.WithTx(t, pool) result := seedling.InsertOne[User](t, tx)
Common Workflows ¶
Reuse an existing parent row with Use:
company := seedling.InsertOne[Company](t, db).Root()
user := seedling.InsertOne[User](t, db,
seedling.Use("company", company),
).Root()
Customize an auto-created relation with Ref. This also explicitly enables optional relations:
plan := seedling.Build[User](t,
seedling.Ref("company", seedling.Set("Name", "renewal")),
)
result := plan.Insert(t, db)
user := result.Root()
_ = user
Generate multiple rows with InsertMany and Seq:
users := seedling.InsertMany[User](t, db, 3,
seedling.Seq("Email", func(i int) string {
return fmt.Sprintf("user-%d@example.com", i)
}),
)
_ = users
Or inspect / clean up the full batch execution with InsertManyE:
result, err := seedling.InsertManyE[User](ctx, db, 3)
if err != nil {
_ = result.CleanupE(ctx, db)
}
company, ok := result.NodeAt(1, "company")
_ = company
_ = ok
users := result.Roots()
_ = users
InsertMany batch-shares auto-created belongs-to relations when the same relation path resolves to the same static option tree after Seq and SeqRef are expanded. Relation-local Use, With, Generate, When, and rand-driven options disable sharing for that relation.
Skip unnecessary relations with Only:
// Only insert task + project subtree; skip assignee and other relations.
result := seedling.InsertOne[Task](t, db,
seedling.Only("project"),
)
// Only also works with InsertMany and applies per root.
result, _ := seedling.InsertManyE[Task](ctx, db, 2,
seedling.Only("project"),
)
_ = result
Generate deterministic fake values with Generate, WithSeed, and the seedling/faker subpackage.
SQL Integration ¶
seedling does not generate SQL at runtime. Your blueprint owns the Insert and optional Delete callbacks, so the library works with any DB abstraction. Install seedling-gen with Homebrew (brew install --cask mhiro2/tap/seedling-gen) or go install github.com/mhiro2/seedling/cmd/seedling-gen@latest.
The seedling-gen CLI can generate blueprint skeletons from multiple sources:
- SQL DDL: seedling-gen sql schema.sql
- sqlc config: seedling-gen sqlc --config sqlc.yaml
- GORM models: seedling-gen gorm --dir ./models --import-path example/models
- ent schemas: seedling-gen ent --dir ./ent/schema --import-path example/ent
- Atlas HCL: seedling-gen atlas schema.hcl
Use `seedling-gen --explain` to inspect parsed tables, foreign keys, and inferred blueprint relations before generating code. Use `--json` for the same report in machine-readable form.
Related APIs ¶
Frequently used APIs:
- InsertOne, InsertOneE, InsertMany, InsertManyE
- Build, BuildE
- Set, Use, Ref, Omit, When, With, Only
- BlueprintTrait, InlineTrait
- Seq, Generate, WithRand, WithSeed
- WithContext, WithInsertLog
- Plan.Validate, Plan.DebugString, Plan.DryRunString
- WithTx, NewTestSession
- Result.Root, Result.DebugString, Result.Cleanup, Result.CleanupE
- BatchResult.Roots, BatchResult.RootAt, BatchResult.NodeAt, BatchResult.NodesForRoot
- BatchResult.DebugString, BatchResult.Cleanup, BatchResult.CleanupE
Index ¶
- Variables
- func InsertMany[T any](tb testing.TB, db DBTX, n int, opts ...Option) []T
- func MustNodeAs[T any](lookup interface{ ... }, name string) T
- func MustRegister[T any](bp Blueprint[T])
- func MustRegisterTo[T any](dst *Registry, bp Blueprint[T])
- func NodeAs[T any](lookup interface{ ... }, name string) (T, bool, error)
- func NodesAs[T any](lookup interface{ ... }, name string) ([]T, error)
- func Register[T any](bp Blueprint[T]) error
- func RegisterTo[T any](dst *Registry, bp Blueprint[T]) error
- func ResetRegistry()
- func WhenFunc[T any](fn func(T) bool) func(any) bool
- func WithTx(tb testing.TB, db TxBeginner) *sql.Tx
- type BatchResult
- func (r BatchResult[T]) All() map[string]NodeResult
- func (r BatchResult[T]) Cleanup(tb testing.TB, db DBTX)
- func (r BatchResult[T]) CleanupE(ctx context.Context, db DBTX) error
- func (r BatchResult[T]) DebugString() string
- func (r BatchResult[T]) Len() int
- func (r BatchResult[T]) MustNode(name string) NodeResult
- func (r BatchResult[T]) MustNodeAt(rootIndex int, name string) NodeResult
- func (r BatchResult[T]) MustRootAt(index int) T
- func (r BatchResult[T]) Node(name string) (NodeResult, bool)
- func (r BatchResult[T]) NodeAt(rootIndex int, name string) (NodeResult, bool)
- func (r BatchResult[T]) Nodes(name string) []NodeResult
- func (r BatchResult[T]) NodesForRoot(rootIndex int, name string) []NodeResult
- func (r BatchResult[T]) RootAt(index int) (T, bool)
- func (r BatchResult[T]) Roots() []T
- type Blueprint
- type Builder
- func (b *Builder[T]) AfterInsert(fn func(T, DBTX)) *Builder[T]
- func (b *Builder[T]) AfterInsertE(fn func(T, DBTX) error) *Builder[T]
- func (b *Builder[T]) Apply(opts ...Option) *Builder[T]
- func (b *Builder[T]) BlueprintTrait(name string) *Builder[T]
- func (b *Builder[T]) Build(tb testing.TB) *Plan[T]
- func (b *Builder[T]) BuildE() (*Plan[T], error)
- func (b *Builder[T]) Generate(fn func(*rand.Rand, *T)) *Builder[T]
- func (b *Builder[T]) GenerateE(fn func(*rand.Rand, *T) error) *Builder[T]
- func (b *Builder[T]) InlineTrait(opts ...Option) *Builder[T]
- func (b *Builder[T]) Insert(tb testing.TB, db DBTX) Result[T]
- func (b *Builder[T]) InsertE(ctx context.Context, db DBTX) (Result[T], error)
- func (b *Builder[T]) InsertMany(tb testing.TB, db DBTX, n int) []T
- func (b *Builder[T]) InsertManyE(ctx context.Context, db DBTX, n int) (BatchResult[T], error)
- func (b *Builder[T]) Omit(name string) *Builder[T]
- func (b *Builder[T]) Ref(name string, opts ...Option) *Builder[T]
- func (b *Builder[T]) Set(field string, value any) *Builder[T]
- func (b *Builder[T]) Use(name string, value any) *Builder[T]
- func (b *Builder[T]) With(fn func(*T)) *Builder[T]
- func (b *Builder[T]) WithContext(ctx context.Context) *Builder[T]
- func (b *Builder[T]) WithRand(r *rand.Rand) *Builder[T]
- func (b *Builder[T]) WithSeed(seed uint64) *Builder[T]
- type DBTX
- type DeleteFailedError
- type FKBinding
- type InsertFailedError
- type InsertLog
- type NodeResult
- type Option
- func AfterInsert[T any](fn func(t T, db DBTX)) Option
- func AfterInsertE[T any](fn func(t T, db DBTX) error) Option
- func BlueprintTrait(name string) Option
- func Generate[T any](fn func(r *rand.Rand, t *T)) Option
- func GenerateE[T any](fn func(r *rand.Rand, t *T) error) Option
- func InlineTrait(opts ...Option) Option
- func Omit(name string) Option
- func Only(relations ...string) Option
- func Ref(name string, opts ...Option) Option
- func Seq[V any](field string, fn func(i int) V) Option
- func SeqRef(name string, fn func(i int) []Option) Option
- func SeqUse[V any](name string, fn func(i int) V) Option
- func Set(field string, value any) Option
- func Use(name string, value any) Option
- func When[T any](name string, fn func(T) bool) Option
- func With[T any](fn func(*T)) Option
- func WithContext(ctx context.Context) Option
- func WithInsertLog(fn func(InsertLog)) Option
- func WithRand(r *rand.Rand) Option
- func WithSeed(seed uint64) Option
- type Plan
- type Registry
- type Relation
- func BelongsToRelation(name, refBlueprint string, optional bool, localFields ...string) Relation
- func HasManyRelation(name, refBlueprint string, optional bool, count int, localFields ...string) Relation
- func ManyToManyRelation(name, throughBlueprint, refBlueprint string, optional bool, count int, ...) Relation
- type RelationKind
- type Result
- func (r Result[T]) All() map[string]NodeResult
- func (r Result[T]) Cleanup(tb testing.TB, db DBTX)
- func (r Result[T]) CleanupE(ctx context.Context, db DBTX) error
- func (r Result[T]) DebugString() string
- func (r Result[T]) MustNode(name string) NodeResult
- func (r Result[T]) Node(name string) (NodeResult, bool)
- func (r Result[T]) Nodes(name string) []NodeResult
- func (r Result[T]) Root() T
- type Session
- func (s Session[T]) Build(tb testing.TB, opts ...Option) *Plan[T]
- func (s Session[T]) BuildE(opts ...Option) (*Plan[T], error)
- func (s Session[T]) DB() DBTX
- func (s Session[T]) InsertMany(tb testing.TB, db DBTX, n int, opts ...Option) []T
- func (s Session[T]) InsertManyE(ctx context.Context, db DBTX, n int, opts ...Option) (BatchResult[T], error)
- func (s Session[T]) InsertOne(tb testing.TB, db DBTX, opts ...Option) Result[T]
- func (s Session[T]) InsertOneE(ctx context.Context, db DBTX, opts ...Option) (Result[T], error)
- func (s Session[T]) WithDB(db DBTX) Session[T]
- type TxBeginner
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( ErrBlueprintNotFound = errx.ErrBlueprintNotFound ErrRelationNotFound = errx.ErrRelationNotFound ErrFieldNotFound = errx.ErrFieldNotFound ErrCycleDetected = errx.ErrCycleDetected ErrTypeMismatch = errx.ErrTypeMismatch // ErrInsertFailed reports that a blueprint Insert callback failed. // Returned errors also unwrap to the original callback error. ErrInsertFailed = errx.ErrInsertFailed // ErrDeleteFailed reports that a blueprint Delete callback failed. ErrDeleteFailed = errx.ErrDeleteFailed // ErrDeleteNotDefined reports that a blueprint has no Delete function // but Cleanup was called. ErrDeleteNotDefined = errx.ErrDeleteNotDefined ErrDuplicateBlueprint = errx.ErrDuplicateBlueprint ErrInvalidOption = errx.ErrInvalidOption )
Public error sentinels. Use errors.Is() to check.
Functions ¶
func InsertMany ¶
InsertMany creates and inserts n records of type T with the same options. Shared belongs-to dependencies are inserted once when their resolved options are identical across records. Fails the test on error.
Example ¶
package main
import (
"context"
"fmt"
"testing"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
t := &testing.T{}
companies := seedling.InsertMany[ExCompany](t, nil, 3,
seedling.Seq("Name", func(i int) string {
return fmt.Sprintf("company-%d", i)
}),
)
fmt.Printf("%s, %s, %s\n", companies[0].Name, companies[1].Name, companies[2].Name)
}
Output: company-0, company-1, company-2
func MustNodeAs ¶
func MustNodeAs[T any](lookup interface { Node(name string) (NodeResult, bool) }, name string, ) T
MustNodeAs returns a named node cast to T or panics.
func MustRegister ¶
MustRegister registers a blueprint in the package default registry and panics on error.
func MustRegisterTo ¶
MustRegisterTo registers a blueprint in the provided registry and panics on error.
func NodeAs ¶
func NodeAs[T any](lookup interface { Node(name string) (NodeResult, bool) }, name string, ) (T, bool, error)
NodeAs returns a named node cast to T.
func NodesAs ¶
func NodesAs[T any](lookup interface { Nodes(name string) []NodeResult }, name string, ) ([]T, error)
NodesAs returns all named nodes cast to T.
func RegisterTo ¶
RegisterTo registers a blueprint in the provided registry. Each Go type can be registered at most once per registry.
func ResetRegistry ¶
func ResetRegistry()
ResetRegistry clears all blueprints from the package default registry. Intended for testing.
func WhenFunc ¶
WhenFunc creates a type-safe predicate for use in [Relation.When]. It wraps a typed function so callers do not need a manual type assertion:
When: seedling.WhenFunc(func(t Task) bool {
return t.Status == "assigned"
}),
func WithTx ¶
func WithTx(tb testing.TB, db TxBeginner) *sql.Tx
WithTx starts a SQL transaction and rolls it back during test cleanup. This is a convenience wrapper for tests that need a transaction without creating a full Session.
func TestSomething(t *testing.T) {
tx := seedling.WithTx(t, db)
result := seedling.InsertOne[Task](t, tx)
// tx auto-rollbacks at cleanup
}
Types ¶
type BatchResult ¶ added in v0.2.0
type BatchResult[T any] struct { // contains filtered or unexported fields }
BatchResult holds all created nodes after batch insertion.
func InsertManyE ¶
func InsertManyE[T any](ctx context.Context, db DBTX, n int, opts ...Option) (BatchResult[T], error)
InsertManyE creates and inserts n records of type T, returning a BatchResult for cleanup and graph inspection. When Seq options are present, the sequence function is called with the 0-based index for each record. Shared belongs-to dependencies are inserted once when their resolved options are identical across records.
Example ¶
package main
import (
"context"
"fmt"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
result, err := seedling.InsertManyE[ExTask](context.Background(), nil, 2,
seedling.Ref("project", seedling.Set("Name", "shared-project")),
)
if err != nil {
return
}
node0, ok0 := result.NodeAt(0, "project")
if !ok0 {
return
}
project0, ok := node0.Value().(ExProject)
if !ok {
return
}
node1, ok1 := result.NodeAt(1, "project")
if !ok1 {
return
}
project1, ok := node1.Value().(ExProject)
if !ok {
return
}
fmt.Printf("%s %t\n", project0.Name, project0.ID == project1.ID)
}
Output: shared-project true
func (BatchResult[T]) All ¶ added in v0.2.0
func (r BatchResult[T]) All() map[string]NodeResult
All returns all nodes in the result as a map keyed by node ID.
func (BatchResult[T]) Cleanup ¶ added in v0.2.0
func (r BatchResult[T]) Cleanup(tb testing.TB, db DBTX)
Cleanup deletes all records that were inserted by seedling in reverse dependency order.
func (BatchResult[T]) CleanupE ¶ added in v0.2.0
func (r BatchResult[T]) CleanupE(ctx context.Context, db DBTX) error
CleanupE deletes all records that were inserted by seedling in reverse dependency order. CleanupE is fail-fast: it stops at the first delete error and returns it.
func (BatchResult[T]) DebugString ¶ added in v0.2.0
func (r BatchResult[T]) DebugString() string
DebugString returns a human-readable tree of the execution result.
func (BatchResult[T]) Len ¶ added in v0.2.0
func (r BatchResult[T]) Len() int
Len returns the number of inserted root records.
func (BatchResult[T]) MustNode ¶ added in v0.2.0
func (r BatchResult[T]) MustNode(name string) NodeResult
MustNode returns a named node or panics.
func (BatchResult[T]) MustNodeAt ¶ added in v0.2.2
func (r BatchResult[T]) MustNodeAt(rootIndex int, name string) NodeResult
MustNodeAt returns the named node associated with the root at index or panics.
func (BatchResult[T]) MustRootAt ¶ added in v0.2.0
func (r BatchResult[T]) MustRootAt(index int) T
MustRootAt returns the inserted root record at index or panics.
func (BatchResult[T]) Node ¶ added in v0.2.0
func (r BatchResult[T]) Node(name string) (NodeResult, bool)
Node returns a named node from the full batch dependency graph by blueprint name. If multiple nodes share the same blueprint name, the one with the lexicographically smallest node ID is returned across all roots.
For root-scoped lookups, use BatchResult.NodeAt or BatchResult.NodesForRoot.
func (BatchResult[T]) NodeAt ¶ added in v0.2.2
func (r BatchResult[T]) NodeAt(rootIndex int, name string) (NodeResult, bool)
NodeAt returns the named node associated with the root at index. Shared belongs-to dependencies are included when that root references them.
Example ¶
package main
import (
"context"
"fmt"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
result, err := seedling.InsertManyE[ExTask](context.Background(), nil, 2,
seedling.SeqRef("project", func(i int) []seedling.Option {
return []seedling.Option{seedling.Set("Name", fmt.Sprintf("project-%d", i))}
}),
)
if err != nil {
return
}
node, ok := result.NodeAt(1, "project")
if !ok {
return
}
project, ok := node.Value().(ExProject)
if !ok {
return
}
fmt.Println(project.Name)
}
Output: project-1
func (BatchResult[T]) Nodes ¶ added in v0.2.0
func (r BatchResult[T]) Nodes(name string) []NodeResult
Nodes returns all nodes that match the given blueprint name across the full batch.
func (BatchResult[T]) NodesForRoot ¶ added in v0.2.2
func (r BatchResult[T]) NodesForRoot(rootIndex int, name string) []NodeResult
NodesForRoot returns all named nodes associated with the root at index, sorted by node ID. Shared belongs-to dependencies are included when that root references them.
func (BatchResult[T]) RootAt ¶ added in v0.2.0
func (r BatchResult[T]) RootAt(index int) (T, bool)
RootAt returns the inserted root record at index.
func (BatchResult[T]) Roots ¶ added in v0.2.0
func (r BatchResult[T]) Roots() []T
Roots returns the inserted root records.
type Blueprint ¶
type Blueprint[T any] struct { // Name is the unique identifier for this blueprint (e.g. "task", "company"). // Used for relation references (RefBlueprint) and Result.Node() lookups. Name string // Table is the database table name, used for debug output only. Table string // PKField is the Go struct field name of the primary key (e.g. "ID"). // The executor reads this field from parent records to populate FK fields // on child records. PKField string // PKFields is the multi-column form of PKField for composite primary keys. // When set, its values are used in the given order. PKFields []string // Defaults returns a new instance of T with default field values. // This function is called once per record creation to avoid shared mutable state. // Always return a fresh value — do not return a package-level variable. // // Defaults: func() User { // return User{Name: "test-user", Role: "member"} // } Defaults func() T // Relations defines the dependencies of this blueprint. // Relations can point to parents via BelongsTo or auto-create children via HasMany. Relations []Relation // Traits defines named option presets that can be applied by name. // When a trait is defined here, callers can use BlueprintTrait("name") // without re-specifying the options each time. // // Traits: map[string][]seedling.Option{ // "admin": {seedling.Set("Role", "admin"), seedling.Set("Active", true)}, // } Traits map[string][]Option // Insert performs the actual database insertion and returns the inserted // record with the auto-generated primary key populated. // The returned value must have PKField set so that child records can // reference it via their FK fields. The callback is responsible for // handling the concrete DBTX type it expects. // // Insert: func(ctx context.Context, db seedling.DBTX, v User) (User, error) { // return queries.InsertUser(ctx, db.(*sql.DB), v) // } Insert func(ctx context.Context, db DBTX, v T) (T, error) // Delete performs the actual database deletion for a previously inserted record. // This is optional and only required when using [Result.Cleanup] or [Result.CleanupE]. // The value passed to Delete is the fully populated record as returned by Insert, // so primary key fields are available for constructing the DELETE query. // // Delete: func(ctx context.Context, db seedling.DBTX, v User) error { // return queries.DeleteUser(ctx, db.(*sql.DB), v.ID) // } Delete func(ctx context.Context, db DBTX, v T) error }
Blueprint describes how to create and insert a model of type T.
type Builder ¶
type Builder[T any] struct { // contains filtered or unexported fields }
Builder provides a fluent API for constructing and inserting records.
seedling.For[Task]().Set("Title", "urgent").Ref("project", seedling.Set("Name", "x")).Insert(t, db)
func For ¶
For creates a new Builder for type T using the default registry.
Example ¶
package main
import (
"context"
"fmt"
"testing"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
t := &testing.T{}
result := seedling.For[ExTask]().
Set("Title", "builder-task").
Ref("project", seedling.Set("Name", "builder-project")).
Insert(t, nil)
project, ok, err := seedling.NodeAs[ExProject](result, "project")
if err != nil || !ok {
return
}
fmt.Printf("%s %s\n", result.Root().Title, project.Name)
}
Output: builder-task builder-project
func ForSession ¶
ForSession creates a new Builder for type T using a specific session.
func (*Builder[T]) AfterInsert ¶
AfterInsert registers a callback that runs after the root record is inserted.
func (*Builder[T]) AfterInsertE ¶
AfterInsertE registers an error-returning callback that runs after the root record is inserted.
func (*Builder[T]) Apply ¶
Apply appends arbitrary options. Use this for options that require additional type parameters (e.g., Seq, SeqUse) which cannot be expressed as methods on Builder[T].
func (*Builder[T]) BlueprintTrait ¶
BlueprintTrait applies a named trait defined on the target blueprint.
func (*Builder[T]) Build ¶
Build constructs a dependency plan without inserting anything. Fails the test on error.
func (*Builder[T]) GenerateE ¶
GenerateE applies a rand-driven mutation function that can return an error.
func (*Builder[T]) InlineTrait ¶
InlineTrait applies an inline trait composed from explicit options.
func (*Builder[T]) InsertE ¶
InsertE creates and inserts a single record, returning an error on failure.
func (*Builder[T]) InsertMany ¶
InsertMany creates and inserts n records. Shared belongs-to dependencies are inserted once when their resolved options are identical across records. Fails the test on error.
func (*Builder[T]) InsertManyE ¶
InsertManyE creates and inserts n records, returning a BatchResult. Shared belongs-to dependencies are inserted once when their resolved options are identical across records.
func (*Builder[T]) Use ¶
Use provides an existing record for a direct relation, skipping auto-creation.
func (*Builder[T]) WithContext ¶
WithContext sets the context used for insert operations.
type DBTX ¶
type DBTX any
DBTX is an opaque database handle passed through to Blueprint Insert functions. seedling does not execute SQL directly; it delegates all database operations to user-provided Insert functions which can accept *sql.DB, *sql.Tx, pgx.Tx, etc. Callers and Insert callbacks must agree on the concrete handle type. seedling does not validate or convert db before invoking the callback.
type DeleteFailedError ¶
type DeleteFailedError = errx.DeleteFailedError
DeleteFailedError wraps ErrDeleteFailed with the blueprint name and the original delete callback error. Use errors.As to extract the blueprint name:
var dfe *seedling.DeleteFailedError
if errors.As(err, &dfe) {
log.Printf("blueprint %s failed", dfe.Blueprint())
}
type FKBinding ¶
type FKBinding struct {
// ChildField is the FK field on the inserted record.
ChildField string
// ParentBlueprint is the parent blueprint name.
ParentBlueprint string
// ParentTable is the parent table name.
ParentTable string
// ParentField is the parent PK field name.
ParentField string
// Value is the PK value assigned to the FK field.
Value any
}
FKBinding describes a single FK assignment made before an insert.
type InsertFailedError ¶
type InsertFailedError = errx.InsertFailedError
InsertFailedError wraps ErrInsertFailed with the blueprint name and the original insert callback error. Use errors.As to extract the blueprint name:
var ife *seedling.InsertFailedError
if errors.As(err, &ife) {
log.Printf("blueprint %s failed", ife.Blueprint())
}
type InsertLog ¶
type InsertLog struct {
// Step is the 1-based position in the topological execution order.
Step int
// Blueprint is the blueprint name of the inserted record.
Blueprint string
// Table is the database table name.
Table string
// Provided is true when the record was supplied via [Use] (no INSERT executed).
Provided bool
// FKBindings describes the FK fields that were populated from parent PKs
// before this record was inserted.
FKBindings []FKBinding
}
InsertLog holds information about a single insert operation performed by the executor. Use WithInsertLog to receive these entries during execution.
type NodeResult ¶
type NodeResult struct {
// contains filtered or unexported fields
}
NodeResult holds the result of a single node.
func (NodeResult) Name ¶
func (n NodeResult) Name() string
Name returns the blueprint name of this node.
type Option ¶
type Option interface {
// contains filtered or unexported methods
}
Option configures how a record is built and inserted.
func AfterInsert ¶
AfterInsert registers a callback that runs after the root record is inserted. The callback receives the inserted root record and the database handle. This is useful for post-insert side effects like password hashing or populating join tables.
seedling.AfterInsert(func(u User, db seedling.DBTX) {
// hash password, insert into join table, etc.
})
Example ¶
package main
import (
"context"
"fmt"
"testing"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
t := &testing.T{}
var names []string
seedling.InsertOne[ExCompany](t, nil,
seedling.AfterInsert(func(c ExCompany, db seedling.DBTX) {
names = append(names, c.Name)
}),
)
fmt.Println(names[0])
}
Output: test-company
func AfterInsertE ¶
AfterInsertE registers a callback that runs after the root record is inserted. Unlike AfterInsert, the callback can return an error to signal failure.
seedling.AfterInsertE(func(u User, db seedling.DBTX) error {
_, err := db.(*sql.DB).Exec("INSERT INTO roles ...")
return err
})
func BlueprintTrait ¶
BlueprintTrait applies a named trait defined on the target blueprint.
seedling.InsertOne[User](t, db, seedling.BlueprintTrait("admin"))
Example ¶
package main
import (
"context"
"fmt"
"testing"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
t := &testing.T{}
user := seedling.InsertOne[ExUser](t, nil, seedling.BlueprintTrait("named")).Root()
fmt.Println(user.Name)
}
Output: trait-user
func Generate ¶
Generate applies a rand-driven mutation function to the current node before Set/With options run. This is useful when integrating seedling with property-based tests.
seedling.Generate(func(r *rand.Rand, t *Task) {
t.Title = fmt.Sprintf("task-%d", r.IntN(1000))
})
Example ¶
setupExampleBlueprints()
t := &testing.T{}
user := seedling.InsertOne[ExUser](t, nil,
seedling.WithSeed(42),
seedling.Generate(func(r *rand.Rand, u *ExUser) {
f := faker.New(r)
u.Name = f.Name()
}),
).Root()
fmt.Println(user.Name)
Output: Amanda Sanders
func GenerateE ¶
GenerateE applies a rand-driven mutation function that can return an error. Unlike Generate, the function can signal failure by returning a non-nil error.
seedling.GenerateE(func(r *rand.Rand, t *Task) error {
name, err := randomName(r)
if err != nil { return err }
t.Title = name
return nil
})
func InlineTrait ¶
InlineTrait creates a group of options that is expanded immediately. Use this when you want to compose reusable option bundles in Go code rather than reference a trait stored on the blueprint.
adminTrait := seedling.InlineTrait(seedling.Set("Role", "admin"))
seedling.InsertOne[User](t, db, adminTrait)
Example ¶
package main
import (
"context"
"fmt"
"testing"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
t := &testing.T{}
user := seedling.InsertOne[ExUser](t, nil,
seedling.InlineTrait(seedling.Set("Name", "inline-user")),
).Root()
fmt.Println(user.Name)
}
Output: inline-user
func Only ¶
Only restricts the planner to build only the root node and the specified relation subtrees. Relations not listed are never expanded, so the resulting graph contains only the necessary nodes. Plan.DebugString and Plan.DryRunString reflect this lazily built subgraph.
With no arguments, Only builds only the root node:
seedling.InsertOne[Task](t, db, seedling.Only())
With arguments, the named relations and their transitive dependencies are also included:
seedling.InsertOne[Task](t, db, seedling.Only("project"))
Example ¶
package main
import (
"context"
"fmt"
"testing"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
t := &testing.T{}
plan := seedling.Build[ExTask](t,
seedling.Set("Status", "assigned"),
seedling.Only("project"),
)
fmt.Println(plan.DebugString())
}
Output: task (Set: Status) └─ project └─ company
Example (RootOnly) ¶
package main
import (
"context"
"fmt"
"testing"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
t := &testing.T{}
plan := seedling.Build[ExTask](t, seedling.Only())
fmt.Println(plan.DebugString())
}
Output: task
func Ref ¶
Ref applies nested options to a specific relation's blueprint. It also enables expansion for optional relations.
Options that only apply to the root record — WithContext, AfterInsert, AfterInsertE, and WithInsertLog — cannot appear under Ref (including inside traits resolved for that relation) and return ErrInvalidOption.
seedling.Ref("project", seedling.Set("Name", "renewal"))
Example ¶
package main
import (
"context"
"fmt"
"testing"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
t := &testing.T{}
plan := seedling.Build[ExTask](t,
seedling.Ref("project", seedling.Set("Name", "custom-project")),
)
result := plan.Insert(t, nil)
project, ok, err := seedling.NodeAs[ExProject](result, "project")
if err != nil || !ok {
return
}
fmt.Println(project.Name)
}
Output: custom-project
func Seq ¶
Seq sets a field value based on the index when used with InsertMany. The function receives a 0-based index and returns the value for that field.
seedling.InsertMany[User](t, db, 3,
seedling.Seq("Name", func(i int) string { return fmt.Sprintf("user-%d", i) }),
)
func SeqRef ¶
SeqRef generates per-record Ref options when used with InsertMany. The function receives a 0-based index and returns the nested options for that relation.
seedling.InsertMany[Task](t, db, 3,
seedling.SeqRef("project", func(i int) []seedling.Option {
return []seedling.Option{seedling.Set("Name", fmt.Sprintf("proj-%d", i))}
}),
)
Example ¶
package main
import (
"context"
"fmt"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
result, err := seedling.InsertManyE[ExTask](context.Background(), nil, 2,
seedling.SeqRef("project", func(i int) []seedling.Option {
return []seedling.Option{seedling.Set("Name", fmt.Sprintf("project-%d", i))}
}),
)
if err != nil {
return
}
node0, ok0 := result.NodeAt(0, "project")
if !ok0 {
return
}
project0, ok := node0.Value().(ExProject)
if !ok {
return
}
node1, ok1 := result.NodeAt(1, "project")
if !ok1 {
return
}
project1, ok := node1.Value().(ExProject)
if !ok {
return
}
fmt.Printf("%s, %s\n", project0.Name, project1.Name)
}
Output: project-0, project-1
func SeqUse ¶
SeqUse provides per-record existing records for a relation when used with InsertMany.
companies := seedling.InsertMany[Company](t, db, 3)
seedling.InsertMany[Task](t, db, 3,
seedling.SeqUse("company", func(i int) Company { return companies[i] }),
)
Example ¶
package main
import (
"context"
"fmt"
"testing"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
t := &testing.T{}
companies := seedling.InsertMany[ExCompany](t, nil, 2,
seedling.Seq("Name", func(i int) string {
return fmt.Sprintf("company-%d", i)
}),
)
users := seedling.InsertMany[ExUser](t, nil, 2,
seedling.SeqUse("company", func(i int) ExCompany {
return companies[i]
}),
)
fmt.Printf("%d, %d\n", users[0].CompanyID, users[1].CompanyID)
}
Output: 1, 2
func Set ¶
Set overrides a struct field value by its Go field name.
seedling.Set("Title", "urgent task")
Example ¶
package main
import (
"context"
"fmt"
"testing"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
t := &testing.T{}
project := seedling.InsertOne[ExProject](t, nil,
seedling.Set("Name", "custom-project"),
).Root()
fmt.Println(project.Name)
}
Output: custom-project
func Use ¶
Use provides an existing record for a direct relation, skipping auto-creation.
seedling.Use("company", existingCompany)
Example ¶
package main
import (
"context"
"fmt"
"testing"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
t := &testing.T{}
project := seedling.InsertOne[ExProject](t, nil,
seedling.Set("Name", "existing-project"),
).Root()
task := seedling.InsertOne[ExTask](t, nil,
seedling.Use("project", project),
).Root()
fmt.Println(task.ProjectID)
}
Output: 2
func When ¶
When conditionally expands a relation based on the current record's state. The predicate receives the owning record after defaults and Set/With options are applied. If it returns true, the relation is expanded, including optional relations. If it returns false, the relation is skipped regardless of the blueprint's Required flag.
This provides dynamic, insert-time control over relation expansion:
seedling.InsertOne[Task](t, db,
seedling.Set("Status", "assigned"),
seedling.When("assignee", func(t Task) bool {
return t.Status == "assigned"
}),
)
Example ¶
package main
import (
"context"
"fmt"
"testing"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
t := &testing.T{}
task := seedling.InsertOne[ExTask](t, nil,
seedling.Set("Status", "assigned"),
).Root()
fmt.Println(task.AssigneeUserID > 0)
}
Output: true
func With ¶
With applies a type-safe modification function to the root struct.
seedling.With(func(t *Task) { t.Title = "urgent" })
Example ¶
package main
import (
"context"
"fmt"
"testing"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
t := &testing.T{}
user := seedling.InsertOne[ExUser](t, nil,
seedling.With(func(u *ExUser) {
u.Name = "modified-user"
}),
).Root()
fmt.Println(user.Name)
}
Output: modified-user
func WithContext ¶
WithContext sets the context used for insert operations. If not specified, testing-based APIs use t.Context() and error-returning APIs use the ctx passed to InsertOneE, InsertManyE, or Plan.InsertE.
seedling.InsertOne[Task](t, db, seedling.WithContext(ctx))
func WithInsertLog ¶
WithInsertLog registers a callback that is invoked for each step in the execution plan, including both inserted and provided (skipped) nodes. The callback receives an InsertLog describing the operation, including FK bindings that were resolved from parent PK values.
This is useful for debugging the dependency resolution order and understanding which FK values were assigned:
seedling.InsertOne[Task](t, db,
seedling.WithInsertLog(func(log seedling.InsertLog) {
t.Logf("Step %d: %s (table: %s)", log.Step, log.Blueprint, log.Table)
for _, fk := range log.FKBindings {
t.Logf(" SET %s = %v (from %s.%s)", fk.ChildField, fk.Value, fk.ParentTable, fk.ParentField)
}
}),
)
Example ¶
package main
import (
"context"
"fmt"
"testing"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
t := &testing.T{}
var logs []seedling.InsertLog
seedling.InsertOne[ExTask](t, nil,
seedling.WithInsertLog(func(log seedling.InsertLog) {
logs = append(logs, log)
}),
)
last := logs[len(logs)-1]
fmt.Printf("%d %s %s\n", len(logs), last.Blueprint, last.FKBindings[0].ChildField)
}
Output: 3 task ProjectID
type Plan ¶
type Plan[T any] struct { // contains filtered or unexported fields }
Plan represents a dependency graph ready for insertion. A plan can be executed multiple times. Each execution operates on a cloned graph so the built plan remains unchanged.
Note: AfterInsert callbacks registered via options are captured at Build time and shared across executions. Go closures cannot be cloned, so reusing a plan also reuses any callback state captured by those closures. Prefer stateless callbacks, or rebuild the plan when callback state must be isolated.
func Build ¶
Build constructs a dependency plan for type T without inserting anything. Fails the test on error.
Example ¶
package main
import (
"context"
"fmt"
"testing"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
t := &testing.T{}
plan := seedling.Build[ExTask](t)
fmt.Println(plan.DebugString())
}
Output: task └─ project └─ company
func BuildE ¶
BuildE constructs a dependency plan for type T without inserting anything.
Example ¶
package main
import (
"context"
"fmt"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
plan, err := seedling.BuildE[ExTask](seedling.Set("Status", "assigned"))
if err != nil {
return
}
fmt.Println(plan.DebugString())
}
Output: task (Set: Status) ├─ user │ └─ company └─ project └─ company
func (*Plan[T]) DebugString ¶
DebugString returns a human-readable tree representation of the plan.
func (*Plan[T]) DryRunString ¶
DryRunString returns the planned INSERT execution order with FK assignments. Each step shows which table will be inserted and how FK fields are populated from parent PK values. Provided nodes (via Use) are marked as skipped.
This is useful for understanding how seedling will resolve dependencies before actually executing inserts.
Example ¶
package main
import (
"context"
"fmt"
"testing"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
t := &testing.T{}
plan := seedling.Build[ExTask](t,
seedling.Use("project", ExProject{ID: 42, CompanyID: 7, Name: "existing-project"}),
)
fmt.Println(plan.DryRunString())
}
Output: Step 1: SKIP projects (provided) (blueprint: project) Step 2: INSERT INTO tasks (blueprint: task) SET ProjectID ← projects.ID
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry stores registered blueprints independently from the package default registry. A registry enforces a 1:1 mapping between a Go type and a blueprint name. If you need multiple blueprints for similar shapes, define distinct Go types.
type Relation ¶
type Relation struct {
// Name is the identifier used with Use() and Ref() (e.g. "company", "assignee").
Name string
// Kind is the type of relationship (BelongsTo, HasMany, or ManyToMany).
Kind RelationKind
// LocalField is the legacy single-column form of LocalFields.
// For BelongsTo: the FK field on the child struct (e.g. "CompanyID").
// For HasMany: the FK field on the referenced (child) blueprint's struct
// that points back to this parent (e.g. "CompanyID" on User).
// For ManyToMany: the FK field on the join-table struct pointing to the
// current (parent) blueprint.
LocalField string
// LocalFields is the multi-column form of LocalField.
// Use this when the related blueprint has a composite primary key.
LocalFields []string
// RefBlueprint is the name of the related blueprint.
// For BelongsTo: the parent blueprint.
// For HasMany: the child blueprint to auto-create.
// For ManyToMany: the related blueprint to auto-create.
RefBlueprint string
// ThroughBlueprint is required for ManyToMany and names the join-table
// blueprint that should be auto-created between the current blueprint and
// RefBlueprint.
ThroughBlueprint string
// RemoteField is the legacy single-column form of RemoteFields.
// Only used for ManyToMany, where it identifies the FK field on the
// join-table struct pointing to the related blueprint.
RemoteField string
// RemoteFields is the multi-column form of RemoteField.
// Only used for ManyToMany.
RemoteFields []string
// Optional disables default automatic expansion for this relation.
// When false (the zero value), the relation is required and the planner
// will automatically expand it. Set to true when you want to keep a
// relation nullable by default; Ref, Use, or a true When predicate can
// still enable expansion explicitly.
Optional bool
// Count specifies how many related records to create for HasMany and
// ManyToMany relations. Defaults to 1 if not set.
Count int
// When is an optional predicate that dynamically controls whether this
// relation should be expanded. The function receives the current record
// value (the owning blueprint's struct) and returns true to expand the
// relation or false to skip it. When nil, the relation uses the
// standard Optional logic.
//
// This allows conditional relation expansion based on the record's
// field values at plan time:
//
// When: func(v any) bool {
// return v.(Task).Status == "assigned"
// },
When func(v any) bool
}
Relation describes a dependency between two blueprints.
func BelongsToRelation ¶
BelongsToRelation builds a BelongsTo relation with explicit semantics.
type RelationKind ¶
type RelationKind string
RelationKind describes the type of relationship between blueprints.
const ( // BelongsTo indicates a foreign key relationship where the child holds // a reference to the parent's primary key. BelongsTo RelationKind = "belongs_to" // HasMany indicates a one-to-many relationship where creating the parent // automatically creates N child records. The LocalField is the FK field // on the child blueprint that points back to the parent. HasMany RelationKind = "has_many" // ManyToMany indicates a relationship where creating the parent // automatically creates related records plus the join-table rows that link // them together. ThroughBlueprint identifies the join-table blueprint. ManyToMany RelationKind = "many_to_many" )
type Result ¶
type Result[T any] struct { // contains filtered or unexported fields }
Result holds all created nodes after insertion.
func InsertOne ¶
InsertOne creates and inserts a single record of type T with all required dependencies automatically resolved. Fails the test on error.
Example ¶
package main
import (
"context"
"fmt"
"testing"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
t := &testing.T{}
task := seedling.InsertOne[ExTask](t, nil).Root()
fmt.Printf("%s %d\n", task.Title, task.ProjectID)
}
Output: test-task 2
func InsertOneE ¶
InsertOneE creates and inserts a single record of type T, returning an error on failure.
Example ¶
package main
import (
"context"
"fmt"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
result, err := seedling.InsertOneE[ExTask](context.Background(), nil,
seedling.Set("Status", "assigned"),
)
if err != nil {
return
}
fmt.Println(result.Root().AssigneeUserID > 0)
}
Output: true
func (Result[T]) All ¶
func (r Result[T]) All() map[string]NodeResult
All returns all nodes in the result as a map keyed by node ID. This is useful for inspecting every record that was created during insertion.
func (Result[T]) Cleanup ¶
Cleanup deletes all records that were inserted by seedling in reverse dependency order (children before parents). Records provided via Use are skipped because they were not created by seedling.
Every blueprint whose records appear in the result must have a [Blueprint.Delete] function defined; otherwise Cleanup returns ErrDeleteNotDefined.
Cleanup is useful when transaction rollback is not available, such as when using testcontainers or external databases.
func (Result[T]) CleanupE ¶
CleanupE deletes all records that were inserted by seedling in reverse dependency order (children before parents). Records provided via Use are skipped because they were not created by seedling.
Every blueprint whose records appear in the result must have a [Blueprint.Delete] function defined; otherwise CleanupE returns ErrDeleteNotDefined.
Delete functions are captured at result creation time, so cleanup behavior is not affected by subsequent registry resets or re-registrations.
CleanupE is fail-fast: it stops at the first delete error and returns it.
func (Result[T]) DebugString ¶
DebugString returns a human-readable tree of the execution result, showing each node's state (inserted/provided) and PK value.
func (Result[T]) MustNode ¶
func (r Result[T]) MustNode(name string) NodeResult
MustNode returns a named node or panics.
func (Result[T]) Node ¶
func (r Result[T]) Node(name string) (NodeResult, bool)
Node returns a named node from the dependency graph by blueprint name. If multiple nodes share the same blueprint name, the one with the lexicographically smallest node ID is returned. Node IDs are constructed as "root.relation" paths, so the smallest ID is typically the node closest to the root in the dependency graph.
To retrieve all nodes that match a given blueprint name, use Result.Nodes.
Example ¶
package main
import (
"context"
"fmt"
"testing"
"github.com/mhiro2/seedling"
)
type ExCompany struct {
ID int
Name string
}
type ExUser struct {
ID int
CompanyID int
Name string
}
type ExProject struct {
ID int
CompanyID int
Name string
}
type ExTask struct {
ID int
ProjectID int
AssigneeUserID int
Title string
Status string
}
func setupExampleBlueprints() {
seedling.ResetRegistry()
nextID := 0
next := func() int {
nextID++
return nextID
}
seedling.MustRegister(seedling.Blueprint[ExCompany]{
Name: "company",
Table: "companies",
PKField: "ID",
Defaults: func() ExCompany {
return ExCompany{Name: "test-company"}
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExCompany) (ExCompany, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExUser]{
Name: "user",
Table: "users",
PKField: "ID",
Defaults: func() ExUser {
return ExUser{Name: "test-user"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Traits: map[string][]seedling.Option{
"named": {seedling.Set("Name", "trait-user")},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExUser) (ExUser, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExProject]{
Name: "project",
Table: "projects",
PKField: "ID",
Defaults: func() ExProject {
return ExProject{Name: "test-project"}
},
Relations: []seedling.Relation{
{Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExProject) (ExProject, error) {
v.ID = next()
return v, nil
},
})
seedling.MustRegister(seedling.Blueprint[ExTask]{
Name: "task",
Table: "tasks",
PKField: "ID",
Defaults: func() ExTask {
return ExTask{Title: "test-task", Status: "open"}
},
Relations: []seedling.Relation{
{Name: "project", Kind: seedling.BelongsTo, LocalField: "ProjectID", RefBlueprint: "project"},
{
Name: "assignee",
Kind: seedling.BelongsTo,
LocalField: "AssigneeUserID",
RefBlueprint: "user",
When: seedling.WhenFunc(func(t ExTask) bool {
return t.Status == "assigned"
}),
},
},
Insert: func(ctx context.Context, db seedling.DBTX, v ExTask) (ExTask, error) {
v.ID = next()
return v, nil
},
})
}
func main() {
setupExampleBlueprints()
t := &testing.T{}
result := seedling.InsertOne[ExTask](t, nil)
node, ok := result.Node("project")
if !ok {
return
}
project, ok := node.Value().(ExProject)
if !ok {
return
}
fmt.Printf("%s %s\n", node.Name(), project.Name)
}
Output: project test-project
func (Result[T]) Nodes ¶
func (r Result[T]) Nodes(name string) []NodeResult
Nodes returns all nodes that match the given blueprint name, sorted by node ID (lexicographic order). Returns nil if no nodes match.
type Session ¶
type Session[T any] struct { // contains filtered or unexported fields }
Session binds seedling operations for type T to a specific registry and optional database handle.
func NewSession ¶
NewSession returns a typed session backed by the provided registry. If reg is nil, the package default registry is used.
func NewTestSession ¶
func NewTestSession[T any](tb testing.TB, reg *Registry, db TxBeginner, txOptions *sql.TxOptions) Session[T]
NewTestSession starts a SQL transaction, binds it to the session, and rolls it back during test cleanup.
This helper is specific to database/sql. If you use a different driver (e.g. pgx), use the companion package github.com/mhiro2/seedling/seedlingpgx or begin and defer-rollback the transaction yourself and pass it via NewSession + Session.WithDB.
func (Session[T]) Build ¶
Build constructs a dependency plan for type T without inserting anything. Fails the test on error.
func (Session[T]) BuildE ¶
BuildE constructs a dependency plan for type T without inserting anything.
func (Session[T]) InsertMany ¶
InsertMany creates and inserts n records of type T with the same options. Shared belongs-to dependencies are inserted once when their resolved options are identical across records. Fails the test on error.
func (Session[T]) InsertManyE ¶
func (s Session[T]) InsertManyE(ctx context.Context, db DBTX, n int, opts ...Option) (BatchResult[T], error)
InsertManyE creates and inserts n records of type T, returning a BatchResult for cleanup and graph inspection. When Seq options are present, the sequence function is called with the 0-based index for each record. Shared belongs-to dependencies are inserted once when their resolved options are identical across records.
func (Session[T]) InsertOne ¶
InsertOne creates and inserts a single record of type T with all required dependencies automatically resolved. Fails the test on error.
func (Session[T]) InsertOneE ¶
InsertOneE creates and inserts a single record of type T, returning an error on failure.
type TxBeginner ¶
TxBeginner begins SQL transactions for NewTestSession.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
seedling-gen
command
|
|
|
examples
|
|
|
Package faker provides deterministic fake data generators for use with seedling's Generate option.
|
Package faker provides deterministic fake data generators for use with seedling's Generate option. |
|
internal
|
|