skill
seedling
Dependency-aware test data builder for Go that resolves foreign-key DB relationships automatically.
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
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, andRef - ♻️
WithTxfor 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/fakerwith 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.
-
Generate blueprints from your schema
# From SQL DDL seedling-gen sql --pkg testutil --out blueprints_gen.go schema.sqlOther input sources are supported (sqlc, GORM, ent, Atlas) -- see the Guide for the full list.
This generates struct types and a
Blueprintsstruct (built byNewBlueprints()) holding one blueprint per table, with deterministicDefaultsfor common scalar fields, relations, andRegister/MustRegistermethods. 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
sqlandatlasleaveInsertunset and registration fails until you set it. The sqlc, GORM, and ent adapters generateInsertwhere they can. Re-run the same command after a schema change; your hand-written file needs no repair, and adding--checkin CI fails when the generated file is stale.Generated relations retain the referenced columns, so foreign keys targeting unique non-primary columns use
RefField/RefFieldsinstead 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. GeneratedDefaultsintentionally 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. -
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 astestutil, andreg := 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
| 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: seedling-gen output kept apart from a hand-written
NewRegistry(), matching the README Quick Start - custom-defaults: customize values with
Set,With, andGenerate - reuse-parent: reuse existing rows with
Use - batch-insert: batch inserts with shared
Refdependencies and per-rowSeqRefoverrides - with-tx:
database/sqltransaction helper withseedling.WithTx - sqlc: wire blueprints to sqlc-generated query code
- pgx transactions: use
github.com/mhiro2/seedling/seedlingpgxwithpgxpool.Poolor*pgx.Conn - GORM / ent / Atlas: use the
seedling-gen gorm,seedling-gen ent, orseedling-gen atlassubcommands 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
- Guide -- workflows, full option reference (
Only,When,InsertMany, batch sharing, ...), and integration patterns - Architecture -- internal pipeline design (planner, graph, executor)
- Agent Skill: seedling-gen CLI -- instructions for AI agents that need to choose the right generator mode and scaffold blueprints
- Agent Skill: seedling test setup -- instructions for AI agents that write Go tests using seedling blueprints
- pkg.go.dev API reference -- full type and function docs
🛠️ 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