Remove unused RSDatabase/ODB.

This commit is contained in:
Brent Simmons 2024-11-08 22:02:15 -08:00
parent 7e2c668974
commit 7751bff896
11 changed files with 0 additions and 1112 deletions

View File

@ -21,7 +21,6 @@ let package = Package(
.target(
name: "RSDatabase",
dependencies: ["RSDatabaseObjC"],
exclude: ["ODB/README.markdown"],
swiftSettings: [.unsafeFlags(["-warnings-as-errors"])]
),
.target(

View File

@ -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.
// Its 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.
/// Its 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 theres 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;
"""
}

View File

@ -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 }
}

View File

@ -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, its ignored: "root" is implied.
An empty array or ["root"] refers to the root table.
A path does not necessarily point to something that exists. Its 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.
/// Wont 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: dont 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
}
}

View File

@ -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 youre 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
}
}

View File

@ -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 {
// Dont 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
}
}

View File

@ -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)
}
}

View File

@ -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: theyre 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
}
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -1,149 +0,0 @@
# ODB
**NOTE**: This all has been excluded from building. Its a work in progress, not ready for use.
ODB stands for Object Database.
“Object” doesnt mean object in the object-oriented programming sense — it just means *thing*.
Think of the ODB as a nested Dictionary thats *persistent*. Its schema-less. Tables (which are like dictionaries) can contain other tables. Its 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.
Its 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 its 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 Frontiers 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 Winers.)
## How to use it
### Create an ODB
`let odb = ODB(filepath: somePath)` creates a new ODB for that path. If theres an existing database on disk, it uses that one. Otherwise it creates a new one.
### Ensuring that a table exists
Lets say youre writing an RSS reader, and you want to make sure theres 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 its 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 dont exist.
Operations referencing `ODBTable` and `ODBValueObject` must be enclosed in an `ODB.perform` block. This is for thread safety. If you dont 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`: its a protocol.
### Setting a value
Lets 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 youve 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 doesnt exist, then it gets created.
(Yes, this would be so much easier in a scripting language. Youd 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, its good practice to use that ability sparingly. When you can break things down into simple primitive types, thats best. Treating an entire table, with multiple stored values, as a unit is often the way to go. But not always.
### Getting a value
Lets say you want to get back the edited name of the feed. Youd 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. Its an immutable struct with no connection to the database. But in reality youd probably write the above code more like this:
var name: String?
ODB.perform {
name = path.value? as? String
}
Its totally a-okay to use Swifts built-in types this way instead of checking the ODBValues `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 feeds name, and now you want to delete `RSS.feeds.[feedID].editedName` — given the path, youd 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 wont use this directly all that often. It wraps an `ODBValue`, which youll use way more often. The useful API for `ODBValueObject` is almost entirely in `ODBObject`.
## Notes
### The root table
The one table you cant delete is the root table — every ODB has a top-level table named `root`. You dont usually specify `root` as the first part of a path, but you could. Its implied.
A path like `["RSS", "feeds"]` is precisely the same as `["root", "RSS", "feeds"]` — theyre interchangeable paths.
### Case-sensitivity
Frontiers object database was case-insensitive: you could refer to the "feeds" table as the "FEeDs" table — it would be the same thing.
While I dont know this for sure, I assume this was because the Macs file system is also case-insensitive. This was considered one of the user-friendly things about Macs.
Were 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 machines locale actually is. This is something to be aware of.
### Caching and Performance
The database is cached in memory as it is used. A tables children are not read into memory until referenced.
For objects already in memory, reads are fast since theres no need to query the SQLite database.
If this caching becomes a problem in production use — if it tends to use too much memory — well 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 its 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 dont exist. The database can change while you hold an `ODBPath` reference, and thats okay: its 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 doesnt 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 apps 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, its necessary to use a lock when working with these — thats 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.