Skip to content

Commit

Permalink
Rework EOGlobalID classes into a struct/enum
Browse files Browse the repository at this point in the history
Within ZeeQL we do not really need anything else,
but adaptors and code may have to be adjusted.
  • Loading branch information
helje5 committed Nov 20, 2024
1 parent aaad5b7 commit d6886e0
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 32 deletions.
9 changes: 6 additions & 3 deletions Sources/ZeeQL/Access/AccessDataSourceFinders.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ public extension AccessDataSource { // GIDs

func fetchObjects<S: Sequence>(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) }
Expand All @@ -123,14 +124,16 @@ public extension AccessDataSource { // GIDs
try fetchObjects(fs, cb: yield)
}
func fetchObjects<S: Sequence>(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<S: Collection>(with globalIDs: S) throws -> [ Object ]
where S.Element : GlobalID
//where S.Element : GlobalID
where S.Element == KeyGlobalID
{
var objects = [ Object ]()
objects.reserveCapacity(globalIDs.count)
Expand Down
9 changes: 9 additions & 0 deletions Sources/ZeeQL/Access/Entity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)")
Expand Down
202 changes: 194 additions & 8 deletions Sources/ZeeQL/Control/GlobalID.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<I: BinaryInteger>(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<AnyHashable>.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 = "<GID: \(entityName)"
switch value {
case .int (let value) : ms += " \(value)"
case .string(let value) : ms += " \(value)"
case .uuid (let value) : ms += " \(value.uuidString)"
case .singleNil : ms += " <nil>"
case .values(let values) :
for value in values {
if let value = value { ms += " \(value)" }
else { ms += " <nil>" }
}
}
ms += ">"
return ms
}
}

#else // GLOBALID_AS_OPEN_CLASS

open class GlobalID : EquatableType, Hashable {
// Note: cannot be a protocol because Hashable (because Equatable)
Expand Down Expand Up @@ -175,3 +360,4 @@ public final class SingleIntKeyGlobalID : KeyGlobalID {
}
}

#endif // GLOBALID_AS_OPEN_CLASS
77 changes: 56 additions & 21 deletions Sources/ZeeQL/SQLite3Adaptor/SQLite3AdaptorChannel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -340,30 +340,65 @@ open class SQLite3AdaptorChannel : AdaptorChannel {
if doLogSQL { log.log(" BIND[\(idx)]: \(attr.name)") }
}

// TODO: Add a protocol to do this?
let rc : Int32
if let value = bind.value {
if let value = value as? String {
if doLogSQL { log.log(" [\(idx)]> 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) }
Expand Down

0 comments on commit d6886e0

Please sign in to comment.