Schema
Column behavior and write types
Choose the values a column returns and accepts, including defaults and generated values.
Give each operation its own type
Every column has a selected output type. It can also describe what inserts and updates accept:
| Option | Selected output | Insert input | Update input |
|---|---|---|---|
| nullable: true | T or null | accepts T or null | accepts T or null |
| hasDefault: true | unchanged | key is optional | unchanged |
| generated: true | unchanged | key is omitted | key is omitted |
Use column<Output, Insert, Update>() when the driver returns a different type
from the type the application writes:
import { column, integer, table, text } from "qubu"
const accounts = table("accounts", {
id: integer({ generated: true }),
email: text(),
nickname: text({ nullable: true, hasDefault: true }),
externalScore: column<number, string, number>({ nullable: true }),
})
The selected externalScore is number | null. Inserts accept string | null, and
updates accept number | null.
Describe defaults and generated columns
The hasDefault and generated flags describe which values writes accept. Use
complete metadata when schema tooling also needs the database fact:
import { boolean, generatedColumn, identityColumn, integer, table, text, value } from "qubu"
import { defineSchemaExpression } from "qubu/schema"
const currentTimestamp = defineSchemaExpression("function", (context) => {
context.append("CURRENT_TIMESTAMP")
})
const accounts = table("accounts", {
id: integer({ identity: identityColumn("always") }),
status: text({ default: "pending" }),
active: boolean({ default: true }),
score: integer({
generatedColumn: generatedColumn(value(1), "stored"),
}),
createdAt: text({
default: currentTimestamp,
}),
})
Primitive values in default are canonical literals. Strings are never
interpreted as SQL, and booleans remain semantic values so each dialect can
choose its own spelling. Pass a branded deterministic schema expression
directly when the default is SQL, and use unsafeSchemaSql() only for trusted
syntax Qubu does not model.
Generated expressions record stored or virtual mode. An identity descriptor stays separate because identity behavior is not an ordinary generated expression.
Complete defaults cannot be combined with generated or identity metadata.
Contradictory flags fail with a structured ColumnBehaviorError. Use
externalDefault() or externalGeneratedColumn() when another schema authority
owns the missing detail.
Supply defaults at runtime
Use defaultFn when Qubu should supply an omitted insert value at runtime:
const sessions = table("sessions", {
token: text({ defaultFn: () => crypto.randomUUID() }),
})
Runtime defaults make the insert key optional and run once for each omitted row value. They remain live column behavior: snapshots and emitted DDL do not record a database default.
A column may declare both default and defaultFn;
Qubu writes use the runtime value while the database default remains available
to other clients.
Check database-specific rules
Identity details stay on the identity descriptor:
- SQLite
autoIncrementrequires an exactINTEGERrowid alias that is the sole column of a primary key. - MySQL
AUTO_INCREMENTis a column-level identity extension. - MySQL
ON UPDATEaccepts a branded deterministic expression.
The database-specific restrictions are listed in the snapshot overview and its dialect matrices.
Narrow an application type
Use $type<T>() to narrow a helper's TypeScript type without changing its
runtime column definition:
import { table, text } from "qubu"
const users = table("users", {
status: text().$type<"active" | "disabled">(),
})
The narrowed type applies to selected values and insert and update inputs. It does not validate values at runtime or add a database constraint.
Derive write input types
TableInsertInput and TableUpdateInput expose the same rules to application
code. The following example uses the accounts table from the earlier example:
import type { TableInsertInput, TableUpdateInput } from "qubu"
type AccountInsert = TableInsertInput<typeof accounts.definitions>
type AccountUpdate = TableUpdateInput<typeof accounts.definitions>
const insert: AccountInsert = {
email: "ada@example.com",
externalScore: "10",
}
const update: AccountUpdate = {
nickname: null,
externalScore: 10,
}
id is not accepted because it is generated. nickname is optional on insert
because the database supplies a default, but it remains a valid nullable update
field.
Continue with Write mutations for typed INSERT, UPDATE, and DELETE statements.