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.
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.
Install the core module:
go get github.com/mhiro2/seedling@latestThen 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@latestInstall 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@latestThe 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.go schema.sqlOther input sources are supported (sqlc, GORM, ent, Atlas) -- see the Guide for the full list.
This generates struct types,
NewRegistry(),RegisterBlueprints(reg), deterministicDefaultsfor common scalar fields, relations, and Insert stubs. Fill in the// TODOcallbacks with your DB logic:Insert: func(ctx context.Context, db seedling.DBTX, v Company) (Company, error) { return insertCompany(ctx, db, v) // your DB call },
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.
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() has been called. The package name is set by --pkg; rename to whatever fits your project.
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()
_ = userUse 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()
_ = userWrap 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.
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.
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)
_ = resultWithInsertLog 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)
}),
)
_ = resultIf 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.
| 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 |
- basic: register blueprints and insert rows with automatic parent creation
- quickstart: generated-style
NewRegistry()/RegisterBlueprints(reg)flow that matches 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
- 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
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. Because seedlingpgx is a separate module, it pins the seedling version its consumers resolve; make check-seedlingpgx-parent-version reports whether that pin still matches the newest published release.
MIT
