Skip to content

Commit 1923a30

Browse files
committed
wrap migrations in a transaction, enable exclusive locking
1 parent 3a82718 commit 1923a30

3 files changed

Lines changed: 51 additions & 20 deletions

File tree

drivers/sqlite.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,19 @@ class Torm extends TormBase<sqlite3.DatabaseSync> {
350350
}
351351
}
352352

353+
public override transaction = <T>(fn: () => T) => (): T => {
354+
try {
355+
this.driver.exec('BEGIN TRANSACTION')
356+
const result = fn()
357+
this.driver.exec('COMMIT')
358+
return result
359+
} catch(e) {
360+
this.driver.exec('ROLLBACK')
361+
throw e
362+
}
363+
}
364+
365+
353366
private get_backup_name(folder: string, name: string, suffix?: number): string {
354367
const now = new Date()
355368
let backup_name = `${now.getUTCFullYear()}-${now.getUTCMonth().toString().padStart(2, '0')}-${now.getUTCDay().toString().padStart(2, '0')}_${name}`

src/migration.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,16 @@ class MigrationsManager {
159159
}
160160
const next_version = upgrade_versions.find(version => version > current_version)
161161
if (next_version === undefined) throw new Error('No new version exists. Database is up to date')
162-
// TODO put a transaction around this block
162+
163163
for (const migration of migration_map.get(next_version)!) {
164164
migration.prepare_queries()
165-
migration.call()
165+
if (migration.TRANSACTION) {
166+
this.#torm.transaction(() => {
167+
migration.call()
168+
})()
169+
} else {
170+
migration.call()
171+
}
166172
}
167173
this.#torm.schemas.unsafe_version_set(next_version)
168174

@@ -225,6 +231,8 @@ interface MigrationClass {
225231
}
226232

227233
interface MigrationInstance extends ModelBase {
234+
/** Whether or not to use a transaction when running this migration. Disabling transactions around migrations should only be done if you know what you are doing. */
235+
TRANSACTION: boolean
228236
version: Version
229237
call: (driver?: Driver) => void
230238
is_seed_migration(): boolean
@@ -233,6 +241,7 @@ interface MigrationInstance extends ModelBase {
233241
abstract class MigrationBase extends ModelBase implements MigrationInstance {
234242
public abstract version: Version
235243
public abstract call(driver?: Driver): void
244+
public TRANSACTION: boolean = true
236245

237246
public is_seed_migration(): boolean {
238247
return false

src/torm.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -123,27 +123,34 @@ abstract class TormBase<D extends Driver> {
123123
this.migrations_manager.initialize_database()
124124
}
125125

126-
if (auto_migrate) {
127-
while (this.migrations_manager.is_database_outdated()) {
128-
this.status = 'outdated'
129-
const current_version = this.schemas.version()
130-
const migration_operation: MigrationOperation = {
131-
start_version: current_version,
132-
backup: false,
133-
next_version: -1,
134-
}
135-
if (backup_before_migrate) {
136-
if (!options?.backups?.folder) {
137-
throw new Error(`backups_folder must be defined in order to use automatic backups`)
126+
if (auto_migrate && this.migrations_manager.is_database_outdated()) {
127+
try {
128+
driver.exec(`PRAGMA locking_mode = EXCLUSIVE`)
129+
130+
while (this.migrations_manager.is_database_outdated()) {
131+
this.status = 'outdated'
132+
const current_version = this.schemas.version()
133+
const migration_operation: MigrationOperation = {
134+
start_version: current_version,
135+
backup: false,
136+
next_version: -1,
137+
}
138+
if (backup_before_migrate) {
139+
if (!options?.backups?.folder) {
140+
throw new Error(`backups_folder must be defined in order to use automatic backups`)
141+
}
142+
migration_operation.backup = true
143+
this.backup(options.backups.folder, `migration_backup_v${current_version}`)
138144
}
139-
migration_operation.backup = true
140-
this.backup(options.backups.folder, `migration_backup_v${current_version}`)
145+
const next_version = this.migrations_manager.upgrade_database()
146+
migration_operation.next_version = next_version
147+
migration_operations.push(migration_operation)
141148
}
142-
const next_version = this.migrations_manager.upgrade_database()
143-
migration_operation.next_version = next_version
144-
migration_operations.push(migration_operation)
149+
this.initialize_models()
150+
151+
} finally {
152+
driver.exec(`PRAGMA locking_mode = normal`)
145153
}
146-
this.initialize_models()
147154
}
148155
if (this.migrations_manager.is_database_outdated()) {
149156
this.status = 'outdated'
@@ -164,6 +171,8 @@ abstract class TormBase<D extends Driver> {
164171

165172
public abstract backup(folder: string, name: string): void
166173

174+
public abstract transaction<T>(fn: () => T): () => T
175+
167176
private initialize_models = () => {
168177
for (const model of this.model_registry) {
169178
model.prepare_queries()

0 commit comments

Comments
 (0)