├── .gitignore ├── Package.resolved ├── Package.swift ├── Sources ├── ORM │ ├── Engines │ │ ├── Core Data │ │ │ ├── CoreDataEngine.swift │ │ │ └── NSManagedObjectModel+Construction.swift │ │ ├── SQLite │ │ │ ├── SQLite+Schema.swift │ │ │ ├── SQLiteEngine.swift │ │ │ ├── SQLiteHandle.swift │ │ │ └── SQLiteTable.swift │ │ ├── StorageEngine.swift │ │ └── StorageError.swift │ ├── Internals │ │ ├── Bimap.swift │ │ ├── ForEachField.swift │ │ └── TypeDetectors.swift │ ├── Schema │ │ ├── Schema.swift │ │ └── SchemaError.swift │ └── StoredTypes │ │ ├── StoredField.swift │ │ ├── StoredType.swift │ │ └── TypeDescriptions │ │ ├── CompositeTypeDescription.swift │ │ ├── MultiValueTypeDescription.swift │ │ ├── PrimitiveTypeDescription.swift │ │ └── StoredTypeDescription.swift └── debug │ ├── File.swift │ └── main.swift └── Tests └── ORMTests ├── CoreDataEngineTests.swift ├── SQLiteEngineTests.swift └── StorageDescriptionTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | .swiftpm 10 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "6955c719c1ba62fcd42298bc071fe7522fe29ac92ebd9f76ba8e7e50a1526a52", 3 | "pins" : [ 4 | { 5 | "identity" : "sqlite.swift", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/stephencelis/SQLite.swift.git", 8 | "state" : { 9 | "revision" : "a95fc6df17d108bd99210db5e8a9bac90fe984b8", 10 | "version" : "0.15.3" 11 | } 12 | }, 13 | { 14 | "identity" : "sqlitesyntax", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/davedelong/SQLiteSyntax.git", 17 | "state" : { 18 | "branch" : "main", 19 | "revision" : "88300df7a820f2291bc8955c9b14f83de829db53" 20 | } 21 | } 22 | ], 23 | "version" : 3 24 | } 25 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.10 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ORM", 8 | platforms: [.macOS(.v14)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, making them visible to other packages. 11 | .library(name: "ORM", targets: ["ORM"]), 12 | .executable(name: "debug", targets: ["debug"]) 13 | ], 14 | dependencies: [ 15 | .package(url: "https://github.com/davedelong/SQLiteSyntax.git", branch: "main"), 16 | .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.3"), 17 | // .package(url: "https://github.com/groue/GRDB.swift.git", from: "7.4.1") 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package, defining a module or a test suite. 21 | // Targets can depend on other targets in this package and products from dependencies. 22 | .executableTarget(name: "debug", dependencies: ["ORM"]), 23 | .target(name: "ORM", dependencies: [ 24 | .product(name: "SQLiteSyntax", package: "SQLiteSyntax"), 25 | .product(name: "SQLite", package: "SQLite.swift"), 26 | // .product(name: "GRDB", package: "GRDB.swift") 27 | ]), 28 | .testTarget(name: "ORMTests", dependencies: ["ORM"]), 29 | ] 30 | ) 31 | -------------------------------------------------------------------------------- /Sources/ORM/Engines/Core Data/CoreDataEngine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataEngine.swift 3 | // ORM 4 | // 5 | // Created by Dave DeLong on 3/16/25. 6 | // 7 | 8 | #if canImport(CoreData) 9 | import CoreData 10 | 11 | public actor CoreDataEngine: StorageEngine { 12 | 13 | internal let model: NSManagedObjectModel 14 | internal let coordinator: NSPersistentStoreCoordinator 15 | 16 | public init(schema: Schema, at url: URL) async throws(StorageError) { 17 | self.model = try NSManagedObjectModel(compositeTypes: schema.compositeTypes) 18 | 19 | let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model) 20 | do { 21 | try await withUnsafeThrowingContinuation { (c: UnsafeContinuation) in 22 | let desc = NSPersistentStoreDescription(url: url) 23 | desc.type = NSSQLiteStoreType 24 | desc.shouldAddStoreAsynchronously = true 25 | desc.shouldMigrateStoreAutomatically = true 26 | coordinator.addPersistentStore(with: desc, completionHandler: { _, err in 27 | if let err { 28 | c.resume(throwing: err) 29 | } else { 30 | c.resume(returning: ()) 31 | } 32 | }) 33 | } 34 | } catch { 35 | 36 | } 37 | 38 | self.coordinator = coordinator 39 | } 40 | 41 | public func save(_ value: any StoredType) throws(StorageError) { 42 | 43 | } 44 | 45 | } 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /Sources/ORM/Engines/Core Data/NSManagedObjectModel+Construction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // ORM 4 | // 5 | // Created by Dave DeLong on 3/18/25. 6 | // 7 | 8 | #if canImport(CoreData) 9 | 10 | import Foundation 11 | import CoreData 12 | 13 | extension NSManagedObjectModel { 14 | private typealias NSAD = NSAttributeDescription 15 | private typealias NSRD = NSRelationshipDescription 16 | 17 | convenience init(compositeTypes: Array) throws(StorageError) { 18 | self.init() 19 | 20 | for compositeType in compositeTypes { 21 | try buildEntity(from: compositeType) 22 | } 23 | 24 | // Relationships can't be created until all the base entities exist 25 | // create them now (which may involve creating more entities) 26 | for compositeType in compositeTypes { 27 | try buildRelationships(from: compositeType) 28 | } 29 | 30 | } 31 | 32 | fileprivate func entity(named name: String) -> NSEntityDescription { 33 | guard let e = entitiesByName[name] else { 34 | fatalError("Entity '\(name)' is missing!") 35 | } 36 | 37 | return e 38 | } 39 | 40 | private func buildEntity(from description: CompositeTypeDescription) throws(StorageError) { 41 | let e = NSEntityDescription() 42 | e.name = description.name 43 | e.properties = description.fields.compactMap { NSAD.attribute(from: $0) } 44 | 45 | if description.isIdentifiable { 46 | e.uniquenessConstraints = [["id"]] 47 | } 48 | 49 | self.entities.append(e) 50 | } 51 | 52 | private func buildRelationships(from description: CompositeTypeDescription) throws(StorageError) { 53 | 54 | for field in description.fields { 55 | if field.description is CompositeTypeDescription { 56 | try NSRD.singleValueRelationship(from: description, field: field, in: self) 57 | } else if let opt = field.description as? OptionalTypeDescription, opt.wrappedType is CompositeTypeDescription { 58 | try NSRD.singleValueRelationship(from: description, field: field, in: self) 59 | } else if field.description is MultiValueTypeDescription { 60 | try NSRD.multiValueRelationship(from: description, field: field, in: self) 61 | } 62 | } 63 | 64 | } 65 | 66 | } 67 | 68 | extension NSAttributeDescription { 69 | 70 | internal static func attribute(from field: StoredField) -> NSAttributeDescription? { 71 | return attribute(named: field.name, description: field.description) 72 | } 73 | 74 | fileprivate static func attribute(named name: String, description: any StoredTypeDescription) -> NSAttributeDescription? { 75 | if let p = description as? PrimitiveTypeDescription { 76 | var type: NSAttributeType? 77 | switch p.baseType { 78 | case is Bool.Type: type = .booleanAttributeType 79 | case is Int.Type: type = .integer64AttributeType 80 | case is Int8.Type: type = .integer16AttributeType 81 | case is Int16.Type: type = .integer16AttributeType 82 | case is Int32.Type: type = .integer32AttributeType 83 | case is Int64.Type: type = .integer64AttributeType 84 | case is UInt.Type: type = .integer64AttributeType 85 | case is UInt8.Type: type = .integer16AttributeType 86 | case is UInt16.Type: type = .integer16AttributeType 87 | case is UInt32.Type: type = .integer32AttributeType 88 | case is UInt64.Type: type = .integer64AttributeType 89 | case is Float.Type: type = .floatAttributeType 90 | case is Double.Type: type = .doubleAttributeType 91 | case is String.Type: type = .stringAttributeType 92 | case is Date.Type: type = .dateAttributeType 93 | case is Data.Type: type = .binaryDataAttributeType 94 | case is UUID.Type: type = .UUIDAttributeType 95 | default: break 96 | } 97 | guard let type else { 98 | fatalError("Unknown primitive type: \(p.baseType)") 99 | } 100 | 101 | let a = NSAttributeDescription() 102 | a.name = name 103 | a.attributeType = type 104 | a.isOptional = false 105 | return a 106 | } else if let o = description as? OptionalTypeDescription { 107 | let attr = attribute(named: name, description: o.wrappedType) 108 | attr?.isOptional = true 109 | return attr 110 | } else if description is MultiValueTypeDescription { 111 | return nil 112 | } else if description is CompositeTypeDescription { 113 | return nil 114 | } else { 115 | fatalError("Unknown StoredTypeDescription: \(description)") 116 | } 117 | } 118 | 119 | } 120 | 121 | extension NSRelationshipDescription { 122 | 123 | fileprivate static func singleValueRelationship(from sourceType: CompositeTypeDescription, field: StoredField, in model: NSManagedObjectModel) throws(StorageError) { 124 | 125 | let sourceEntity = model.entity(named: sourceType.name) 126 | 127 | let destinationType: CompositeTypeDescription 128 | let isOptional: Bool 129 | 130 | if let composite = field.description as? CompositeTypeDescription { 131 | destinationType = composite 132 | isOptional = false 133 | } else if let opt = field.description as? OptionalTypeDescription, let composite = opt.wrappedType as? CompositeTypeDescription { 134 | destinationType = composite 135 | isOptional = true 136 | } else { 137 | return 138 | } 139 | 140 | let destinationEntity = model.entity(named: destinationType.name) 141 | 142 | let srcToDest = NSRelationshipDescription() 143 | srcToDest.name = field.name 144 | srcToDest.destinationEntity = destinationEntity 145 | srcToDest.maxCount = 1 146 | srcToDest.minCount = isOptional ? 0 : 1 147 | srcToDest.isOptional = isOptional 148 | srcToDest.deleteRule = .nullifyDeleteRule 149 | 150 | let destToSrc = NSRelationshipDescription() 151 | destToSrc.name = "\(sourceType.name)_\(field.name)" 152 | destToSrc.destinationEntity = sourceEntity 153 | destToSrc.minCount = 0 154 | destToSrc.maxCount = destinationType.isIdentifiable ? Int.max : 1 155 | destToSrc.isOptional = true 156 | 157 | switch (sourceType.isIdentifiable, isOptional) { 158 | case (true, true): destToSrc.deleteRule = .nullifyDeleteRule 159 | case (false, true): destToSrc.deleteRule = .nullifyDeleteRule 160 | case (true, false): destToSrc.deleteRule = .denyDeleteRule 161 | case (false, false): destToSrc.deleteRule = .cascadeDeleteRule 162 | } 163 | 164 | srcToDest.inverseRelationship = destToSrc 165 | destToSrc.inverseRelationship = srcToDest 166 | 167 | sourceEntity.properties.append(srcToDest) 168 | destinationEntity.properties.append(destToSrc) 169 | } 170 | 171 | fileprivate static func multiValueRelationship(from sourceType: CompositeTypeDescription, field: StoredField, in model: NSManagedObjectModel) throws(StorageError) { 172 | 173 | guard let multi = field.description as? MultiValueTypeDescription else { return } 174 | 175 | /* 176 | Scenarios: 177 | - List to entity 178 | - List to primitive (requires intermediate entity) 179 | - Map to primitive (requires intermediate entity) 180 | - Map to entity (requires intermediate entity) 181 | */ 182 | 183 | if multi.keyType == nil { 184 | try multiValueList(from: sourceType, field: field, in: model) 185 | } else { 186 | try multiValueMap(from: sourceType, field: field, in: model) 187 | } 188 | 189 | } 190 | 191 | private static func multiValueList(from sourceType: CompositeTypeDescription, field: StoredField, in model: NSManagedObjectModel) throws(StorageError) { 192 | 193 | let multi = field.description as! MultiValueTypeDescription 194 | 195 | let sourceEntity = model.entity(named: sourceType.name) 196 | let destinationEntity: NSEntityDescription 197 | 198 | let srcToDest = NSRelationshipDescription() 199 | srcToDest.name = field.name 200 | srcToDest.minCount = 0 201 | srcToDest.maxCount = Int.max 202 | srcToDest.isOrdered = multi.isOrdered 203 | 204 | let destToSrc = NSRelationshipDescription() 205 | destToSrc.name = "\(sourceType.name)_\(field.name)" 206 | destToSrc.deleteRule = .nullifyDeleteRule 207 | 208 | if let p = multi.valueType as? PrimitiveTypeDescription { 209 | srcToDest.deleteRule = .cascadeDeleteRule 210 | 211 | destinationEntity = NSEntityDescription() 212 | destinationEntity.name = "\(sourceType.name)_\(field.name)" 213 | model.entities.append(destinationEntity) 214 | 215 | guard let value = NSAttributeDescription.attribute(named: "value", description: p) else { 216 | fatalError("Cannot construct value property for intermediate entity off of \(sourceType.name).\(field.name)") 217 | } 218 | 219 | destinationEntity.properties.append(value) 220 | 221 | destToSrc.minCount = 1 222 | destToSrc.maxCount = 1 223 | destToSrc.isOptional = false 224 | 225 | } else if let c = multi.valueType as? CompositeTypeDescription { 226 | srcToDest.deleteRule = .nullifyDeleteRule 227 | 228 | destinationEntity = model.entity(named: c.name) 229 | 230 | destToSrc.minCount = 0 231 | destToSrc.maxCount = c.isIdentifiable ? Int.max : 1 232 | destToSrc.isOptional = true 233 | } else { 234 | return 235 | } 236 | 237 | srcToDest.destinationEntity = destinationEntity 238 | destToSrc.destinationEntity = sourceEntity 239 | 240 | srcToDest.inverseRelationship = destToSrc 241 | destToSrc.inverseRelationship = srcToDest 242 | 243 | sourceEntity.properties.append(srcToDest) 244 | destinationEntity.properties.append(destToSrc) 245 | } 246 | 247 | private static func multiValueMap(from sourceType: CompositeTypeDescription, field: StoredField, in model: NSManagedObjectModel) throws(StorageError) { 248 | 249 | let multi = field.description as! MultiValueTypeDescription 250 | let key = multi.keyType! 251 | 252 | let sourceEntity = model.entity(named: sourceType.name) 253 | 254 | // build the destination entity 255 | let mapEntity = NSEntityDescription() 256 | mapEntity.name = "\(sourceType.name)_\(field.name)" 257 | model.entities.append(mapEntity) 258 | 259 | guard let keyAttr = NSAttributeDescription.attribute(named: "key", description: key) else { 260 | fatalError("Invalid key type on \(sourceType.name).\(field.name)") 261 | } 262 | 263 | mapEntity.properties.append(keyAttr) 264 | 265 | if let primitive = multi.valueType as? PrimitiveTypeDescription { 266 | // map to primitive 267 | guard let valueAttr = NSAttributeDescription.attribute(named: "value", description: primitive) else { 268 | fatalError("Invalid value type on \(sourceType.name).\(field.name)") 269 | } 270 | mapEntity.properties.append(valueAttr) 271 | } else if let composite = multi.valueType as? CompositeTypeDescription { 272 | // map to stored type 273 | let valueEntity = model.entity(named: composite.name) 274 | 275 | let mapToValue = NSRelationshipDescription() 276 | mapToValue.name = "value" 277 | mapToValue.minCount = 1 278 | mapToValue.maxCount = 1 279 | mapToValue.isOptional = false 280 | mapToValue.destinationEntity = valueEntity 281 | mapToValue.deleteRule = .nullifyDeleteRule 282 | 283 | let valueToMap = NSRelationshipDescription() 284 | valueToMap.name = "\(sourceType.name)_\(field.name)" 285 | valueToMap.minCount = 0 286 | valueToMap.maxCount = Int.max 287 | valueToMap.isOptional = true 288 | valueToMap.deleteRule = .cascadeDeleteRule // when the destination deletes, cascade and delete the map entry 289 | 290 | mapToValue.inverseRelationship = valueToMap 291 | valueToMap.inverseRelationship = mapToValue 292 | 293 | mapEntity.properties.append(mapToValue) 294 | valueEntity.properties.append(valueToMap) 295 | } else { 296 | fatalError("This should be unreachable") 297 | } 298 | 299 | let srcToMap = NSRelationshipDescription() 300 | srcToMap.name = field.name 301 | srcToMap.destinationEntity = mapEntity 302 | srcToMap.minCount = 0 303 | srcToMap.maxCount = Int.max 304 | srcToMap.deleteRule = .cascadeDeleteRule 305 | 306 | let mapToSrc = NSRelationshipDescription() 307 | mapToSrc.name = "source" 308 | mapToSrc.destinationEntity = sourceEntity 309 | mapToSrc.deleteRule = .nullifyDeleteRule 310 | mapToSrc.minCount = 1 311 | mapToSrc.maxCount = 1 312 | mapToSrc.isOptional = false 313 | mapToSrc.deleteRule = .nullifyDeleteRule 314 | 315 | srcToMap.inverseRelationship = mapToSrc 316 | mapToSrc.inverseRelationship = srcToMap 317 | 318 | sourceEntity.properties.append(srcToMap) 319 | mapEntity.properties.append(mapToSrc) 320 | 321 | mapEntity.uniquenessConstraints = [["source", "key"]] 322 | } 323 | } 324 | 325 | 326 | #endif 327 | -------------------------------------------------------------------------------- /Sources/ORM/Engines/SQLite/SQLite+Schema.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // ORM 4 | // 5 | // Created by Dave DeLong on 4/3/25. 6 | // 7 | 8 | import Foundation 9 | import SQLiteSyntax 10 | 11 | internal struct SQLiteSchema { 12 | 13 | let base: Schema 14 | var tables: Array 15 | 16 | init(schema: Schema) throws { 17 | self.base = schema 18 | self.tables = [] 19 | 20 | for type in schema.compositeTypes { 21 | SQLiteTable.buildTables(from: type, into: &self) 22 | } 23 | } 24 | 25 | func build(into connection: SQLiteHandle) throws { 26 | try connection.transaction { h in 27 | for table in tables { 28 | let sql = try table.create.sql() 29 | try h.execute(sql) 30 | } 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Sources/ORM/Engines/SQLite/SQLiteEngine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SQLiteEngine.swift 3 | // ORM 4 | // 5 | // Created by Dave DeLong on 3/15/25. 6 | // 7 | 8 | import Foundation 9 | import SQLite 10 | import SQLite3 11 | import SQLiteSyntax 12 | 13 | /* 14 | 15 | NOTES: 16 | - sqlite3_deserialize + sqlite3_serialize to convert an in-memory database to/from a writable Data 17 | - sqlite3_snapshot_open for queries? to make sure that retrieving related values is consistent? 18 | - sqlite3_update_hook for observation deltas 19 | 20 | */ 21 | 22 | public actor SQLiteEngine: StorageEngine { 23 | private let schema: Schema 24 | private let sqliteSchema: SQLiteSchema 25 | private let handle: SQLiteHandle 26 | 27 | public init(schema: Schema, at url: URL) async throws { 28 | self.schema = schema 29 | self.sqliteSchema = try! SQLiteSchema(schema: schema) 30 | 31 | self.handle = try SQLiteHandle(path: url.path(percentEncoded: false)) 32 | 33 | try! self.sqliteSchema.build(into: handle) 34 | } 35 | 36 | public func save(_ value: any StoredType) throws(StorageError) { 37 | let valueType = type(of: value) 38 | guard let description = schema.compositeTypes.first(where: { $0.baseType == valueType }) else { 39 | throw StorageError.unknownStoredType(value) 40 | } 41 | guard let table = sqliteSchema.tables.first(where: { $0.name == description.name }) else { 42 | throw StorageError.unknownStoredType(value) 43 | } 44 | 45 | let (insert, keyPaths) = table.insertStatement(valueCount: 1) 46 | 47 | do { 48 | try handle.transaction { h in 49 | let sql = try insert.sql() 50 | let statement = try h.prepare(sql) 51 | 52 | let values = try description.extract(values: keyPaths, from: value) 53 | for (offset, value) in values.enumerated() { 54 | try statement.bind(value, at: Int32(offset + 1)) 55 | } 56 | 57 | let result = try h.run(statement) 58 | print(result) 59 | try statement.finish() 60 | } 61 | } catch { 62 | print("ERROR: \(error)") 63 | } 64 | } 65 | 66 | } 67 | 68 | extension CompositeTypeDescription { 69 | 70 | fileprivate func extract(values keyPaths: Array, from value: any StoredType) throws -> Array { 71 | guard type(of: value) == self.baseType else { throw StorageError.unknownStoredType(value) } 72 | 73 | return try keyPaths.map { kp in 74 | let field = try self.fieldsByKeyPath[kp] ?! StorageError.unknownFieldType(self.baseType, "", kp, kp.erasedValueType) 75 | guard field.kind != .multiValue else { fatalError() } 76 | 77 | guard field.kind == .primitive else { fatalError("FOR NOW") } 78 | 79 | let raw = value[keyPath: kp] 80 | if let opt = raw as? OptionalType, opt.isNull { 81 | return .null 82 | } else if let b = raw as? Bool { 83 | return .bool(b) 84 | } else if let i = raw as? Int64 { 85 | return .int(i) 86 | } else if let d = raw as? Double { 87 | return .double(d) 88 | } else if let s = raw as? String { 89 | return .text(s) 90 | } else if let d = raw as? Data { 91 | return .blob(d) 92 | } else { 93 | throw StorageError.unknownFieldType(self.baseType, field.name, kp, kp.erasedValueType) 94 | } 95 | } 96 | } 97 | 98 | 99 | } 100 | -------------------------------------------------------------------------------- /Sources/ORM/Engines/SQLite/SQLiteHandle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SQLiteHandle.swift 3 | // ORM 4 | // 5 | // Created by Dave DeLong on 5/17/25. 6 | // 7 | 8 | import Foundation 9 | import SQLite3 10 | 11 | infix operator ?!: NilCoalescingPrecedence 12 | 13 | internal func ?! (lhs: T?, rhs: @autoclosure () -> E) throws(E) -> T { 14 | if let lhs { return lhs } 15 | throw rhs() 16 | } 17 | 18 | fileprivate func unwrap(_ lhs: T?) throws(SQLiteError) -> T { 19 | return try lhs ?! SQLiteError(rawValue: SQLITE_INTERNAL) 20 | } 21 | 22 | struct SQLiteError: Error, CustomStringConvertible { 23 | let rawValue: Int32 24 | 25 | var description: String { 26 | "SQLite Error \(rawValue): \(String(cString: sqlite3_errstr(rawValue)))" 27 | } 28 | } 29 | 30 | fileprivate func check(_ status: Int32) throws(SQLiteError) { 31 | if status == SQLITE_OK { return } 32 | if status == SQLITE_ROW { return } 33 | if status == SQLITE_DONE { return } 34 | 35 | throw SQLiteError(rawValue: status) 36 | } 37 | 38 | internal class SQLiteHandle { 39 | private let dbHandle: OpaquePointer 40 | private var statements = Dictionary>() 41 | 42 | init(path: String) throws { 43 | dbHandle = try path.withCString { pathPtr in 44 | var ptr: OpaquePointer? 45 | let status = sqlite3_open_v2(pathPtr, &ptr, (SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_URI), nil) 46 | try check(status) 47 | return try ptr ?! SQLiteError(rawValue: SQLITE_INTERNAL) 48 | } 49 | 50 | try execute("PRAGMA journal_mode=WAL") 51 | } 52 | 53 | deinit { 54 | statements.values.forEach { $0.value?.handle = nil; $0.value = nil } 55 | statements.removeAll() 56 | sqlite3_close_v2(dbHandle) 57 | } 58 | 59 | func transaction(_ perform: (SQLiteHandle) throws -> Void) throws { 60 | try execute("BEGIN TRANSACTION") 61 | try perform(self) 62 | let txn = try execute("COMMIT TRANSACTION") 63 | print(txn) 64 | } 65 | 66 | @discardableResult 67 | func execute(_ sql: String) throws -> SQLiteStatement.Step { 68 | let s = try prepare(sql) 69 | let r = try run(s) 70 | try s.finish() 71 | return r 72 | } 73 | 74 | func prepare(_ sql: String) throws -> SQLiteStatement { 75 | print(sql) 76 | if let existing = statements[sql]?.value { 77 | try existing.reset() 78 | return existing 79 | } 80 | 81 | var handle: OpaquePointer? 82 | let status = sqlite3_prepare_v2(dbHandle, sql, -1, &handle, nil) 83 | try check(status) 84 | 85 | let statement = try SQLiteStatement(handle: unwrap(handle)) 86 | statements[sql] = Weak(value: statement) 87 | return statement 88 | } 89 | 90 | func run(_ statement: SQLiteStatement) throws -> SQLiteStatement.Step { 91 | return try statement.step() 92 | } 93 | 94 | } 95 | 96 | private class Weak { 97 | 98 | fileprivate(set) weak var value: T? 99 | 100 | init(value: T) { 101 | self.value = value 102 | } 103 | 104 | } 105 | 106 | /** 107 | The constant, `SQLITE_TRANSIENT`, may be passed to indicate that the object is to be copied prior to the return from `sqlite3_bind_*()`. 108 | The object and pointer to it must remain valid until then. SQLite will then manage the lifetime of its private copy. 109 | 110 | ``` 111 | #define SQLITE_TRANSIENT ((sqlite3_destructor_type)-1) 112 | ``` 113 | */ 114 | private let transient = unsafeBitCast(-1, to: sqlite3_destructor_type.self) 115 | 116 | internal class SQLiteStatement { 117 | 118 | enum Step { 119 | case ok(Dictionary) 120 | case done(Dictionary) 121 | } 122 | 123 | fileprivate var handle: OpaquePointer? { 124 | didSet { 125 | if let oldValue, handle == nil { 126 | sqlite3_finalize(oldValue) 127 | } 128 | } 129 | } 130 | let parameterCount: Int32 131 | 132 | fileprivate init(handle: OpaquePointer) { 133 | self.handle = handle 134 | self.parameterCount = sqlite3_bind_parameter_count(handle) 135 | } 136 | 137 | fileprivate func reset() throws { 138 | let h = try unwrap(handle) 139 | try check(sqlite3_reset(h)) 140 | try check(sqlite3_clear_bindings(h)) 141 | } 142 | 143 | internal func bind(_ value: SQLiteValue, name: String) throws { 144 | let h = try unwrap(handle) 145 | let idx = sqlite3_bind_parameter_index(h, name) 146 | try bind(value, at: idx) 147 | } 148 | 149 | internal func bind(_ value: SQLiteValue, at idx: Int32) throws { 150 | guard idx > 0 else { throw SQLiteError(rawValue: SQLITE_NOTFOUND) } 151 | guard idx <= parameterCount else { throw SQLiteError(rawValue: SQLITE_RANGE) } 152 | print("BINDING \(idx) = \(value)") 153 | let h = try unwrap(handle) 154 | switch value { 155 | case .null: 156 | try check(sqlite3_bind_null(h, idx)) 157 | case .bool(let b): 158 | try check(sqlite3_bind_int64(h, idx, b ? 1 : 0)) 159 | case .int(let i): 160 | try check(sqlite3_bind_int64(h, idx, i)) 161 | case .double(let d): 162 | try check(sqlite3_bind_double(h, idx, d)) 163 | case .text(let t): 164 | try check(sqlite3_bind_text(h, idx, t, -1, transient)) 165 | case .blob(let d): 166 | if d.isEmpty { 167 | try check(sqlite3_bind_zeroblob(h, idx, 0)) 168 | } else { 169 | try d.withUnsafeBytes { ptr in 170 | try check(sqlite3_bind_blob(h, idx, ptr.baseAddress, Int32(ptr.count), transient)) 171 | } 172 | } 173 | } 174 | } 175 | 176 | fileprivate func step() throws -> Step { 177 | let stmt = try unwrap(handle) 178 | 179 | let status = sqlite3_step(stmt) 180 | try check(status) 181 | 182 | let columnCount = sqlite3_column_count(stmt) 183 | var results = Dictionary() 184 | for col in 0 ..< columnCount { 185 | let name = String(cString: sqlite3_column_name(stmt, col)) 186 | let type = sqlite3_column_type(stmt, col) 187 | switch type { 188 | case SQLITE_INTEGER: 189 | results[name] = .int(Int64(sqlite3_column_int(stmt, col))) 190 | case SQLITE_FLOAT: 191 | results[name] = .double(sqlite3_column_double(stmt, col)) 192 | case SQLITE_TEXT: 193 | results[name] = .text(String(cString: sqlite3_column_text(stmt, col))) 194 | case SQLITE_BLOB: 195 | let byteCount = sqlite3_column_bytes(stmt, col) 196 | if byteCount > 0 { 197 | let data = Data(bytes: sqlite3_column_blob(stmt, col), count: Int(byteCount)) 198 | results[name] = .blob(data) 199 | } else { 200 | results[name] = .blob(Data()) 201 | } 202 | case SQLITE_NULL: 203 | results[name] = .null 204 | default: fatalError() 205 | } 206 | } 207 | 208 | if status == SQLITE_DONE { 209 | return .done(results) 210 | } else { 211 | return .ok(results) 212 | } 213 | } 214 | 215 | func finish() throws { 216 | guard let handle else { return } 217 | try check(sqlite3_reset(handle)) 218 | } 219 | } 220 | 221 | internal enum SQLiteValue { 222 | case null 223 | case bool(Bool) 224 | case int(Int64) 225 | case double(Double) 226 | case text(String) 227 | case blob(Data) 228 | } 229 | -------------------------------------------------------------------------------- /Sources/ORM/Engines/SQLite/SQLiteTable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // ORM 4 | // 5 | // Created by Dave DeLong on 5/2/25. 6 | // 7 | 8 | import Foundation 9 | import SQLite 10 | import SQLiteSyntax 11 | 12 | /* 13 | NOTES ON INSERTING: 14 | 15 | When inserting values, it needs to be done in a particular order: 16 | 1. First, non-identifiable related values need to be inserted (INSERT OR IGNORE), so that their respective ROWIDs can be used 17 | 2. Then, the row can be inserted for the value, using any ROWIDs for single-value non-identifiable relations 18 | 3. Then, the multi-value values can be inserted, using the ROWID/id of the value that was just inserted 19 | 4. Finally, ordered-multi-values need their orders updated 20 | 21 | */ 22 | 23 | struct SQLiteColumn { 24 | let name: String 25 | let keyPath: AnyKeyPath 26 | let sqliteName: Name 27 | } 28 | 29 | internal struct SQLiteTable { 30 | 31 | let name: String 32 | 33 | var create: SQLiteSyntax.Table.Create 34 | 35 | var columns = Array() 36 | private(set) var insertValuesKeyPaths = Array() 37 | private(set) var insertStatement: SQLiteSyntax.InsertStatement 38 | private var insertValuesTemplate = Group>(contents: []) 39 | 40 | init(name: String) { 41 | self.name = name 42 | self.create = Table.Create(name: Name(value: name)) 43 | self.insertStatement = InsertStatement( 44 | action: .insert(.replace), 45 | tableName: Name(value: name), 46 | values: .values([], nil), 47 | returning: nil 48 | ) 49 | } 50 | 51 | fileprivate mutating func finalizeInitialization(forIdentifiableType isIdentifiable: Bool) { 52 | self.insertValuesKeyPaths = columns.map(\.keyPath) 53 | self.insertStatement.columns = List(columns.map(\.sqliteName)) 54 | self.insertValuesTemplate.contents.items = (0 ..< columns.count).map { _ in 55 | return .bindParameter(.next) 56 | } 57 | 58 | let idColumn = isIdentifiable ? Name(value: "id") : Name(value: "ROWID") 59 | self.insertStatement.returning = .init(values: [.expression(.column(idColumn), alias: Name("id"))]) 60 | } 61 | 62 | func insertStatement(valueCount: Int) -> (SQLiteSyntax.InsertStatement, Array) { 63 | var stmt = insertStatement 64 | let values = Array(repeating: insertValuesTemplate, count: valueCount) 65 | stmt.values = .values(List(values), nil) 66 | 67 | return (stmt, insertValuesKeyPaths) 68 | } 69 | } 70 | 71 | extension SQLiteTable { 72 | 73 | static func buildTables(from thisType: CompositeTypeDescription, into schema: inout SQLiteSchema) { 74 | var base = SQLiteTable(name: thisType.name) 75 | for field in thisType.fields { 76 | if let p = field.description as? PrimitiveTypeDescription { 77 | var column = SQLiteSyntax.ColumnDefinition(name: Name(value: field.name), 78 | typeName: p.sqliteTypeName, 79 | constraints: [ 80 | .notNull 81 | ]) 82 | if field.name == "id" && thisType.isIdentifiable { 83 | column.constraints?.append(.init(name: nil, constraint: .primaryKey(nil, .none, autoincrement: false))) 84 | } 85 | base.create.columns.append(column) 86 | base.columns.append(.init(name: field.name, keyPath: field.keyPath, sqliteName: column.name)) 87 | 88 | } else if let o = field.description as? OptionalTypeDescription, let p = o.wrappedType as? PrimitiveTypeDescription { 89 | let column = SQLiteSyntax.ColumnDefinition(name: Name(value: field.name), 90 | typeName: p.sqliteTypeName, 91 | constraints: []) 92 | base.create.columns.append(column) 93 | base.columns.append(.init(name: field.name, keyPath: field.keyPath, sqliteName: column.name)) 94 | 95 | } else if let thatType = field.description as? CompositeTypeDescription { 96 | let name = Name(value: field.name) 97 | let that = Name(value: thatType.name) 98 | if thatType.isIdentifiable { 99 | // if that thing gets deleted, cascade delete this 100 | let thatIDType = thatType.idField 101 | base.create.addForeignKey(name, references: that, column: "id", type: thatIDType?.sqliteTypeName, canBeNull: false, onDelete: .cascade) 102 | } else { 103 | // if that thing gets deleted, do nothing 104 | // Maybe we add a trigger that cascade deletes the value if this is deleted? 105 | base.create.addForeignKey(name, references: that, column: "ROWID", type: .integer, canBeNull: false) 106 | } 107 | base.columns.append(.init(name: field.name, keyPath: field.keyPath, sqliteName: name)) 108 | 109 | } else if let o = field.description as? OptionalTypeDescription, let c = o.wrappedType as? CompositeTypeDescription { 110 | let name = Name(value: field.name) 111 | let that = Name(value: c.name) 112 | let ref = Name(value: c.isIdentifiable ? "id" : "ROWID") 113 | let type: TypeName = c.idField?.sqliteTypeName ?? .integer 114 | base.create.addForeignKey(name, references: that, column: ref, type: type, canBeNull: true) 115 | base.columns.append(.init(name: field.name, keyPath: field.keyPath, sqliteName: name)) 116 | 117 | } else if let m = field.description as? MultiValueTypeDescription { 118 | // need an intermediate table 119 | 120 | let joinTableName = "\(thisType.name)_\(field.name)" 121 | var join = SQLiteTable(name: joinTableName) 122 | 123 | var uniqueTuple = Array() 124 | 125 | let parentIDColumn: Name = thisType.isIdentifiable ? "id" : "ROWID" 126 | let parentIDType: TypeName = thisType.idField?.sqliteTypeName ?? .integer 127 | join.create.addForeignKey("parent", 128 | references: Name(value: thisType.name), 129 | column: parentIDColumn, 130 | type: parentIDType, 131 | canBeNull: false, 132 | onDelete: .cascade) 133 | uniqueTuple.append("parent") 134 | 135 | if let key = m.keyType as? PrimitiveTypeDescription { 136 | join.create.addColumn("key", type: key.sqliteTypeName, canBeNull: false) 137 | uniqueTuple.append("key") 138 | } else if m.isOrdered { 139 | join.create.addColumn("order", type: .integer, canBeNull: false) 140 | uniqueTuple.append("order") 141 | } 142 | 143 | if let p = m.valueType as? PrimitiveTypeDescription { 144 | join.create.addColumn("value", type: p.sqliteTypeName, canBeNull: false) 145 | } else if let c = m.valueType as? CompositeTypeDescription { 146 | let name: Name = c.isIdentifiable ? "id" : "ROWID" 147 | let type = c.idField?.sqliteTypeName ?? .integer 148 | join.create.addForeignKey("value", references: Name(value: c.name), column: name, type: type, canBeNull: false, onDelete: .cascade) 149 | } 150 | 151 | if m.isOrdered == false && m.keyType == nil { 152 | // this is a "set", so duplicate values are not allowed 153 | uniqueTuple.append("value") 154 | } 155 | 156 | join.create.addColumn("processed", type: .integer) 157 | 158 | let uniqueColumns = uniqueTuple.map { IndexedColumn(name: .columnName(Name(value: $0))) } 159 | join.create.constraints.append( 160 | TableConstraint(constraint: .unique(List(uniqueColumns), .abort)) 161 | ) 162 | 163 | join.finalizeInitialization(forIdentifiableType: false) 164 | schema.tables.append(join) 165 | } 166 | } 167 | 168 | base.finalizeInitialization(forIdentifiableType: thisType.isIdentifiable) 169 | schema.tables.append(base) 170 | } 171 | 172 | } 173 | 174 | -------------------------------------------------------------------------------- /Sources/ORM/Engines/StorageEngine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // ORM 4 | // 5 | // Created by Dave DeLong on 3/15/25. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol StorageEngine: Actor { 11 | 12 | init(schema: Schema, at url: URL) async throws 13 | 14 | func save(_ value: any StoredType) throws 15 | } 16 | 17 | extension StorageEngine { 18 | 19 | public init(_ first: any StoredType.Type, _ others: any StoredType.Type..., at url: URL) async throws { 20 | var all: Array = [first] 21 | all.append(contentsOf: others) 22 | let s = try Schema(types: all) 23 | try await self.init(schema: s, at: url) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Sources/ORM/Engines/StorageError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageError.swift 3 | // ORM 4 | // 5 | // Created by Dave DeLong on 3/30/25. 6 | // 7 | 8 | public enum StorageError: Error, Equatable { 9 | public static func ==(lhs: Self, rhs: Self) -> Bool { 10 | switch (lhs, rhs) { 11 | case (.storedTypeMissingIdentifier(let l), .storedTypeMissingIdentifier(let r)): 12 | return l == r 13 | case (.storedTypeIsEmpty(let l), .storedTypeIsEmpty(let r)): 14 | return l == r 15 | case (.storedTypeIsNotIdentifiable(let l), .storedTypeIsNotIdentifiable(let r)): 16 | return l == r 17 | case (.storedTypeMustBeValueType(let l), .storedTypeMustBeValueType(let r)): 18 | return l == r 19 | 20 | case (.identifierCannotBeOptional(let l), .identifierCannotBeOptional(let r)): 21 | return l == r 22 | case (.identifierMustBePrimitive(let l), .identifierMustBePrimitive(let r)): 23 | return l == r 24 | 25 | case (.invalidFieldName(let lType, let lName, _), .invalidFieldName(let rType, let rName, _)): 26 | return lType == rType && lName.lowercased() == rName.lowercased() 27 | case (.unknownFieldType(let lType, let lName, _, _), .unknownFieldType(let rType, let rName, _, _)): 28 | return lType == rType && lName == rName 29 | 30 | case (.multiValueFieldsCannotBeOptional(let lType, let lField), .multiValueFieldsCannotBeOptional(let rType, let rField)): 31 | return lType == rType && lField.name == rField.name 32 | 33 | case (.optionalFieldCannotNestOptional(let lType, let lField), .optionalFieldCannotNestOptional(let rType, let rField)): 34 | return lType == rType && lField.name == rField.name 35 | 36 | case (.multiValueFieldsCannotBeNested(let lType, let lField), .multiValueFieldsCannotBeNested(let rType, let rField)): 37 | return lType == rType && lField.name == rField.name 38 | 39 | case (.dictionaryKeyCannotBeOptional(let lType, let lField), .dictionaryKeyCannotBeOptional(let rType, let rField)): 40 | return lType == rType && lField.name == rField.name 41 | 42 | case (.dictionaryKeyMustBePrimitive(let lType, let lField), .dictionaryKeyMustBePrimitive(let rType, let rField)): 43 | return lType == rType && lField.name == rField.name 44 | 45 | case (.multiValueFieldsCannotNestOptionals(let lType, let lField), .multiValueFieldsCannotNestOptionals(let rType, let rField)): 46 | return lType == rType && lField.name == rField.name 47 | 48 | default: 49 | return false 50 | } 51 | } 52 | 53 | case storedTypeMissingIdentifier(any StoredType.Type) 54 | case storedTypeIsEmpty(any StoredType.Type) 55 | case storedTypeIsNotIdentifiable(any StoredType.Type) 56 | case storedTypeMustBeValueType(any StoredType.Type) 57 | 58 | case identifierCannotBeOptional(any StoredType.Type) 59 | case identifierMustBePrimitive(any StoredType.Type) 60 | 61 | case invalidFieldName(any StoredType.Type, String, AnyKeyPath) 62 | case unknownFieldType(any StoredType.Type, String, AnyKeyPath, Any.Type) 63 | 64 | case optionalFieldCannotNestOptional(any StoredType.Type, StoredField) 65 | 66 | case multiValueFieldsCannotBeOptional(any StoredType.Type, StoredField) 67 | case multiValueFieldsCannotBeNested(any StoredType.Type, StoredField) 68 | case multiValueFieldsCannotNestOptionals(any StoredType.Type, StoredField) 69 | 70 | case dictionaryKeyCannotBeOptional(any StoredType.Type, StoredField) 71 | case dictionaryKeyMustBePrimitive(any StoredType.Type, StoredField) 72 | 73 | case unknownStoredType(any StoredType) 74 | 75 | case unknown(any Error) 76 | } 77 | -------------------------------------------------------------------------------- /Sources/ORM/Internals/Bimap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 6/16/24. 6 | // 7 | 8 | import Foundation 9 | 10 | internal struct Bimap: CustomStringConvertible { 11 | 12 | private var leftToRight: Dictionary 13 | private var rightToLeft: Dictionary 14 | 15 | internal var description: String { 16 | let pairs = leftToRight.map { "\($0) <-> \($1)" }.joined(separator: ", ") 17 | return "Bimap<\(L.self), \(R.self)>(\(leftToRight.count): \(pairs))" 18 | } 19 | 20 | internal init() { 21 | leftToRight = [:] 22 | rightToLeft = [:] 23 | } 24 | 25 | internal init(_ elements: any Collection<(L, R)>) { 26 | leftToRight = Dictionary(uniqueKeysWithValues: elements) 27 | rightToLeft = Dictionary(uniqueKeysWithValues: elements.map { ($1, $0) }) 28 | } 29 | 30 | subscript(left: L) -> R? { 31 | get { leftToRight[left] } 32 | set { leftToRight[left] = newValue } 33 | } 34 | 35 | subscript(right: R) -> L? { 36 | get { rightToLeft[right] } 37 | set { rightToLeft[right] = newValue } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/ORM/Internals/ForEachField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 6/8/24. 6 | // 7 | 8 | import Foundation 9 | 10 | func fields(of type: Any.Type) -> Array<(String, AnyKeyPath)> { 11 | _openExistential(type, do: fields(of:)) 12 | } 13 | 14 | func fields(of type: T.Type = T.self) -> Array<(String, PartialKeyPath)> { 15 | var all = Array<(String, PartialKeyPath)>() 16 | enumerateFields(of: type, using: { 17 | all.append(($0, $1)) 18 | }) 19 | return all 20 | } 21 | 22 | func enumerateFields(of type: T.Type = T.self, using block: (String, PartialKeyPath) -> Void) { 23 | var options: EachFieldOptions = [.ignoreUnknown] 24 | _ = forEachFieldWithKeyPath(of: type, options: &options, body: { label, keyPath in 25 | let string = String(cString: label) 26 | block(string, keyPath) 27 | return true 28 | }) 29 | } 30 | 31 | extension AnyKeyPath { 32 | internal var erasedRootType: any Any.Type { type(of: self).rootType } 33 | internal var erasedValueType: any Any.Type { type(of: self).valueType } 34 | } 35 | 36 | internal struct EachFieldOptions: OptionSet { 37 | static let classType = Self(rawValue: 1 << 0) 38 | static let ignoreUnknown = Self(rawValue: 1 << 1) 39 | 40 | let rawValue: UInt32 41 | 42 | init(rawValue: UInt32) { 43 | self.rawValue = rawValue 44 | } 45 | } 46 | 47 | @discardableResult 48 | @_silgen_name("$ss24_forEachFieldWithKeyPath2of7options4bodySbxm_s01_bC7OptionsVSbSPys4Int8VG_s07PartialeF0CyxGtXEtlF") 49 | private func forEachFieldWithKeyPath( 50 | of type: Root.Type, 51 | options: inout EachFieldOptions, 52 | body: (UnsafePointer, PartialKeyPath) -> Bool 53 | ) -> Bool 54 | -------------------------------------------------------------------------------- /Sources/ORM/Internals/TypeDetectors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypeDetectors.swift 3 | // ORM 4 | // 5 | // Created by Dave DeLong on 3/13/25. 6 | // 7 | 8 | internal protocol ArrayType { 9 | static var elementType: Any.Type { get } 10 | } 11 | 12 | internal protocol SetType { 13 | static var elementType: Any.Type { get } 14 | } 15 | 16 | internal protocol DictionaryType { 17 | static var keyType: Any.Type { get } 18 | static var valueType: Any.Type { get } 19 | } 20 | 21 | internal protocol OptionalType { 22 | static var wrappedType: Any.Type { get } 23 | var isNull: Bool { get } 24 | } 25 | 26 | extension Array: ArrayType { 27 | static var elementType: any Any.Type { Element.self } 28 | } 29 | 30 | extension Set: SetType { 31 | static var elementType: any Any.Type { Element.self } 32 | } 33 | 34 | extension Dictionary: DictionaryType { 35 | static var keyType: Any.Type { Key.self } 36 | static var valueType: any Any.Type { Value.self } 37 | } 38 | 39 | extension Optional: OptionalType { 40 | static var wrappedType: any Any.Type { Wrapped.self } 41 | var isNull: Bool { 42 | if case .none = self { return true } 43 | return false 44 | } 45 | } 46 | 47 | extension RawRepresentable { 48 | static var rawValueType: Any.Type { RawValue.self } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/ORM/Schema/Schema.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Schema.swift 3 | // ORM 4 | // 5 | // Created by Dave DeLong on 3/15/25. 6 | // 7 | 8 | public struct Schema { 9 | 10 | public let storedTypes: Array 11 | 12 | public let compositeTypes: Array 13 | 14 | public var primitiveTypes: Array { 15 | storedTypes.compactMap({ $0 as? PrimitiveTypeDescription }) 16 | } 17 | 18 | public var multiValueTypes: Array { 19 | storedTypes.compactMap({ $0 as? MultiValueTypeDescription }) 20 | } 21 | 22 | public let baseTypes: Array 23 | 24 | private let typeDescriptionLookup: Dictionary 25 | 26 | public init(_ first: any StoredType.Type, _ types: any StoredType.Type...) throws(StorageError) { 27 | let all = [first] + types 28 | try self.init(types: all) 29 | } 30 | 31 | public init(types: Array) throws(StorageError) { 32 | var unprocessedTypeDescriptions = Array() 33 | for type in types { unprocessedTypeDescriptions.append(try type.storedTypeDescription) } 34 | 35 | var processedTypes = Set() 36 | var storageDescriptions = Array() 37 | 38 | repeat { 39 | let description = unprocessedTypeDescriptions.removeFirst() 40 | let typeIdentifier = ObjectIdentifier(description.baseType) 41 | guard processedTypes.insert(typeIdentifier).inserted == true else { continue } 42 | 43 | storageDescriptions.append(description) 44 | 45 | unprocessedTypeDescriptions.append(contentsOf: description.transitiveTypeDescriptions) 46 | 47 | } while unprocessedTypeDescriptions.isEmpty == false 48 | 49 | self.storedTypes = storageDescriptions 50 | self.baseTypes = storageDescriptions.map(\.baseType) 51 | self.compositeTypes = storageDescriptions.compactMap({ $0 as? CompositeTypeDescription }) 52 | 53 | self.typeDescriptionLookup = Dictionary(uniqueKeysWithValues: compositeTypes.map { 54 | (ObjectIdentifier($0.baseType), $0) 55 | }) 56 | } 57 | 58 | internal func description(for value: any StoredType) -> CompositeTypeDescription? { 59 | let type = type(of: value) 60 | return typeDescriptionLookup[ObjectIdentifier(type)] 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Sources/ORM/Schema/SchemaError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // ORM 4 | // 5 | // Created by Dave DeLong on 3/16/25. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Schema { 11 | 12 | public enum Error: Swift.Error, Equatable { 13 | public static func ==(lhs: Self, rhs: Self) -> Bool { 14 | switch (lhs, rhs) { 15 | case (.storedTypeMissingIdentifier(let l), .storedTypeMissingIdentifier(let r)): 16 | return l == r 17 | case (.storedTypeIsEmpty(let l), .storedTypeIsEmpty(let r)): 18 | return l == r 19 | case (.storedTypeIsNotIdentifiable(let l), .storedTypeIsNotIdentifiable(let r)): 20 | return l == r 21 | case (.storedTypeMustBeValueType(let l), .storedTypeMustBeValueType(let r)): 22 | return l == r 23 | 24 | case (.identifierCannotBeOptional(let l), .identifierCannotBeOptional(let r)): 25 | return l == r 26 | case (.identifierMustBePrimitive(let l), .identifierMustBePrimitive(let r)): 27 | return l == r 28 | 29 | case (.invalidFieldName(let lType, let lName, _), .invalidFieldName(let rType, let rName, _)): 30 | return lType == rType && lName.lowercased() == rName.lowercased() 31 | case (.unknownFieldType(let lType, let lName, _, _), .unknownFieldType(let rType, let rName, _, _)): 32 | return lType == rType && lName == rName 33 | 34 | case (.multiValueFieldsCannotBeOptional(let lType, let lField), .multiValueFieldsCannotBeOptional(let rType, let rField)): 35 | return lType == rType && lField.name == rField.name 36 | 37 | case (.optionalFieldCannotNestOptional(let lType, let lField), .optionalFieldCannotNestOptional(let rType, let rField)): 38 | return lType == rType && lField.name == rField.name 39 | 40 | case (.multiValueFieldsCannotBeNested(let lType, let lField), .multiValueFieldsCannotBeNested(let rType, let rField)): 41 | return lType == rType && lField.name == rField.name 42 | 43 | case (.dictionaryKeyCannotBeOptional(let lType, let lField), .dictionaryKeyCannotBeOptional(let rType, let rField)): 44 | return lType == rType && lField.name == rField.name 45 | 46 | case (.dictionaryKeyMustBePrimitive(let lType, let lField), .dictionaryKeyMustBePrimitive(let rType, let rField)): 47 | return lType == rType && lField.name == rField.name 48 | 49 | case (.multiValueFieldsCannotNestOptionals(let lType, let lField), .multiValueFieldsCannotNestOptionals(let rType, let rField)): 50 | return lType == rType && lField.name == rField.name 51 | 52 | default: 53 | return false 54 | } 55 | } 56 | 57 | case storedTypeMissingIdentifier(any StoredType.Type) 58 | case storedTypeIsEmpty(any StoredType.Type) 59 | case storedTypeIsNotIdentifiable(any StoredType.Type) 60 | case storedTypeMustBeValueType(any StoredType.Type) 61 | 62 | case identifierCannotBeOptional(any StoredType.Type) 63 | case identifierMustBePrimitive(any StoredType.Type) 64 | 65 | case invalidFieldName(any StoredType.Type, String, AnyKeyPath) 66 | case unknownFieldType(any StoredType.Type, String, AnyKeyPath, Any.Type) 67 | 68 | case optionalFieldCannotNestOptional(any StoredType.Type, StoredField) 69 | 70 | case multiValueFieldsCannotBeOptional(any StoredType.Type, StoredField) 71 | case multiValueFieldsCannotBeNested(any StoredType.Type, StoredField) 72 | case multiValueFieldsCannotNestOptionals(any StoredType.Type, StoredField) 73 | 74 | case dictionaryKeyCannotBeOptional(any StoredType.Type, StoredField) 75 | case dictionaryKeyMustBePrimitive(any StoredType.Type, StoredField) 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /Sources/ORM/StoredTypes/StoredField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // ORM 4 | // 5 | // Created by Dave DeLong on 3/30/25. 6 | // 7 | 8 | import Foundation 9 | 10 | extension StoredType { 11 | 12 | internal static var storedFields: Array { 13 | get throws(StorageError) { 14 | var fields = Array() 15 | for (name, keyPath) in Self.fields { 16 | fields.append(try StoredField(baseType: Self.self, name: name, keyPath: keyPath)) 17 | } 18 | return fields 19 | } 20 | } 21 | 22 | internal static var Fields: Fields { .init() } 23 | 24 | } 25 | 26 | public struct StoredField { 27 | 28 | internal enum Kind { 29 | case primitive 30 | case reference 31 | case multiValue 32 | 33 | init(_ type: any StoredTypeDescription) { 34 | if type is PrimitiveTypeDescription { 35 | self = .primitive 36 | } else if type is CompositeTypeDescription { 37 | self = .reference 38 | } else if type is MultiValueTypeDescription { 39 | self = .multiValue 40 | } else if let o = type as? OptionalTypeDescription { 41 | self = Kind(o.wrappedType) 42 | } else { 43 | fatalError("UNKNOWN TYPE DESCRIPTION \(type)") 44 | } 45 | } 46 | } 47 | 48 | public let name: String 49 | public let keyPath: AnyKeyPath 50 | public let description: any StoredTypeDescription 51 | 52 | internal let kind: Kind 53 | internal let isOptional: Bool 54 | 55 | internal init(baseType: any StoredType.Type, 56 | name: String, 57 | keyPath: AnyKeyPath) throws(StorageError) { 58 | 59 | let fieldType = keyPath.erasedValueType 60 | 61 | self.name = name 62 | self.keyPath = keyPath 63 | self.isOptional = fieldType is OptionalType 64 | 65 | if let storedType = fieldType as? any StoredType.Type { 66 | let description = try storedType.storedTypeDescription 67 | self.description = description 68 | self.kind = Kind(description) 69 | 70 | } else if let rawType = fieldType as? any RawRepresentable.Type, let desc = try rawType.storedTypeDescription { 71 | self.description = desc 72 | self.kind = Kind(desc) 73 | 74 | } else { 75 | throw StorageError.unknownFieldType(baseType, name, keyPath, fieldType) 76 | } 77 | } 78 | 79 | } 80 | 81 | @dynamicMemberLookup 82 | internal struct Fields { 83 | 84 | subscript(dynamicMember keyPath: KeyPath) -> StoredField { 85 | get throws(StorageError) { 86 | guard let (name, keyPath) = S.fields.first(where: { $1 == keyPath }) else { 87 | throw .unknownFieldType(S.self, "", keyPath, V.self) 88 | } 89 | 90 | return try StoredField(baseType: S.self, name: name, keyPath: keyPath) 91 | } 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /Sources/ORM/StoredTypes/StoredType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 12/22/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol StoredType { 11 | static var storedTypeDescription: any StoredTypeDescription { 12 | get throws(StorageError) 13 | } 14 | } 15 | 16 | extension StoredType { 17 | 18 | internal static var fields: Array<(String, PartialKeyPath)> { 19 | return ORM.fields(of: Self.self) 20 | } 21 | 22 | public static var storedTypeDescription: any StoredTypeDescription { 23 | get throws(StorageError) { 24 | try CompositeTypeDescription(Self.self) 25 | } 26 | } 27 | 28 | } 29 | 30 | extension StoredType where Self: AnyObject { 31 | 32 | public static var storedTypeDescription: any StoredTypeDescription { 33 | get throws(StorageError) { 34 | try CompositeTypeDescription(Self.self) 35 | } 36 | } 37 | } 38 | 39 | extension Optional: StoredType where Wrapped: StoredType { 40 | public static var storedTypeDescription: any StoredTypeDescription { 41 | get throws(StorageError) { try OptionalTypeDescription(Self.self) } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/ORM/StoredTypes/TypeDescriptions/CompositeTypeDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 1/9/25. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct CompositeTypeDescription: StoredTypeDescription { 11 | public let name: String 12 | public let baseType: any StoredType.Type 13 | public let fields: Array 14 | internal let fieldsByKeyPath: Dictionary 15 | internal let fieldsByName: Dictionary 16 | 17 | public var isIdentifiable: Bool { baseType is any Identifiable.Type } 18 | 19 | public var idField: PrimitiveTypeDescription? { 20 | guard isIdentifiable else { return nil } 21 | guard let field = fields.first(where: { $0.name == "id" }) else { return nil } 22 | return field.description as? PrimitiveTypeDescription 23 | } 24 | 25 | public var transitiveTypeDescriptions: Array { 26 | return fields.map(\.description) 27 | } 28 | 29 | init(_ type: S.Type) throws(StorageError) { 30 | throw .storedTypeMustBeValueType(S.self) 31 | } 32 | 33 | init(_ type: S.Type) throws(StorageError) { 34 | self.baseType = type 35 | self.name = "\(S.self)" 36 | self.fields = try S.storedFields 37 | self.fieldsByKeyPath = Dictionary(uniqueKeysWithValues: fields.map { ($0.keyPath, $0) }) 38 | self.fieldsByName = Dictionary(uniqueKeysWithValues: fields.map { ($0.name, $0) }) 39 | 40 | if fields.isEmpty { throw .storedTypeIsEmpty(S.self) } 41 | try validateIdentifiable() 42 | for field in fields { 43 | try field.validate(on: baseType) 44 | } 45 | } 46 | 47 | private func validateIdentifiable() throws(StorageError) { 48 | let isIdentifiable = (baseType is any Identifiable.Type) 49 | if let field = fields.first(where: { $0.name == "id" }) { 50 | guard isIdentifiable else { 51 | // a stored type with an "id" field must be Identifiable 52 | throw .storedTypeIsNotIdentifiable(baseType) 53 | } 54 | 55 | if field.description.isOptional { throw .identifierCannotBeOptional(baseType) } 56 | guard field.description is PrimitiveTypeDescription else { throw .identifierMustBePrimitive(baseType) } 57 | 58 | } else if isIdentifiable { 59 | throw .storedTypeMissingIdentifier(baseType) 60 | } 61 | } 62 | } 63 | 64 | extension StoredField { 65 | 66 | func validate(on baseType: any StoredType.Type) throws(StorageError) { 67 | 68 | if name.lowercased() == "rowid" { 69 | throw .invalidFieldName(baseType, name, keyPath) 70 | } 71 | 72 | if let opt = description as? OptionalTypeDescription { 73 | if opt.wrappedType is MultiValueTypeDescription { 74 | throw .multiValueFieldsCannotBeOptional(baseType, self) 75 | } 76 | 77 | if opt.wrappedType is OptionalTypeDescription { 78 | throw .optionalFieldCannotNestOptional(baseType, self) 79 | } 80 | } 81 | 82 | if let multi = description as? MultiValueTypeDescription { 83 | // disallow eg Arrays of Arrays 84 | if multi.valueType is MultiValueTypeDescription { 85 | throw .multiValueFieldsCannotBeNested(baseType, self) 86 | } 87 | if multi.valueType is OptionalTypeDescription { 88 | throw .multiValueFieldsCannotNestOptionals(baseType, self) 89 | } 90 | 91 | if let key = multi.keyType { 92 | // dictionary field 93 | 94 | if key is OptionalTypeDescription { 95 | throw .dictionaryKeyCannotBeOptional(baseType, self) 96 | } 97 | 98 | if (key is PrimitiveTypeDescription) == false { 99 | throw .dictionaryKeyMustBePrimitive(baseType, self) 100 | } 101 | } 102 | 103 | } 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /Sources/ORM/StoredTypes/TypeDescriptions/MultiValueTypeDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultiValueTypeDescription.swift 3 | // ORM 4 | // 5 | // Created by Dave DeLong on 3/15/25. 6 | // 7 | 8 | public struct MultiValueTypeDescription: StoredTypeDescription { 9 | public var baseType: any StoredType.Type 10 | public var isOrdered: Bool 11 | public var keyType: StoredTypeDescription? 12 | public var valueType: StoredTypeDescription 13 | 14 | public var transitiveTypeDescriptions: Array { 15 | if let keyType { return [keyType, valueType] } 16 | return [valueType] 17 | } 18 | } 19 | 20 | extension Array: StoredType where Element: StoredType { 21 | public static var storedTypeDescription: any StoredTypeDescription { 22 | get throws(StorageError) { 23 | return MultiValueTypeDescription(baseType: Self.self, 24 | isOrdered: true, 25 | valueType: try Element.storedTypeDescription) 26 | } 27 | } 28 | } 29 | 30 | extension Set: StoredType where Element: StoredType { 31 | public static var storedTypeDescription: any StoredTypeDescription { 32 | get throws(StorageError) { 33 | return MultiValueTypeDescription(baseType: Self.self, 34 | isOrdered: false, 35 | valueType: try Element.storedTypeDescription) 36 | } 37 | } 38 | } 39 | 40 | extension Dictionary: StoredType where Key: StoredType, Value: StoredType { 41 | public static var storedTypeDescription: any StoredTypeDescription { 42 | get throws(StorageError) { 43 | return MultiValueTypeDescription(baseType: Self.self, 44 | isOrdered: false, 45 | keyType: try Key.storedTypeDescription, 46 | valueType: try Value.storedTypeDescription) 47 | } 48 | } 49 | } 50 | 51 | internal struct SetTableSchema: StoredType { 52 | var parent: P 53 | var value: T 54 | var processed: Bool 55 | } 56 | 57 | internal struct ArrayTableSchema: StoredType { 58 | var parent: P 59 | var value: T 60 | var order: Int 61 | var processed: Bool 62 | } 63 | 64 | internal struct MapTableSchema: StoredType { 65 | var parent: P 66 | var key: K 67 | var value: T 68 | var processed: Bool 69 | } 70 | -------------------------------------------------------------------------------- /Sources/ORM/StoredTypes/TypeDescriptions/PrimitiveTypeDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrimitiveTypeDescription.swift 3 | // ORM 4 | // 5 | // Created by Dave DeLong on 3/15/25. 6 | // 7 | 8 | import Foundation 9 | import SQLiteSyntax 10 | 11 | public struct PrimitiveTypeDescription: StoredTypeDescription { 12 | public var baseType: any StoredType.Type 13 | public var transitiveTypeDescriptions: Array { [] } 14 | 15 | internal let sqliteTypeName: TypeName 16 | 17 | init(_ type: T.Type, typeName: TypeName) { 18 | baseType = T.self 19 | sqliteTypeName = typeName 20 | } 21 | } 22 | 23 | struct OptionalTypeDescription: StoredTypeDescription { 24 | var baseType: any StoredType.Type 25 | var wrappedType: any StoredTypeDescription 26 | var transitiveTypeDescriptions: Array { [wrappedType] } 27 | 28 | init(_ type: Optional.Type) throws(StorageError) { 29 | baseType = Optional.self 30 | wrappedType = try T.storedTypeDescription 31 | } 32 | } 33 | 34 | extension Character: StoredType { 35 | static public var storedTypeDescription: any StoredTypeDescription { 36 | PrimitiveTypeDescription(Self.self, typeName: .text) 37 | } 38 | } 39 | 40 | extension String: StoredType { 41 | static public var storedTypeDescription: any StoredTypeDescription { 42 | PrimitiveTypeDescription(Self.self, typeName: .text) 43 | } 44 | } 45 | 46 | extension UUID: StoredType { 47 | static public var storedTypeDescription: any StoredTypeDescription { 48 | PrimitiveTypeDescription(Self.self, typeName: .text) 49 | } 50 | } 51 | 52 | extension Bool: StoredType { 53 | static public var storedTypeDescription: any StoredTypeDescription { 54 | PrimitiveTypeDescription(Self.self, typeName: .integer) 55 | } 56 | } 57 | 58 | extension Int: StoredType { 59 | static public var storedTypeDescription: any StoredTypeDescription { 60 | PrimitiveTypeDescription(Self.self, typeName: .integer) 61 | } 62 | } 63 | 64 | extension Int8: StoredType { 65 | static public var storedTypeDescription: any StoredTypeDescription { 66 | PrimitiveTypeDescription(Self.self, typeName: .integer) 67 | } 68 | } 69 | 70 | extension Int16: StoredType { 71 | static public var storedTypeDescription: any StoredTypeDescription { 72 | PrimitiveTypeDescription(Self.self, typeName: .integer) 73 | } 74 | } 75 | 76 | extension Int32: StoredType { 77 | static public var storedTypeDescription: any StoredTypeDescription { 78 | PrimitiveTypeDescription(Self.self, typeName: .integer) 79 | } 80 | } 81 | 82 | extension Int64: StoredType { 83 | static public var storedTypeDescription: any StoredTypeDescription { 84 | PrimitiveTypeDescription(Self.self, typeName: .integer) 85 | } 86 | } 87 | 88 | extension UInt: StoredType { 89 | static public var storedTypeDescription: any StoredTypeDescription { 90 | PrimitiveTypeDescription(Self.self, typeName: .integer) 91 | } 92 | } 93 | 94 | extension UInt8: StoredType { 95 | static public var storedTypeDescription: any StoredTypeDescription { 96 | PrimitiveTypeDescription(Self.self, typeName: .integer) 97 | } 98 | } 99 | 100 | extension UInt16: StoredType { 101 | static public var storedTypeDescription: any StoredTypeDescription { 102 | PrimitiveTypeDescription(Self.self, typeName: .integer) 103 | } 104 | } 105 | 106 | extension UInt32: StoredType { 107 | static public var storedTypeDescription: any StoredTypeDescription { 108 | PrimitiveTypeDescription(Self.self, typeName: .integer) 109 | } 110 | } 111 | 112 | extension UInt64: StoredType { 113 | static public var storedTypeDescription: any StoredTypeDescription { 114 | PrimitiveTypeDescription(Self.self, typeName: .integer) 115 | } 116 | } 117 | 118 | extension Float: StoredType { 119 | static public var storedTypeDescription: any StoredTypeDescription { 120 | PrimitiveTypeDescription(Self.self, typeName: .real) 121 | } 122 | } 123 | 124 | extension Double: StoredType { 125 | static public var storedTypeDescription: any StoredTypeDescription { 126 | PrimitiveTypeDescription(Self.self, typeName: .real) 127 | } 128 | } 129 | 130 | extension Decimal: StoredType { 131 | static public var storedTypeDescription: any StoredTypeDescription { 132 | PrimitiveTypeDescription(Self.self, typeName: .numeric) 133 | } 134 | } 135 | 136 | extension Date: StoredType { 137 | static public var storedTypeDescription: any StoredTypeDescription { 138 | PrimitiveTypeDescription(Self.self, typeName: .text) 139 | } 140 | } 141 | 142 | extension Data: StoredType { 143 | static public var storedTypeDescription: any StoredTypeDescription { 144 | PrimitiveTypeDescription(Self.self, typeName: .blob) 145 | } 146 | } 147 | 148 | extension RawRepresentable { 149 | 150 | static internal var storedTypeDescription: (any StoredTypeDescription)? { 151 | get throws(StorageError) { 152 | guard let storedRawType = RawValue.self as? any StoredType.Type else { 153 | return nil 154 | } 155 | return try storedRawType.storedTypeDescription 156 | } 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /Sources/ORM/StoredTypes/TypeDescriptions/StoredTypeDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 1/9/25. 6 | // 7 | 8 | import Foundation 9 | import SQLite 10 | 11 | public protocol StoredTypeDescription { 12 | var baseType: any StoredType.Type { get } 13 | var transitiveTypeDescriptions: Array { get } 14 | } 15 | 16 | extension StoredTypeDescription { 17 | 18 | internal var isIdentifiable: Bool { baseType is any Identifiable.Type } 19 | internal var isOptional: Bool { baseType is any OptionalType.Type } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Sources/debug/File.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // ORM 4 | // 5 | // Created by Dave DeLong on 4/1/25. 6 | // 7 | 8 | import Foundation 9 | import ORM 10 | 11 | @dynamicMemberLookup 12 | struct Snapshot { 13 | 14 | subscript(dynamicMember keyPath: KeyPath) -> V { 15 | fatalError() 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Sources/debug/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Dave DeLong on 1/7/25. 6 | // 7 | 8 | import Foundation 9 | import ORM 10 | 11 | struct T1: StoredType, Identifiable { 12 | var id: UUID 13 | var name: String 14 | var t2: T2? 15 | } 16 | 17 | struct T2: StoredType { 18 | var age: Int 19 | } 20 | 21 | struct Meetup: StoredType, Identifiable { 22 | var id: UUID 23 | var name: String 24 | 25 | var startDate: Date 26 | var attendees: Array 27 | } 28 | 29 | struct Person: StoredType, Identifiable { 30 | var id: UUID 31 | var firstName: String 32 | var lastName: String 33 | var emails: Array 34 | 35 | var settings: Dictionary 36 | } 37 | 38 | let schema = try Schema(Meetup.self) 39 | print(schema.compositeTypes) 40 | 41 | let url = URL(filePath: "/Users/dave/Desktop/schema.sql") 42 | let engine = try await CoreDataEngine(schema: schema, at: url) 43 | -------------------------------------------------------------------------------- /Tests/ORMTests/CoreDataEngineTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataEngineTests.swift 3 | // ORM 4 | // 5 | // Created by Dave DeLong on 3/16/25. 6 | // 7 | 8 | @testable import ORM 9 | import Testing 10 | 11 | #if canImport(CoreData) 12 | import CoreData 13 | 14 | @Suite(.serialized) 15 | struct CoreDataEngineTests { 16 | 17 | func temporaryURL(_ caller: StaticString = #function) -> URL { 18 | let name = "\(Self.self)-\(caller.description).db" 19 | return URL.temporaryDirectory.appending(component: name) 20 | } 21 | 22 | @Test func singlePropertyEntity() async throws { 23 | struct S: StoredType { 24 | var name: String 25 | } 26 | 27 | let e = try await CoreDataEngine(S.self, at: temporaryURL()) 28 | let model = await e.model 29 | let sEntity = try #require(model.entities.first) 30 | #expect(sEntity.name == "S") 31 | #expect(sEntity.properties.count == 1) 32 | #expect(sEntity.uniquenessConstraints.isEmpty) 33 | 34 | let nameAttr = try #require(sEntity.attributesByName["name"]) 35 | #expect(nameAttr.type == .string) 36 | #expect(nameAttr.isOptional == false) 37 | } 38 | 39 | @Test func singlePropertyUniqueEntity() async throws { 40 | struct S: StoredType, Identifiable { 41 | var id: String 42 | } 43 | 44 | let e = try await CoreDataEngine(S.self, at: temporaryURL()) 45 | let model = await e.model 46 | let sEntity = try #require(model.entities.first) 47 | #expect(sEntity.name == "S") 48 | #expect(sEntity.properties.count == 1) 49 | 50 | let nameAttr = try #require(sEntity.attributesByName["id"]) 51 | #expect(nameAttr.type == .string) 52 | #expect(nameAttr.isOptional == false) 53 | 54 | let constraints = try #require(sEntity.uniquenessConstraints as? Array<[String]>) 55 | #expect(constraints == [["id"]]) 56 | } 57 | 58 | @Test func multipleSimpleEntities() async throws { 59 | struct A: StoredType, Identifiable { 60 | var id: String 61 | var value: Data? 62 | } 63 | 64 | struct B: StoredType { 65 | var timestamp: Date 66 | var message: String 67 | } 68 | 69 | let e = try await CoreDataEngine(A.self, B.self, at: temporaryURL()) 70 | 71 | let a = try #require(await e.model.entitiesByName["A"]) 72 | let aUnique = try #require(a.uniquenessConstraints as? Array<[String]>) 73 | #expect(aUnique == [["id"]]) 74 | 75 | let aID = try #require(a.attributesByName["id"]) 76 | #expect(aID.attributeType == .stringAttributeType) 77 | #expect(aID.isOptional == false) 78 | 79 | let aValue = try #require(a.attributesByName["value"]) 80 | #expect(aValue.attributeType == .binaryDataAttributeType) 81 | #expect(aValue.isOptional == true) 82 | 83 | let b = try #require(await e.model.entitiesByName["B"]) 84 | #expect(b.uniquenessConstraints.isEmpty) 85 | 86 | let bTimestamp = try #require(b.attributesByName["timestamp"]) 87 | #expect(bTimestamp.attributeType == .dateAttributeType) 88 | #expect(bTimestamp.isOptional == false) 89 | 90 | let bMessage = try #require(b.attributesByName["message"]) 91 | #expect(bMessage.attributeType == .stringAttributeType) 92 | #expect(bMessage.isOptional == false) 93 | } 94 | 95 | @Test func requiredToOneRelationship_UnidentifiedToUnidentified() async throws { 96 | struct A: StoredType { 97 | var b: B 98 | } 99 | 100 | struct B: StoredType { 101 | var name: String 102 | } 103 | 104 | let e = try await CoreDataEngine(A.self, at: temporaryURL()) 105 | 106 | let a = try #require(await e.model.entitiesByName["A"]) 107 | let _ = try #require(await e.model.entitiesByName["B"]) 108 | 109 | let aB = try #require(a.relationshipsByName["b"]) 110 | #expect(aB.isOrdered == false) 111 | #expect(aB.isToMany == false) 112 | #expect(aB.isOptional == false) 113 | #expect(aB.deleteRule == .nullifyDeleteRule) 114 | 115 | let aBInverse = try #require(aB.inverseRelationship) 116 | #expect(aBInverse.name == "A_b") 117 | #expect(aBInverse.isOrdered == false) 118 | #expect(aBInverse.isToMany == false) 119 | #expect(aBInverse.deleteRule == .cascadeDeleteRule) 120 | } 121 | 122 | @Test func requiredToOneRelationship_IdentifiedToUnidentified() async throws { 123 | struct A: StoredType, Identifiable { 124 | var id: String 125 | var b: B 126 | } 127 | 128 | struct B: StoredType { 129 | var name: String 130 | } 131 | 132 | let e = try await CoreDataEngine(A.self, at: temporaryURL()) 133 | 134 | let a = try #require(await e.model.entitiesByName["A"]) 135 | let _ = try #require(await e.model.entitiesByName["B"]) 136 | 137 | let aB = try #require(a.relationshipsByName["b"]) 138 | #expect(aB.isOrdered == false) 139 | #expect(aB.isToMany == false) 140 | #expect(aB.isOptional == false) 141 | #expect(aB.deleteRule == .nullifyDeleteRule) 142 | 143 | let aBInverse = try #require(aB.inverseRelationship) 144 | #expect(aBInverse.name == "A_b") 145 | #expect(aBInverse.isOrdered == false) 146 | #expect(aBInverse.isToMany == false) 147 | #expect(aBInverse.deleteRule == .denyDeleteRule) 148 | } 149 | 150 | @Test func requiredToOneRelationship_IdentifiedToIdentified() async throws { 151 | struct A: StoredType, Identifiable { 152 | var id: String 153 | var b: B 154 | } 155 | 156 | struct B: StoredType, Identifiable { 157 | var id: String 158 | var name: String 159 | } 160 | 161 | let e = try await CoreDataEngine(A.self, at: temporaryURL()) 162 | 163 | let a = try #require(await e.model.entitiesByName["A"]) 164 | let _ = try #require(await e.model.entitiesByName["B"]) 165 | 166 | let aB = try #require(a.relationshipsByName["b"]) 167 | #expect(aB.isOrdered == false) 168 | #expect(aB.isToMany == false) 169 | #expect(aB.isOptional == false) 170 | #expect(aB.deleteRule == .nullifyDeleteRule) 171 | 172 | let aBInverse = try #require(aB.inverseRelationship) 173 | #expect(aBInverse.name == "A_b") 174 | #expect(aBInverse.isOrdered == false) 175 | #expect(aBInverse.isToMany == true) 176 | #expect(aBInverse.deleteRule == .denyDeleteRule) 177 | } 178 | 179 | @Test func requiredToOneRelationship_UnidentifiedToIdentified() async throws { 180 | struct A: StoredType { 181 | var b: B 182 | } 183 | 184 | struct B: StoredType, Identifiable { 185 | var id: String 186 | var name: String 187 | } 188 | 189 | let e = try await CoreDataEngine(A.self, at: temporaryURL()) 190 | 191 | let a = try #require(await e.model.entitiesByName["A"]) 192 | let _ = try #require(await e.model.entitiesByName["B"]) 193 | 194 | let aB = try #require(a.relationshipsByName["b"]) 195 | #expect(aB.isOrdered == false) 196 | #expect(aB.isToMany == false) 197 | #expect(aB.isOptional == false) 198 | #expect(aB.deleteRule == .nullifyDeleteRule) 199 | 200 | let aBInverse = try #require(aB.inverseRelationship) 201 | #expect(aBInverse.name == "A_b") 202 | #expect(aBInverse.isOrdered == false) 203 | #expect(aBInverse.isToMany == true) 204 | #expect(aBInverse.deleteRule == .cascadeDeleteRule) 205 | } 206 | 207 | @Test func optionalToOneRelationship_UnidentifiedToUnidentified() async throws { 208 | struct A: StoredType { 209 | var b: B? 210 | } 211 | 212 | struct B: StoredType { 213 | var name: String 214 | } 215 | 216 | let e = try await CoreDataEngine(A.self, at: temporaryURL()) 217 | 218 | let a = try #require(await e.model.entitiesByName["A"]) 219 | let _ = try #require(await e.model.entitiesByName["B"]) 220 | 221 | let aB = try #require(a.relationshipsByName["b"]) 222 | #expect(aB.isOrdered == false) 223 | #expect(aB.isToMany == false) 224 | #expect(aB.isOptional == true) 225 | #expect(aB.deleteRule == .nullifyDeleteRule) 226 | 227 | let aBInverse = try #require(aB.inverseRelationship) 228 | #expect(aBInverse.name == "A_b") 229 | #expect(aBInverse.isOrdered == false) 230 | #expect(aBInverse.isToMany == false) 231 | #expect(aBInverse.deleteRule == .nullifyDeleteRule) 232 | } 233 | 234 | @Test func optionalToOneRelationship_IdentifiedToUnidentified() async throws { 235 | struct A: StoredType, Identifiable { 236 | var id: String 237 | var b: B? 238 | } 239 | 240 | struct B: StoredType { 241 | var name: String 242 | } 243 | 244 | let e = try await CoreDataEngine(A.self, at: temporaryURL()) 245 | 246 | let a = try #require(await e.model.entitiesByName["A"]) 247 | let _ = try #require(await e.model.entitiesByName["B"]) 248 | 249 | let aB = try #require(a.relationshipsByName["b"]) 250 | #expect(aB.isOrdered == false) 251 | #expect(aB.isToMany == false) 252 | #expect(aB.isOptional == true) 253 | #expect(aB.deleteRule == .nullifyDeleteRule) 254 | 255 | let aBInverse = try #require(aB.inverseRelationship) 256 | #expect(aBInverse.name == "A_b") 257 | #expect(aBInverse.isOrdered == false) 258 | #expect(aBInverse.isToMany == false) 259 | #expect(aBInverse.deleteRule == .nullifyDeleteRule) 260 | } 261 | 262 | @Test func optionalToOneRelationship_IdentifiedToIdentified() async throws { 263 | struct A: StoredType, Identifiable { 264 | var id: String 265 | var b: B? 266 | } 267 | 268 | struct B: StoredType, Identifiable { 269 | var id: String 270 | var name: String 271 | } 272 | 273 | let e = try await CoreDataEngine(A.self, at: temporaryURL()) 274 | 275 | let a = try #require(await e.model.entitiesByName["A"]) 276 | let _ = try #require(await e.model.entitiesByName["B"]) 277 | 278 | let aB = try #require(a.relationshipsByName["b"]) 279 | #expect(aB.isOrdered == false) 280 | #expect(aB.isToMany == false) 281 | #expect(aB.isOptional == true) 282 | #expect(aB.deleteRule == .nullifyDeleteRule) 283 | 284 | let aBInverse = try #require(aB.inverseRelationship) 285 | #expect(aBInverse.name == "A_b") 286 | #expect(aBInverse.isOrdered == false) 287 | #expect(aBInverse.isToMany == true) 288 | #expect(aBInverse.deleteRule == .nullifyDeleteRule) 289 | } 290 | 291 | @Test func optionalToOneRelationship_UnidentifiedToIdentified() async throws { 292 | struct A: StoredType { 293 | var b: B? 294 | } 295 | 296 | struct B: StoredType, Identifiable { 297 | var id: String 298 | var name: String 299 | } 300 | 301 | let e = try await CoreDataEngine(A.self, at: temporaryURL()) 302 | 303 | let a = try #require(await e.model.entitiesByName["A"]) 304 | let _ = try #require(await e.model.entitiesByName["B"]) 305 | 306 | let aB = try #require(a.relationshipsByName["b"]) 307 | #expect(aB.isOrdered == false) 308 | #expect(aB.isToMany == false) 309 | #expect(aB.isOptional == true) 310 | #expect(aB.deleteRule == .nullifyDeleteRule) 311 | 312 | let aBInverse = try #require(aB.inverseRelationship) 313 | #expect(aBInverse.name == "A_b") 314 | #expect(aBInverse.isOrdered == false) 315 | #expect(aBInverse.isToMany == true) 316 | #expect(aBInverse.deleteRule == .nullifyDeleteRule) 317 | } 318 | 319 | @Test func toManyRelationship_UnidentifiedToUnidentified() async throws { 320 | struct A: StoredType { 321 | var b: Array 322 | } 323 | struct B: StoredType { 324 | var name: String 325 | } 326 | 327 | let e = try await CoreDataEngine(A.self, at: temporaryURL()) 328 | 329 | let a = try #require(await e.model.entitiesByName["A"]) 330 | let b = try #require(await e.model.entitiesByName["B"]) 331 | 332 | let aB = try #require(a.relationshipsByName["b"]) 333 | #expect(aB.destinationEntity == b) 334 | #expect(aB.isOrdered == true) 335 | #expect(aB.isToMany == true) 336 | #expect(aB.deleteRule == .nullifyDeleteRule) 337 | 338 | let aBInverse = try #require(b.relationshipsByName["A_b"]) 339 | #expect(aBInverse.destinationEntity == a) 340 | #expect(aBInverse.inverseRelationship == aB) 341 | #expect(aBInverse.isToMany == false) 342 | #expect(aBInverse.deleteRule == .nullifyDeleteRule) 343 | } 344 | 345 | @Test func toManyRelationship_UnidentifiedToIdentified() async throws { 346 | struct A: StoredType { 347 | var b: Array 348 | } 349 | struct B: StoredType, Identifiable { 350 | var id: String 351 | } 352 | 353 | let e = try await CoreDataEngine(A.self, at: temporaryURL()) 354 | 355 | let a = try #require(await e.model.entitiesByName["A"]) 356 | let b = try #require(await e.model.entitiesByName["B"]) 357 | 358 | let aB = try #require(a.relationshipsByName["b"]) 359 | #expect(aB.destinationEntity == b) 360 | #expect(aB.isOrdered == true) 361 | #expect(aB.isToMany == true) 362 | #expect(aB.deleteRule == .nullifyDeleteRule) 363 | 364 | let aBInverse = try #require(b.relationshipsByName["A_b"]) 365 | #expect(aBInverse.destinationEntity == a) 366 | #expect(aBInverse.inverseRelationship == aB) 367 | #expect(aBInverse.isToMany == true) 368 | #expect(aBInverse.deleteRule == .nullifyDeleteRule) 369 | } 370 | } 371 | 372 | #endif 373 | -------------------------------------------------------------------------------- /Tests/ORMTests/SQLiteEngineTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // ORM 4 | // 5 | // Created by Dave DeLong on 4/29/25. 6 | // 7 | 8 | @testable import ORM 9 | import Testing 10 | import Foundation 11 | 12 | @Suite(.serialized) 13 | struct SQLiteEngineTests { 14 | 15 | func temporaryURL(_ caller: StaticString = #function) -> URL { 16 | let name = "\(Self.self)-\(caller.description).db" 17 | return URL.temporaryDirectory.appending(component: name) 18 | } 19 | 20 | @Test func simpleStruct() async throws { 21 | struct Simple: StoredType { 22 | var name: String 23 | } 24 | 25 | let url = temporaryURL() 26 | let _ = try await SQLiteEngine(Simple.self, at: url) 27 | print(url) 28 | } 29 | 30 | @Test func simpleIdentifiableStruct() async throws { 31 | 32 | struct Simple: Identifiable, StoredType { 33 | var id: UUID 34 | var name: String 35 | var age: Int 36 | var registrationDate: Date 37 | } 38 | 39 | let url = temporaryURL() 40 | let _ = try await SQLiteEngine(Simple.self, at: url) 41 | print(url) 42 | } 43 | 44 | @Test func simpleIdentifiableWithOptionalsStruct() async throws { 45 | 46 | struct Simple: Identifiable, StoredType { 47 | var id: UUID 48 | var name: String? 49 | var age: Int? 50 | var registrationDate: Date 51 | } 52 | 53 | let url = temporaryURL() 54 | let _ = try await SQLiteEngine(Simple.self, at: url) 55 | print(url) 56 | } 57 | 58 | @Test func simpleStructWithArray() async throws { 59 | struct S: StoredType { 60 | var name: String 61 | var emails: Array 62 | } 63 | 64 | let _ = try await SQLiteEngine(S.self, at: temporaryURL()) 65 | } 66 | 67 | @Test func relationshipBetweenIdentifiables() async throws { 68 | struct A: Identifiable, StoredType { 69 | let id: UUID 70 | var name: String 71 | } 72 | 73 | struct B: Identifiable, StoredType { 74 | let id: UUID 75 | var name: String 76 | var a: A? 77 | } 78 | 79 | let _ = try await SQLiteEngine(B.self, at: temporaryURL()) 80 | } 81 | 82 | @Test func saveSimpleValue() async throws { 83 | struct A: Identifiable, StoredType { 84 | let id: String 85 | let name: String 86 | } 87 | 88 | let e = try await SQLiteEngine(A.self, at: temporaryURL()) 89 | try await e.save(A(id: "test", name: "Arthur")) 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /Tests/ORMTests/StorageDescriptionTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | @testable import ORM 3 | 4 | 5 | struct SchemaTests { 6 | 7 | @Test func storedTypeCannotBeAClass() { 8 | class S: StoredType { } 9 | #expect(throws: StorageError.storedTypeMustBeValueType(S.self)) { try Schema(S.self) } 10 | } 11 | 12 | @Test func storedTypeCannotBeEmpty() { 13 | struct S: StoredType { } 14 | #expect(throws: StorageError.storedTypeIsEmpty(S.self)) { try Schema(S.self) } 15 | } 16 | 17 | @Test func storedTypeCannotHaveOptionalIdentifier() { 18 | struct S: StoredType, Identifiable { 19 | var id: String? 20 | } 21 | 22 | #expect(throws: StorageError.identifierCannotBeOptional(S.self)) { try Schema(S.self) } 23 | } 24 | 25 | @Test func storedTypeCannotHaveComplexIdentifier() { 26 | struct S: StoredType, Identifiable { 27 | var id: Array 28 | } 29 | 30 | #expect(throws: StorageError.identifierMustBePrimitive(S.self)) { try Schema(S.self) } 31 | } 32 | 33 | @Test func storedTypeCannotHaveComputedIdentifier() { 34 | struct S: StoredType, Identifiable { 35 | var id: String { name } 36 | var name: String 37 | } 38 | 39 | #expect(throws: StorageError.storedTypeMissingIdentifier(S.self)) { try Schema(S.self) } 40 | } 41 | 42 | @Test func storedTypeCannotHaveNonStorableFields() { 43 | struct I { } 44 | struct S: StoredType { 45 | var i: I 46 | } 47 | 48 | #expect(throws: StorageError.unknownFieldType(S.self, "i", \S.i, I.self)) { try Schema(S.self) } 49 | } 50 | 51 | @Test func storedTypeCanHaveRawRepresentableID() throws { 52 | enum ID: String { case foo } 53 | struct S: StoredType, Identifiable { 54 | var id: ID 55 | } 56 | 57 | _ = try Schema(S.self) 58 | } 59 | 60 | @Test func storedTypeCannotHaveComplexRawRepresentableID() throws { 61 | struct ID: RawRepresentable, Hashable { var rawValue: Array } 62 | struct S: StoredType, Identifiable { 63 | var id: ID 64 | } 65 | 66 | #expect(throws: StorageError.identifierMustBePrimitive(S.self)) { try Schema(S.self) } 67 | } 68 | 69 | @Test func schemaIgnoresDuplicateDeclaredTypes() throws { 70 | struct S: StoredType { 71 | var name: String 72 | } 73 | 74 | let s = try Schema(S.self, S.self) 75 | try #require(s.compositeTypes.count == 1) 76 | } 77 | 78 | @Test func schemaFollowsTransitiveDeclaredTypes() throws { 79 | struct S: StoredType { 80 | var t: T? 81 | } 82 | struct T: StoredType { 83 | var name: String 84 | } 85 | 86 | let s = try Schema(S.self) 87 | try #require(s.compositeTypes.count == 2) 88 | } 89 | 90 | @Test func schemaIgnoresTransitiveDuplicateTypes() throws { 91 | struct A: StoredType { 92 | var c: C 93 | } 94 | struct B: StoredType { 95 | var c: C 96 | } 97 | struct C: StoredType { 98 | var name: String 99 | } 100 | 101 | let s = try Schema(A.self, B.self) 102 | try #require(s.compositeTypes.count == 3) 103 | } 104 | 105 | @Test func storedDictionaryCannotHaveOptionalKey() throws { 106 | struct A: StoredType { 107 | var map: Dictionary 108 | } 109 | 110 | #expect(throws: StorageError.dictionaryKeyCannotBeOptional(A.self, try A.Fields.map)) { try Schema(A.self) } 111 | } 112 | 113 | @Test func storedDictionaryCannotHaveComplexKey() throws { 114 | struct A: StoredType { 115 | var map: Dictionary 116 | } 117 | struct B: StoredType, Hashable { 118 | var value: Int 119 | } 120 | 121 | #expect(throws: StorageError.dictionaryKeyMustBePrimitive(A.self, try A.Fields.map)) { try Schema(A.self) } 122 | } 123 | 124 | @Test func multiValueFieldsCannotBeOptional() throws { 125 | struct A: StoredType { 126 | var ints: Array? 127 | } 128 | 129 | #expect(throws: StorageError.multiValueFieldsCannotBeOptional(A.self, try A.Fields.ints)) { try Schema(A.self) } 130 | } 131 | 132 | @Test func multiValueFieldsCannotBeNested() throws { 133 | struct A: StoredType { 134 | var names: Array> 135 | } 136 | 137 | #expect(throws: StorageError.multiValueFieldsCannotBeNested(A.self, try A.Fields.names)) { try Schema(A.self) } 138 | } 139 | 140 | @Test func dictionaryValuesCannotBeNested() throws { 141 | struct A: StoredType { 142 | var names: Dictionary> 143 | } 144 | 145 | #expect(throws: StorageError.multiValueFieldsCannotBeNested(A.self, try A.Fields.names)) { try Schema(A.self) } 146 | } 147 | 148 | @Test func noDoubleOptionals() throws { 149 | struct A: StoredType { 150 | var name: String?? 151 | } 152 | 153 | #expect(throws: StorageError.optionalFieldCannotNestOptional(A.self, try A.Fields.name)) { try Schema(A.self) } 154 | } 155 | 156 | @Test func noPropertyNamedRowID() throws { 157 | struct A: StoredType { 158 | var rowid: Int 159 | } 160 | 161 | #expect(throws: StorageError.invalidFieldName(A.self, "rowid", \A.rowid)) { try Schema(A.self) } 162 | } 163 | } 164 | --------------------------------------------------------------------------------