Skip to content
Conduit

skill

seedling

Dependency-aware test data builder for Go that resolves foreign-key DB relationships automatically.

mhiro2/seedlingcommunitylistedunreviewed

Set up

Skills are installed by placing their folder where your agent loads skills.

The download comes from github.com/mhiro2/seedling itself, not from our server, so what you get is exactly what its author published.

Install instructions come from the README

Skills do not use an MCP config. Follow the steps in the README below.

README

synced Oct 26, 2026

✨ 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 and minimal graph expansion
  • 🌿 First-class relation kinds: BelongsTo, HasMany, ManyToMany, plus composite keys
  • 🔧 Per-test overrides via Set, Use, and Ref
  • ♻️ WithTx for auto-rollback transactions -- no manual cleanup
  • 🔌 Works with sqlc, database/sql, pgx, GORM, ent, or any other DB handle you own
  • 🎲 Deterministic fake data via seedling/faker with multi-locale support (en, ja, zh, ko, de, fr)

For advanced features such as InsertMany, batch sharing, Only, When, and dry runs, see the Guide.

📦 Installation

Install the core module:

go get github.com/mhiro2/seedling@latest

Then add an import in your code:

import "github.com/mhiro2/seedling"

seedling/faker is included in the core module. The pgx companion is published as a separate Go module and must be installed separately:

go get github.com/mhiro2/seedling/seedlingpgx@latest

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

The shortest path is two steps: generate blueprints from your schema, then call InsertOne in a test.

  1. Generate blueprints from your schema

    # From SQL DDL
    seedling-gen sql --pkg testutil --out blueprints_gen.go schema.sql
    

    Other input sources are supported (sqlc, GORM, ent, Atlas) -- see the Guide for the full list.

    This generates struct types and a Blueprints struct (built by NewBlueprints()) holding one blueprint per table, with deterministic Defaults for common scalar fields, relations, and Register / MustRegister methods. The file is rewritten on every run, so keep your DB logic in a separate hand-written file of the same package:

    // blueprints.go (hand-written)
    func NewRegistry() *seedling.Registry {
        blueprints := NewBlueprints()
        blueprints.Company.Insert = func(ctx context.Context, db seedling.DBTX, v Company) (Company, error) {
            return insertCompany(ctx, db, v) // your DB call
        }
        blueprints.User.Insert = func(ctx context.Context, db seedling.DBTX, v User) (User, error) {
            return insertUser(ctx, db, v) // your DB call
        }
    
        reg := seedling.NewRegistry()
        blueprints.MustRegister(reg)
        return reg
    }
    

    Plain DDL does not say how rows are written, so sql and atlas leave Insert unset and registration fails until you set it. The sqlc, GORM, and ent adapters generate Insert where they can. Re-run the same command after a schema change; your hand-written file needs no repair, and adding --check in CI fails when the generated file is stale.

    Generated relations retain the referenced columns, so foreign keys targeting unique non-primary columns use RefField/RefFields instead of being treated as primary-key references. Relations are still named after the referenced table, so recording those columns does not rename an existing relation; a name derived from the FK columns is used only to separate two foreign keys that would otherwise collide. Generated Defaults intentionally skip primary keys, relation FK fields, and unsupported custom types. They are meant to make the first insert usable with zero setup, not to satisfy every unique or business constraint automatically.

    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) {
        reg := testutil.NewRegistry()
    
        result := seedling.NewSession[testutil.User](reg).InsertOne(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")
        }
    }
    

That is the entire happy path. The next section shows the handful of patterns you reach for once tests grow.

🧩 Typical Use Cases

These three patterns cover the majority of real test code. Anything beyond them lives in the Guide.

[!NOTE] All snippets below assume the generated package (e.g. from seedling-gen sql --pkg testutil schema.sql) is imported as testutil, and reg := testutil.NewRegistry() (the hand-written function above) has been called. The package name is set by --pkg; rename to whatever fits your project.

Override a field

Use Set when a test needs a specific column value:

result := seedling.NewSession[testutil.User](reg).InsertOne(t, db,
    seedling.Set("Name", "alice"),
)
user := result.Root()
_ = user

Reuse an existing parent row

Use Use to bind a relation to a row you already inserted, instead of letting seedling create another one:

company := seedling.NewSession[testutil.Company](reg).InsertOne(t, db).Root()

result := seedling.NewSession[testutil.User](reg).InsertOne(t, db,
    seedling.Use("company", company),
)
user := result.Root()
_ = user

Auto-rollback per test

