seedling

package module
v0.2.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 31, 2026 License: MIT Imports: 17 Imported by: 0

README

seedling logo

seedling

Dependency-aware test data builder for Go and SQL databases.
seedling lets tests create only the rows they need while automatically resolving foreign-key dependencies in the correct order. You provide the insert logic. seedling handles planning, FK assignment, and execution order.

Go Reference CI MIT License


✨ Why seedling?

Manually wiring FK dependencies across 4 tables:

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 graph is resolved automatically:

func TestCreateTask(t *testing.T) {
    result := seedling.InsertOne[Task](t, db)
    task := result.Root()
    _ = task
}

seedling handles FK ordering, graph expansion, and cleanup so your tests stay focused on what matters:

  • 🪶 Zero runtime dependencies in the core module; optional DB helpers live in companion packages
  • 🔗 Automatic FK resolution with topological insert ordering
  • 🌿 Minimal graph expansion: only required ancestors are inserted
  • 🔧 Type-safe per-test overrides with Set, Use, Ref, With, When, and Only
  • ♻️ WithTx and companion helpers for auto-rollback transactions -- no manual cleanup
  • 🔌 Works with sqlc, database/sql, pgx, GORM, or any other DB handle you own
  • 📊 Supports HasMany, ManyToMany, composite keys, cleanup, dry runs, and insert logging
  • 🎲 Includes deterministic fake data via seedling/faker with multi-locale support (en, ja, zh, ko, de, fr)

📦 Installation

Add an import in your code, then let the toolchain record the dependency:

import "github.com/mhiro2/seedling"

Use the same pattern for companion packages when you need them, for example seedling/faker (github.com/mhiro2/seedling/faker) or seedlingpgx (github.com/mhiro2/seedling/seedlingpgx).

Install the seedling-gen CLI (pick one):

# Homebrew (macOS / Linux) — [third-party tap](https://github.com/mhiro2/homebrew-tap)
brew install --cask mhiro2/tap/seedling-gen
# Go toolchain
go install github.com/mhiro2/seedling/cmd/seedling-gen@latest

🚀 Quick Start

  1. Generate blueprints from your schema

    # From SQL DDL
    seedling-gen sql --pkg testutil --out blueprints.go schema.sql
    
    # Or from other sources:
    seedling-gen sqlc --config sqlc.yaml --pkg testutil --out blueprints.go
    seedling-gen gorm --dir ./models --import-path github.com/you/app/models --pkg testutil
    seedling-gen ent --dir ./ent/schema --import-path github.com/you/app/ent --pkg testutil
    seedling-gen atlas --pkg testutil schema.hcl
    seedling-gen sql --explain schema.sql
    

    This generates struct types, RegisterBlueprints(), relations, and Insert stubs. Fill in the // TODO callbacks with your DB logic:

    Insert: func(ctx context.Context, db seedling.DBTX, v Company) (Company, error) {
        return insertCompany(ctx, db, v) // your DB call
    },
    

    The snippets below assume the generated package is named testutil. For a runnable minimal version of this flow, see examples/quickstart.

  2. Use it in tests

    func TestUser(t *testing.T) {
        seedling.ResetRegistry()
        testutil.RegisterBlueprints()
    
        result := seedling.InsertOne[testutil.User](t, db)
        user := result.Root()
    
        if user.ID == 0 {
            t.Fatal("expected user ID to be set")
        }
        if user.CompanyID == 0 {
            t.Fatal("expected company to be inserted automatically")
        }
    }
    
  3. Override only what the test cares about

    func TestNamedUser(t *testing.T) {
        seedling.ResetRegistry()
        testutil.RegisterBlueprints()
    
        company := seedling.InsertOne[testutil.Company](t, db).Root()
    
        result := seedling.InsertOne[testutil.User](t, db,
            seedling.Set("Name", "alice"),
            seedling.Use("company", company),
        )
    
        user := result.Root()
        _ = user
    }
    
    func TestTaskProject(t *testing.T) {
        seedling.ResetRegistry()
        testutil.RegisterBlueprints()
    
        // Only("project") inserts task + project subtree only,
        // skipping the assignee relation entirely.
        result := seedling.InsertOne[testutil.Task](t, db,
            seedling.Only("project"),
        )
        _ = result
    }
    

    When you want automatic rollback with database/sql, use seedling.WithTx(t, db). For a runnable transaction-focused example, see examples/with-tx.

    For a runnable batch-oriented example, see examples/batch-insert.

