Schema

Storage and schema SQL

Describe database storage types and write expressions for schema definitions.

Keep application and SQL types separate

A column can describe four different things:

  • Its application value type.
  • Its SQL semantic domain, which controls compatible operations.
  • Its physical database storage type.
  • Its target type when used in a cast.

For example, numeric() decodes to a TypeScript number, carries SqlDecimal, uses portable numeric storage, and has a logical decimal cast target. Read SQL semantic types for operator compatibility and contextual literals.

Record physical storage

Built-in helpers use portable storage descriptors:

Helper Portable storage
integer() integer
numeric() numeric
text() text
boolean() boolean
date() date
timestamp() timestamp
uuid() uuid
json() json
bigint() bigint
binary() binary

Use a dialect-native descriptor when a column needs an exact vendor declaration:

import { nativeColumn, nativeStorage, table } from "qubu"

const accounts = table("accounts", {
  handle: nativeColumn(nativeStorage("postgresql", 'citext COLLATE "C"'), {
    sqlType: "postgres.citext",
  }),
})

nativeStorage() preserves the declaration text and freezes the descriptor. The ColumnStorageOf, ColumnStorageTypeOf, ColumnStorageDialectOf, and ColumnStorageDeclarationOf helpers read its metadata. Native storage is descriptive.

The optional sqlType field is the runtime semantic domain passed to adapters; provide it for custom domains because the compile-time SQL type is not available at runtime. It does not change selection, mutation, or query rendering.

Render deterministic schema expressions

Schema SQL uses a different context from query SQL. Built-in scalar expressions can be rendered for checks, generated columns, indexes, and other declaration metadata:

import { eq, table, text } from "qubu"
import { renderSchemaSql } from "qubu/schema"

const accounts = table("accounts", { status: text() })

renderSchemaSql(eq(accounts.status, "active"), { mode: "check" })
// ("status" = 'active')

The schema context emits SQL literals instead of placeholders. It supports strings, finite numbers, booleans, bigint, and NULL through the portable fallback. A dialect can provide renderSchemaLiteral for another spelling. Unsupported values and direct calls to parameter() fail.

Column references render as bare physical identifiers for generated, check, and index expressions. Default expressions reject column references because a column default cannot depend on another row value. Aggregates, windows, and subqueries are rejected in every schema mode.

An extension must opt into the schema contract with defineSchemaExpression():

import { defineSchemaExpression, renderSchemaSql } from "qubu/schema"

const currentDate = defineSchemaExpression("function", (context) => {
  context.append("CURRENT_DATE")
})

renderSchemaSql(currentDate, { mode: "default" })

Use raw schema SQL only when necessary

Use unsafeSchemaSql(dialect, sql) only for trusted, parameter-free syntax that Qubu does not model. Its dialect tag is checked when rendering, and its text is preserved apart from normalizing line endings. A normal makeExpression() extension must pass through the explicit schemaExpression() audit boundary before schema rendering accepts it.

Warning

Raw schema SQL does not quote identifiers or bind values. Keep it in trusted extension code.