<commit_before>import XCTest
#if GRDBCUSTOMSQLITE
    import GRDBCustomSQLite
#else
    #if SWIFT_PACKAGE
        import CSQLite
    #else
        import SQLite3
    #endif
    import GRDB
#endif

class ValueObservationReadonlyTests: GRDBTestCase {
    
    func testReadOnlyObservation() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") }
        
        var counts: [Int] = []
        let notificationExpectation = expectation(description: "notification")
        notificationExpectation.assertForOverFulfill = true
        notificationExpectation.expectedFulfillmentCount = 2
        
        let observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: {
            try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")!
        })
        let observer = observation.start(
            in: dbQueue,
            onError: { error in XCTFail("Unexpected error: \(error)") },
            onChange: { count in
                counts.append(count)
                notificationExpectation.fulfill()
        })
        try withExtendedLifetime(observer) {
            try dbQueue.write {
                try $0.execute(sql: "INSERT INTO t DEFAULT VALUES")
            }
            
            waitForExpectations(timeout: 1, handler: nil)
            XCTAssertEqual(counts, [0, 1])
        }
    }
    
    func testWriteObservationFailsByDefaultWithErrorHandling() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") }
        
        let observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { db -> Int in
            try db.execute(sql: "INSERT INTO t DEFAULT VALUES")
            return 0
        })
        
        var error: DatabaseError!
        _ = observation.start(
            in: dbQueue,
            onError: { error = $0 as? DatabaseError },
            onChange: { _ in fatalError() })
        
        XCTAssertEqual(error.resultCode, .SQLITE_READONLY)
        XCTAssertEqual(error.message, "attempt to write a readonly database")
        XCTAssertEqual(error.sql!, "INSERT INTO t DEFAULT VALUES")
        XCTAssertEqual(error.description, "SQLite error 8 with statement `INSERT INTO t DEFAULT VALUES`: attempt to write a readonly database")
    }
    
    func testWriteObservation() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") }
        
        var counts: [Int] = []
        let notificationExpectation = expectation(description: "notification")
        notificationExpectation.assertForOverFulfill = true
        notificationExpectation.expectedFulfillmentCount = 2
        
        var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { db -> Int in
            XCTAssert(db.isInsideTransaction, "expected a wrapping transaction")
            try db.execute(sql: "CREATE TEMPORARY TABLE temp AS SELECT * FROM t")
            let result = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM temp")!
            try db.execute(sql: "DROP TABLE temp")
            return result
        })
        observation.requiresWriteAccess = true
        let observer = observation.start(
            in: dbQueue,
            onError: { error in XCTFail("Unexpected error: \(error)") },
            onChange: { count in
                counts.append(count)
                notificationExpectation.fulfill()
        })
        try withExtendedLifetime(observer) {
            try dbQueue.write {
                try $0.execute(sql: "INSERT INTO t DEFAULT VALUES")
            }
            
            waitForExpectations(timeout: 1, handler: nil)
            XCTAssertEqual(counts, [0, 1])
        }
    }
    
    func testWriteObservationIsWrappedInSavepointWithErrorHandling() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.write {
            try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)")
        }
        
        struct TestError: Error { }
        var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { db in
            try db.execute(sql: "INSERT INTO t DEFAULT VALUES")
            throw TestError()
        })
        observation.requiresWriteAccess = true
        
        var error: Error?
        _ = observation.start(
            in: dbQueue,
            onError: { error = $0 },
            onChange: { _ in fatalError() })
        guard error is TestError else {
            XCTFail("Expected TestError")
            return
        }
        
        let count = try dbQueue.read { try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! }
        XCTAssertEqual(count, 0)
    }
}<commit_msg>More test refactoring with assertValueObservationRecordingMatch<commit_after>import XCTest
#if GRDBCUSTOMSQLITE
import GRDBCustomSQLite
#else
#if SWIFT_PACKAGE
import CSQLite
#else
import SQLite3
#endif
import GRDB
#endif

class ValueObservationReadonlyTests: GRDBTestCase {
    
