import XCTest
@testable import GRDB

class DatabaseRegionTests : GRDBTestCase {
    
    func testRegionEquatable() {
        // An array of distinct selection infos
        let regions = [
            DatabaseRegion.fullDatabase,
            DatabaseRegion(),
            DatabaseRegion(table: "foo"),
            DatabaseRegion(table: "foo", columns: ["a", "b"]),
            DatabaseRegion(table: "foo", columns: ["b", "c"]),
            DatabaseRegion(table: "foo", rowIds: [1, 2]),
            DatabaseRegion(table: "foo", rowIds: [2, 3]),
            DatabaseRegion(table: "bar")]
        
        for (i1, s1) in regions.enumerated() {
            for (i2, s2) in regions.enumerated() {
                if i1 == i2 {
                    XCTAssertEqual(s1, s2)
                } else {
                    XCTAssertNotEqual(s1, s2)
                }
            }
        }
        
        // Case insensitivity
        XCTAssertEqual(
            DatabaseRegion(table: "foo"),
            DatabaseRegion(table: "FOO"))
        XCTAssertEqual(
            DatabaseRegion(table: "foo", columns: ["a", "b"]),
            DatabaseRegion(table: "FOO", columns: ["A", "B"]))
    }
    
    func testRegionUnion() {
        let regions = [
            DatabaseRegion.fullDatabase,
            DatabaseRegion(),
            DatabaseRegion(table: "foo"),
            DatabaseRegion(table: "foo", columns: ["a", "b"]),
            DatabaseRegion(table: "foo", columns: ["b", "c"]),
            DatabaseRegion(table: "foo", rowIds: [1, 2]),
            DatabaseRegion(table: "foo", rowIds: [2, 3]),
            DatabaseRegion(table: "bar")]
        
        var unions: [DatabaseRegion] = []
        for s1 in regions {
            for s2 in regions {
                unions.append(s1.union(s2))
            }
        }
        
        XCTAssertEqual(unions.map(\.description), [
            "full database",
            "full database",
            "full database",
            "full database",
            "full database",
            "full database",
            "full database",
            "full database",
            
            "full database",
            "empty",
            "foo(*)",
            "foo(a,b)",
            "foo(b,c)",
            "foo(*)[1,2]",
            "foo(*)[2,3]",
            "bar(*)",
            
            "full database",
            "foo(*)",
            "foo(*)",
            "foo(*)",
            "foo(*)",
            "foo(*)",
            "foo(*)",
            "bar(*),foo(*)",
            
            "full database",
            "foo(a,b)",
            "foo(*)",
            "foo(a,b)",
            "foo(a,b,c)",
            "foo(*)",
            "foo(*)",
            "bar(*),foo(a,b)",
            
            "full database",
            "foo(b,c)",
            "foo(*)",
            "foo(a,b,c)",
            "foo(b,c)",
            "foo(*)",
            "foo(*)",
            "bar(*),foo(b,c)",
            
            "full database",
            "foo(*)[1,2]",
            "foo(*)",
            "foo(*)",
            "foo(*)",
            "foo(*)[1,2]",
            "foo(*)[1,2,3]",
            "bar(*),foo(*)[1,2]",
            
            "full database",
            "foo(*)[2,3]",
            "foo(*)",
            "foo(*)",
            "foo(*)",
            "foo(*)[1,2,3]",
            "foo(*)[2,3]",
            "bar(*),foo(*)[2,3]",
            
            "full database",
            "bar(*)",
            "bar(*),foo(*)",
            "bar(*),foo(a,b)",
            "bar(*),foo(b,c)",
            "bar(*),foo(*)[1,2]",
            "bar(*),foo(*)[2,3]",
            "bar(*)"])
    }
    
    func testRegionUnionOfColumnsAndRows() {
        let regions = [
            DatabaseRegion(table: "foo", columns: ["a"]).intersection(DatabaseRegion(table: "foo", rowIds: [1])),
            DatabaseRegion(table: "foo", columns: ["b"]).intersection(DatabaseRegion(table: "foo", rowIds: [2])),
            ]
        
        var unions: [DatabaseRegion] = []
        for s1 in regions {
            for s2 in regions {
                unions.append(s1.union(s2))
            }
        }
        
        XCTAssertEqual(unions.map(\.description), ["foo(a)[1]", "foo(a,b)[1,2]", "foo(a,b)[1,2]", "foo(b)[2]"])
    }
    
