schema

package
v0.1.108 Latest Latest
Warning

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

Go to latest
Published: Oct 12, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package schema parses schema.lidza, the single source of truth for an application's data shapes, and generates Go structs with validation, the Postgres DDL and migrations, Rust serde structs and TypeScript types from it.

The file format:

enum Status { draft published }

model Post @table("posts") {
  id        uuid     @id @default(uuid())
  title     string   @min(1) @max(200)
  body      text?
  status    Status   @default(draft)
  authorId  uuid     @ref(User)
  topicId   int?     @ref(Topic, setnull)
  tags      string[]
  price     decimal(12, 2)? @min(0)
  createdAt time     @default(now())
  @@index(status, createdAt)
}

type CreatePost {
  title string @min(1) @max(200)
  body  text?
}

A model becomes a table and a struct; a type becomes a struct only. "?" marks an optional (nullable) field, "[]" an array. Scalar types: string, text, int, bigint, float, decimal(p, s), bool, time, date, uuid, json, bytes. Field attributes: @id, @unique, @index, @default(v) (uuid(), now(), autoincrement() on an int or bigint, or a literal), @ref(Model), @min(n), @max(n), @email, @url, @pattern("re"). Block attributes: @@index(a, b), @@unique(a, b); model attributes @table("name"), @public and @shared. A @ref field has the type of the referenced model's id: uuid, int, bigint or string.

A model is owned when a field ties each row to a user (Schema.Owner): `lidza gen resource` scopes its queries to the signed-in user and `lidza check` (L018) flags a query that does not. @public marks a model whose rows anyone may read and write: not owned, and its generated routes are not behind sign-in. @shared marks one whose rows every signed-in user shares: routes behind sign-in, queries not scoped, even with an owner-like field (a team's projects with a createdBy user).

Index

Constants

View Source
const (
	MigrationsDir = "db/migrations"
	LockFile      = "db/schema.lock.json"
)

Migration files, relative to the project root. The lock file is the schema as of the last generated migration and is committed; the next `lidza gen` diffs the current schema against it.

View Source
const DecimalPattern = `^[+-]?[0-9]+(\.[0-9]+)?$`

DecimalPattern is the JSON Schema pattern of a decimal field: plain notation, an optional sign and fraction.

View Source
const FileName = "schema.lidza"

FileName is the schema source, at the project root.

View Source
const GoFile = "schema/schema.go"

GoFile is the generated Go source, relative to the project root.

View Source
const GoPackage = "schema"

GoPackage is the package name of the generated Go file.

View Source
const LocalTimePattern = `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?$`

LocalTimePattern is the JSON Schema pattern of a localtime field: what an HTML datetime-local input sends, with optional seconds.

View Source
const NoTransaction = "-- lidza:no-transaction"

NoTransaction is the first line of a migration the db pack runs statement by statement outside a transaction (db.NoTransaction).

View Source
const RustFile = "src/schema.rs"

RustFile is the generated Rust module, relative to the crate directory. The crate needs `serde = { version = "1", features = ["derive"] }` and `mod schema;` in lib.rs.

View Source
const SQLFile = "db/schema.sql"

SQLFile is the full DDL of the current schema, relative to the project root. It is regenerated on every `lidza gen`; sqlc reads it.

View Source
const StampLayout = "20060102150405"

StampLayout names a migration by the UTC time it was written: 20261006140512_create_post. Two branches that each add one no longer pick the same number, and the existing numbered migrations (0001_...) sort before every timestamped one.

Variables

View Source
var Clock = time.Now

Clock is the time new migrations are named by; tests set it.

View Source
var Initialisms = []string{"id", "url", "api", "http", "json", "uuid", "sql", "ip", "html"}

Initialisms are the name parts Go spells in capitals: authorId is AuthorID, url is URL. Their plurals keep a lowercase s: tagIds is TagIDs, imageUrls is ImageURLs. The schema package and sqlc's db/queries/gen follow the same rule: `lidza gen` writes this list and the plurals into sqlc.yaml (SQLCNames). Model and enum names are the ones schema.lidza gives; sqlc derives its struct and enum names from the table and the Postgres type (SQLCName).

View Source
var Scalars = map[string]bool{
	"string": true, "text": true, "int": true, "bigint": true, "float": true,
	"decimal": true, "bool": true, "time": true, "date": true, "uuid": true, "json": true, "bytes": true,

	"localtime": true,
}

Scalars are the built-in types.

View Source
var UserModels = []string{"User", "AuthUser"}

UserModels are the model names Owner treats as the app's users: an app's own User and the auth pack's AuthUser (auth.Mount's accounts).

