Schema
Generate a schema from introspection
Generate a TypeScript schema module from a complete introspection result with no omitted facts.
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.
What the module contains
The module exports each ordinary Snapshot v1 table and one schema registry. It reconstructs:
- Physical names and exact native storage.
- Column write behavior and defaults.
- Generated-column and identity metadata.
- Constraints and indexes.
- Opaque predicates and expressions.
- 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 fails when the input contains:
- Failed or lossy introspection.
- An edited snapshot that no longer matches its catalog.
- Omitted Snapshot v1 facts or unresolved references.
- Unsafe names or invalid mapping tokens.
- Data that cannot be printed safely as source.
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. It does not emit:
- Views and materialized views.
- Sequences, enums, and domains.
- Routines and triggers.
- Partitions and policies.
- Collations and extensions.
- Comments and ownership.
- Retained opaque or deferred objects.
If any excluded family is non-empty, generation reports diagnostics rather than presenting the module as complete.
The caller handles file writes and driver integration. Generation does not merge hand edits or run migrations.
Use Database introspection for the catalog boundary and Canonical schema snapshots for the identity artifact.