Skip to content

Commit a285e4c

Browse files
committed
Merge remote-tracking branch 'upstream/master'
2 parents 0cacfca + b83108d commit a285e4c

9 files changed

Lines changed: 69 additions & 51 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:
77

88
#### 7.x Releases
99

10-
- `7.11.x` Releases - [7.11.0](#7110)
10+
- `7.11.x` Releases - [7.11.0](#7110) - [7.11.1](#7111)
1111
- `7.10.x` Releases - [7.10.0](#7100)
1212
- `7.9.x` Releases - [7.9.0](#790)
1313
- `7.8.x` Releases - [7.8.0](#780)
@@ -143,6 +143,12 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:
143143

144144
---
145145

146+
## 7.11.1
147+
148+
Released June 18, 2026
149+
150+
- **New**: Make migration registration an O(1) operation [@MarkusSintonen](https://github.com/MarkusSintonen) in [#1869](https://github.com/groue/GRDB.swift/pull/1869)
151+
146152
## 7.11.0
147153

148154
Released June 1, 2026

GRDB.swift.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = 'GRDB.swift'
3-
s.version = '7.11.0'
3+
s.version = '7.11.1'
44

55
s.license = { :type => 'MIT', :file => 'LICENSE' }
66
s.summary = 'A toolkit for SQLite databases, with a focus on application development.'

GRDB/Core/DatabaseFunction.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import SQLCipher
1010
import GRDBSQLite
1111
#endif
1212

13+
import Foundation
14+
1315
/// A custom SQL function or aggregate.
1416
///
1517
/// ## Topics

GRDB/Core/Support/StandardLibrary/StandardLibrary.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import SQLCipher
1010
import GRDBSQLite
1111
#endif
1212

13+
import Foundation
14+
1315
// MARK: - Value Types
1416

1517
/// Bool adopts DatabaseValueConvertible and StatementColumnConvertible.

GRDB/Migration/DatabaseMigrator.swift

Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ public struct DatabaseMigrator: Sendable {
111111
/// See also ``hasSchemaChanges(_:)``.
112112
public var eraseDatabaseOnSchemaChange = false
113113
private var defersForeignKeyChecks = true
114-
private var _migrations: [Migration] = []
114+
private var _migrations: OrderedDictionary<String, Migration> = [:]
115115

116116
/// A new migrator.
117117
public init() {
@@ -349,10 +349,10 @@ public struct DatabaseMigrator: Sendable {
349349
/// - parameter writer: A DatabaseWriter.
350350
/// - throws: The error thrown by the first failed migration.
351351
public func migrate(_ writer: any DatabaseWriter) throws {
352-
guard let lastMigration = _migrations.last else {
352+
guard let lastMigrationIdentifier = _migrations.keys.last else {
353353
return
354354
}
355-
try migrate(writer, upTo: lastMigration.identifier)
355+
try migrate(writer, upTo: lastMigrationIdentifier)
356356
}
357357

358358
/// Runs all unapplied migrations, in the same order as they
@@ -387,8 +387,8 @@ public struct DatabaseMigrator: Sendable {
387387
writer.asyncBarrierWriteWithoutTransaction { dbResult in
388388
do {
389389
let db = try dbResult.get()
390-
if let lastMigration = _migrations.last {
391-
try migrate(db, upTo: lastMigration.identifier)
390+
if let lastMigrationIdentifier = _migrations.keys.last {
391+
try migrate(db, upTo: lastMigrationIdentifier)
392392
}
393393
completion(.success(db))
394394
} catch {
@@ -426,14 +426,13 @@ public struct DatabaseMigrator: Sendable {
426426
///
427427
public func hasSchemaChanges(_ db: Database) throws -> Bool {
428428
let appliedIdentifiers = try appliedIdentifiers(db)
429-
let knownIdentifiers = Set(_migrations.map { $0.identifier })
429+
let knownIdentifiers = Set(_migrations.keys)
430430
if !appliedIdentifiers.isSubset(of: knownIdentifiers) {
431431
// Database contains an unknown migration
432432
return true
433433
}
434434

435-
if let lastAppliedIdentifier = _migrations
436-
.map(\.identifier)
435+
if let lastAppliedIdentifier = _migrations.keys
437436
.last(where: { appliedIdentifiers.contains($0) })
438437
{
439438
// Some migrations were already applied.
@@ -492,7 +491,7 @@ public struct DatabaseMigrator: Sendable {
492491
/// The list of registered migration identifiers, in the same order as they
493492
/// have been registered.
494493
public var migrations: [String] {
495-
_migrations.map(\.identifier)
494+
_migrations.keys
496495
}
497496

498497
/// Returns the identifiers of registered and applied migrations, in the
@@ -502,7 +501,7 @@ public struct DatabaseMigrator: Sendable {
502501
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
503502
public func appliedMigrations(_ db: Database) throws -> [String] {
504503
let appliedIdentifiers = try self.appliedIdentifiers(db)
505-
return _migrations.map { $0.identifier }.filter { appliedIdentifiers.contains($0) }
504+
return _migrations.keys.filter { appliedIdentifiers.contains($0) }
506505
}
507506

508507
/// Returns the applied migration identifiers, even unregistered ones.
@@ -531,7 +530,7 @@ public struct DatabaseMigrator: Sendable {
531530
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
532531
public func completedMigrations(_ db: Database) throws -> [String] {
533532
let appliedIdentifiers = try appliedMigrations(db)
534-
let knownIdentifiers = _migrations.map(\.identifier)
533+
let knownIdentifiers = _migrations.keys
535534
return zip(appliedIdentifiers, knownIdentifiers)
536535
.prefix(while: { (applied: String, known: String) in applied == known })
537536
.map { $0.0 }
@@ -543,7 +542,7 @@ public struct DatabaseMigrator: Sendable {
543542
/// - parameter db: A database connection.
544543
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
545544
public func hasCompletedMigrations(_ db: Database) throws -> Bool {
546-
try completedMigrations(db).last == _migrations.last?.identifier
545+
try completedMigrations(db).last == _migrations.keys.last
547546
}
548547

549548
/// A boolean value indicating whether the database refers to
@@ -556,7 +555,7 @@ public struct DatabaseMigrator: Sendable {
556555
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
557556
public func hasBeenSuperseded(_ db: Database) throws -> Bool {
558557
let appliedIdentifiers = try self.appliedIdentifiers(db)
559-
let knownIdentifiers = _migrations.map(\.identifier)
558+
let knownIdentifiers = _migrations.keys
560559
return appliedIdentifiers.contains { !knownIdentifiers.contains($0) }
561560
}
562561

@@ -574,51 +573,53 @@ public struct DatabaseMigrator: Sendable {
574573

575574
private mutating func registerMigration(_ migration: Migration) {
576575
GRDBPrecondition(
577-
!_migrations.map({ $0.identifier }).contains(migration.identifier),
576+
_migrations[migration.identifier] == nil,
578577
"already registered migration: \(String(reflecting: migration.identifier))")
579-
_migrations.append(migration)
578+
_migrations.appendValue(migration, forKey: migration.identifier)
580579
}
581580

582581
/// Returns unapplied migration executions
583582
private func unappliedExecutions(upTo targetIdentifier: String, appliedIdentifiers: Set<String>) -> [Execution] {
584-
var expectedMigrations: [Migration] = []
585-
for migration in _migrations {
586-
expectedMigrations.append(migration)
587-
if migration.identifier == targetIdentifier {
588-
break
589-
}
590-
}
591-
592-
// targetIdentifier must refer to a registered migration
593-
GRDBPrecondition(
594-
expectedMigrations.last?.identifier == targetIdentifier,
595-
"undefined migration: \(String(reflecting: targetIdentifier))")
583+
var executions: [Execution] = []
584+
var foundTarget = false
596585

597-
return expectedMigrations.compactMap { migration in
598-
if appliedIdentifiers.contains(migration.identifier) {
599-
if migration.mergedIdentifiers.isDisjoint(with: appliedIdentifiers) {
600-
// Nothing to do
601-
return nil
602-
} else {
586+
for (identifier, migration) in _migrations {
587+
if appliedIdentifiers.contains(identifier) {
588+
if !migration.mergedIdentifiers.isDisjoint(with: appliedIdentifiers) {
603589
// Migration is applied, but we have some merged identifiers to delete
604-
return Execution(migration: migration, mode: .deleteMergedIdentifiers)
590+
executions.append(Execution(migration: migration, mode: .deleteMergedIdentifiers))
605591
}
606592
} else {
607593
// Migration is not applied yet.
608594
let appliedMergedIdentifiers = migration.mergedIdentifiers.intersection(appliedIdentifiers)
609-
return Execution(migration: migration, mode: .run(mergedIdentifiers: appliedMergedIdentifiers))
595+
executions.append(Execution(
596+
migration: migration,
597+
mode: .run(mergedIdentifiers: appliedMergedIdentifiers)
598+
))
599+
}
600+
601+
if identifier == targetIdentifier {
602+
foundTarget = true
603+
break
610604
}
611605
}
606+
607+
// targetIdentifier must refer to a registered migration
608+
GRDBPrecondition(
609+
foundTarget,
610+
"undefined migration: \(String(reflecting: targetIdentifier))")
611+
612+
return executions
612613
}
613614

614615
private func runMigrations(_ db: Database, upTo targetIdentifier: String) throws {
615616
try db.execute(sql: "CREATE TABLE IF NOT EXISTS grdb_migrations (identifier TEXT NOT NULL PRIMARY KEY)")
616617

617618
// Subsequent migration must not be applied
618619
let appliedMigrations = try self.appliedMigrations(db) // Only known ids
619-
if let targetIndex = _migrations.firstIndex(where: { $0.identifier == targetIdentifier }),
620+
if let targetIndex = _migrations.keys.firstIndex(of: targetIdentifier),
620621
let lastAppliedMigration = appliedMigrations.last,
621-
let lastAppliedIndex = _migrations.firstIndex(where: { $0.identifier == lastAppliedMigration }),
622+
let lastAppliedIndex = _migrations.keys.firstIndex(of: lastAppliedMigration),
622623
targetIndex < lastAppliedIndex
623624
{
624625
fatalError("database is already migrated beyond migration \(String(reflecting: targetIdentifier))")

Package.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ let package = Package(
7777
path: "GRDB",
7878
resources: [.copy("PrivacyInfo.xcprivacy")],
7979
cSettings: cSettings,
80-
swiftSettings: swiftSettings),
80+
swiftSettings: swiftSettings + [
81+
.enableUpcomingFeature("MemberImportVisibility"),
82+
]),
8183
.testTarget(
8284
name: "GRDBTests",
8385
dependencies: ["GRDB"],

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ The [SQLCipher community forum](https://discuss.zetetic.net/c/sqlcipher) is a go
2424
<a href="https://github.com/groue/GRDB.swift/actions/workflows/CI.yml"><img alt="CI Status" src="https://github.com/groue/GRDB.swift/actions/workflows/CI.yml/badge.svg?branch=master"></a>
2525
</p>
2626

27-
**Latest release**: June 1, 2026 • [version 7.11.0](https://github.com/groue/GRDB.swift/tree/v7.11.0) • [CHANGELOG](CHANGELOG.md) • [Migrating From GRDB 6 to GRDB 7](Documentation/GRDB7MigrationGuide.md)
27+
**Latest release**: June 18, 2026 • [version 7.11.1](https://github.com/groue/GRDB.swift/tree/v7.11.1) • [CHANGELOG](CHANGELOG.md) • [Migrating From GRDB 6 to GRDB 7](Documentation/GRDB7MigrationGuide.md)
2828

2929
**Requirements**: iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 7.0+ &bull; SQLite 3.20.0+ &bull; Swift 6.1+ / Xcode 16.3+
3030

Support/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<key>CFBundlePackageType</key>
1616
<string>FMWK</string>
1717
<key>CFBundleShortVersionString</key>
18-
<string>7.11.0</string>
18+
<string>7.11.1</string>
1919
<key>CFBundleSignature</key>
2020
<string>????</string>
2121
<key>CFBundleVersion</key>

Tests/GRDBTests/ValueObservation/ValueObservationTests.swift

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,18 +1016,23 @@ class ValueObservationTests: GRDBTestCase {
10161016
return "cancelled loop"
10171017
}
10181018

1019-
// Lanch the task that cancels
1019+
// Launch the task that cancels
10201020
Task {
1021-
let observation = ValueObservation.trackingConstantRegion(Table("t").fetchCount)
1022-
for try await count in observation.values(in: writer) {
1023-
if count >= 3 {
1024-
cancelledTask.cancel()
1025-
break
1026-
} else {
1027-
try await writer.write {
1028-
try $0.execute(sql: "INSERT INTO t DEFAULT VALUES")
1021+
do {
1022+
let observation = ValueObservation.trackingConstantRegion(Table("t").fetchCount)
1023+
for try await count in observation.values(in: writer) {
1024+
if count >= 3 {
1025+
cancelledTask.cancel()
1026+
break
1027+
} else {
1028+
try await writer.write {
1029+
try $0.execute(sql: "INSERT INTO t DEFAULT VALUES")
1030+
}
10291031
}
10301032
}
1033+
} catch {
1034+
XCTFail("Unexpected error: \(error)")
1035+
cancelledTask.cancel()
10311036
}
10321037
}
10331038

0 commit comments

Comments
 (0)