    func testReadOnlyObservation() throws {
        func test(writer: DatabaseWriter, observation: ValueObservation<ValueReducers.Fetch<Int>>) throws {
            try writer.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") }
            let recorder = observation.record(in: writer)
            try writer.write {
                try $0.execute(sql: "INSERT INTO t DEFAULT VALUES")
            }
            
            let expectedValues = [0, 1]
            let values = try wait(
                for: recorder
                    .prefix(expectedValues.count + 2 /* async pool may perform double initial fetch */)
                    .inverted,
                timeout: 0.5)
            try assertValueObservationRecordingMatch(
                recorded: values,
                expected: expectedValues,
                "\(type(of: writer)), \(observation.scheduling)")
        }
        
        let schedulings: [ValueObservationScheduling] = [
            .mainQueue,
            .async(onQueue: .main),
            .unsafe
        ]
        
        for scheduling in schedulings {
            var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: {
                try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")!
            })
            observation.scheduling = scheduling
            
            try test(writer: DatabaseQueue(), observation: observation)
            try test(writer: makeDatabaseQueue(), observation: observation)
            try test(writer: makeDatabasePool(), observation: observation)
        }
    }
    
    func testWriteObservationFailsByDefaultWithErrorHandling() throws {
        func test(writer: DatabaseWriter, observation: ValueObservation<ValueReducers.Fetch<Int>>) throws {
            try writer.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") }
            let recorder = observation.record(in: writer)
            
            let (values, error) = try wait(for: recorder.failure(), timeout: 0.5)
            XCTAssert(values.isEmpty)
            do {
                throw error
            } catch let error as DatabaseError {
                XCTAssertEqual(error.resultCode, .SQLITE_READONLY)
                XCTAssertEqual(error.message, "attempt to write a readonly database")
                XCTAssertEqual(error.sql!, "INSERT INTO t DEFAULT VALUES")
                XCTAssertEqual(error.description, "SQLite error 8 with statement `INSERT INTO t DEFAULT VALUES`: attempt to write a readonly database")
            }
        }
        
        let schedulings: [ValueObservationScheduling] = [
            .mainQueue,
            .async(onQueue: .main),
            .unsafe
        ]
        
        for scheduling in schedulings {
            var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { db -> Int in
                try db.execute(sql: "INSERT INTO t DEFAULT VALUES")
                return 0
            })
            observation.scheduling = scheduling
            
            try test(writer: DatabaseQueue(), observation: observation)
            try test(writer: makeDatabaseQueue(), observation: observation)
            try test(writer: makeDatabasePool(), observation: observation)
        }
    }
    
    func testWriteObservation() throws {
        func test(writer: DatabaseWriter, observation: ValueObservation<ValueReducers.Fetch<Int>>) throws {
            try writer.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") }
            let recorder = observation.record(in: writer)
            try writer.write {
                try $0.execute(sql: "INSERT INTO t DEFAULT VALUES")
            }
            
            let expectedValues = [0, 1]
            let values = try wait(for: recorder.next(2), timeout: 0.5)
            try assertValueObservationRecordingMatch(
                recorded: values,
                expected: expectedValues,
                "\(type(of: writer)), \(observation.scheduling)")
        }
        
        let schedulings: [ValueObservationScheduling] = [
            .mainQueue,
            .async(onQueue: .main),
            .unsafe
        ]
        
        for scheduling in schedulings {
            var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { db -> Int in
                XCTAssert(db.isInsideTransaction, "expected a wrapping transaction")
                try db.execute(sql: "CREATE TEMPORARY TABLE temp AS SELECT * FROM t")
                let result = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM temp")!
                try db.execute(sql: "DROP TABLE temp")
                return result
            })
            observation.requiresWriteAccess = true
            observation.scheduling = scheduling
            
            try test(writer: DatabaseQueue(), observation: observation)
            try test(writer: makeDatabaseQueue(), observation: observation)
            try test(writer: makeDatabasePool(), observation: observation)
        }
    }
    
    func testWriteObservationIsWrappedInSavepointWithErrorHandling() throws {
        struct TestError: Error { }
        
        func test(writer: DatabaseWriter, observation: ValueObservation<ValueReducers.Fetch<Void>>) throws {
            try writer.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") }
            let recorder = observation.record(in: writer)
            
            let (_, error) = try wait(for: recorder.failure(), timeout: 0.5)
            do {
                throw error
            } catch is TestError {
                let count = try writer.read { try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! }
                XCTAssertEqual(count, 0)
            }
        }
        
        let schedulings: [ValueObservationScheduling] = [
            .mainQueue,
            .async(onQueue: .main),
            .unsafe
        ]
        
        for scheduling in schedulings {
            var observation = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { db in
                try db.execute(sql: "INSERT INTO t DEFAULT VALUES")
                throw TestError()
            })
            observation.requiresWriteAccess = true
            observation.scheduling = scheduling
            
            try test(writer: DatabaseQueue(), observation: observation)
            try test(writer: makeDatabaseQueue(), observation: observation)
            try test(writer: makeDatabasePool(), observation: observation)
        }
    }
}
{'fraction_non_alphanumeric': 0.08508652867322701, 'fraction_numerical': 0.0040719375636240245, 'mean_word_length': 2.002801833927662, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4663946', 'n_tokens_mistral': 3091, 'n_tokens_neox': 2945, 'n_words': 813}