closedset

package
v1.0.12 Latest Latest
Warning

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

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

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

View Source
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

func IsQualified(name string) bool

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 Names

func Names() []string

Names returns every forge-exported set's name, sorted.

func ResolveGo

func ResolveGo(dir, qualified string) ([]string, error)

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

func Analyze(dir string, resolve Resolver) ([]Finding, error)

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

func ParseCheck(expr string) (List, error)

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

type Resolver func(name string) ([]string, error)

Resolver returns the values of the closed set a declaration names.

func NewResolver

func NewResolver(dir string) Resolver

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.

func Lookup

func Lookup(name string) (Set, bool)

Lookup returns the forge-exported set called name.

Jump to

Keyboard shortcuts

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