View Source
var WorkspaceModels = []string{"Workspace", "AuthWorkspace"}

WorkspaceModels are the model names Workspace treats as the app's workspaces: the auth pack's AuthWorkspace (auth.Teams) and an app's own Workspace.

Functions

func GenerateGo

func GenerateGo(s *Schema) string

GenerateGo renders the Go package: enums as string types with constants, models and types as structs with json and db tags, and a Validate method per struct that applies the field rules.

func GenerateRust

func GenerateRust(s *Schema) string

GenerateRust renders serde structs and enums. Time, date, uuid and decimal fields are strings (a decimal as "12.50", exact), so the crate needs no chrono, uuid or decimal dependency; packs that want typed values convert at the boundary. Bytes fields are Vec<u8> carried as base64 (the JSON form Go uses), through the b64 helpers the file defines when it needs them (crate dependency: base64).

func GenerateSQL

func GenerateSQL(s *Schema) string

GenerateSQL renders the Postgres DDL for every enum and model.

func GenerateTS

func GenerateTS(s *Schema) string

GenerateTS renders the TypeScript types: enums as string literal unions with a values array, models and types as interfaces. Optional fields are `T | null` because the Go side encodes a nil pointer as null. A decimal is a string ("12.50"), exact as the server sent it.

func GoName

func GoName(name string) string

GoName is the exported Go name of a schema name, camelCase or snake_case: authorId -> AuthorID, image_urls -> ImageURLs.

func Gofmt

func Gofmt(src string) string

Gofmt formats generated Go source. On a syntax error the source is returned as is, so the compiler reports it.

func JSONSchema

func JSONSchema(s *Schema) map[string]any

JSONSchema renders the JSON Schema (2020-12) definitions for every enum, model and type, keyed by name, for OpenAPI's components.schemas.

func MergeLocks

func MergeLocks(base, ours, theirs []byte) (merged []byte, conflicts []string, err error)

MergeLocks merges two branches' lock files with their common ancestor, model by model and enum by enum: an entry one branch changed (or added, or removed) and the other left as it was takes the change; the same change on both is taken once. Line numbers are ignored: an edit higher up in schema.lidza moves them in every entry below it. Conflicts name the entries both branches changed differently; the result then keeps ours for them, and the developer merges schema.lidza and runs lidza gen.

func Migrations

func Migrations(root string) []string

Migrations lists the migration names in root, in order.

func NextSeq

func NextSeq(dir string) int

NextSeq returns the next migration number from the files in dir: the numbering before migrations were named by time.

func NextStamp

func NextStamp(dir string, now time.Time) string