    func testRegionIntersection() {
        let regions = [
            DatabaseRegion.fullDatabase,
            DatabaseRegion(),
            DatabaseRegion(table: "foo"),
            DatabaseRegion(table: "foo", columns: ["a", "b"]),
            DatabaseRegion(table: "foo", columns: ["b", "c"]),
            DatabaseRegion(table: "foo", rowIds: [1, 2]),
            DatabaseRegion(table: "foo", rowIds: [2, 3]),
            DatabaseRegion(table: "bar")]
        
        var intersection: [DatabaseRegion] = []
        for s1 in regions {
            for s2 in regions {
                intersection.append(s1.intersection(s2))
            }
        }
        
        XCTAssertEqual(intersection.map(\.description), [
            "full database",
            "empty",
            "foo(*)",
            "foo(a,b)",
            "foo(b,c)",
            "foo(*)[1,2]",
            "foo(*)[2,3]",
            "bar(*)",
            
            "empty",
            "empty",
            "empty",
            "empty",
            "empty",
            "empty",
            "empty",
            "empty",
            
            "foo(*)",
            "empty",
            "foo(*)",
            "foo(a,b)",
            "foo(b,c)",
            "foo(*)[1,2]",
            "foo(*)[2,3]",
            "empty",
            
            "foo(a,b)",
            "empty",
            "foo(a,b)",
            "foo(a,b)",
            "foo(b)",
            "foo(a,b)[1,2]",
            "foo(a,b)[2,3]",
            "empty",
            
            "foo(b,c)",
            "empty",
            "foo(b,c)",
            "foo(b)",
            "foo(b,c)",
            "foo(b,c)[1,2]",
            "foo(b,c)[2,3]",
            "empty",
            
            "foo(*)[1,2]",
            "empty",
            "foo(*)[1,2]",
            "foo(a,b)[1,2]",
            "foo(b,c)[1,2]",
            "foo(*)[1,2]",
            "foo(*)[2]",
            "empty",
            
            "foo(*)[2,3]",
            "empty",
            "foo(*)[2,3]",
            "foo(a,b)[2,3]",
            "foo(b,c)[2,3]",
            "foo(*)[2]",
            "foo(*)[2,3]",
            "empty",
            
            "bar(*)",
            "empty",
            "empty",
            "empty",
            "empty",
            "empty",
            "empty",
            "bar(*)"])
    }
    
    func testRegionIntersectionOfColumnsAndRows() {
        let regions = [
            DatabaseRegion(table: "foo", columns: ["a"]).intersection(DatabaseRegion(table: "foo", rowIds: [1])),
            DatabaseRegion(table: "foo", columns: ["b"]).intersection(DatabaseRegion(table: "foo", rowIds: [2])),
            ]
        
        var intersection: [DatabaseRegion] = []
        for s1 in regions {
            for s2 in regions {
                intersection.append(s1.intersection(s2))
            }
        }
        
        XCTAssertEqual(intersection.map(\.description), ["foo(a)[1]", "empty", "empty", "foo(b)[2]"])
    }
    
    func testSelectStatement_rowid() throws {
        guard #available(iOS 11, *, tvOS 11) else {
            // iOS 10.3.1 is not testable on Big Sur :-(
            // This test breaks on iOS 10.3.1, with no known bad consequence.
            // However this test is useful as a reminder of the behavior of
            // the SQLite authorizer (rowid is not *precisely* observable).
            throw XCTSkip("Skip test for rowid region with old SQLite version")
        }
        
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            try db.execute(sql: "CREATE TABLE foo (id INTEGER PRIMARY KEY, name TEXT)")
            
