Documentation
¶
Overview ¶
Package cache holds the committed oracle cache and the catalog snapshot model. In P3 only the catalog data model is needed (the resolver and star expansion consume it); the oracle fills it from a live database in P4. See docs/design/04-type-oracle.md §3.
Index ¶
- Constants
- func CatalogFileName(fp string) string
- func EncodeCatalog(cat *Catalog) ([]byte, error)
- func EncodeEnv(e *Env) ([]byte, error)
- func EncodeOracle(e *OracleEntry) ([]byte, error)
- func EnvFileName(fp string) string
- func Fingerprint(dialectName, serverVersion string, files []SchemaFile) string
- func NumericVersionPrefix(raw string) string
- func OracleFileName(fp, renderedSQL string) string
- func ReadFileCapped(path string) ([]byte, error)
- func WriteFileAtomic(path string, data []byte, perm os.FileMode) error
- type Catalog
- type Column
- type EntryColumn
- type EntryType
- type Env
- type OracleEntry
- type SchemaFile
- type Store
- func (s *Store) LoadCatalog(fp string) (*Catalog, bool)
- func (s *Store) LoadEnv(fp string) (*Env, bool)
- func (s *Store) LoadOracle(fp, renderedSQL string) (*OracleEntry, bool)
- func (s *Store) SaveCatalog(cat *Catalog) error
- func (s *Store) SaveEnv(e *Env) error
- func (s *Store) SaveOracle(e *OracleEntry) error
- type Table
Constants ¶
const FormatVersion = 1
FormatVersion is the on-disk cache format. Every written file carries it; loads treat any other value — including its absence in pre-1.0 caches — as a miss, so a format change can never misread an old entry: the pipeline falls back to the database and rewrites.
const MaxFileBytes = 64 << 20
MaxFileBytes bounds every cache file sqletch reads. Cache file names are fingerprint-derived, hence attacker-computable: a cloned repo can plant a file at the exact hit path, so an unbounded os.ReadFile would OOM before any key check runs. 64 MiB dwarfs any real catalog/oracle entry while capping the blast radius (mirrors the LSP body cap).
Variables ¶
This section is empty.
Functions ¶
func CatalogFileName ¶
CatalogFileName and OracleFileName expose the store's dir-relative file naming, so harnesses (the oracle corpus, entry pruning) can address files without duplicating the hashing scheme.
func EncodeCatalog ¶
EncodeCatalog returns the exact canonical bytes SaveCatalog writes. It stamps FormatVersion. Anything that compares against a committed catalog file byte-wise (the oracle corpus harness) must serialize through here, never through its own marshaling.
func EncodeEnv ¶
EncodeEnv returns the exact canonical bytes SaveEnv writes, stamping FormatVersion.
func EncodeOracle ¶
func EncodeOracle(e *OracleEntry) ([]byte, error)
EncodeOracle returns the exact canonical bytes SaveOracle writes, stamping FormatVersion — the byte form the corpus harness compares.
func EnvFileName ¶
EnvFileName exposes the sidecar's dir-relative naming, mirroring CatalogFileName.
func Fingerprint ¶
func Fingerprint(dialectName, serverVersion string, files []SchemaFile) string
Fingerprint is the offline-computable schema identity: sha256 over (dialect, pinned server version, ordered schema inputs).
func NumericVersionPrefix ¶
NumericVersionPrefix reduces a server-reported version string to the value drift detection compares: its leading dotted-numeric run.
Servers spell the same version differently depending on how they were built — PostgreSQL reports "16.4 (Debian 16.4-1.pgdg120+1)" on the Debian images and a bare "16.4" on Alpine, MySQL appends "-log". Comparing raw strings would report a base-image change as an environment drift, which is noise, not signal.
func OracleFileName ¶
func ReadFileCapped ¶
ReadFileCapped reads path but refuses more than MaxFileBytes, so an attacker-planted giant file at a computable cache path cannot OOM the process. It reads at most MaxFileBytes+1 and rejects if that ceiling is reached, so the bound holds even if the file grows after an initial stat (no size TOCTOU).
func WriteFileAtomic ¶
WriteFileAtomic writes data to path atomically without ever following a symlink at path or at a predictable temp name. A cloned repo can pre-plant `<path>.tmp` (a computable name) as a symlink to a secret or config file; writing through it and renaming over the target would corrupt an arbitrary location. os.CreateTemp opens with O_CREATE|O_EXCL and a RANDOM suffix, so it neither follows nor collides with any planted link; the final os.Rename replaces a symlink sitting at path with our regular file rather than writing through it.
Types ¶
type Catalog ¶
type Catalog struct {
Format int `json:"format"`
SchemaFP string `json:"schema_fp"`
Tables []Table `json:"tables"`
}
Catalog is an offline snapshot of the schema portions sqletch needs: relation and column existence, types, NOT NULL, and defaults.
func (*Catalog) Lookup ¶
Lookup finds a table by unqualified name, preferring the "public" schema on ties. Returns nil when absent.
func (*Catalog) LookupQualified ¶
LookupQualified finds a table by an explicit schema qualifier, or falls back to Lookup's unqualified resolution when schema is empty. An explicitly qualified name never falls back: resolving it to a same-named table of another schema is exactly the confusion the nullability analysis must not inherit.
type EntryColumn ¶
type EntryColumn struct {
Name string `json:"name"`
OID uint32 `json:"oid"`
TypeName string `json:"type_name"`
SrcRel uint32 `json:"src_rel,omitempty"`
SrcAtt int16 `json:"src_att,omitempty"`
}
EntryColumn records what the ORACLE said about a result column — and nothing derived: nullability verdicts are recomputed from the catalog and the parse tree on every run (design 05 §4), so they never enter these byte-pinned files. (A vestigial always-false "nullable" field was removed 2026-08 without a FormatVersion bump: old entries still decode — v1 tolerates the extra field — and re-derivation gates were regenerated.)
type EntryType ¶
EntryType / EntryColumn mirror dialect.TypeRef / dialect.ColumnDesc without importing the dialect package (cache is a leaf; dialect imports cache for the Catalog model).
type Env ¶
type Env struct {
Format int `json:"format"`
SchemaFP string `json:"schema_fp"`
// Dialect and OracleBackend are recorded for forensics only. The
// dialect is already fingerprint input, and the backend is
// deliberately NOT compared: server and native backends are
// required to produce byte-identical output, so a backend
// difference is either a no-op or a sqletch bug for the corpus
// gates to catch — never a reason to fail a user's build.
Dialect string `json:"dialect"`
OracleBackend string `json:"oracle_backend"`
// ServerVersion is the compared value: the leading dotted-numeric
// run of what the server reported (see NumericVersionPrefix).
ServerVersion string `json:"server_version"`
// ServerVersionRaw is the full reported string, kept so the
// diagnostic can name the actual builds involved.
ServerVersionRaw string `json:"server_version_raw"`
}
Env is the sidecar record of the environment a committed cache was generated in.
It is deliberately NOT a cache key. The schema fingerprint must stay offline-computable (spec requirement), so the version of a server we have not contacted cannot enter it; and the bytes of catalog and oracle entries are pinned byte-identical across oracle backends by internal/corpus, so nothing backend- or connection-specific may enter those files either. The sidecar therefore lives beside them, keyed by the same fingerprint, and is read only by runs that contact a server anyway — where it answers one question the cache otherwise cannot: "was what I am connected to the same thing that produced these entries?" (docs/design/04-type-oracle.md §3.1).
Only facts that are semantically part of the oracle's answer belong here. Host names, user names, and timestamps do not: they would churn the committed diff on every developer's machine and break the project's determinism invariant.
type OracleEntry ¶
type OracleEntry struct {
Format int `json:"format"`
SchemaFP string `json:"schema_fp"`
RenderedSQL string `json:"rendered_sql"`
Params []EntryType `json:"params"`
Columns []EntryColumn `json:"columns"`
}
OracleEntry is one cached Describe result, self-describing with its full keys.
type SchemaFile ¶
SchemaFile is one ordered schema input contributing to the fingerprint.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the committed, offline-usable cache of oracle results and catalog snapshots. Hashes are an index, never identity: every entry stores its full inputs and loads compare them byte-wise (store-and-compare; design 04 §3).
func (*Store) LoadCatalog ¶
LoadCatalog returns the snapshot for fp, or ok=false on miss or key mismatch.
func (*Store) LoadEnv ¶
LoadEnv returns the recorded generation environment for fp.
Every way of not having a usable record — absent file, unreadable file, format drift, key mismatch — returns ok=false, which callers must treat as "no record yet", never as a failure. Caches committed before this sidecar existed have no record, and adopting the connected server on the next write is the correct migration.
func (*Store) LoadOracle ¶
func (s *Store) LoadOracle(fp, renderedSQL string) (*OracleEntry, bool)
LoadOracle returns the cached Describe result for (fp, renderedSQL), comparing the stored full keys (never trusting the filename hash).
func (*Store) SaveCatalog ¶
func (*Store) SaveOracle ¶
func (s *Store) SaveOracle(e *OracleEntry) error
type Table ¶
type Table struct {
Schema string `json:"schema"`
Name string `json:"name"`
OID uint32 `json:"oid"`
// HasChildren marks a plain-inheritance parent (PostgreSQL:
// relhassubclass on relkind 'r'). Children may DROP an inherited
// NOT NULL (proven on PG 16), so a parent scan can return NULL
// where attnotnull says otherwise — the analyzer must not narrow
// such tables unless the reference is `FROM ONLY`. Partitioned
// parents ('p') are exempt: partitions cannot drop inherited NOT
// NULL (42P16). omitempty keeps every inheritance-free catalog
// byte-identical.
HasChildren bool `json:"has_children,omitempty"`
// IsView marks a relation that is a VIEW rather than a base table.
// SQLite's column-origin attribution (sqlite3_column_origin_name)
// resolves a view's result columns THROUGH to the view's base
// tables, whose declared NOT NULL the view's (invisible, possibly
// null-extending) body need not preserve — so a base table appearing
// directly in FROM must not be allowed to vouch for a column that
// actually flows through a view. The nullability analyzer treats any
// view in play as a wholesale narrowing kill-switch. PostgreSQL and
// MySQL report the view's own identity (never the base table) and so
// never set this; omitempty keeps their catalogs byte-identical.
IsView bool `json:"is_view,omitempty"`
Cols []Column `json:"cols"`
}