NextStamp is the name prefix of a new migration in dir: now in UTC, moved past the newest migration there (a clock behind another machine's must not run a migration before one it builds on) and past any name taken.

func SQLCName

func SQLCName(name string) string

SQLCName is the Go name sqlc gives a Postgres name (a column, a table in the singular, an enum type, or an enum type and value joined by _) once sqlc.yaml carries SQLCNames: api_key is APIKey, tag_ids is TagIDs, status_draft is StatusDraft.

func SQLCNames

func SQLCNames(s *Schema) (initialisms []string, rename map[string]string)

SQLCNames returns what sqlc.yaml needs so that db/queries/gen follows the naming of the schema package: the initialisms, and a rename for each name sqlc cannot spell from them alone (a plural: tag_ids is TagIDs). A column's name is then its field's name in schema/.

func SQLCRenames

func SQLCRenames(names []string) map[string]string

SQLCRenames returns, for snake_case names sqlc turns into Go names outside the schema (a query's sqlc.arg('ids') or @ids parameter), the ones its initialisms alone spell differently from SQLCName.

func SQLCRowType

func SQLCRowType(table string) string

SQLCRowType is the Go type sqlc generates for a table's row: a model with @table("store_products") reads rows of queries.StoreProduct.

func SaveLock

func SaveLock(root string, s *Schema) error

SaveLock writes the schema as the lock file. Types are omitted: they have no tables.

Types

type Enum

type Enum struct {
	Name   string
	Values []string
	Line   int
}

Enum is a named set of string values.

type Field

type Field struct {
	Name string
	// Type is a scalar name, an enum name or (for types) another type name.
	Type     string
	Optional bool
	Array    bool
	ID       bool
	Unique   bool
	Index    bool
	// Default is the literal as written: 42, true, "x", draft, uuid(),
	// now(), autoincrement().
	Default string
	// Ref is the model the field references (the field has the type of
	// that model's id); OnDelete is what happens
	// to the row when the referenced one is deleted: "cascade" (delete
	// it), "setnull" (clear the field; it must be optional) or "" (the
	// delete fails while the row exists).
	Ref      string
	OnDelete string
	Min      *float64
	Max      *float64
	// MinText and MaxText are @min and @max as written, for a decimal
	// field, whose rules compare exactly.
	MinText string `json:",omitempty"`
	MaxText string `json:",omitempty"`
	// Precision and Scale are the arguments of decimal(p, s).
	Precision int `json:",omitempty"`
	Scale     int `json:",omitempty"`
	Email     bool
	// Search is @search: the field is part of the model's full-text
	// search, with SearchWeight (A to D; by position when empty).
	Search       bool   `json:",omitempty"`
	SearchWeight string `json:",omitempty"`
	URL          bool
	// Timezone requires an IANA zone name ("Europe/Paris").
	Timezone bool
	Pattern  string
	Line     int
}

Field is one line of a block.

func (*Field) Autoincrement

func (f *Field) Autoincrement() bool

Autoincrement reports whether the field is an identity column, @default(autoincrement()): the database numbers the rows and inserts leave it out.

func (*Field) Column

func (f *Field) Column() string

Column is the field's column name in Postgres, unquoted: ownerId is owner_id.

func (*Field) TypeText

func (f *Field) TypeText() string

TypeText is the field's type as written, without ? and []: "decimal(12, 2)" for a decimal, else Type.

type Index

type Index struct {
	Fields []string
	Unique bool
}

Index is a block-level @@index or @@unique.

type Migration

type Migration struct {
	// Name is "0003_add_post"; the files are Name+".up.sql" and ".down.sql".
	Name string
	Up   []string
	Down []string
	// After holds what would lock a table that already has rows if it
	// ran in Up: indexes built CONCURRENTLY, constraints validated. It
	// becomes a second migration, marked NoTransaction, named after Up's
	// with the next stamp; AfterDown reverts it.
	After     []string
	AfterDown []string
}

Migration is one generated pair of up and down scripts, and the statements that must run outside a transaction after it.

func Diff

func Diff(prev, cur *Schema, seq int) *Migration

Diff computes the migration from prev (nil for a fresh project) to cur. It returns nil when nothing changed. Handled: enums created, dropped or given new values; tables created or dropped; columns added, dropped, or changed in type, nullability or default; indexes created or dropped. A dropped or narrowed column loses data; the statement is generated and marked with a comment so it is reviewed before it runs.

func (*Migration) AfterFiles added in v0.1.101

func (m *Migration) AfterFiles(name string) (up, down string)

AfterFiles renders the After scripts, named name (the migration that follows this one): NoTransaction first, then a statement per line.

func (*Migration) Files

func (m *Migration) Files() (up, down string)

Files renders the migration's two scripts.

type Model

type Model struct {
	Name string
	// Table is the Postgres table name; empty for types.
	Table   string
	Fields  []*Field
	Indexes []Index
	Line    int
	// Persisted is true for "model", false for "type".
	Persisted bool
	// Public is the model attribute @public: the rows are not owned and
	// the generated routes are open to visitors.
	Public bool `json:",omitempty"`
	// Shared is the model attribute @shared: the rows are not owned and
	// the generated routes are behind sign-in.
	Shared bool `json:",omitempty"`
	// SearchConfig is @@search("english"): the text search configuration
	// of the model's @search fields; "simple" when empty.
	SearchConfig string `json:",omitempty"`
}

Model is a model or type block.

func (*Model) IDField

func (m *Model) IDField() *Field

IDField returns the model's @id field, or nil.

func (*Model) SearchDocument

func (m *Model) SearchDocument() string

SearchDocument is the plain text the highlights come from: the @search fields joined.

func (*Model) SearchFields

func (m *Model) SearchFields() []*Field

SearchFields are the model's @search fields, in order.

func (*Model) SearchLanguage

func (m *Model) SearchLanguage() string

SearchLanguage is the model's text search configuration: @@search's, else "simple" (no stemming: right for names, codes and mixed languages).

func (*Model) SearchVector

func (m *Model) SearchVector() string

SearchVector is the expression of the model's search document: each @search field weighted (its @search(A) weight, else A, B, C, then D by position) and joined. The GIN index and every search query use this exact text, so the planner matches them; "" without @search fields.

type Rename

type Rename struct {
	// Where is "schema.Post" or "queries (column post.tag_ids)".
	Where    string
	Old, New string
}

Rename is a Go name that changed with a Līdza release.

func QueryRenames

func QueryRenames(s *Schema) []Rename

QueryRenames lists the db/queries/gen names that change when sqlc.yaml first gets the names SQLCNames gives: before, sqlc knew only the initialism id. Fields of the query rows and parameters change with the columns they come from.

func (Rename) String

func (r Rename) String() string

type Result

type Result struct {
	// Files are the paths written, relative to the project root.
	Files []string
	// Migration is the new migration's name, empty when nothing changed.
	Migration string
	// Renamed are the Go names in the schema package that changed from
	// the previous schema/schema.go with this release's naming (plurals
	// of initialisms as IDs, URLs): the app's code must follow.
	Renamed []Rename
}

Result says what Generate wrote.

func Generate

func Generate(root string, s *Schema, cargoDir, tsFile string) (*Result, error)

Generate runs every generator for the project in root: the Go package, the SQL schema, a migration when the models changed since the lock, the Rust module when the project has a crate at cargoDir (empty to skip), and the TypeScript types at tsFile (relative to root, empty to skip).

func (*Result) Describe

func (r *Result) Describe() string

Describe returns a one-line summary for logs.

type Schema

type Schema struct {
	Enums  []*Enum
	Models []*Model // persisted: table and struct
	Types  []*Model // API shapes: struct only
}

Schema is a parsed schema.lidza.

func Load

func Load(dir string) (*Schema, error)

Load parses dir/schema.lidza. It returns nil, nil when the file does not exist: a project without a schema is valid.

func LoadLock

func LoadLock(root string) (*Schema, error)

LoadLock reads the lock file; nil when there is none.

func Parse

func Parse(src string) (*Schema, error)

Parse parses schema source text.

func (*Schema) Enum

func (s *Schema) Enum(name string) *Enum

Enum returns the enum called name, or nil.

func (*Schema) Model

func (s *Schema) Model(name string) *Model

Model returns the model or type called name, or nil.

func (*Schema) Owner

func (s *Schema) Owner(m *Model) *Field

Owner returns the field that ties a row of m to one user, or nil when m is not owned. The first match wins:

  1. a field named ownerId;
  2. the first field with @ref(User) or @ref(AuthUser), such as userId uuid @ref(User) or authorId uuid @ref(User).

A userId without the @ref is not the owner: in a join table (memberships: userId, projectId) the rows belong to the project. The id and arrays are never the owner. A model marked @public or @shared, and a type, have none. The owner's value is the signed-in user's id, auth.CurrentUser(ctx).ID.

func (*Schema) Workspace

func (s *Schema) Workspace(m *Model) *Field

Workspace returns the field that ties a row of m to a workspace (auth.Teams), or nil: a field named workspaceId, else the first with @ref(Workspace) or @ref(AuthWorkspace). Its value is the request's workspace, auth.WorkspaceID(ctx), which auth.RequireWorkspace checks the signed-in user belongs to. It takes precedence over Owner: the rows belong to the workspace, not to whoever wrote them. A model marked @public or @shared, and a type, have none.

Jump to

Keyboard shortcuts

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