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 
62   public let databaseQueue: DispatchQueue
63   private var db: OpaquePointer?
64 
65   public required override init() {
66     self.databaseQueue = DispatchQueue(label: "expo.database.DatabaseQueue")
67   }
68 
69   deinit {
70     closeDatabase()
71   }
72 
73   public func openDatabase(inDirectory directory: URL) throws {
74     dispatchPrecondition(condition: .onQueue(databaseQueue))
75     db = try UpdatesDatabaseInitialization.initializeDatabaseWithLatestSchema(inDirectory: directory)
76   }
77 
78   public func closeDatabase() {
79     sqlite3_close(db)
80     db = nil
81   }
82 
83   public func execute(sql: String, withArgs args: [Any?]?) throws -> [[String: Any?]] {
84     dispatchPrecondition(condition: .onQueue(databaseQueue))
85     return try UpdatesDatabaseUtils.execute(sql: sql, withArgs: args, onDatabase: db.require("Missing database handle"))
86   }
87 
88   public func executeForObjC(sql: String, withArgs args: [Any]?) throws -> [Any] {
89     return try execute(sql: sql, withArgs: args)
90   }
91 
92   public func addUpdate(_ update: Update) throws {
93     let sql = """
94       INSERT INTO "updates" ("id", "scope_key", "commit_time", "runtime_version", "manifest", "status" , "keep", "last_accessed", "successful_launch_count", "failed_launch_count")
95       VALUES (?1, ?2, ?3, ?4, ?5, ?6, 1, ?7, ?8, ?9);
96     """
97     _ = try execute(
98       sql: sql,
99       withArgs: [
100         update.updateId,
101         update.scopeKey,
102         update.commitTime,
103         update.runtimeVersion,
104         update.manifest.rawManifestJSON(),
105         update.status.rawValue,
106         update.lastAccessed,
107         update.successfulLaunchCount,
108         update.failedLaunchCount
109       ]
110     )
111   }
112 
113   public func addNewAssets(_ assets: [UpdateAsset], toUpdateWithId updateId: UUID) throws {
114     sqlite3_exec(db, "BEGIN;", nil, nil, nil)
115 
116     let assetInsertSql = """
117       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")
118       VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, 0);
119     """
120     for asset in assets {
121       do {
122         _ = try execute(
123           sql: assetInsertSql,
124           withArgs: [
125             asset.key,
126             asset.url,
127             asset.headers,
128             asset.extraRequestHeaders,
129             asset.type,
130             asset.metadata,
131             asset.downloadTime.require("asset downloadTime should be nonnull"),
132             asset.filename,
133             asset.contentHash,
134             UpdatesDatabaseHashType.Sha1.rawValue,
135             asset.expectedHash
136           ]
137         )
138       } catch {
139         sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
140         return
141       }
142 
143       // statements must stay in precisely this order for last_insert_rowid() to work correctly
144       if asset.isLaunchAsset {
145         let updateSql = "UPDATE updates SET launch_asset_id = last_insert_rowid() WHERE id = ?1;"
146         do {
147           _ = try execute(sql: updateSql, withArgs: [updateId])
148         } catch {
149           sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
150           return
151         }
152       }
153 
154       let updateInsertSql = """
155         INSERT OR REPLACE INTO updates_assets ("update_id", "asset_id") VALUES (?1, last_insert_rowid());
156       """
157       do {
158         _ = try execute(sql: updateInsertSql, withArgs: [updateId])
159       } catch {
160         sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
161         return
162       }
163     }
164 
165     sqlite3_exec(db, "COMMIT;", nil, nil, nil)
166   }
167 
168   public func addExistingAsset(_ asset: UpdateAsset, toUpdateWithId updateId: UUID) throws -> Bool {
169     guard let key = asset.key else {
170       return false
171     }
172 
173     sqlite3_exec(db, "BEGIN;", nil, nil, nil)
174 
175     let assetSelectSql = """
176       SELECT id FROM assets WHERE "key" = ?1 LIMIT 1;
177     """
178     let rows = try execute(sql: assetSelectSql, withArgs: [key])
179     if !rows.isEmpty {
180       let assetId: NSNumber = rows[0].requiredValue(forKey: "id")
181       let insertSql = """
182         INSERT OR REPLACE INTO updates_assets ("update_id", "asset_id") VALUES (?1, ?2);
183       """
184       do {
185         _ = try execute(sql: insertSql, withArgs: [updateId, assetId.intValue])
186       } catch {
187         sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
188         throw UpdatesDatabaseError.addExistingAssetInsertError
189       }
190 
191       if asset.isLaunchAsset {
192         let updateSql = "UPDATE updates SET launch_asset_id = ?1 WHERE id = ?2;"
193         do {
194           _ = try execute(sql: updateSql, withArgs: [assetId.intValue, updateId])
195         } catch {
196           sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
197           throw UpdatesDatabaseError.addExistingAssetInsertError
198         }
199       }
200     }
201 
202     sqlite3_exec(db, "COMMIT;", nil, nil, nil)
203 
204     if rows.isEmpty {
205       return false
206     }
207 
208     return true
209   }
210 
211   public func updateAsset(_ asset: UpdateAsset) throws {
212     let assetUpdateSql = """
213       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;
214     """
215     _ = try execute(
216       sql: assetUpdateSql,
217       withArgs: [
218         asset.key,
219         asset.headers,
220         asset.extraRequestHeaders,
221         asset.type,
222         asset.metadata,
223         asset.downloadTime.require("asset downloadTime should be nonnull"),
224         asset.filename,
225         asset.contentHash.require("asset contentHash should be nonnull"),
226         asset.expectedHash,
227         asset.url?.absoluteString
228       ]
229     )
230   }
231 
232   public func mergeAsset(_ asset: UpdateAsset, withExistingEntry existingAsset: UpdateAsset) throws {
233     var shouldUpdate = false
234 
235     // if the existing entry came from an embedded manifest, it may not have a URL in the database
236     if let url = asset.url,
237       existingAsset.url == nil || url != existingAsset.url {
238       existingAsset.url = url
239       shouldUpdate = true
240     }
241 
242     if let extraRequestHeaders = asset.extraRequestHeaders,
243       existingAsset.extraRequestHeaders == nil || !NSDictionary(dictionary: extraRequestHeaders).isEqual(to: existingAsset.extraRequestHeaders!) {
244       existingAsset.extraRequestHeaders = extraRequestHeaders
245       shouldUpdate = true
246     }
247 
248     if shouldUpdate {
249       try updateAsset(existingAsset)
250     }
251 
252     // all other properties should be overridden by database values
253     asset.filename = existingAsset.filename
254     asset.contentHash = existingAsset.contentHash
255     asset.expectedHash = existingAsset.expectedHash
256     asset.downloadTime = existingAsset.downloadTime
257   }
258 
259   public func markUpdateFinished(_ update: Update) throws {
260     if update.status != UpdateStatus.StatusDevelopment {
261       update.status = UpdateStatus.StatusReady
262     }
263 
264     let updateSql = "UPDATE updates SET status = ?1, keep = 1 WHERE id = ?2;"
265     _ = try execute(sql: updateSql, withArgs: [update.status.rawValue, update.updateId])
266   }
267 
268   public func markUpdateAccessed(_ update: Update) throws {
269     update.lastAccessed = Date()
270     let updateSql = "UPDATE updates SET last_accessed = ?1 WHERE id = ?2;"
271     _ = try execute(sql: updateSql, withArgs: [update.lastAccessed, update.updateId])
272   }
273 
274   public func incrementSuccessfulLaunchCountForUpdate(_ update: Update) throws {
275     update.successfulLaunchCount += 1
276     let updateSql = "UPDATE updates SET successful_launch_count = ?1 WHERE id = ?2;"
277     _ = try execute(sql: updateSql, withArgs: [update.successfulLaunchCount, update.updateId])
278   }
279 
280   public func incrementFailedLaunchCountForUpdate(_ update: Update) throws {
281     update.failedLaunchCount += 1
282     let updateSql = "UPDATE updates SET failed_launch_count = ?1 WHERE id = ?2;"
283     _ = try execute(sql: updateSql, withArgs: [update.failedLaunchCount, update.updateId])
284   }
285 
286   public func setScopeKey(_ scopeKey: String, onUpdate update: Update) throws {
287     let updateSql = "UPDATE updates SET scope_key = ?1 WHERE id = ?2;"
288     _ = try execute(sql: updateSql, withArgs: [scopeKey, update.updateId])
289   }
290 
291   public func setUpdateCommitTime(_ commitTime: Date, onUpdate update: Update) throws {
292     let updateSql = "UPDATE updates SET commit_time = ?1 WHERE id = ?2;"
293     _ = try execute(sql: updateSql, withArgs: [commitTime, update.updateId])
294   }
295 
296   public func markMissingAssets(_ assets: [UpdateAsset]) throws {
297     sqlite3_exec(db, "BEGIN;", nil, nil, nil)
298 
299     let updateSql = "UPDATE updates SET status = ?1 WHERE id IN (SELECT DISTINCT update_id FROM updates_assets WHERE asset_id = ?2);"
300     for asset in assets {
301       do {
302         _ = try execute(sql: updateSql, withArgs: [UpdateStatus.StatusPending.rawValue, asset.assetId])
303       } catch {
304         sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
305         throw UpdatesDatabaseError.markMissingAssetsError
306       }
307     }
308 
309     sqlite3_exec(db, "COMMIT;", nil, nil, nil)
310   }
311 
312   public func deleteUpdates(_ updates: [Update]) throws {
313     sqlite3_exec(db, "BEGIN;", nil, nil, nil)
314 
315     let updateSql = "DELETE FROM updates WHERE id = ?1;"
316     for update in updates {
317       do {
318         _ = try execute(sql: updateSql, withArgs: [update.updateId])
319       } catch {
320         sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
321         throw UpdatesDatabaseError.deleteUpdatesError
322       }
323     }
324 
325     sqlite3_exec(db, "COMMIT;", nil, nil, nil)
326   }
327 
328   public func deleteUnusedAssets() throws -> [UpdateAsset] {
329     // the simplest way to mark the assets we want to delete
330     // is to mark all assets for deletion, then go back and unmark
331     // those assets in updates we want to keep
332     // this is safe as long as we do this inside of a transaction
333 
334     sqlite3_exec(db, "BEGIN;", nil, nil, nil)
335 
336     let update1Sql = "UPDATE assets SET marked_for_deletion = 1;"
337     do {
338       _ = try execute(sql: update1Sql, withArgs: nil)
339     } catch {
340       sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
341       throw UpdatesDatabaseError.deleteUnusedAssetsError
342     }
343 
344     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);"
345     do {
346       _ = try execute(sql: update2Sql, withArgs: nil)
347     } catch {
348       sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
349       throw UpdatesDatabaseError.deleteUnusedAssetsError
350     }
351 
352     // check for duplicate rows representing a single file on disk
353     let update3Sql = "UPDATE assets SET marked_for_deletion = 0 WHERE relative_path IN (SELECT relative_path FROM assets WHERE marked_for_deletion = 0);"
354     do {
355       _ = try execute(sql: update3Sql, withArgs: nil)
356     } catch {
357       sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
358       throw UpdatesDatabaseError.deleteUnusedAssetsError
359     }
360 
361     var rows: [[String: Any?]]
362     let selectSql = "SELECT * FROM assets WHERE marked_for_deletion = 1;"
363     do {
364       rows = try execute(sql: selectSql, withArgs: nil)
365     } catch {
366       sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
367       throw UpdatesDatabaseError.deleteUnusedAssetsError
368     }
369 
370     let assets = rows.map { row in
371       asset(withRow: row)
372     }
373 
374     let deleteSql = "DELETE FROM assets WHERE marked_for_deletion = 1;"
375     do {
376       _ = try execute(sql: deleteSql, withArgs: nil)
377     } catch {
378       sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
379       throw UpdatesDatabaseError.deleteUnusedAssetsError
380     }
381 
382     sqlite3_exec(db, "COMMIT;", nil, nil, nil)
383 
384     return assets
385   }
386 
387   public func allUpdates(withConfig config: UpdatesConfig) throws -> [Update] {
388     let sql = "SELECT * FROM updates;"
389     let rows = try execute(sql: sql, withArgs: nil)
390     return rows.map { row in
391       update(withRow: row, config: config)
392     }
393   }
394 
395   public func allUpdates(withStatus status: UpdateStatus, config: UpdatesConfig) throws -> [Update] {
396     let sql = "SELECT * FROM updates WHERE status = ?1;"
397     let rows = try execute(sql: sql, withArgs: [status.rawValue])
398     return rows.map { row in
399       update(withRow: row, config: config)
400     }
401   }
402 
403   public func allUpdateIds(withStatus status: UpdateStatus) throws -> [UUID] {
404     let sql = "SELECT id FROM updates WHERE status = ?1;"
405     let rows = try execute(sql: sql, withArgs: [status.rawValue])
406     return rows.map { row in
407       // swiftlint:disable:next force_cast
408       row["id"] as! UUID
409     }
410   }
411 
412   public func launchableUpdates(withConfig config: UpdatesConfig) throws -> [Update] {
413     // if an update has successfully launched at least once, we treat it as launchable
414     // even if it has also failed to launch at least once
415     let sql = String(
416       format: "SELECT * FROM updates WHERE scope_key = ?1 AND (successful_launch_count > 0 OR failed_launch_count < 1) AND status IN (%li, %li, %li);",
417       UpdateStatus.StatusReady.rawValue,
418       UpdateStatus.StatusEmbedded.rawValue,
419       UpdateStatus.StatusDevelopment.rawValue
420     )
421 
422     let rows = try execute(sql: sql, withArgs: [config.scopeKey])
423     return rows.map { row in
424       update(withRow: row, config: config)
425     }
426   }
427 
428   public func update(withId updateId: UUID, config: UpdatesConfig) throws -> Update? {
429     let sql = "SELECT * FROM updates WHERE updates.id = ?1;"
430     let rows = try execute(sql: sql, withArgs: [updateId])
431     if rows.isEmpty {
432       return nil
433     }
434     return update(withRow: rows.first!, config: config)
435   }
436 
437   public func allAssets() throws -> [UpdateAsset] {
438     let sql = "SELECT * FROM assets;"
439     let rows = try execute(sql: sql, withArgs: nil)
440     return rows.map { row in
441       asset(withRow: row)
442     }
443   }
444 
445   public func assets(withUpdateId updateId: UUID) throws -> [UpdateAsset] {
446     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;"
447     let rows = try execute(sql: sql, withArgs: [updateId])
448     return rows.map { row in
449       asset(withRow: row)
450     }
451   }
452 
453   public func asset(withKey key: String?) throws -> UpdateAsset? {
454     guard let key = key else {
455       return nil
456     }
457 
458     let sql = """
459       SELECT * FROM assets WHERE "key" = ?1 LIMIT 1;
460     """
461 
462     let rows = try execute(sql: sql, withArgs: [key])
463     if rows.isEmpty {
464       return nil
465     }
466     return asset(withRow: rows.first!)
467   }
468 
469   private func jsonData(withKey key: String, scopeKey: String) throws -> [String: Any]? {
470     let sql = """
471       SELECT * FROM json_data WHERE "key" = ?1 AND "scope_key" = ?2
472     """
473     let rows = try execute(sql: sql, withArgs: [key, scopeKey])
474     guard let firstRow = rows.first,
475       let value = firstRow["value"] as? String else {
476       return nil
477     }
478 
479     return try JSONSerialization.jsonObject(with: value.data(using: .utf8)!) as? [String: Any]
480   }
481 
482   private func setJsonData(_ data: [String: Any], withKey key: String, scopeKey: String, isInTransaction: Bool) throws {
483     if !isInTransaction {
484       sqlite3_exec(db, "BEGIN;", nil, nil, nil)
485     }
486 
487     let deleteSql = """
488       DELETE FROM json_data WHERE "key" = ?1 AND "scope_key" = ?2;
489     """
490     do {
491       _ = try execute(sql: deleteSql, withArgs: [key, scopeKey])
492     } catch {
493       if !isInTransaction {
494         sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
495       }
496       throw UpdatesDatabaseError.setJsonDataError
497     }
498 
499     let insertSql = """
500       INSERT INTO json_data ("key", "value", "last_updated", "scope_key") VALUES (?1, ?2, ?3, ?4);
501     """
502     do {
503       _ = try execute(sql: insertSql, withArgs: [key, data, Date().timeIntervalSince1970 * 1000, scopeKey])
504     } catch {
505       if !isInTransaction {
506         sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
507       }
508       throw UpdatesDatabaseError.setJsonDataError
509     }
510 
511     if !isInTransaction {
512       sqlite3_exec(db, "COMMIT;", nil, nil, nil)
513     }
514   }
515 
516   public func serverDefinedHeaders(withScopeKey scopeKey: String) throws -> [String: Any]? {
517     return try jsonData(withKey: UpdatesDatabase.ServerDefinedHeadersKey, scopeKey: scopeKey)
518   }
519 
520   public func manifestFilters(withScopeKey scopeKey: String) throws -> [String: Any]? {
521     return try jsonData(withKey: UpdatesDatabase.ManifestFiltersKey, scopeKey: scopeKey)
522   }
523 
524   public func staticBuildData(withScopeKey scopeKey: String) throws -> [String: Any]? {
525     return try jsonData(withKey: UpdatesDatabase.StaticBuildDataKey, scopeKey: scopeKey)
526   }
527 
528   public func setServerDefinedHeaders(_ serverDefinedHeaders: [String: Any], withScopeKey scopeKey: String) throws {
529     return try setJsonData(serverDefinedHeaders, withKey: UpdatesDatabase.ServerDefinedHeadersKey, scopeKey: scopeKey, isInTransaction: false)
530   }
531 
532   public func setManifestFilters(_ manifestFilters: [String: Any], withScopeKey scopeKey: String) throws {
533     return try setJsonData(manifestFilters, withKey: UpdatesDatabase.ManifestFiltersKey, scopeKey: scopeKey, isInTransaction: false)
534   }
535 
536   public func setStaticBuildData(_ staticBuildData: [String: Any], withScopeKey scopeKey: String) throws {
537     return try setJsonData(staticBuildData, withKey: UpdatesDatabase.StaticBuildDataKey, scopeKey: scopeKey, isInTransaction: false)
538   }
539 
540   internal func setMetadata(withResponseHeaderData responseHeaderData: ResponseHeaderData, scopeKey: String) throws {
541     sqlite3_exec(db, "BEGIN;", nil, nil, nil)
542 
543     if let serverDefinedHeaders = responseHeaderData.serverDefinedHeaders {
544       do {
545         _ = try setJsonData(serverDefinedHeaders, withKey: UpdatesDatabase.ServerDefinedHeadersKey, scopeKey: scopeKey, isInTransaction: true)
546       } catch {
547         sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
548         throw UpdatesDatabaseError.setJsonDataError
549       }
550     }
551 
552     if let manifestFilters = responseHeaderData.manifestFilters {
553       do {
554         _ = try setJsonData(manifestFilters, withKey: UpdatesDatabase.ManifestFiltersKey, scopeKey: scopeKey, isInTransaction: true)
555       } catch {
556         sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
557         throw UpdatesDatabaseError.setJsonDataError
558       }
559     }
560 
561     sqlite3_exec(db, "COMMIT;", nil, nil, nil)
562   }
563 
564   private func update(withRow row: [String: Any?], config: UpdatesConfig) -> Update {
565     let rowManifest = row["manifest"]
566     var manifest: [String: Any]?
567     if let rowManifest = rowManifest as? String {
568       manifest = (try? JSONSerialization.jsonObject(with: rowManifest.data(using: .utf8)!) as? [String: Any]).require("Update manifest should be a valid JSON object")
569     }
570 
571     let keep: NSNumber = row.requiredValue(forKey: "keep")
572     let status: NSNumber = row.requiredValue(forKey: "status")
573     let successfulLaunchCount: NSNumber = row.requiredValue(forKey: "successful_launch_count")
574     let failedLaunchCount: NSNumber = row.requiredValue(forKey: "failed_launch_count")
575 
576     let update = Update(
577       manifest: ManifestFactory.manifest(forManifestJSON: manifest ?? [:]),
578       config: config,
579       database: self,
580       updateId: row.requiredValue(forKey: "id"),
581       scopeKey: row.requiredValue(forKey: "scope_key"),
582       commitTime: UpdatesDatabaseUtils.date(fromUnixTimeMilliseconds: row.requiredValue(forKey: "commit_time")),
583       runtimeVersion: row.requiredValue(forKey: "runtime_version"),
584       keep: keep.intValue != 0,
585       status: UpdateStatus.init(rawValue: status.intValue)!,
586       isDevelopmentMode: false,
587       assetsFromManifest: nil
588     )
589     update.lastAccessed = UpdatesDatabaseUtils.date(fromUnixTimeMilliseconds: row.requiredValue(forKey: "last_accessed"))
590     update.successfulLaunchCount = successfulLaunchCount.intValue
591     update.failedLaunchCount = failedLaunchCount.intValue
592     return update
593   }
594 
595   private func asset(withRow row: [String: Any?]) -> UpdateAsset {
596     let rowMetadata = row["metadata"]
597     var metadata: [String: Any]?
598     if let rowMetadata = rowMetadata as? String {
599       metadata = (try? JSONSerialization.jsonObject(with: rowMetadata.data(using: .utf8)!) as? [String: Any]).require("Asset metadata should be a valid JSON object")
600     }
601 
602     let rowExtraRequestHeaders = row["extra_request_headers"]
603     var extraRequestHeaders: [String: Any]?
604     if let rowExtraRequestHeaders = rowExtraRequestHeaders as? String {
605       extraRequestHeaders = (try? JSONSerialization.jsonObject(with: rowExtraRequestHeaders.data(using: .utf8)!) as? [String: Any]).require("Asset extra_request_headers should be a valid JSON object")
606     }
607 
608     let launchAssetId: NSNumber? = row.optionalValue(forKey: "launch_asset_id")
609 
610     var url: URL?
611     let rowUrl: Any? = row.optionalValue(forKey: "url")
612     if let rowUrl = rowUrl as? String {
613       url = URL(string: rowUrl)
614     }
615 
616     var key: String?
617     let rowKey: Any? = row.optionalValue(forKey: "key")
618     if let rowKey = rowKey as? String {
619       key = rowKey
620     }
621 
622     let assetId: NSNumber = row.requiredValue(forKey: "id")
623     let asset = UpdateAsset(key: key, type: row.optionalValue(forKey: "type"))
624     asset.assetId = assetId.intValue
625     asset.url = url
626     asset.extraRequestHeaders = extraRequestHeaders
627     asset.downloadTime = UpdatesDatabaseUtils.date(fromUnixTimeMilliseconds: row.requiredValue(forKey: "download_time"))
628     asset.filename = row.requiredValue(forKey: "relative_path")
629     asset.contentHash = row.requiredValue(forKey: "hash")
630     asset.expectedHash = row.optionalValue(forKey: "expected_hash")
631     asset.metadata = metadata
632 
633     if let launchAssetId = launchAssetId?.intValue,
634        launchAssetId == assetId.intValue {
635       asset.isLaunchAsset = true
636     } else {
637       asset.isLaunchAsset = false
638     }
639 
640     return asset
641   }
642 }
643