Rails-style db:seed for GORM: load JSON fixtures into your
database with idempotent, conflict-aware inserts.
Test-fixture loaders truncate and reload — wrong for reference, demo, and
initial data. gorm-seed is built for repeatable seeding: by default it
emits INSERT ... ON CONFLICT DO NOTHING, so running it twice is a no-op
instead of a duplicate-key error.
import gormseed "github.com/promptrails/gorm-seed"
seeder := gormseed.New(db, gormseed.WithAutoOrder()).
Add("users.json", &[]User{}).
Add("posts.json", &[]Post{}, gormseed.OnConflict(gormseed.UpdateAll()))
res, err := seeder.Run(ctx, gormseed.Dir("fixtures"))
fmt.Printf("seeded %d rows\n", res.Inserted())fixtures/users.json:
[
{ "ID": 1, "Name": "Alice", "Email": "alice@example.com" },
{ "ID": 2, "Name": "Bob", "Email": "bob@example.com" }
]go get github.com/promptrails/gorm-seedRequires Go 1.26+. The only runtime dependency is GORM itself.
- Idempotent by default —
ON CONFLICT DO NOTHING; safe to run on every boot. - Conflict strategies —
Skip(default),Update(cols…),UpdateAll(), orError(), per spec or as a seeder-wide default; override the conflict target withConflictTarget. - Foreign-key-safe ordering —
WithAutoOrder()inspects belongs-to and has-many relationships and loads parents before children;After(...)adds explicit edges. Cycles are reported, not silently mis-ordered. - Any
fs.FS— a directory (Dir("...")) or anembed.FSwork the same way. - Profiles — tag specs (
Profile("demo")) and activate them per run (WithProfiles("demo")); untagged specs always load. - Dry run, transactions, reporting —
WithDryRun(),WithTransaction(), and a per-specResult. - Pluggable decoders — JSON built in; add YAML or others with
WithDecoder, no extra dependency in the core. - Batch inserts —
WithBatchSize(n)for large fixture files. - Table cleanup —
Clean(ctx)to drop and recreate tables. - Hooks —
WithBeforeSeedHook/WithAfterSeedHookfor instrumentation. - Logging —
WithLogger(l)for progress output.
gormseed.Skip() // keep existing row (default, idempotent)
gormseed.Update("name") // overwrite named columns on conflict
gormseed.UpdateAll() // overwrite every non-key column
gormseed.Error() // plain insert; duplicates surface as an errorseeder.Add("plans.json", &[]Plan{}, gormseed.OnConflict(gormseed.UpdateAll()))Override the conflict target:
seeder.Add("plans.json", &[]Plan{},
gormseed.OnConflict(gormseed.UpdateAll()),
gormseed.ConflictTarget("code"),
)Set a seeder-wide default:
gormseed.New(db, gormseed.WithDefaultConflict(gormseed.UpdateAll()))Without options, specs load in registration order — you control the sequence.
Turn on WithAutoOrder() to let the seeder derive a foreign-key-safe order from
your models' belongs-to and has-many relationships, or pin specific edges with
After:
gormseed.New(db, gormseed.WithAutoOrder()).
Add("posts.json", &[]Post{}). // registered first…
Add("users.json", &[]User{}) // …but loaded first (Post belongs to User)Has-many relationships are also respected:
gormseed.New(db, gormseed.WithAutoOrder()).
Add("comments.json", &[]Comment{}).
Add("posts.json", &[]Post{}).
Add("users.json", &[]User{})
// Load order: users → posts → commentsExplicit edges with After:
gormseed.New(db).
Add("a.json", &[]User{}, gormseed.After("b.json")).
Add("b.json", &[]User{})
// Load order: b → aseeder := gormseed.New(db, gormseed.WithProfiles("demo")).
Add("users.json", &[]User{}). // always loads
Add("demo_data.json", &[]Order{}, gormseed.Profile("demo")) // only with "demo"seeder := gormseed.New(db, gormseed.WithTransaction(), gormseed.WithAutoOrder()).
Add("users.json", &[]User{}).
Add("posts.json", &[]Post{})
res, err := seeder.Run(ctx, gormseed.Dir("fixtures"))
// On error, all inserts are rolled back.seeder := gormseed.New(db, gormseed.WithBatchSize(100)).
Add("large_fixture.json", &[]User{})
// Inserts happen in batches of 100 rows.seeder := gormseed.New(db,
gormseed.WithBeforeSeedHook(func(ctx context.Context, name string, planned int) {
fmt.Printf("seeding %s (%d rows)...\n", name, planned)
}),
gormseed.WithAfterSeedHook(func(ctx context.Context, name string, planned int) {
fmt.Printf("done seeding %s\n", name)
}),
)seeder := gormseed.New(db, gormseed.WithLogger(log.Printf))seeder := gormseed.New(db).Add("users.json", &[]User{})
seeder.Run(ctx, fsys)
seeder.Clean(ctx) // drops and recreates the users table//go:embed fixtures/*.json
var fixtures embed.FS
sub, _ := fs.Sub(fixtures, "fixtures")
seeder.Run(ctx, sub)import "gopkg.in/yaml.v3"
seeder := gormseed.New(db, gormseed.WithDecoder(".yaml", yaml.Unmarshal))
seeder.Add("users.yaml", &[]User{})For context-aware decoders:
seeder := gormseed.New(db, gormseed.WithDecoderContext(".yaml",
func(ctx context.Context, data []byte, dest any) error {
return yaml.Unmarshal(data, dest)
},
))Seeder— main struct, created withNewConflict— conflict strategyResult— run result withSpecs []SpecResultSpecResult— per-spec outcomeLogger— interface for loggingSeedHook— callback type for hooks
New(db, opts...)— create a seederDir(path)— create anfs.FSfrom a directory pathSkip(),Update(cols...),UpdateAll(),Error()— conflict strategies
| Option | Type | Description |
|---|---|---|
WithAutoOrder() |
Option |
Enable FK-safe ordering |
WithProfiles(names...) |
Option |
Activate profiles |
WithDryRun() |
Option |
Plan only, no writes |
WithSkipMissing() |
Option |
Skip missing files |
WithContinueOnError() |
Option |
Continue on spec errors |
WithTransaction() |
Option |
Wrap in a DB transaction |
WithDefaultConflict(c) |
Option |
Set default conflict strategy |
WithDecoder(ext, fn) |
Option |
Register a decoder |
WithDecoderContext(ext, fn) |
Option |
Register a context-aware decoder |
WithBatchSize(n) |
Option |
Set batch insert size |
WithLogger(l) |
Option |
Set logger |
WithBeforeSeedHook(h) |
Option |
Register before-seed hook |
WithAfterSeedHook(h) |
Option |
Register after-seed hook |
OnConflict(c)— per-spec conflict strategyConflictTarget(cols...)— override conflict target columnsProfile(names...)— tag with profilesAfter(names...)— declare dependencies
(*Seeder) Add(name, dest, opts...)— register a fixture(*Seeder) Run(ctx, fsys)— execute seeding(*Seeder) Clean(ctx)— drop and recreate tables(*Result) Inserted()— total inserted rows
MIT — see LICENSE.