Add CI/CD configuration and API documentation

This commit is contained in:
2026-07-01 21:40:53 +08:00
commit c590135d68
4168 changed files with 740252 additions and 0 deletions
@@ -0,0 +1,21 @@
import XCTest
import GRDB
class DatabasePoolCrashTests: GRDBCrashTestCase {
func testReaderCanNotStartTransaction() {
assertCrash("DatabasePool readers can not start transactions or savepoints.") {
try dbPool.read { db in
let statement = try db.makeStatement(sql: "BEGIN TRANSACTION")
}
}
}
func testReaderCanNotStartSavepoint() {
assertCrash("DatabasePool readers can not start transactions or savepoints.") {
try dbPool.read { db in
let statement = try db.makeStatement(sql: "SAVEPOINT foo")
}
}
}
}
@@ -0,0 +1,105 @@
import XCTest
import GRDB
class DatabaseQueueCrashTests: GRDBCrashTestCase {
// =========================================================================
// MARK: - Reentrancy
func testInDatabaseIsNotReentrant() {
assertCrash("Database methods are not reentrant.") {
dbQueue.inDatabase { db in
dbQueue.inDatabase { db in
}
}
}
}
func testInTransactionInsideInDatabaseIsNotReentrant() {
assertCrash("Database methods are not reentrant.") {
try dbQueue.inDatabase { db in
try dbQueue.inTransaction { db in
return .commit
}
}
}
}
func testInTransactionIsNotReentrant() {
assertCrash("Database methods are not reentrant.") {
try dbQueue.inTransaction { db in
try dbQueue.inTransaction { db in
return .commit
}
return .commit
}
}
}
// =========================================================================
// MARK: - Sequence iteration in wrong queue
func testRowSequenceCanNotBeGeneratedOutsideOfDatabaseQueue() {
assertCrash("Database was not used on the correct thread: execute your statements inside DatabaseQueue.inDatabase() or DatabaseQueue.inTransaction(). If you get this error while iterating the result of a fetch() method, consider using the array returned by fetchAll() instead.") {
var rows: DatabaseSequence<Row>?
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE persons (name TEXT)")
rows = try Row.fetch(db, "SELECT * FROM persons")
}
_ = rows!.makeIterator()
}
}
func testRowSequenceCanNotBeIteratedOutsideOfDatabaseQueue() {
assertCrash("Database was not used on the correct thread: execute your statements inside DatabaseQueue.inDatabase() or DatabaseQueue.inTransaction(). If you get this error while iterating the result of a fetch() method, consider using the array returned by fetchAll() instead.") {
var iterator: DatabaseIterator<Row>?
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE persons (name TEXT)")
iterator = try Row.fetch(db, "SELECT * FROM persons").makeIterator()
}
_ = iterator!.next()
}
}
// =========================================================================
// MARK: - Concurrency
func testReaderCrashDuringExclusiveTransaction() {
assertCrash("SQLite error 5 with statement `SELECT * FROM stuffs`: database is locked") {
let dbQueue1 = try! makeDatabaseQueue(path: dbQueuepath, configuration: dbConfiguration)
let dbQueue2 = try! makeDatabaseQueue(path: dbQueuepath, configuration: dbConfiguration)
try! dbQueue1.inDatabase { db in
try db.execute(sql: "CREATE TABLE stuffs (id INTEGER PRIMARY KEY)")
}
let queue = NSOperationQueue()
queue.maxConcurrentOperationCount = 2
queue.addOperation(NSBlockOperation {
do {
try dbQueue1.inTransaction(.exclusive) { db in
sleep(2) // let other queue try to read.
return .commit
}
}
catch is DatabaseError {
}
catch {
XCTFail("\(error)")
}
})
queue.addOperation(NSBlockOperation {
dbQueue2.inDatabase { db in
sleep(1) // let other queue open transaction
_ = try Row.fetch(db, "SELECT * FROM stuffs") // Crash expected
}
})
queue.waitUntilAllOperationsAreFinished()
}
}
}
@@ -0,0 +1,87 @@
import XCTest
import GRDB
// A type that adopts DatabaseValueConvertible but does not adopt StatementColumnConvertible
private struct IntConvertible: DatabaseValueConvertible {
let int: Int
init(int: Int) {
self.int = int
}
var databaseValue: DatabaseValue { int.databaseValue }
static func fromDatabaseValue(_ dbValue: DatabaseValue) -> IntConvertible? {
guard let int = Int.fromDatabaseValue(dbValue) else {
return nil
}
return IntConvertible(int: int)
}
}
class DatabaseValueConvertibleCrashTests: GRDBCrashTestCase {
func testCrashFetchDatabaseValueConvertibleFromStatement() {
assertCrash("could not convert NULL to IntConvertible.") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE ints (int Int)")
try db.execute(sql: "INSERT INTO ints (int) VALUES (1)")
try db.execute(sql: "INSERT INTO ints (int) VALUES (NULL)")
let statement = try db.makeStatement(sql: "SELECT int FROM ints ORDER BY int")
let sequence = IntConvertible.fetch(statement)
for _ in sequence { }
}
}
}
func testCrashFetchAllDatabaseValueConvertibleFromStatement() {
assertCrash("could not convert NULL to IntConvertible.") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE ints (int Int)")
try db.execute(sql: "INSERT INTO ints (int) VALUES (1)")
try db.execute(sql: "INSERT INTO ints (int) VALUES (NULL)")
let statement = try db.makeStatement(sql: "SELECT int FROM ints ORDER BY int")
_ = IntConvertible.fetchAll(statement)
}
}
}
func testCrashFetchDatabaseValueConvertibleFromDatabase() {
assertCrash("could not convert NULL to IntConvertible.") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE ints (int Int)")
try db.execute(sql: "INSERT INTO ints (int) VALUES (1)")
try db.execute(sql: "INSERT INTO ints (int) VALUES (NULL)")
let sequence = IntConvertible.fetch(db, "SELECT int FROM ints ORDER BY int")
for _ in sequence { }
}
}
}
func testCrashFetchAllDatabaseValueConvertibleFromDatabase() {
assertCrash("could not convert NULL to IntConvertible.") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE ints (int Int)")
try db.execute(sql: "INSERT INTO ints (int) VALUES (1)")
try db.execute(sql: "INSERT INTO ints (int) VALUES (NULL)")
_ = IntConvertible.fetchAll(db, sql: "SELECT int FROM ints ORDER BY int")
}
}
}
func testCrashDatabaseValueConvertibleInvalidConversionFromNULL() {
assertCrash("could not convert NULL to IntConvertible.") {
let row = Row(["int": nil])
_ = row["int"] as IntConvertible
}
}
func testCrashDatabaseValueConvertibleInvalidConversionFromInvalidType() {
assertCrash("could not convert \"foo\" to IntConvertible") {
let row = Row(["int": "foo"])
_ = row["int"] as IntConvertible
}
}
}
@@ -0,0 +1,15 @@
import XCTest
class GRDBCrashTestCase: GRDBTestCase {
// This method does not actually catch any crash.
// But it expresses an intent :-)
func assertCrash(message: String, @noescape block: () throws -> ()) {
do {
try block()
XCTFail("Crash expected: \(message)")
} catch {
XCTFail("Unexpected error: \(error)")
}
}
}
@@ -0,0 +1,13 @@
import GRDB
class MigrationCrashTests: GRDBCrashTestCase {
func testMigrationNamesMustBeUnique() {
assertCrash("already registered migration: \"foo\"") {
var migrator = DatabaseMigrator()
migrator.registerMigration("foo") { db in }
migrator.registerMigration("foo") { db in }
}
}
}
@@ -0,0 +1,324 @@
import XCTest
import GRDB
private class RecordWithoutDatabaseTableName: Record { }
private class RecordWithInexistingDatabaseTable: Record {
override class var databaseTableName: String {
"foo"
}
}
private class RecordWithEmptyPersistentDictionary : Record {
override class var databaseTableName: String {
"records"
}
}
private class RecordWithNilPrimaryKey : Record {
override class var databaseTableName: String {
"records"
}
override func encode(to container: inout PersistenceContainer) throws {
container["id"] = nil
}
}
private class RecordForTableWithoutPrimaryKey : Record {
override class var databaseTableName: String {
"records"
}
override func encode(to container: inout PersistenceContainer) throws {
container["name"] = "foo"
}
}
private class RecordForTableWithMultipleColumnsPrimaryKey : Record {
override class var databaseTableName: String {
"records"
}
override func encode(to container: inout PersistenceContainer) throws {
container["name"] = "foo"
}
}
private class RecordWithRowIDPrimaryKeyNotExposedInPersistentDictionary : Record {
override class var databaseTableName: String {
"records"
}
override func encode(to container: inout PersistenceContainer) throws {
container["name"] = "foo"
}
}
class RecordCrashTests: GRDBCrashTestCase {
// =========================================================================
// MARK: - RecordWithoutDatabaseTableName
func testRecordWithoutDatabaseTableNameCanNotBeFetchedByID() {
assertCrash("subclass must override") {
dbQueue.inDatabase { db in
_ = RecordWithoutDatabaseTableName.fetchOne(db, key: 1)
}
}
}
func testRecordWithoutDatabaseTableNameCanNotBeFetchedByKey() {
assertCrash("subclass must override") {
dbQueue.inDatabase { db in
_ = RecordWithoutDatabaseTableName.fetchOne(db, key: ["foo": "bar"])
}
}
}
func testRecordWithoutDatabaseTableNameCanNotBeInserted() {
assertCrash("subclass must override") {
try dbQueue.inDatabase { db in
try RecordWithoutDatabaseTableName().insert(db)
}
}
}
func testRecordWithoutDatabaseTableNameCanNotBeUpdated() {
assertCrash("subclass must override") {
try dbQueue.inDatabase { db in
try RecordWithoutDatabaseTableName().update(db)
}
}
}
func testRecordWithoutDatabaseTableNameCanNotBeSaved() {
assertCrash("subclass must override") {
try dbQueue.inDatabase { db in
try RecordWithoutDatabaseTableName().save(db)
}
}
}
func testRecordWithoutDatabaseTableNameCanNotBeDeleted() {
assertCrash("subclass must override") {
try dbQueue.inDatabase { db in
try RecordWithoutDatabaseTableName().delete(db)
}
}
}
func testRecordWithoutDatabaseTableNameCanNotBeTestedForExistence() {
assertCrash("subclass must override") {
dbQueue.inDatabase { db in
RecordWithoutDatabaseTableName().exists(db)
}
}
}
// =========================================================================
// MARK: - RecordWithInexistingDatabaseTable
func testRecordWithInexistingDatabaseTableCanNotBeFetchedByID() {
assertCrash("no such table: foo") {
dbQueue.inDatabase { db in
_ = RecordWithInexistingDatabaseTable.fetchOne(db, key: 1)
}
}
}
func testRecordWithInexistingDatabaseTableCanNotBeFetchedByKey() {
assertCrash("SQLite error 1 with statement `SELECT * FROM \"foo\" WHERE (\"id\" = ?)`: no such table: foo") {
dbQueue.inDatabase { db in
_ = RecordWithInexistingDatabaseTable.fetchOne(db, key: ["id": 1])
}
}
}
func testRecordWithInexistingDatabaseTableCanNotBeInserted() {
assertCrash("no such table: foo") {
try dbQueue.inDatabase { db in
try RecordWithInexistingDatabaseTable().insert(db)
}
}
}
func testRecordWithInexistingDatabaseTableCanNotBeUpdated() {
assertCrash("no such table: foo") {
try dbQueue.inDatabase { db in
try RecordWithInexistingDatabaseTable().update(db)
}
}
}
func testRecordWithInexistingDatabaseTableCanNotBeSaved() {
assertCrash("no such table: foo") {
try dbQueue.inDatabase { db in
try RecordWithInexistingDatabaseTable().save(db)
}
}
}
func testRecordWithInexistingDatabaseTableCanNotBeDeleted() {
assertCrash("no such table: foo") {
try dbQueue.inDatabase { db in
try RecordWithInexistingDatabaseTable().delete(db)
}
}
}
func testRecordWithInexistingDatabaseTableCanNotBeTestedForExistence() {
assertCrash("no such table: foo") {
dbQueue.inDatabase { db in
RecordWithInexistingDatabaseTable().exists(db)
}
}
}
// =========================================================================
// MARK: - RecordWithEmptyPersistentDictionary
func testRecordWithEmptyPersistentDictionaryCanNotBeInserted() {
assertCrash("RecordWithEmptyPersistentDictionary.persistentDictionary: invalid empty dictionary") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE records (id INTEGER PRIMARY KEY)")
try RecordWithEmptyPersistentDictionary().insert(db)
}
}
}
func testRecordWithEmptyPersistentDictionaryCanNotBeUpdated() {
assertCrash("RecordWithEmptyPersistentDictionary.persistentDictionary: invalid empty dictionary") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE records (id INTEGER PRIMARY KEY)")
try RecordWithEmptyPersistentDictionary().update(db)
}
}
}
func testRecordWithEmptyPersistentDictionaryCanNotBeSaved() {
assertCrash("RecordWithEmptyPersistentDictionary.persistentDictionary: invalid empty dictionary") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE records (id INTEGER PRIMARY KEY)")
try RecordWithEmptyPersistentDictionary().save(db)
}
}
}
func testRecordWithEmptyPersistentDictionaryCanNotBeDeleted() {
assertCrash("RecordWithEmptyPersistentDictionary.persistentDictionary: invalid empty dictionary") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE records (id INTEGER PRIMARY KEY)")
try RecordWithEmptyPersistentDictionary().delete(db)
}
}
}
func testRecordWithEmptyPersistentDictionaryCanNotBeTestedForExistence() {
assertCrash("RecordWithEmptyPersistentDictionary.persistentDictionary: invalid empty dictionary") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE records (id INTEGER PRIMARY KEY)")
RecordWithEmptyPersistentDictionary().exists(db)
}
}
}
// =========================================================================
// MARK: - RecordWithNilPrimaryKey
func testRecordWithNilPrimaryKeyCanNotBeUpdated() {
assertCrash("invalid primary key in <RecordWithNilPrimaryKey id:nil>") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE records (id INTEGER PRIMARY KEY)")
try RecordWithNilPrimaryKey().update(db)
}
}
}
func testRecordWithNilPrimaryKeyCanNotBeDeleted() {
assertCrash("invalid primary key in <RecordWithNilPrimaryKey id:nil>") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE records (id INTEGER PRIMARY KEY)")
try RecordWithNilPrimaryKey().delete(db)
}
}
}
func testRecordWithNilPrimaryKeyCanNotBeTestedForExistence() {
assertCrash("invalid primary key in <RecordWithNilPrimaryKey id:nil>") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE records (id INTEGER PRIMARY KEY)")
RecordWithNilPrimaryKey().exists(db)
}
}
}
// =========================================================================
// MARK: - RecordForTableWithoutPrimaryKey
func testRecordForTableWithoutPrimaryKeyCanNotBeFetchedByID() {
assertCrash("expected single column primary key in table: records") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE records (name TEXT)")
_ = RecordForTableWithoutPrimaryKey.fetchOne(db, key: 1)
}
}
}
func testRecordForTableWithoutPrimaryKeyCanNotBeUpdated() {
assertCrash("invalid primary key in <RecordForTableWithoutPrimaryKey name:\"foo\">") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE records (name TEXT)")
try RecordForTableWithoutPrimaryKey().update(db)
}
}
}
func testRecordForTableWithoutPrimaryKeyCanNotBeDeleted() {
assertCrash("invalid primary key in <RecordForTableWithoutPrimaryKey name:\"foo\">") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE records (name TEXT)")
try RecordForTableWithoutPrimaryKey().delete(db)
}
}
}
func testRecordForTableWithoutPrimaryKeyCanNotBeTestedForExistence() {
assertCrash("invalid primary key in <RecordForTableWithoutPrimaryKey name:\"foo\">") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE records (name TEXT)")
RecordForTableWithoutPrimaryKey().exists(db)
}
}
}
// =========================================================================
// MARK: - RecordForTableWithMultipleColumnsPrimaryKey
func testRecordForTableWithMultipleColumnsPrimaryKeyCanNotBeFetchedByID() {
assertCrash("expected single column primary key in table: records") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE records (a TEXT, b TEXT, PRIMARY KEY(a,b))")
_ = RecordForTableWithMultipleColumnsPrimaryKey.fetchOne(db, key: 1)
}
}
}
// =========================================================================
// MARK: - RecordWithRowIDPrimaryKeyNotExposedInPersistentDictionary
func testRecordWithRowIDPrimaryKeyNotExposedInPersistentDictionaryCanNotBeInserted() {
assertCrash("invalid primary key in <RecordWithRowIDPrimaryKeyNotExposedInPersistentDictionary name:\"foo\">") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE records (id INTEGER PRIMARY KEY, name TEXT)")
try RecordWithRowIDPrimaryKeyNotExposedInPersistentDictionary().update(db)
}
}
}
}
@@ -0,0 +1,58 @@
import XCTest
import GRDB
class StatementColumnConvertibleCrashTests: GRDBCrashTestCase {
func testCrashFetchStatementColumnConvertibleFromStatement() {
assertCrash("could not convert NULL to Int.") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE ints (int Int)")
try db.execute(sql: "INSERT INTO ints (int) VALUES (1)")
try db.execute(sql: "INSERT INTO ints (int) VALUES (NULL)")
let statement = try db.makeStatement(sql: "SELECT int FROM ints ORDER BY int")
let sequence = try Int.fetch(statement)
for _ in sequence { }
}
}
}
func testCrashFetchAllStatementColumnConvertibleFromStatement() {
assertCrash("could not convert NULL to Int.") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE ints (int Int)")
try db.execute(sql: "INSERT INTO ints (int) VALUES (1)")
try db.execute(sql: "INSERT INTO ints (int) VALUES (NULL)")
let statement = try db.makeStatement(sql: "SELECT int FROM ints ORDER BY int")
_ = try Int.fetchAll(statement)
}
}
}
func testCrashFetchStatementColumnConvertibleFromDatabase() {
assertCrash("could not convert NULL to Int.") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE ints (int Int)")
try db.execute(sql: "INSERT INTO ints (int) VALUES (1)")
try db.execute(sql: "INSERT INTO ints (int) VALUES (NULL)")
let sequence = try Int.fetch(db, "SELECT int FROM ints ORDER BY int")
for _ in sequence { }
}
}
}
func testCrashFetchAllStatementColumnConvertibleFromDatabase() {
assertCrash("could not convert NULL to Int.") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE ints (int Int)")
try db.execute(sql: "INSERT INTO ints (int) VALUES (1)")
try db.execute(sql: "INSERT INTO ints (int) VALUES (NULL)")
_ = try Int.fetchAll(db, sql: "SELECT int FROM ints ORDER BY int")
}
}
}
}
@@ -0,0 +1,75 @@
import XCTest
import GRDB
class StatementCrashTests: GRDBCrashTestCase {
func testInvalidStatementArguments() {
assertCrash("SQLite error 1 with statement `INSERT INTO persons (name, age) VALUES (:name, :age)`: missing statement argument(s): age") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE persons (name TEXT, age INT)")
try! db.execute(sql: "INSERT INTO persons (name, age) VALUES (:name, :age)", arguments: ["name": "Arthur"])
}
}
assertCrash("SQLite error 21 with statement `INSERT INTO persons (name, age) VALUES ('Arthur', ?);`: wrong number of statement arguments: 0") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE persons (name TEXT, age INT)")
try db.execute(sql: """
INSERT INTO persons (name, age) VALUES ('Arthur', ?);
INSERT INTO persons (name, age) VALUES ('Barbara', ?);
""")
}
}
assertCrash("SQLite error 21 with statement `INSERT INTO persons (name, age) VALUES ('Barbara', ?);`: wrong number of statement arguments: 0") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE persons (name TEXT, age INT)")
try db.execute(sql: """
INSERT INTO persons (name, age) VALUES ('Arthur', ?);
INSERT INTO persons (name, age) VALUES ('Barbara', ?);
""", arguments: [41])
}
}
assertCrash("SQLite error 21 with statement `INSERT INTO persons (name, age) VALUES ('Arthur', :age1);`: missing statement argument(s): age1") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE persons (name TEXT, age INT)")
try db.execute(sql: """
INSERT INTO persons (name, age) VALUES ('Arthur', :age1);
INSERT INTO persons (name, age) VALUES ('Barbara', :age2);
""")
}
}
assertCrash("SQLite error 21 with statement `INSERT INTO persons (name, age) VALUES ('Barbara', :age2);`: missing statement argument(s): age2") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE persons (name TEXT, age INT)")
try db.execute(sql: """
INSERT INTO persons (name, age) VALUES ('Arthur', :age1);
INSERT INTO persons (name, age) VALUES ('Barbara', :age2);
""", arguments: ["age1": 41])
}
}
assertCrash("SQLite error 21: wrong number of statement arguments: 3") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE persons (name TEXT, age INT)")
try db.execute(sql: """
INSERT INTO persons (name, age) VALUES ('Arthur', :age1);
INSERT INTO persons (name, age) VALUES ('Arthur', :age2);
""", arguments: [41, 32, 17])
}
}
assertCrash("SQLite error 21: wrong number of statement arguments: 3") {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE persons (name TEXT, age INT)")
try db.execute(sql: """
INSERT INTO persons (name, age) VALUES ('Arthur', ?);
INSERT INTO persons (name, age) VALUES ('Arthur', ?);
""", arguments: [41, 32, 17])
}
}
}
}