Remove unused RSDatabase/ODB.
This commit is contained in:
parent
7e2c668974
commit
7751bff896
|
@ -21,7 +21,6 @@ let package = Package(
|
|||
.target(
|
||||
name: "RSDatabase",
|
||||
dependencies: ["RSDatabaseObjC"],
|
||||
exclude: ["ODB/README.markdown"],
|
||||
swiftSettings: [.unsafeFlags(["-warnings-as-errors"])]
|
||||
),
|
||||
.target(
|
||||
|
|
|
@ -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;
|
||||
"""
|
||||
}
|
||||
|
||||
|
|
@ -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 }
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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<ODBTable> {
|
||||
guard let rs: FMResultSet = database.executeQuery("select * from odb_tables where parent_id = ?", withArgumentsIn: [table.uniqueID]) else {
|
||||
return Set<ODBTable>()
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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<ODBValueObject> {
|
||||
guard let rs = database.rs_selectRowsWhereKey(Key.parentID, equalsValue: table.uniqueID, tableName: name) else {
|
||||
return Set<ODBValueObject>()
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue