Documentation
¶
Overview ¶
Package closedset keeps a database CHECK constraint's value list equal to the Go set it mirrors.
Some vocabularies are enforced twice: in Go, where a value that cannot be spelled cannot be granted (accesstoken.AllScopes), and in postgres, where a CHECK decides what a row may hold at all. The two must not drift. When Go gains a value the CHECK lacks, every write of it fails the constraint with an opaque error; when the CHECK admits one Go lacks, a row becomes unreadable. Every project that stores such a set used to pin the two with a test of its own, and a test that needs a database is skipped on the -short tier — which is how a forge bump that added two scopes reached a project's main green and turned it red there.
So the CHECK DECLARES the set it mirrors, in the schema:
COMMENT ON CONSTRAINT access_tokens_scopes ON access_tokens IS
'forge:closed-set accesstoken.AllScopes';
and `forge lint` (the closed-sets lane, on by default) reads the CHECK the migrations last defined, compares its value list with the named Go set, and on a mismatch names the missing and extra values and prints the forward migration that fixes it. It reads migration text, not a database, so it runs everywhere lint does.
A set name resolves one of two ways, and never by reflection over code forge has not vetted:
- a short name (`accesstoken.AllScopes`) is a set forge exports, looked up in the registry below. It is compiled into the forge binary, which is the pinned forge (forge refuses to run at a version the project does not pin), so the lint compares against exactly the set the project builds against.
- a qualified name (`github.com/acme/app/internal/billing.AllTiers`) is a set the PROJECT owns: a package-level variable whose initializer is a composite literal of constants. forge type-checks that package and reads the constants' values (ResolveGo); it runs none of the project's code.
Index ¶
Constants ¶
const ( FormContainedBy = "<@ ARRAY[…]" FormAny = "= ANY (ARRAY[…])" FormIn = "IN (…)" )
The value-list shapes ParseCheck reads. Each is how a CHECK says "this column holds only these values": the array-containment form for an array column, and the two postgres treats as one for a scalar (it normalizes `IN (…)` to `= ANY (ARRAY[…])` in pg_get_constraintdef and pg_dump).
Variables ¶
This section is empty.
Functions ¶
func IsQualified ¶
IsQualified reports whether name is a project set's qualified Go name (`<import path>.<Var>`) rather than a forge-exported short name. An import path of a module always carries a `/`; a registry name never does.
func ResolveGo ¶
ResolveGo reads a project-owned closed set: qualified is `<import path>.<Var>`, a package-level variable whose initializer is a composite literal (`[]Tier{TierFree, TierPro}`, `[...]string{"a", "b"}`) of string or integer constants. The package is type-checked from source in dir's module and the members are the constants' values, so a member named by a constant (`TierFree`) reads as the value it stands for. Nothing in the project is executed.
A variable initialized any other way — from a function call, from another variable — is refused with the reason: forge reads values it can prove from the source, and names the literal to point at instead.
Types ¶
type Finding ¶
type Finding struct {
// File and Line locate the finding: the migration that last defined the
// CHECK when its list is wrong, the COMMENT ON CONSTRAINT otherwise.
File string
Line int
// Table and Constraint name the CHECK as the migrations write them.
Table, Constraint string
// Set is the declared set's name, "" when the declaration names none.
Set string
// Problem is the sentence saying what is wrong.
Problem string
// Missing are the set's values the CHECK lacks; Extra the CHECK's values
// the set lacks. Both empty unless the lists differ.
Missing, Extra []string
// Fix is the forward migration that repairs the finding, nil when there
// is none forge can write (an unknown set, a CHECK it cannot read).
Fix *Fix
}
Finding is one closed-set declaration whose CHECK does not hold its set, or that cannot be checked at all.
func Analyze ¶
Analyze reads every *.up.sql migration under dir in apply order and checks each constraint whose catalog comment declares `forge:closed-set <name>`: the value list of the CHECK the migrations LAST defined for it must equal the named set. A missing directory has no declarations.
It models what postgres does with the statements, as far as the declaration goes: a DROP CONSTRAINT drops the constraint's comment with it, so a CHECK replaced by DROP + ADD has lost its declaration unless the migration re-issues the COMMENT. That is reported too — silently losing the declaration would silently end the check.
type Fix ¶
type Fix struct {
// BackwardCompatible is true when the fix only widens the CHECK: every
// row the previous release wrote still satisfies it. Reason says why,
// either way — the text of the migration's compatibility declaration.
BackwardCompatible bool
Reason string
// SQL is the migration body: the CHECK replaced whole, and the
// declaration re-issued (postgres drops a constraint's comment with it).
SQL string
// Migration is the whole file, compatibility declaration first, ready to
// paste into a new `forge db migration new` file.
Migration string
}
Fix is the forward migration that brings a CHECK back to its set.
type List ¶
type List struct {
// Column is the column the list constrains, as postgres resolves it.
Column string
// Form is the SQL shape the list was read from: FormContainedBy,
// FormAny or FormIn.
Form string
// Values are the list's members, in the order the CHECK spells them.
Values []string
// Start and End are the byte span, in the expression, that holds the
// members (first member's start to last member's end, casts included).
// Replacing it rewrites the list and leaves the rest of the expression —
// `cardinality(scopes) > 0 AND …`, the trailing `::text[]` — verbatim.
Start, End int
// Cast is the per-member cast every member carries (`::text`,
// `::character varying`), "" when the members carry none or differ. A
// rewritten list repeats it, so a pg_dump-normalized CHECK stays in the
// form pg_dump would write.
Cast string
// ArrayLiteral is set when the members came from one array-literal
// string (`'{a,b}'::text[]`) rather than ARRAY[…] or a list; the span is
// then the string literal itself.
ArrayLiteral bool
// Multiline is set when the CHECK puts its members on separate lines,
// and Indent is the whitespace before the first one, so a rewritten
// list keeps the layout.
Multiline bool
Indent string
}
List is the closed value list a CHECK expression holds.
func ParseCheck ¶
ParseCheck reads the one closed value list in a CHECK expression — the text inside `CHECK ( … )`, as a migration or pg_get_constraintdef spells it.
It reads `col <@ ARRAY[…]` (an array column), `col = ANY (ARRAY[…])` and `col IN (…)`, with or without casts on the members, the array or the column (`(status)::text = ANY ((ARRAY['a'::character varying])::text[])` is pg_dump's spelling of a varchar IN), and `'{a,b}'` array literals in place of ARRAY[…]. Anything around the list is allowed: `cardinality(scopes) > 0 AND scopes <@ …`, `kind IS NULL OR kind = ANY (…)`.
An expression with no such list, or with more than one (forge cannot tell which the set describes), is an error that says so. A NOT IN list is a deny list, not a closed set, and is not read.
type Resolver ¶
Resolver returns the values of the closed set a declaration names.
func NewResolver ¶
NewResolver resolves declaration names for the project at dir: a short name from the registry of forge-exported sets, a qualified one from the project's own Go source (ResolveGo). Each package is loaded once.
type Set ¶
type Set struct {
// Name is how a forge:closed-set declaration names it: the Go package
// name and the variable, `accesstoken.AllScopes`.
Name string
// Values is the set's members, in the Go order — the order a fix
// migration lists them in.
Values []string
}
Set is one closed set forge exports.