storm

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 27, 2026 License: MIT Imports: 14 Imported by: 0

README

storm

Every other Go ORM builds SQL at runtime. storm builds it at compile time — including the dynamic queries.

An embeddable ORM for PostgreSQL and Go. Generated, not reflected. Imported, not deployed. Zero CGO; the driver lives behind a four-method port.

Model-first: one plain Go struct per table — no tags, no DSL. Everything the type cannot say goes in a Schema method using field pointers, so the editor enforces names and refactors follow them. storm emits reviewable migrations and never applies DDL.

Status

v0.1.0 is tagged, and M0–M8 have passed. The read path, migrations, relations, writes, the typed escape hatch and the tooling gate are built, benchmarked and hardened; the first adopter migrated a whole bounded context (M6) and runs on the published module. The milestone log with every exit gate is docs/PLAN.md, and what would still stop a team adopting this is written down, with gates, in docs/PRODUCTION-READINESS.md.

Every claim below is a test or a benchmark in this repository. The quickstart is executable: examples/blog runs as a test in CI, so it cannot drift from the library the way prose does.

The sixty-second tour

// The model: a plain struct. *T = nullable, Xxx Yyy = foreign key,
// []T = has-many. Field pointers declare the rest.
type Author struct {
    storm.Model            // uuid id + created_at/updated_at
    Name     string
    Email    string
    Articles []Article
}

func (a *Author) Schema(t *storm.Table)           { t.Unique(&a.Email) }
func (a *Author) Plans(p *storm.Plans)            { p.Named("Feed").With(&a.Articles) }
func (a *Author) Projections(p *storm.Projections) { p.Named("Card", &a.Name, &a.Email) }
// Typed queries. A dynamic query has a bounded set of shapes; each compiles
// once, and a warm call allocates NOTHING to build its SQL.
rows, err := article.New().
    Where(article.AuthorID.Eq(id), article.PublishedAt.IsNotNull()).
    Order(article.PublishedAt.Desc()).
    All(ctx, ex, nil)

// The semi-join: authors who HAVE a published article. Child predicates are
// typed by the child's package; one EXISTS probe per row.
store.AuthorHavingArticles(author.New(), article.PublishedAt.IsNotNull())

// The named plan: authors WITH their articles — exactly two round trips
// whatever the row count, and reading an unloaded relation DOES NOT COMPILE.
feed, err := store.AuthorFeed().Limit(10).All(ctx, ex)

// Writes are masked: unset columns take their database defaults, and an
// UPDATE writes only what was assigned. A version column makes stale writers
// lose loudly. Graph writes flush in FK order, one batch, atomic.
n := author.Create(); n.SetName("Ada"); n.SetEmail("ada@example.com")
ada, err := n.Insert(ctx, ex)

// Anything PostgreSQL can run, typed, validated against the model at
// generate time — mismatches fail the build naming the column and the fix.
var Top = storm.SQL[TopRow](`WITH ranked AS (...) SELECT ... LIMIT $1`)
var Purge = storm.SQLExec(`DELETE FROM sessions WHERE expires_at < now()`)

The numbers

Measured, never quoted from memory — the methodology and every caveat live in bench/RESULTS.md. At 1,000 rows, allocations per query:

storm raw pgx sqlc Bun Ent GORM
6 5,012 5,022 13,899 23,016 23,934

