diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index 68a630095b..9e1fa3c21f 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -279,6 +279,9 @@ 564CE5AE21B8FAB400652B19 /* DatabaseRegionObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564CE5AB21B8FAB400652B19 /* DatabaseRegionObservation.swift */; }; 564CE5BE21B8FFA300652B19 /* DatabaseRegionObservationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564CE5BD21B8FFA300652B19 /* DatabaseRegionObservationTests.swift */; }; 564CE5BF21B8FFA300652B19 /* DatabaseRegionObservationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564CE5BD21B8FFA300652B19 /* DatabaseRegionObservationTests.swift */; }; + 564D4F7E261C6DC200F55856 /* CaseInsensitiveIdentifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564D4F7D261C6DC200F55856 /* CaseInsensitiveIdentifierTests.swift */; }; + 564D4F7F261C6DC200F55856 /* CaseInsensitiveIdentifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564D4F7D261C6DC200F55856 /* CaseInsensitiveIdentifierTests.swift */; }; + 564D4F80261C6DC200F55856 /* CaseInsensitiveIdentifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564D4F7D261C6DC200F55856 /* CaseInsensitiveIdentifierTests.swift */; }; 564E73DF203D50B9000C443C /* JoinSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564E73DE203D50B9000C443C /* JoinSupportTests.swift */; }; 564E73E0203D50B9000C443C /* JoinSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564E73DE203D50B9000C443C /* JoinSupportTests.swift */; }; 564F9C1E1F069B4E00877A00 /* DatabaseAggregateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564F9C1D1F069B4E00877A00 /* DatabaseAggregateTests.swift */; }; @@ -469,6 +472,10 @@ 56703297212B5450007D270F /* DatabaseUUIDEncodingStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56703290212B544F007D270F /* DatabaseUUIDEncodingStrategyTests.swift */; }; 56703298212B5450007D270F /* DatabaseUUIDEncodingStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56703290212B544F007D270F /* DatabaseUUIDEncodingStrategyTests.swift */; }; 567156181CB142AA007DC145 /* DatabaseQueueReadOnlyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567156151CB142AA007DC145 /* DatabaseQueueReadOnlyTests.swift */; }; + 56717271261C68E900423B6F /* CaseInsensitiveIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56717270261C68E900423B6F /* CaseInsensitiveIdentifier.swift */; }; + 56717272261C68EA00423B6F /* CaseInsensitiveIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56717270261C68E900423B6F /* CaseInsensitiveIdentifier.swift */; }; + 56717273261C68EA00423B6F /* CaseInsensitiveIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56717270261C68E900423B6F /* CaseInsensitiveIdentifier.swift */; }; + 56717274261C68EA00423B6F /* CaseInsensitiveIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56717270261C68E900423B6F /* CaseInsensitiveIdentifier.swift */; }; 5671FC201DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5671FC1F1DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift */; }; 5671FC231DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5671FC1F1DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift */; }; 5671FC261DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5671FC1F1DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift */; }; @@ -1348,6 +1355,7 @@ 564CE59621B7A8B500652B19 /* RemoveDuplicates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoveDuplicates.swift; sourceTree = ""; }; 564CE5AB21B8FAB400652B19 /* DatabaseRegionObservation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseRegionObservation.swift; sourceTree = ""; }; 564CE5BD21B8FFA300652B19 /* DatabaseRegionObservationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseRegionObservationTests.swift; sourceTree = ""; }; + 564D4F7D261C6DC200F55856 /* CaseInsensitiveIdentifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaseInsensitiveIdentifierTests.swift; sourceTree = ""; }; 564E73DE203D50B9000C443C /* JoinSupportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinSupportTests.swift; sourceTree = ""; }; 564F9C1D1F069B4E00877A00 /* DatabaseAggregateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAggregateTests.swift; sourceTree = ""; }; 564F9C2C1F075DD200877A00 /* DatabaseFunction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseFunction.swift; sourceTree = ""; }; @@ -1419,6 +1427,7 @@ 56703290212B544F007D270F /* DatabaseUUIDEncodingStrategyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseUUIDEncodingStrategyTests.swift; sourceTree = ""; }; 567156151CB142AA007DC145 /* DatabaseQueueReadOnlyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueReadOnlyTests.swift; sourceTree = ""; }; 567156701CB18050007DC145 /* EncryptionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncryptionTests.swift; sourceTree = ""; }; + 56717270261C68E900423B6F /* CaseInsensitiveIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaseInsensitiveIdentifier.swift; sourceTree = ""; }; 5671FC1F1DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS3TokenizerDescriptor.swift; sourceTree = ""; }; 5672DE581CDB72520022BA81 /* DatabaseQueueBackupTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueBackupTests.swift; sourceTree = ""; }; 5672DE661CDB751D0022BA81 /* DatabasePoolBackupTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabasePoolBackupTests.swift; sourceTree = ""; }; @@ -2072,6 +2081,7 @@ 5659F4861EA8D94E004A4992 /* Utils */ = { isa = PBXGroup; children = ( + 56717270261C68E900423B6F /* CaseInsensitiveIdentifier.swift */, 563EF4492161F179007DAACD /* Inflections.swift */, 569BBA482291707D00478429 /* Inflections+English.swift */, 566BE7172342542F00A8254B /* LockedBox.swift */, @@ -2150,6 +2160,7 @@ 569978D31B539038005EBEED /* Private */ = { isa = PBXGroup; children = ( + 564D4F7D261C6DC200F55856 /* CaseInsensitiveIdentifierTests.swift */, 563363CF1C943D13000BE133 /* DatabasePoolReleaseMemoryTests.swift */, 569531281C908A5B00CF1A2B /* DatabasePoolSchemaCacheTests.swift */, 563363D41C94484E000BE133 /* DatabaseQueueReleaseMemoryTests.swift */, @@ -2980,6 +2991,7 @@ 565490BC1D5AE236005622CB /* DatabaseSchemaCache.swift in Sources */, 563EF42F2161180D007DAACD /* AssociationAggregate.swift in Sources */, 565490BA1D5AE236005622CB /* DatabaseQueue.swift in Sources */, + 56717273261C68EA00423B6F /* CaseInsensitiveIdentifier.swift in Sources */, 56781B0D243F86E600650A83 /* Refinable.swift in Sources */, 56256EDB25D1B316008C2BDD /* ForeignKey.swift in Sources */, 565490CD1D5AE252005622CB /* Date.swift in Sources */, @@ -3119,6 +3131,7 @@ 560D92481C672C4B00F4F92B /* PersistableRecord.swift in Sources */, 5613ED4521A95B2C00DC7A68 /* ValueReducer.swift in Sources */, 560D92431C672C3E00F4F92B /* StatementColumnConvertible.swift in Sources */, + 56717272261C68EA00423B6F /* CaseInsensitiveIdentifier.swift in Sources */, 5613ED3621A95A5C00DC7A68 /* Map.swift in Sources */, 56E9FAD8221053DD00C703A8 /* SQL.swift in Sources */, 56781B0C243F86E600650A83 /* Refinable.swift in Sources */, @@ -3269,6 +3282,7 @@ 56419C5924A51999004967E1 /* Finished.swift in Sources */, 56A2386A1B9C74A90082EB20 /* RecordSubClassTests.swift in Sources */, 56A2383E1B9C74A90082EB20 /* DatabaseValueTests.swift in Sources */, + 564D4F7F261C6DC200F55856 /* CaseInsensitiveIdentifierTests.swift in Sources */, 567156181CB142AA007DC145 /* DatabaseQueueReadOnlyTests.swift in Sources */, 56EA86951C91DFE7002BB4DF /* DatabaseReaderTests.swift in Sources */, 56A8C2471D1918F00096E9D4 /* FoundationNSUUIDTests.swift in Sources */, @@ -3499,6 +3513,7 @@ 56FEB8F8248403000081AF83 /* DatabaseTraceTests.swift in Sources */, 56419C5124A51998004967E1 /* Finished.swift in Sources */, 56176C5E1EACCCC7000F3F2B /* FTS5WrapperTokenizerTests.swift in Sources */, + 564D4F7E261C6DC200F55856 /* CaseInsensitiveIdentifierTests.swift in Sources */, 56FEE7FB1F47253700D930EA /* TableRecordTests.swift in Sources */, 56D496641D81304E008276D7 /* FoundationUUIDTests.swift in Sources */, 56D496921D81316E008276D7 /* RowFromDictionaryLiteralTests.swift in Sources */, @@ -3709,6 +3724,7 @@ AAA4DCDF230F1E0600C74B15 /* PersistableRecord.swift in Sources */, AAA4DCE0230F1E0600C74B15 /* ValueReducer.swift in Sources */, AAA4DCE2230F1E0600C74B15 /* StatementColumnConvertible.swift in Sources */, + 56717274261C68EA00423B6F /* CaseInsensitiveIdentifier.swift in Sources */, AAA4DCE3230F1E0600C74B15 /* Map.swift in Sources */, AAA4DCE5230F1E0600C74B15 /* SQL.swift in Sources */, 56781B0E243F86E600650A83 /* Refinable.swift in Sources */, @@ -3859,6 +3875,7 @@ 56419C6124A5199B004967E1 /* Finished.swift in Sources */, AAA4DD79230F262000C74B15 /* RecordSubClassTests.swift in Sources */, AAA4DD7A230F262000C74B15 /* DatabaseValueTests.swift in Sources */, + 564D4F80261C6DC200F55856 /* CaseInsensitiveIdentifierTests.swift in Sources */, AAA4DD7B230F262000C74B15 /* DatabaseQueueReadOnlyTests.swift in Sources */, AAA4DD7C230F262000C74B15 /* DatabaseReaderTests.swift in Sources */, AAA4DD7D230F262000C74B15 /* FoundationNSUUIDTests.swift in Sources */, @@ -4069,6 +4086,7 @@ 56A238831B9C75030082EB20 /* DatabaseQueue.swift in Sources */, 5605F1671C672E4000235C62 /* NSNumber.swift in Sources */, 56E9FADA221053DD00C703A8 /* SQL.swift in Sources */, + 56717271261C68E900423B6F /* CaseInsensitiveIdentifier.swift in Sources */, C96C0F2B2084A442006B2981 /* SQLiteDateParser.swift in Sources */, 56781B0B243F86E600650A83 /* Refinable.swift in Sources */, 56A238871B9C75030082EB20 /* Row.swift in Sources */, diff --git a/GRDB/Core/Database+Schema.swift b/GRDB/Core/Database+Schema.swift index 0b2ac318fe..0693431722 100644 --- a/GRDB/Core/Database+Schema.swift +++ b/GRDB/Core/Database+Schema.swift @@ -421,14 +421,14 @@ extension Database { return foreignKeys } - /// Returns the actual name of the database table, in the main or temp schema. + /// Returns the actual name of the database table, in the main or temp + /// schema, or nil if the table does not exist. /// /// - throws: A DatabaseError if table does not exist. - func canonicalTableName(_ tableName: String) throws -> String { + func canonicalTableName(_ tableName: String) throws -> String? { // SQLite has temporary tables shadow main ones try schema(.temp).canonicalName(tableName, ofType: .table) ?? schema(.main).canonicalName(tableName, ofType: .table) - ?? { throw DatabaseError.noSuchTable(tableName) }() } func schema(_ schemaID: SchemaIdentifier) throws -> SchemaInfo { @@ -865,7 +865,9 @@ struct SchemaInfo: Equatable { /// try db.schema().canonicalName("foobar", ofType: .table) // "FooBar" func canonicalName(_ name: String, ofType type: SchemaObjectType) -> String? { let name = name.lowercased() - return objects.first { $0.name.lowercased() == name }?.name + return objects + .first { $0.type == type.rawValue && $0.name.lowercased() == name }? + .name } private struct SchemaObject: Codable, Hashable, FetchableRecord { diff --git a/GRDB/Core/DatabaseRegion.swift b/GRDB/Core/DatabaseRegion.swift index ae145c868b..033349beb4 100644 --- a/GRDB/Core/DatabaseRegion.swift +++ b/GRDB/Core/DatabaseRegion.swift @@ -30,9 +30,9 @@ /// let request = Player.filter(key: 1) /// let region = try request.databaseRegion(db) public struct DatabaseRegion: CustomStringConvertible, Equatable { - private let tableRegions: [String: TableRegion]? + private let tableRegions: [CaseInsensitiveIdentifier: TableRegion]? - private init(tableRegions: [String: TableRegion]?) { + private init(tableRegions: [CaseInsensitiveIdentifier: TableRegion]?) { self.tableRegions = tableRegions } @@ -64,16 +64,20 @@ public struct DatabaseRegion: CustomStringConvertible, Equatable { /// /// - parameter table: A table name. public init(table: String) { + let table = CaseInsensitiveIdentifier(rawValue: table) self.init(tableRegions: [table: TableRegion(columns: nil, rowIds: nil)]) } /// Full columns in a table: (some columns in a table) × (all rows) init(table: String, columns: Set) { + let table = CaseInsensitiveIdentifier(rawValue: table) + let columns = Set(columns.map(CaseInsensitiveIdentifier.init)) self.init(tableRegions: [table: TableRegion(columns: columns, rowIds: nil)]) } /// Full rows in a table: (all columns in a table) × (some rows) init(table: String, rowIds: Set) { + let table = CaseInsensitiveIdentifier(rawValue: table) self.init(tableRegions: [table: TableRegion(columns: nil, rowIds: rowIds)]) } @@ -86,7 +90,7 @@ public struct DatabaseRegion: CustomStringConvertible, Equatable { guard let tableRegions = tableRegions else { return other } guard let otherTableRegions = other.tableRegions else { return self } - var tableRegionsIntersection: [String: TableRegion] = [:] + var tableRegionsIntersection: [CaseInsensitiveIdentifier: TableRegion] = [:] for (table, tableRegion) in tableRegions { guard let otherTableRegion = otherTableRegions .first(where: { (otherTable, _) in otherTable == table })? @@ -105,6 +109,7 @@ public struct DatabaseRegion: CustomStringConvertible, Equatable { return DatabaseRegion(table: table, rowIds: rowIds) } + let table = CaseInsensitiveIdentifier(rawValue: table) guard let tableRegion = tableRegions[table] else { return self } @@ -123,7 +128,7 @@ public struct DatabaseRegion: CustomStringConvertible, Equatable { guard let tableRegions = tableRegions else { return .fullDatabase } guard let otherTableRegions = other.tableRegions else { return .fullDatabase } - var tableRegionsUnion: [String: TableRegion] = [:] + var tableRegionsUnion: [CaseInsensitiveIdentifier: TableRegion] = [:] let tableNames = Set(tableRegions.keys).union(Set(otherTableRegions.keys)) for table in tableNames { let tableRegion = tableRegions[table] @@ -148,26 +153,39 @@ public struct DatabaseRegion: CustomStringConvertible, Equatable { self = union(other) } - /// Returns a region suitable for database observation by removing views. + /// Returns a region suitable for database observation + func observableRegion(_ db: Database) throws -> DatabaseRegion { + // SQLite does not expose schema changes to the + // TransactionObserver protocol. By removing internal SQLite tables from + // the observed region, we optimize database observation. + // + // And by canonicalizing table names, we remove views, and help the + // `isModified` methods. + try ignoringInternalSQLiteTables().canonicalTables(db) + } + + /// Returns a region only made of actual tables with their canonical names. + /// Canonical names help the `isModified` methods. /// - /// We can do it because modifications only happen in actual tables. And we - /// want to do it because we have a fast path for regions that span a - /// single table. - func ignoringViews(_ db: Database) throws -> DatabaseRegion { + /// This method removes views (assuming no table exists with the same name + /// as a view). + private func canonicalTables(_ db: Database) throws -> DatabaseRegion { guard let tableRegions = tableRegions else { return .fullDatabase } - let mainViewNames = try db.schema(.main).names(ofType: .view) - let tempViewNames = try db.schema(.temp).names(ofType: .view) - let viewNames = mainViewNames.union(tempViewNames) - guard viewNames.isEmpty == false else { return self } - let filteredRegions = tableRegions.filter { viewNames.contains($0.key) == false } - return DatabaseRegion(tableRegions: filteredRegions) + var region = DatabaseRegion() + for (table, tableRegion) in tableRegions { + if let canonicalTableName = try db.canonicalTableName(table.rawValue) { + let table = CaseInsensitiveIdentifier(rawValue: canonicalTableName) + region.formUnion(DatabaseRegion(tableRegions: [table: tableRegion])) + } + } + return region } /// Returns a region which doesn't contain any SQLite internal table. - func ignoringInternalSQLiteTables() -> DatabaseRegion { + private func ignoringInternalSQLiteTables() -> DatabaseRegion { guard let tableRegions = tableRegions else { return .fullDatabase } let filteredRegions = tableRegions.filter { - !Database.isSQLiteInternalTable($0.key) + !Database.isSQLiteInternalTable($0.key.rawValue) } return DatabaseRegion(tableRegions: filteredRegions) } @@ -194,7 +212,7 @@ extension DatabaseRegion { return true } - guard let tableRegion = tableRegions[event.tableName] else { + guard let tableRegion = tableRegions[CaseInsensitiveIdentifier(rawValue: event.tableName)] else { // FTS4 (and maybe other virtual tables) perform unadvertised // changes. For example, an "INSERT INTO document ..." statement // advertises an insertion in the `document` table, but the @@ -242,11 +260,11 @@ extension DatabaseRegion { return "empty" } return tableRegions - .sorted(by: { (l, r) in l.key < r.key }) + .sorted(by: { (l, r) in l.key.rawValue < r.key.rawValue }) .map({ (table, tableRegion) in - var desc = table + var desc = table.rawValue if let columns = tableRegion.columns { - desc += "(" + columns.sorted().joined(separator: ",") + ")" + desc += "(" + columns.map(\.rawValue).sorted().joined(separator: ",") + ")" } else { desc += "(*)" } @@ -260,7 +278,7 @@ extension DatabaseRegion { } private struct TableRegion: Equatable { - var columns: Set? // nil means "all columns" + var columns: Set? // nil means "all columns" var rowIds: Set? // nil means "all rowids" var isEmpty: Bool { @@ -270,7 +288,7 @@ private struct TableRegion: Equatable { } func intersection(_ other: TableRegion) -> TableRegion { - let columnsIntersection: Set? + let columnsIntersection: Set? switch (self.columns, other.columns) { case let (nil, columns), let (columns, nil): columnsIntersection = columns @@ -290,7 +308,7 @@ private struct TableRegion: Equatable { } func union(_ other: TableRegion) -> TableRegion { - let columnsUnion: Set? + let columnsUnion: Set? switch (self.columns, other.columns) { case (nil, _), (_, nil): columnsUnion = nil diff --git a/GRDB/Core/DatabaseRegionObservation.swift b/GRDB/Core/DatabaseRegionObservation.swift index 87809700f7..153809d542 100644 --- a/GRDB/Core/DatabaseRegionObservation.swift +++ b/GRDB/Core/DatabaseRegionObservation.swift @@ -91,7 +91,7 @@ extension DatabaseRegionObservation { // Use unsafeReentrantWrite so that observation can start from any // dispatch queue. return try dbWriter.unsafeReentrantWrite { db -> TransactionObserver in - let region = try observedRegion(db).ignoringViews(db) + let region = try observedRegion(db).observableRegion(db) let observer = DatabaseRegionObserver(region: region, onChange: onChange) db.add(transactionObserver: observer, extent: extent) return observer diff --git a/GRDB/QueryInterface/SQLGeneration/SQLQueryGenerator.swift b/GRDB/QueryInterface/SQLGeneration/SQLQueryGenerator.swift index 4fbc9b3113..3305e6e154 100644 --- a/GRDB/QueryInterface/SQLGeneration/SQLQueryGenerator.swift +++ b/GRDB/QueryInterface/SQLGeneration/SQLQueryGenerator.swift @@ -161,9 +161,7 @@ struct SQLQueryGenerator: Refinable { return selectedRegion } - // Database regions are case-sensitive: use the canonical table name - let canonicalTableName = try db.canonicalTableName(tableName) - return selectedRegion.tableIntersection(canonicalTableName, rowIds: rowIDs) + return selectedRegion.tableIntersection(tableName, rowIds: rowIDs) } /// If true, executing this query yields at most one row. diff --git a/GRDB/Utils/CaseInsensitiveIdentifier.swift b/GRDB/Utils/CaseInsensitiveIdentifier.swift new file mode 100644 index 0000000000..d9bcd172da --- /dev/null +++ b/GRDB/Utils/CaseInsensitiveIdentifier.swift @@ -0,0 +1,44 @@ +/// A case-preserving, case-insensitive identifier +/// that matches the ASCII version of sqlite3_stricmp +struct CaseInsensitiveIdentifier: Hashable { + var rawValue: String + + init(rawValue: String) { + self.rawValue = rawValue + } + + static func == (lhs: CaseInsensitiveIdentifier, rhs: CaseInsensitiveIdentifier) -> Bool { + guard lhs.rawValue.utf8.count == rhs.rawValue.utf8.count else { return false } + for (l, r) in zip(lhs.rawValue.utf8, rhs.rawValue.utf8) + where upperToLower[Int(l)] != upperToLower[Int(r)] { + return false + } + return true + } + + func hash(into hasher: inout Hasher) { + for c in rawValue.utf8 { + hasher.combine(upperToLower[Int(c)]) + } + } +} + +// swiftlint:disable comma +/// The same table as SQLite +private let upperToLower: [UTF8.CodeUnit] = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, + 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, + 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 97, 98, 99,100,101,102,103, + 104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121, + 122, 91, 92, 93, 94, 95, 96, 97, 98, 99,100,101,102,103,104,105,106,107, + 108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125, + 126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143, + 144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161, + 162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179, + 180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197, + 198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215, + 216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233, + 234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251, + 252,253,254,255 +] diff --git a/GRDB/ValueObservation/ValueObserver.swift b/GRDB/ValueObservation/ValueObserver.swift index 75f7885639..ba4f998b33 100644 --- a/GRDB/ValueObservation/ValueObserver.swift +++ b/GRDB/ValueObservation/ValueObserver.swift @@ -213,12 +213,7 @@ extension ValueObserver { var region = DatabaseRegion() let result = try db.recordingSelection(®ion, fetch) - - // SQLite does not expose views and schema changes to the - // TransactionObserver protocol. By removing them from the observed - // region, we optimize our TransactionObserver conformance. - observedRegion = try region.ignoringViews(db).ignoringInternalSQLiteTables() - + observedRegion = try region.observableRegion(db) return result } } diff --git a/GRDBCustom.xcodeproj/project.pbxproj b/GRDBCustom.xcodeproj/project.pbxproj index e6954c5575..220ca551eb 100755 --- a/GRDBCustom.xcodeproj/project.pbxproj +++ b/GRDBCustom.xcodeproj/project.pbxproj @@ -166,6 +166,10 @@ 564CE5B821B8FBEB00652B19 /* DatabaseRegionObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564CE5B521B8FBEA00652B19 /* DatabaseRegionObservation.swift */; }; 564CE5C621B8FFE600652B19 /* DatabaseRegionObservationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564CE5C521B8FFE500652B19 /* DatabaseRegionObservationTests.swift */; }; 564CE5C721B8FFE600652B19 /* DatabaseRegionObservationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564CE5C521B8FFE500652B19 /* DatabaseRegionObservationTests.swift */; }; + 564D4F93261E1D3300F55856 /* CaseInsensitiveIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564D4F91261E1D3300F55856 /* CaseInsensitiveIdentifier.swift */; }; + 564D4F94261E1D3400F55856 /* CaseInsensitiveIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564D4F91261E1D3300F55856 /* CaseInsensitiveIdentifier.swift */; }; + 564D4F9A261E1E0200F55856 /* CaseInsensitiveIdentifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564D4F99261E1E0200F55856 /* CaseInsensitiveIdentifierTests.swift */; }; + 564D4F9B261E1E0300F55856 /* CaseInsensitiveIdentifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564D4F99261E1E0200F55856 /* CaseInsensitiveIdentifierTests.swift */; }; 564E73F3203DA2AC000C443C /* JoinSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564E73E7203DA278000C443C /* JoinSupportTests.swift */; }; 564E73F4203DA2AD000C443C /* JoinSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564E73E7203DA278000C443C /* JoinSupportTests.swift */; }; 564F9C211F069B4E00877A00 /* DatabaseAggregateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564F9C1D1F069B4E00877A00 /* DatabaseAggregateTests.swift */; }; @@ -864,6 +868,8 @@ 564CE4E221B2E05400652B19 /* ValueObservationMapTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationMapTests.swift; sourceTree = ""; }; 564CE5B521B8FBEA00652B19 /* DatabaseRegionObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseRegionObservation.swift; sourceTree = ""; }; 564CE5C521B8FFE500652B19 /* DatabaseRegionObservationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseRegionObservationTests.swift; sourceTree = ""; }; + 564D4F91261E1D3300F55856 /* CaseInsensitiveIdentifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CaseInsensitiveIdentifier.swift; sourceTree = ""; }; + 564D4F99261E1E0200F55856 /* CaseInsensitiveIdentifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaseInsensitiveIdentifierTests.swift; sourceTree = ""; }; 564E73E7203DA278000C443C /* JoinSupportTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JoinSupportTests.swift; sourceTree = ""; }; 564F9C1D1F069B4E00877A00 /* DatabaseAggregateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAggregateTests.swift; sourceTree = ""; }; 564F9C2C1F075DD200877A00 /* DatabaseFunction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseFunction.swift; sourceTree = ""; }; @@ -1563,6 +1569,7 @@ 5659F4861EA8D94E004A4992 /* Utils */ = { isa = PBXGroup; children = ( + 564D4F91261E1D3300F55856 /* CaseInsensitiveIdentifier.swift */, 563EF44C2161F196007DAACD /* Inflections.swift */, 569BBA4B229170B300478429 /* Inflections+English.swift */, 566BE7132342541F00A8254B /* LockedBox.swift */, @@ -1626,6 +1633,7 @@ 569978D31B539038005EBEED /* Private */ = { isa = PBXGroup; children = ( + 564D4F99261E1E0200F55856 /* CaseInsensitiveIdentifierTests.swift */, 563363CF1C943D13000BE133 /* DatabasePoolReleaseMemoryTests.swift */, 569531281C908A5B00CF1A2B /* DatabasePoolSchemaCacheTests.swift */, 563363D41C94484E000BE133 /* DatabaseQueueReleaseMemoryTests.swift */, @@ -2360,6 +2368,7 @@ F3BA80141CFB2876003DC1BA /* DatabaseValueConvertible.swift in Sources */, 5656A8702295BD56001FF3FF /* Association.swift in Sources */, 563B8F9C249E74E5007A48C9 /* Trace.swift in Sources */, + 564D4F94261E1D3400F55856 /* CaseInsensitiveIdentifier.swift in Sources */, 5698AD261DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift in Sources */, 5656A85C2295BD56001FF3FF /* TableDefinition.swift in Sources */, 5656A8562295BD56001FF3FF /* FTS5+QueryInterface.swift in Sources */, @@ -2528,6 +2537,7 @@ 5698AC9D1DA4B0430056AF8C /* FTS4RecordTests.swift in Sources */, 56419C8324A51D6F004967E1 /* DatabaseWriterWritePublisherTests.swift in Sources */, 5665FA3E2129EED8004D8612 /* DatabaseDateEncodingStrategyTests.swift in Sources */, + 564D4F9B261E1E0300F55856 /* CaseInsensitiveIdentifierTests.swift in Sources */, 56677C28241E6EA20050755D /* ValueObservationRecorder.swift in Sources */, 563B8F9F249E8AB0007A48C9 /* ValueObservationPrintTests.swift in Sources */, 56419C9524A51D7F004967E1 /* NextOne.swift in Sources */, @@ -2719,6 +2729,7 @@ F3BA80701CFB2E55003DC1BA /* DatabaseValueConvertible.swift in Sources */, 5656A86F2295BD56001FF3FF /* Association.swift in Sources */, 563B8F9B249E74E5007A48C9 /* Trace.swift in Sources */, + 564D4F93261E1D3300F55856 /* CaseInsensitiveIdentifier.swift in Sources */, 5698AD231DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift in Sources */, 5656A85B2295BD56001FF3FF /* TableDefinition.swift in Sources */, 5656A8552295BD56001FF3FF /* FTS5+QueryInterface.swift in Sources */, @@ -2887,6 +2898,7 @@ 5623935A1DEE013C00A6B01F /* FilterCursorTests.swift in Sources */, 56419C7E24A51D6E004967E1 /* DatabaseWriterWritePublisherTests.swift in Sources */, 5665FA3D2129EED8004D8612 /* DatabaseDateEncodingStrategyTests.swift in Sources */, + 564D4F9A261E1E0200F55856 /* CaseInsensitiveIdentifierTests.swift in Sources */, 56677C27241E6EA20050755D /* ValueObservationRecorder.swift in Sources */, 563B8F9E249E8AB0007A48C9 /* ValueObservationPrintTests.swift in Sources */, 56419C8B24A51D7D004967E1 /* NextOne.swift in Sources */, diff --git a/Tests/GRDBTests/CaseInsensitiveIdentifierTests.swift b/Tests/GRDBTests/CaseInsensitiveIdentifierTests.swift new file mode 100644 index 0000000000..7e2703ab5f --- /dev/null +++ b/Tests/GRDBTests/CaseInsensitiveIdentifierTests.swift @@ -0,0 +1,71 @@ +import XCTest +@testable import GRDB + +class CaseInsensitiveIdentifierTests: XCTestCase { + func testCasePreserving() { + let identifier = CaseInsensitiveIdentifier(rawValue: "tableName") + XCTAssertEqual(identifier.rawValue, "tableName") + } + + func testCaseInsensitiveEquality() { + let identifier = CaseInsensitiveIdentifier(rawValue: "tableName") + XCTAssertEqual(identifier, CaseInsensitiveIdentifier(rawValue: "tableName")) + XCTAssertEqual(identifier, CaseInsensitiveIdentifier(rawValue: "tablename")) + XCTAssertEqual(identifier, CaseInsensitiveIdentifier(rawValue: "TABLENAME")) + XCTAssertNotEqual(identifier, CaseInsensitiveIdentifier(rawValue: "foo")) + XCTAssertNotEqual(identifier, CaseInsensitiveIdentifier(rawValue: "tableName2")) + } + + func testCaseInsensitiveHash() { + func hashValue(_ value: T) -> Int { + var hasher = Hasher() + hasher.combine(value) + return hasher.finalize() + } + let identifier = CaseInsensitiveIdentifier(rawValue: "tableName") + XCTAssertEqual(hashValue(identifier), hashValue(CaseInsensitiveIdentifier(rawValue: "tableName"))) + XCTAssertEqual(hashValue(identifier), hashValue(CaseInsensitiveIdentifier(rawValue: "tablename"))) + XCTAssertEqual(hashValue(identifier), hashValue(CaseInsensitiveIdentifier(rawValue: "TABLENAME"))) + XCTAssertNotEqual(hashValue(identifier), hashValue(CaseInsensitiveIdentifier(rawValue: "foo"))) + XCTAssertNotEqual(hashValue(identifier), hashValue(CaseInsensitiveIdentifier(rawValue: "tableName2"))) + } + + func testSet() { + let set: Set = [ + CaseInsensitiveIdentifier(rawValue: ""), + CaseInsensitiveIdentifier(rawValue: "a"), + CaseInsensitiveIdentifier(rawValue: "A"), + CaseInsensitiveIdentifier(rawValue: "id"), + CaseInsensitiveIdentifier(rawValue: "ID"), + CaseInsensitiveIdentifier(rawValue: "foo"), + CaseInsensitiveIdentifier(rawValue: "FOO"), + CaseInsensitiveIdentifier(rawValue: "score"), + CaseInsensitiveIdentifier(rawValue: "Score"), + CaseInsensitiveIdentifier(rawValue: "tablename"), + CaseInsensitiveIdentifier(rawValue: "tableName"), + CaseInsensitiveIdentifier(rawValue: "someReasonablyLongDatabaseIdentifier"), + CaseInsensitiveIdentifier(rawValue: "someReasonablyLongDatabaseIdentifier"), + CaseInsensitiveIdentifier(rawValue: "someReasonablyLongDatabaseIdentifiex"), + CaseInsensitiveIdentifier(rawValue: "xomeReasonablyLongDatabaseIdentifier"), + ] + XCTAssertEqual(set.count, 9) + XCTAssertEqual(Set(set.map { $0.rawValue.lowercased() }), [ + "", + "a", + "id", + "foo", + "score", + "tablename", + "somereasonablylongdatabaseidentifier", + "somereasonablylongdatabaseidentifiex", + "xomereasonablylongdatabaseidentifier", + ]) + } + + func testDictionary() { + let dictionary = [CaseInsensitiveIdentifier(rawValue: "foo"): 1] + XCTAssertEqual(dictionary[CaseInsensitiveIdentifier(rawValue: "foo")], 1) + XCTAssertEqual(dictionary[CaseInsensitiveIdentifier(rawValue: "FOO")], 1) + XCTAssertEqual(dictionary[CaseInsensitiveIdentifier(rawValue: "bar")], nil) + } +} diff --git a/Tests/GRDBTests/DatabaseRegionTests.swift b/Tests/GRDBTests/DatabaseRegionTests.swift index 6641e3a579..316a9b7fd9 100644 --- a/Tests/GRDBTests/DatabaseRegionTests.swift +++ b/Tests/GRDBTests/DatabaseRegionTests.swift @@ -9,9 +9,7 @@ class DatabaseRegionTests : GRDBTestCase { DatabaseRegion.fullDatabase, DatabaseRegion(), DatabaseRegion(table: "foo"), - DatabaseRegion(table: "FOO"), // selection info is case-sensitive on table name DatabaseRegion(table: "foo", columns: ["a", "b"]), - DatabaseRegion(table: "foo", columns: ["A", "B"]), // selection info is case-sensitive on columns names DatabaseRegion(table: "foo", columns: ["b", "c"]), DatabaseRegion(table: "foo", rowIds: [1, 2]), DatabaseRegion(table: "foo", rowIds: [2, 3]), @@ -26,6 +24,14 @@ class DatabaseRegionTests : GRDBTestCase { } } } + + // Case insensitivity + XCTAssertEqual( + DatabaseRegion(table: "foo"), + DatabaseRegion(table: "FOO")) + XCTAssertEqual( + DatabaseRegion(table: "foo", columns: ["a", "b"]), + DatabaseRegion(table: "FOO", columns: ["A", "B"])) } func testRegionUnion() { @@ -243,15 +249,63 @@ class DatabaseRegionTests : GRDBTestCase { 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.makeSelectStatement(sql: "SELECT id FROM foo") + let expectedRegion = DatabaseRegion(table: "foo") + XCTAssertEqual(statement.databaseRegion, expectedRegion) + XCTAssertEqual(statement.databaseRegion.description, "foo(*)") + } + do { + let statement = try db.makeSelectStatement(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, name TEXT)") - try db.execute(sql: "CREATE TABLE bar (id INTEGER, fooId INTEGER)") + 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.makeSelectStatement(sql: "SELECT foo.name FROM FOO JOIN BAR ON fooId = foo.id") + let statement = try db.makeSelectStatement(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.makeSelectStatement(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.makeSelectStatement(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.makeSelectStatement(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) @@ -269,6 +323,18 @@ class DatabaseRegionTests : GRDBTestCase { XCTAssertEqual(statement.databaseRegion.description, "foo(*)") } } + do { + let statement = try db.makeSelectStatement(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(*)") + } + } } } @@ -474,16 +540,29 @@ class DatabaseRegionTests : GRDBTestCase { func testUpdateStatement() 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.makeUpdateStatement(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 + try db.execute(sql: "CREATE TABLE foo (id INTEGER PRIMARY KEY, bar TEXT, baz TEXT, qux TEXT)") + do { + let statement = try db.makeUpdateStatement(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.makeUpdateStatement(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"])) } - XCTAssertEqual(tableName, "foo") - XCTAssertEqual(columnNames, Set(["bar", "baz"])) } } diff --git a/Tests/GRDBTests/ValueObservationFetchTests.swift b/Tests/GRDBTests/ValueObservationFetchTests.swift index 9da456677c..eba1e2e1e8 100644 --- a/Tests/GRDBTests/ValueObservationFetchTests.swift +++ b/Tests/GRDBTests/ValueObservationFetchTests.swift @@ -3,6 +3,7 @@ import GRDB class ValueObservationFetchTests: GRDBTestCase { func testFetch() throws { + // Count try assertValueObservation( ValueObservation.trackingConstantRegion { try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! @@ -16,8 +17,182 @@ class ValueObservationFetchTests: GRDBTestCase { try db.execute(sql: "UPDATE t SET id = id") try db.execute(sql: "INSERT INTO t DEFAULT VALUES") }) + + // Select rowid + try assertValueObservation( + ValueObservation.trackingConstantRegion { + try Int.fetchAll($0, sql: "SELECT id FROM t ORDER BY id") + }, + records: [[], [1], [1], [1, 2]], + setup: { db in + try db.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") + }, + recordedUpdates: { db in + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try db.execute(sql: "UPDATE t SET id = id") + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + }) + + // Select non-rowid + try assertValueObservation( + ValueObservation.trackingConstantRegion { + try String.fetchAll($0, sql: "SELECT name FROM t ORDER BY name") + }, + records: [[], ["Arthur"], ["Arthur", "Barbara"]], + setup: { db in + try db.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT)") + }, + recordedUpdates: { db in + try db.execute(sql: "INSERT INTO t (name) VALUES ('Arthur')") + try db.execute(sql: "UPDATE t SET id = id") // does not trigger the observation + try db.execute(sql: "INSERT INTO t (name) VALUES ('Barbara')") + }) + } + + // Regression test for https://github.com/groue/GRDB.swift/issues/954 + func testCaseInsensitivityForTable() throws { + // Count + try assertValueObservation( + ValueObservation.trackingConstantRegion { + try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! + }, + records: [0, 1, 1, 2], + setup: { db in + try db.execute(sql: "CREATE TABLE T(id INTEGER PRIMARY KEY AUTOINCREMENT)") + }, + recordedUpdates: { db in + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try db.execute(sql: "UPDATE t SET id = id") + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + }) + + // Select rowid + try assertValueObservation( + ValueObservation.trackingConstantRegion { + try Int.fetchAll($0, sql: "SELECT id FROM t ORDER BY id") + }, + records: [[], [1], [1], [1, 2]], + setup: { db in + try db.execute(sql: "CREATE TABLE T(id INTEGER PRIMARY KEY AUTOINCREMENT)") + }, + recordedUpdates: { db in + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try db.execute(sql: "UPDATE t SET id = id") + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + }) + + // Select non-rowid + try assertValueObservation( + ValueObservation.trackingConstantRegion { + try String.fetchAll($0, sql: "SELECT name FROM t ORDER BY name") + }, + records: [[], ["Arthur"], ["Arthur", "Barbara"]], + setup: { db in + try db.execute(sql: "CREATE TABLE T(id INTEGER PRIMARY KEY, name TEXT)") + }, + recordedUpdates: { db in + try db.execute(sql: "INSERT INTO t (name) VALUES ('Arthur')") + try db.execute(sql: "UPDATE t SET id = id") // does not trigger the observation + try db.execute(sql: "INSERT INTO t (name) VALUES ('Barbara')") + }) + } + + // Regression test for https://github.com/groue/GRDB.swift/issues/954 + func testCaseInsensitivityForFetch() throws { + // Count + try assertValueObservation( + ValueObservation.trackingConstantRegion { + try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM T")! + }, + records: [0, 1, 1, 2], + setup: { db in + try db.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") + }, + recordedUpdates: { db in + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try db.execute(sql: "UPDATE t SET id = id") + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + }) + + // Select rowid + try assertValueObservation( + ValueObservation.trackingConstantRegion { + try Int.fetchAll($0, sql: "SELECT ID FROM T ORDER BY ID") + }, + records: [[], [1], [1], [1, 2]], + setup: { db in + try db.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") + }, + recordedUpdates: { db in + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + try db.execute(sql: "UPDATE t SET id = id") + try db.execute(sql: "INSERT INTO t DEFAULT VALUES") + }) + + // Select non-rowid + try assertValueObservation( + ValueObservation.trackingConstantRegion { + try String.fetchAll($0, sql: "SELECT NAME FROM T ORDER BY NAME") + }, + records: [[], ["Arthur"], ["Arthur", "Barbara"]], + setup: { db in + try db.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT)") + }, + recordedUpdates: { db in + try db.execute(sql: "INSERT INTO t (name) VALUES ('Arthur')") + try db.execute(sql: "UPDATE t SET id = id") // does not trigger the observation + try db.execute(sql: "INSERT INTO t (name) VALUES ('Barbara')") + }) } + // Regression test for https://github.com/groue/GRDB.swift/issues/954 + func testCaseInsensitivityForUpdates() throws { + // Count + try assertValueObservation( + ValueObservation.trackingConstantRegion { + try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t")! + }, + records: [0, 1, 1, 2], + setup: { db in + try db.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") + }, + recordedUpdates: { db in + try db.execute(sql: "INSERT INTO T DEFAULT VALUES") + try db.execute(sql: "UPDATE T SET ID = ID") + try db.execute(sql: "INSERT INTO T DEFAULT VALUES") + }) + + // Select rowid + try assertValueObservation( + ValueObservation.trackingConstantRegion { + try Int.fetchAll($0, sql: "SELECT id FROM t ORDER BY id") + }, + records: [[], [1], [1], [1, 2]], + setup: { db in + try db.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") + }, + recordedUpdates: { db in + try db.execute(sql: "INSERT INTO T DEFAULT VALUES") + try db.execute(sql: "UPDATE T SET ID = ID") + try db.execute(sql: "INSERT INTO T DEFAULT VALUES") + }) + + // Select non-rowid + try assertValueObservation( + ValueObservation.trackingConstantRegion { + try String.fetchAll($0, sql: "SELECT name FROM t ORDER BY name") + }, + records: [[], ["Arthur"], ["Arthur", "Barbara"]], + setup: { db in + try db.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT)") + }, + recordedUpdates: { db in + try db.execute(sql: "INSERT INTO T (NAME) VALUES ('Arthur')") + try db.execute(sql: "UPDATE T SET ID = ID") // does not trigger the observation + try db.execute(sql: "INSERT INTO T (NAME) VALUES ('Barbara')") + }) + } + func testRemoveDuplicated() throws { try assertValueObservation( ValueObservation