⚖️ Comparison

Tool Main model Strong at Not designed for
seedling Dependency-aware builders with DB callbacks Per-test graph generation, automatic FK resolution, type-safe overrides, graph inspection, codegen Bulk loading large static fixture files
eyo-chen/gofacto Generic factory with explicit FK associations Ergonomic zero-config field filling, WithOne/WithMany associations, multi-DB support Automatic graph resolution, minimal graph expansion
go-testfixtures/testfixtures Fixture files loaded into DB Stable predefined datasets for integration tests Relation-aware per-test graph construction
bluele/factory-go In-memory object factories Flexible object construction and traits-like composition Planning SQL insert order across FK graphs
brianvoe/gofakeit Fake data generator Realistic random values Database insertion orchestration or relation expansion

📂 Examples

  • basic: register blueprints and insert rows with automatic parent creation
  • quickstart: generated-style RegisterBlueprints() flow that matches the README Quick Start
  • custom-defaults: customize values with Set, With, and Generate
  • reuse-parent: reuse existing rows with Use
  • batch-insert: batch inserts with shared Ref dependencies and per-row SeqRef overrides
  • with-tx: database/sql transaction helper with seedling.WithTx
  • sqlc: wire blueprints to sqlc-generated query code
  • pgx transactions: use github.com/mhiro2/seedling/seedlingpgx with pgxpool.Pool or *pgx.Conn
  • GORM / ent / Atlas: use seedling-gen with -gorm, -ent, or -atlas flags to generate blueprints from your existing schema definitions

📚 Learn More

📝 License

MIT

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

Blueprint

A blueprint defines how to create one model type: default field values, primary-key metadata, relations, and insert/delete callbacks.

Relation

Relations describe graph edges such as belongs-to, has-many, and many-to-many. seedling uses them to expand the graph and bind keys.

Option

Options customize a single insert/build call. Common examples are Set, Use, Ref, Omit, When, and With.

Plan and Result

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.

Session

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.

Frequently used APIs:

Index

Examples

Constants

This section is empty.

Variables

View Source
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

func InsertMany[T any](tb testing.TB, db DBTX, n int, opts ...Option) []T

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

func MustRegister[T any](bp Blueprint[T])

MustRegister registers a blueprint in the package default registry and panics on error.

func MustRegisterTo

func MustRegisterTo[T any](dst *Registry, bp Blueprint[T])

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 Register

func Register[T any](bp Blueprint[T]) error

Register registers a blueprint in the package default registry.

func RegisterTo

func RegisterTo[T any](dst *Registry, bp Blueprint[T]) error

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

func WhenFunc[T any](fn func(T) bool) func(any) bool

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

func For[T any]() *Builder[T]

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

func ForSession[T any](s Session[T]) *Builder[T]

ForSession creates a new Builder for type T using a specific session.

func (*Builder[T]) AfterInsert

func (b *Builder[T]) AfterInsert(fn func(T, DBTX)) *Builder[T]

AfterInsert registers a callback that runs after the root record is inserted.

func (*Builder[T]) AfterInsertE

func (b *Builder[T]) AfterInsertE(fn func(T, DBTX) error) *Builder[T]

AfterInsertE registers an error-returning callback that runs after the root record is inserted.

func (*Builder[T]) Apply

func (b *Builder[T]) Apply(opts ...Option) *Builder[T]

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

func (b *Builder[T]) BlueprintTrait(name string) *Builder[T]

