Skip to content

Commit

Permalink
Merge branch 'development'
Browse files Browse the repository at this point in the history
  • Loading branch information
groue committed Jan 21, 2024
2 parents 181ccd8 + c791e37 commit e069e27
Show file tree
Hide file tree
Showing 12 changed files with 315 additions and 39 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Expand Up @@ -7,7 +7,7 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:

#### 6.x Releases

- `6.24.x` Releases - [6.24.0](#6240) - [6.24.1](#6241)
- `6.24.x` Releases - [6.24.0](#6240) - [6.24.1](#6241) - [6.24.2](#6242)
- `6.23.x` Releases - [6.23.0](#6230)
- `6.22.x` Releases - [6.22.0](#6220)
- `6.21.x` Releases - [6.21.0](#6210)
Expand Down Expand Up @@ -122,6 +122,13 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:

---

## 6.24.2

Released January 21, 2024

- **Documentation Update**: [#1485](https://github.com/groue/GRDB.swift/pull/1485) The [Sharing a Database](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasesharing) guide was updated with a new recommendation for databases shared between multiple processes. Writers should always perform IMMEDIATE transactions in order to avoid the `SQLITE_BUSY` error that can occur when transactions overlap. The new recommendation fits in a single line of code: `configuration.defaultTransactionKind = .immediate`.
- **New**: Associations that involve views instead of tables were already supported, with an explicit `ForeignKey` in their definition. When the foreign key is missing, a clear diagnostic message is now emitted, instead of an unhelpful "no such table" runtime error.

## 6.24.1

Released January 6, 2024
Expand Down
Expand Up @@ -25,7 +25,7 @@
<key>NSExtensionAttributes</key>
<dict>
<key>WKAppBundleIdentifier</key>
<string>com.github.groue.GRDBDemoiOS.watchkitapp</string>
<string>com.github.groue.GRDBDemoiOS2.watchkitapp</string>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.watchkit</string>
Expand Down
Expand Up @@ -26,7 +26,7 @@
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>WKCompanionAppBundleIdentifier</key>
<string>com.github.groue.GRDBDemoiOS</string>
<string>com.github.groue.GRDBDemoiOS2</string>
<key>WKWatchKitApp</key>
<true/>
</dict>
Expand Down
Expand Up @@ -640,7 +640,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS.watchkitapp.watchkitextension;
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS2.watchkitapp.watchkitextension;
PRODUCT_NAME = "${TARGET_NAME}";
SDKROOT = watchos;
SKIP_INSTALL = YES;
Expand All @@ -660,7 +660,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS.watchkitapp.watchkitextension;
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS2.watchkitapp.watchkitextension;
PRODUCT_NAME = "${TARGET_NAME}";
SDKROOT = watchos;
SKIP_INSTALL = YES;
Expand All @@ -677,7 +677,7 @@
DEVELOPMENT_TEAM = AMD8W895CT;
IBSC_MODULE = GRDBDemoWatchOS_Extension;
INFOPLIST_FILE = GRDBDemoWatchOS/Info.plist;
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS.watchkitapp;
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS2.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
SKIP_INSTALL = YES;
Expand All @@ -694,7 +694,7 @@
DEVELOPMENT_TEAM = AMD8W895CT;
IBSC_MODULE = GRDBDemoWatchOS_Extension;
INFOPLIST_FILE = GRDBDemoWatchOS/Info.plist;
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS.watchkitapp;
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS2.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
SKIP_INSTALL = YES;
Expand Down Expand Up @@ -832,7 +832,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS;
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS2;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
Expand All @@ -849,7 +849,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS;
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS2;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
Expand Down
2 changes: 1 addition & 1 deletion GRDB.swift.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'GRDB.swift'
s.version = '6.24.1'
s.version = '6.24.2'

s.license = { :type => 'MIT', :file => 'LICENSE' }
s.summary = 'A toolkit for SQLite databases, with a focus on application development.'
Expand Down
5 changes: 4 additions & 1 deletion GRDB/Documentation.docc/DatabaseSharing.md
Expand Up @@ -152,11 +152,14 @@ If several processes want to write in the database, configure the database pool

```swift
var configuration = Configuration()
configuration.defaultTransactionKind = .immediate
configuration.busyMode = .timeout(/* a TimeInterval */)
let dbPool = try DatabasePool(path: ..., configuration: configuration)
```

With such a setup, you may still get `SQLITE_BUSY` errors from all write operations. They will occur if the database remains locked by another process for longer than the specified timeout. You can catch those errors:
Both the `defaultTransactionKind` and `busyMode` are important for preventing `SQLITE_BUSY`. The `immediate` transaction kind prevents write transactions from overlapping, and the busy timeout has write transactions wait, instead of throwing `SQLITE_BUSY`, whenever another process is writing.

With such a setup, you will still get `SQLITE_BUSY` errors if the database remains locked by another process for longer than the specified timeout. You can catch those errors:

```swift
do {
Expand Down
108 changes: 82 additions & 26 deletions GRDB/QueryInterface/SQL/SQLForeignKeyRequest.swift
Expand Up @@ -32,41 +32,76 @@ struct SQLForeignKeyRequest {
}

// Incomplete information: let's look for schema foreign keys
let foreignKeys = try db.foreignKeys(on: originTable).filter { foreignKey in
if destinationTable.lowercased() != foreignKey.destinationTable.lowercased() {
return false
//
// But maybe the tables are views. In this case, don't throw
// "no such table" error, because this is confusing for the user,
// as discovered in <https://github.com/groue/GRDB.swift/discussions/1481>.
// Instead, we'll crash with a clear message.

guard let originType = try tableType(db, for: originTable) else {
throw DatabaseError.noSuchTable(originTable)
}

if originType.isView {
if originColumns == nil {
fatalError("""
Could not infer foreign key from '\(originTable)' \
to '\(destinationTable)'. To fix this error, provide an \
explicit `ForeignKey` in the association definition.
""")
}
if let originColumns {
let originColumns = Set(originColumns.lazy.map { $0.lowercased() })
let foreignKeyColumns = Set(foreignKey.mapping.lazy.map { $0.origin.lowercased() })
if originColumns != foreignKeyColumns {
} else {
let foreignKeys = try db.foreignKeys(on: originTable).filter { foreignKey in
if destinationTable.lowercased() != foreignKey.destinationTable.lowercased() {
return false
}
}
if let destinationColumns {
// TODO: test
let destinationColumns = Set(destinationColumns.lazy.map { $0.lowercased() })
let foreignKeyColumns = Set(foreignKey.mapping.lazy.map { $0.destination.lowercased() })
if destinationColumns != foreignKeyColumns {
return false
if let originColumns {
let originColumns = Set(originColumns.lazy.map { $0.lowercased() })
let foreignKeyColumns = Set(foreignKey.mapping.lazy.map { $0.origin.lowercased() })
if originColumns != foreignKeyColumns {
return false
}
}
if let destinationColumns {
// TODO: test
let destinationColumns = Set(destinationColumns.lazy.map { $0.lowercased() })
let foreignKeyColumns = Set(foreignKey.mapping.lazy.map { $0.destination.lowercased() })
if destinationColumns != foreignKeyColumns {
return false
}
}
return true
}
return true
}

// Matching foreign key(s) found
if let foreignKey = foreignKeys.first {
if foreignKeys.count == 1 {
// Non-ambiguous
return foreignKey.mapping
} else {
// Ambiguous: can't choose
fatalError("Ambiguous foreign key from \(originTable) to \(destinationTable)")

// Matching foreign key(s) found
if let foreignKey = foreignKeys.first {
if foreignKeys.count == 1 {
// Non-ambiguous
return foreignKey.mapping
} else {
// Ambiguous: can't choose
fatalError("""
Ambiguous foreign key from '\(originTable)' to \
'\(destinationTable)'. To fix this error, provide an \
explicit `ForeignKey` in the association definition.
""")
}
}
}

// No matching foreign key found: use the destination primary key
if let originColumns {
guard let destinationType = try tableType(db, for: destinationTable) else {
throw DatabaseError.noSuchTable(destinationTable)
}
if destinationType.isView {
fatalError("""
Could not infer foreign key from '\(originTable)' \
to '\(destinationTable)'. To fix this error, provide an \
explicit `ForeignKey` in the association definition, \
with both origin and destination columns.
""")
}
let destinationColumns = try db.primaryKey(destinationTable).columns
if originColumns.count == destinationColumns.count {
let mapping = zip(originColumns, destinationColumns).map {
Expand All @@ -76,7 +111,28 @@ struct SQLForeignKeyRequest {
}
}

fatalError("Could not infer foreign key from \(originTable) to \(destinationTable)")
fatalError("""
Could not infer foreign key from '\(originTable)' to \
'\(destinationTable)'. To fix this error, provide an \
explicit `ForeignKey` in the association definition.
""")
}

private struct TableType {
var isView: Bool
}

private func tableType(_ db: Database, for name: String) throws -> TableType? {
for schemaID in try db.schemaIdentifiers() {
if try db.schema(schemaID).containsObjectNamed(name, ofType: .table) {
return TableType(isView: false)
}
if try db.schema(schemaID).containsObjectNamed(name, ofType: .view) {
return TableType(isView: true)
}
}

return nil
}
}

Expand Down
1 change: 1 addition & 0 deletions GRDB/ValueObservation/ValueObservation.swift
Expand Up @@ -310,6 +310,7 @@ extension ValueObservation {
}
}

// TODO: [GRDB7] Make it Sendable for easier integration with AsyncAlgorithms
/// An asynchronous sequence of values observed by a ``ValueObservation``.
///
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -15,7 +15,7 @@
<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>
</p>

**Latest release**: January 6, 2024 • [version 6.24.1](https://github.com/groue/GRDB.swift/tree/v6.24.1)[CHANGELOG](CHANGELOG.md)[Migrating From GRDB 5 to GRDB 6](Documentation/GRDB6MigrationGuide.md)
**Latest release**: January 6, 2024 • [version 6.24.2](https://github.com/groue/GRDB.swift/tree/v6.24.2)[CHANGELOG](CHANGELOG.md)[Migrating From GRDB 5 to GRDB 6](Documentation/GRDB6MigrationGuide.md)

**Requirements**: iOS 11.0+ / macOS 10.13+ / tvOS 11.0+ / watchOS 4.0+ &bull; SQLite 3.19.3+ &bull; Swift 5.7+ / Xcode 14+

Expand Down
2 changes: 1 addition & 1 deletion Support/Info.plist
Expand Up @@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>6.24.1</string>
<string>6.24.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
Expand Down
75 changes: 75 additions & 0 deletions Tests/GRDBTests/AssociationBelongsToSQLTests.swift
Expand Up @@ -1733,6 +1733,81 @@ class AssociationBelongsToSQLTests: GRDBTestCase {
}
}

func testTableBelongsToView() throws {
try makeDatabaseQueue().write { db in
try db.execute(sql: """
CREATE TABLE child (foo);
CREATE VIEW parent AS SELECT 1 AS bar;
""")

let child = Table("child")
let parent = Table("parent")
let foreignKey = ForeignKey(["foo"], to: ["bar"])
let association = child.belongsTo(parent, using: foreignKey)

try assertEqualSQL(db, child.joining(required: association), """
SELECT "child".* \
FROM "child" \
JOIN "parent" ON "parent"."bar" = "child"."foo"
""")
try assertEqualSQL(db, child.joining(optional: association), """
SELECT "child".* \
FROM "child" \
LEFT JOIN "parent" ON "parent"."bar" = "child"."foo"
""")
}
}

func testViewBelongsToTable() throws {
try makeDatabaseQueue().write { db in
try db.execute(sql: """
CREATE VIEW child AS SELECT 1 AS foo;
CREATE TABLE parent(id INTEGER PRIMARY KEY);
""")

let child = Table("child")
let parent = Table("parent")
let foreignKey = ForeignKey(["foo"])
let association = child.belongsTo(parent, using: foreignKey)

try assertEqualSQL(db, child.joining(required: association), """
SELECT "child".* \
FROM "child" \
JOIN "parent" ON "parent"."id" = "child"."foo"
""")
try assertEqualSQL(db, child.joining(optional: association), """
SELECT "child".* \
FROM "child" \
LEFT JOIN "parent" ON "parent"."id" = "child"."foo"
""")
}
}

func testViewBelongsToView() throws {
try makeDatabaseQueue().write { db in
try db.execute(sql: """
CREATE VIEW child AS SELECT 1 AS foo;
CREATE VIEW parent AS SELECT 1 AS bar;
""")

let child = Table("child")
let parent = Table("parent")
let foreignKey = ForeignKey(["foo"], to: ["bar"])
let association = child.belongsTo(parent, using: foreignKey)

try assertEqualSQL(db, child.joining(required: association), """
SELECT "child".* \
FROM "child" \
JOIN "parent" ON "parent"."bar" = "child"."foo"
""")
try assertEqualSQL(db, child.joining(optional: association), """
SELECT "child".* \
FROM "child" \
LEFT JOIN "parent" ON "parent"."bar" = "child"."foo"
""")
}
}

// Regression test for https://github.com/groue/GRDB.swift/issues/495
func testFetchCount() throws {
let dbQueue = try makeDatabaseQueue()
Expand Down

0 comments on commit e069e27

Please sign in to comment.