Schema
Canonical schema snapshots
Serialize schema metadata into strict, deterministic data and keep serialization separate from diffing, planning, and DDL emission.
Qubu's schema tooling lives behind the qubu/snapshot entrypoint. It converts
an immutable schema() registry into versioned data that can be inspected,
hashed, checked into source control, and handed to a dialect adapter. Importing
the snapshot entrypoint is optional; ordinary query imports do not load it.
import { createSchemaSnapshot, encodeSchemaSnapshot } from "qubu/snapshot"
const snapshot = createSchemaSnapshot(appSchema)
const json = encodeSchemaSnapshot(snapshot)
The v1 envelope contains a format version, an independently versioned dialect extension, a versioned naming-policy description, an optional namespace, and arrays of tables. Tables, columns, constraints, and indexes are sorted by stable logical ID. Physical names are values in the snapshot, not identities: changing a physical name does not change the TypeScript field or metadata key.
Snapshot data is deliberately not executable Qubu state. Expressions are parameter-free data records, and decoding never creates tables, column references, or render closures. The neutral fallback renders branded built-in expressions through the standard schema context; a dialect adapter may replace that hook with its own literal and expression policy. An explicitly unsafe expression retains its dialect tag and is rejected when it does not match the selected snapshot dialect.
import { decodeSchemaSnapshot } from "qubu/snapshot"
const decoded = decodeSchemaSnapshot(json)
if (!decoded.ok) {
for (const issue of decoded.diagnostics) {
console.error(issue.path.join("."), issue.code, issue.message)
}
}
The decoder is strict. It reports unknown fields, malformed nodes, unsupported
future format or extension versions, non-canonical entity ordering, wrong
dialect metadata, and broken foreign-key or column references as structured
diagnostics. It does not call process.exit() and has no runtime validation
library dependency.
schemaSnapshotFingerprint() computes a deterministic content fingerprint from canonical
JSON. The fingerprint is useful for cache keys and fixture assertions only. It is not
an entity identity, a rename marker, or migration lineage.
Adapter boundary
SchemaDialect is a capability superset of Dialect. Create one with
createSchemaDialect(queryDialect, hooks); the resulting object retains the
query dialect's name, identifier quoting, placeholders, literals, JSON, casts,
and advertised capabilities while adding schema encoders and validation under
.schema. Snapshot adapters reference that object instead of constructing a
second query dialect. The schema snapshot format version remains independent
from the dialect identity.
The common traversal owns logical IDs, fixed property order, canonical sorting,
portable constraints, cross-reference checks, and the immutable snapshot
envelope. A dialect adapter owns physical storage mapping, SQL literal and
expression encoding, dialect extensions, capability checks, and any dialect
naming policy. PostgreSQL, SQLite, and MySQL adapters can implement
SchemaSnapshotAdapter without duplicating traversal or decoder rules.
The neutral API stays at qubu/snapshot; built-in dialect adapters have
dedicated subpaths so importing neutral snapshot utilities does not widen that
API:
import { createSchemaSnapshot } from "qubu/snapshot"
import { createSchemaSnapshot as createPostgresSnapshot } from "qubu/snapshot/postgres"
const neutral = createSchemaSnapshot(appSchema)
const postgres = createPostgresSnapshot(appSchema)
The PostgreSQL adapter is documented in the PostgreSQL snapshot support
matrix. Its schema dialect extends the
postgresql query dialect, and snapshot metadata uses that same identity.
The SQLite adapter is documented in the SQLite snapshot support
matrix.
The MySQL adapter is documented in the MySQL snapshot support
matrix. Its query and snapshot dialects both
use mysql, while MySQL-only ON UPDATE and AUTO_INCREMENT details remain
inside the column and identity metadata they describe.
Snapshot serialization remains separate from database introspection,
comparison, rename resolution, migration planning, and DDL emission. The
optional qubu/introspection entrypoint can produce the same canonical
Snapshot v1 data from a user-owned catalog connection. The complete normalized
catalog can also be encoded as strict Snapshot v2 with the dedicated
complete-snapshot APIs described in the catalog model.
Readers and connection lifecycle do not belong to this pure serialization
layer. Diffing can compare either snapshot version. Resolved diffs feed
migration plans, and approved plans feed DDL emission. The package-wide
ownership map keeps
those pure steps separate from application-owned database execution.
The optional schema source generator consumes a complete, non-lossy introspection result and makes its generated schema the next identity baseline. It does not replace snapshot serialization or generate Snapshot v2 object families.