Migrations
Migrations let you evolve your database schema as your application changes. TalaDB applies pending migrations at open time, in version order, inside a single atomic transaction.
How migrations work
- TalaDB reads the current version from a
meta::db_versiontable inside the database. - It compares it to the highest version in your
migrationsarray. - Any migration whose
versionis greater than the stored version is considered pending. - Pending migrations are sorted by
versionand executed in order. - After all migrations succeed, the stored version is updated.
- If any migration throws, the entire transaction rolls back and the database is left at its previous version.
Defining migrations
Pass a migrations array to openDB:
import { openDB } from 'taladb'
const db = await openDB('myapp.db', {
migrations: [
{
version: 1,
description: 'Create indexes',
up: async (db) => {
await db.collection('users').createIndex('email')
await db.collection('users').createIndex('createdAt')
},
},
{
version: 2,
description: 'Add role field to existing users',
up: async (db) => {
const users = db.collection('users')
const all = await users.find({})
for (const user of all) {
if (!user.role) {
await users.updateOne(
{ _id: user._id },
{ $set: { role: 'user' } },
)
}
}
},
},
{
version: 3,
description: 'Create posts collection index',
up: async (db) => {
await db.collection('posts').createIndex('authorId')
await db.collection('posts').createIndex('publishedAt')
},
},
],
})Migration shape
interface Migration {
version: number // positive integer, must be unique and monotonically increasing
description: string // human-readable label (shown in logs)
up: (db: TalaDB) => Promise<void>
}Rules
Versions must be positive integers in ascending order. Gaps are allowed — you can go from version 1 directly to version 5. But you must never reuse or lower a version number once it has been applied.
Never modify an existing migration. A migration runs exactly once. If you change the up function after it has run on a device, that device will not re-run it. Instead, add a new migration at a higher version.
Migrations are atomic. All pending migrations run in a single write transaction. If migration 3 fails after 1 and 2 succeed, the entire batch rolls back. On the next open, all three are retried.
createIndex is idempotent. Calling it for an index that already exists is safe — it does nothing.
Fresh installs
On a fresh install, the stored version is 0. All migrations run in order on first open. This means there is no separate "initial schema" step — version 1 is your initial schema.
Inspecting the current version
You can query the current version outside of migrations using the CLI:
taladb inspect myapp.dbOr in code (advanced):
import { TalaDBNode } from '@taladb/node'
const db = TalaDBNode.open('./myapp.db')
// The version is stored in the 'meta::db_version' redb tableExample: adding a new collection over time
const migrations = [
{
version: 1,
description: 'Initial schema',
up: async (db) => {
await db.collection('users').createIndex('email')
},
},
{
version: 2,
description: 'Add posts collection',
up: async (db) => {
await db.collection('posts').createIndex('authorId')
await db.collection('posts').createIndex('_fts:body')
},
},
{
version: 3,
description: 'Normalise email to lowercase',
up: async (db) => {
const users = db.collection('users')
const all = await users.find({})
for (const user of all) {
if (typeof user.email === 'string') {
await users.updateOne(
{ _id: user._id },
{ $set: { email: user.email.toLowerCase() } },
)
}
}
},
},
]