// // GamesDatabase.swift // Delta // // Created by Riley Testut on 11/16/16. // Copyright © 2016 Riley Testut. All rights reserved. // import Foundation import SQLite private extension UserDefaults { @NSManaged var previousGamesDatabaseVersion: Int } extension ExpressionType { static var name: SQLite.Expression { return SQLite.Expression("releaseTitleName") } static var artworkAddress: SQLite.Expression { return SQLite.Expression("releaseCoverFront") } static var sha1Hash: SQLite.Expression { return SQLite.Expression("romHashSHA1") } static var romID: SQLite.Expression { return SQLite.Expression("romID") } static var releaseID: SQLite.Expression { return SQLite.Expression("releaseID") } } extension Table { static var roms: Table { return Table("ROMs") } static var releases: Table { return Table("RELEASES") } } extension VirtualTable { static var search: VirtualTable { return VirtualTable("Search") } } extension GamesDatabase { enum Error: LocalizedError { case doesNotExist var errorDescription: String? { switch self { case .doesNotExist: return NSLocalizedString("The SQLite database could not be found.", comment: "") } } } } class GamesDatabase { static let version = 3 static var previousVersion: Int? { return UserDefaults.standard.previousGamesDatabaseVersion } private let connection: Connection init() throws { let fileURL = DatabaseManager.gamesDatabaseURL do { self.connection = try Connection(fileURL.path) } catch { throw error } self.invalidateVirtualTableIfNeeded() } func metadataResults(forGameName gameName: String) -> [GameMetadata] { let releaseID = Expression.releaseID let romID = Expression.romID let name = Expression.name let artworkAddress = Expression.artworkAddress let query = VirtualTable.search.select(releaseID, romID, name, artworkAddress).filter(name.match(gameName + "*")) do { let rows = try self.connection.prepare(query) let results = rows.map { (row) -> GameMetadata in let artworkURL: URL? if let address = row[artworkAddress] { artworkURL = URL(string: address) } else { artworkURL = nil } let metadata = GameMetadata(releaseID: row[releaseID], romID: row[romID], name: row[name], artworkURL: artworkURL) return metadata } return results } catch SQLite.Result.error(_, let code, _) where code == 1 { // Table does not exist if self.prepareFTS() { return self.metadataResults(forGameName: gameName) } } catch { print(error) } return [] } func metadata(for game: Game) -> GameMetadata? { let releaseID = Expression.releaseID let name = Expression.name let artworkAddress = Expression.artworkAddress let sha1Hash = Expression.sha1Hash let romID = Expression.romID let gameHash = game.identifier.uppercased() let query = Table.roms.select(releaseID, name, artworkAddress, Table.roms[romID]).filter(sha1Hash == gameHash).join(Table.releases, on: Table.roms[romID] == Table.releases[romID]) do { if let row = try self.connection.pluck(query) { let artworkURL: URL? if let address = row[artworkAddress] { artworkURL = URL(string: address) } else { artworkURL = nil } let metadata = GameMetadata(releaseID: row[releaseID], romID: row[Table.roms[romID]], name: row[name], artworkURL: artworkURL) return metadata } } catch { print(error) } return nil } } private extension GamesDatabase { func invalidateVirtualTableIfNeeded() { guard UserDefaults.standard.previousGamesDatabaseVersion != GamesDatabase.version else { return } do { try self.connection.run(VirtualTable.search.drop(ifExists: true)) UserDefaults.standard.previousGamesDatabaseVersion = GamesDatabase.version } catch { print(error) } } func prepareFTS() -> Bool { let name = Expression.name let artworkAddress = Expression.artworkAddress let releaseID = Expression.releaseID let romID = Expression.romID do { try self.connection.run(VirtualTable.search.create(.FTS4([releaseID, romID, name, artworkAddress], tokenize: .Unicode61()))) let update = VirtualTable.search.insert(Table.releases.select(releaseID, romID, name, artworkAddress)) _ = try self.connection.run(update) } catch { print(error) return false } return true } }