@@ -41,8 +41,12 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
41
41
private var cache = [ 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) ,
@@ -350,6 +355,7 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
350
355
result [ collection. identifier] = collection
351
356
}
352
357
358
+ // For each package, find the containing collections
353
359
let packageCollections = matches. filter { collectionDict. keys. contains ( $0. collection) }
354
360
. reduce ( into: [ PackageIdentity : ( package : Model . Package , collections: Set < Model . CollectionIdentifier > ) ] ( ) ) { result, match in
355
361
var entry = result. removeValue ( forKey: match. package )
@@ -447,35 +453,28 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
447
453
}
448
454
}
449
455
} catch is NotFoundError {
450
- // do nothing if no matches
456
+ // Do nothing if no matches found
451
457
} catch {
452
458
return callback ( . failure( error) )
453
459
}
454
460
} else {
455
461
do {
456
462
let targetQuery = " SELECT collection_id_blob_base64, package_repository_url, name FROM \( Self . targetsFTSName) WHERE name LIKE ?; "
457
463
try self . executeStatement ( targetQuery) { statement in
458
- try statement. bind ( [ . string( " \( query) % " ) ] )
459
-
464
+ switch type {
465
+ case . exactMatch:
466
+ try statement. bind ( [ . string( " \( query) " ) ] )
467
+ case . prefix:
468
+ try statement. bind ( [ . string( " \( query) % " ) ] )
469
+ }
470
+
460
471
while let row = try statement. step ( ) {
461
- let targetName = row. string ( at: 2 )
462
- let match : Bool
463
- switch type {
464
- case . exactMatch:
465
- // Cannot do case-insensitive exact-match with FTS
466
- match = query. lowercased ( ) == targetName. lowercased ( )
467
- case . prefix:
468
- // FTS LIKE and MATCH are case-insensitive
469
- match = true
470
- }
471
-
472
- if match,
473
- let collectionData = Data ( base64Encoded: row. string ( at: 0 ) ) ,
472
+ if let collectionData = Data ( base64Encoded: row. string ( at: 0 ) ) ,
474
473
let collection = try ? self . decoder. decode ( Model . CollectionIdentifier. self, from: collectionData) {
475
474
matches. append ( (
476
475
collection: collection,
477
476
package : PackageIdentity ( url: row. string ( at: 1 ) ) ,
478
- targetName: targetName
477
+ targetName: row . string ( at : 2 )
479
478
) )
480
479
}
481
480
}
@@ -489,6 +488,7 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
489
488
result [ collection. identifier] = collection
490
489
}
491
490
491
+ // For each package, find the containing collections
492
492
var packageCollections = [ PackageIdentity : ( package : Model . Package , collections: Set < Model . CollectionIdentifier > ) ] ( )
493
493
// For each matching target, find the containing package version(s)
494
494
var targetPackageVersions = [ Model . Target : [ PackageIdentity : Set < Model . TargetListResult . PackageVersion > ] ] ( )
0 commit comments