Schema
Generate a schema from introspection
Turn one complete, non-lossy Snapshot v1 introspection result into a deterministic, machine-owned TypeScript schema module.
Source generation is an optional capability exported from qubu/codegen. It
is a pure handoff after introspection: it opens no connection, runs no catalog
query, and writes no file. The caller owns those boundaries.
Generate a module
Read and map one namespace in strict mode, then pass that exact result to the generator:
import { writeFile } from "node:fs/promises"
import { generateSchemaSource } from "qubu/codegen"
import { mapCatalogToSnapshot } from "qubu/introspection"
import { readCatalog } from "qubu/introspection/sqlite"
const catalog = await readCatalog(connection, { namespace: "main" })
const introspection = mapCatalogToSnapshot(catalog, { namespace: "main" })
const generated = generateSchemaSource(introspection)
if (!generated.ok) {
throw new Error(generated.diagnostics.map((issue) => issue.message).join("\n"))
}
await writeFile("src/schema.generated.ts", generated.source, "utf8")
writeFile() belongs to the application; generateSchemaSource() only
returns data. A successful result contains deterministic source and every
retained diagnostic. A failed result contains diagnostics and no partial
source.
The module exports one declaration for every ordinary Snapshot v1 table and
one schema registry. It reconstructs physical names, exact native storage,
column write behavior, defaults, generated and identity metadata, constraints,
indexes, opaque predicates and expressions, and dialect extensions. Checks use
catalogCheck(). Foreign keys use lazy catalogForeignKey() targets so
forward declarations and cycles remain valid.
Adopt the generated identity baseline
The first introspection snapshot commonly uses physical names as logical IDs. Generated declarations use deterministic camelCase registry, table, column, constraint, and index IDs while retaining every physical database name. Once the generated module is accepted, its serialized snapshot becomes the identity baseline for the next catalog read:
import { mapCatalogToSnapshot } from "qubu/introspection"
import { createSchemaSnapshot } from "qubu/snapshot/sqlite"
import { mainSchema } from "./schema.generated.ts"
const previousSnapshot = createSchemaSnapshot(mainSchema)
const next = mapCatalogToSnapshot(nextCatalog, {
namespace: "main",
previousSnapshot,
})
This handoff is deliberate. Do not keep using the pre-generation snapshot as the long-term identity source, or later diffs will compare physical IDs with the new generated IDs.
Control names and column types
Application output, insert, and update types default independently to
unknown. Qubu may attach a SQL semantic domain only when catalog evidence is
exact. Native storage always keeps the catalog declaration, even when the
semantic domain remains SqlUnknown.
Use the controlled callbacks to adopt trusted names or application mappings:
const generated = generateSchemaSource(introspection, {
naming(context) {
if (context.kind === "table" && context.physicalName === "user_records") {
return "users"
}
},
mapColumn(context) {
if (context.columnPhysicalName === "account_id") {
return {
output: "string",
insert: "string",
update: "string",
sqlDomain: "uuid",
}
}
},
})
Callbacks select names and fixed type tokens; they never take over printing and cannot return imports, expressions, comments, or arbitrary source. A returned name must still be a safe camelCase ID. Collisions and unsafe names fail with diagnostics and no source.
Diagnostics and source safety
Generation rejects failed or lossy introspection, an altered snapshot that no longer matches its catalog, omitted Snapshot v1 facts, unresolved references, unsafe names, invalid mapping tokens, and data that cannot be represented without source injection. Existing introspection diagnostics stay attached to the result.
Important
A database can allow a foreign key to reference a nullable UNIQUE
constraint. Snapshot v1 retains that constraint as nullable uniqueness, not
as a Qubu candidate key. Source generation returns an
unrepresentable-fact diagnostic instead of weakening the generated
references() proof. Use a non-null primary key, strict unique key, or
candidate index as the foreign-key target before adopting generated source.
Catalog names, native declarations, SQL, and extension metadata are untrusted input. The printer serializes them only as controlled literals beneath a static header. It does not interpolate catalog text as code or comments, parse opaque SQL, or merge a previous generated file with hand edits. Treat the file as replaceable output and keep application customizations in separate modules.
The public types and TSDoc on generateSchemaSource(),
SchemaCodegenOptions, and CodegenDiagnostic define the exact callback and
result contracts.
Snapshot v1 boundary
Generation covers ordinary Snapshot v1 tables in one namespace. Complete catalog families outside that model—views, materialized views, sequences, enums, domains, routines, triggers, partitions, policies, collations, extensions, comments, ownership, and retained opaque or deferred objects—are not emitted. Non-empty excluded families produce diagnostics so the generated module does not look complete by omission.
The entrypoint does not provide a CLI, filesystem ownership, live driver integration, multiple namespaces, runtime schema materialization, migrations, DDL, Snapshot v2 object generation, or hand-edit merging. Use Database introspection for the catalog boundary and Canonical schema snapshots for the identity artifact.