Wall clock is round-trip-dominated for every ORM — the honest claims are allocations, GC pressure (storm 21 GCs vs pgx's 102 on the 2M-row workload), and the plans: Exists() is a LIMIT 1 probe, relation loads carry no useless ORDER BY, per-parent limits lower to LATERAL (measured 33× over row_number() at 100 parents), and projections make index-only scans possible.

Tooling

The tool is a library you give a five-line main, because the commands need your models and an installed binary cannot see them (EXAMPLE §2):

func main() { tool.Main(model.All(), nil) }   // cmd/storm/main.go
go run ./cmd/storm generate [dir]   # one package per table + the context package
                     diff <name>    # a reviewable migration; never applied by storm
                     verify         # drift: model vs database
                     verify -stale  # generated code vs model (CI, no database)
                     verify -pending# model vs migrations — "forgot to diff" fails CI
                     lint           # every named plan costed in round trips, budgeted
                     explain        # every statement planned; large seq scans flagged
                     import         # an existing database, written back as a model draft

Design documents

CONCEPT · ARCHITECTURE · DIALECTS · COMPARISON · PLAN — the ADRs, including the rejected-ideas list, are under docs/adr. Some API sketches in the design docs describe planned surface (joins with cross-table rows, aggregate projections); the example and the generated code are the as-built truth.

Boundaries

No applied DDL. No lazy loading — ever; unloaded relations do not compile. No runtime dialect branch. No reflection under runtime/. Every "no" is a year of maintenance not spent; the reasoning lives in the ADRs.

Documentation

Overview

Package storm is the public surface: the types you declare a model with, and the builder that turns those models into a schema.

Index

Constants

View Source
const (
	OpEq       = "="
	OpOverlaps = "&&"
	OpAdjacent = "-|-"
)

Exclusion operators.

View Source
const (
	BTree = "btree"
	GIN   = "gin"
	GiST  = "gist"
	Hash  = "hash"
	BRIN  = "brin"
)

Index methods.

View Source
const MaxTimeOfDay = runtime.MaxTimeOfDay

MaxTimeOfDay is 24:00:00, which PostgreSQL accepts as a `time`.

Variables

This section is empty.

Functions

func Build

func Build(models ...any) (*schema.Schema, error)

Build turns a set of model structs into a schema. Pass pointers to zero values: Build(&User{}, &Org{}, &Post{}).

Every declaration problem is collected and reported together, because failing on the first one would mean N build cycles to find N mistakes.

func DeclOf

func DeclOf(d RawDecl) (reflect.Type, string)

DeclOf reads a registered query's row type and SQL; the generate command uses it and nothing else should.

func RegisterScanner

func RegisterScanner[T any](fn func([][]byte, *T, *runtime.Slab) error)

RegisterScanner is called by generated code from an init(). One scanner per row type: two queries sharing a row type share its scanner, which is safe because the generator validated both against the same descriptor shape.

Types

type Action

type Action string

Action is a referential action for OnDelete/OnUpdate.

const (
	Restrict   Action = "RESTRICT"
	Cascade    Action = "CASCADE"
	SetNull    Action = "SET NULL"
	SetDefault Action = "SET DEFAULT"
	NoAction   Action = "NO ACTION"
)

type ColBuilder

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

ColBuilder configures one column.

func (*ColBuilder) Cidr

func (b *ColBuilder) Cidr() *ColBuilder

Cidr narrows a netip.Prefix column from inet to cidr — the database then rejects host bits, which is the entire difference between the two types.

func (*ColBuilder) Comment

func (b *ColBuilder) Comment(s string) *ColBuilder

func (*ColBuilder) Date

func (b *ColBuilder) Date() *ColBuilder

Date narrows a time.Time column to a calendar date. The Go type stays time.Time (there is no stdlib date), decoded as midnight UTC.

func (*ColBuilder) Default

func (b *ColBuilder) Default(e Expr) *ColBuilder

func (*ColBuilder) Generated

func (b *ColBuilder) Generated(e Expr) *ColBuilder

func (*ColBuilder) Immutable

func (b *ColBuilder) Immutable() *ColBuilder

func (*ColBuilder) Index

func (b *ColBuilder) Index() *ColBuilder

Index adds a single-column index.

func (*ColBuilder) Named

func (b *ColBuilder) Named(n string) *ColBuilder

func (*ColBuilder) NotNull

func (b *ColBuilder) NotNull() *ColBuilder

func (*ColBuilder) Nullable

func (b *ColBuilder) Nullable() *ColBuilder

func (*ColBuilder) Numeric

func (b *ColBuilder) Numeric(p, s int) *ColBuilder

func (*ColBuilder) OnDelete

func (b *ColBuilder) OnDelete(a Action) *ColBuilder

OnDelete and OnUpdate set the referential action of this column's foreign key.

func (*ColBuilder) OnUpdate

func (b *ColBuilder) OnUpdate(a Action) *ColBuilder

func (*ColBuilder) Raw

func (b *ColBuilder) Raw(sqlType string) *ColBuilder

Raw forces a database type storm does not model.

func (*ColBuilder) Size

func (b *ColBuilder) Size(n int) *ColBuilder

func (*ColBuilder) Unique

func (b *ColBuilder) Unique() *ColBuilder

Unique adds a single-column unique constraint. On a foreign key this is what turns one-to-many into one-to-one.

func (*ColBuilder) Version

func (b *ColBuilder) Version() *ColBuilder

type Decimal

type Decimal = runtime.Decimal

Decimal is an exact fixed-point number for a numeric column.

An alias, not a wrapper: the model declares storm.Decimal and generated code reads runtime.Decimal, and those must be the same type or every value would need converting at the boundary storm exists to remove.

float64 is not offered for numeric. It cannot represent 0.10, and an accounting system that rounds is a defect rather than a tolerance — so the choice is made once, here, instead of by whoever writes the model.

func ParseDecimal

func ParseDecimal(s string) (Decimal, error)

ParseDecimal reads a decimal from its text form.

type Enumer

type Enumer interface {
	EnumValues() []string
}

Enumer marks a named type as a Postgres enum. Constants are not discoverable through reflection, so the type has to list them.

type Status string
const (StatusActive Status = "active"; StatusBanned Status = "banned")
func (Status) EnumValues() []string { return []string{"active", "banned"} }

type ExcludeBuilder

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

ExcludeBuilder configures an exclusion constraint.

func (*ExcludeBuilder) Named

func (b *ExcludeBuilder) Named(n string) *ExcludeBuilder

func (*ExcludeBuilder) Using

func (b *ExcludeBuilder) Using(method string) *ExcludeBuilder

func (*ExcludeBuilder) Where

func (b *ExcludeBuilder) Where(e Expr) *ExcludeBuilder

type ExcludeSpec

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

ExcludeSpec is one `<column-or-expression> WITH <operator>` part.

func With

func With(field any, op string) ExcludeSpec

With pairs a field with an exclusion operator.

func WithExpr

func WithExpr(e Expr, op string) ExcludeSpec

WithExpr pairs an expression with an exclusion operator, for the range overlap that scalar columns cannot express:

t.Exclude(storm.With(&b.Room, storm.OpEq),
          storm.WithExpr("tstzrange(starts_at, ends_at)", storm.OpOverlaps))

type Expr

type Expr string

Expr is a raw SQL fragment. It is the one deliberate escape from the typed API: conspicuous, reported by `storm lint --expr`, and validated against the database at generate time.

func GenRandomUUID

func GenRandomUUID() Expr

GenRandomUUID renders gen_random_uuid(), built in since PostgreSQL 13. This is the default for an embedded storm.Model because it works everywhere the rest of storm does.

func Now

func Now() Expr

Now renders the SQL now() default.

func UUIDv7

func UUIDv7() Expr

UUIDv7 renders a uuidv7() default. Requires PostgreSQL 18+.

type IndexBuilder

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

IndexBuilder configures an index after Index(...).

func (*IndexBuilder) Named

func (b *IndexBuilder) Named(n string) *IndexBuilder

func (*IndexBuilder) Unique

func (b *IndexBuilder) Unique() *IndexBuilder

func (*IndexBuilder) Using

func (b *IndexBuilder) Using(method string) *IndexBuilder

func (*IndexBuilder) Where

func (b *IndexBuilder) Where(e Expr) *IndexBuilder

type IndexColumn

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

IndexColumn is a field reference with ordering or an expression applied.

func Asc

func Asc(field any) IndexColumn

Asc is the default; provided for symmetry.

func Desc

func Desc(field any) IndexColumn

Desc orders an index key descending.

func Lower

func Lower(field any) IndexColumn

Lower indexes lower(col) — the usual answer for case-insensitive uniqueness.

func NullsLast

func NullsLast(ic IndexColumn) IndexColumn

NullsLast puts NULLs at the end of an index key.

type Interval

type Interval = runtime.Interval

Interval is a PostgreSQL interval: months, days and microseconds kept separate, because a month has no fixed length and a day is not always 24 hours. An alias for the same reason Decimal is — the model's type and the generated code's type must be one type.

type Model

type Model struct {
	ID        UUID
	CreatedAt time.Time
	UpdatedAt time.Time
}

Model is the conventional embedded primary key and timestamps. Embedding it is optional — declare your own key if you want a natural one.

It carries its own Schema method, so embedding it is the whole declaration: the key gets a default and is immutable, and both timestamps default to now().

func (*Model) Schema

func (m *Model) Schema(t *Table)

type Nested

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

Nested names a relation on a table OTHER than the one declaring the plan — a post's comments, reached through a user's posts.

It exists because a field pointer needs an instance to point into, and the declaring model has no Post to hand. Into supplies one: the builder already allocated a zero value of every registered model, so the closure is called with that and the offset resolves against the right table.

func Into

func Into[T any](pick func(*T) any, nested ...Nested) Nested

Into names a relation on T, for use inside With.

p.Named("Feed").With(&u.Posts, storm.Into(func(p *Post) any { return &p.Comments }))

The type parameter is what says which table the field belongs to, so a pointer into the wrong model is a compile error rather than a mis-resolved offset.

type Null

type Null[T any] struct {
	V     T
	Valid bool
}

Null is the allocation-free nullable. A `*T` in a model becomes a Null[T] in the generated row type: a pointer would cost one allocation per non-nil field per row, and rows are the hot path.

func None

func None[T any]() Null[T]

func Some

func Some[T any](v T) Null[T]

Some and None construct a Null[T].

func (Null[T]) Get

func (n Null[T]) Get() (T, bool)

type OneOf2

type OneOf2[A, B any] struct {
	// contains filtered or unexported fields
}

OneOf2 is an exclusive arc over two variants.

type OneOf3

type OneOf3[A, B, C any] struct {
	// contains filtered or unexported fields
}

OneOf3 is an exclusive arc over three variants.

type OneOf4

type OneOf4[A, B, C, D any] struct {
	// contains filtered or unexported fields
}

OneOf4 is an exclusive arc over four variants.

type OneOf5

type OneOf5[A, B, C, D, E any] struct {
	// contains filtered or unexported fields
}

OneOf5 is an exclusive arc over five variants.

type OneOf6

type OneOf6[A, B, C, D, E, F any] struct {
	// contains filtered or unexported fields
}

OneOf6 is an exclusive arc over six variants.

type OneOf7

type OneOf7[A, B, C, D, E, F, G any] struct {
	// contains filtered or unexported fields
}

OneOf7 is an exclusive arc over seven variants.

type OneOf8

type OneOf8[A, B, C, D, E, F, G, H any] struct {
	// contains filtered or unexported fields
}

OneOf8 is an exclusive arc over eight variants.

type PlanBuilder

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

PlanBuilder collects the relations one plan loads.

func (*PlanBuilder) With

func (b *PlanBuilder) With(relPtr any, nested ...Nested) *PlanBuilder

With adds a relation to the plan, addressed by field pointer. Anything passed after it is loaded THROUGH it and costs one more round trip each.

type Planner

type Planner interface {
	Plans(*Plans)
}

Planner is implemented by models that declare fetch plans. Optional: a model with no Plans method gets the one-plan-per-relation tier, which is finite by construction and needs no declaration.

type Plans

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

Named fetch plans.

A plan says which relations are loaded together, and the generator emits a distinct type per plan whose fields are exactly those relations. Reading an unloaded relation is then a compile error rather than an empty slice.

You name the plans. That is the whole answer to the projection-type explosion: generating a type per `With(...)` combination is 2ⁿ per entity, and the fix is not a cleverer generator but a shorter list — the one the developer actually uses.

It also makes `plans.go` the single reviewable file listing every load pattern in a system, which is a thing no other Go ORM has, and which a linter can cost in round trips.

func (u *User) Plans(p *storm.Plans) {
    p.Named("Feed").With(&u.Posts).With(&u.Org)
}

Relations are named by FIELD POINTER, like everything else in the declaration API, so the editor enforces them and a rename follows.

func (*Plans) Named

func (p *Plans) Named(name string) *PlanBuilder

Named starts a plan. The generated type is the model name plus this name, so "Feed" on User becomes UserFeed.

type Projections

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

Named projections: read less than the whole row, by name.

func (u *User) Projections(p *storm.Projections) {
    p.Named("Contact", &u.Email, &u.Name)
}

rows, err := user.New().Where(...).AllContact(ctx, ex)   // []user.ContactRow

The full-row read is the safe default and the expensive one: every column travels, TOAST'd values are fetched whether or not anyone looks, and an index-only scan is impossible by construction. A named projection is the declared, reviewable subset — same predicates, same ordering, same keyset machinery, narrower tuple, its own generated row type and scanner.

Named rather than a Select(cols...) builder for R3's reason: a type per combination is 2ⁿ per entity, and the fix is a shorter list — the one the code actually uses.

func (*Projections) Named

func (p *Projections) Named(name string, fieldPtrs ...any)

Named declares one projection over the given column fields.

type Projector

type Projector interface {
	Projections(*Projections)
}

Projector is implemented by models that declare projections. Optional.

type RawDecl

type RawDecl interface {
	// contains filtered or unexported methods
}

RawDecl is implemented by every SQLQuery, so a bootstrap can register them as a plain []any the way it registers models.

type SQLQuery

type SQLQuery[T any] struct {
	// contains filtered or unexported fields
}

The typed escape hatch (M5).

Anything PostgreSQL can run is expressible here — CTEs, windows, lateral joins — years before the native IR grows each construct. What storm adds is the part hand-rolled SQL always loses: the RESULT is typed, the scanner is generated, and the statement was validated against the model at GENERATE time, so a query whose columns drifted from its row type fails the build naming the column, not the 3am page.

var TopEarners = storm.SQL[EarnerRow](`
    WITH ranked AS (...)
    SELECT ... WHERE tenant_id = $1 ... LIMIT $2`)

rows, err := TopEarners.Query(ctx, db, tid, 3)   // []EarnerRow

How the scanner arrives

`storm generate` PREPAREs the statement, matches the result descriptor against T's fields, and emits a scanner that registers itself by type in an init(). The first Query looks it up once and caches it in the value; the warm path is an atomic load. Running a query nothing generated for is an error naming the fix, not a reflective fallback — one reflection path becomes THE path.

func SQL

func SQL[T any](sql string) *SQLQuery[T]

SQL declares a raw query returning rows of T.

func (*SQLQuery[T]) One

func (q *SQLQuery[T]) One(ctx context.Context, ex runtime.Executor, args ...any) (T, bool, error)

One runs the statement and returns the first row, if any.

func (*SQLQuery[T]) Query

func (q *SQLQuery[T]) Query(ctx context.Context, ex runtime.Executor, args ...any) ([]T, error)

Query runs the statement and scans every row.

Args are variadic and checked against the statement's placeholder count before anything reaches the server; the ROW is where the typing lives, which is the half hand-rolled SQL cannot have.

type SQLStmt

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

SQLStmt is the no-rows half of the escape hatch: DELETEs, junction-table INSERTs, `SELECT maintenance_fn(...)` calls. It carries no row type — and the generator enforces that, failing generation if the statement's result descriptor has columns, so "I meant to read those rows" cannot compile into silently dropping them.

func SQLExec

func SQLExec(sql string) *SQLStmt

SQLExec declares a raw statement executed for its effect.

func (*SQLStmt) Exec

func (q *SQLStmt) Exec(ctx context.Context, ex runtime.Executor, args ...any) (int64, error)

Exec runs the statement and reports rows affected.

type Schemer

type Schemer interface {
	Schema(t *Table)
}

Schemer is implemented by models that need more than their Go types can say.

The receiver MUST be a pointer. With a value receiver Go copies the struct before the method runs, so &u.Email points into the copy and cannot be resolved back to a field — the builder rejects that at Build time rather than producing a silently wrong schema.

type Table

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

Table is the builder handed to a model's Schema method. Every reference to a field is a *field pointer* (&u.Email), so a rename is a compile error and a typo never compiles.

func (*Table) Check

func (t *Table) Check(e Expr) *Table

Check adds a CHECK constraint.

func (*Table) Col

func (t *Table) Col(fieldPtr any) *ColBuilder

Col addresses a column by field pointer and returns a builder for it.

t.Col(&u.Email).Unique().Size(320)

func (*Table) Comment

func (t *Table) Comment(s string) *Table

Comment sets a table comment.

func (*Table) Exclude

func (t *Table) Exclude(parts ...ExcludeSpec) *ExcludeBuilder

Exclude adds an exclusion constraint — the correct answer to booking and scheduling overlap, and reachable from no other Go ORM.

t.Exclude(storm.With(&b.Room, storm.OpEq), storm.With(&b.Period, storm.OpOverlaps))

func (*Table) Index

func (t *Table) Index(cols ...any) *IndexBuilder

Index adds a secondary index. Wrap a field in Desc(...) or Lower(...) to order or transform it.

func (*Table) Name

func (t *Table) Name(n string) *Table

Name overrides the table name inferred from the Go type.

func (*Table) PrimaryKey

func (t *Table) PrimaryKey(fields ...any) *Table

PrimaryKey overrides the inferred key. Pass several field pointers for a composite key.

func (*Table) Unique

func (t *Table) Unique(cols ...any) *Table

Unique adds a table-level uniqueness constraint.

Postgres UNIQUE constraints cannot contain expressions, so a Unique over one (Lower(&u.Email)) is emitted as a UNIQUE INDEX instead. Same guarantee, different object — and the alternative is DDL that does not parse.

type TimeOfDay

type TimeOfDay = runtime.TimeOfDay

TimeOfDay is a PostgreSQL `time` — microseconds since midnight, no date and no zone. See runtime.TimeOfDay for why it is not a time.Time.

func NewTimeOfDay

func NewTimeOfDay(hour, min, sec, micro int) (TimeOfDay, bool)

NewTimeOfDay builds a time of day from its parts, reporting false for parts out of range rather than normalising them — 25:00 is a mistake, not 01:00 tomorrow.

type UUID

type UUID [16]byte

UUID keeps the core dependency-free. Map it to your preferred package once with a codec rather than importing one here.

func (UUID) String

func (u UUID) String() string

Directories

Path Synopsis
bench
entbench/schema
Package schema is the Ent declaration for the shared bench table.
Package schema is the Ent declaration for the shared bench table.
cmd
genbench command
genspike command
Command genspike generates the two table packages the M3 plan-type spike sits on top of.
Command genspike generates the two table packages the M3 plan-type spike sits on top of.
storm command
Command storm is a STUB, and says so when you run it.
Command storm is a STUB, and says so when you run it.
Package codegen emits Go from the schema IR.
Package codegen emits Go from the schema IR.
compile
pgddl
Package pgddl lowers the schema IR to PostgreSQL DDL.
Package pgddl lowers the schema IR to PostgreSQL DDL.
pgsql
Package pgsql lowers query structure to PostgreSQL text.
Package pgsql lowers query structure to PostgreSQL text.
examples
blog/gen command
Command gen regenerates the example's store from its model.
Command gen regenerates the example's store from its model.
blog/model
Package model is the quickstart's schema: two tables, one relation, and the declared plans and projections this example reads with.
Package model is the quickstart's schema: two tables, one relation, and the declared plans and projections this example reads with.
internal
aliasrow
Package aliasrowx deliberately declares a name that differs from its directory (aliasrow).
Package aliasrowx deliberately declares a name that differs from its directory (aliasrow).
benchmodel
Package benchmodel is the M0 benchmark table, declared as a model so the generator can be measured against the hand-written spike on identical SQL.
Package benchmodel is the M0 benchmark table, declared as a model so the generator can be measured against the hand-written spike on identical SQL.
testmodel
Package testmodel is the fixture domain used by M1's round-trip tests.
Package testmodel is the fixture domain used by M1's round-trip tests.
Package migrate diffs two schemas and emits a reviewable migration.
Package migrate diffs two schemas and emits a reviewable migration.
Package runtime is what generated code calls at query time.
Package runtime is what generated code calls at query time.
pgxdrv
Package pgxdrv is the only package in storm that knows pgx exists.
Package pgxdrv is the only package in storm that knows pgx exists.
Package schema is storm's schema IR: the single representation every front end produces and every back end consumes.
Package schema is storm's schema IR: the single representation every front end produces and every back end consumes.
pg
Package pg reads a live PostgreSQL database into storm's schema IR.
Package pg reads a live PostgreSQL database into storm's schema IR.
Package tool is the developer-facing command set — render DDL, diff a migration, generate packages, and verify that model, generated code, migrations and the live database all still agree — as a LIBRARY.
Package tool is the developer-facing command set — render DDL, diff a migration, generate packages, and verify that model, generated code, migrations and the live database all still agree — as a LIBRARY.

Jump to

Keyboard shortcuts

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