diff --git a/Sources/ZeeQL/Access/AccessDataSourceFinders.swift b/Sources/ZeeQL/Access/AccessDataSourceFinders.swift index 6f959d9..58e43a4 100644 --- a/Sources/ZeeQL/Access/AccessDataSourceFinders.swift +++ b/Sources/ZeeQL/Access/AccessDataSourceFinders.swift @@ -114,7 +114,8 @@ public extension AccessDataSource { // GIDs func fetchObjects(with globalIDs: S, yield: ( Object ) throws -> Void) throws - where S.Element : GlobalID + //where S.Element : GlobalID + where S.Element == KeyGlobalID { guard let entity = entity else { throw AccessDataSourceError.MissingEntity } let gidQualifiers = globalIDs.map { entity.qualifierForGlobalID($0) } @@ -123,14 +124,16 @@ public extension AccessDataSource { // GIDs try fetchObjects(fs, cb: yield) } func fetchObjects(with globalIDs: S) throws -> [ Object ] - where S.Element : GlobalID + //where S.Element : GlobalID + where S.Element == KeyGlobalID { var objects = [ Object ]() try fetchObjects(with: globalIDs) { objects.append($0) } return objects } func fetchObjects(with globalIDs: S) throws -> [ Object ] - where S.Element : GlobalID + //where S.Element : GlobalID + where S.Element == KeyGlobalID { var objects = [ Object ]() objects.reserveCapacity(globalIDs.count) diff --git a/Sources/ZeeQL/Access/Entity.swift b/Sources/ZeeQL/Access/Entity.swift index 61f1eae..1bb115e 100644 --- a/Sources/ZeeQL/Access/Entity.swift +++ b/Sources/ZeeQL/Access/Entity.swift @@ -93,6 +93,7 @@ public extension Entity { // default imp return lookupPrimaryKeyAttributeNames() } + @inlinable func lookupPrimaryKeyAttributeNames() -> [ String ]? { // FancyModelMaker also has a `assignPrimaryKeyIfMissing` guard !attributes.isEmpty else { return nil } @@ -127,6 +128,7 @@ public extension Entity { // default imp // MARK: - GlobalIDs + @inlinable func globalIDForRow(_ row: AdaptorRecord?) -> GlobalID? { guard let row = row else { return nil } guard let pkeys = primaryKeyAttributeNames, !pkeys.isEmpty @@ -135,6 +137,7 @@ public extension Entity { // default imp let pkeyValues = pkeys.map { row[$0] } return KeyGlobalID.make(entityName: name, values: pkeyValues) } + @inlinable func globalIDForRow(_ row: AdaptorRow?) -> GlobalID? { guard let row = row else { return nil } guard let pkeys = primaryKeyAttributeNames, !pkeys.isEmpty @@ -143,6 +146,7 @@ public extension Entity { // default imp let pkeyValues = pkeys.map { row[$0] ?? nil } return KeyGlobalID.make(entityName: name, values: pkeyValues) } + @inlinable func globalIDForRow(_ row: Any?) -> GlobalID? { guard let row = row else { return nil } guard let pkeys = primaryKeyAttributeNames, !pkeys.isEmpty @@ -154,13 +158,18 @@ public extension Entity { // default imp return KeyGlobalID.make(entityName: name, values: pkeyValues) } + @inlinable func qualifierForGlobalID(_ globalID: GlobalID) -> Qualifier { + #if !GLOBALID_AS_OPEN_CLASS + let kglobalID = globalID + #else guard let kglobalID = globalID as? KeyGlobalID else { globalZeeQLLogger.warn("globalID is not a KeyGlobalID:", globalID, type(of: globalID)) assertionFailure("attempt to use an unsupported globalID \(globalID)") return BooleanQualifier.falseQualifier } + #endif guard kglobalID.keyCount != 0 else { globalZeeQLLogger.warn("globalID w/o keys:", globalID) assertionFailure("globalID w/o keys: \(globalID)") diff --git a/Sources/ZeeQL/Control/GlobalID.swift b/Sources/ZeeQL/Control/GlobalID.swift index eeb1e4f..7ac0a2c 100644 --- a/Sources/ZeeQL/Control/GlobalID.swift +++ b/Sources/ZeeQL/Control/GlobalID.swift @@ -6,14 +6,199 @@ // Copyright © 2017-2024 ZeeZide GmbH. All rights reserved. // -// FIXME(hh 2024-11-19): Those should not be classes! -// - see below -// - make GlobalID a protocol -// - and the specific ones structs -// Should not break anything, they are immutable? Or not, for the temporary -// ID? -// - maybe we could just makes this an enum? no one is going to invent own -// additional GIDs? +#if !GLOBALID_AS_OPEN_CLASS + +import Foundation + +// For ZeeQL we only really need this. +// Avoid the overdesign of arbitrary GIDs. At least for now. +// This could also be a protocol, but again, this just overcomplicates the thing +// and right now we only really need KeyGlobalID's for ZeeQL. +public typealias GlobalID = KeyGlobalID + +public typealias SingleIntKeyGlobalID = KeyGlobalID // compact to object version + +public struct KeyGlobalID: Hashable, @unchecked Sendable { + // unchecked Sendable for AnyHashable, but those will only contain base types. + + public enum Value: Hashable { + // This is a little more complicated than it seems, because values may be + // `nil`. Consider this: `id INTEGER NULL PRIMARY KEY`. + // The `[AnyHashable?]` `init` actually normalizes those values., + // careful w/ assigning such manually. + + case int (Int) + case string (String) + case uuid (UUID) + case singleNil + + case values([ AnyHashable? ]) // TBD: issue for Sendable + // maybe this should be `any Hashable & Sendable`, but restricts Swift + // version. + + @inlinable + public var count: Int { + switch self { + case .int, .string, .uuid, .singleNil: return 1 + case .values(let values): return values.count + } + } + + @inlinable // legacy + public var keyCount: Int { return count } + + @inlinable + public subscript(i: Int) -> Any? { + guard i >= 0 && i < count else { return nil } + switch self { + case .singleNil : return Optional.none // vs `nil`? + case .int (let value) : return value + case .string (let value) : return value + case .uuid (let value) : return value + case .values (let values) : return values[i] + } + } + } + + public let entityName : String + public let value : Value + + @inlinable + public var count: Int { return value.count } + @inlinable // legacy + public var keyCount: Int { return count } + + @inlinable + public subscript(i: Int) -> Any? { return value[i] } +} + +public extension KeyGlobalID.Value { // Initializers and Factory + + @inlinable + init(_ values: [ AnyHashable? ]) { + if values.count == 1, let opt = values.first { + if let v = opt { + switch v { // TBD: `as any BinaryInteger`, but requires 5.5+? + case let v as Int : self = .int(v) + case let v as Int64 : self = .int(Int(v)) + case let v as Int32 : self = .int(Int(v)) + case let v as UInt32 : self = .int(Int(v)) // assumes 64-bit + case let v as String : self = .string(v) + case let v as UUID : self = .uuid(v) + default: + assert(!(v.base is any BinaryInteger), "Unexpected BinaryInteger") + self = .values(values) + } + } + else { + self = .singleNil + } + } + else { self = .values(values) } + } +} + +public extension KeyGlobalID { // Initializers and Factory + + @inlinable + init(entityName: String, value: I) { + self.entityName = entityName + self.value = .int(Int(value)) + } + @inlinable + init(entityName: String, value: String) { + self.entityName = entityName + self.value = .string(value) + } + @inlinable + init(entityName: String, value: UUID) { + self.entityName = entityName + self.value = .uuid(value) + } + + @inlinable + init(entityName: String, values: [ AnyHashable? ]) { + self.entityName = entityName + self.value = Value(values) + } + + @inlinable // legacy + static func make(entityName: String, values: [ Any? ]) -> KeyGlobalID + { + if values.isEmpty { return KeyGlobalID(entityName: entityName, values: []) } + + if values.count == 1, let opt = values.first { + if let v = opt { + switch v { // TBD: `as any BinaryInteger`, but requires 5.5+? + case let v as Int : + return KeyGlobalID(entityName: entityName, value: v) + case let v as Int64 : + return KeyGlobalID(entityName: entityName, value: Int(v)) + case let v as Int32 : + return KeyGlobalID(entityName: entityName, value: Int(v)) + case let v as UInt32 : + return KeyGlobalID(entityName: entityName, value: Int(v)) // assumes 64-bit + case let v as String : + return KeyGlobalID(entityName: entityName, value: v) + case let v as UUID : + return KeyGlobalID(entityName: entityName, value: v) + default: + assert(!(v is any BinaryInteger), "Unexpected BinaryInteger") + assertionFailure("Custom key value type, add explicit check") + if let v = v as? AnyHashable { + return KeyGlobalID(entityName: entityName, values: [ v ]) + } + fatalError("Unsupported key type \(type(of: v))") + } + } + else { + return KeyGlobalID(entityName: entityName, + values: [ Optional.none ]) + } + } + let hashables : [ AnyHashable? ] = values.compactMap { + guard let value = $0 else { return nil } + guard let h = value as? any Hashable else { + fatalError("Key value must be Hashable \(entityName) \(values)") + } + return AnyHashable(h) + } + assert(hashables.count == values.count) + return KeyGlobalID(entityName: entityName, values: hashables) + } +} + +extension KeyGlobalID: EquatableType { + + @inlinable + public func isEqual(to other: Any?) -> Bool { + return self == (other as? GlobalID) + } + @inlinable + public func isEqual(to other: Self) -> Bool { self == other } +} + +extension KeyGlobalID: CustomStringConvertible { + + public var description: String { + var ms = " bind string \"\(value)\"") } - rc = sqlite3_bind_text(stmt, idx, pool.pstrdup(value), -1, nil) - } - else if let value = value as? SingleIntKeyGlobalID { // hacky - if doLogSQL { log.log(" [\(idx)]> bind key \(value)") } - rc = sqlite3_bind_int64(stmt, idx, sqlite3_int64(value.value)) + func bindAnyValue(_ value: Any?) throws -> Int32 { + guard let value = value else { + if doLogSQL { log.log(" [\(idx)]> bind NULL") } + return sqlite3_bind_null(stmt, idx) } - else if let value = value as? Int { // TODO: Other Integers - if doLogSQL { log.log(" [\(idx)]> bind int \(value)") } - rc = sqlite3_bind_int64(stmt, idx, sqlite3_int64(value)) - } - else { // TODO - if doLogSQL { log.log(" [\(idx)]> bind other \(value)") } - rc = sqlite3_bind_text(stmt, idx, pool.pstrdup(value), -1, nil) + switch value { + case let value as String: + if doLogSQL { log.log(" [\(idx)]> bind string \"\(value)\"") } + return sqlite3_bind_text(stmt, idx, pool.pstrdup(value), -1, nil) + case let value as Int: + if doLogSQL { log.log(" [\(idx)]> bind int \(value)") } + return sqlite3_bind_int64(stmt, idx, sqlite3_int64(value)) + case let value as Int32: + if doLogSQL { log.log(" [\(idx)]> bind int \(value)") } + return sqlite3_bind_int64(stmt, idx, sqlite3_int64(value)) + case let value as Int64: + if doLogSQL { log.log(" [\(idx)]> bind int \(value)") } + return sqlite3_bind_int64(stmt, idx, sqlite3_int64(value)) + case let value as GlobalID: + assert(value.keyCount == 1) + switch value.value { + case .singleNil: + if doLogSQL { log.log(" [\(idx)]> bind NULL") } + return sqlite3_bind_null(stmt, idx) + case .int(let value): + if doLogSQL { log.log(" [\(idx)]> bind int \(value)") } + return sqlite3_bind_int64(stmt, idx, sqlite3_int64(value)) + case .string(let value): + if doLogSQL { + log.log(" [\(idx)]> bind string \"\(value)\"") } + return sqlite3_bind_text(stmt, idx, pool.pstrdup(value), -1, nil) + case .uuid(let value): + if doLogSQL { + log.log(" [\(idx)]> bind string \"\(value)\"") } + return sqlite3_bind_text(stmt, idx, + pool.pstrdup(value.uuidString), -1, nil) + case .values(let values): + if values.count > 1 { + let rc = SQLITE_MISMATCH // TBD + throw Error.BindFailed(rc, message(for: rc), bind) + } + if let value = values.first { + return try bindAnyValue(value) + } + else { + assertionFailure("Empty key") + if doLogSQL { log.log(" [\(idx)]> bind NULL") } + return sqlite3_bind_null(stmt, idx) + } + } + default: + assertionFailure("Unexpected value, please add explicit type") + if doLogSQL { log.log(" [\(idx)]> bind other \(value)") } + return sqlite3_bind_text(stmt, idx, pool.pstrdup(value), -1, nil) } } - else { - if doLogSQL { log.log(" [\(idx)]> bind NULL") } - rc = sqlite3_bind_null(stmt, idx) - } + + // TODO: Add a protocol to do this? + let rc = try bindAnyValue(bind.value) guard rc == SQLITE_OK else { throw Error.BindFailed(rc, message(for: rc), bind) }