diff --git a/RSDatabase/Package.swift b/RSDatabase/Package.swift index 7d5d683d2..b4b419ac2 100644 --- a/RSDatabase/Package.swift +++ b/RSDatabase/Package.swift @@ -21,7 +21,6 @@ let package = Package( .target( name: "RSDatabase", dependencies: ["RSDatabaseObjC"], - exclude: ["ODB/README.markdown"], swiftSettings: [.unsafeFlags(["-warnings-as-errors"])] ), .target( diff --git a/RSDatabase/Sources/RSDatabase/ODB/ODB.swift b/RSDatabase/Sources/RSDatabase/ODB/ODB.swift deleted file mode 100644 index 10783ef98..000000000 --- a/RSDatabase/Sources/RSDatabase/ODB/ODB.swift +++ /dev/null @@ -1,179 +0,0 @@ -// -// ODB.swift -// RSDatabase -// -// Created by Brent Simmons on 4/20/18. -// Copyright © 2018 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import RSDatabaseObjC - -// This is not thread-safe. Neither are the other ODB* objects and structs. -// It’s up to the caller to implement thread safety. - -public final class ODB: Hashable { - - public let filepath: String - - public var isClosed: Bool { - return _closed - } - - static let rootTableID = -1 - public lazy var rootTable: ODBTable? = { - ODBTable(uniqueID: ODB.rootTableID, name: ODBPath.rootTableName, parentTable: nil, isRootTable: true, odb: self) - }() - - private var _closed = false - private let queue: RSDatabaseQueue - private var odbTablesTable: ODBTablesTable? = ODBTablesTable() - private var odbValuesTable: ODBValuesTable? = ODBValuesTable() - - public init(filepath: String) { - self.filepath = filepath - let queue = RSDatabaseQueue(filepath: filepath, excludeFromBackup: false) - queue.createTables(usingStatementsSync: ODB.tableCreationStatements) - self.queue = queue - } - - /// Call when finished, to make sure no stray references can do undefined things. - /// It’s not necessary to call this on app termination. - public func close() { - guard !_closed else { - return - } - _closed = true - queue.close() - odbValuesTable = nil - odbTablesTable = nil - rootTable?.close() - rootTable = nil - } - - /// Get a reference to an ODBTable at a path, making sure it exists. - /// Returns nil if there’s a value in the path preventing the table from being made. - public func ensureTable(_ path: ODBPath) -> ODBTable? { - return path.ensureTable(with: self) - } - - /// Compact the database on disk. - public func vacuum() { - queue.vacuum() - } - - // MARK: - Hashable - - public func hash(into hasher: inout Hasher) { - hasher.combine(filepath) - } - - // MARK: - Equatable - - public static func ==(lhs: ODB, rhs: ODB) -> Bool { - return lhs.filepath == rhs.filepath - } -} - -extension ODB { - - func delete(_ object: ODBObject) -> Bool { - guard let odbValuesTable = odbValuesTable, let odbTablesTable = odbTablesTable else { - return false - } - - if let valueObject = object as? ODBValueObject { - let uniqueID = valueObject.uniqueID - queue.updateSync { (database) in - odbValuesTable.deleteObject(uniqueID: uniqueID, database: database) - } - } - else if let tableObject = object as? ODBTable { - let uniqueID = tableObject.uniqueID - queue.updateSync { (database) in - odbTablesTable.deleteTable(uniqueID: uniqueID, database: database) - } - } - return true - } - - func deleteChildren(of table: ODBTable) -> Bool { - guard let odbValuesTable = odbValuesTable, let odbTablesTable = odbTablesTable else { - return false - } - - let parentUniqueID = table.uniqueID - queue.updateSync { (database) in - odbTablesTable.deleteChildTables(parentUniqueID: parentUniqueID, database: database) - odbValuesTable.deleteChildObjects(parentUniqueID: parentUniqueID, database: database) - } - return true - } - - func insertTable(name: String, parent: ODBTable) -> ODBTable? { - guard let odbTablesTable = odbTablesTable else { - return nil - } - - var table: ODBTable? = nil - queue.fetchSync { (database) in - table = odbTablesTable.insertTable(name: name, parentTable: parent, odb: self, database: database) - } - return table! - } - - func insertValueObject(name: String, value: ODBValue, parent: ODBTable) -> ODBValueObject? { - guard let odbValuesTable = odbValuesTable else { - return nil - } - - var valueObject: ODBValueObject? = nil - queue.updateSync { (database) in - valueObject = odbValuesTable.insertValueObject(name: name, value: value, parentTable: parent, database: database) - } - return valueObject! - } - - func fetchChildren(of table: ODBTable) -> ODBDictionary { - guard let odbValuesTable = odbValuesTable, let odbTablesTable = odbTablesTable else { - return ODBDictionary() - } - - var children = ODBDictionary() - - queue.fetchSync { (database) in - - let tables = odbTablesTable.fetchSubtables(of: table, database: database, odb: self) - let valueObjects = odbValuesTable.fetchValueObjects(of: table, database: database) - - // Keys are lower-cased, since we case-insensitive lookups. - - for valueObject in valueObjects { - children[valueObject.name] = valueObject - } - - for table in tables { - children[table.name] = table - } - } - - return children - } -} - -private extension ODB { - - static let tableCreationStatements = """ - CREATE TABLE if not EXISTS odb_tables (id INTEGER PRIMARY KEY AUTOINCREMENT, parent_id INTEGER NOT NULL, name TEXT NOT NULL); - - CREATE TABLE if not EXISTS odb_values (id INTEGER PRIMARY KEY AUTOINCREMENT, odb_table_id INTEGER NOT NULL, name TEXT NOT NULL, primitive_type INTEGER NOT NULL, application_type TEXT, value BLOB); - - CREATE INDEX if not EXISTS odb_tables_parent_id_index on odb_tables (parent_id); - CREATE INDEX if not EXISTS odb_values_odb_table_id_index on odb_values (odb_table_id); - - CREATE TRIGGER if not EXISTS odb_tables_after_delete_trigger_delete_subtables after delete on odb_tables begin delete from odb_tables where parent_id = OLD.id; end; - CREATE TRIGGER if not EXISTS odb_tables_after_delete_trigger_delete_child_values after delete on odb_tables begin delete from odb_values where odb_table_id = OLD.id; end; - """ -} - - diff --git a/RSDatabase/Sources/RSDatabase/ODB/ODBObject.swift b/RSDatabase/Sources/RSDatabase/ODB/ODBObject.swift deleted file mode 100644 index 7caacdb3b..000000000 --- a/RSDatabase/Sources/RSDatabase/ODB/ODBObject.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// ODBObject.swift -// RSDatabase -// -// Created by Brent Simmons on 4/24/18. -// Copyright © 2018 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -public typealias ODBDictionary = [String: ODBObject] - -// ODBTable and ODBValueObject conform to ODBObject. - -public protocol ODBObject { - var name: String { get } - var parentTable: ODBTable? { get } -} diff --git a/RSDatabase/Sources/RSDatabase/ODB/ODBPath.swift b/RSDatabase/Sources/RSDatabase/ODB/ODBPath.swift deleted file mode 100644 index 4fa0e10f6..000000000 --- a/RSDatabase/Sources/RSDatabase/ODB/ODBPath.swift +++ /dev/null @@ -1,196 +0,0 @@ -// -// ODBPath.swift -// RSDatabase -// -// Created by Brent Simmons on 4/21/18. -// Copyright © 2018 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -/** - An ODBPath is an array like ["system", "verbs", "apps", "Xcode"]. - The first element in the array may be "root". If so, it’s ignored: "root" is implied. - An empty array or ["root"] refers to the root table. - A path does not necessarily point to something that exists. It’s like file paths or URLs. -*/ - -public struct ODBPath: Hashable { - - /// The last element in the path. May not have same capitalization as canonical name in the database. - public let name: String - - /// True if this path points to a root table. - public let isRoot: Bool - - /// Root table name. Constant. - public static let rootTableName = "root" - - /// Elements of the path minus any unneccessary initial "root" element. - public let elements: [String] - - /// ODBPath that represents the root table. - public static let root = ODBPath.path([String]()) - - /// The optional path to the parent table. Nil only if path is to the root table. - public var parentTablePath: ODBPath? { - if isRoot { - return nil - } - return ODBPath.path(Array(elements.dropLast())) - } - - private static var pathCache = [[String]: ODBPath]() - private static let pathCacheLock = NSLock() - - private init(elements: [String]) { - - let canonicalElements = ODBPath.dropLeadingRootElement(from: elements) - self.elements = canonicalElements - - if canonicalElements.count < 1 { - self.name = ODBPath.rootTableName - self.isRoot = true - } - else { - self.name = canonicalElements.last! - self.isRoot = false - } - } - - // MARK: - API - - /// Create a path. - public static func path(_ elements: [String]) -> ODBPath { - - pathCacheLock.lock() - defer { - pathCacheLock.unlock() - } - - if let cachedPath = pathCache[elements] { - return cachedPath - } - let path = ODBPath(elements: elements) - pathCache[elements] = path - return path - } - - /// Create a path by adding an element. - public func pathByAdding(_ element: String) -> ODBPath { - return ODBPath.path(elements + [element]) - } - - /// Create a path by adding an element. - public static func +(lhs: ODBPath, rhs: String) -> ODBPath { - return lhs.pathByAdding(rhs) - } - - /// Fetch the database object at this path. - public func odbObject(with odb: ODB) -> ODBObject? { - return resolvedObject(odb) - } - - /// Fetch the value at this path. - public func odbValue(with odb: ODB) -> ODBValue? { - return parentTable(with: odb)?.odbValue(name) - } - - /// Set a value for this path. Will overwrite existing value or table. - public func setODBValue(_ value: ODBValue, odb: ODB) -> Bool { - return parentTable(with: odb)?.set(value, name: name) ?? false - } - - /// Fetch the raw value at this path. - public func rawValue(with odb: ODB) -> Any? { - return parentTable(with: odb)?.rawValue(name) - } - - /// Set the raw value for this path. Will overwrite existing value or table. - @discardableResult - public func setRawValue(_ rawValue: Any, odb: ODB) -> Bool { - return parentTable(with: odb)?.set(rawValue, name: name) ?? false - } - - /// Delete value or table at this path. - public func delete(from odb: ODB) -> Bool { - return parentTable(with: odb)?.delete(name: name) ?? false - } - - /// Fetch the table at this path. - public func table(with odb: ODB) -> ODBTable? { - return odbObject(with: odb) as? ODBTable - } - - /// Fetch the parent table. Nil if this is the root table. - public func parentTable(with odb: ODB) -> ODBTable? { - return parentTablePath?.table(with: odb) - } - - /// Creates a table — will delete existing table. - public func createTable(with odb: ODB) -> ODBTable? { - return parentTable(with: odb)?.addSubtable(name: name) - } - - /// Return the table for the final item in the path. - /// Won’t delete anything. - @discardableResult - public func ensureTable(with odb: ODB) -> ODBTable? { - - if isRoot { - return odb.rootTable - } - - if let existingObject = odbObject(with: odb) { - if let existingTable = existingObject as? ODBTable { - return existingTable - } - return nil // It must be a value: don’t overwrite. - } - - if let parentTable = parentTablePath!.ensureTable(with: odb) { - return parentTable.addSubtable(name: name) - } - return nil - } - - // MARK: - Hashable - - public func hash(into hasher: inout Hasher) { - hasher.combine(elements) - } - - // MARK: - Equatable - - public static func ==(lhs: ODBPath, rhs: ODBPath) -> Bool { - return lhs.elements == rhs.elements - } -} - -// MARK: - Private - -private extension ODBPath { - - func resolvedObject(_ odb: ODB) -> ODBObject? { - if isRoot { - return odb.rootTable - } - guard let table = parentTable(with: odb) else { - return nil - } - return table[name] - } - - static func dropLeadingRootElement(from elements: [String]) -> [String] { - if elements.count < 1 { - return elements - } - - let firstElement = elements.first! - if firstElement == ODBPath.rootTableName { - return Array(elements.dropFirst()) - } - - return elements - } -} diff --git a/RSDatabase/Sources/RSDatabase/ODB/ODBRawValueTable.swift b/RSDatabase/Sources/RSDatabase/ODB/ODBRawValueTable.swift deleted file mode 100644 index 90d8455c0..000000000 --- a/RSDatabase/Sources/RSDatabase/ODB/ODBRawValueTable.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// ODBRawValueTable.swift -// RSDatabase -// -// Created by Brent Simmons on 9/13/18. -// Copyright © 2018 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -// Use this when you’re just getting/setting raw values from a table. - -public final class ODBRawValueTable { - - let table: ODBTable - - init(table: ODBTable) { - self.table = table - } - - public subscript(_ name: String) -> Any? { - get { - return table.rawValue(name) - } - set { - if let rawValue = newValue { - table.set(rawValue, name: name) - } - else { - table.delete(name: name) - } - } - } - - public func string(for name: String) -> String? { - return self[name] as? String - } - - public func setString(_ stringValue: String?, for name: String) { - self[name] = stringValue - } -} diff --git a/RSDatabase/Sources/RSDatabase/ODB/ODBTable.swift b/RSDatabase/Sources/RSDatabase/ODB/ODBTable.swift deleted file mode 100644 index 9ef4b4420..000000000 --- a/RSDatabase/Sources/RSDatabase/ODB/ODBTable.swift +++ /dev/null @@ -1,170 +0,0 @@ -// -// ODBTable.swift -// RSDatabase -// -// Created by Brent Simmons on 4/21/18. -// Copyright © 2018 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -public final class ODBTable: ODBObject, Hashable { - - let uniqueID: Int - public let isRootTable: Bool - public let odb: ODB - public let parentTable: ODBTable? - public let name: String - public let path: ODBPath - private var _children: ODBDictionary? - - public var children: ODBDictionary { - get { - if _children == nil { - _children = odb.fetchChildren(of: self) - } - return _children! - } - set { - _children = newValue - } - } - - public lazy var rawValueTable = { - return ODBRawValueTable(table: self) - }() - - init(uniqueID: Int, name: String, parentTable: ODBTable?, isRootTable: Bool, odb: ODB) { - self.uniqueID = uniqueID - self.name = name - self.parentTable = parentTable - self.isRootTable = isRootTable - self.path = isRootTable ? ODBPath.root : parentTable!.path + name - self.odb = odb - } - - /// Get the ODBObject for the given name. - public subscript(_ name: String) -> ODBObject? { - return children[name] - } - - /// Fetch the ODBValue for the given name. - public func odbValue(_ name: String) -> ODBValue? { - return (self[name] as? ODBValueObject)?.value - } - - /// Set the ODBValue for the given name. - public func set(_ odbValue: ODBValue, name: String) -> Bool { - // Don’t bother if key/value pair already exists. - // If child with same name exists, delete it. - - let existingObject = self[name] - if let existingValue = existingObject as? ODBValueObject, existingValue.value == odbValue { - return true - } - - guard let valueObject = odb.insertValueObject(name: name, value: odbValue, parent: self) else { - return false - } - if let existingObject = existingObject { - delete(existingObject) - } - addChild(name: name, object: valueObject) - return true - } - - /// Fetch the raw value for the given name. - public func rawValue(_ name: String) -> Any? { - return (self[name] as? ODBValueObject)?.value.rawValue - } - - /// Create a value object and set it for the given name. - @discardableResult - public func set(_ rawValue: Any, name: String) -> Bool { - guard let odbValue = ODBValue(rawValue: rawValue) else { - return false - } - return set(odbValue, name: name) - } - - /// Delete all children — empty the table. - public func deleteChildren() -> Bool { - guard odb.deleteChildren(of: self) else { - return false - } - _children = ODBDictionary() - return true - } - - /// Delete a child object. - @discardableResult - public func delete(_ object: ODBObject) -> Bool { - return odb.delete(object) - } - - /// Delete a child with the given name. - @discardableResult - public func delete(name: String) -> Bool { - guard let child = self[name] else { - return false - } - return delete(child) - } - - /// Fetch the subtable with the given name. - public func subtable(name: String) -> ODBTable? { - return self[name] as? ODBTable - } - - /// Add a subtable with the given name. Overwrites previous child with that name. - public func addSubtable(name: String) -> ODBTable? { - let existingObject = self[name] - guard let subTable = odb.insertTable(name: name, parent: self) else { - return nil - } - if let existingObject = existingObject { - delete(existingObject) - } - addChild(name: name, object: subTable) - return subTable - } - - // MARK: - Hashable - - public func hash(into hasher: inout Hasher) { - hasher.combine(uniqueID) - hasher.combine(odb) - } - - // MARK: - Equatable - - public static func ==(lhs: ODBTable, rhs: ODBTable) -> Bool { - return lhs.uniqueID == rhs.uniqueID && lhs.odb == rhs.odb - } -} - -extension ODBTable { - - func close() { - // Called from ODB when database is closing. - if let rawChildren = _children { - rawChildren.forEach { (key: String, value: ODBObject) in - if let table = value as? ODBTable { - table.close() - } - } - } - _children = nil - } -} - -private extension ODBTable { - - func addChild(name: String, object: ODBObject) { - children[name] = object - } - - func ensureChildren() { - let _ = children - } -} diff --git a/RSDatabase/Sources/RSDatabase/ODB/ODBTablesTable.swift b/RSDatabase/Sources/RSDatabase/ODB/ODBTablesTable.swift deleted file mode 100644 index 24d4764c9..000000000 --- a/RSDatabase/Sources/RSDatabase/ODB/ODBTablesTable.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// ODBTablesTable.swift -// RSDatabase -// -// Created by Brent Simmons on 4/20/18. -// Copyright © 2018 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import RSDatabaseObjC - -final class ODBTablesTable: DatabaseTable { - - let name = "odb_tables" - - private struct Key { - static let uniqueID = "id" - static let parentID = "parent_id" - static let name = "name" - } - - func fetchSubtables(of table: ODBTable, database: FMDatabase, odb: ODB) -> Set { - guard let rs: FMResultSet = database.executeQuery("select * from odb_tables where parent_id = ?", withArgumentsIn: [table.uniqueID]) else { - return Set() - } - return rs.mapToSet{ createTable(with: $0, parentTable: table, odb: odb) } - } - - func insertTable(name: String, parentTable: ODBTable, odb: ODB, database: FMDatabase) -> ODBTable { - let d: DatabaseDictionary = [Key.parentID: parentTable.uniqueID, Key.name: name] - insertRow(d, insertType: .normal, in: database) - let uniqueID = Int(database.lastInsertRowId()) - return ODBTable(uniqueID: uniqueID, name: name, parentTable: parentTable, isRootTable: false, odb: odb) - } - - func deleteTable(uniqueID: Int, database: FMDatabase) { - database.rs_deleteRowsWhereKey(Key.uniqueID, equalsValue: uniqueID, tableName: name) - } - - func deleteChildTables(parentUniqueID: Int, database: FMDatabase) { - database.rs_deleteRowsWhereKey(Key.parentID, equalsValue: parentUniqueID, tableName: name) - } -} - -private extension ODBTablesTable { - - func createTable(with row: FMResultSet, parentTable: ODBTable, odb: ODB) -> ODBTable? { - - guard let name = row.string(forColumn: Key.name) else { - return nil - } - let uniqueID = Int(row.longLongInt(forColumn: Key.uniqueID)) - - return ODBTable(uniqueID: uniqueID, name: name, parentTable: parentTable, isRootTable: false, odb: odb) - } -} diff --git a/RSDatabase/Sources/RSDatabase/ODB/ODBValue.swift b/RSDatabase/Sources/RSDatabase/ODB/ODBValue.swift deleted file mode 100644 index 3e6fd253b..000000000 --- a/RSDatabase/Sources/RSDatabase/ODB/ODBValue.swift +++ /dev/null @@ -1,164 +0,0 @@ -// -// ODBValue.swift -// RSDatabase -// -// Created by Brent Simmons on 4/24/18. -// Copyright © 2018 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -public struct ODBValue: Hashable { - - // Values are arbitrary but must not change: they’re stored in the database. - public enum PrimitiveType: Int { - case boolean=8 - case integer=16 - case double=32 - case date=64 - case string=128 - case data=256 - } - - public let rawValue: Any - public let primitiveType: PrimitiveType - public let applicationType: String? // Application-defined - - public init(rawValue: Any, primitiveType: PrimitiveType, applicationType: String?) { - self.rawValue = rawValue - self.primitiveType = primitiveType - self.applicationType = applicationType - } - - public init(rawValue: Any, primitiveType: PrimitiveType) { - self.init(rawValue: rawValue, primitiveType: primitiveType, applicationType: nil) - } - - public init?(rawValue: Any) { - guard let primitiveType = ODBValue.primitiveTypeForRawValue(rawValue) else { - return nil - } - self.init(rawValue: rawValue, primitiveType: primitiveType) - } - - // MARK: - Hashable - - public func hash(into hasher: inout Hasher) { - if let booleanValue = rawValue as? Bool { - hasher.combine(booleanValue) - } - else if let integerValue = rawValue as? Int { - hasher.combine(integerValue) - } - else if let doubleValue = rawValue as? Double { - hasher.combine(doubleValue) - } - else if let stringValue = rawValue as? String { - hasher.combine(stringValue) - } - else if let dataValue = rawValue as? Data { - hasher.combine(dataValue) - } - else if let dateValue = rawValue as? Date { - hasher.combine(dateValue) - } - - hasher.combine(primitiveType) - hasher.combine(applicationType) - } - - // MARK: - Equatable - - public static func ==(lhs: ODBValue, rhs: ODBValue) -> Bool { - - if lhs.primitiveType != rhs.primitiveType || lhs.applicationType != rhs.applicationType { - return false - } - - switch lhs.primitiveType { - case .boolean: - return compareBooleans(lhs.rawValue, rhs.rawValue) - case .integer: - return compareIntegers(lhs.rawValue, rhs.rawValue) - case .double: - return compareDoubles(lhs.rawValue, rhs.rawValue) - case .string: - return compareStrings(lhs.rawValue, rhs.rawValue) - case .data: - return compareData(lhs.rawValue, rhs.rawValue) - case .date: - return compareDates(lhs.rawValue, rhs.rawValue) - } - } -} - -private extension ODBValue { - - static func compareBooleans(_ left: Any, _ right: Any) -> Bool { - - guard let left = left as? Bool, let right = right as? Bool else { - return false - } - return left == right - } - - static func compareIntegers(_ left: Any, _ right: Any) -> Bool { - - guard let left = left as? Int, let right = right as? Int else { - return false - } - return left == right - } - - static func compareDoubles(_ left: Any, _ right: Any) -> Bool { - - guard let left = left as? Double, let right = right as? Double else { - return false - } - return left == right - } - - static func compareStrings(_ left: Any, _ right: Any) -> Bool { - - guard let left = left as? String, let right = right as? String else { - return false - } - return left == right - } - - static func compareData(_ left: Any, _ right: Any) -> Bool { - - guard let left = left as? Data, let right = right as? Data else { - return false - } - return left == right - } - - static func compareDates(_ left: Any, _ right: Any) -> Bool { - - guard let left = left as? Date, let right = right as? Date else { - return false - } - return left == right - } - - static func primitiveTypeForRawValue(_ rawValue: Any) -> ODBValue.PrimitiveType? { - - switch rawValue { - case is Bool: - return .boolean - case is Int: - return .integer - case is Double: - return .double - case is Date: - return .date - case is String: - return .string - case is Data: - return .data - default: - return nil - } - } -} diff --git a/RSDatabase/Sources/RSDatabase/ODB/ODBValueObject.swift b/RSDatabase/Sources/RSDatabase/ODB/ODBValueObject.swift deleted file mode 100644 index a622197ed..000000000 --- a/RSDatabase/Sources/RSDatabase/ODB/ODBValueObject.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// ODBValueObject.swift -// RSDatabase -// -// Created by Brent Simmons on 4/21/18. -// Copyright © 2018 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -public struct ODBValueObject: ODBObject, Hashable { - - let uniqueID: Int - public let value: ODBValue - - // ODBObject protocol properties - public let name: String - public let parentTable: ODBTable? - - init(uniqueID: Int, parentTable: ODBTable, name: String, value: ODBValue) { - - self.uniqueID = uniqueID - self.parentTable = parentTable - self.name = name - self.value = value - } - - // MARK: - Hashable - - public func hash(into hasher: inout Hasher) { - hasher.combine(uniqueID) - hasher.combine(value) - } - - // MARK: - Equatable - - public static func ==(lhs: ODBValueObject, rhs: ODBValueObject) -> Bool { - return lhs.uniqueID == rhs.uniqueID && lhs.value == rhs.value - } -} diff --git a/RSDatabase/Sources/RSDatabase/ODB/ODBValuesTable.swift b/RSDatabase/Sources/RSDatabase/ODB/ODBValuesTable.swift deleted file mode 100644 index c86356a3f..000000000 --- a/RSDatabase/Sources/RSDatabase/ODB/ODBValuesTable.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// ODBValuesTable.swift -// RSDatabase -// -// Created by Brent Simmons on 4/20/18. -// Copyright © 2018 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import RSDatabaseObjC - -final class ODBValuesTable: DatabaseTable { - - let name = "odb_values" - - private struct Key { - static let uniqueID = "id" - static let parentID = "odb_table_id" - static let name = "name" - static let primitiveType = "primitive_type" - static let applicationType = "application_type" - static let value = "value" - } - - func fetchValueObjects(of table: ODBTable, database: FMDatabase) -> Set { - guard let rs = database.rs_selectRowsWhereKey(Key.parentID, equalsValue: table.uniqueID, tableName: name) else { - return Set() - } - return rs.mapToSet{ valueObject(with: $0, parentTable: table) } - } - - func deleteObject(uniqueID: Int, database: FMDatabase) { - database.rs_deleteRowsWhereKey(Key.uniqueID, equalsValue: uniqueID, tableName: name) - } - - func deleteChildObjects(parentUniqueID: Int, database: FMDatabase) { - database.rs_deleteRowsWhereKey(Key.parentID, equalsValue: parentUniqueID, tableName: name) - } - - func insertValueObject(name: String, value: ODBValue, parentTable: ODBTable, database: FMDatabase) -> ODBValueObject { - - var d: DatabaseDictionary = [Key.parentID: parentTable.uniqueID, Key.name: name, Key.primitiveType: value.primitiveType.rawValue, Key.value: value.rawValue] - if let applicationType = value.applicationType { - d[Key.applicationType] = applicationType - } - - insertRow(d, insertType: .normal, in: database) - let uniqueID = Int(database.lastInsertRowId()) - return ODBValueObject(uniqueID: uniqueID, parentTable: parentTable, name: name, value: value) - } -} - -private extension ODBValuesTable { - - func valueObject(with row: FMResultSet, parentTable: ODBTable) -> ODBValueObject? { - - guard let value = value(with: row) else { - return nil - } - guard let name = row.string(forColumn: Key.name) else { - return nil - } - let uniqueID = Int(row.longLongInt(forColumn: Key.uniqueID)) - - return ODBValueObject(uniqueID: uniqueID, parentTable: parentTable, name: name, value: value) - } - - func value(with row: FMResultSet) -> ODBValue? { - - guard let primitiveType = ODBValue.PrimitiveType(rawValue: Int(row.longLongInt(forColumn: Key.primitiveType))) else { - return nil - } - var value: Any? = nil - - switch primitiveType { - case .boolean: - value = row.bool(forColumn: Key.value) - case .integer: - value = Int(row.longLongInt(forColumn: Key.value)) - case .double: - value = row.double(forColumn: Key.value) - case .string: - value = row.string(forColumn: Key.value) - case .data: - value = row.data(forColumn: Key.value) - case .date: - value = row.date(forColumn: Key.value) - } - - guard let fetchedValue = value else { - return nil - } - - let applicationType = row.string(forColumn: Key.applicationType) - return ODBValue(rawValue: fetchedValue, primitiveType: primitiveType, applicationType: applicationType) - } -} diff --git a/RSDatabase/Sources/RSDatabase/ODB/README.markdown b/RSDatabase/Sources/RSDatabase/ODB/README.markdown deleted file mode 100644 index 4c7e8b4d6..000000000 --- a/RSDatabase/Sources/RSDatabase/ODB/README.markdown +++ /dev/null @@ -1,149 +0,0 @@ -# ODB - -**NOTE**: This all has been excluded from building. It’s a work in progress, not ready for use. - -ODB stands for Object Database. - -“Object” doesn’t mean object in the object-oriented programming sense — it just means *thing*. - -Think of the ODB as a nested Dictionary that’s *persistent*. It’s schema-less. Tables (which are like dictionaries) can contain other tables. It’s all key-value pairs. - -The inspiration for this comes from [UserLand Frontier](http://frontier.userland.com/), which featured an ODB which made persistence for scripts easy. - -You could write a script like `user.personalInfo.name = "Bull Mancuso"` — and, inside the `personalInfo` table, which is inside the `user` table, it would create or set a key/value pair: `name` would be the key, and `Bull Mancuso` would be the value. - -Looking up the value later was as simple as referring to `user.personalInfo.name`. - -This ODB implementation does *not* provide that scripting language. It also does not provide a user interface for the database (Frontier did). It provides just the lowest level: the actual storage and a Swift API for getting, setting, and deleting tables and values. - -It’s built on top of SQLite. It may sound weird to build an ODB on top of a SQL database — but SQLite is amazingly robust and fast, and it’s the hammer I know best. - -My hunch is that lots of apps could benefit from this kind of storage. It was the *only* kind I used for seven years in my early career, and we wrote lots of powerful software using Frontier’s ODB. (Blogging, RSS, podcasting, web services over HTTP, OPML — all these were invented or popularized or fleshed-out using Frontier and its ODB. Not that I take personal credit: I was an employee of UserLand Software, and the vision was Dave Winer’s.) - -## How to use it - -### Create an ODB - -`let odb = ODB(filepath: somePath)` creates a new ODB for that path. If there’s an existing database on disk, it uses that one. Otherwise it creates a new one. - -### Ensuring that a table exists - -Let’s say you’re writing an RSS reader, and you want to make sure there’s a table at `RSS.feeds.[feedID]`. Given feedID and odb: - - let pathElements = ["RSS", "feeds", feedID] - let path = ODBPath(elements: pathElements, odb: odb) - ODB.perform { - let _ = path.ensureTable() - } - -The `ensureTable` function returns an `ODBTable`. It makes sure that the entire path exists. The only way `ensureTable` would return nil is if something in the path exists and it’s a value instead of a table. `ensureTable` never deletes anything. - -There is a similar `createTable` function that deletes any existing table at that path and then creates a new table. It does *not* ensure that the entire path exists, and it returns nil if the necessary ancestor tables don’t exist. - -Operations referencing `ODBTable` and `ODBValueObject` must be enclosed in an `ODB.perform` block. This is for thread safety. If you don’t use an `ODB.perform` block, it will crash deliberately with a `preconditionFailure`. - -You should *not* hold a reference to an `ODBTable`, `ODBValueObject`, or `ODBObject` outside of the `perform` block. You *can* hold a reference to an `ODBPath` and to an `ODBValue`. - -An `ODBObject` is either an `ODBTable` or `ODBValueObject`: it’s a protocol. - -### Setting a value - -Let’s say the user of your RSS reader can edit the name of a feed, and you want to store the edited name in the database. The key for the value is `editedName`. Assume that you’ve already used `ensureTable` as above. - - let path = ODBPath(elements: ["RSS", "feeds", feedID, "editedName"], odb: odb) - let value = ODBValue(value: name, primitiveType: .string, applicationType: nil) - ODB.perform { - path.setValue(value) - } - -If `editedName` exists, it gets replaced. If it doesn’t exist, then it gets created. - -(Yes, this would be so much easier in a scripting language. You’d just write: `RSS.feeds.[feedID].editedName = name` — the above is the equivalent of that.) - -See `ODBValue` for the different types of values that can be stored. Each value must be one of a few primitive types — string, date, data, etc. — but each value can optionally have its own `applicationType`. For instance, you might store OPML text as a string, but then give it an `applicationType` of `"OPML"`, so that your application knows what it is and can encode/decode properly. This lets you store values of any arbitrary complexity. - -In general, it’s good practice to use that ability sparingly. When you can break things down into simple primitive types, that’s best. Treating an entire table, with multiple stored values, as a unit is often the way to go. But not always. - -### Getting a value - -Let’s say you want to get back the edited name of the feed. You’d create the path the same way as before. And then: - - var nameValue: ODBValue? = nil - ODB.perform { - nameValue = path.value - } - let name = nameValue? as? String - -The above is written to demonstrate that you can refer to `ODBValue` outside of a `perform` call. It’s an immutable struct with no connection to the database. But in reality you’d probably write the above code more like this: - - var name: String? - ODB.perform { - name = path.value? as? String - } - -It’s totally a-okay to use Swift’s built-in types this way instead of checking the ODBValue’s `primitiveType`. The primitive types map directly to `Bool`, `Int`, `Double`, `Date`, `String`, and `Data`. - -### Deleting a table or value - -Say the user undoes editing the feed’s name, and now you want to delete `RSS.feeds.[feedID].editedName` — given the path, you’d do this: - - ODB.perform { - path.delete() - } - -This works on both tables and values. You can also call `delete()` directly on an `ODBTable`, `ODBValueObject`, or `ODBObject`. - -### ODBObject - -Some functions take or return an `ODBObject`. This is a protocol — the object is either an `ODBTable` or `ODBValueObject`. - -There is useful API to be aware of in ODBObject.swift. (But, again, an `ODBObject` reference is valid only with an `ODB.perform` call.) - -### ODBTable - -You can do some of the same things you can do with an `ODBPath`. You can also get the entire dictionary of `children`, look up any child object, delete all children, add child objects, and more. - -### ODBValueObject - -You won’t use this directly all that often. It wraps an `ODBValue`, which you’ll use way more often. The useful API for `ODBValueObject` is almost entirely in `ODBObject`. - -## Notes - -### The root table - -The one table you can’t delete is the root table — every ODB has a top-level table named `root`. You don’t usually specify `root` as the first part of a path, but you could. It’s implied. - -A path like `["RSS", "feeds"]` is precisely the same as `["root", "RSS", "feeds"]` — they’re interchangeable paths. - -### Case-sensitivity - -Frontier’s object database was case-insensitive: you could refer to the "feeds" table as the "FEeDs" table — it would be the same thing. - -While I don’t know this for sure, I assume this was because the Mac’s file system is also case-insensitive. This was considered one of the user-friendly things about Macs. - -We’re preserved this: this ODB is also case-insensitive. When comparing two keys it always uses the English locale, so that results are predictable no matter what the machine’s locale actually is. This is something to be aware of. - -### Caching and Performance - -The database is cached in memory as it is used. A table’s children are not read into memory until referenced. - -For objects already in memory, reads are fast since there’s no need to query the SQLite database. - -If this caching becomes a problem in production use — if it tends to use too much memory — we’ll make it smarter. - -### Thread safety - -Why is it okay to create and refer to `ODBPath` and `ODBValue` objects outside of an `ODB.perform` call, while it’s not okay with `ODBObject`, `ODBTable`, and `ODBValueObject`? - -Because: - -`ODBPath` represents a *query* rather than a direct reference. Each time you resolve the object it points to, it recalculates. You can create paths to things that don’t exist. The database can change while you hold an `ODBPath` reference, and that’s okay: it’s by design. Just know that you might get back something different every time you refer to `path.object`, `path.value`, and `path.table`. - -`ODBValue` is an immutable struct with no connection to the database. Once you get one, it doesn’t change, even if the database object it came from changes. (In general these will be short-lived — you just use them for wrapping and unwrapping your app’s data.) - -On the other hand, `ODBObject`, `ODBTable`, and `ODBValueObject` are direct references to the database. To prevent conflicts and maintain the structure of the database properly, it’s necessary to use a lock when working with these — that’s what `ODB.perform` does. - -Say you have a particular table that your app uses a lot. It would seem natural to want to keep a reference to that particular `ODBTable`. Instead, create and keep a reference to an `ODBPath` and refer to `path.table` inside an `ODB.perform` block when you need the table. - - -