On this page
Package chain is the pure kernel of pgdesign's migration algebra: revision manifests, the parent-linked edge graph, and three-way typed invertibility.
#internal/chain
#internal/chain
Package chain is the pure kernel of pgdesign's migration algebra: revision manifests, the parent-linked edge graph (the free category on edges), and three-way typed invertibility (roadmap kernel 1.4, laws L3 + L4 + L7).
It is deliberately PURE. It imports only the other kernel packages (internal/enc, internal/rev, internal/objstore, internal/model) plus the standard library, and pgregory.net/rapid in TEST files only. It never imports migrate, introspect, serve, or cmd — the abstract Op interface here is what keeps the dependency direction kernel <- adapter: roadmap 5.1's concrete op families implement Op, so the kernel reasons about migrations without knowing their concrete shapes.
Scope boundary (what 1.4 is NOT): there are no concrete op families, no on-disk chain files, no tracking schemas, and no database — those are phase 5. 1.4 is the types and the laws and their property tests.
Revision / manifest reconciliation (ONE concept, not two): Part I of the roadmap frames the revision two ways that must be reconciled:
- "revision = hash of canonical bytes" (kernel 1.5, internal/rev): rev concatenates every per-object canonical form, in sorted manifest-key order, behind a versioned+class-tagged preamble, and takes the SHA-256 of that whole-model byte string. This is the AUTHORITATIVE revision, the opaque class-tagged rev.Revision printed by validate/build. - "revision = id of a whole-model manifest — a sorted map of kind-qualified keys -> object-id" (Part I / this package): a Manifest.
These are the SAME content viewed two ways, and there is exactly ONE Revision type (rev.Revision) and ONE revision value per (model, class). The reconciliation is precise:
- Both derive from the identical per-object encoding, enc.EncodeObjects(s). - A Manifest maps each kind-qualified enc.Key to object-id = SHA-256 of that object's canonical bytes (objstore.ID) — exactly the bytes rev embeds. - So the Manifest is the MERKLE SUMMARY of the whole-model form: the whole-model form embeds the object bytes; the manifest embeds their hashes. Under SHA-256 collision resistance (L2, boundary item 14) they carry the same information, so revision-equal <=> manifest-equal.
This package therefore does NOT mint a second hash and call it "the revision". RevisionOf delegates to rev.Compute; the Manifest is the store-facing, Merkle-facing, diff-fast-path index over the same objects. The authoritative identity is rev.Compute (whole-model form); the roadmap's "revision = id of the manifest" phrasing is honored as "the manifest and the revision are two faithful summaries of one per-object byte set", and ConsistentRevisionAndManifest / the property tests pin that they cannot disagree.
Why the whole-model FORM (bytes) is authoritative rather than a hash of the key->id map: the whole-model form is SELF-CONTAINED — a revision can be verified from its own bytes with no store present (rev.Parse does exactly this). A hash of the key->id map would require the store to resolve ids before the content could be recovered, coupling identity verification to store availability. 1.5 shipped the self-contained form; 1.4 adopts it as the single revision and layers the manifest on top as the index.
#MechanicallyInvertible
const MechanicallyInvertible InvertibilityClass = iotaMechanicallyInvertible: the inverse is derivable from the op's own structure (e.g. ADD COLUMN <-> DROP COLUMN). These are the ONLY ops a manifest-diff down may represent (see MechanicalRange).
#DeclaredInverse
const DeclaredInverseDeclaredInverse: the op carries a recorded inverse that is NOT derivable from structure. This INCLUDES DML ops whose declared inverse is VACUOUS (data is not restored — today's reversibility semantics, made explicit).
#NonInvertible
const NonInvertibleNonInvertible: the op has no inverse.
#ErrNotFullyMechanical
var ErrNotFullyMechanical = errors.New("chain: op-list is not fully mechanically invertible; a manifest-diff down is not representable for it")ErrNotFullyMechanical is returned by NewMechanicalRange when the op-list contains any op that is not MechanicallyInvertible.
#Edge
type Edge structEdge is a parent-linked migration edge between two revisions. An edge carries a parent (from) revision, a target (to) revision, an ordered op-list, and a human-readable slug. Its IDENTITY is CONTENT-DERIVED (see ID): the edge is named by a hash of its content plus the slug, never by a counter. A DISPLAY SEQUENCE for listings is derived from graph topology at listing time and is never part of identity — so parallel edges, pure-DML endomorphisms (R -> R), and concurrent branch allocation can never collide on a name or race a counter.
The GENESIS edge has a NULL parent, modeled as a zero-value rev.Revision (Parent.IsZero() == true): it establishes an initial revision from nothing.
#OpSimulator
type OpSimulator interfaceOpSimulator is the plug-in point for edge-endpoint consistency: given a from-manifest and an op-list, it returns the manifest the ops produce. It is the second half of the store<->chain consistency check (the first is VerifyClosure). Concrete simulation of op families lands with roadmap 5.2; the kernel defines only this interface so the checker (VerifyEdgeEndpoint) exists now and 5.2 supplies the simulator.
#InvertibilityClass
type InvertibilityClass intInvertibilityClass is L4's three-way typing of a primitive op's reversibility. It is the type-level fact the whole rollback/squash story rests on: whether an op can be reversed mechanically, only via a recorded declared inverse (which may be vacuous), or not at all.
#Op
type Op interfaceOp is the ABSTRACT migration op the kernel reasons about. Roadmap 5.1's concrete families (create table, add column, RawSQL, DML, ...) implement it; the kernel stays free of migrate imports. An op names its kind, its target object (a kind-qualified manifest key), its L4 invertibility class, and references its structured payload BY CONTENT ID into the object store (no lossy inline mirrors — L1+L2).
#MechanicalRange
type MechanicalRange structMechanicalRange is a PROOF-CARRYING wrapper around an op-list whose EVERY op is MechanicallyInvertible. A manifest-diff down — reversing a range by diffing its endpoint manifests into structural inverse ops — is representable ONLY for such a range. The constructor REFUSES any list containing a declared-inverse (including vacuous DML) or non-invertible op, so the type system makes "a manifest-diff down over a data-bearing range" UNREPRESENTABLE by construction (L4). This forecloses the ruled-out "net manifest delta" trap: DROP populated column then ADD column has an empty net delta and destroys data — per-op typing, enforced here, is the correct criterion.
#Manifest
type Manifest map[enc.Key]stringManifest is a whole-model revision manifest: a map of kind-qualified manifest keys (enc.Key) to object-ids (objstore content ids). It is the Merkle summary of a model — one level of a two-level Merkle DAG (store objects below, manifests above). Manifest comparison is key-wise symmetric difference (see Diff); the diff fast path compares per-object ids before deep comparison (see ChangedKeys). enc.Key is a comparable struct, so it is a valid map key.
#Putter
type Putter interfacePutter is the write side of a content-addressed store (objstore.Store implements it). Kept minimal so the kernel depends on a capability, not a concrete store.
#ManifestDelta
type ManifestDelta structManifestDelta is the key-wise symmetric difference of two manifests, plus the keys present in both whose object-ids differ. It is a FLAT description of change at the object granularity — not a morphism (Deltas do not compose; composition happens on op-lists, L3). All key slices are sorted by Key.String() for determinism.
#Resolver
type Resolver interfaceResolver is the read side of a content-addressed store used by closure verification (objstore.Store implements Has). Kept minimal so the checker depends on a capability, not a concrete store.
#FindHeads
func FindHeads(edges []Edge) []rev.RevisionFindHeads returns the HEAD revisions of an edge set: revisions that are some edge's target and are NOT any (non-genesis) edge's parent. A linear chain has one head; a fork has two or more. The result is deterministic, ordered by the revision string, and de-duplicated. Genesis parents (null) are not revisions and never count as "having a child edge below them" for this purpose.
#FindGenesis
func FindGenesis(edges []Edge) []EdgeFindGenesis returns the genesis edges of an edge set (those with a null parent), in input order.
#ComposePath
func ComposePath(edges []Edge) ([]Op, error)ComposePath is composition in the free category (L3): the concatenation of a CONTIGUOUS path of edges' op-lists. The edges must chain end-to-end — each edge's target must equal the next edge's parent — or ComposePath returns an error naming the break. The identity morphism (an empty path) is VIRTUAL: it is the empty op-list, never a stored edge, so composing zero edges yields nil ops with no error.
Composition operates on OP-LISTS, not on Deltas (Deltas do not compose). A consolidation edge whose ops are exactly this concatenation is squash-sound by construction under the adopted concatenation form (roadmap 5.3); the substantive squash-commutation check lives there, not here.
#VerifyEdgeEndpoint
func VerifyEdgeEndpoint(e Edge, from, to Manifest, sim OpSimulator) errorVerifyEdgeEndpoint is the EDGE-ENDPOINT CONSISTENCY primitive: it asserts that simulating the edge's ops on its from-manifest reproduces its to-manifest. It requires an OpSimulator (roadmap 5.2); passing nil is a hard error, not a silent skip — a caller that cannot simulate must not claim the endpoint is consistent.
#InverseOfList
func InverseOfList(ops []Op) ([]Op, bool)InverseOfList returns the inverse of a composite op-list: the REVERSED composition of each component's inverse. It is defined WHEN AND ONLY WHEN every component has an inverse (mechanical or declared); if any component is NonInvertible it returns (nil, false).
This is L4's conservative UNDER-approximation, stated in the roadmap: a composite CAN be semantically invertible when a component is not (chained type changes whose endpoint diff yields a clean structural down), but the kernel never assumes it — the manifest-diff down for such a range is unrepresentable (see MechanicalRange), and elsewhere recorded downs compose. An iff-form of this rule is a ruled-out design (false converse).
#AllMechanicallyInvertible
func AllMechanicallyInvertible(ops []Op) boolAllMechanicallyInvertible reports whether every op in the list is MechanicallyInvertible. It is the exact precondition NewMechanicalRange enforces.
#NewMechanicalRange
func NewMechanicalRange(ops []Op) (MechanicalRange, error)NewMechanicalRange constructs a MechanicalRange, returning ErrNotFullyMechanical unless EVERY op is MechanicallyInvertible. A caller who cannot build one must compose the components' recorded declared inverses instead (InverseOfList), never a manifest-diff down.
#BuildManifest
func BuildManifest(s *model.Schema) (Manifest, error)BuildManifest computes the revision manifest of a resolved model: for every schema object, its kind-qualified key -> object-id (SHA-256 of the object's canonical bytes). It is a pure function of the CANONICALIZED model; the caller is responsible for having Built/Canonicalized s.
#BuildManifestInto
func BuildManifestInto(s *model.Schema, store Putter) (Manifest, error)BuildManifestInto encodes every object of s, PUTS each into store, and returns the resulting manifest. It is the bridge that makes a manifest's ids resolve: after this call VerifyClosure(m, store) passes. The put ids equal the manifest ids by construction (both are SHA-256 of the same canonical bytes).
#ChangedKeys
func ChangedKeys(base, target Manifest) []enc.KeyChangedKeys is the per-object-id DIFF FAST PATH primitive: the set of keys that differ between two manifests (added, removed, or content-changed), sorted. A deep differ can compare per-object ids first and DEEP-compare only these keys, skipping every object whose id is unchanged. When the result is empty the two models are byte-identical object-for-object, hence ≈_syn-equal, hence diff-empty (the forward conformance direction). Callers that already hold both manifests (roadmap 5.2's on-disk chain) use this directly; the pure differ consumes it via a whole-model equality short-circuit (see internal/diff).
#RevisionOf
func RevisionOf(s *model.Schema, class rev.ModelClass) (rev.Revision, error)RevisionOf returns the AUTHORITATIVE revision of a model under model class class. It delegates to rev.Compute (the whole-model form hash) — see the package doc's reconciliation note: the manifest and the revision are two summaries of the same per-object bytes, and there is exactly one Revision type and one revision value. This function exists so callers reach the single revision through the chain package without re-deriving it.
#VerifyClosure
func VerifyClosure(m Manifest, store Resolver) errorVerifyClosure is the Merkle CLOSURE VERIFICATION primitive: it asserts that every object-id referenced by manifest m resolves in the store (Part I's "shared consistency checker"). A dangling id — a manifest referencing content the store does not hold — is a hard error naming the offending key and id.
This is HALF of the store<->chain consistency check. The other half is edge-endpoint consistency (an edge's ops, simulated, map its from-manifest to its to-manifest); that half needs op SIMULATION, which lands with roadmap 5.2 and plugs in via VerifyEdgeEndpoint + OpSimulator. The interfaces are defined here so 5.2 wires the simulator in without reshaping the checker.
#Edge.IsGenesis
func (e Edge) IsGenesis() bool { return e.Parent.IsZero() }IsGenesis reports whether the edge has a null parent (establishes an initial revision from nothing).
#Edge.ID
func (e Edge) ID() stringID returns the content-derived identity of the edge: the lowercase SHA-256 hex of its canonical content projection. Equal content (same parent, target, slug, and op sequence by their observable facets) yields the same id; any difference — different ops, a different slug, a different endpoint — yields a different id. Parallel edges (same parent and target, different ops or slug) and endomorphisms (parent == target) therefore never collide unless they are genuinely the SAME edge.
#InvertibilityClass.String
func (c InvertibilityClass) String() string#MechanicalRange.Ops
func (r MechanicalRange) Ops() []OpOps returns a copy of the range's ops.
#MechanicalRange.ManifestDiffDown
func (r MechanicalRange) ManifestDiffDown() []OpManifestDiffDown yields the down op-list for a fully-mechanical range: the reversed composition of the components' mechanical inverses. Because every op is MechanicallyInvertible, each Inverse() succeeds, so the result is TOTAL — the boolean from InverseOfList can only be true here, which is precisely the invariant the MechanicalRange type guarantees. This method existing ONLY on MechanicalRange is what confines manifest-diff downs to fully-mechanical ranges.
#Manifest.Equal
func (m Manifest) Equal(other Manifest) boolEqual reports whether two manifests map the same keys to the same object-ids.
#ManifestDelta.Empty
func (d ManifestDelta) Empty() boolEmpty reports whether the delta has no added, removed, or changed keys.
#Manifest.Diff
func (m Manifest) Diff(other Manifest) ManifestDeltaDiff computes the manifest delta from base (m) to target (other): keys only in other are Added, keys only in m are Removed, keys in both with differing ids are Changed. This is the manifest-level comparison Part I calls "key-wise symmetric difference". Renames are delete+add at this level by construction (a renamed object has a different key), gated by roadmap 5.9's detection.