BlueprintTrait applies a named trait defined on the target blueprint.

func (*Builder[T]) Build

func (b *Builder[T]) Build(tb testing.TB) *Plan[T]

Build constructs a dependency plan without inserting anything. Fails the test on error.

func (*Builder[T]) BuildE

func (b *Builder[T]) BuildE() (*Plan[T], error)

BuildE constructs a dependency plan without inserting anything.

func (*Builder[T]) Generate

func (b *Builder[T]) Generate(fn func(*rand.Rand, *T)) *Builder[T]

Generate applies a rand-driven mutation function.

func (*Builder[T]) GenerateE

func (b *Builder[T]) GenerateE(fn func(*rand.Rand, *T) error) *Builder[T]

GenerateE applies a rand-driven mutation function that can return an error.

func (*Builder[T]) InlineTrait

func (b *Builder[T]) InlineTrait(opts ...Option) *Builder[T]

InlineTrait applies an inline trait composed from explicit options.

func (*Builder[T]) Insert

func (b *Builder[T]) Insert(tb testing.TB, db DBTX) Result[T]

Insert creates and inserts a single record. Fails the test on error.

func (*Builder[T]) InsertE

func (b *Builder[T]) InsertE(ctx context.Context, db DBTX) (Result[T], error)

InsertE creates and inserts a single record, returning an error on failure.

func (*Builder[T]) InsertMany

func (b *Builder[T]) InsertMany(tb testing.TB, db DBTX, n int) []T

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

func (b *Builder[T]) InsertManyE(ctx context.Context, db DBTX, n int) (BatchResult[T], error)

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]) Omit

func (b *Builder[T]) Omit(name string) *Builder[T]

Omit prevents auto-creation of an optional relation.

func (*Builder[T]) Ref

func (b *Builder[T]) Ref(name string, opts ...Option) *Builder[T]

Ref applies nested options to a specific relation's blueprint.

func (*Builder[T]) Set

func (b *Builder[T]) Set(field string, value any) *Builder[T]

Set overrides a struct field value by its Go field name.

func (*Builder[T]) Use

func (b *Builder[T]) Use(name string, value any) *Builder[T]

Use provides an existing record for a direct relation, skipping auto-creation.

func (*Builder[T]) With

func (b *Builder[T]) With(fn func(*T)) *Builder[T]

With applies a type-safe modification function to the root struct.

func (*Builder[T]) WithContext

func (b *Builder[T]) WithContext(ctx context.Context) *Builder[T]

WithContext sets the context used for insert operations.

func (*Builder[T]) WithRand

func (b *Builder[T]) WithRand(r *rand.Rand) *Builder[T]

WithRand sets the RNG used by Generate options.

func (*Builder[T]) WithSeed

func (b *Builder[T]) WithSeed(seed uint64) *Builder[T]

WithSeed sets the RNG seed used by Generate options.

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.

func (NodeResult) Value

func (n NodeResult) Value() any

Value returns the inserted value.

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option configures how a record is built and inserted.

func AfterInsert

func AfterInsert[T any](fn func(t T, db DBTX)) Option

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

func AfterInsertE[T any](fn func(t T, db DBTX) error) Option

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

func BlueprintTrait(name string) Option

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

func Generate[T any](fn func(r *rand.Rand, t *T)) Option

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

func GenerateE[T any](fn func(r *rand.Rand, t *T) error) Option

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

func InlineTrait(opts ...Option) Option

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 Omit

func Omit(name string) Option

Omit prevents auto-creation of an optional relation.

seedling.Omit("assignee")

func Only

func Only(relations ...string) Option

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

func Ref(name string, opts ...Option) Option

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

func Seq[V any](field string, fn func(i int) V) Option

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

func SeqRef(name string, fn func(i int) []Option) Option

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

func SeqUse[V any](name string, fn func(i int) V) Option

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

func Set(field string, value any) Option

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

func Use(name string, value any) Option

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

