Skip to content

Commit

Permalink
Add schemaName parameter to foreign key checking functions
Browse files Browse the repository at this point in the history
  • Loading branch information
barnettben committed Dec 8, 2023
1 parent 65167ec commit 27a4215
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 13 deletions.
40 changes: 32 additions & 8 deletions GRDB/Core/Database+Schema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -692,10 +692,27 @@ extension Database {

/// Returns a cursor over foreign key violations in the table.
///
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or if no
/// such table exists in the main or temp schema, or in an
/// attached database.
public func foreignKeyViolations(in tableName: String) throws -> RecordCursor<ForeignKeyViolation> {
/// When `schemaName` is not specified, known schemas are checked in
/// SQLite resolution order and the first matching table is used.
///
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, if
/// the specified schema does not exist, or if no such table or view
/// with this name exists in the main or temp schema, or in an attached
/// database.
public func foreignKeyViolations(
in tableName: String,
in schemaName: String? = nil)
throws -> RecordCursor<ForeignKeyViolation>
{
if let schemaName {
let schemaID = try schemaIdentifier(named: schemaName)
if try exists(type: .table, name: tableName, in: schemaID) {
return try foreignKeyViolations(in: TableIdentifier(schemaID: schemaID, name: tableName))
} else {
throw DatabaseError.noSuchTable(tableName)
}
}

for schemaIdentifier in try schemaIdentifiers() {
if try exists(type: .table, name: tableName, in: schemaIdentifier) {
return try foreignKeyViolations(in: TableIdentifier(schemaID: schemaIdentifier, name: tableName))
Expand Down Expand Up @@ -724,14 +741,21 @@ extension Database {

/// Throws an error if there exists a foreign key violation in the table.
///
/// When `schemaName` is not specified, known schemas are checked in
/// SQLite resolution order and the first matching table is used.
///
/// On the first foreign key violation found in the table, this method
/// throws a ``DatabaseError`` with extended code
/// `SQLITE_CONSTRAINT_FOREIGNKEY`.
///
/// If you are looking for the list of foreign key violations, prefer
/// ``foreignKeyViolations(in:)`` instead.
public func checkForeignKeys(in tableName: String) throws {
try checkForeignKeys(from: foreignKeyViolations(in: tableName))
/// ``foreignKeyViolations(in:in:)`` instead.
///
/// - throws: A ``DatabaseError`` as described above; when a
/// specified schema does not exist; if no such table or view with this
/// name exists in the main or temp schema or in an attached database.
public func checkForeignKeys(in tableName: String, in schemaName: String? = nil) throws {
try checkForeignKeys(from: foreignKeyViolations(in: tableName, in: schemaName))
}

private func checkForeignKeys(from violations: RecordCursor<ForeignKeyViolation>) throws {
Expand Down Expand Up @@ -1095,7 +1119,7 @@ public struct IndexInfo {
///
/// You get instances of `ForeignKeyViolation` from the `Database` methods
/// ``Database/foreignKeyViolations()`` and
/// ``Database/foreignKeyViolations(in:)`` methods.
/// ``Database/foreignKeyViolations(in:in:)`` methods.
///
/// For example:
///
Expand Down
4 changes: 2 additions & 2 deletions GRDB/Documentation.docc/DatabaseSchema.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,9 +410,9 @@ extension Team: TableRecord {
### Integrity Checks

- ``Database/checkForeignKeys()``
- ``Database/checkForeignKeys(in:)``
- ``Database/checkForeignKeys(in:in:)``
- ``Database/foreignKeyViolations()``
- ``Database/foreignKeyViolations(in:)``
- ``Database/foreignKeyViolations(in:in:)``
- ``ForeignKeyViolation``

### Sunsetted Methods
Expand Down
2 changes: 1 addition & 1 deletion GRDB/Documentation.docc/Migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ To prevent a migration from committing foreign key violations on disk, you can:
}
```

As in the above example, check for foreign key violations with the ``Database/checkForeignKeys()`` and ``Database/checkForeignKeys(in:)`` methods. They throw a nicely detailed ``DatabaseError`` that contains a lot of debugging information:
As in the above example, check for foreign key violations with the ``Database/checkForeignKeys()`` and ``Database/checkForeignKeys(in:in:)`` methods. They throw a nicely detailed ``DatabaseError`` that contains a lot of debugging information:

```swift
// SQLite error 19: FOREIGN KEY constraint violation - from book(authorId) to author(id),
Expand Down
4 changes: 2 additions & 2 deletions GRDB/Migration/DatabaseMigrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public struct DatabaseMigrator {
/// ``DatabaseMigrator/disablingDeferredForeignKeyChecks()``.
///
/// In this case, you can perform your own deferred foreign key checks
/// with ``Database/checkForeignKeys(in:)`` or
/// with ``Database/checkForeignKeys(in:in:)`` or
/// ``Database/checkForeignKeys()``:
///
/// ```swift
Expand Down Expand Up @@ -118,7 +118,7 @@ public struct DatabaseMigrator {
/// The returned migrator is _unsafe_, because it no longer guarantees the
/// integrity of the database. It is now _your_ responsibility to register
/// migrations that do not break foreign key constraints. See
/// ``Database/checkForeignKeys()`` and ``Database/checkForeignKeys(in:)``.
/// ``Database/checkForeignKeys()`` and ``Database/checkForeignKeys(in:in:)``.
///
/// Running migrations without foreign key checks can improve migration
/// performance on huge databases.
Expand Down
164 changes: 164 additions & 0 deletions Tests/GRDBTests/ForeignKeyInfoTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -319,4 +319,168 @@ class ForeignKeyInfoTests: GRDBTestCase {
}
}
}

func testForeignKeyViolationsUnknownSchema() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.writeWithoutTransaction { db in
try db.execute(sql: "CREATE TABLE parent (id PRIMARY KEY)")
try db.execute(sql: "CREATE TABLE child (parentId REFERENCES parent)")
do {
_ = try db.foreignKeyViolations(in: "child", in: "invalid")
XCTFail("Expected Error")
} catch let error as DatabaseError {
XCTAssertEqual(error.resultCode, .SQLITE_ERROR)
XCTAssertEqual(error.message, "no such schema: invalid")
XCTAssertEqual(error.description, "SQLite error 1: no such schema: invalid")
}

do {
_ = try db.checkForeignKeys(in: "child", in: "invalid")
XCTFail("Expected Error")
} catch let error as DatabaseError {
XCTAssertEqual(error.resultCode, .SQLITE_ERROR)
XCTAssertEqual(error.message, "no such schema: invalid")
XCTAssertEqual(error.description, "SQLite error 1: no such schema: invalid")
}
}
}

func testForeignKeyViolationsMainSchema() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.writeWithoutTransaction { db in
try db.execute(sql: """
CREATE TABLE parent(id TEXT NOT NULL PRIMARY KEY);
CREATE TABLE child(id INTEGER NOT NULL PRIMARY KEY, parentId TEXT REFERENCES parent(id));
PRAGMA foreign_keys = OFF;
INSERT INTO child (id, parentId) VALUES (13, '1');
""")
do {
let violations = try Array(db.foreignKeyViolations(in: "child", in: "main"))
XCTAssertEqual(violations.count, 1)
}
do {
_ = try db.checkForeignKeys(in: "child", in: "main")
XCTFail("Expected Error")
} catch let error as DatabaseError {
XCTAssertEqual(DatabaseError.SQLITE_CONSTRAINT_FOREIGNKEY, error.extendedResultCode)
}
}
}

func testForeignKeyViolationsInSpecifiedSchemaWithTableNameCollisions() throws {
let attached = try makeDatabaseQueue(filename: "attached1")
try attached.inDatabase { db in
try db.execute(sql: """
CREATE TABLE parent(id TEXT NOT NULL PRIMARY KEY);
CREATE TABLE child(id INTEGER NOT NULL PRIMARY KEY, parentId TEXT REFERENCES parent(id));
PRAGMA foreign_keys = OFF;
INSERT INTO child (id, parentId) VALUES (20, '1');
""")
}
let main = try makeDatabaseQueue(filename: "main")
try main.inDatabase { db in
try db.execute(sql: """
CREATE TABLE parent(id TEXT NOT NULL PRIMARY KEY);
CREATE TABLE child(id INTEGER NOT NULL PRIMARY KEY, parentId TEXT REFERENCES parent(id));
PRAGMA foreign_keys = OFF;
INSERT INTO child (id, parentId) VALUES (10, '1');
""")
try db.execute(literal: "ATTACH DATABASE \(attached.path) AS attached")

do {
let violations = try Array(try db.foreignKeyViolations(in: "child", in: "attached"))
XCTAssertEqual(violations.count, 1)
if let violation = violations.first(where: { $0.originRowID == 20 }) {
XCTAssertEqual(violation.originTable, "child")
XCTAssertEqual(violation.destinationTable, "parent")
} else {
XCTFail("Missing violation")
}
}

do {
_ = try db.checkForeignKeys(in: "child", in: "attached")
XCTFail("Expected Error")
} catch let error as DatabaseError {
XCTAssertEqual(DatabaseError.SQLITE_CONSTRAINT_FOREIGNKEY, error.extendedResultCode)
}
}
}

// The `child` table in the attached database should not
// be found unless explicitly specified as it is after
// `main.child` in resolution order.
func testForeignKeyViolationsInUnspecifiedSchemaWithTableNameCollisions() throws {
let attached = try makeDatabaseQueue(filename: "attached1")
try attached.inDatabase { db in
try db.execute(sql: """
CREATE TABLE parent(id TEXT NOT NULL PRIMARY KEY);
CREATE TABLE child(id INTEGER NOT NULL PRIMARY KEY, parentId TEXT REFERENCES parent(id));
PRAGMA foreign_keys = OFF;
INSERT INTO child (id, parentId) VALUES (20, '1');
""")
}
let main = try makeDatabaseQueue(filename: "main")
try main.inDatabase { db in
try db.execute(sql: """
CREATE TABLE parent(id TEXT NOT NULL PRIMARY KEY);
CREATE TABLE child(id INTEGER NOT NULL PRIMARY KEY, parentId TEXT REFERENCES parent(id));
PRAGMA foreign_keys = OFF;
INSERT INTO child (id, parentId) VALUES (10, '1');
""")
try db.execute(literal: "ATTACH DATABASE \(attached.path) AS attached")

do {
let violations = try Array(try db.foreignKeyViolations(in: "child"))
XCTAssertEqual(violations.count, 1)
if let violation = violations.first(where: { $0.originRowID == 10 }) {
XCTAssertEqual(violation.originTable, "child")
XCTAssertEqual(violation.destinationTable, "parent")
} else {
XCTFail("Missing violation")
}
}

do {
_ = try db.checkForeignKeys(in: "child")
XCTFail("Expected Error")
} catch let error as DatabaseError {
XCTAssertEqual(DatabaseError.SQLITE_CONSTRAINT_FOREIGNKEY, error.extendedResultCode)
}
}
}

func testForeignKeyViolationsInUnspecifiedSchemaFindsAttachedDatabase() throws {
let attached = try makeDatabaseQueue(filename: "attached1")
try attached.inDatabase { db in
try db.execute(sql: """
CREATE TABLE parent(id TEXT NOT NULL PRIMARY KEY);
CREATE TABLE child(id INTEGER NOT NULL PRIMARY KEY, parentId TEXT REFERENCES parent(id));
PRAGMA foreign_keys = OFF;
INSERT INTO child (id, parentId) VALUES (20, '1');
""")
}
let main = try makeDatabaseQueue(filename: "main")
try main.inDatabase { db in
try db.execute(literal: "ATTACH DATABASE \(attached.path) AS attached")

do {
let violations = try Array(try db.foreignKeyViolations(in: "child"))
XCTAssertEqual(violations.count, 1)
if let violation = violations.first(where: { $0.originRowID == 20 }) {
XCTAssertEqual(violation.originTable, "child")
XCTAssertEqual(violation.destinationTable, "parent")
} else {
XCTFail("Missing violation")
}
}

do {
_ = try db.checkForeignKeys(in: "child")
XCTFail("Expected Error")
} catch let error as DatabaseError {
XCTAssertEqual(DatabaseError.SQLITE_CONSTRAINT_FOREIGNKEY, error.extendedResultCode)
}
}
}
}

0 comments on commit 27a4215

Please sign in to comment.