Schema

DDL emission

Preview the SQL for a migration plan before preparing it for execution.

The @qubu/migrate/ddl entrypoint accepts only a MigrationPlan and a SchemaDialect. It does not read a catalog, open a connection, start a transaction, or write a migration journal. Preflight runs before rendering, so a blocked or incompatible plan returns diagnostics and no SQL.

import { emitMigrationPlan } from "@qubu/migrate/ddl"
import { postgresSchemaDialect } from "qubu/snapshot/postgres"

const result = emitMigrationPlan(plan, postgresSchemaDialect)
if (!result.ok) {
  // Review result.diagnostics. result.sql is an empty string.
  throw new Error(result.diagnostics.map((item) => item.message).join("\n"))
}

for (const statement of result.statements) {
  console.log(statement.operationId, statement.sql, statement.parameters)
}

statements contains the SQL preview in dependency order. Each statement has:

  • Its operation ID.
  • Its position in that order.
  • SQL text.
  • An ordered parameter list.

Schema literals and expressions are parameter-free by contract. sql joins the statements with a newline and adds a semicolon for migration-file writers.

Review gates

The emitter rejects a plan with ready: false, decision-required operations, unknown or lossy facts, unsupported safety, or destructive changes unless the caller supplies the matching explicit option.

allowUnsafe is available for preview integrations, but it is not accepted as an artifact approval and does not make an opaque object renderable. Opaque and deferred catalog records need an explicit tagged custom-sql operation for preview and an operation-scoped custom program for sealed execution.

Check execution requirements

Lock and transaction requirements describe what a later executor must provide. Pass lock or transaction to preflight those requirements against the executor's context. A required transaction with transaction: 'autocommit' produces transaction-conflict; no transaction is opened by the emitter.

const result = emitMigrationPlan(plan, postgresSchemaDialect, {
  transaction: "managed",
  lock: "exclusive",
  serverVersion: "16",
})

First-party support

Object or operation PostgreSQL SQLite MySQL
Tables, columns, constraints, indexes yes tables, columns, and indexes; constraints require an inline/rebuild path yes
Views yes yes yes
Materialized views yes no no
Sequences, enums, domains, policies, extensions yes no no
Routines functions and procedures no functions and procedures
Partitions yes no add/drop partition forms
Triggers yes yes yes
Comments and ownership yes no table comments only
Explicit custom SQL dialect tag must match dialect tag must match dialect tag must match

The support table describes syntax Qubu can render from modeled snapshot facts. Server-version checks still apply, such as SQLite column rename and drop-column limits and MySQL check-constraint support. An unsupported operation is reported as a diagnostic rather than silently omitted. SQLite table constraints are not emitted as ALTER TABLE ... ADD CONSTRAINT; use a schema snapshot that carries an inline constraint declaration or an explicit rebuild/custom-SQL operation.

Custom SQL stays opaque and appears at its plan position. The emitter does not inspect it for object names or infer SQL from an opaque catalog record.

Prepare for execution

For execution, compile the plan with compileMigrationProgram() from @qubu/migrate/artifact. The versioned program—not the aggregate sql string—is authoritative. See Artifacts and approval policy.

CREATE TABLE statements follow column ordinalPosition values, preserving schema declaration order across PostgreSQL, MySQL, and SQLite. Snapshot arrays remain sorted by ID for deterministic serialization; array order does not determine DDL column order.

For new PostgreSQL tables, opt in to alignment ordering:

import { emitMigrationPlan } from "@qubu/migrate/ddl/postgres"

const preview = emitMigrationPlan(plan, { columnOrder: "alignment" })

The default is "declaration". Alignment ordering places known fixed-width types first, in descending alignment order, preserving declaration order for ties. Variable-length and unknown types retain their relative order after that group. This is a conservative heuristic: nulls and variable-length values affect actual savings. It does not rebuild or reorder existing tables or change index key order. MySQL and SQLite reject "alignment".