Documentation
¶
Overview ¶
Package autoseed reads an ORM model through an adapter's ModelSource and seeds a database from it: full referential integrity, deterministic output, no hand-written ordering or fixtures.
Index ¶
- Variables
- func ApplyCoverageOverrides(entity Entity, rows []map[string]any)
- func EnsureUnique(entity Entity, rows []map[string]any, seed *SeededSource) error
- func JunctionIndices(participants []JunctionParticipant, driverTarget string, ...) []int
- type DeferredReference
- type DependencyGraph
- type Entity
- type EntityGenerationPlan
- type Field
- type GenerationPlan
- type JunctionParticipant
- type ModelSource
- type Option
- type Options
- type Plan
- type Reference
- type SeededSource
- type SkipReason
- type TopologicalSortResult
Constants ¶
This section is empty.
Variables ¶
var ErrDuplicateEntity = errors.New("autoseed: duplicate entity name")
ErrDuplicateEntity is returned when two or more Entity values in a model share the same Name: a ModelSource bug, since the graph cannot tell them apart.
var ErrInvalidReference = errors.New("autoseed: reference names no fields")
ErrInvalidReference is returned when a Reference names no Fields: there is no column for it to mean anything.
var ErrInvalidScale = errors.New("autoseed: scale must not be negative")
ErrInvalidScale is returned when Options.Scale is negative: there is no meaningful row count to draw from it.
var ErrNilSeed = errors.New("autoseed: seed is nil")
ErrNilSeed is returned when a *SeededSource parameter is nil: every random draw in the pipeline must derive from one, so there is nothing safe to do with its absence.
var ErrUnknownReference = errors.New("autoseed: reference targets an unknown entity")
ErrUnknownReference is returned when an Entity's Reference names a Target that does not match any Entity.Name in the model.
var ErrUnsatisfiableCycle = errors.New("autoseed: unsatisfiable required foreign key cycle")
ErrUnsatisfiableCycle is returned when a group of entities form a foreign key cycle with no nullable reference to break it: no insertion order can satisfy every required foreign key at once.
var ErrUnsatisfiableUniqueness = errors.New("autoseed: could not generate a unique value")
ErrUnsatisfiableUniqueness is returned when a duplicate value for a unique field could not be fixed within the retry budget: the value space is too small for the requested row count.
var ErrUnsupportedField = errors.New("autoseed: no inference rule for field")
ErrUnsupportedField is returned when no inference rule can produce a value for a field: an adapter read a construct value generation does not understand yet, named rather than silently generating the wrong thing or the zero value.
Functions ¶
func ApplyCoverageOverrides ¶
ApplyCoverageOverrides mutates rows in place so entity's own Nullable, bool-kind and sized string fields each cycle through their declared boundary states across the row set PlanCoverage already sized for this — a field never touches more than one axis, Nullable taking priority over bool-kind for a field that happens to be both, so a later axis's write can never silently undo an earlier one's. A reference's own foreign key field is skipped entirely: its real value is assigned separately, during persistence, and overwrites whatever a generated row held for it regardless — touching it here would only risk an adapter treating a placeholder null as if it meant something, for a field that will be overwritten before it ever reaches the database. A primary key field is skipped the same way: it is either database-generated or a natural key GenerateRow already owns, never a field whose boundary states are this feature's business. A Unique bool-kind field only has the override applied to its first two rows — enough to prove both true and false occur — since a bool has only two possible values, and forcing the same alternating pattern past a third row would manufacture a duplicate EnsureUnique cannot repair (it only rewrites string-kind fields); rows beyond the second keep whatever GenerateRow produced for them. GenerateRow's own value for a field no axis here claims is left untouched.
func EnsureUnique ¶
func EnsureUnique(entity Entity, rows []map[string]any, seed *SeededSource) error
EnsureUnique rewrites duplicate values, in row order, for every single-column string field on entity marked Unique, and for every composite tuple in entity.UniqueConstraints with at least one independent string column to rewrite. A constraint that names a foreign key field is left alone here entirely: at this stage a row's reference fields hold only a placeholder value, not the real parent key persistence assigns later, so there is nothing meaningful to deduplicate yet — that shape's uniqueness is guaranteed by construction in the generation plan instead. A constraint with no string field to rewrite is likewise out of scope, the same way a single non-string Unique field already is. The first row to use a value or tuple keeps it; a later duplicate has its rewritable field rewritten with a random numeric suffix, retried up to 20 times. Two constraints that share a rewritable field are resolved together, not independently: rewriting one row's field to satisfy one constraint can change what that same row's tuple looks like under every other constraint containing that field, so every affected constraint is re-checked until a row settles. seed must be scoped to the entity, not a specific row, and must not be nil. It returns ErrUnsatisfiableUniqueness, naming the entity and field(s), if a value cannot be fixed within the retry budget, or ErrNilSeed for a nil seed.
func JunctionIndices ¶
func JunctionIndices(participants []JunctionParticipant, driverTarget string, driverRow, blockLocal int, rows func(target string) int) []int
JunctionIndices returns, for one row at position blockLocal within its driver row's block, the parent row index to pair with for each of participants, in the same order. rows reports how many rows already exist for a given entity name. driverRow is the driver's own row index for this block; a participant whose Target equals driverTarget is self-referencing and skips driverRow itself, since a row can never pair with itself.
The mapping decomposes blockLocal in mixed radix over each participant's own usable row count, in participants' own order: this is injective as long as blockLocal is less than the product of those counts, exactly the guarantee PlanGeneration's cap on ChildCounts already gives every block. Two rows in the same driver block therefore always draw a distinct combination across every participant; two rows in different blocks differ by driverRow alone.
Types ¶
type DeferredReference ¶
DeferredReference is a nullable reference that participates in a cycle: it must be inserted as null and patched to its real value once every entity in Order has been written.
type DependencyGraph ¶
type DependencyGraph struct {
// contains filtered or unexported fields
}
DependencyGraph is the directed graph of foreign key references between an ORM model's entities, built from a ModelSource's Entities.
func NewDependencyGraph ¶
func NewDependencyGraph(entities []Entity) (*DependencyGraph, error)
NewDependencyGraph builds a DependencyGraph from entities. It returns ErrDuplicateEntity if two entities share a Name, ErrUnknownReference if any Reference.Target does not match an Entity.Name in entities, and ErrInvalidReference if a Reference names no Fields.
func (*DependencyGraph) Resolve ¶
func (g *DependencyGraph) Resolve() (*TopologicalSortResult, error)
Resolve resolves every cycle in the graph and returns a stable topological order for the remaining, acyclic references. Ties in the order break on entity name, never on map iteration order, so the same graph always resolves to the same order. It returns ErrUnsatisfiableCycle if a cycle has no nullable reference to break it.
type Entity ¶
type Entity struct {
Name string
Fields []Field
References []Reference
UniqueConstraints [][]string
}
Entity describes one seedable type read from an ORM model: its name, the fields that hold values, and the foreign key references to other entities. UniqueConstraints holds one entry per composite unique index: two or more field names that, taken together as a tuple, must be unique across every row. A single-column unique constraint is expressed on the Field itself instead — an entry here always has two or more Fields.
type EntityGenerationPlan ¶
type EntityGenerationPlan struct {
Entity string
RowCount int
Driver string
DriverFields []string
ChildCounts []int
JunctionParticipants []JunctionParticipant
}
EntityGenerationPlan is the row count and cardinality decision for one entity: how many rows to generate and, for a dependent entity, how many of them belong to each row of its driving principal. Driver and DriverFields together identify the specific reference chosen as the driver — Fields disambiguates the case where two references target the same entity (a self-referencing many-to-many). JunctionParticipants is set the same way, only when entity's own primary key, or one of its UniqueConstraints, is exactly covered by the driver reference together with one or more other required references — a many-to-many join table, an explicit composite-key "attributed join" entity like an inventory or order-line row, or a ternary association with three or more participating references — where each driver row can pair with a given combination of JunctionParticipants rows at most once.
type Field ¶
type Field struct {
Name string
Type reflect.Type
Size int
Nullable bool
Unique bool
PrimaryKey bool
AutoIncrement bool
SoftDelete bool
}
Field describes one column-backed value on an Entity. Type is the Go type value generation must produce; Size is the column's maximum length for a string type, zero when the column has no declared limit. Nullable reports whether generation may leave this field's value out entirely and have persistence turn that into a real database NULL — not merely whether the underlying column's schema allows NULL. A column that allows NULL but whose Go representation has no way to express absence (a plain, non-pointer field with no database/sql Null* wrapper) is Nullable=false: generation would have nothing meaningful to leave out, only its own zero value.
type GenerationPlan ¶
type GenerationPlan struct {
Entities []EntityGenerationPlan
}
GenerationPlan is the row count and cardinality decision for every entity, in the same order Resolve produced.
func PlanCoverage ¶
func PlanCoverage(entities []Entity, order []string, deferred []DeferredReference) (*GenerationPlan, error)
PlanCoverage computes the smallest row count and cardinality decision per entity that exercises every axis SeedCoverage promises: a required, non-driver reference target never left at zero rows (the same backstop PlanGeneration itself relies on); a driver row hosting zero, one, and several ("many" — two, enough to prove the relationship isn't capped at one, without inflating row counts further) children for whatever depends on it; and, folded into whatever rows that already produces rather than adding rows of their own, a Nullable field, a bool-kind field, and a sized string field each cycling through their own boundary states across the entity's row set. It shares PlanGeneration's junction, shared-key and backstop rules unchanged through planWithStrategy — only how many rows a root gets, how many children a driver row gets, and the floor a dependent's own row count must reach, differ from the bulk, random-seed path: a dependent entity is itself floored at coverageRowCount too, through planWithStrategy's rowFloor hook, so an entity that is simultaneously a dependent (has a required reference of its own) and a driver target (or carries its own field axes) still gets enough rows for both roles, headroom under any shared-key or junction cap permitting. order and deferred come from DependencyGraph.Resolve().
func PlanGeneration ¶
func PlanGeneration(entities []Entity, order []string, deferred []DeferredReference, seed *SeededSource, options Options) (*GenerationPlan, error)
PlanGeneration computes a row count for every entity in order: a root entity — one with no required, non-deferred reference to another entity — gets options.Scale rows directly. A dependent entity picks a driving principal (its required reference whose Target sorts first, Fields breaking a tie between two references to the same target; only one side of a many-to-many join table ever becomes a driver, by the same rule) and draws a child count per driver row from an Exponential(mean) distribution, long-tailed and occasionally zero, the same shape the .NET sibling uses. seed is the root SeededSource; order and deferred come from DependencyGraph.Resolve(). It returns ErrNilSeed for a nil seed and ErrInvalidScale for a negative options.Scale.
type JunctionParticipant ¶
JunctionParticipant identifies one non-driver reference in a junction shape: a required reference that, together with the driver and zero or more sibling participants, exactly covers a composite key. Target and Fields work like Reference's own.
type ModelSource ¶
ModelSource is the contract every ORM adapter implements: reading an ORM's model and exposing it as entities, fields and references. Everything downstream of this package consumes only what Entities returns.
type Option ¶
type Option func(*Options)
Option configures Options.
func WithLocale ¶
WithLocale sets which locale's own rules claim a name, address and phone field before the generic, English-shaped fallback gets a turn — "pt_BR" today, more as demand shows up, the same on-demand bar every adapter and rule in this project ships under. An unrecognized locale, including the empty default, falls back to the generic rules — the same as not setting a locale at all, never an error, matching every other Option's own silent-normalize behavior (WithNilRate clamps rather than rejecting an out-of-range rate).
func WithNilRate ¶
WithNilRate sets the per-row probability that a Nullable field's value is left out entirely instead of generated — a field the adapter marks Nullable only when persistence can actually turn a missing value into a real database NULL. rate is clamped to [0,1].
type Options ¶
Options configures Seed (and, once built, the planned SeedCoverage). Build one with NewOptions and functional Option values; the zero value is not meaningful on its own.
func NewOptions ¶
NewOptions returns the default Options with every given Option applied. The default seed is 0; the default scale is 100; the default nil rate is 0 (every nullable field always gets a generated value); the default locale is "" (generic, English-shaped values).
type Plan ¶
type Plan struct {
Order []string
Deferred []DeferredReference
Skipped []SkipReason
}
Plan is the result of resolving a ModelSource without writing anything: the insertion order, the references deferred to a second pass, and the constructs an adapter left out on purpose.
func Explain ¶
func Explain(source ModelSource, skipped ...SkipReason) (*Plan, error)
Explain resolves source into a Plan without writing to any database. skipped carries adapter-specific constructs — a polymorphic association, say — that source's Entities left out of its references on purpose; Explain folds them into the report instead of discarding them.
type Reference ¶
Reference describes a foreign key on an Entity that points at another entity's primary key. Fields are the field names on the owning entity that carry the key, more than one for a composite foreign key, always in the same order as the referenced entity's own key fields. Target is the referenced Entity's Name.
type SeededSource ¶
type SeededSource struct {
// contains filtered or unexported fields
}
SeededSource derives deterministic randomness for one position in the generation tree — root, entity, row index, field — so that generating row 500 of an entity in isolation produces exactly the value it would produce inside a full batch. Every autoseed.Rand call anywhere in the pipeline must come from a SeededSource, never from math/rand directly.
func NewSeededSource ¶
func NewSeededSource(seed uint64) *SeededSource
NewSeededSource returns the root SeededSource for a seed.
func (*SeededSource) Entity ¶
func (s *SeededSource) Entity(name string) *SeededSource
Entity returns the SeededSource scoped to one entity.
func (*SeededSource) Field ¶
func (s *SeededSource) Field(name string) *SeededSource
Field returns the SeededSource scoped to one field within a row.
func (*SeededSource) Rand ¶
func (s *SeededSource) Rand() *rand.Rand
Rand returns the deterministic *rand.Rand for the current position.
func (*SeededSource) Row ¶
func (s *SeededSource) Row(index int) *SeededSource
Row returns the SeededSource scoped to one row index within an entity.
func (*SeededSource) Seed ¶
func (s *SeededSource) Seed() uint64
Seed returns the raw derived seed for the current position, for callers that need to build their own rand.Source-compatible generator (gofakeit, for one) rather than a *rand.Rand.
type SkipReason ¶
SkipReason names one construct an adapter chose not to translate into the ModelSource contract: an entity, the field involved, and why. Explain reports these by name instead of an adapter silently generating wrong data for them.
type TopologicalSortResult ¶
type TopologicalSortResult struct {
Order []string
Deferred []DeferredReference
}
TopologicalSortResult is the outcome of resolving a DependencyGraph: the insertion order and the references that could not be satisfied on first insert and must be patched in a second pass.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
autoseed
command
Command autoseed is a CLI wrapper around entseed.Explain: it reads an ent schema straight from source and prints the same insertion-order and skip report the Go API's Explain returns, with no database connection and no generated *ent.Client needed.
|
Command autoseed is a CLI wrapper around entseed.Explain: it reads an ent schema straight from source and prints the same insertion-order and skip report the Go API's Explain returns, with no database connection and no generated *ent.Client needed. |
|
Package entseed reads an ent schema via entc.LoadGraph and adapts it to the autoseed.ModelSource contract.
|
Package entseed reads an ent schema via entc.LoadGraph and adapts it to the autoseed.ModelSource contract. |
|
internal/entfixtures
Package entfixtures holds a small, real ent schema and its generated client, used only by entseed's own tests to exercise the adapter against real ent code (ent has no runtime schema introspection the way GORM does — a genuine client must be generated to test against).
|
Package entfixtures holds a small, real ent schema and its generated client, used only by entseed's own tests to exercise the adapter against real ent code (ent has no runtime schema introspection the way GORM does — a genuine client must be generated to test against). |
|
internal/entfixtures/schema
Package schema is a small, real ent schema used only to generate a client entseed's own tests exercise the adapter against.
|
Package schema is a small, real ent schema used only to generate a client entseed's own tests exercise the adapter against. |
|
Package gormseed reads a GORM model through schema.Parse and adapts it to the autoseed.ModelSource contract.
|
Package gormseed reads a GORM model through schema.Parse and adapts it to the autoseed.ModelSource contract. |
|
Package inference generates field values through gofakeit, seeded hierarchically from an autoseed.SeededSource, so autoseed's core stays testable without a value generator: the same separation the .NET sibling keeps between AutoSeed.Core and AutoSeed.Inference.
|
Package inference generates field values through gofakeit, seeded hierarchically from an autoseed.SeededSource, so autoseed's core stays testable without a value generator: the same separation the .NET sibling keeps between AutoSeed.Core and AutoSeed.Inference. |