func When[T any](name string, fn func(T) bool) Option

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

func With[T any](fn func(*T)) Option

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

func WithContext(ctx context.Context) Option

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

func WithInsertLog(fn func(InsertLog)) Option

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

func WithRand

func WithRand(r *rand.Rand) Option

WithRand sets the RNG used by Generate options on the current node.

func WithSeed

func WithSeed(seed uint64) Option

WithSeed is a convenience wrapper around WithRand(rand.New(rand.NewPCG(seed, seed^goldenGamma))).

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

func Build[T any](tb testing.TB, opts ...Option) *Plan[T]

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

func BuildE[T any](opts ...Option) (*Plan[T], error)

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

func (p *Plan[T]) DebugString() string

DebugString returns a human-readable tree representation of the plan.

func (*Plan[T]) DryRunString

func (p *Plan[T]) DryRunString() string

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

func (*Plan[T]) Insert

func (p *Plan[T]) Insert(tb testing.TB, db DBTX) Result[T]

Insert executes the plan and inserts all records. Fails the test on error.

func (*Plan[T]) InsertE

func (p *Plan[T]) InsertE(ctx context.Context, db DBTX) (Result[T], error)

InsertE executes the plan and inserts all records, returning an error on failure.

func (*Plan[T]) Validate

func (p *Plan[T]) Validate() error

Validate performs a dry-run of the plan, checking for type mismatches and constraint violations without inserting any records.

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.

func NewRegistry

func NewRegistry() *Registry

NewRegistry creates an isolated blueprint registry.

func (*Registry) Reset

func (r *Registry) Reset()

Reset clears all blueprints from the registry.

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

func BelongsToRelation(name, refBlueprint string, optional bool, localFields ...string) Relation

BelongsToRelation builds a BelongsTo relation with explicit semantics.

func HasManyRelation

func HasManyRelation(name, refBlueprint string, optional bool, count int, localFields ...string) Relation

HasManyRelation builds a HasMany relation with explicit semantics.

func ManyToManyRelation

func ManyToManyRelation(name, throughBlueprint, refBlueprint string, optional bool, count int, localFields, remoteFields []string) Relation

ManyToManyRelation builds a ManyToMany 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

func InsertOne[T any](tb testing.TB, db DBTX, opts ...Option) Result[T]

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

func InsertOneE[T any](ctx context.Context, db DBTX, opts ...Option) (Result[T], error)

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

func (r Result[T]) Cleanup(tb testing.TB, db DBTX)

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

func (r Result[T]) CleanupE(ctx context.Context, db DBTX) error

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

func (r Result[T]) DebugString() string

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.

func (Result[T]) Root

func (r Result[T]) Root() T

Root returns the root record that was inserted.

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

func NewSession[T any](reg *Registry) Session[T]

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

func (s Session[T]) Build(tb testing.TB, opts ...Option) *Plan[T]

Build constructs a dependency plan for type T without inserting anything. Fails the test on error.

func (Session[T]) BuildE

func (s Session[T]) BuildE(opts ...Option) (*Plan[T], error)

BuildE constructs a dependency plan for type T without inserting anything.

func (Session[T]) DB

func (s Session[T]) DB() DBTX

DB returns the database handle bound to the session.

func (Session[T]) InsertMany

func (s Session[T]) InsertMany(tb testing.TB, db DBTX, n int, opts ...Option) []T

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

func (s Session[T]) InsertOne(tb testing.TB, db DBTX, opts ...Option) Result[T]

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

func (s Session[T]) InsertOneE(ctx context.Context, db DBTX, opts ...Option) (Result[T], error)

InsertOneE creates and inserts a single record of type T, returning an error on failure.

func (Session[T]) WithDB

func (s Session[T]) WithDB(db DBTX) Session[T]

WithDB returns a copy of the session bound to db.

type TxBeginner

type TxBeginner interface {
	BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error)
}

TxBeginner begins SQL transactions for NewTestSession.

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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL