1 //  Copyright © 2019 650 Industries. All rights reserved.
2 
3 // A lot of stuff in this class was originally written in objective-c, and the swift
4 // equivalents don't seem to work quite the same, which is important to have backwards
5 // data compatibility.
6 // swiftlint:disable legacy_objc_type
7 // swiftlint:disable line_length
8 // swiftlint:disable type_body_length
9 // swiftlint:disable force_unwrapping
10 // swiftlint:disable file_length
11 
12 import Foundation
13 import sqlite3
14 import EXManifests
15 
16 internal enum UpdatesDatabaseError: Error {
17   case addExistingAssetMissingAssetKey
18   case addExistingAssetInsertError
19   case addExistingAssetAssetNotFoundError
20   case markMissingAssetsError
21   case deleteUpdatesError
22   case deleteUnusedAssetsError
23   case getUpdatesError
24   case setJsonDataError
25 }
26 
27 enum UpdatesDatabaseHashType: Int {
28   case Sha1 = 0
29 }
30 
31 /**
32  * SQLite database that keeps track of updates currently loaded/loading to disk, including the
33  * update manifest and metadata, status, and the individual assets (including bundles/bytecode) that
34  * comprise the update. (Assets themselves are stored on the device's file system, and a relative
35  * path is kept in SQLite.)
36  *
37  * SQLite allows a many-to-many relationship between updates and assets, which means we can keep
38  * only one copy of each asset on disk at a time while also being able to clear unused assets with
39  * relative ease (see UpdatesReaper).
40  *
41  * Occasionally it's necessary to add migrations when the data structures for updates or assets must
42  * change. Extra care must be taken here, since these migrations will happen on users' devices for
43  * apps we do not control. See
44  * https://github.com/expo/expo/blob/main/packages/expo-updates/guides/migrations.md for step by
45  * step instructions.
46  *
47  * UpdatesDatabase provides a serial queue on which all database operations must be run (methods
48  * in this class will assert). This is primarily for control over what high-level operations
49  * involving the database can occur simultaneously - e.g. we don't want to be trying to download a
50  * new update at the same time UpdatesReaper is running.
51  *
52  * The `scopeKey` field in various methods here is only relevant in environments such as Expo Go in
53  * which updates from multiple scopes can be launched.
54  */
55 @objc(EXUpdatesDatabase)
56 @objcMembers
57 public final class UpdatesDatabase: NSObject {
58   private static let ManifestFiltersKey = "manifestFilters"
59   private static let ServerDefinedHeadersKey = "serverDefinedHeaders"
60   private static let StaticBuildDataKey = "staticBuildData"
61   private static let ExtraParmasKey = "extraParams"
62 
63   public let databaseQueue: DispatchQueue
64   private var db: OpaquePointer?
65 
66   public required override init() {
67     self.databaseQueue = DispatchQueue(label: "expo.database.DatabaseQueue")
68   }
69 
70   deinit {
71     closeDatabase()
72   }
73 
openDatabasenull74   public func openDatabase(inDirectory directory: URL) throws {
75     dispatchPrecondition(condition: .onQueue(databaseQueue))
76     db = try UpdatesDatabaseInitialization.initializeDatabaseWithLatestSchema(inDirectory: directory)
77   }
78 
closeDatabasenull79   public func closeDatabase() {
80     sqlite3_close(db)
81     db = nil
82   }
83 
executenull84   public func execute(sql: String, withArgs args: [Any?]?) throws -> [[String: Any?]] {
85     dispatchPrecondition(condition: .onQueue(databaseQueue))
86     return try UpdatesDatabaseUtils.execute(sql: sql, withArgs: args, onDatabase: db.require("Missing database handle"))
87   }
88 
executeForObjCnull89   public func executeForObjC(sql: String, withArgs args: [Any]?) throws -> [Any] {
90     return try execute(sql: sql, withArgs: args)
91   }
92 
addUpdatenull93   public func addUpdate(_ update: Update) throws {
94     let sql = """
95       INSERT INTO "updates" ("id", "scope_key", "commit_time", "runtime_version", "manifest", "status" , "keep", "last_accessed", "successful_launch_count", "failed_launch_count")
96       VALUES (?1, ?2, ?3, ?4, ?5, ?6, 1, ?7, ?8, ?9);
97     """
98     _ = try execute(
99       sql: sql,
100       withArgs: [
101         update.updateId,
102         update.scopeKey.require("Update must have scopeKey to be stored in database"),
103         update.commitTime,
104         update.runtimeVersion,
105         update.manifest.rawManifestJSON(),
106         update.status.rawValue,
107         update.lastAccessed,
108         update.successfulLaunchCount,
109         update.failedLaunchCount
110       ]
111     )
112   }
113 
addNewAssetsnull114   public func addNewAssets(_ assets: [UpdateAsset], toUpdateWithId updateId: UUID) throws {
115     sqlite3_exec(db, "BEGIN;", nil, nil, nil)
116 
117     let assetInsertSql = """
118       INSERT OR REPLACE INTO "assets" ("key", "url", "headers", "extra_request_headers", "type", "metadata", "download_time", "relative_path", "hash", "hash_type", "expected_hash", "marked_for_deletion")
119       VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, 0);
120     """
121     for asset in assets {
122       do {
123         _ = try execute(
124           sql: assetInsertSql,
125           withArgs: [
126             asset.key,
127             asset.url,
128             asset.headers,
129             asset.extraRequestHeaders,
130             asset.type,
131             asset.metadata,
132             asset.downloadTime.require("asset downloadTime should be nonnull"),
133             asset.filename,
134             asset.contentHash,
135             UpdatesDatabaseHashType.Sha1.rawValue,
136             asset.expectedHash
137           ]
138         )
139       } catch {
140         sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
141         return
142       }
143 
144       // statements must stay in precisely this order for last_insert_rowid() to work correctly
145       if asset.isLaunchAsset {
146         let updateSql = "UPDATE updates SET launch_asset_id = last_insert_rowid() WHERE id = ?1;"
147         do {
148           _ = try execute(sql: updateSql, withArgs: [updateId])
149         } catch {
150           sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
151           return
152         }
153       }
154 
155       let updateInsertSql = """
156         INSERT OR REPLACE INTO updates_assets ("update_id", "asset_id") VALUES (?1, last_insert_rowid());
157       """
158       do {
159         _ = try execute(sql: updateInsertSql, withArgs: [updateId])
160       } catch {
161         sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
162         return
163       }
164     }
165 
166     sqlite3_exec(db, "COMMIT;", nil, nil, nil)
167   }
168 
addExistingAssetnull169   public func addExistingAsset(_ asset: UpdateAsset, toUpdateWithId updateId: UUID) throws -> Bool {
170     guard let key = asset.key else {
171       return false
172     }
173 
174     sqlite3_exec(db, "BEGIN;", nil, nil, nil)
175 
176     let assetSelectSql = """
177       SELECT id FROM assets WHERE "key" = ?1 LIMIT 1;
178     """
179     let rows = try execute(sql: assetSelectSql, withArgs: [key])
180     if !rows.isEmpty {
181       let assetId: NSNumber = rows[0].requiredValue(forKey: "id")
182       let insertSql = """
183         INSERT OR REPLACE INTO updates_assets ("update_id", "asset_id") VALUES (?1, ?2);
184       """
185       do {
186         _ = try execute(sql: insertSql, withArgs: [updateId, assetId.intValue])
187       } catch {
188         sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
189         throw UpdatesDatabaseError.addExistingAssetInsertError
190       }
191 
192       if asset.isLaunchAsset {
193         let updateSql = "UPDATE updates SET launch_asset_id = ?1 WHERE id = ?2;"
194         do {
195           _ = try execute(sql: updateSql, withArgs: [assetId.intValue, updateId])
196         } catch {
197           sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
198           throw UpdatesDatabaseError.addExistingAssetInsertError
199         }
200       }
201     }
202 
203     sqlite3_exec(db, "COMMIT;", nil, nil, nil)
204 
205     if rows.isEmpty {
206       return false
207     }
208 
209     return true
210   }
211 
updateAssetnull212   public func updateAsset(_ asset: UpdateAsset) throws {
213     let assetUpdateSql = """
214       UPDATE "assets" SET "headers" = ?2, "extra_request_headers" = ?3, "type" = ?4, "metadata" = ?5, "download_time" = ?6, "relative_path" = ?7, "hash" = ?8, "expected_hash" = ?9, "url" = ?10 WHERE "key" = ?1;
215     """
216     _ = try execute(
217       sql: assetUpdateSql,
218       withArgs: [
219         asset.key,
220         asset.headers,
221         asset.extraRequestHeaders,
222         asset.type,
223         asset.metadata,
224         asset.downloadTime.require("asset downloadTime should be nonnull"),
225         asset.filename,
226         asset.contentHash.require("asset contentHash should be nonnull"),
227         asset.expectedHash,
228         asset.url?.absoluteString
229       ]
230     )
231   }
232 
mergeAssetnull233   public func mergeAsset(_ asset: UpdateAsset, withExistingEntry existingAsset: UpdateAsset) throws {
234     var shouldUpdate = false
235 
236     // if the existing entry came from an embedded manifest, it may not have a URL in the database
237     if let url = asset.url,
238       existingAsset.url == nil || url != existingAsset.url {
239       existingAsset.url = url
240       shouldUpdate = true
241     }
242 
243     if let extraRequestHeaders = asset.extraRequestHeaders,
244       existingAsset.extraRequestHeaders == nil || !NSDictionary(dictionary: extraRequestHeaders).isEqual(to: existingAsset.extraRequestHeaders!) {
245       existingAsset.extraRequestHeaders = extraRequestHeaders
246       shouldUpdate = true
247     }
248 
249     if shouldUpdate {
250       try updateAsset(existingAsset)
251     }
252 
253     // all other properties should be overridden by database values
254     asset.filename = existingAsset.filename
255     asset.contentHash = existingAsset.contentHash
256     asset.expectedHash = existingAsset.expectedHash
257     asset.downloadTime = existingAsset.downloadTime
258   }
259 
markUpdateFinishednull260   public func markUpdateFinished(_ update: Update) throws {
261     if update.status != UpdateStatus.StatusDevelopment {
262       update.status = UpdateStatus.StatusReady
263     }
264 
265     let updateSql = "UPDATE updates SET status = ?1, keep = 1 WHERE id = ?2;"
266     _ = try execute(sql: updateSql, withArgs: [update.status.rawValue, update.updateId])
267   }
268 
markUpdateAccessednull269   public func markUpdateAccessed(_ update: Update) throws {
270     update.lastAccessed = Date()
271     let updateSql = "UPDATE updates SET last_accessed = ?1 WHERE id = ?2;"
272     _ = try execute(sql: updateSql, withArgs: [update.lastAccessed, update.updateId])
273   }
274 
incrementSuccessfulLaunchCountForUpdatenull275   public func incrementSuccessfulLaunchCountForUpdate(_ update: Update) throws {
276     update.successfulLaunchCount += 1
277     let updateSql = "UPDATE updates SET successful_launch_count = ?1 WHERE id = ?2;"
278     _ = try execute(sql: updateSql, withArgs: [update.successfulLaunchCount, update.updateId])
279   }
280 
incrementFailedLaunchCountForUpdatenull281   public func incrementFailedLaunchCountForUpdate(_ update: Update) throws {
282     update.failedLaunchCount += 1
283     let updateSql = "UPDATE updates SET failed_launch_count = ?1 WHERE id = ?2;"
284     _ = try execute(sql: updateSql, withArgs: [update.failedLaunchCount, update.updateId])
285   }
286 
setScopeKeynull287   public func setScopeKey(_ scopeKey: String, onUpdate update: Update) throws {
288     let updateSql = "UPDATE updates SET scope_key = ?1 WHERE id = ?2;"
289     _ = try execute(sql: updateSql, withArgs: [scopeKey, update.updateId])
290   }
291 
setUpdateCommitTimenull292   public func setUpdateCommitTime(_ commitTime: Date, onUpdate update: Update) throws {
293     let updateSql = "UPDATE updates SET commit_time = ?1 WHERE id = ?2;"
294     _ = try execute(sql: updateSql, withArgs: [commitTime, update.updateId])
295   }
296 
markMissingAssetsnull297   public func markMissingAssets(_ assets: [UpdateAsset]) throws {
298     sqlite3_exec(db, "BEGIN;", nil, nil, nil)
299 
300     let updateSql = "UPDATE updates SET status = ?1 WHERE id IN (SELECT DISTINCT update_id FROM updates_assets WHERE asset_id = ?2);"
301     for asset in assets {
302       do {
303         _ = try execute(sql: updateSql, withArgs: [UpdateStatus.StatusPending.rawValue, asset.assetId])
304       } catch {
305         sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
306         throw UpdatesDatabaseError.markMissingAssetsError
307       }
308     }
309 
310     sqlite3_exec(db, "COMMIT;", nil, nil, nil)
311   }
312 
deleteUpdatesnull313   public func deleteUpdates(_ updates: [Update]) throws {
314     sqlite3_exec(db, "BEGIN;", nil, nil, nil)
315 
316     let updateSql = "DELETE FROM updates WHERE id = ?1;"
317     for update in updates {
318       do {
319         _ = try execute(sql: updateSql, withArgs: [update.updateId])
320       } catch {
321         sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
322         throw UpdatesDatabaseError.deleteUpdatesError
323       }
324     }
325 
326     sqlite3_exec(db, "COMMIT;", nil, nil, nil)
327   }
328 
deleteUnusedAssetsnull329   public func deleteUnusedAssets() throws -> [UpdateAsset] {
330     // the simplest way to mark the assets we want to delete
331     // is to mark all assets for deletion, then go back and unmark
332     // those assets in updates we want to keep
333     // this is safe as long as we do this inside of a transaction
334 
335     sqlite3_exec(db, "BEGIN;", nil, nil, nil)
336 
337     let update1Sql = "UPDATE assets SET marked_for_deletion = 1;"
338     do {
339       _ = try execute(sql: update1Sql, withArgs: nil)
340     } catch {
341       sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
342       throw UpdatesDatabaseError.deleteUnusedAssetsError
343     }
344 
345     let update2Sql = "UPDATE assets SET marked_for_deletion = 0 WHERE id IN (SELECT asset_id FROM updates_assets INNER JOIN updates ON updates_assets.update_id = updates.id WHERE updates.keep = 1);"
346     do {
347       _ = try execute(sql: update2Sql, withArgs: nil)
348     } catch {
349       sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
350       throw UpdatesDatabaseError.deleteUnusedAssetsError
351     }
352 
353     // check for duplicate rows representing a single file on disk
354     let update3Sql = "UPDATE assets SET marked_for_deletion = 0 WHERE relative_path IN (SELECT relative_path FROM assets WHERE marked_for_deletion = 0);"
355     do {
356       _ = try execute(sql: update3Sql, withArgs: nil)
357     } catch {
358       sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
359       throw UpdatesDatabaseError.deleteUnusedAssetsError
360     }
361 
362     var rows: [[String: Any?]]
363     let selectSql = "SELECT * FROM assets WHERE marked_for_deletion = 1;"
364     do {
365       rows = try execute(sql: selectSql, withArgs: nil)
366     } catch {
367       sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
368       throw UpdatesDatabaseError.deleteUnusedAssetsError
369     }
370 
371     let assets = rows.map { row in
372       asset(withRow: row)
373     }
374 
375     let deleteSql = "DELETE FROM assets WHERE marked_for_deletion = 1;"
376     do {
377       _ = try execute(sql: deleteSql, withArgs: nil)
378     } catch {
379       sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
380       throw UpdatesDatabaseError.deleteUnusedAssetsError
381     }
382 
383     sqlite3_exec(db, "COMMIT;", nil, nil, nil)
384 
385     return assets
386   }
387 
allUpdatesnull388   public func allUpdates(withConfig config: UpdatesConfig) throws -> [Update] {
389     let sql = "SELECT * FROM updates;"
390     let rows = try execute(sql: sql, withArgs: nil)
391     return rows.map { row in
392       update(withRow: row, config: config)
393     }
394   }
395 
allUpdatesnull396   public func allUpdates(withStatus status: UpdateStatus, config: UpdatesConfig) throws -> [Update] {
397     let sql = "SELECT * FROM updates WHERE status = ?1;"
398     let rows = try execute(sql: sql, withArgs: [status.rawValue])
399     return rows.map { row in
400       update(withRow: row, config: config)
401     }
402   }
403 
allUpdateIdsnull404   public func allUpdateIds(withStatus status: UpdateStatus) throws -> [UUID] {
405     let sql = "SELECT id FROM updates WHERE status = ?1;"
406     let rows = try execute(sql: sql, withArgs: [status.rawValue])
407     return rows.map { row in
408       // swiftlint:disable:next force_cast
409       row["id"] as! UUID
410     }
411   }
412 
launchableUpdatesnull413   public func launchableUpdates(withConfig config: UpdatesConfig) throws -> [Update] {
414     // if an update has successfully launched at least once, we treat it as launchable
415     // even if it has also failed to launch at least once
416     let sql = String(
417       format: "SELECT * FROM updates WHERE scope_key = ?1 AND (successful_launch_count > 0 OR failed_launch_count < 1) AND status IN (%li, %li, %li);",
418       UpdateStatus.StatusReady.rawValue,
419       UpdateStatus.StatusEmbedded.rawValue,
420       UpdateStatus.StatusDevelopment.rawValue
421     )
422 
423     let rows = try execute(sql: sql, withArgs: [config.scopeKey])
424     return rows.map { row in
425       update(withRow: row, config: config)
426     }
427   }
428 
updatenull429   public func update(withId updateId: UUID, config: UpdatesConfig) throws -> Update? {
430     let sql = "SELECT * FROM updates WHERE updates.id = ?1;"
431     let rows = try execute(sql: sql, withArgs: [updateId])
432     if rows.isEmpty {
433       return nil
434     }
435     return update(withRow: rows.first!, config: config)
436   }
437 
allAssetsnull438   public func allAssets() throws -> [UpdateAsset] {
439     let sql = "SELECT * FROM assets;"
440     let rows = try execute(sql: sql, withArgs: nil)
441     return rows.map { row in
442       asset(withRow: row)
443     }
444   }
445 
assetsnull446   public func assets(withUpdateId updateId: UUID) throws -> [UpdateAsset] {
447     let sql = "SELECT assets.*, launch_asset_id FROM assets INNER JOIN updates_assets ON updates_assets.asset_id = assets.id INNER JOIN updates ON updates_assets.update_id = updates.id WHERE updates.id = ?1;"
448     let rows = try execute(sql: sql, withArgs: [updateId])
449     return rows.map { row in
450       asset(withRow: row)
451     }
452   }
453 
assetnull454   public func asset(withKey key: String?) throws -> UpdateAsset? {
455     guard let key = key else {
456       return nil
457     }
458 
459     let sql = """
460       SELECT * FROM assets WHERE "key" = ?1 LIMIT 1;
461     """
462 
463     let rows = try execute(sql: sql, withArgs: [key])
464     if rows.isEmpty {
465       return nil
466     }
467     return asset(withRow: rows.first!)
468   }
469 
jsonDatanull470   private func jsonData(withKey key: String, scopeKey: String) throws -> [String: Any]? {
471     let sql = """
472       SELECT * FROM json_data WHERE "key" = ?1 AND "scope_key" = ?2
473     """
474     let rows = try execute(sql: sql, withArgs: [key, scopeKey])
475     guard let firstRow = rows.first,
476       let value = firstRow["value"] as? String else {
477       return nil
478     }
479 
480     return try JSONSerialization.jsonObject(with: value.data(using: .utf8)!) as? [String: Any]
481   }
482 
setJsonDatanull483   private func setJsonData(_ data: [String: Any], withKey key: String, scopeKey: String, isInTransaction: Bool) throws {
484     if !isInTransaction {
485       sqlite3_exec(db, "BEGIN;", nil, nil, nil)
486     }
487 
488     let deleteSql = """
489       DELETE FROM json_data WHERE "key" = ?1 AND "scope_key" = ?2;
490     """
491     do {
492       _ = try execute(sql: deleteSql, withArgs: [key, scopeKey])
493     } catch {
494       if !isInTransaction {
495         sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
496       }
497       throw UpdatesDatabaseError.setJsonDataError
498     }
499 
500     let insertSql = """
501       INSERT INTO json_data ("key", "value", "last_updated", "scope_key") VALUES (?1, ?2, ?3, ?4);
502     """
503     do {
504       _ = try execute(sql: insertSql, withArgs: [key, data, Date().timeIntervalSince1970 * 1000, scopeKey])
505     } catch {
506       if !isInTransaction {
507         sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
508       }
509       throw UpdatesDatabaseError.setJsonDataError
510     }
511 
512     if !isInTransaction {
513       sqlite3_exec(db, "COMMIT;", nil, nil, nil)
514     }
515   }
516 
serverDefinedHeadersnull517   public func serverDefinedHeaders(withScopeKey scopeKey: String) throws -> [String: Any]? {
518     return try jsonData(withKey: UpdatesDatabase.ServerDefinedHeadersKey, scopeKey: scopeKey)
519   }
520 
manifestFiltersnull521   public func manifestFilters(withScopeKey scopeKey: String) throws -> [String: Any]? {
522     return try jsonData(withKey: UpdatesDatabase.ManifestFiltersKey, scopeKey: scopeKey)
523   }
524 
staticBuildDatanull525   public func staticBuildData(withScopeKey scopeKey: String) throws -> [String: Any]? {
526     return try jsonData(withKey: UpdatesDatabase.StaticBuildDataKey, scopeKey: scopeKey)
527   }
528 
extraParamsnull529   public func extraParams(withScopeKey scopeKey: String) throws -> [String: String]? {
530     return try jsonData(withKey: UpdatesDatabase.ExtraParmasKey, scopeKey: scopeKey) as? [String: String]
531   }
532 
setServerDefinedHeadersnull533   public func setServerDefinedHeaders(_ serverDefinedHeaders: [String: Any], withScopeKey scopeKey: String) throws {
534     return try setJsonData(serverDefinedHeaders, withKey: UpdatesDatabase.ServerDefinedHeadersKey, scopeKey: scopeKey, isInTransaction: false)
535   }
536 
setManifestFiltersnull537   public func setManifestFilters(_ manifestFilters: [String: Any], withScopeKey scopeKey: String) throws {
538     return try setJsonData(manifestFilters, withKey: UpdatesDatabase.ManifestFiltersKey, scopeKey: scopeKey, isInTransaction: false)
539   }
540 
setStaticBuildDatanull541   public func setStaticBuildData(_ staticBuildData: [String: Any], withScopeKey scopeKey: String) throws {
542     return try setJsonData(staticBuildData, withKey: UpdatesDatabase.StaticBuildDataKey, scopeKey: scopeKey, isInTransaction: false)
543   }
544 
setExtraParamnull545   public func setExtraParam(key: String, value: String?, withScopeKey scopeKey: String) throws {
546     sqlite3_exec(db, "BEGIN;", nil, nil, nil)
547 
548     do {
549       var extraParamsToWrite = try extraParams(withScopeKey: scopeKey) ?? [:]
550       if let value = value {
551         extraParamsToWrite[key] = value
552       } else {
553         extraParamsToWrite.removeValue(forKey: key)
554       }
555 
556       // ensure that this can be serialized to a structured-header dictionary
557       // this will throw for invalid values
558       _ = try StringStringDictionarySerializer.serialize(dictionary: extraParamsToWrite)
559 
560       _ = try setJsonData(extraParamsToWrite, withKey: UpdatesDatabase.ExtraParmasKey, scopeKey: scopeKey, isInTransaction: true)
561     } catch {
562       sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
563       throw error
564     }
565 
566     sqlite3_exec(db, "COMMIT;", nil, nil, nil)
567   }
568 
setMetadatanull569   internal func setMetadata(withResponseHeaderData responseHeaderData: ResponseHeaderData, scopeKey: String) throws {
570     sqlite3_exec(db, "BEGIN;", nil, nil, nil)
571 
572     if let serverDefinedHeaders = responseHeaderData.serverDefinedHeaders {
573       do {
574         _ = try setJsonData(serverDefinedHeaders, withKey: UpdatesDatabase.ServerDefinedHeadersKey, scopeKey: scopeKey, isInTransaction: true)
575       } catch {
576         sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
577         throw UpdatesDatabaseError.setJsonDataError
578       }
579     }
580 
581     if let manifestFilters = responseHeaderData.manifestFilters {
582       do {
583         _ = try setJsonData(manifestFilters, withKey: UpdatesDatabase.ManifestFiltersKey, scopeKey: scopeKey, isInTransaction: true)
584       } catch {
585         sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
586         throw UpdatesDatabaseError.setJsonDataError
587       }
588     }
589 
590     sqlite3_exec(db, "COMMIT;", nil, nil, nil)
591   }
592 
updatenull593   private func update(withRow row: [String: Any?], config: UpdatesConfig) -> Update {
594     let rowManifest: String = row.requiredValue(forKey: "manifest")
595     let manifest = (try? JSONSerialization.jsonObject(with: rowManifest.data(using: .utf8)!) as? [String: Any]).require("Update manifest should be a valid JSON object")
596     let keep: NSNumber = row.requiredValue(forKey: "keep")
597     let status: NSNumber = row.requiredValue(forKey: "status")
598     let successfulLaunchCount: NSNumber = row.requiredValue(forKey: "successful_launch_count")
599     let failedLaunchCount: NSNumber = row.requiredValue(forKey: "failed_launch_count")
600 
601     let update = Update(
602       manifest: ManifestFactory.manifest(forManifestJSON: manifest),
603       config: config,
604       database: self,
605       updateId: row.requiredValue(forKey: "id"),
606       scopeKey: row.requiredValue(forKey: "scope_key"),
607       commitTime: UpdatesDatabaseUtils.date(fromUnixTimeMilliseconds: row.requiredValue(forKey: "commit_time")),
608       runtimeVersion: row.requiredValue(forKey: "runtime_version"),
609       keep: keep.intValue != 0,
610       status: UpdateStatus.init(rawValue: status.intValue)!,
611       isDevelopmentMode: false,
612       assetsFromManifest: nil
613     )
614     update.lastAccessed = UpdatesDatabaseUtils.date(fromUnixTimeMilliseconds: row.requiredValue(forKey: "last_accessed"))
615     update.successfulLaunchCount = successfulLaunchCount.intValue
616     update.failedLaunchCount = failedLaunchCount.intValue
617     return update
618   }
619 
assetnull620   private func asset(withRow row: [String: Any?]) -> UpdateAsset {
621     let rowMetadata = row["metadata"]
622     var metadata: [String: Any]?
623     if let rowMetadata = rowMetadata as? String {
624       metadata = (try? JSONSerialization.jsonObject(with: rowMetadata.data(using: .utf8)!) as? [String: Any]).require("Asset metadata should be a valid JSON object")
625     }
626 
627     let rowExtraRequestHeaders = row["extra_request_headers"]
628     var extraRequestHeaders: [String: Any]?
629     if let rowExtraRequestHeaders = rowExtraRequestHeaders as? String {
630       extraRequestHeaders = (try? JSONSerialization.jsonObject(with: rowExtraRequestHeaders.data(using: .utf8)!) as? [String: Any]).require("Asset extra_request_headers should be a valid JSON object")
631     }
632 
633     let launchAssetId: NSNumber? = row.optionalValue(forKey: "launch_asset_id")
634 
635     var url: URL?
636     let rowUrl: Any? = row.optionalValue(forKey: "url")
637     if let rowUrl = rowUrl as? String {
638       url = URL(string: rowUrl)
639     }
640 
641     var key: String?
642     let rowKey: Any? = row.optionalValue(forKey: "key")
643     if let rowKey = rowKey as? String {
644       key = rowKey
645     }
646 
647     let assetId: NSNumber = row.requiredValue(forKey: "id")
648     let asset = UpdateAsset(key: key, type: row.optionalValue(forKey: "type"))
649     asset.assetId = assetId.intValue
650     asset.url = url
651     asset.extraRequestHeaders = extraRequestHeaders
652     asset.downloadTime = UpdatesDatabaseUtils.date(fromUnixTimeMilliseconds: row.requiredValue(forKey: "download_time"))
653     asset.filename = row.requiredValue(forKey: "relative_path")
654     asset.contentHash = row.requiredValue(forKey: "hash")
655     asset.expectedHash = row.optionalValue(forKey: "expected_hash")
656     asset.metadata = metadata
657 
658     if let launchAssetId = launchAssetId?.intValue,
659        launchAssetId == assetId.intValue {
660       asset.isLaunchAsset = true
661     } else {
662       asset.isLaunchAsset = false
663     }
664 
665     return asset
666   }
667 }
668