@@ -18,6 +18,7 @@ import Foundation
1818/// ### Registering Migrations
1919///
2020/// - ``registerMigration(_:foreignKeyChecks:migrate:)``
21+ /// - ``registerMigration(_:foreignKeyChecks:merging:migrate:)``
2122/// - ``ForeignKeyChecks``
2223///
2324/// ### Configuring a DatabaseMigrator
@@ -161,9 +162,8 @@ public struct DatabaseMigrator: Sendable {
161162
162163 /// Registers a migration.
163164 ///
164- /// The registered migration is appended to the list of migrations to run:
165- /// it will execute after previously registered migrations, and before
166- /// migrations that are registered later.
165+ /// The registered migration is appended to the list of migrations. It
166+ /// will execute after previously registered migrations.
167167 ///
168168 /// For example:
169169 ///
@@ -214,6 +214,107 @@ public struct DatabaseMigrator: Sendable {
214214 _ identifier: String ,
215215 foreignKeyChecks: ForeignKeyChecks = . deferred,
216216 migrate: @escaping @Sendable ( Database) throws -> Void )
217+ {
218+ registerMigration ( identifier, foreignKeyChecks: foreignKeyChecks, merging: [ ] ) { db, ids in
219+ precondition ( ids. isEmpty)
220+ try migrate ( db)
221+ }
222+ }
223+
224+ /// Registers a merged migration.
225+ ///
226+ /// Like migrations registered with ``registerMigration(_:foreignKeyChecks:migrate:)``,
227+ /// the merged migration is appended to the list of migrations. It
228+ /// will execute after previously registered migrations.
229+ ///
230+ /// A merged migration merges and replaces a set of migrations defined
231+ /// in a previous version of the application. For example, to merge the
232+ /// migrations "v1", "v2" and "v3", redefine the "v3" migration so that
233+ /// it merges "v1" and "v2", as in the example below.
234+ ///
235+ /// The second argument of the `migrate` closure is the subset of merged
236+ /// migrations that have already been applied when the merged
237+ /// migration runs.
238+ ///
239+ /// ```swift
240+ /// // Old code
241+ /// migrator.registerMigration("v1") { db in
242+ /// // Apply schema version 1
243+ /// }
244+ /// migrator.registerMigration("v2") { db in
245+ /// // Apply schema version 2
246+ /// }
247+ /// migrator.registerMigration("v3") { db in
248+ /// // Apply schema version 3
249+ /// }
250+ ///
251+ /// // New code:
252+ /// // - Migrations v1 and v2 are deleted.
253+ /// // - Migration v3 is redefined and merges v1 and v2:
254+ /// migrator.registerMigration("v3", merging: ["v1", "v2"]) { db, appliedIDs in
255+ /// if !appliedIDs.contains("v1") {
256+ /// // Apply schema version 1
257+ /// }
258+ /// if !appliedIDs.contains("v2") {
259+ /// // Apply schema version 2
260+ /// }
261+ /// // Apply schema version 3
262+ /// }
263+ /// ```
264+ ///
265+ /// In the above sample code, the merged migration is named like the
266+ /// last one of the merged set. You can also give it a brand new name,
267+ /// as in the alternative below. Notice the different logic in the
268+ /// migration code.
269+ ///
270+ /// **In all cases avoid naming the merged migration like the first
271+ /// elements in the merged set** (`v1` or `v2` in our example).
272+ ///
273+ /// ```swift
274+ /// // Alternative new code:
275+ /// // - Migrations v1, v2 and v3 are deleted.
276+ /// // - The new migration v3-new merges v1, v2 and v3:
277+ /// migrator.registerMigration("v3-new", merging: ["v1", "v2", "v3"]) { db, appliedIDs in
278+ /// if !appliedIDs.contains("v1") {
279+ /// // Apply schema version 1
280+ /// }
281+ /// if !appliedIDs.contains("v2") {
282+ /// // Apply schema version 2
283+ /// }
284+ /// if !appliedIDs.contains("v3") {
285+ /// // Apply schema version 3
286+ /// }
287+ /// }
288+ /// ```
289+ ///
290+ /// - parameters:
291+ /// - identifier: The migration identifier.
292+ /// - mergedIdentifiers: A set of previous migration identifiers
293+ /// that are merged in this migration.
294+ /// - foreignKeyChecks: This parameter is ignored if the database
295+ /// ``Configuration`` has disabled foreign keys.
296+ ///
297+ /// The default `.deferred` checks have the migration run with
298+ /// disabled foreign keys, until foreign keys are checked right before
299+ /// changes are committed on disk. These deferred checks are not
300+ /// executed if the migrator is the result of
301+ /// ``disablingDeferredForeignKeyChecks()``.
302+ ///
303+ /// The `.immediate` checks have the migration run with foreign
304+ /// keys enabled. Make sure you only use `.immediate` if the migration
305+ /// does not perform schema changes described in
306+ /// <https://www.sqlite.org/lang_altertable.html#making_other_kinds_of_table_schema_changes>
307+ /// - migrate: A closure that performs database operations. The
308+ /// first argument is a database connection. The second argument
309+ /// is the set of previous migrations that has been applied when
310+ /// the merged migration runs.
311+ /// - precondition: No migration with the same identifier as already
312+ /// been registered.
313+ public mutating func registerMigration(
314+ _ identifier: String ,
315+ foreignKeyChecks: ForeignKeyChecks = . deferred,
316+ merging mergedIdentifiers: Set < String > = [ ] ,
317+ migrate: @escaping @Sendable ( _ db: Database , _ appliedIdentifiers: Set < String > ) throws -> Void )
217318 {
218319 let migrationChecks : Migration . ForeignKeyChecks
219320 switch foreignKeyChecks {
@@ -226,7 +327,11 @@ public struct DatabaseMigrator: Sendable {
226327 case . immediate:
227328 migrationChecks = . immediate
228329 }
229- registerMigration ( Migration ( identifier: identifier, foreignKeyChecks: migrationChecks, migrate: migrate) )
330+ registerMigration ( Migration (
331+ identifier: identifier,
332+ mergedIdentifiers: mergedIdentifiers,
333+ foreignKeyChecks: migrationChecks,
334+ migrate: migrate) )
230335 }
231336
232337 // MARK: - Applying Migrations
@@ -450,15 +555,25 @@ public struct DatabaseMigrator: Sendable {
450555
451556 // MARK: - Non public
452557
558+ private struct Execution {
559+ enum Mode {
560+ case run( mergedIdentifiers: Set < String > )
561+ case deleteMergedIdentifiers
562+ }
563+
564+ var migration : Migration
565+ var mode : Mode
566+ }
567+
453568 private mutating func registerMigration( _ migration: Migration ) {
454569 GRDBPrecondition (
455570 !_migrations. map ( { $0. identifier } ) . contains ( migration. identifier) ,
456571 " already registered migration: \( String ( reflecting: migration. identifier) ) " )
457572 _migrations. append ( migration)
458573 }
459574
460- /// Returns unapplied migration identifier,
461- private func unappliedMigrations ( upTo targetIdentifier: String , appliedIdentifiers: [ String ] ) -> [ Migration ] {
575+ /// Returns unapplied migration executions
576+ private func unappliedExecutions ( upTo targetIdentifier: String , appliedIdentifiers: Set < String > ) -> [ Execution ] {
462577 var expectedMigrations : [ Migration ] = [ ]
463578 for migration in _migrations {
464579 expectedMigrations. append ( migration)
@@ -472,32 +587,52 @@ public struct DatabaseMigrator: Sendable {
472587 expectedMigrations. last? . identifier == targetIdentifier,
473588 " undefined migration: \( String ( reflecting: targetIdentifier) ) " )
474589
475- return expectedMigrations. filter { !appliedIdentifiers. contains ( $0. identifier) }
590+ return expectedMigrations. compactMap { migration in
591+ if appliedIdentifiers. contains ( migration. identifier) {
592+ if migration. mergedIdentifiers. isDisjoint ( with: appliedIdentifiers) {
593+ // Nothing to do
594+ return nil
595+ } else {
596+ // Migration is applied, but we have some merged identifiers to delete
597+ return Execution ( migration: migration, mode: . deleteMergedIdentifiers)
598+ }
599+ } else {
600+ // Migration is not applied yet.
601+ let appliedMergedIdentifiers = migration. mergedIdentifiers. intersection ( appliedIdentifiers)
602+ return Execution ( migration: migration, mode: . run( mergedIdentifiers: appliedMergedIdentifiers) )
603+ }
604+ }
476605 }
477606
478607 private func runMigrations( _ db: Database , upTo targetIdentifier: String ) throws {
479608 try db. execute ( sql: " CREATE TABLE IF NOT EXISTS grdb_migrations (identifier TEXT NOT NULL PRIMARY KEY) " )
480- let appliedIdentifiers = try self . appliedMigrations ( db)
481609
482610 // Subsequent migration must not be applied
611+ let appliedMigrations = try self . appliedMigrations ( db) // Only known ids
483612 if let targetIndex = _migrations. firstIndex ( where: { $0. identifier == targetIdentifier } ) ,
484- let lastAppliedIdentifier = appliedIdentifiers . last,
485- let lastAppliedIndex = _migrations. firstIndex ( where: { $0. identifier == lastAppliedIdentifier } ) ,
613+ let lastAppliedMigration = appliedMigrations . last,
614+ let lastAppliedIndex = _migrations. firstIndex ( where: { $0. identifier == lastAppliedMigration } ) ,
486615 targetIndex < lastAppliedIndex
487616 {
488617 fatalError ( " database is already migrated beyond migration \( String ( reflecting: targetIdentifier) ) " )
489618 }
490619
491- let unappliedMigrations = self . unappliedMigrations (
620+ let appliedIdentifiers = try self . appliedIdentifiers ( db) // All ids, even unknown ones
621+ let unappliedExecutions = self . unappliedExecutions (
492622 upTo: targetIdentifier,
493623 appliedIdentifiers: appliedIdentifiers)
494624
495- if unappliedMigrations . isEmpty {
625+ if unappliedExecutions . isEmpty {
496626 return
497627 }
498628
499- for migration in unappliedMigrations {
500- try migration. run ( db)
629+ for execution in unappliedExecutions {
630+ switch execution. mode {
631+ case . run( let mergedIdentifiers) :
632+ try execution. migration. run ( db, mergedIdentifiers: mergedIdentifiers)
633+ case . deleteMergedIdentifiers:
634+ try execution. migration. deleteMergedIdentifiers ( db)
635+ }
501636 }
502637 }
503638
0 commit comments