Skip to content

Commit

Permalink
Add getMigrations method for Migrator. closes #58
Browse files Browse the repository at this point in the history
  • Loading branch information
koskimas committed Jan 25, 2022
1 parent 7c12299 commit 05d5636
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 17 deletions.
30 changes: 21 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,14 +204,24 @@ export async function down(db: Kysely<any>): Promise<void> {
```

The `up` function is called when you update your database schema to next version and `down`
when you go back to previous version. The only argument to the functions is an instance of
`Kysely<any>`. It is important to use `Kysely<any>` and not `Kysely<YourDatabase>`. Migrations
should never depend on the current code of your app because they need to work even if the app
changes completely. Migrations need to be "frozen in time".
when you go back to previous version. The only argument for the functions is an instance of
`Kysely<any>`. It's important to use `Kysely<any>` and not `Kysely<YourDatabase>`. Migrations
should never depend on the current code of your app because they need to work even when the app
changes. Migrations need to be "frozen in time".

The migrations can use the [Kysely.schema](https://koskimas.github.io/kysely/classes/SchemaModule.html)
module to modify the schema. Migrations can also run normal queries to modify data.

Execution order of the migrations is the alpabetical order of their names. An excellent way to name your
migrations is to prefix them with an ISO 8601 date string. A date prefix works well in large teams
where multiple team members may add migrations at the same time in parallel commits without knowing
about the other migrations.

You don't need to store your migrations as separate files if you don't want to. You can easily
implement your own [MigrationProvider](https://koskimas.github.io/kysely/interfaces/MigrationProvider.html)
and give it to the [Migrator](https://koskimas.github.io/kysely/classes/Migrator.html) class
when you instantiate one.

### PostgreSQL migration example

```ts
Expand Down Expand Up @@ -295,9 +305,9 @@ const migrator = new Migrator(migratorConfig);
await migrator.migrateToLatest(pathToMigrationsFolder)
```

to run all migrations that have not yet been run. The migrations are executed in alphabetical
order by their name. See the [Migrator](https://koskimas.github.io/kysely/classes/Migrator.html)
documentation for more info.
to run all migrations that have not yet been run. See the
[Migrator](https://koskimas.github.io/kysely/classes/Migrator.html)
class's documentation for more info.

Kysely doesn't have a CLI for running migrations and probably never will. This is because Kysely's
migrations are also written in typescript. To run the migrations, you need to first build the
Expand Down Expand Up @@ -352,9 +362,11 @@ async function migrateToLatest() {
migrateToLatest()
```

The migration methods use a lock on the database level, and parallel calls are executed serially.
The migration methods use a lock on the database level and parallel calls are executed serially.
This means that you can safely call `migrateToLatest` and other migration methods from multiple
server instances simultaneously and the migrations are guaranteed to only be executed once.
server instances simultaneously and the migrations are guaranteed to only be executed once. The
locks are also automatically released if the migration process crashes or the connection to the
database fails.

# Deno

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "kysely",
"version": "0.16.8",
"version": "0.16.9",
"description": "Type safe SQL query builder",
"repository": {
"type": "git",
Expand Down
77 changes: 70 additions & 7 deletions src/migration/migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,38 @@ export interface Migration {
* ```
*/
export class Migrator {
readonly #props: MigratorProps
readonly #props: InternalMigratorProps

constructor(props: MigratorProps) {
this.#props = freeze(props)
}

/**
* Returns a {@link MigrationInfo} object for each migration.
*
* The returned array is sorted by migration name.
*/
async getMigrations(): Promise<ReadonlyArray<MigrationInfo>> {
const executedMigrations = (await this.#doesTableExists(MIGRATION_TABLE))
? await this.#props.db
.selectFrom(MIGRATION_TABLE)
.select(['name', 'timestamp'])
.execute()
: []

const migrations = await this.#resolveMigrations()

return migrations.map(({ name, ...migration }) => {
const executed = executedMigrations.find((it) => it.name === name)

return {
name,
migration,
executedAt: executed ? new Date(executed.timestamp) : undefined,
}
})
}

/**
* Runs all migrations that have not yet been run.
*
Expand Down Expand Up @@ -250,7 +276,7 @@ export class Migrator {
try {
await this.#props.db
.insertInto(MIGRATION_LOCK_TABLE)
.values({ id: MIGRATION_LOCK_ID })
.values({ id: MIGRATION_LOCK_ID, is_locked: 0 })
.execute()
} catch (error) {
if (!(await this.#doesLockRowExists())) {
Expand Down Expand Up @@ -281,7 +307,9 @@ export class Migrator {
async #runMigrations(
getTargetMigrationIndex: (state: MigrationState) => number | undefined
): Promise<MigrationResultSet> {
const run = async (db: Kysely<any>): Promise<MigrationResultSet> => {
const run = async (
db: Kysely<MigrationTables>
): Promise<MigrationResultSet> => {
try {
await this.#props.db[PRIVATE_ADAPTER].acquireMigrationLock(db)

Expand Down Expand Up @@ -316,7 +344,7 @@ export class Migrator {
}
}

async #getState(db: Kysely<any>): Promise<MigrationState> {
async #getState(db: Kysely<MigrationTables>): Promise<MigrationState> {
const migrations = await this.#resolveMigrations()
const executedMigrations = await this.#getExecutedMigrations(db)

Expand All @@ -342,7 +370,7 @@ export class Migrator {
}

async #getExecutedMigrations(
db: Kysely<any>
db: Kysely<MigrationTables>
): Promise<ReadonlyArray<string>> {
const executedMigrations = await db
.selectFrom(MIGRATION_TABLE)
Expand Down Expand Up @@ -378,7 +406,7 @@ export class Migrator {
}

async #migrateDown(
db: Kysely<any>,
db: Kysely<MigrationTables>,
state: MigrationState,
targetIndex: number
): Promise<MigrationResultSet> {
Expand Down Expand Up @@ -429,7 +457,7 @@ export class Migrator {
}

async #migrateUp(
db: Kysely<any>,
db: Kysely<MigrationTables>,
state: MigrationState,
targetIndex: number
): Promise<MigrationResultSet> {
Expand Down Expand Up @@ -569,6 +597,41 @@ export interface NoMigrations {
readonly __noMigrations__: true
}

export interface MigrationInfo {
/**
* Name of the migration.
*/
name: string

/**
* The actual migration.
*/
migration: Migration

/**
* When was the migration executed.
*
* If this is undefined, the migration hasn't been executed yet.
*/
executedAt?: Date
}

interface InternalMigratorProps {
readonly db: Kysely<MigrationTables>
readonly provider: MigrationProvider
}

interface MigrationTables {
[MIGRATION_TABLE]: {
name: string
timestamp: string
}
[MIGRATION_LOCK_TABLE]: {
id: string
is_locked: 0 | 1
}
}

interface NamedMigration extends Migration {
readonly name: string
}
Expand Down
30 changes: 30 additions & 0 deletions test/node/src/migration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,36 @@ for (const dialect of BUILT_IN_DIALECTS) {
await destroyTest(ctx)
})

describe('getMigrations', () => {
it('should get migrations', async () => {
const [migrator] = createMigrations([
'migration1',
'migration2',
'migration3',
])

const migrations1 = await migrator.getMigrations()
expect(migrations1).to.have.length(3)
expect(migrations1[0].name).to.equal('migration1')
expect(migrations1[0].executedAt).to.equal(undefined)
expect(migrations1[1].name).to.equal('migration2')
expect(migrations1[1].executedAt).to.equal(undefined)
expect(migrations1[2].name).to.equal('migration3')
expect(migrations1[2].executedAt).to.equal(undefined)

await migrator.migrateTo('migration2')

const migrations2 = await migrator.getMigrations()
expect(migrations2).to.have.length(3)
expect(migrations2[0].name).to.equal('migration1')
expect(migrations2[0].executedAt).to.be.instanceOf(Date)
expect(migrations2[1].name).to.equal('migration2')
expect(migrations2[1].executedAt).to.be.instanceOf(Date)
expect(migrations2[2].name).to.equal('migration3')
expect(migrations2[2].executedAt).to.equal(undefined)
})
})

describe('migrateToLatest', () => {
it('should run all unexecuted migrations', async () => {
const [migrator1, executedUpMethods1] = createMigrations([
Expand Down

0 comments on commit 05d5636

Please sign in to comment.