Documentation
¶
Overview ¶
Package restcompat diffs the REST contract two schemas generate, and classifies each delta as breaking, additive, or neutral for a deployed client. It is the engine behind `sqlb impact` (ADR-0039: a schema edit is an API edit, and the break is diffed).
It is a sibling of migrate.Diff, not a consumer of it. migrate.Diff reads the columns and types that produce DDL and ignores capabilities, because capabilities emit no SQL. This reads the capabilities — Filterable, the Op set, exposure — precisely because the sharpest API breaks produce no DDL at all: un-exposing a column, dropping an operation, or a rename that is a clean migration and a wire break at the same time. So the two functions run over the same pair of registries and read different projections of them.
Like migrate.Diff it is a pure function over two *schema.Registry values, with no database and no running server, which is what makes it golden-testable.
What is deliberately honest here ¶
A change that is compatible for a reader and breaking for a writer — a column going NOT NULL widens the create body's required set while leaving responses untouched — is reported as two separate breaks, one per side, never folded into one. A classifier that reported the reader side and forgot the writer side would be a guard that fires sometimes, which reads as coverage it does not have (ADR-0016). Where a type change cannot be classified confidently in both directions, it is reported LevelUnknown rather than guessed as neutral.
Index ¶
Constants ¶
const SnapshotVersion = 1
SnapshotVersion is the format version of a captured contract. It is stamped into every Snapshot so a future format change can be recognised rather than misread — the snapshot is a checked-in artefact, and ADR-0039 flags its format as the expensive thing to change once teams have committed one. This format is still experimental and may change before it is frozen.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type ActionPropSnap ¶ added in v0.5.0
type ActionPropSnap struct {
Name string `json:"name"`
Type string `json:"type"`
Enum []string `json:"enum,omitempty"`
Nullable bool `json:"nullable,omitempty"`
HasDefault bool `json:"has_default,omitempty"`
}
ActionPropSnap is one property of an action's request body.
type ActionSnap ¶ added in v0.5.0
type ActionSnap struct {
Name string `json:"name"`
Path string `json:"path"`
// Body is the request body's properties, in declaration order.
Body []ActionPropSnap `json:"body,omitempty"`
// Writes names the columns the envelope persists. No client couples to it,
// so a change here is neutral — but it widens or narrows what one route
// can mutate, which is exactly the blast-radius question this tool is for.
Writes []string `json:"writes,omitempty"`
}
ActionSnap is one declared verb's contract.
type Break ¶
type Break struct {
Level Level
Resource string // the collection path, e.g. "/posts"
Facet Facet
Field string // column or relation name; empty for resource- and ops-level
Summary string // one line, in the allow-list voice: what changed, for whom
}
Break is one classified delta between two generated contracts.
func Breaking ¶
Breaking returns only the breaks a strict gate would fail on — the breaking ones and the unknowns, which cannot be shown safe. This is what `--api-compat=error` would count.
func Diff ¶
Diff reports how the REST contract changes moving from old to new. The result is deterministic: sorted by resource, then by facet, then by field. An empty result means the contract is byte-for-byte compatible.
This is the convenience form for two registries in hand. `sqlb impact` diffs a registry against a checked-in Snapshot instead — see DiffSnapshots — because "backward compatible relative to what?" needs a committed answer, not the other side of a comparison that only exists at generation time.
func DiffSnapshots ¶
DiffSnapshots is Diff over two captured contracts. It is what the CLI runs: the old side is read from a file in the repository, the new side is captured from the current schema.
type Facet ¶
type Facet string
Facet names the part of the contract a break sits in. Facets are ordered so that a resource-level break sorts above the field-level breaks under it.
const ( FacetResource Facet = "resource" // the endpoint set as a whole FacetOps Facet = "ops" // which operations exist FacetResponse Facet = "response" // the fields a read returns FacetFilter Facet = "filter" // ?column=op.value parameters FacetSort Facet = "sort" // ?sort columns FacetExpand Facet = "expand" // ?expand relations FacetCreate Facet = "create-body" // the POST body FacetPatch Facet = "patch-body" // the PATCH body FacetAction Facet = "action" // a declared domain verb and its body )
type FieldSnap ¶
type FieldSnap struct {
Name string `json:"name"`
Rel string `json:"rel,omitempty"`
Type string `json:"type"`
Array bool `json:"array,omitempty"`
Size int `json:"size,omitempty"`
Enum []string `json:"enum,omitempty"`
Nullable bool `json:"nullable,omitempty"`
HasDefault bool `json:"has_default,omitempty"`
Hidden bool `json:"hidden,omitempty"`
ReadOnly bool `json:"read_only,omitempty"`
Immutable bool `json:"immutable,omitempty"`
Filterable bool `json:"filterable,omitempty"`
Sortable bool `json:"sortable,omitempty"`
Expandable bool `json:"expandable,omitempty"`
RenamedFrom string `json:"renamed_from,omitempty"`
}
FieldSnap is one column's contract-relevant shape. Storage-only properties — the primary-key flag, the index list, the constraint names — are deliberately absent: they do not change how a client couples to the field.
type Level ¶
type Level int
Level is how a contract delta lands on a client that already exists.
const ( // LevelNeutral: no client is affected. A response field going from nullable // to always-present is neutral for a reader that already handled the type. LevelNeutral Level = iota // LevelAdditive: a new capability. Nothing that a client sends or reads // today changes meaning; a client that ignores the addition is unaffected. LevelAdditive // LevelBreaking: an existing client can break — a request that worked now // fails, or a response field it relied on is gone or changed shape. LevelBreaking // LevelUnknown: the delta is real but its effect depends on how a specific // client generated its types (a widened integer overflows a narrow one), so // it is surfaced for review rather than claimed safe. Treat it as breaking // under a strict gate. LevelUnknown )
type ResourceSnap ¶
type ResourceSnap struct {
Path string `json:"path"`
Ops []string `json:"ops"` // create, read, update, delete, list
Fields []FieldSnap `json:"fields"`
// Actions are the declared domain verbs. A snapshot recorded before they
// existed has none, which reads correctly: every verb in the new schema is
// an addition.
Actions []ActionSnap `json:"actions,omitempty"`
}
ResourceSnap is one exposed table's contract.
type Snapshot ¶
type Snapshot struct {
Version int `json:"version"`
Resources []ResourceSnap `json:"resources"`
}
Snapshot is the serialisable REST contract of a schema: one entry per exposed resource, holding exactly the capabilities a client couples to and nothing about storage. It is what `sqlb impact -write` records and what a later run diffs against.