cache

package
v0.0.2 Latest Latest
Warning

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

Go to latest
Published: Sep 11, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

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

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

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

func CatalogFileName(fp string) string

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

func EncodeCatalog(cat *Catalog) ([]byte, error)

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

func EncodeEnv(e *Env) ([]byte, error)

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

func EnvFileName(fp string) string

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

func NumericVersionPrefix(raw string) string

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 OracleFileName(fp, renderedSQL string) string

func ReadFileCapped

func ReadFileCapped(path string) ([]byte, error)

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

func WriteFileAtomic(path string, data []byte, perm os.FileMode) error

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

func (c *Catalog) Lookup(name string) *Table

Lookup finds a table by unqualified name, preferring the "public" schema on ties. Returns nil when absent.

func (*Catalog) LookupOID

func (c *Catalog) LookupOID(oid uint32) *Table

LookupOID finds a table by OID. Returns nil when absent.

func (*Catalog) LookupQualified

func (c *Catalog) LookupQualified(schema, name string) *Table

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 Column

type Column struct {
	Name       string `json:"name"`
	Att        int16  `json:"att"`
	TypeOID    uint32 `json:"type_oid"`
	TypeName   string `json:"type_name"`
	NotNull    bool   `json:"not_null"`
	HasDefault bool   `json:"has_default"`
}

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

type EntryType struct {
	OID  uint32 `json:"oid"`
	Name string `json:"name"`
}

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

type SchemaFile struct {
	Path    string
	Content []byte
}

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 NewStore

func NewStore(dir string) *Store

func (*Store) LoadCatalog

func (s *Store) LoadCatalog(fp string) (*Catalog, bool)

LoadCatalog returns the snapshot for fp, or ok=false on miss or key mismatch.

func (*Store) LoadEnv

func (s *Store) LoadEnv(fp string) (*Env, bool)

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 (s *Store) SaveCatalog(cat *Catalog) error

func (*Store) SaveEnv

func (s *Store) SaveEnv(e *Env) error

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"`
}

func (*Table) Col

func (t *Table) Col(name string) *Column

func (*Table) ColByAtt

func (t *Table) ColByAtt(att int16) *Column

Jump to

Keyboard shortcuts

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