Skip to content

orm

A typed, fluent communications bridge over backend Media. Callers build intent; a Medium transports it. The same Schema declaration lands against DuckDB today, Postgres tomorrow, a Borg DataNode next week — the bridge is stateless, the backend’s capability is what changes. Built on core/go — every query returns a Result.

Terminal window
go get dappco.re/go/orm@latest
import "dappco.re/go/orm"

orm.Define collects field declarations into a Schema value. Field builders are chainable so constraints stay close to the column they describe:

type User struct {
ID int64
Name string
Email string
}
func (User) Schema() orm.Schema {
return orm.Define(func(b *orm.Builder) {
b.Name("users")
b.PK("id")
b.String("name").NotNull()
b.String("email").Unique()
})
}
type Post struct {
ID int64
UserID int64
Title string
}
func (Post) Schema() orm.Schema {
return orm.Define(func(b *orm.Builder) {
b.Name("posts")
b.PK("id")
b.Int64("user_id")
b.String("title")
})
}

Schemas are values — pass them around, serialise them to JSON via SchemaFromJSON, ship them across the polyglot boundary (per RFC §12 the same Schema() declaration ports across Go, PHP, and TS implementations producing identical JSON shape).

A Medium is what carries intent to a real backend. Today: in-memory (NewMemium), DuckDB (via go/store), more to follow. Mount once on a Core and every Bridge call routes through:

c := core.New()
mem := orm.NewMemium()
orm.Mount(c, "default", mem)

Bridge[T] is the typed query builder. Construct one from a Schema + Core, chain predicates, terminate with a fetch verb that returns core.Result:

bridge := orm.From[User](c)
r := bridge.
Where("email", "=", "a@b.com").
First()
if !r.OK { return r }
user := r.Value.(*User)
// Multi-row + ordering + limit
r := orm.From[Post](c).
Where("user_id", "=", user.ID).
OrderBy("id", "desc").
Limit(10).
Get()
posts := r.Value.([]*Post)
// Aggregates
r := orm.From[Post](c).Where("user_id", "=", user.ID).Count()
n := r.Value.(int)

The fluent surface mirrors Eloquent’s reading rhythm without dragging in Eloquent’s machinery. Every predicate, every order, every join is just intent in a Go value until the Medium turns it into a backend call.

Writes go through the same Bridge or the package-level helpers:

// Through the Bridge
bridge.Insert(&User{Name: "alice", Email: "a@b.com"})
bridge.Where("id", "=", 42).Update(map[string]any{"name": "renamed"})
bridge.Where("id", "=", 42).Delete(&User{})
// Package-level — multi-row insert
orm.Insert(c, &User{Name: "bob"}, &User{Name: "carol"})
orm.Delete(c, &User{ID: 7})

Every write returns core.Result — failures surface the bridge intent that didn’t transport plus the underlying Medium error, so the call site sees what was attempted.

Bridge.From(...) aliases a related Schema for cross-table predicates; Bridge.With(...) declares eager-load relations the Medium should pre-fetch:

users := orm.From[User](c).
From(orm.A{"posts": Post{}.Schema()}).
Where("posts.title", "like", "%golang%").
With("posts").
Get()

Compound (A AND (B OR C)) predicates with WhereGroup:

bridge.
Where("status", "=", "active").
WhereGroup(func(g *orm.Group) {
g.Where("role", "=", "admin").
OrWhere("role", "=", "owner")
}).
Get()
  • go/store — the persistence layer most orm Mediums route to
  • go/io — Medium transport contract orm extends
  • go/api — the polyglot boundary orm Schemas cross at runtime

github.com/dAppCore/orm — full source, RFC, and the IMPLEMENTATION_PLAN that sequences the Go v1 build.