@@ -94,15 +94,18 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
94
94
let fileSystem : FileSystem
95
95
let location : SQLite . Location
96
96
97
- private let jsonEncoder : JSONEncoder
98
- private let jsonDecoder : JSONDecoder
97
+ private let encoder : JSONEncoder
98
+ private let decoder : JSONDecoder
99
99
100
100
// for concurrent for DB access
101
101
private let queue = DispatchQueue ( label: " org.swift.swiftpm.SQLitePackageCollectionsStorage " , attributes: . concurrent)
102
102
103
103
private var state = State . idle
104
104
private let stateLock = Lock ( )
105
105
106
+ private var cache = [ PackageCollectionsModel . CollectionIdentifier: PackageCollectionsModel . Collection] ( )
107
+ private let cacheLock = Lock ( )
108
+
106
109
init ( location: SQLite . Location ? = nil ) {
107
110
self . location = location ?? . path( localFileSystem. swiftPMCacheDirectory. appending ( components: " package-collection.db " ) )
108
111
switch self . location {
@@ -111,8 +114,8 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
111
114
case . memory:
112
115
self . fileSystem = InMemoryFileSystem ( )
113
116
}
114
- self . jsonEncoder = JSONEncoder ( )
115
- self . jsonDecoder = JSONDecoder ( )
117
+ self . encoder = JSONEncoder ( )
118
+ self . decoder = JSONDecoder ( )
116
119
}
117
120
118
121
convenience init ( path: AbsolutePath ) {
@@ -138,9 +141,10 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
138
141
callback: @escaping ( Result < PackageCollectionsModel . Collection , Error > ) -> Void ) {
139
142
self . queue. async {
140
143
do {
144
+ // write to db
141
145
let query = " INSERT OR IGNORE INTO PACKAGES_COLLECTIONS VALUES (?, ?); "
142
146
try self . executeStatement ( query) { statement -> Void in
143
- let data = try self . jsonEncoder . encode ( collection)
147
+ let data = try self . encoder . encode ( collection)
144
148
145
149
let bindings : [ SQLite . SQLiteValue ] = [
146
150
. string( collection. identifier. databaseKey ( ) ) ,
@@ -149,6 +153,10 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
149
153
try statement. bind ( bindings)
150
154
try statement. step ( )
151
155
}
156
+ // write to cache
157
+ self . cacheLock. withLock {
158
+ self . cache [ collection. identifier] = collection
159
+ }
152
160
callback ( . success( collection) )
153
161
} catch {
154
162
callback ( . failure( error) )
@@ -160,6 +168,7 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
160
168
callback: @escaping ( Result < Void , Error > ) -> Void ) {
161
169
self . queue. async {
162
170
do {
171
+ // write to db
163
172
let query = " DELETE FROM PACKAGES_COLLECTIONS WHERE key == ?; "
164
173
try self . executeStatement ( query) { statement -> Void in
165
174
let bindings : [ SQLite . SQLiteValue ] = [
@@ -168,6 +177,10 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
168
177
try statement. bind ( bindings)
169
178
try statement. step ( )
170
179
}
180
+ // write to cache
181
+ self . cacheLock. withLock {
182
+ self . cache [ identifier] = nil
183
+ }
171
184
callback ( . success( ( ) ) )
172
185
} catch {
173
186
callback ( . failure( error) )
@@ -177,6 +190,12 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
177
190
178
191
func get( identifier: PackageCollectionsModel . CollectionIdentifier ,
179
192
callback: @escaping ( Result < PackageCollectionsModel . Collection , Error > ) -> Void ) {
193
+ // try read to cache
194
+ if let collection = ( self . cacheLock. withLock { self . cache [ identifier] } ) {
195
+ return callback ( . success( collection) )
196
+ }
197
+
198
+ // go to db if not found
180
199
self . queue. async {
181
200
do {
182
201
let query = " SELECT value FROM PACKAGES_COLLECTIONS WHERE key == ? LIMIT 1; "
@@ -188,7 +207,7 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
188
207
throw NotFoundError ( " \( identifier) " )
189
208
}
190
209
191
- let collection = try self . jsonDecoder . decode ( PackageCollectionsModel . Collection. self, from: data)
210
+ let collection = try self . decoder . decode ( PackageCollectionsModel . Collection. self, from: data)
192
211
return collection
193
212
}
194
213
callback ( . success( collection) )
@@ -200,11 +219,21 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
200
219
201
220
func list( identifiers: [ PackageCollectionsModel . CollectionIdentifier ] ? = nil ,
202
221
callback: @escaping ( Result < [ PackageCollectionsModel . Collection ] , Error > ) -> Void ) {
222
+ // try read to cache
223
+ let cached = self . cacheLock. withLock {
224
+ identifiers? . compactMap { identifier in
225
+ self . cache [ identifier]
226
+ }
227
+ }
228
+ if let cached = cached, cached. count > 0 , cached. count == identifiers? . count {
229
+ return callback ( . success( cached) )
230
+ }
231
+
232
+ // go to db if not found
203
233
self . queue. async {
204
234
do {
205
235
var blobs = [ Data] ( )
206
236
if let identifiers = identifiers {
207
- // TODO: consider running these in parallel
208
237
var index = 0
209
238
while index < identifiers. count {
210
239
let slice = identifiers [ index ..< min ( index + Self. batchSize, identifiers. count) ]
@@ -226,25 +255,86 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
226
255
}
227
256
}
228
257
229
- // TODO: consider some diagnostics / warning for invalid data
230
- let collections = blobs. compactMap { data -> PackageCollectionsModel . Collection ? in
231
- try ? self . jsonDecoder. decode ( PackageCollectionsModel . Collection. self, from: data)
258
+ // decoding is a performance bottleneck (10+s for 1000 collections)
259
+ // workaround is to decode in parallel if list is large enough to justify it
260
+ var collections : [ PackageCollectionsModel . Collection ]
261
+ if blobs. count < 50 {
262
+ // TODO: consider some diagnostics / warning for invalid data
263
+ collections = blobs. compactMap { data -> PackageCollectionsModel . Collection ? in
264
+ try ? self . decoder. decode ( PackageCollectionsModel . Collection. self, from: data)
265
+ }
266
+ } else {
267
+ let lock = Lock ( )
268
+ let sync = DispatchGroup ( )
269
+ collections = [ PackageCollectionsModel . Collection] ( )
270
+ blobs. forEach { data in
271
+ sync. enter ( )
272
+ self . queue. async {
273
+ defer { sync. leave ( ) }
274
+ if let collection = try ? self . decoder. decode ( PackageCollectionsModel . Collection. self, from: data) {
275
+ lock. withLock {
276
+ collections. append ( collection)
277
+ }
278
+ }
279
+ }
280
+ }
281
+ sync. wait ( )
232
282
}
283
+
233
284
callback ( . success( collections) )
234
285
} catch {
235
286
callback ( . failure( error) )
236
287
}
237
288
}
238
289
}
239
290
240
- // FIXME: implement this
291
+ // TODO: this is PoC for search, need a more performant version of this
241
292
func searchPackages( identifiers: [ PackageCollectionsModel . CollectionIdentifier ] ? = nil ,
242
293
query: String ,
243
294
callback: @escaping ( Result < PackageCollectionsModel . PackageSearchResult , Error > ) -> Void ) {
244
- fatalError ( " not implemented " )
295
+ let queryString = query. lowercased ( )
296
+
297
+ self . list ( identifiers: identifiers) { result in
298
+ switch result {
299
+ case . failure( let error) :
300
+ callback ( . failure( error) )
301
+ case . success( let collections) :
302
+ let collectionsPackages = collections. reduce ( [ PackageCollectionsModel . CollectionIdentifier: [ PackageCollectionsModel . Collection. Package] ] ( ) ) { partial, collection in
303
+ var map = partial
304
+ map [ collection. identifier] = collection. packages. filter { package in
305
+ if package . repository. url. lowercased ( ) . contains ( queryString) { return true }
306
+ if let summary = package . summary, summary. lowercased ( ) . contains ( queryString) { return true }
307
+ return package . versions. contains ( where: { version in
308
+ if version. packageName. lowercased ( ) . contains ( queryString) { return true }
309
+ if version. products. contains ( where: { $0. name. lowercased ( ) . contains ( queryString) } ) { return true }
310
+ return version. targets. contains ( where: { $0. name. lowercased ( ) . contains ( queryString) } )
311
+ } )
312
+ }
313
+ return map
314
+ }
315
+
316
+ // compose result :p
317
+
318
+ var packageCollections = [ PackageReference : ( package : PackageCollectionsModel . Collection . Package , collections: Set < PackageCollectionsModel . CollectionIdentifier > ) ] ( )
319
+ collectionsPackages. forEach { collectionIdentifier, packages in
320
+ packages. forEach { package in
321
+ // Avoid copy-on-write: remove entry from dictionary before mutating
322
+ var entry = packageCollections. removeValue ( forKey: package . reference) ?? ( package , . init( ) )
323
+ entry. collections. insert ( collectionIdentifier)
324
+ packageCollections [ package . reference] = entry
325
+ }
326
+ }
327
+
328
+ let result = PackageCollectionsModel . PackageSearchResult ( items: packageCollections. map { entry in
329
+ . init( package : entry. value. package , collections: Array ( entry. value. collections) )
330
+ } )
331
+
332
+ callback ( . success( result) )
333
+ }
334
+ }
245
335
}
246
336
247
- // FIXME : this is PoC for search, need a more performant version of this
337
+ // TODO : this is PoC for search, need a more performant version of this
248
338
func findPackage( identifier: PackageReference . PackageIdentity ,
249
339
collectionIdentifiers: [ PackageCollectionsModel . CollectionIdentifier ] ? ,
250
340
callback: @escaping ( Result < PackageCollectionsModel . PackageSearchResult . Item , Error > ) -> Void ) {
@@ -269,14 +359,89 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
269
359
}
270
360
}
271
361
272
- // FIXME: implement this
362
+ // TODO: this is PoC for search, need a more performant version of this
273
363
func searchTargets( identifiers: [ PackageCollectionsModel . CollectionIdentifier ] ? = nil ,
274
364
query: String ,
275
365
type: PackageCollectionsModel . TargetSearchType ,
276
366
callback: @escaping ( Result < PackageCollectionsModel . TargetSearchResult , Error > ) -> Void ) {
277
- fatalError ( " not implemented " )
367
+ let query = query. lowercased ( )
368
+
369
+ self . list ( identifiers: identifiers) { result in
370
+ switch result {
371
+ case . failure( let error) :
372
+ callback ( . failure( error) )
373
+ case . success( let collections) :
374
+ let collectionsPackages = collections. reduce ( [ PackageCollectionsModel . CollectionIdentifier: [ ( target: PackageCollectionsModel . PackageTarget, package : PackageCollectionsModel . Collection. Package) ] ] ( ) ) { partial, collection in
375
+ var map = partial
376
+ collection. packages. forEach { package in
377
+ package . versions. forEach { version in
378
+ version. targets. forEach { target in
379
+ let match : Bool
380
+ switch type {
381
+ case . exactMatch:
382
+ match = target. name. lowercased ( ) == query
383
+ case . prefix:
384
+ match = target. name. lowercased ( ) . hasPrefix ( query)
385
+ }
386
+ if match {
387
+ // Avoid copy-on-write: remove entry from dictionary before mutating
388
+ var entry = map. removeValue ( forKey: collection. identifier) ?? . init( )
389
+ entry. append ( ( target, package ) )
390
+ map [ collection. identifier] = entry
391
+ }
392
+ }
393
+ }
394
+ }
395
+ return map
396
+ }
397
+
398
+ // compose result :p
399
+
400
+ var packageCollections = [ PackageReference : ( package : PackageCollectionsModel . Collection . Package , collections: Set < PackageCollectionsModel . CollectionIdentifier > ) ] ( )
401
+ var targetsPackages = [ PackageCollectionsModel . PackageTarget : Set < PackageReference > ] ( )
402
+
403
+ collectionsPackages. forEach { collectionIdentifier, packagesAndTargets in
404
+ packagesAndTargets. forEach { item in
405
+ // Avoid copy-on-write: remove entry from dictionary before mutating
406
+ var packageCollectionsEntry = packageCollections. removeValue ( forKey: item. package . reference) ?? ( item. package , . init( ) )
407
+ packageCollectionsEntry. collections. insert ( collectionIdentifier)
408
+ packageCollections [ item. package . reference] = packageCollectionsEntry
409
+
410
+ // Avoid copy-on-write: remove entry from dictionary before mutating
411
+ var targetsPackagesEntry = targetsPackages. removeValue ( forKey: item. target) ?? . init( )
412
+ targetsPackagesEntry. insert ( item. package . reference)
413
+ targetsPackages [ item. target] = targetsPackagesEntry
414
+ }
415
+ }
416
+
417
+ let result = PackageCollectionsModel . TargetSearchResult ( items: targetsPackages. map { target, packages in
418
+ let targetsPackages = packages
419
+ . compactMap { packageCollections [ $0] }
420
+ . map { pair -> PackageCollectionsModel . TargetListItem . Package in
421
+ let versions = pair. package . versions. map { PackageCollectionsModel . TargetListItem. Package. Version ( version: $0. version, packageName: $0. packageName) }
422
+ return PackageCollectionsModel . TargetListItem. Package ( repository: pair. package . repository,
423
+ description: pair. package . summary,
424
+ versions: versions,
425
+ collections: Array ( pair. collections) )
426
+ }
427
+
428
+ return PackageCollectionsModel . TargetListItem ( target: target, packages: targetsPackages)
429
+ } )
430
+
431
+ callback ( . success( result) )
432
+ }
433
+ }
434
+ }
435
+
436
+ // for testing
437
+ internal func resetCache( ) {
438
+ self . cacheLock. withLock {
439
+ self . cache = [ : ]
440
+ }
278
441
}
279
442
443
+ // MARK: - Private
444
+
280
445
private func createSchemaIfNecessary( db: SQLite ) throws {
281
446
let table = """
282
447
CREATE TABLE IF NOT EXISTS PACKAGES_COLLECTIONS (
0 commit comments