Practical workflows and API patterns for using seedling in your tests. Start with README Installation and Quick Start if you haven't set up seedling yet.
Use InsertOne when a test needs a single root record and all required parents.
result := seedling.InsertOne[Task](t, db)
task := result.Root()Use InsertMany when a test needs multiple records of the same type.
users := seedling.InsertMany[User](t, db, 3,
seedling.Seq("Name", func(i int) string {
return fmt.Sprintf("user-%d", i)
}),
)Use InsertManyE when you need the full batch result for debugging or cleanup.
result, err := seedling.InsertManyE[User](ctx, db, 3,
seedling.Seq("Name", func(i int) string {
return fmt.Sprintf("user-%d", i)
}),
)
if err != nil {
_ = result.CleanupE(ctx, db)
t.Fatal(err)
}
users := result.Roots()
_ = users
company, ok := result.NodeAt(1, "company")
_ = company
_ = okInsertMany batch-shares auto-created BelongsTo parents when each record resolves to the same static relation options.
tasks := seedling.InsertMany[Task](t, db, 2,
seedling.Ref("project", seedling.Set("Name", "shared-project")),
)In this example, project is inserted once and both tasks point to the same row.
- Sharing applies only to auto-created
BelongsTorelations inInsertMany - The dedupe key is the relation path plus the resolved option tree for that path, after
SeqandSeqRefare expanded per record - Static option trees made of
Set, nestedRef, andOmitcan be shared Use,With,Generate,When, and rand-driven options make that relation non-shareable- If the resolved options differ per record, seedling inserts separate parent rows
Use Build when you want to inspect or validate the graph before executing inserts.
plan := seedling.Build[Task](t,
seedling.Ref("project", seedling.Set("Name", "renewal")),
)
if err := plan.Validate(); err != nil {
t.Fatal(err)
}
result := plan.Insert(t, db)
task := result.Root()
_ = taskWhen a plan includes AfterInsert / AfterInsertE, remember that those callbacks are captured once at Build time. Reusing the same Plan also reuses any closure state captured by those callbacks, so rebuild the plan if each execution needs isolated callback state.
Use Use to bind a relation to a row that already exists.
company := seedling.InsertOne[Company](t, db).Root()
user := seedling.InsertOne[User](t, db,
seedling.Use("company", company),
).Root()
_ = userUse Only when a blueprint has many relations but the test only needs a subset. The planner builds only the necessary subgraph, skipping relations not listed in Only. DebugString / DryRunString reflect this lazily built graph.
// Insert task + project subtree only. Assignee and its dependencies are skipped.
result := seedling.InsertOne[Task](t, db,
seedling.Only("project"),
)
// Only() with no arguments inserts just the root record.
result := seedling.InsertOne[Task](t, db, seedling.Only())Only also works with InsertMany. The filter is applied per root before batch sharing is resolved, so matching BelongsTo parents can still be shared across the batch.
Use WithTx to wrap each test in a transaction that rolls back on cleanup. This is the simplest way to isolate test data without manual deletion.
func TestUser(t *testing.T) {
tx := seedling.WithTx(t, db)
user := seedling.InsertOne[User](t, tx).Root()
// tx.Rollback() is called automatically when the test finishes.
_ = user
}For more control (custom sql.TxOptions, registry binding), use NewTestSession instead.
If you use pgx directly, github.com/mhiro2/seedling/seedlingpgx provides the same workflow for pgxpool.Pool or *pgx.Conn.
func TestUser(t *testing.T) {
tx := seedlingpgx.WithTx(t, pool)
user := seedling.InsertOne[User](t, tx).Root()
_ = user
}Set: override one field by Go struct field nameRef: customize the auto-created node behind a relation and explicitly enable an optional relationUse: reuse an existing record instead of inserting a new relation targetOmit: skip an optional relationOnly: restrict insertion to specific relation subtreesWhen: expand a relation only when the current record matches a condition, including optional relations when the predicate returns trueWith: mutate the root struct with full type safetyGenerate+WithSeed/WithRand: generate deterministic values for property-style testsWithInsertLog: observe insert steps and FK assignments during execution
For runnable examples of these options, see example_test.go.
BelongsTo: insert required parent rows and write the parent key into the childHasMany: insert children automatically from a parent blueprint usingCountManyToMany: create related rows and join-table rows together- Composite keys: use
PKFields,LocalFields, andRemoteFields
The execution model and graph expansion rules are documented in ARCHITECTURE.md.
seedling does not generate SQL at runtime. Your blueprint owns the Insert and optional Delete callbacks, so the library works with any DB abstraction that your code already uses.
- sqlc: map
Insertcallbacks to generated query methods. Preferseedling-gen sqlc --config ...for automatic setup database/sql: pass*sql.DBor*sql.Tx- pgx: pass your pool or transaction handle, or use
github.com/mhiro2/seedling/seedlingpgxfor rollback-on-cleanup helpers - GORM: use
-gormto generate blueprints withgorm.DB-based Insert/Delete callbacks - ent: use
-entto generate blueprints with ent fluent builder Insert/Delete callbacks - Atlas HCL: use
-atlasto generate blueprints from Atlas schema definitions
When you use database/sql, WithTx is the easiest way to get a rollback-on-cleanup transaction. NewTestSession offers the same with registry binding and custom sql.TxOptions. For pgx, use seedlingpgx.WithTx or seedlingpgx.NewTestSession.
Plan.DebugString: inspect the dependency tree before insertsPlan.DryRunString: inspect insert order and FK assignments without executing insertsResult.DebugString: inspect inserted nodes with primary-key valuesResult.Node(name): returns the lexicographically smallest matching node ID when multiple nodes share the same blueprint nameResult.Nodes(name): returns all matching nodes in node ID orderResult.Cleanup/CleanupE: delete inserted rows in reverse dependency order when transaction rollback is not availableBatchResult.DebugString: inspect the full batch execution graph with primary-key valuesBatchResult.Node(name): searches across the full batch, so use it only when cross-root ambiguity is acceptableBatchResult.NodeAt(rootIndex, name)/NodesForRoot(rootIndex, name): inspect one root and its shared ancestors without mixing in sibling rootsBatchResult.Cleanup/CleanupE: delete rows inserted byInsertManyE; cleanup is fail-fast and stops at the first delete error
seedling-gen generates model and blueprint skeletons from multiple input sources.
Generated blueprints now include deterministic Defaults for common scalar fields such as strings, numbers, booleans, byte slices, and time.Time. Primary keys, relation FK fields, and unsupported custom types stay untouched so relation resolution and application-specific constraints remain explicit.
Install the CLI:
# Homebrew (macOS / Linux)
brew install --cask mhiro2/tap/seedling-gen# Go toolchain
go install github.com/mhiro2/seedling/cmd/seedling-gen@latestExamples:
# SQL DDL
seedling-gen sql --pkg blueprints schema.sql
# sqlc config: auto-resolves schema, output dir, and import path from sqlc.yaml
seedling-gen sqlc --config sqlc.yaml --pkg blueprints
# sqlc manual mode: use generated Go files plus an explicit schema.sql
seedling-gen sqlc --dir ./internal/db --import-path github.com/you/app/internal/db --pkg blueprints schema.sql
# GORM models: parses Go source with gorm struct tags
seedling-gen gorm --dir ./models --import-path github.com/you/app/models --pkg blueprints
# ent schemas: parses ent schema directory (Fields/Edges methods)
seedling-gen ent --dir ./ent/schema --import-path github.com/you/app/ent --pkg blueprints
# Atlas HCL: parses Atlas schema file
seedling-gen atlas --pkg blueprints schema.hclAll subcommands support --pkg (generated package name) and --out (output file path). The sql and sqlc subcommands also support --dialect (auto, postgres, mysql, sqlite) as a validation hint. The SQL parser itself uses the same logic for all dialects, so --dialect does not change parsing behavior.
All subcommands also support diagnostic output modes:
--explain: print the parsed schema/model metadata plus the inferred blueprint relations instead of generated Go code--json: print the same diagnostic report as JSON, which is useful for tooling or CI checks
The sqlc subcommand has two input modes:
--config: readsqlc.yamland auto-resolve schema files, output directory, and Go import path--dir+--import-path+<schema.sql>: manually point at generated sqlc Go files and the schema DDL
When --out is specified, the output is written atomically via a temporary file so that a generation failure never leaves a partial file on disk.
Treat generated Defaults as ergonomic starters, not full fake-data generation. If a test needs unique values or domain-specific shapes, override them with Set, With, or Generate.
The faker package supports multiple locales for generating locale-appropriate fake data. Use NewWithLocale to select a locale:
seedling.Generate(func(r *rand.Rand, u *User) {
f := faker.NewWithLocale(r, "ja")
u.Name = f.Name() // "佐藤太郎"
u.Email = f.Email() // "taro.sato@example.com"
u.Phone = f.Phone() // "+81-03-1234-5678"
})Supported locales: en (default), ja, zh, ko, de, fr.
New(r) defaults to "en" and is fully backward compatible.
- 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 -- use
seedling.WithTxfor automatic rollback withdatabase/sql - sqlc -- wire blueprints to sqlc-generated query code
- Architecture -- internal pipeline design (planner, graph, executor)
- README -- project overview, Quick Start, and comparison table
- pkg.go.dev package docs
- faker package docs