README
¶
gormtables
gormtables is a code-generation tool that scans your GORM model packages,
resolves the foreign-key dependencies between models, and emits an ordered Go
slice for use with db.AutoMigrate.
go install github.com/hr3lxphr6j/gormtables@latest
Why?
gorm.AutoMigrate accepts a variadic list of model pointers and creates (or
alters) the corresponding tables. When your models have foreign-key
constraints the order matters: the referenced table must exist before the
referencing table can add its constraint.
Maintaining that order by hand is error-prone. gormtables derives the
correct order automatically by reading the gorm:"foreignKey:…" struct tags
and topologically sorting the models. The output is a plain Go source file
that is easy to review and diff in pull requests.
Dependency semantics
Ordering follows one rule: the table that carries the foreign-key column must be created after the table it points at. Both association directions are read, because either one can put the FK on either table.
A gorm:"foreignKey:<FK>" tag is classified by where <FK> lives:
<FK> is a field of |
Association | Which table gets the FK | Edge |
|---|---|---|---|
| the struct declaring the tag | BelongsTo | that same struct | it depends on the referenced type |
| the referenced struct | HasMany / HasOne | the referenced struct | the referenced type depends on the declaring one |
The HasMany/HasOne row is easy to get backwards. The FK column does live in the
other table — but that is exactly why it constrains ordering: GORM copies the
relation onto the referenced schema, so CREATE TABLE for the child emits the FK
inline and the parent has to exist by then.
Skipped, in both directions:
constraint:-— GORM builds no FK, so there is nothing to order.- Self-references (tree structures) — a FK pointing at the table being created is satisfied by that same statement.
many2many— GORM materialises the join table and orders it itself.
Cycles among two or more models are reported as an error naming the structs involved. Declaring one association from both sides is not a cycle: the same edge is simply discovered twice and collapsed, matching GORM, which emits a single FK for such a pair.
Given these declarations:
type User struct {
BaseModel
Comments []Comment `gorm:"foreignKey:AuthorID"` // HasMany → FK on comments
}
type Post struct {
BaseModel
UserID uint64
User *User `gorm:"foreignKey:UserID"` // BelongsTo → FK on posts
}
type Comment struct {
BaseModel
PostID uint64
AuthorID uint64
Post *Post `gorm:"foreignKey:PostID"` // BelongsTo → FK on comments
}
the tables holding a FK are posts (→ users) and comments (→ users, posts):
graph LR
posts -->|FK| users
comments -->|FK| users
comments -->|FK| posts
gormtables produces User → Post → Comment, so AutoMigrate creates users
first, then posts, then comments — each table arriving after everything its
own foreign keys reference.
Opt-in / opt-out rules
A struct is included if it satisfies at least one of:
| Rule | Example |
|---|---|
Anonymously embeds a base struct listed in -base |
BaseModel (default) or gorm.Model |
Carries the enable marker comment (see -enable-marker) |
// AutoMigrate:enable |
A struct is excluded (regardless of the above) if it carries the disable
marker comment (see -disable-marker):
// AutoMigrate:disable
type Draft struct {
BaseModel
Body string
}
Usage
go:generate
//go:generate gormtables -models ./internal/model -out ./internal/db/tables.go
Then run:
go generate ./internal/db/...
Command line
gormtables \
-models ./internal/model \
-out ./internal/db/tables.go \
-pkg database \
-var autoMigrateTables
Generated output example
Given models:
// internal/model/user.go
type User struct {
BaseModel
Name string
}
// internal/model/post.go
type Post struct {
BaseModel
UserID uint64
User *User `gorm:"foreignKey:UserID"`
Title string
}
gormtables generates:
// Code generated by gormtables; DO NOT EDIT.
package database
import (
model "example.com/app/internal/model"
)
// autoMigrateTables is the ordered list of GORM models for AutoMigrate.
// Referenced tables appear earlier in the slice, so that by the time a table
// with a foreign key is created the table it points at already exists.
var autoMigrateTables = []any{
&model.User{},
&model.Post{},
}
Plug it in:
if err := db.AutoMigrate(autoMigrateTables...); err != nil {
log.Fatal(err)
}
Flag reference
| Flag | Default | Description |
|---|---|---|
-models |
(required) | Comma-separated list of model directories to scan |
-out |
(required) | Output file path |
-pkg |
database |
Package name written in the generated file |
-var |
autoMigrateTables |
Name of the generated []any variable |
-base |
BaseModel,gorm.Model |
Comma-separated base-embed names that qualify a struct |
-enable-marker |
AutoMigrate:enable |
Comment text that force-includes a struct |
-disable-marker |
AutoMigrate:disable |
Comment text that force-excludes a struct |
-tag |
gorm |
Struct-tag key used for foreign-key inspection |
Tests
go test ./... # generator: scanning, sorting, output
Ordering also has to hold against a real database, which no amount of unit
testing can establish. integration/ migrates fixtures covering
the common association shapes against MySQL and PostgreSQL:
docker compose -f integration/docker-compose.yml up -d --wait
go test -C integration ./...
It is a separate module so that installing the generator stays free of GORM and
database drivers. See integration/README.md for the
fixture groups and how to add one.
License
MIT — see LICENSE.