            do {
                // Select the rowid
                let statement = try db.makeStatement(sql: "SELECT id FROM foo")
                let expectedRegion = DatabaseRegion(table: "foo")
                XCTAssertEqual(statement.databaseRegion, expectedRegion)
                XCTAssertEqual(statement.databaseRegion.description, "foo(*)")
            }
            do {
                let statement = try db.makeStatement(sql: "SELECT ID FROM FOO")
                let expectedRegion = DatabaseRegion(table: "foo")
                XCTAssertEqual(statement.databaseRegion, expectedRegion)
                XCTAssertEqual(statement.databaseRegion.description, "foo(*)")
            }
        }
    }
    
    func testSelectStatement() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            try db.execute(sql: "CREATE TABLE foo (id INTEGER PRIMARY KEY, name TEXT)")
            try db.execute(sql: "CREATE TABLE bar (id INTEGER PRIMARY KEY, fooId INTEGER)")
            
            do {
                let statement = try db.makeStatement(sql: "SELECT name FROM foo")
                let expectedRegion = DatabaseRegion(table: "foo", columns: ["name"])
                XCTAssertEqual(statement.databaseRegion, expectedRegion)
                XCTAssertEqual(statement.databaseRegion.description, "foo(name)")
            }
            do {
                let statement = try db.makeStatement(sql: "SELECT NAME FROM FOO")
                let expectedRegion = DatabaseRegion(table: "foo", columns: ["name"])
                XCTAssertEqual(statement.databaseRegion, expectedRegion)
                XCTAssertEqual(statement.databaseRegion.description, "foo(name)")
            }
            do {
                let statement = try db.makeStatement(sql: "SELECT foo.name FROM foo JOIN bar ON fooId = foo.id")
                let expectedRegion = DatabaseRegion(table: "foo", columns: ["name", "id"])
                    .union(DatabaseRegion(table: "bar", columns: ["fooId"]))
                XCTAssertEqual(statement.databaseRegion, expectedRegion)
                XCTAssertEqual(statement.databaseRegion.description, "bar(fooId),foo(id,name)")
            }
            do {
                let statement = try db.makeStatement(sql: "SELECT FOO.NAME FROM FOO JOIN BAR ON FOOID = FOO.ID")
                let expectedRegion = DatabaseRegion(table: "foo", columns: ["name", "id"])
                    .union(DatabaseRegion(table: "bar", columns: ["fooId"]))
                XCTAssertEqual(statement.databaseRegion, expectedRegion)
                XCTAssertEqual(statement.databaseRegion.description, "bar(fooId),foo(id,name)")
            }
            do {
                let statement = try db.makeStatement(sql: "SELECT COUNT(*) FROM foo")
                if sqlite3_libversion_number() < 3019000 {
                    let expectedRegion = DatabaseRegion.fullDatabase
                    XCTAssertEqual(statement.databaseRegion, expectedRegion)
                    XCTAssertEqual(statement.databaseRegion.description, "full database")
                } else {
                    let expectedRegion = DatabaseRegion(table: "foo")
                    XCTAssertEqual(statement.databaseRegion, expectedRegion)
                    XCTAssertEqual(statement.databaseRegion.description, "foo(*)")
                }
            }
            do {
                let statement = try db.makeStatement(sql: "SELECT COUNT(*) FROM FOO")
                if sqlite3_libversion_number() < 3019000 {
                    let expectedRegion = DatabaseRegion.fullDatabase
                    XCTAssertEqual(statement.databaseRegion, expectedRegion)
                    XCTAssertEqual(statement.databaseRegion.description, "full database")
                } else {
                    let expectedRegion = DatabaseRegion(table: "foo")
                    XCTAssertEqual(statement.databaseRegion, expectedRegion)
                    XCTAssertEqual(statement.databaseRegion.description, "FOO(*)")
                }
            }
        }
    }
    
    func testRegionRowIds() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            try db.execute(sql: "CREATE TABLE foo (id INTEGER PRIMARY KEY, a TEXT)")
            struct Record: TableRecord {
                static let databaseTableName = "foo"
            }
            
            // Undefined rowIds
            
            do {
                let request = Record.all()
                try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)")
            }
            do {
                let request = Record.filter(Column("a") == 1)
                try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)")
            }
            do {
                let request = Record.filter(Column("id") >= 1)
                try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)")
            }
            do {
                let request = Record.filter((Column("id") == 1) || (Column("a") == "foo"))
                try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)")
            }

            // No rowId
            
            do {
                let request = Record.none()
                try XCTAssertEqual(request.databaseRegion(db).description, "empty")
            }
            do {
                let request = Record.filter([Int]().contains(Column("id")))
                try XCTAssertEqual(request.databaseRegion(db).description, "empty")
            }
            do {
                let request = Record.filter([String]().contains(Column("a")))
                try XCTAssertEqual(request.databaseRegion(db).description, "empty")
            }
            do {
                let request = Record.filter(Column("id") == nil)
                try XCTAssertEqual(request.databaseRegion(db).description, "empty")
            }
            do {
                let request = Record.filter(Column("id") === nil)
                try XCTAssertEqual(request.databaseRegion(db).description, "empty")
            }
            do {
                let request = Record.filter(nil == Column("id"))
                try XCTAssertEqual(request.databaseRegion(db).description, "empty")
            }
            do {
                let request = Record.filter(nil === Column("id"))
                try XCTAssertEqual(request.databaseRegion(db).description, "empty")
            }
            do {
                let request = Record.filter((Column("id") == 1) && (Column("id") == 2))
                try XCTAssertEqual(request.databaseRegion(db).description, "empty")
            }
            do {
                let request = Record.filter(key: 1).filter(key: 2)
                try XCTAssertEqual(request.databaseRegion(db).description, "empty")
            }

            // Single rowId
            
            do {
                let request = Record.filter(Column("id") == 1)
                try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)[1]")
            }
            do {
                let request = Record.filter(Column("id") === 1)
                try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)[1]")
            }
            do {
                let request = Record.filter(Column("id") == 1 && Column("a") == "foo")
                try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)[1]")
            }
            do {
                let request = Record.filter(Column.rowID == 1)
                try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)[1]")
            }
            do {
                let request = Record.filter(1 == Column("id"))
                try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)[1]")
            }
            do {
                let request = Record.filter(1 === Column("id"))
                try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)[1]")
            }
            do {
                let request = Record.filter(1 === Column.rowID)
                try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)[1]")
            }
            do {
                let request = Record.filter(key: 1)
                try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)[1]")
            }
            do {
                let request = Record.filter(key: 1).filter(key: 1)
                try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)[1]")
            }
            do {
                let request = Record.filter(key: 1).filter(Column("a") == "foo")
                try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)[1]")
            }

            // Multiple rowIds
            
            do {
                let request = Record.filter(Column("id") == 1 || Column.rowID == 2)
                try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)[1,2]")
            }
            do {
                let request = Record.filter((Column("id") == 1 && Column("a") == "foo") || Column.rowID == 2)
                try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)[1,2]")
            }
            do {
                let request = Record.filter([1, 2, 3].contains(Column("id")))
                try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)[1,2,3]")
            }
            do {
                let request = Record.filter([1, 2, 3].contains(Column.rowID))
                try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)[1,2,3]")
            }
            do {
                let request = Record.filter([1, 2, 3].contains(Column("id")) || [2, 3, 4].contains(Column.rowID))
                try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)[1,2,3,4]")
            }
            do {
                let request = Record.filter([1, 2, 3].contains(Column("id")) && [2, 3, 4].contains(Column.rowID))
                try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)[2,3]")
            }
            do {
                let request = Record.filter(keys: [1, 2, 3])
                try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)[1,2,3]")
            }
        }
    }
    
    func testDatabaseRegionOfJoinedRequests() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            try db.execute(sql: "CREATE TABLE a (id INTEGER PRIMARY KEY, name TEXT)")
            try db.execute(sql: "CREATE TABLE b (id INTEGER PRIMARY KEY, name TEXT, aid INTEGER REFERENCES a(id))")
            try db.execute(sql: "CREATE TABLE c (id INTEGER PRIMARY KEY, name TEXT, aid INTEGER REFERENCES a(id))")
            struct A: TableRecord {
                static let databaseTableName = "a"
                static let b = hasOne(B.self)
                static let c = hasMany(C.self)
            }
            struct B: TableRecord {
                static let databaseTableName = "b"
                static let a = belongsTo(A.self)
            }
            struct C: TableRecord {
                static let databaseTableName = "c"
            }
            do {
                let request = A.filter(key: 1)
                    .including(optional: A.b.filter(key: 2))
                    .including(optional: A.c.filter(keys: [1, 2, 3]))
                // This test will fail when we are able to improve regions of joined requestt
                try XCTAssertEqual(request.databaseRegion(db).description, "a(id,name)[1],b(aid,id,name),c(aid,id,name)")
            }
            do {
                let request = B.filter(key: 1)
                    .including(optional: B.a.filter(key: 2)
                        .including(optional: A.c.filter(keys: [1, 2, 3])))
                // This test will fail when we are able to improve regions of joined requestt
                try XCTAssertEqual(request.databaseRegion(db).description, "a(id,name),b(aid,id,name)[1],c(aid,id,name)")
            }
        }
    }
    
    func testDatabaseRegionOfDerivedRequests() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            try db.execute(sql: "CREATE TABLE foo (id INTEGER PRIMARY KEY, a TEXT)")
            struct Record: TableRecord {
                static let databaseTableName = "foo"
            }
            
            let request = Record.filter(keys: [1, 2, 3])
            try XCTAssertEqual(request.databaseRegion(db).description, "foo(a,id)[1,2,3]")

            do {
                let derivedRequest = AnyFetchRequest(request)
                try XCTAssertEqual(derivedRequest.databaseRegion(db).description, "foo(a,id)[1,2,3]")
            }
            do {
                let derivedRequest: AdaptedFetchRequest = request.adapted { db in SuffixRowAdapter(fromIndex: 1) }
                try XCTAssertEqual(derivedRequest.databaseRegion(db).description, "foo(a,id)[1,2,3]")
            }
        }
    }
    
    func testUpdateStatement() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            try db.execute(sql: "CREATE TABLE foo (id INTEGER PRIMARY KEY, bar TEXT, baz TEXT, qux TEXT)")
            do {
                let statement = try db.makeStatement(sql: "UPDATE foo SET bar = 'bar', baz = 'baz' WHERE id = 1")
                XCTAssertFalse(statement.invalidatesDatabaseSchemaCache)
                XCTAssertEqual(statement.databaseEventKinds.count, 1)
                guard case .update(let tableName, let columnNames) = statement.databaseEventKinds[0] else {
                    XCTFail()
                    return
                }
                XCTAssertEqual(tableName, "foo")
                XCTAssertEqual(columnNames, Set(["bar", "baz"]))
            }
            do {
                let statement = try db.makeStatement(sql: "UPDATE FOO SET BAR = 'bar', BAZ = 'baz' WHERE ID = 1")
                XCTAssertFalse(statement.invalidatesDatabaseSchemaCache)
                XCTAssertEqual(statement.databaseEventKinds.count, 1)
                guard case .update(let tableName, let columnNames) = statement.databaseEventKinds[0] else {
                    XCTFail()
                    return
                }
                XCTAssertEqual(tableName, "foo")
                XCTAssertEqual(columnNames, Set(["bar", "baz"]))
            }
        }
    }
    
    func testRowIdNameInSelectStatement() throws {
        // Here we test that sqlite authorizer gives the "ROWID" name to
        // the rowid column, regardless of its name in the request (rowid, oid, _rowid_)
        //
        // See also testRowIdNameInUpdateStatement
        
        guard sqlite3_libversion_number() < 3019003 else {
            // This test fails on SQLite 3.19.3 (iOS 11.2) and SQLite 3.21.0 (custom build),
            // but succeeds on SQLite 3.16.0 (iOS 10.3.1).
            // TODO: evaluate the consequences
            return
        }
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            try db.execute(sql: "CREATE TABLE foo (name TEXT)")
            do {
                let statement = try db.makeStatement(sql: "SELECT rowid FROM FOO")
                let expectedRegion = DatabaseRegion(table: "foo", columns: ["ROWID"])
                XCTAssertEqual(statement.databaseRegion, expectedRegion)
                XCTAssertEqual(statement.databaseRegion.description, "foo(ROWID)")
            }
            do {
                let statement = try db.makeStatement(sql: "SELECT _ROWID_ FROM FOO")
                let expectedRegion = DatabaseRegion(table: "foo", columns: ["ROWID"])
                XCTAssertEqual(statement.databaseRegion, expectedRegion)
                XCTAssertEqual(statement.databaseRegion.description, "foo(ROWID)")
            }
            do {
                let statement = try db.makeStatement(sql: "SELECT oID FROM FOO")
                let expectedRegion = DatabaseRegion(table: "foo", columns: ["ROWID"])
                XCTAssertEqual(statement.databaseRegion, expectedRegion)
                XCTAssertEqual(statement.databaseRegion.description, "foo(ROWID)")
            }
        }
    }

    func testRowIdNameInUpdateStatement() throws {
        // Here we test that sqlite authorizer gives the "ROWID" name to
        // the rowid column, regardless of its name in the request (rowid, oid, _rowid_)
        //
        // See also testRowIdNameInSelectStatement
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            try db.execute(sql: "CREATE TABLE foo (name TEXT)")
            do {
                let statement = try db.makeStatement(sql: "UPDATE foo SET rowid = 1")
                XCTAssertEqual(statement.databaseEventKinds.count, 1)
                guard case .update(let tableName, let columnNames) = statement.databaseEventKinds[0] else {
                    XCTFail()
                    return
                }
                XCTAssertEqual(tableName, "foo")
                XCTAssertEqual(columnNames, ["ROWID"])
            }
            do {
                let statement = try db.makeStatement(sql: "UPDATE foo SET _ROWID_ = 1")
                XCTAssertEqual(statement.databaseEventKinds.count, 1)
                guard case .update(let tableName, let columnNames) = statement.databaseEventKinds[0] else {
                    XCTFail()
                    return
                }
                XCTAssertEqual(tableName, "foo")
                XCTAssertEqual(columnNames, ["ROWID"])
            }
            do {
                let statement = try db.makeStatement(sql: "UPDATE foo SET oID = 1")
                XCTAssertEqual(statement.databaseEventKinds.count, 1)
                guard case .update(let tableName, let columnNames) = statement.databaseEventKinds[0] else {
                    XCTFail()
                    return
                }
                XCTAssertEqual(tableName, "foo")
                XCTAssertEqual(columnNames, ["ROWID"])
            }
        }
    }

    func testInsertStatement() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            try db.execute(sql: "CREATE TABLE foo (id INTEGER, bar TEXT, baz TEXT, qux TEXT)")
            let statement = try db.makeStatement(sql: "INSERT INTO foo (id, bar) VALUES (1, 'bar')")
            XCTAssertFalse(statement.invalidatesDatabaseSchemaCache)
            XCTAssertEqual(statement.databaseEventKinds.count, 1)
            guard case .insert(let tableName) = statement.databaseEventKinds[0] else {
                XCTFail()
                return
            }
            XCTAssertEqual(tableName, "foo")
        }
    }

    func testDeleteStatement() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            try db.execute(sql: "CREATE TABLE foo (id INTEGER, bar TEXT, baz TEXT, qux TEXT)")
            let statement = try db.makeStatement(sql: "DELETE FROM foo")
            XCTAssertFalse(statement.invalidatesDatabaseSchemaCache)
            XCTAssertEqual(statement.databaseEventKinds.count, 1)
            guard case .delete(let tableName) = statement.databaseEventKinds[0] else {
                XCTFail()
                return
            }
            XCTAssertEqual(tableName, "foo")
        }
    }

    func testUpdateStatementInvalidatesDatabaseSchemaCache() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            do {
                let statement = try db.makeStatement(sql: "CREATE TABLE foo (id INTEGER)")
                XCTAssertTrue(statement.invalidatesDatabaseSchemaCache)
                try statement.execute()
            }
            do {
                let statement = try db.makeStatement(sql: "ALTER TABLE foo ADD COLUMN name TEXT")
                XCTAssertTrue(statement.invalidatesDatabaseSchemaCache)
            }
            do {
                let statement = try db.makeStatement(sql: "DROP TABLE foo")
                XCTAssertTrue(statement.invalidatesDatabaseSchemaCache)
            }
        }
    }
    
    func testRegionIsModifiedByDatabaseEvent() {
        do {
            // Empty selection
            let region = DatabaseRegion()
            XCTAssertEqual(region.description, "empty")
            
            do {
                let eventKind = DatabaseEventKind.insert(tableName: "foo")
                XCTAssertFalse(region.isModified(byEventsOfKind: eventKind))
                // Can't test for individual events due to DatabaseRegion.isModified(by:) precondition
            }

            do {
                let eventKind = DatabaseEventKind.delete(tableName: "foo")
                XCTAssertFalse(region.isModified(byEventsOfKind: eventKind))
                // Can't test for individual events due to DatabaseRegion.isModified(by:) precondition
            }
            
            do {
                let eventKind = DatabaseEventKind.update(tableName: "foo", columnNames: ["a", "b"])
                XCTAssertFalse(region.isModified(byEventsOfKind: eventKind))
                // Can't test for individual events due to DatabaseRegion.isModified(by:) precondition
            }
        }
        
        do {
            // Full database selection
            let region = DatabaseRegion.fullDatabase
            XCTAssertEqual(region.description, "full database")
            
            do {
                let tableName = "foo"
                tableName.withCString { tableNameCString in
                    let eventKind = DatabaseEventKind.insert(tableName: tableName)
                    let event = DatabaseEvent(kind: .insert, rowID: 1, databaseNameCString: nil, tableNameCString: tableNameCString)
                    XCTAssertTrue(region.isModified(byEventsOfKind: eventKind))
                    XCTAssertTrue(region.isModified(by: event))
                }
            }
            
            do {
                let tableName = "foo"
                tableName.withCString { tableNameCString in
                    let eventKind = DatabaseEventKind.delete(tableName: tableName)
                    let event = DatabaseEvent(kind: .delete, rowID: 1, databaseNameCString: nil, tableNameCString: tableNameCString)
                    XCTAssertTrue(region.isModified(byEventsOfKind: eventKind))
                    XCTAssertTrue(region.isModified(by: event))
                }
            }
            
            do {
                let tableName = "foo"
                tableName.withCString { tableNameCString in
                    let eventKind = DatabaseEventKind.update(tableName: tableName, columnNames: ["a", "b"])
                    let event = DatabaseEvent(kind: .update, rowID: 1, databaseNameCString: nil, tableNameCString: tableNameCString)
                    XCTAssertTrue(region.isModified(byEventsOfKind: eventKind))
                    XCTAssertTrue(region.isModified(by: event))
                }
            }
        }
        
        do {
            // Complex selection
            let region = DatabaseRegion(table: "foo")
                .union(DatabaseRegion(table: "bar", columns: ["a"])
                    .intersection(DatabaseRegion(table: "bar", rowIds: [1])))
            XCTAssertEqual(region.description, "bar(a)[1],foo(*)")
            
            do {
                let tableName = "foo"
                tableName.withCString { tableNameCString in
                    let eventKind = DatabaseEventKind.insert(tableName: tableName)
                    let event1 = DatabaseEvent(kind: .insert, rowID: 1, databaseNameCString: nil, tableNameCString: tableNameCString)
                    let event2 = DatabaseEvent(kind: .insert, rowID: 2, databaseNameCString: nil, tableNameCString: tableNameCString)
                    XCTAssertTrue(region.isModified(byEventsOfKind: eventKind))
                    XCTAssertTrue(region.isModified(by: event1))
                    XCTAssertTrue(region.isModified(by: event2))
                }
            }
            
            do {
                let tableName = "foo"
                tableName.withCString { tableNameCString in
                    let eventKind = DatabaseEventKind.delete(tableName: tableName)
                    let event1 = DatabaseEvent(kind: .delete, rowID: 1, databaseNameCString: nil, tableNameCString: tableNameCString)
                    let event2 = DatabaseEvent(kind: .delete, rowID: 2, databaseNameCString: nil, tableNameCString: tableNameCString)
                    XCTAssertTrue(region.isModified(byEventsOfKind: eventKind))
                    XCTAssertTrue(region.isModified(by: event1))
                    XCTAssertTrue(region.isModified(by: event2))
                }
            }
            
            do {
                let tableName = "foo"
                tableName.withCString { tableNameCString in
                    let eventKind = DatabaseEventKind.update(tableName: tableName, columnNames: ["a", "b"])
                    let event1 = DatabaseEvent(kind: .update, rowID: 1, databaseNameCString: nil, tableNameCString: tableNameCString)
                    let event2 = DatabaseEvent(kind: .update, rowID: 2, databaseNameCString: nil, tableNameCString: tableNameCString)
                    XCTAssertTrue(region.isModified(byEventsOfKind: eventKind))
                    XCTAssertTrue(region.isModified(by: event1))
                    XCTAssertTrue(region.isModified(by: event2))
                }
            }
            
            do {
                let tableName = "bar"
                tableName.withCString { tableNameCString in
                    let eventKind = DatabaseEventKind.insert(tableName: tableName)
                    let event1 = DatabaseEvent(kind: .insert, rowID: 1, databaseNameCString: nil, tableNameCString: tableNameCString)
                    let event2 = DatabaseEvent(kind: .insert, rowID: 2, databaseNameCString: nil, tableNameCString: tableNameCString)
                    XCTAssertTrue(region.isModified(byEventsOfKind: eventKind))
                    XCTAssertTrue(region.isModified(by: event1))
                    XCTAssertFalse(region.isModified(by: event2))
                }
            }
            
            do {
                let tableName = "bar"
                tableName.withCString { tableNameCString in
                    let eventKind = DatabaseEventKind.delete(tableName: tableName)
                    let event1 = DatabaseEvent(kind: .delete, rowID: 1, databaseNameCString: nil, tableNameCString: tableNameCString)
                    let event2 = DatabaseEvent(kind: .delete, rowID: 2, databaseNameCString: nil, tableNameCString: tableNameCString)
                    XCTAssertTrue(region.isModified(byEventsOfKind: eventKind))
                    XCTAssertTrue(region.isModified(by: event1))
                    XCTAssertFalse(region.isModified(by: event2))
                }
            }
            
            do {
                let tableName = "bar"
                tableName.withCString { tableNameCString in
                    let eventKind = DatabaseEventKind.update(tableName: tableName, columnNames: ["a", "b"])
                    let event1 = DatabaseEvent(kind: .update, rowID: 1, databaseNameCString: nil, tableNameCString: tableNameCString)
                    let event2 = DatabaseEvent(kind: .update, rowID: 2, databaseNameCString: nil, tableNameCString: tableNameCString)
                    XCTAssertTrue(region.isModified(byEventsOfKind: eventKind))
                    XCTAssertTrue(region.isModified(by: event1))
                    XCTAssertFalse(region.isModified(by: event2))
                }
            }
            
            do {
                let eventKind = DatabaseEventKind.update(tableName: "bar", columnNames: ["b", "c"])
                XCTAssertFalse(region.isModified(byEventsOfKind: eventKind))
                // Can't test for individual events due to DatabaseRegion.isModified(by:) precondition
            }
            
            do {
                let eventKind = DatabaseEventKind.insert(tableName: "qux")
                XCTAssertFalse(region.isModified(byEventsOfKind: eventKind))
                // Can't test for individual events due to DatabaseRegion.isModified(by:) precondition
            }
            
            do {
                let eventKind = DatabaseEventKind.delete(tableName: "qux")
                XCTAssertFalse(region.isModified(byEventsOfKind: eventKind))
                // Can't test for individual events due to DatabaseRegion.isModified(by:) precondition
            }
            
            do {
                let eventKind = DatabaseEventKind.update(tableName: "qux", columnNames: ["a", "b"])
                XCTAssertFalse(region.isModified(byEventsOfKind: eventKind))
                // Can't test for individual events due to DatabaseRegion.isModified(by:) precondition
            }
        }
    }
    
    // Regression test for https://github.com/groue/GRDB.swift/issues/514
    func testIssue514() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.write { db in
            try db.execute(sql: """
                CREATE TABLE a (id INTEGER PRIMARY KEY, name TEXT);
                CREATE TABLE b (id TEXT, name TEXT);
                """)
            
            // INTEGER PRIMARY KEY
            do {
                // TODO: contact SQLite and ask if this test is expected to fail
//                let statement = try db.makeStatement(sql: "SELECT id FROM a")
//                let expectedRegion = DatabaseRegion(table: "a", columns: ["id"])
//                XCTAssertEqual(statement.databaseRegion, expectedRegion)
            }
            do {
                let statement = try db.makeStatement(sql: "SELECT name FROM a")
                let expectedRegion = DatabaseRegion(table: "a", columns: ["name"])
                XCTAssertEqual(statement.databaseRegion, expectedRegion)
            }
            do {
                let statement = try db.makeStatement(sql: "SELECT id, name FROM a")
                let expectedRegion = DatabaseRegion(table: "a", columns: ["id", "name"])
                XCTAssertEqual(statement.databaseRegion, expectedRegion)
            }
            
            // TEXT primary key
            do {
                let statement = try db.makeStatement(sql: "SELECT id FROM b")
                let expectedRegion = DatabaseRegion(table: "b", columns: ["id"])
                XCTAssertEqual(statement.databaseRegion, expectedRegion)
            }
            do {
                let statement = try db.makeStatement(sql: "SELECT name FROM b")
                let expectedRegion = DatabaseRegion(table: "b", columns: ["name"])
                XCTAssertEqual(statement.databaseRegion, expectedRegion)
            }
            do {
                let statement = try db.makeStatement(sql: "SELECT id, name FROM b")
                let expectedRegion = DatabaseRegion(table: "b", columns: ["id", "name"])
                XCTAssertEqual(statement.databaseRegion, expectedRegion)
            }
        }
    }
}
