import XCTest @testable import GRDB class SelectStatementTests : GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() migrator.registerMigration("createPersons") { db in try db.execute(sql: """ CREATE TABLE persons ( id INTEGER PRIMARY KEY, creationDate TEXT, name TEXT NOT NULL, age INT) """) try db.execute(sql: "INSERT INTO persons (name, age) VALUES (?,?)", arguments: ["Arthur", 41]) try db.execute(sql: "INSERT INTO persons (name, age) VALUES (?,?)", arguments: ["Barbara", 26]) try db.execute(sql: "INSERT INTO persons (name, age) VALUES (?,?)", arguments: ["Craig", 13]) } try migrator.migrate(dbWriter) } func testStatementCursor() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let sql = "SELECT 'Arthur' AS firstName, 'Martin' AS lastName UNION ALL SELECT 'Barbara', 'Gourde'" let statement = try db.makeStatement(sql: sql) let cursor = try statement.makeCursor() // Test that cursor provides statement information XCTAssertEqual(cursor.sql, sql) XCTAssertEqual(cursor.arguments, []) XCTAssertEqual(cursor.columnCount, 2) XCTAssertEqual(cursor.columnNames, ["firstName", "lastName"]) XCTAssertEqual(cursor.databaseRegion.description, "empty") XCTAssertFalse(try cursor.next() == nil) XCTAssertFalse(try cursor.next() == nil) XCTAssertTrue(try cursor.next() == nil) // end XCTAssertTrue(try cursor.next() == nil) // past the end } } func testStatementCursorStepFailure() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let customError = NSError(domain: "Custom", code: 0xDEAD) db.add(function: DatabaseFunction("throw", argumentCount: 0, pure: true) { _ in throw customError }) func test(_ cursor: StatementCursor) throws { let sql = cursor.sql do { _ = try cursor.next() XCTFail() } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ERROR) XCTAssertEqual(error.message, "\(customError)") XCTAssertEqual(error.sql!, sql) XCTAssertEqual(error.description, "SQLite error 1: \(customError) - while executing `\(sql)`") } do { _ = try cursor.next() XCTFail() } catch is DatabaseError { // Various SQLite and SQLCipher versions don't emit the same // error. What we care about is that there is an error. } } try test(db.makeStatement(sql: "SELECT throw(), NULL").makeCursor()) try test(db.makeStatement(sql: "SELECT 0, throw(), NULL").makeCursor()) } } func testArrayStatementArguments() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let statement = try db.makeStatement(sql: "SELECT COUNT(*) FROM persons WHERE age < ?") let ages = [20, 30, 40, 50] let counts = try ages.map { try Int.fetchOne(statement, arguments: [$0])! } XCTAssertEqual(counts, [1,2,2,3]) } } func testStatementArgumentsSetterWithArray() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let statement = try db.makeStatement(sql: "SELECT COUNT(*) FROM persons WHERE age < ?") let ages = [20, 30, 40, 50] let counts = try ages.map { (age: Int) -> Int in statement.arguments = [age] return try Int.fetchOne(statement)! } XCTAssertEqual(counts, [1,2,2,3]) } } func testDictionaryStatementArguments() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let statement = try db.makeStatement(sql: "SELECT COUNT(*) FROM persons WHERE age < :age") let ageDicts: [[String: (any DatabaseValueConvertible)?]] = [["age": 20], ["age": 30], ["age": 40], ["age": 50]] let counts = try ageDicts.map { dic -> Int in // Make sure we don't trigger a failible initializer let arguments: StatementArguments = StatementArguments(dic) return try Int.fetchOne(statement, arguments: arguments)! } XCTAssertEqual(counts, [1,2,2,3]) } } func testStatementArgumentsSetterWithDictionary() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let statement = try db.makeStatement(sql: "SELECT COUNT(*) FROM persons WHERE age < :age") let ageDicts: [[String: (any DatabaseValueConvertible)?]] = [["age": 20], ["age": 30], ["age": 40], ["age": 50]] let counts = try ageDicts.map { ageDict -> Int in statement.arguments = StatementArguments(ageDict) return try Int.fetchOne(statement)! } XCTAssertEqual(counts, [1,2,2,3]) } } func testDatabaseErrorThrownBySelectStatementContainSQL() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { _ = try db.makeStatement(sql: "SELECT * FROM blah") XCTFail() } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ERROR) XCTAssertEqual(error.message!, "no such table: blah") XCTAssertEqual(error.sql!, "SELECT * FROM blah") XCTAssertEqual(error.description, "SQLite error 1: no such table: blah - while executing `SELECT * FROM blah`") } } } func testCachedSelectStatementStepFailure() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in var needsThrow = false db.add(function: DatabaseFunction("bomb", argumentCount: 0, pure: false) { _ in if needsThrow { throw DatabaseError(message: "boom") } return "success" }) let sql = "SELECT bomb()" needsThrow = false XCTAssertEqual(try String.fetchAll(db.cachedStatement(sql: sql)), ["success"]) do { needsThrow = true _ = try String.fetchAll(db.cachedStatement(sql: sql)) XCTFail() } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ERROR) XCTAssertEqual(error.message!, "boom") XCTAssertEqual(error.sql!, sql) XCTAssertEqual(error.description, "SQLite error 1: boom - while executing `\(sql)`") } needsThrow = false XCTAssertEqual(try String.fetchAll(db.cachedStatement(sql: sql)), ["success"]) } } func testConsumeMultipleStatements() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.writeWithoutTransaction { db in do { // SQL, no argument let statements = try db.allStatements(sql: """ SELECT age FROM persons ORDER BY age; SELECT age FROM persons ORDER BY age DESC; """) let ages = try Array(statements.flatMap { try Int.fetchCursor($0) }) XCTAssertEqual(ages, [13, 26, 41, 41, 26, 13]) } do { // Literal, no argument let statements = try db.allStatements(literal: """ SELECT age FROM persons ORDER BY age; SELECT age FROM persons ORDER BY age DESC; """) let ages = try Array(statements.flatMap { try Int.fetchCursor($0) }) XCTAssertEqual(ages, [13, 26, 41, 41, 26, 13]) } do { // SQL, missing arguments let statements = try db.allStatements(sql: """ SELECT count(*) FROM persons WHERE age > ?; SELECT count(*) FROM persons WHERE age < ?; """) let counts = try Array(statements.map { try Int.fetchOne($0, arguments: [30])! }) XCTAssertEqual(counts, [1, 2]) } do { // Literal, missing arguments let statements = try db.allStatements(literal: """ SELECT count(*) FROM persons WHERE age > ?; SELECT count(*) FROM persons WHERE age < ?; """) let counts = try Array(statements.map { try Int.fetchOne($0, arguments: [30])! }) XCTAssertEqual(counts, [1, 2]) } do { // SQL, matching arguments let statements = try db.allStatements(sql: """ SELECT name FROM persons WHERE name = ?; SELECT name FROM persons WHERE age > ? ORDER BY name; """, arguments: ["Arthur", 20]) let names = try Array(statements.map { try String.fetchAll($0) }) XCTAssertEqual(names, [["Arthur"], ["Arthur", "Barbara"]]) } do { // Literal, matching arguments let statements = try db.allStatements(literal: """ SELECT name FROM persons WHERE name = \("Arthur"); SELECT name FROM persons WHERE age > \(20) ORDER BY name; """) let names = try Array(statements.map { try String.fetchAll($0) }) XCTAssertEqual(names, [["Arthur"], ["Arthur", "Barbara"]]) } do { // SQL, too few arguments let statements = try db.allStatements(sql: """ SELECT name FROM persons WHERE name = ?; SELECT name FROM persons WHERE age > ? ORDER BY name; """, arguments: ["Arthur"]) _ = try Array(statements.map { try String.fetchAll($0) }) XCTFail("Expected Error") } catch DatabaseError.SQLITE_MISUSE { // OK } do { // Literal, too few arguments let statements = try db.allStatements(literal: """ SELECT name FROM persons WHERE name = \("Arthur"); SELECT name FROM persons WHERE age > ? ORDER BY name; """) _ = try Array(statements.map { try String.fetchAll($0) }) XCTFail("Expected Error") } catch DatabaseError.SQLITE_MISUSE { // OK } do { // SQL, too many arguments let statements = try db.allStatements(sql: """ SELECT name FROM persons WHERE name = ?; SELECT name FROM persons WHERE age > ? ORDER BY name; """, arguments: ["Arthur", 20, 55]) _ = try Array(statements.map { try String.fetchAll($0) }) XCTFail("Expected Error") } catch DatabaseError.SQLITE_MISUSE { // OK } do { // Mix statement kinds let statements = try db.allStatements(literal: """ CREATE TABLE t(a); INSERT INTO t VALUES (0); SELECT a FROM t ORDER BY a; INSERT INTO t VALUES (1); SELECT a FROM t ORDER BY a; """) let values = try Array(statements.map { try Int.fetchAll($0) }) XCTAssertEqual(values, [[], [], [0], [], [0, 1]]) } } } func testRegion() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.writeWithoutTransaction { db in class Observer: TransactionObserver { private var didChange = false var triggered = false let region: DatabaseRegion init(region: DatabaseRegion) { self.region = region } func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool { region.isModified(byEventsOfKind: eventKind) } func databaseDidChange(with event: DatabaseEvent) { didChange = true } func databaseDidCommit(_ db: Database) { triggered = didChange didChange = false } func databaseDidRollback(_ db: Database) { didChange = false } } try db.create(table: "table1") { t in t.primaryKey("id", .integer) t.column("id3", .integer).references("table3", column: "id", onDelete: .cascade, onUpdate: .cascade) t.column("id4", .integer).references("table4", column: "id", onDelete: .setNull, onUpdate: .cascade) t.column("a", .integer) t.column("b", .integer) } try db.create(table: "table2") { t in t.primaryKey("id", .integer) t.column("a", .integer) t.column("b", .integer) } try db.create(table: "table3") { t in t.primaryKey("id", .integer) } try db.create(table: "table4") { t in t.primaryKey("id", .integer) } try db.create(table: "table5") { t in t.primaryKey("id", .integer) } try db.execute(sql: "CREATE TRIGGER table5trigger AFTER INSERT ON table5 BEGIN INSERT INTO table1 (id3, id4, a, b) VALUES (NULL, NULL, 0, 0); END") let statements = try [ db.makeStatement(sql: "SELECT * FROM table1"), db.makeStatement(sql: "SELECT id, id3, a FROM table1"), db.makeStatement(sql: "SELECT table1.id, table1.a, table2.a FROM table1 JOIN table2 ON table1.id = table2.id"), db.makeStatement(sql: "SELECT COUNT(*) FROM table1"), ] let observers = statements.map { Observer(region: $0.databaseRegion) } XCTAssertEqual(observers.map { $0.region.description }, ["table1(a,b,id,id3,id4)","table1(a,id,id3)", "table1(a,id),table2(a,id)", "table1(*)"]) for observer in observers { db.add(transactionObserver: observer) } try db.execute(sql: "INSERT INTO table3 (id) VALUES (1)") try db.execute(sql: "INSERT INTO table4 (id) VALUES (1)") try db.execute(sql: "INSERT INTO table1 (id, a, b, id3, id4) VALUES (NULL, 0, 0, 1, 1)") XCTAssertEqual(observers.map(\.triggered), [true, true, true, true]) try db.execute(sql: "INSERT INTO table2 (id, a, b) VALUES (NULL, 0, 0)") XCTAssertEqual(observers.map(\.triggered), [false, false, true, false]) try db.execute(sql: "UPDATE table1 SET a = 1") XCTAssertEqual(observers.map(\.triggered), [true, true, true, true]) try db.execute(sql: "UPDATE table1 SET b = 1") XCTAssertEqual(observers.map(\.triggered), [true, false, false, true]) try db.execute(sql: "UPDATE table2 SET a = 1") XCTAssertEqual(observers.map(\.triggered), [false, false, true, false]) try db.execute(sql: "UPDATE table2 SET b = 1") XCTAssertEqual(observers.map(\.triggered), [false, false, false, false]) try db.execute(sql: "UPDATE table3 SET id = 2 WHERE id = 1") XCTAssertEqual(observers.map(\.triggered), [true, true, false, true]) try db.execute(sql: "UPDATE table4 SET id = 2 WHERE id = 1") XCTAssertEqual(observers.map(\.triggered), [true, false, false, true]) try db.execute(sql: "DELETE FROM table4") XCTAssertEqual(observers.map(\.triggered), [true, false, false, true]) try db.execute(sql: "INSERT INTO table4 (id) VALUES (1)") try db.execute(sql: "DELETE FROM table4") XCTAssertEqual(observers.map(\.triggered), [false, false, false, false]) try db.execute(sql: "DELETE FROM table3") XCTAssertEqual(observers.map(\.triggered), [true, true, true, true]) try db.execute(sql: "INSERT INTO table5 (id) VALUES (NULL)") XCTAssertEqual(observers.map(\.triggered), [true, true, true, true]) } } }