Migrations (@ydbjs/drizzle-adapter/migrator)
migrate() applies a sequence of migrations to a YDB database, records what's been applied in a bookkeeping table, and (optionally) coordinates with other deployers through a lock table. The build*Sql DDL builders are exposed alongside for tooling that needs to render statements without running them.
Folder-based migrations
The drizzle-kit format works as-is — point migrate() at the directory and it reads meta/_journal.json plus every *.sql next to it:
import { migrate } from '@ydbjs/drizzle-adapter/migrator'
await migrate(db, { migrationsFolder: './drizzle' })In a deployment script:
await migrate(db, {
migrationsFolder: './drizzle',
migrationsTable: '__migrations',
})migrationsTable defaults to __drizzle_migrations.
Inline migrations
Pass migrations: [...] for programmatic schemas (e.g. tests, ephemeral databases). Each entry is { name, operations | sql }:
import { migrate } from '@ydbjs/drizzle-adapter/migrator'
import { ydbTable, integer, text } from '@ydbjs/drizzle-adapter/schema'
let users = ydbTable('users', {
id: integer('id').notNull().primaryKey(),
name: text('name').notNull(),
})
await migrate(db, {
migrations: [
{
name: '001_create_users',
operations: [{ kind: 'create_table', table: users, ifNotExists: true }],
},
{
name: '002_seed',
sql: ["UPSERT INTO `users` (id, name) VALUES (1, 'admin')"],
},
],
})Mixing operations (typed) and sql (raw YQL strings) in the same migration is supported. operations cover create_table, drop_table, add_column, drop_column, add_index, drop_index, add_changefeed, drop_changefeed, rename_table, plus topics, replication, transfer, and ACL operations.
Distributed lock
When multiple deploy jobs can run in parallel (blue/green, autoscaler restart, manual kubectl rollout), enable the lock table so only one process applies migrations at a time:
await migrate(db, {
migrationsFolder: './drizzle',
migrationLock: {
key: 'production',
leaseMs: 10 * 60 * 1000,
acquireTimeoutMs: 60 * 1000,
retryIntervalMs: 300,
},
})keyidentifies the lock — use one per logical databaseleaseMsis the lease length; if a deployer crashes mid-migration, the lease expires after this window so the next deployer can take overacquireTimeoutMsbounds how long competing deployers waitretryIntervalMsis the poll interval
The lock table defaults to <migrationsTable>_lock; override with migrationLock.table.
Recovery
A migration can crash mid-way (network blip, OOM, deployer kill). The next run sees a row in __migrations with status = 'running' for the same migration. migrationRecovery controls what happens:
await migrate(db, {
migrationsFolder: './drizzle',
migrationRecovery: {
mode: 'retry',
staleRunningAfterMs: 60 * 60 * 1000,
},
})mode | Behaviour |
|---|---|
'retry' (default) | After staleRunningAfterMs, mark the row failed and re-apply |
'fail' | Abort immediately and surface the stale row to the operator |
'skip' | Mark the row failed and continue to the next migration |
Use 'retry' for stateless DDL (typical case); use 'fail' if your migrations have manual cleanup that must happen first.
DDL builders for tooling
Every YQL statement the migrator emits is also available as a pure builder function. They take typed table/column/index objects and return strings:
import {
buildCreateTableSql,
buildAlterTableSql,
buildAddIndexSql,
buildDropChangefeedSql,
} from '@ydbjs/drizzle-adapter/migrator'
let createSql = buildCreateTableSql(users)
let alterSql = buildAlterTableSql('users', [
{ kind: 'add_column', column: usersWithEmail.email },
{ kind: 'add_index', index: emailIndex },
])The full set covers tables, indexes, column families, changefeeds, topics, async replication, transfer, views, groups, users, secrets, ACL grants/ revokes, and ANALYZE / RENAME / SHOW CREATE. Reach for these when you want to dry-run a migration, render diff previews, or feed YQL to an external tool.
Running migrations in CI/CD
Run migrations as a single deploy step before app instances start serving traffic. With the lock table enabled, parallel deploy jobs won't race.
node scripts/migrate.mjs// scripts/migrate.mjs
import { createDrizzle } from '@ydbjs/drizzle-adapter'
import { migrate } from '@ydbjs/drizzle-adapter/migrator'
let db = createDrizzle({ connectionString: process.env.YDB_CONNECTION_STRING })
try {
await migrate(db, {
migrationsFolder: './drizzle',
migrationLock: { key: 'production', leaseMs: 600_000, acquireTimeoutMs: 60_000 },
})
} finally {
await db.$client.close?.()
}The repository's examples/drizzle-adapter demonstrates inline migrations with the lock + recovery options against a local YDB container.