@@ -41,8 +41,12 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
41
41
private let cache = ThreadSafeKeyValueStore < Model . CollectionIdentifier , Model . Collection > ( )
42
42
private let cacheLock = Lock ( )
43
43
44
+ // Lock helps prevent concurrency errors with transaction statements during e.g. `refreshCollections`,
45
+ // since only one transaction is allowed per SQLite connection. We need transactions to speed up bulk updates.
46
+ // TODO: we could potentially optimize this with db connection pool
44
47
private let ftsLock = Lock ( )
45
48
49
+ // Targets have in-memory trie in addition to SQLite FTS as optimization
46
50
private let targetTrie = Trie < CollectionPackage > ( )
47
51
private var targetTrieReady = ThreadSafeBox < Bool > ( false )
48
52
@@ -97,24 +101,23 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
97
101
try statement. step ( )
98
102
}
99
103
100
- // Update search indices
101
104
try self . ftsLock. withLock {
102
- // Lock helps prevent "SQL logic error" thrown by transaction statements during `refreshCollections`,
103
- // since only one transaction is allowed per connection. Bulk updates are faster with transaction.
105
+ // Update search indices
104
106
try self . withDB { db in
105
107
try db. exec ( query: " BEGIN TRANSACTION; " )
106
108
107
109
// First delete existing data
108
110
try self . removeFromSearchIndices ( identifier: collection. identifier)
109
111
110
- // Then insert new data
111
112
let packagesStatement = try db. prepare ( query: " INSERT INTO \( Self . packagesFTSName) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); " )
112
113
let targetsStatement = try db. prepare ( query: " INSERT INTO \( Self . targetsFTSName) VALUES (?, ?, ?); " )
113
114
115
+ // Then insert new data
114
116
try collection. packages. forEach { package in
115
117
var targets = Set < String > ( )
116
118
117
119
try package . versions. forEach { version in
120
+ // Packages FTS
118
121
let packagesBindings : [ SQLite . SQLiteValue ] = [
119
122
. string( try self . encoder. encode ( collection. identifier) . base64EncodedString ( ) ) ,
120
123
. string( package . reference. identity. description) ,
@@ -137,8 +140,10 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
137
140
138
141
let collectionPackage = CollectionPackage ( collection: collection. identifier, package : package . reference. identity)
139
142
try targets. forEach { target in
143
+ // Targets in-memory trie
140
144
self . targetTrie. insert ( word: target. lowercased ( ) , foundIn: collectionPackage)
141
145
146
+ // Targets FTS
142
147
let targetsBindings : [ SQLite . SQLiteValue ] = [
143
148
. string( try self . encoder. encode ( collection. identifier) . base64EncodedString ( ) ) ,
144
149
. string( package . repository. url) ,
@@ -338,6 +343,7 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
338
343
result [ collection. identifier] = collection
339
344
}
340
345
346
+ // For each package, find the containing collections
341
347
let packageCollections = matches. filter { collectionDict. keys. contains ( $0. collection) }
342
348
. reduce ( into: [ PackageIdentity : ( package : Model . Package , collections: Set < Model . CollectionIdentifier > ) ] ( ) ) { result, match in
343
349
var entry = result. removeValue ( forKey: match. package )
@@ -435,35 +441,28 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
435
441
}
436
442
}
437
443
} catch is NotFoundError {
438
- // do nothing if no matches
444
+ // Do nothing if no matches found
439
445
} catch {
440
446
return callback ( . failure( error) )
441
447
}
442
448
} else {
443
449
do {
444
450
let targetQuery = " SELECT collection_id_blob_base64, package_repository_url, name FROM \( Self . targetsFTSName) WHERE name LIKE ?; "
445
451
try self . executeStatement ( targetQuery) { statement in
446
- try statement. bind ( [ . string( " \( query) % " ) ] )
447
-
452
+ switch type {
453
+ case . exactMatch:
454
+ try statement. bind ( [ . string( " \( query) " ) ] )
455
+ case . prefix:
456
+ try statement. bind ( [ . string( " \( query) % " ) ] )
457
+ }
458
+
448
459
while let row = try statement. step ( ) {
449
- let targetName = row. string ( at: 2 )
450
- let match : Bool
451
- switch type {
452
- case . exactMatch:
453
- // Cannot do case-insensitive exact-match with FTS
454
- match = query. lowercased ( ) == targetName. lowercased ( )
455
- case . prefix:
456
- // FTS LIKE and MATCH are case-insensitive
457
- match = true
458
- }
459
-
460
- if match,
461
- let collectionData = Data ( base64Encoded: row. string ( at: 0 ) ) ,
460
+ if let collectionData = Data ( base64Encoded: row. string ( at: 0 ) ) ,
462
461
let collection = try ? self . decoder. decode ( Model . CollectionIdentifier. self, from: collectionData) {
463
462
matches. append ( (
464
463
collection: collection,
465
464
package : PackageIdentity ( url: row. string ( at: 1 ) ) ,
466
- targetName: targetName
465
+ targetName: row . string ( at : 2 )
467
466
) )
468
467
}
469
468
}
@@ -477,6 +476,7 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
477
476
result [ collection. identifier] = collection
478
477
}
479
478
479
+ // For each package, find the containing collections
480
480
var packageCollections = [ PackageIdentity : ( package : Model . Package , collections: Set < Model . CollectionIdentifier > ) ] ( )
481
481
// For each matching target, find the containing package version(s)
482
482
var targetPackageVersions = [ Model . Target : [ PackageIdentity : Set < Model . TargetListResult . PackageVersion > ] ] ( )
0 commit comments