gormseed

package module
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 7, 2026 License: MIT Imports: 13 Imported by: 0

README

gorm-seed

Rails-style db:seed for GORM: load JSON fixtures into your database with idempotent, conflict-aware inserts.

Go Reference CI Go Report Card codecov

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" }
]

Install

go get github.com/promptrails/gorm-seed

Requires Go 1.26+. The only runtime dependency is GORM itself.

Features

  • Idempotent by defaultON CONFLICT DO NOTHING; safe to run on every boot.
  • Conflict strategiesSkip (default), Update(cols…), UpdateAll(), or Error(), per spec or as a seeder-wide default; override the conflict target with ConflictTarget.
  • Foreign-key-safe orderingWithAutoOrder() 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 an embed.FS work the same way.
  • Profiles — tag specs (Profile("demo")) and activate them per run (WithProfiles("demo")); untagged specs always load.
  • Dry run, transactions, reportingWithDryRun(), WithTransaction(), and a per-spec Result.
  • Pluggable decoders — JSON built in; add YAML or others with WithDecoder, no extra dependency in the core.
  • Batch insertsWithBatchSize(n) for large fixture files.
  • Table cleanupClean(ctx) to drop and recreate tables.
  • HooksWithBeforeSeedHook / WithAfterSeedHook for instrumentation.
  • LoggingWithLogger(l) for progress output.

Conflict strategies

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 error
seeder.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()))

Ordering

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 → comments

Explicit edges with After:

gormseed.New(db).
    Add("a.json", &[]User{}, gormseed.After("b.json")).
    Add("b.json", &[]User{})
// Load order: b → a

Profiles

seeder := gormseed.New(db, gormseed.WithProfiles("demo")).
    Add("users.json", &[]User{}).                                // always loads
    Add("demo_data.json", &[]Order{}, gormseed.Profile("demo"))  // only with "demo"

Transactions

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.

Batch inserts

seeder := gormseed.New(db, gormseed.WithBatchSize(100)).
    Add("large_fixture.json", &[]User{})
// Inserts happen in batches of 100 rows.

Hooks

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)
    }),
)

Logging

seeder := gormseed.New(db, gormseed.WithLogger(log.Printf))

Clean (truncate tables)

seeder := gormseed.New(db).Add("users.json", &[]User{})
seeder.Run(ctx, fsys)
seeder.Clean(ctx) // drops and recreates the users table

Embedding fixtures

//go:embed fixtures/*.json
var fixtures embed.FS

sub, _ := fs.Sub(fixtures, "fixtures")
seeder.Run(ctx, sub)

Custom formats (YAML, …)

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)
    },
))

API Reference

Types
  • Seeder — main struct, created with New
  • Conflict — conflict strategy
  • Result — run result with Specs []SpecResult
  • SpecResult — per-spec outcome
  • Logger — interface for logging
  • SeedHook — callback type for hooks
Functions
  • New(db, opts...) — create a seeder
  • Dir(path) — create an fs.FS from a directory path
  • Skip(), Update(cols...), UpdateAll(), Error() — conflict strategies
Options
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
Spec Options
  • OnConflict(c) — per-spec conflict strategy
  • ConflictTarget(cols...) — override conflict target columns
  • Profile(names...) — tag with profiles
  • After(names...) — declare dependencies
