754 lines
26 KiB
Swift
754 lines
26 KiB
Swift
//
|
|
// Copyright 2019 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
import GRDB
|
|
|
|
// This class can be used to:
|
|
//
|
|
// * Back a preferences class.
|
|
// * To persist simple values in our managers.
|
|
// * Etc.
|
|
@objc
|
|
public class SDSKeyValueStore: NSObject {
|
|
|
|
static let tableName = "keyvalue"
|
|
|
|
// By default, all reads/writes use this collection.
|
|
// Key-value stores use "collections" to group related keys.
|
|
@objc
|
|
public let collection: String
|
|
|
|
static let collectionColumn = SDSColumnMetadata(columnName: "collection", columnType: .unicodeString, isOptional: false)
|
|
static let keyColumn = SDSColumnMetadata(columnName: "key", columnType: .unicodeString, isOptional: false)
|
|
static let valueColumn = SDSColumnMetadata(columnName: "value", columnType: .blob, isOptional: false)
|
|
public static let table = SDSTableMetadata(
|
|
tableName: SDSKeyValueStore.tableName,
|
|
columns: [
|
|
collectionColumn,
|
|
keyColumn,
|
|
valueColumn
|
|
]
|
|
)
|
|
|
|
@objc
|
|
public required init(collection: String) {
|
|
self.collection = collection
|
|
|
|
super.init()
|
|
}
|
|
|
|
@objc
|
|
public class func logCollectionStatistics() {
|
|
Logger.info("SDSKeyValueStore statistics:")
|
|
databaseStorage.read { transaction in
|
|
do {
|
|
let sql = """
|
|
SELECT \(collectionColumn.columnName), COUNT(*)
|
|
FROM \(table.tableName)
|
|
GROUP BY \(collectionColumn.columnName)
|
|
ORDER BY COUNT(*) DESC
|
|
LIMIT 10
|
|
"""
|
|
let cursor = try Row.fetchCursor(transaction.unwrapGrdbRead.database, sql: sql)
|
|
while let row = try cursor.next() {
|
|
let collection: String = row[0]
|
|
let count: UInt = row[1]
|
|
Logger.info("- \(collection): \(count) items")
|
|
}
|
|
} catch {
|
|
Logger.error("\(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Class Helpers
|
|
|
|
@objc
|
|
public class func key(int: Int) -> String {
|
|
return NSNumber(value: int).stringValue
|
|
}
|
|
|
|
@objc
|
|
public func hasValue(forKey key: String, transaction: SDSAnyReadTransaction) -> Bool {
|
|
switch transaction.readTransaction {
|
|
case .grdbRead(let grdbTransaction):
|
|
do {
|
|
let count = try UInt.fetchOne(grdbTransaction.database,
|
|
sql: """
|
|
SELECT
|
|
COUNT(*)
|
|
FROM \(SDSKeyValueStore.table.tableName)
|
|
WHERE \(SDSKeyValueStore.keyColumn.columnName) = ?
|
|
AND \(SDSKeyValueStore.collectionColumn.columnName) == ?
|
|
""",
|
|
arguments: [key, collection]) ?? 0
|
|
return count > 0
|
|
} catch {
|
|
owsFailDebug("error: \(error)")
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - String
|
|
|
|
@objc
|
|
public func getString(_ key: String, transaction: SDSAnyReadTransaction) -> String? {
|
|
return read(key, transaction: transaction)
|
|
}
|
|
|
|
@objc
|
|
public func setString(_ value: String?, key: String, transaction: SDSAnyWriteTransaction) {
|
|
guard let value = value else {
|
|
write(nil, forKey: key, transaction: transaction)
|
|
return
|
|
}
|
|
write(value as NSString, forKey: key, transaction: transaction)
|
|
}
|
|
|
|
// MARK: - Date
|
|
|
|
@objc
|
|
public func getDate(_ key: String, transaction: SDSAnyReadTransaction) -> Date? {
|
|
// Our legacy methods sometimes stored dates as NSNumber and
|
|
// sometimes as NSDate, so we are permissive when decoding.
|
|
guard let object: NSObject = read(key, transaction: transaction) else {
|
|
return nil
|
|
}
|
|
if let date = object as? Date {
|
|
return date
|
|
}
|
|
guard let epochInterval = object as? NSNumber else {
|
|
owsFailDebug("Could not decode value: \(type(of: object)).")
|
|
return nil
|
|
}
|
|
return Date(timeIntervalSince1970: epochInterval.doubleValue)
|
|
}
|
|
|
|
@objc
|
|
public func setDate(_ value: Date, key: String, transaction: SDSAnyWriteTransaction) {
|
|
let epochInterval = NSNumber(value: value.timeIntervalSince1970)
|
|
setObject(epochInterval, key: key, transaction: transaction)
|
|
}
|
|
|
|
// MARK: - Bool
|
|
|
|
public func getBool(_ key: String, transaction: SDSAnyReadTransaction) -> Bool? {
|
|
guard let number: NSNumber = read(key, transaction: transaction) else {
|
|
return nil
|
|
}
|
|
return number.boolValue
|
|
}
|
|
|
|
@objc
|
|
public func getBool(_ key: String, defaultValue: Bool, transaction: SDSAnyReadTransaction) -> Bool {
|
|
return getBool(key, transaction: transaction) ?? defaultValue
|
|
}
|
|
|
|
@objc
|
|
public func setBool(_ value: Bool, key: String, transaction: SDSAnyWriteTransaction) {
|
|
write(NSNumber(value: value), forKey: key, transaction: transaction)
|
|
}
|
|
|
|
@objc
|
|
public func setBoolIfChanged(_ value: Bool,
|
|
defaultValue: Bool,
|
|
key: String,
|
|
transaction: SDSAnyWriteTransaction) {
|
|
let didChange = value != getBool(key,
|
|
defaultValue: defaultValue,
|
|
transaction: transaction)
|
|
guard didChange else {
|
|
return
|
|
}
|
|
setBool(value, key: key, transaction: transaction)
|
|
}
|
|
|
|
// MARK: - UInt
|
|
|
|
public func getUInt(_ key: String, transaction: SDSAnyReadTransaction) -> UInt? {
|
|
guard let number: NSNumber = read(key, transaction: transaction) else {
|
|
return nil
|
|
}
|
|
return number.uintValue
|
|
}
|
|
|
|
// TODO: Handle numerics more generally.
|
|
@objc
|
|
public func getUInt(_ key: String, defaultValue: UInt, transaction: SDSAnyReadTransaction) -> UInt {
|
|
return getUInt(key, transaction: transaction) ?? defaultValue
|
|
}
|
|
|
|
@objc
|
|
public func setUInt(_ value: UInt, key: String, transaction: SDSAnyWriteTransaction) {
|
|
write(NSNumber(value: value), forKey: key, transaction: transaction)
|
|
}
|
|
|
|
// MARK: - Data
|
|
|
|
@objc
|
|
public func getData(_ key: String, transaction: SDSAnyReadTransaction) -> Data? {
|
|
return readData(key, transaction: transaction)
|
|
}
|
|
|
|
@objc
|
|
public func setData(_ value: Data?, key: String, transaction: SDSAnyWriteTransaction) {
|
|
writeData(value, forKey: key, transaction: transaction)
|
|
}
|
|
|
|
// MARK: - Numeric
|
|
|
|
@objc
|
|
public func getNSNumber(_ key: String, transaction: SDSAnyReadTransaction) -> NSNumber? {
|
|
let number: NSNumber? = read(key, transaction: transaction)
|
|
return number
|
|
}
|
|
|
|
// MARK: - Int
|
|
|
|
public func getInt(_ key: String, transaction: SDSAnyReadTransaction) -> Int? {
|
|
guard let number: NSNumber = read(key, transaction: transaction) else {
|
|
return nil
|
|
}
|
|
return number.intValue
|
|
}
|
|
|
|
@objc
|
|
public func getInt(_ key: String, defaultValue: Int, transaction: SDSAnyReadTransaction) -> Int {
|
|
return getInt(key, transaction: transaction) ?? defaultValue
|
|
}
|
|
|
|
@objc
|
|
public func setInt(_ value: Int, key: String, transaction: SDSAnyWriteTransaction) {
|
|
setObject(NSNumber(value: value), key: key, transaction: transaction)
|
|
}
|
|
|
|
// MARK: - Int32
|
|
|
|
public func getInt32(_ key: String, transaction: SDSAnyReadTransaction) -> Int32? {
|
|
guard let number: NSNumber = read(key, transaction: transaction) else {
|
|
return nil
|
|
}
|
|
return number.int32Value
|
|
}
|
|
|
|
@objc
|
|
public func getInt32(_ key: String, defaultValue: Int32, transaction: SDSAnyReadTransaction) -> Int32 {
|
|
return getInt32(key, transaction: transaction) ?? defaultValue
|
|
}
|
|
|
|
@objc
|
|
public func setInt32(_ value: Int32, key: String, transaction: SDSAnyWriteTransaction) {
|
|
setObject(NSNumber(value: value), key: key, transaction: transaction)
|
|
}
|
|
|
|
// MARK: - UInt32
|
|
|
|
public func getUInt32(_ key: String, transaction: SDSAnyReadTransaction) -> UInt32? {
|
|
guard let number: NSNumber = read(key, transaction: transaction) else {
|
|
return nil
|
|
}
|
|
return number.uint32Value
|
|
}
|
|
|
|
@objc
|
|
public func getUInt32(_ key: String, defaultValue: UInt32, transaction: SDSAnyReadTransaction) -> UInt32 {
|
|
return getUInt32(key, transaction: transaction) ?? defaultValue
|
|
}
|
|
|
|
@objc
|
|
public func setUInt32(_ value: UInt32, key: String, transaction: SDSAnyWriteTransaction) {
|
|
setObject(NSNumber(value: value), key: key, transaction: transaction)
|
|
}
|
|
|
|
// MARK: - UInt64
|
|
|
|
public func getUInt64(_ key: String, transaction: SDSAnyReadTransaction) -> UInt64? {
|
|
guard let number: NSNumber = read(key, transaction: transaction) else {
|
|
return nil
|
|
}
|
|
return number.uint64Value
|
|
}
|
|
|
|
@objc
|
|
public func getUInt64(_ key: String, defaultValue: UInt64, transaction: SDSAnyReadTransaction) -> UInt64 {
|
|
return getUInt64(key, transaction: transaction) ?? defaultValue
|
|
}
|
|
|
|
@objc
|
|
public func setUInt64(_ value: UInt64, key: String, transaction: SDSAnyWriteTransaction) {
|
|
setObject(NSNumber(value: value), key: key, transaction: transaction)
|
|
}
|
|
|
|
// MARK: - Int64
|
|
|
|
public func getInt64(_ key: String, transaction: SDSAnyReadTransaction) -> Int64? {
|
|
guard let number: NSNumber = read(key, transaction: transaction) else {
|
|
return nil
|
|
}
|
|
return number.int64Value
|
|
}
|
|
|
|
@objc
|
|
public func getInt64(_ key: String, defaultValue: Int64, transaction: SDSAnyReadTransaction) -> Int64 {
|
|
return getInt64(key, transaction: transaction) ?? defaultValue
|
|
}
|
|
|
|
@objc
|
|
public func setInt64(_ value: Int64, key: String, transaction: SDSAnyWriteTransaction) {
|
|
setObject(NSNumber(value: value), key: key, transaction: transaction)
|
|
}
|
|
|
|
// MARK: - Double
|
|
|
|
public func getDouble(_ key: String, transaction: SDSAnyReadTransaction) -> Double? {
|
|
guard let number: NSNumber = read(key, transaction: transaction) else {
|
|
return nil
|
|
}
|
|
return number.doubleValue
|
|
}
|
|
|
|
@objc
|
|
public func getDouble(_ key: String, defaultValue: Double, transaction: SDSAnyReadTransaction) -> Double {
|
|
return getDouble(key, transaction: transaction) ?? defaultValue
|
|
}
|
|
|
|
@objc
|
|
public func setDouble(_ value: Double, key: String, transaction: SDSAnyWriteTransaction) {
|
|
setObject(NSNumber(value: value), key: key, transaction: transaction)
|
|
}
|
|
|
|
// MARK: - Object
|
|
|
|
@objc
|
|
public func getObject(forKey key: String, transaction: SDSAnyReadTransaction) -> Any? {
|
|
return read(key, transaction: transaction)
|
|
}
|
|
|
|
@objc
|
|
public func setObject(_ anyValue: Any?, key: String, transaction: SDSAnyWriteTransaction) {
|
|
guard let anyValue = anyValue else {
|
|
write(nil, forKey: key, transaction: transaction)
|
|
return
|
|
}
|
|
guard let codingValue = anyValue as? NSCoding else {
|
|
owsFailDebug("Invalid value.")
|
|
write(nil, forKey: key, transaction: transaction)
|
|
return
|
|
}
|
|
write(codingValue, forKey: key, transaction: transaction)
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@objc
|
|
public func removeValue(forKey key: String, transaction: SDSAnyWriteTransaction) {
|
|
write(nil, forKey: key, transaction: transaction)
|
|
}
|
|
|
|
@objc
|
|
public func removeValues(forKeys keys: [String], transaction: SDSAnyWriteTransaction) {
|
|
for key in keys {
|
|
write(nil, forKey: key, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public func removeAll(transaction: SDSAnyWriteTransaction) {
|
|
switch transaction.writeTransaction {
|
|
case .grdbWrite(let grdbWrite):
|
|
let sql = """
|
|
DELETE
|
|
FROM \(SDSKeyValueStore.table.tableName)
|
|
WHERE \(SDSKeyValueStore.collectionColumn.columnName) == ?
|
|
"""
|
|
grdbWrite.executeAndCacheStatement(sql: sql, arguments: [collection])
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public func enumerateKeysAndObjects(transaction: SDSAnyReadTransaction, block: (String, Any, UnsafeMutablePointer<ObjCBool>) -> Void) {
|
|
switch transaction.readTransaction {
|
|
case .grdbRead(let grdbRead):
|
|
var stop: ObjCBool = false
|
|
// PERF - we could enumerate with a single query rather than
|
|
// fetching keys then fetching objects one by one. In practice
|
|
// the collections that use this are pretty small.
|
|
for key in allKeys(grdbTransaction: grdbRead) {
|
|
guard !stop.boolValue else {
|
|
return
|
|
}
|
|
guard let value: Any = read(key, transaction: transaction) else {
|
|
owsFailDebug("value was unexpectedly nil")
|
|
continue
|
|
}
|
|
block(key, value, &stop)
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public func enumerateKeys(transaction: SDSAnyReadTransaction, block: (String, UnsafeMutablePointer<ObjCBool>) -> Void) {
|
|
switch transaction.readTransaction {
|
|
case .grdbRead(let grdbRead):
|
|
var stop: ObjCBool = false
|
|
for key in allKeys(grdbTransaction: grdbRead) {
|
|
guard !stop.boolValue else {
|
|
return
|
|
}
|
|
block(key, &stop)
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public func allValues(transaction: SDSAnyReadTransaction) -> [Any] {
|
|
return allKeys(transaction: transaction).compactMap { key in
|
|
return self.read(key, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public func allDataValues(transaction: SDSAnyReadTransaction) -> [Data] {
|
|
return allKeys(transaction: transaction).compactMap { key in
|
|
return self.getData(key, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
private struct PairRecord: Codable, FetchableRecord, PersistableRecord {
|
|
public let key: String?
|
|
public let value: Data?
|
|
}
|
|
|
|
private func allPairs(transaction: SDSAnyReadTransaction) -> [PairRecord] {
|
|
|
|
switch transaction.readTransaction {
|
|
case .grdbRead(let grdbTransaction):
|
|
|
|
let sql = """
|
|
SELECT
|
|
\(SDSKeyValueStore.keyColumn.columnName),
|
|
\(SDSKeyValueStore.valueColumn.columnName)
|
|
FROM \(SDSKeyValueStore.table.tableName)
|
|
WHERE \(SDSKeyValueStore.collectionColumn.columnName) == ?
|
|
"""
|
|
|
|
do {
|
|
return try PairRecord.fetchAll(grdbTransaction.database,
|
|
sql: sql,
|
|
arguments: [collection])
|
|
} catch {
|
|
owsFailDebug("Error: \(error)")
|
|
return []
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public func allBoolValuesMap(transaction: SDSAnyReadTransaction) -> [String: Bool] {
|
|
let pairs = allPairs(transaction: transaction)
|
|
var result = [String: Bool]()
|
|
for pair in pairs {
|
|
guard let key = pair.key else {
|
|
owsFailDebug("missing key.")
|
|
continue
|
|
}
|
|
guard let value = pair.value else {
|
|
owsFailDebug("missing value.")
|
|
continue
|
|
}
|
|
guard let rawObject = parseArchivedValue(value) else {
|
|
owsFailDebug("Could not parse value.")
|
|
continue
|
|
}
|
|
guard let number: NSNumber = parseValueAs(key: key,
|
|
rawObject: rawObject) else {
|
|
owsFailDebug("Invalid value.")
|
|
continue
|
|
}
|
|
result[key] = number.boolValue
|
|
}
|
|
return result
|
|
}
|
|
|
|
@objc
|
|
public func allUIntValuesMap(transaction: SDSAnyReadTransaction) -> [String: UInt] {
|
|
let pairs = allPairs(transaction: transaction)
|
|
var result = [String: UInt]()
|
|
for pair in pairs {
|
|
guard let key = pair.key else {
|
|
owsFailDebug("missing key.")
|
|
continue
|
|
}
|
|
guard let value = pair.value else {
|
|
owsFailDebug("missing value.")
|
|
continue
|
|
}
|
|
guard let rawObject = parseArchivedValue(value) else {
|
|
owsFailDebug("Could not parse value.")
|
|
continue
|
|
}
|
|
guard let number: NSNumber = parseValueAs(key: key,
|
|
rawObject: rawObject) else {
|
|
owsFailDebug("Invalid value.")
|
|
continue
|
|
}
|
|
result[key] = number.uintValue
|
|
}
|
|
return result
|
|
}
|
|
|
|
@objc
|
|
public func anyDataValue(transaction: SDSAnyReadTransaction) -> Data? {
|
|
let randomKey = allKeys(transaction: transaction).randomElement()
|
|
guard let randomKey else {
|
|
return nil
|
|
}
|
|
guard let dataValue = self.getData(randomKey, transaction: transaction) else {
|
|
owsFailDebug("Couldn't fetch random element")
|
|
return nil
|
|
}
|
|
return dataValue
|
|
}
|
|
|
|
@objc
|
|
public func allKeys(transaction: SDSAnyReadTransaction) -> [String] {
|
|
switch transaction.readTransaction {
|
|
case .grdbRead(let grdbRead):
|
|
return allKeys(grdbTransaction: grdbRead)
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public func numberOfKeys(transaction: SDSAnyReadTransaction) -> UInt {
|
|
switch transaction.readTransaction {
|
|
case .grdbRead(let grdbRead):
|
|
let sql = """
|
|
SELECT COUNT(*)
|
|
FROM \(SDSKeyValueStore.table.tableName)
|
|
WHERE \(SDSKeyValueStore.collectionColumn.columnName) == ?
|
|
"""
|
|
do {
|
|
guard let numberOfKeys = try UInt.fetchOne(grdbRead.database,
|
|
sql: sql,
|
|
arguments: [collection]) else {
|
|
throw OWSAssertionError("numberOfKeys was unexpectedly nil")
|
|
}
|
|
return numberOfKeys
|
|
} catch {
|
|
DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary(
|
|
userDefaults: CurrentAppContext().appUserDefaults(),
|
|
error: error
|
|
)
|
|
owsFail("error: \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public var asObjC: SDSKeyValueStoreObjC {
|
|
return SDSKeyValueStoreObjC(sdsKeyValueStore: self)
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@available(*, deprecated, message: "Did you mean removeValue(forKey:transaction:) or setCodable(optional:key:transaction)?")
|
|
public func setCodable<T: Encodable>(_ value: T?, key: String, transaction: SDSAnyWriteTransaction) throws {
|
|
try setCodable(optional: value, key: key, transaction: transaction)
|
|
}
|
|
|
|
public func setCodable<T: Encodable>(_ value: T, key: String, transaction: SDSAnyWriteTransaction) throws {
|
|
// The only difference between setCodable(optional:...) and setCodable(_...) is
|
|
// the non-optional variant has a deprecated overload to warn callers of the ambiguity.
|
|
try setCodable(optional: value, key: key, transaction: transaction)
|
|
}
|
|
|
|
public func setCodable<T: Encodable>(optional value: T, key: String, transaction: SDSAnyWriteTransaction) throws {
|
|
do {
|
|
let data = try JSONEncoder().encode(value)
|
|
setData(data, key: key, transaction: transaction)
|
|
} catch {
|
|
owsFailDebug("Failed to encode: \(error).")
|
|
throw error
|
|
}
|
|
}
|
|
|
|
public func getCodableValue<T: Decodable>(forKey key: String, transaction: SDSAnyReadTransaction) throws -> T? {
|
|
guard let data = getData(key, transaction: transaction) else {
|
|
return nil
|
|
}
|
|
do {
|
|
return try JSONDecoder().decode(T.self, from: data)
|
|
} catch {
|
|
owsFailDebug("Failed to decode: \(error).")
|
|
throw error
|
|
}
|
|
}
|
|
|
|
public func allCodableValues<T: Decodable>(transaction: SDSAnyReadTransaction) throws -> [T] {
|
|
var result = [T]()
|
|
for data in allDataValues(transaction: transaction) {
|
|
do {
|
|
result.append(try JSONDecoder().decode(T.self, from: data))
|
|
} catch {
|
|
owsFailDebug("Failed to decode: \(error).")
|
|
throw error
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// MARK: - Internal Methods
|
|
|
|
private func read<T>(_ key: String, transaction: SDSAnyReadTransaction) -> T? {
|
|
guard let rawObject = readRawObject(key, transaction: transaction) else {
|
|
return nil
|
|
}
|
|
return parseValueAs(key: key, rawObject: rawObject)
|
|
}
|
|
|
|
private func readRawObject(_ key: String, transaction: SDSAnyReadTransaction) -> Any? {
|
|
// GRDB values are serialized to data by this class.
|
|
switch transaction.readTransaction {
|
|
case .grdbRead:
|
|
guard let encoded = readData(key, transaction: transaction) else {
|
|
return nil
|
|
}
|
|
return parseArchivedValue(encoded)
|
|
}
|
|
}
|
|
|
|
private func parseArchivedValue(_ encoded: Data) -> Any? {
|
|
do {
|
|
guard let rawObject = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(encoded) else {
|
|
owsFailDebug("Could not decode value.")
|
|
return nil
|
|
}
|
|
return rawObject
|
|
} catch {
|
|
owsFailDebug("Decode failed.")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private func parseValueAs<T>(key: String, rawObject: Any?) -> T? {
|
|
guard let rawObject = rawObject else {
|
|
return nil
|
|
}
|
|
guard let object = rawObject as? T else {
|
|
owsFailDebug("Couldn't parse value with unexpected type: \(type(of: rawObject)).")
|
|
return nil
|
|
}
|
|
return object
|
|
}
|
|
|
|
private func readData(_ key: String, transaction: SDSAnyReadTransaction) -> Data? {
|
|
let collection = self.collection
|
|
|
|
switch transaction.readTransaction {
|
|
case .grdbRead(let grdbTransaction):
|
|
return SDSKeyValueStore.readData(transaction: grdbTransaction, key: key, collection: collection)
|
|
}
|
|
}
|
|
|
|
private class func readData(transaction: GRDBReadTransaction, key: String, collection: String) -> Data? {
|
|
do {
|
|
return try Data.fetchOne(transaction.database,
|
|
sql: "SELECT \(self.valueColumn.columnName) FROM \(self.table.tableName) WHERE \(self.keyColumn.columnName) = ? AND \(collectionColumn.columnName) == ?",
|
|
arguments: [key, collection])
|
|
} catch {
|
|
DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary(
|
|
userDefaults: CurrentAppContext().appUserDefaults(),
|
|
error: error
|
|
)
|
|
owsFailDebug("error: \(error)")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// TODO: Codable? NSCoding? Other serialization?
|
|
private func write(_ value: NSCoding?, forKey key: String, transaction: SDSAnyWriteTransaction) {
|
|
// GRDB values are serialized to data by this class.
|
|
switch transaction.writeTransaction {
|
|
case .grdbWrite:
|
|
if let value = value {
|
|
let encoded = try? NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: false)
|
|
writeData(encoded, forKey: key, transaction: transaction)
|
|
} else {
|
|
writeData(nil, forKey: key, transaction: transaction)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func writeData(_ data: Data?, forKey key: String, transaction: SDSAnyWriteTransaction) {
|
|
|
|
let collection = self.collection
|
|
|
|
switch transaction.writeTransaction {
|
|
case .grdbWrite(let grdbTransaction):
|
|
do {
|
|
try SDSKeyValueStore.write(transaction: grdbTransaction, key: key, collection: collection, encoded: data)
|
|
} catch {
|
|
owsFailDebug("error: \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
private class func write(transaction: GRDBWriteTransaction, key: String, collection: String, encoded: Data?) throws {
|
|
if let encoded = encoded {
|
|
// See: https://www.sqlite.org/lang_UPSERT.html
|
|
let sql = """
|
|
INSERT INTO \(table.tableName) (
|
|
\(keyColumn.columnName),
|
|
\(collectionColumn.columnName),
|
|
\(valueColumn.columnName)
|
|
) VALUES (?, ?, ?)
|
|
ON CONFLICT (
|
|
\(keyColumn.columnName),
|
|
\(collectionColumn.columnName)
|
|
) DO UPDATE
|
|
SET \(valueColumn.columnName) = ?
|
|
"""
|
|
try update(transaction: transaction, sql: sql, arguments: [ key, collection, encoded, encoded ])
|
|
} else {
|
|
// Setting to nil is a delete.
|
|
let sql = "DELETE FROM \(table.tableName) WHERE \(keyColumn.columnName) == ? AND \(collectionColumn.columnName) == ?"
|
|
try update(transaction: transaction, sql: sql, arguments: [ key, collection ])
|
|
}
|
|
}
|
|
|
|
private class func update(
|
|
transaction: GRDBWriteTransaction,
|
|
sql: String,
|
|
arguments: StatementArguments
|
|
) throws {
|
|
let statement = try transaction.database.cachedStatement(sql: sql)
|
|
try statement.setArguments(arguments)
|
|
|
|
do {
|
|
try statement.execute()
|
|
} catch {
|
|
DatabaseCorruptionState.flagDatabaseCorruptionIfNecessary(
|
|
userDefaults: CurrentAppContext().appUserDefaults(),
|
|
error: error
|
|
)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
private func allKeys(grdbTransaction: GRDBReadTransaction) -> [String] {
|
|
let sql = """
|
|
SELECT \(SDSKeyValueStore.keyColumn.columnName)
|
|
FROM \(SDSKeyValueStore.table.tableName)
|
|
WHERE \(SDSKeyValueStore.collectionColumn.columnName) == ?
|
|
"""
|
|
|
|
return grdbTransaction.database.strictRead { database in
|
|
try String.fetchAll(database, sql: sql, arguments: [collection])
|
|
}
|
|
}
|
|
}
|