Wrap the test in a transaction that rolls back automatically on cleanup. No manual deletion, no leaking state across tests.

func TestUser(t *testing.T) {
    tx := seedling.WithTx(t, db) // rollback runs at t.Cleanup
    user := seedling.NewSession[testutil.User](reg).InsertOne(t, tx).Root()
    _ = user
}

For a runnable example, see examples/with-tx. For pgx-based projects, seedlingpgx.WithTx provides the same workflow.

🩺 Diagnostics

When a graph misbehaves, you usually want to answer one of three questions: what would be inserted, what was inserted, or how do I clean it up. seedling exposes one helper for each.

Inspect the plan before executing

Use Build to construct the plan without inserting, then print the dependency tree or the dry-run insert order:

plan := seedling.NewSession[testutil.Task](reg).Build(t,
    seedling.Ref("project", seedling.Set("Name", "renewal")),
)

t.Log(plan.DebugString())   // dependency tree
t.Log(plan.DryRunString())  // insert order + FK assignments

result := plan.Insert(t, db)
_ = result

Trace inserts at runtime

WithInsertLog reports each insert step (table, primary key, FK assignments) as it happens:

result := seedling.NewSession[testutil.Task](reg).InsertOne(t, db,
    seedling.WithInsertLog(func(log seedling.InsertLog) {
        t.Logf("step %d: %s (fks: %v)", log.Step, log.Table, log.FKBindings)
    }),
)
_ = result

Clean up when transactions are not an option

If you cannot use WithTx, call Result.Cleanup to delete inserted rows in reverse dependency order:

result := seedling.NewSession[testutil.User](reg).InsertOne(t, db)
t.Cleanup(func() { result.Cleanup(t, db) })

Cleanup uses a bounded context that remains active during t.Cleanup, even after the test context is canceled. Its budget follows the test binary's own deadline; pass seedling.WithCleanupTimeout to cap it explicitly when tearing down a large graph against a slow database.

Every blueprint in the inserted graph (the root and every auto-inserted parent) needs a Delete callback. All callbacks are checked up front, so if any is missing Cleanup fails with ErrDeleteNotDefined before deleting anything. seedling-gen emits Delete for gorm and ent, and for sqlc when a unique Delete<Table> query exists; the sql and atlas subcommands leave it for you to write.

The Guide lists the full set of debugging APIs, including BatchResult.NodeAt for InsertMany graphs.

⚖️ Comparison

ToolMain modelStrong atNot designed for
seedlingDependency-aware builders with DB callbacksPer-test graph generation, automatic FK resolution, type-safe overrides, graph inspection, codegenBulk loading large static fixture files
eyo-chen/gofactoGeneric factory with explicit FK associationsErgonomic zero-config field filling, WithOne/WithMany associations, multi-DB supportAutomatic graph resolution, minimal graph expansion
go-testfixtures/testfixturesFixture files loaded into DBStable predefined datasets for integration testsRelation-aware per-test graph construction
bluele/factory-goIn-memory object factoriesFlexible object construction and traits-like compositionPlanning SQL insert order across FK graphs
brianvoe/gofakeitFake data generatorRealistic random valuesDatabase insertion orchestration or relation expansion

📂 Examples

  • basic: register blueprints and insert rows with automatic parent creation
  • quickstart: seedling-gen output kept apart from a hand-written NewRegistry(), matching 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 the seedling-gen gorm, seedling-gen ent, or seedling-gen atlas subcommands to generate blueprints from your existing schema definitions. Run entc first: the Ent adapter verifies exact field and builder signatures against the generated package supplied with --import-path

📚 Learn More

🛠️ Development

make fmt and make lint cover the root, integration, and seedlingpgx modules; make test runs the root and seedlingpgx unit tests. The integration suites run separately with make test-integration-{postgres,mysql,sqlite}. PostgreSQL and MySQL start containers with testcontainers and skip when Docker is unavailable; set SEEDLING_INTEGRATION_REQUIRED=1 (as CI does) to turn that skip into a failure. SQLite runs in-process and never skips. Fuzz targets run weekly in CI and on demand with make fuzz.

Releases are built from vX.Y.Z tags, and seedlingpgx/vX.Y.Z is tagged on the same commit. Because seedlingpgx is a separate module, it pins the seedling version its consumers resolve, while a replace directive builds it against the in-repo root during development. Before tagging, set that pin to the release being cut; make check-seedlingpgx-parent-version VERSION=vX.Y.Z verifies it.

📝 License

MIT

↑↓ navigate openesc close