Methods
  • (*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

License

MIT — see LICENSE.

Documentation

Overview

Package gormseed loads JSON (or custom-format) fixtures into a GORM database with idempotent, conflict-aware inserts — Rails-style db:seed for GORM.

Unlike test-fixture loaders that truncate and reload, gormseed is built for repeatable seeding of reference, demo, and initial data: by default it uses INSERT ... ON CONFLICT DO NOTHING, so running it twice is a no-op rather than a duplicate-key error.

Register fixtures, then run them against any fs.FS (a directory or an embed.FS):

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"))
// or: res, err := seeder.Run(ctx, embeddedFS)

Features:

  • Idempotent inserts with selectable conflict strategy (Skip, Update, UpdateAll, Error), per spec or as a seeder-wide default.
  • Foreign-key-safe ordering: WithAutoOrder loads parents before children by inspecting belongs-to and has-many relationships; After adds explicit edges.
  • Profiles (dev/demo/test), dry runs, transactional runs, and a per-spec Result report.
  • Pluggable decoders (JSON built in; add YAML or others without the core taking on the dependency).
  • Batch inserts for large fixture files via WithBatchSize.
  • Table cleanup via Clean, for resetting data between runs.
  • Before/after seed hooks for monitoring and instrumentation.
  • Optional logger for progress output.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Dir

func Dir(path string) fs.FS

Dir returns an fs.FS rooted at the given directory, for passing to Run.

Types

type Conflict

type Conflict struct {
	// contains filtered or unexported fields
}

Conflict describes what to do when a seeded row collides with an existing one on its conflict target (the model's primary key by default).

func Error

func Error() Conflict

Error performs a plain insert with no conflict handling, so a duplicate row surfaces as an error. Use it when fixtures are expected to apply to a fresh database.

func Skip

func Skip() Conflict

Skip keeps the existing row untouched on conflict (INSERT ... DO NOTHING). This is the default and makes seeding idempotent.

func Update

func Update(columns ...string) Conflict

Update overwrites the named columns of the existing row on conflict.

func UpdateAll

func UpdateAll() Conflict

UpdateAll overwrites every column except the conflict target on conflict.

type Logger added in v0.2.0

type Logger interface {
	Printf(format string, v ...any)
}

Logger is the interface used for logging within the seeder. Implementations must be safe for concurrent use. Set it with WithLogger.

type Option

type Option func(*Seeder)

Option configures a Seeder.

func WithAfterSeedHook added in v0.2.0

func WithAfterSeedHook(hook SeedHook) Option

WithAfterSeedHook registers a hook that is called after each spec is seeded (after insert). Multiple hooks are called in registration order.

func WithAutoOrder

func WithAutoOrder() Option

WithAutoOrder enables automatic, foreign-key-safe load ordering. The seeder inspects each model's belongs-to and has-many relationships and loads parents before children. Explicit After dependencies are always honored on top of this. A dependency cycle makes Run fail; break it with explicit ordering.

func WithBatchSize added in v0.2.0

func WithBatchSize(n int) Option

WithBatchSize sets the number of rows inserted in each batch when creating records. Use this when seeding large fixture files to avoid excessive memory usage or statement-size limits. A value of 0 (default) inserts all rows in a single Create call.

func WithBeforeSeedHook added in v0.2.0

func WithBeforeSeedHook(hook SeedHook) Option

WithBeforeSeedHook registers a hook that is called before each spec is seeded (after decoding, before insert). Multiple hooks are called in registration order.

func WithContinueOnError

func WithContinueOnError() Option

WithContinueOnError records a failing spec's error in its SpecResult and proceeds with the remaining specs instead of aborting the run.

func WithDecoder

func WithDecoder(ext string, fn func(data []byte, dest any) error) Option

WithDecoder registers a decoder for files with the given extension (including the leading dot, e.g. ".yaml"). The function unmarshals the file bytes into a pointer to a slice of models. JSON (".json") is registered by default; use this to add formats such as YAML without the core taking on the dependency.

func WithDecoderContext added in v0.2.0

func WithDecoderContext(ext string, fn func(ctx context.Context, data []byte, dest any) error) Option

WithDecoderContext registers a context-aware decoder for files with the given extension. This is like WithDecoder but the function also receives the context from Run, allowing cancellation and tracing.

func WithDefaultConflict

func WithDefaultConflict(c Conflict) Option

WithDefaultConflict sets the conflict strategy used by specs that do not specify their own. The built-in default is Skip (idempotent seeding).

func WithDryRun

func WithDryRun() Option

WithDryRun decodes and plans every spec but writes nothing. SpecResult.Planned reports how many rows would have been inserted.

func WithLogger added in v0.2.0

func WithLogger(l Logger) Option

WithLogger sets a logger on the seeder. The logger receives informational messages about seeding progress (spec start, rows inserted, etc.).

func WithProfiles

func WithProfiles(profiles ...string) Option

WithProfiles activates the named profiles. Specs tagged with Profile load only when at least one of their profiles is active; untagged specs always load. With no active profiles, tagged specs are skipped.

func WithSkipMissing

func WithSkipMissing() Option

WithSkipMissing treats a registered spec whose file is absent as a skip rather than an error. Useful for optional, directory-based seed sets.

func WithTransaction

func WithTransaction() Option

WithTransaction wraps the entire run in a single database transaction, so a failure rolls back every spec. Note some databases (e.g. MySQL) do not support transactional DDL; this only affects the data writes seeding does.

type Result

type Result struct {
	Specs []SpecResult
}

Result reports what a Run did, one entry per registered spec in load order.

func (*Result) Inserted

func (r *Result) Inserted() int64

Inserted is the total number of rows inserted across all specs.

type SeedHook added in v0.2.0

type SeedHook func(ctx context.Context, specName string, planned int)

SeedHook is called before or after a spec is seeded.

type Seeder

type Seeder struct {
	// contains filtered or unexported fields
}

Seeder loads JSON (or custom-format) fixtures into a GORM database with idempotent, conflict-aware inserts. Build one with New, register fixtures with Add, then call Run. A Seeder is not safe for concurrent Add calls, but Run may be called repeatedly (seeding is idempotent under the default Skip strategy).

func New

func New(db *gorm.DB, opts ...Option) *Seeder

New creates a Seeder for the given database with the given options.

func (*Seeder) Add

func (s *Seeder) Add(name string, dest any, opts ...SpecOption) *Seeder

Add registers a fixture: file name (read from the fs.FS passed to Run) and a pointer to a slice of models it decodes into, e.g. &[]User{}. Specs load in registration order unless WithAutoOrder or After reorders them. Add returns the Seeder for chaining.

func (*Seeder) Clean added in v0.2.0

func (s *Seeder) Clean(ctx context.Context) (*Result, error)

Clean truncates all tables associated with registered specs. Specs are processed in reverse load order so that child tables are emptied before parent tables, avoiding foreign-key violations.

func (*Seeder) Run

func (s *Seeder) Run(ctx context.Context, fsys fs.FS) (*Result, error)

Run loads every registered fixture from fsys and returns a per-spec Result. Reads use fs.FS so a directory (Dir) and an embed.FS work the same way.

type SpecOption

type SpecOption func(*spec)

SpecOption configures a single registered fixture.

func After

func After(names ...string) SpecOption

After declares that this spec must load after the named specs. It is honored regardless of WithAutoOrder.

func ConflictTarget

func ConflictTarget(columns ...string) SpecOption

ConflictTarget overrides the conflict-target columns for this spec. By default the model's primary key is used.

func OnConflict

func OnConflict(c Conflict) SpecOption

OnConflict sets the conflict strategy for this spec, overriding the seeder default.

func Profile

func Profile(profiles ...string) SpecOption

Profile tags this spec with one or more profiles. It then loads only when one of those profiles is active (see WithProfiles).

type SpecResult

type SpecResult struct {
	// Name is the fixture file name.
	Name string
	// Inserted is the number of rows the database reported as affected.
	Inserted int64
	// Planned is the number of rows decoded from the file. With a dry run it is
	// what would have been written; otherwise it equals the input row count.
	Planned int
	// Skipped is true when the spec was not loaded (filtered by profile, file
	// missing with WithSkipMissing, or empty file).
	Skipped bool
	// Reason explains a skip ("profile", "missing", "empty"), when Skipped.
	Reason string
	// Err is set when the spec failed and WithContinueOnError was enabled.
	Err error
}

SpecResult is the outcome of loading one spec.

Directories

Path Synopsis
examples
basic command
Command basic seeds an in-memory SQLite database from embedded JSON fixtures, demonstrating foreign-key-safe ordering and idempotent re-runs.
Command basic seeds an in-memory SQLite database from embedded JSON fixtures, demonstrating foreign-key-safe ordering and idempotent re-runs.
profiles command
Command profiles demonstrates profile-based seeding.
Command profiles demonstrates profile-based seeding.
transaction command
Command transaction demonstrates transactional seeding.
Command transaction demonstrates transactional seeding.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL