diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..b9a3c047 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,38 @@ +{ + "name": "Swift", + "image": "swift:6.1", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": "false", + "username": "vscode", + // "userUid": "1000", + // "userGid": "1000", + "upgradePackages": "false" + }, + "ghcr.io/devcontainers/features/git:1": { + "version": "os-provided", + "ppa": "false" + } + }, + "runArgs": ["--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined"], + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "lldb.library": "/usr/lib/liblldb.so" + }, + // Add the IDs of extensions you want installed when the container is created. + "extensions": ["swiftlang.swift-vscode"] + } + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "swift --version", + + // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} diff --git a/Sources/Kuzu/Connection+Extensions.swift b/Sources/Kuzu/Connection+Extensions.swift new file mode 100644 index 00000000..e72e1868 --- /dev/null +++ b/Sources/Kuzu/Connection+Extensions.swift @@ -0,0 +1,54 @@ +// +// kuzu-swift +// https://github.com/kuzudb/kuzu-swift +// +// Copyright © 2023 - 2025 Kùzu Inc. +// This code is licensed under MIT license (see LICENSE for details) + +import cxx_kuzu + +@_spi(Typed) +extension Connection { + public func execute_( + _ preparedStatement: PreparedStatement, + _ parameters: [String: any KuzuEncodable] + ) throws -> QueryResult { + var cQueryResult = kuzu_query_result() + for (key, value) in parameters { + let kuzuValue = try value.kuzuValue() + let state = kuzu_prepared_statement_bind_value( + &preparedStatement.cPreparedStatement, + key, + kuzuValue.ptr + ) + if state != KuzuSuccess { + throw KuzuError.queryExecutionFailed( + "Failed to bind value with status \(state)" + ) + } + } + kuzu_connection_execute( + &cConnection, + &preparedStatement.cPreparedStatement, + &cQueryResult + ) + if !kuzu_query_result_is_success(&cQueryResult) { + let cErrorMesage: UnsafeMutablePointer? = + kuzu_query_result_get_error_message(&cQueryResult) + defer { + kuzu_query_result_destroy(&cQueryResult) + kuzu_destroy_string(cErrorMesage) + } + if cErrorMesage == nil { + throw KuzuError.queryExecutionFailed( + "Query execution failed with an unknown error." + ) + } else { + let errorMessage = String(cString: cErrorMesage!) + throw KuzuError.queryExecutionFailed(errorMessage) + } + } + let queryResult = QueryResult(self, cQueryResult) + return queryResult + } +} diff --git a/Sources/Kuzu/Connection.swift b/Sources/Kuzu/Connection.swift index d30fb948..7b09e3fb 100644 --- a/Sources/Kuzu/Connection.swift +++ b/Sources/Kuzu/Connection.swift @@ -5,7 +5,7 @@ // Copyright © 2023 - 2025 Kùzu Inc. // This code is licensed under MIT license (see LICENSE for details) -@_implementationOnly import cxx_kuzu +import cxx_kuzu /// Represents a connection to a Kuzu database. public final class Connection: @unchecked Sendable { diff --git a/Sources/Kuzu/Database.swift b/Sources/Kuzu/Database.swift index 9e8b0aae..264f7edb 100644 --- a/Sources/Kuzu/Database.swift +++ b/Sources/Kuzu/Database.swift @@ -5,8 +5,7 @@ // Copyright © 2023 - 2025 Kùzu Inc. // This code is licensed under MIT license (see LICENSE for details) -import Foundation -@_implementationOnly import cxx_kuzu +import cxx_kuzu /// A class representing a Kuzu database instance. public final class Database: @unchecked Sendable { diff --git a/Sources/Kuzu/FlatTuple+Extensions.swift b/Sources/Kuzu/FlatTuple+Extensions.swift new file mode 100644 index 00000000..39571c30 --- /dev/null +++ b/Sources/Kuzu/FlatTuple+Extensions.swift @@ -0,0 +1,59 @@ +// +// kuzu-swift +// https://github.com/kuzudb/kuzu-swift +// +// Copyright © 2023 - 2025 Kùzu Inc. +// This code is licensed under MIT license (see LICENSE for details) + +import cxx_kuzu + +@_spi(Typed) +extension FlatTuple { + private func extractKuzu( + index: Int, + into cValue: inout kuzu_value + ) throws -> KuzuValue { + let state = kuzu_flat_tuple_get_value(&cFlatTuple, UInt64(index), &cValue) + let value = KuzuValue(ptr: &cValue) + guard state == KuzuSuccess else { + throw KuzuError.getValueFailed( + "Get value failed with error code: \(state)" + ) + } + return value + } + + private func checkIndex(_ index: Int) throws { + let count = queryResult.getColumnCount() + guard index < count else { + throw KuzuError.getValueFailed( + "Index overflow on columns count of \(count)" + ) + } + } + + public subscript( + _ index: Int, + as type: T.Type = T.self + ) -> T where T: KuzuDecodable { + get throws { + try checkIndex(index) + var cValue = kuzu_value() + let value = try extractKuzu(index: index, into: &cValue) + return try T.kuzuDecode(from: value) + } + } + + // tuples cannot conform to protocols - helper to not have to specify `KuzuMap` + public subscript( + _ index: Int, + as type: [(T, U)].Type = [(T, U)].self + ) -> [(T, U)] where T: KuzuDecodable, U: KuzuDecodable { + get throws { + try checkIndex(index) + var cValue = kuzu_value() + let value = try extractKuzu(index: index, into: &cValue) + return try KuzuMap.kuzuDecode(from: value).tuples + } + } +} diff --git a/Sources/Kuzu/FlatTuple.swift b/Sources/Kuzu/FlatTuple.swift index 1f352f55..00d50c28 100644 --- a/Sources/Kuzu/FlatTuple.swift +++ b/Sources/Kuzu/FlatTuple.swift @@ -5,8 +5,7 @@ // Copyright © 2023 - 2025 Kùzu Inc. // This code is licensed under MIT license (see LICENSE for details) -import Foundation -@_implementationOnly import cxx_kuzu +import cxx_kuzu /// A class representing a row in the result set of a query. /// FlatTuple provides access to the values in a query result row and methods to convert them to different formats. diff --git a/Sources/Kuzu/KuzuDecodable.swift b/Sources/Kuzu/KuzuDecodable.swift new file mode 100644 index 00000000..0699bb3f --- /dev/null +++ b/Sources/Kuzu/KuzuDecodable.swift @@ -0,0 +1,998 @@ +// +// kuzu-swift +// https://github.com/kuzudb/kuzu-swift +// +// Copyright © 2023 - 2025 Kùzu Inc. +// This code is licensed under MIT license (see LICENSE for details) + +import cxx_kuzu +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +#if os(macOS) || os(iOS) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif os(Windows) +import ucrt +#else +#error("Unknown platform") +#endif + +@_spi(Typed) +public protocol KuzuDecodable { + static var kuzuDataTypes: [KuzuDataType] { get } + static func kuzuDecode(from container: consuming KuzuValue) throws -> Self +} + +@_spi(Typed) +extension KuzuValue { + fileprivate func nullCheck() throws { + if kuzu_value_is_null(ptr) { + throw KuzuError.getValueFailed( + "Value is null" + ) + } + } + + fileprivate func typeCheck(_ expecting: [KuzuDataType]) throws { + let typeId = LogicalType(from: self).id + guard expecting.contains(typeId) else { + throw KuzuError.valueConversionFailed( + "Received \(typeId) type when expecting \(expecting)" + ) + } + } +} + + +@_spi(Typed) +extension Bool: KuzuDecodable { + public static let kuzuDataTypes: [KuzuDataType] = [.bool] + public static func kuzuDecode(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + var value: Bool = Bool() + let state = kuzu_value_get_bool(container.ptr, &value) + guard state == KuzuSuccess else { + throw KuzuError.getValueFailed( + "Failed to get bool value with status \(state)" + ) + } + return value + } +} + +@_spi(Typed) +extension Int64: KuzuDecodable { + public static let kuzuDataTypes: [KuzuDataType] = [.int64, .serial] + public static func kuzuDecode(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + var value: Int64 = Int64() + let state = kuzu_value_get_int64(container.ptr, &value) + guard state == KuzuSuccess else { + throw KuzuError.getValueFailed( + "Failed to get int64 value with status \(state)" + ) + } + return value + } +} + +@_spi(Typed) +extension UInt64: KuzuDecodable { + public static let kuzuDataTypes: [KuzuDataType] = [.uint64] + public static func kuzuDecode(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + var value: UInt64 = UInt64() + let state = kuzu_value_get_uint64(container.ptr, &value) + guard state == KuzuSuccess else { + throw KuzuError.getValueFailed( + "Failed to get uint64 value with status \(state)" + ) + } + return value + } +} + +@_spi(Typed) +extension Int32: KuzuDecodable { + public static let kuzuDataTypes: [KuzuDataType] = [.int32] + public static func kuzuDecode(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + var value: Int32 = Int32() + let state = kuzu_value_get_int32(container.ptr, &value) + guard state == KuzuSuccess else { + throw KuzuError.getValueFailed( + "Failed to get int32 value with status \(state)" + ) + } + return value + } +} + +@_spi(Typed) +extension UInt32: KuzuDecodable { + public static let kuzuDataTypes: [KuzuDataType] = [.uint32] + public static func kuzuDecode(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + var value: UInt32 = UInt32() + let state = kuzu_value_get_uint32(container.ptr, &value) + guard state == KuzuSuccess else { + throw KuzuError.getValueFailed( + "Failed to get uint32 value with status \(state)" + ) + } + return value + } +} + +@_spi(Typed) +extension Int16: KuzuDecodable { + public static let kuzuDataTypes: [KuzuDataType] = [.int16] + public static func kuzuDecode(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + var value: Int16 = Int16() + let state = kuzu_value_get_int16(container.ptr, &value) + guard state == KuzuSuccess else { + throw KuzuError.getValueFailed( + "Failed to get int16 value with status \(state)" + ) + } + return value + } +} + +@_spi(Typed) +extension UInt16: KuzuDecodable { + public static let kuzuDataTypes: [KuzuDataType] = [.uint16] + public static func kuzuDecode(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + var value: UInt16 = UInt16() + let state = kuzu_value_get_uint16(container.ptr, &value) + guard state == KuzuSuccess else { + throw KuzuError.getValueFailed( + "Failed to get uint16 value with status \(state)" + ) + } + return value + } +} + +@_spi(Typed) +extension Int8: KuzuDecodable { + public static let kuzuDataTypes: [KuzuDataType] = [.int8] + public static func kuzuDecode(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + var value: Int8 = Int8() + let state = kuzu_value_get_int8(container.ptr, &value) + guard state == KuzuSuccess else { + throw KuzuError.getValueFailed( + "Failed to get int8 value with status \(state)" + ) + } + return value + } +} + +@_spi(Typed) +extension UInt8: KuzuDecodable { + public static let kuzuDataTypes: [KuzuDataType] = [.uint8] + public static func kuzuDecode(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + var value: UInt8 = UInt8() + let state = kuzu_value_get_uint8(container.ptr, &value) + guard state == KuzuSuccess else { + throw KuzuError.getValueFailed( + "Failed to get uint8 value with status \(state)" + ) + } + return value + } +} + +@_spi(Typed) +extension Float: KuzuDecodable { + public static let kuzuDataTypes: [KuzuDataType] = [.float] + public static func kuzuDecode(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + var value: Float = Float() + let state = kuzu_value_get_float(container.ptr, &value) + guard state == KuzuSuccess else { + throw KuzuError.getValueFailed( + "Failed to get Float value with status \(state)" + ) + } + return value + } +} + +@_spi(Typed) +extension Double: KuzuDecodable { + public static let kuzuDataTypes: [KuzuDataType] = [.double] + public static func kuzuDecode(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + var value: Double = Double() + let state = kuzu_value_get_double(container.ptr, &value) + guard state == KuzuSuccess else { + throw KuzuError.getValueFailed( + "Failed to get Double value with status \(state)" + ) + } + return value + } +} + +@_spi(Typed) +extension KuzuInterval: KuzuDecodable { + public static let kuzuDataTypes: [KuzuDataType] = [.interval] + public static func kuzuDecode(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + var cIntervalValue = kuzu_interval_t() + let state = kuzu_value_get_interval(container.ptr, &cIntervalValue) + guard state == KuzuSuccess else { + throw KuzuError.getValueFailed( + "Failed to get interval value with status \(state)" + ) + } + return KuzuInterval( + months: cIntervalValue.months, + days: cIntervalValue.days, + micros: cIntervalValue.micros + ) + } +} + +@_spi(Typed) +extension String: KuzuDecodable { + public static let kuzuDataTypes: [KuzuDataType] = [.string] + public static func kuzuDecode(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + var strValue: UnsafeMutablePointer? + let state = kuzu_value_get_string(container.ptr, &strValue) + defer { kuzu_destroy_string(strValue) } + guard state == KuzuSuccess, let strValue else { + throw KuzuError.getValueFailed( + "Failed to get string value with status \(state)" + ) + } + return String(cString: strValue) + } +} + +@_spi(Typed) +extension UUID: KuzuDecodable { + public static let kuzuDataTypes: [KuzuDataType] = [.uuid] + public static func kuzuDecode(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + var valueString: UnsafeMutablePointer? + let state = kuzu_value_get_uuid(container.ptr, &valueString) + defer { kuzu_destroy_string(valueString) } + guard state == KuzuSuccess, let valueString, let uuid = UUID(uuidString: String(cString: valueString)) else { + throw KuzuError.getValueFailed( + "Failed to get uuid value with status \(state)" + ) + } + return uuid + } +} + +@_spi(Typed) +extension Date: KuzuDecodable { + public static let kuzuDataTypes: [KuzuDataType] = [.date, .timestamp, .timestampMs, .timestampNs, .timestampTz, .timestampSec] + public static func kuzuDecode(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + + let typeId = LogicalType(from: container).id + switch typeId { + case .date: return try kuzuDate(from: container) + case .timestamp: return try kuzuTimestamp(from: container) + case .timestampMs: return try kuzuTimestampMs(from: container) + case .timestampNs: return try kuzuTimestampNs(from: container) + case .timestampTz: return try kuzuTimestampTz(from: container) + case .timestampSec: return try kuzuTimestampSec(from: container) + default: + throw KuzuError.valueConversionFailed( + "Received \(typeId) type when expecting \(Self.kuzuDataTypes)" + ) + } + } + + static func kuzuTimestamp(from container: consuming KuzuValue) throws -> Self { + var cTimestampValue = kuzu_timestamp_t() + let state = kuzu_value_get_timestamp(container.ptr, &cTimestampValue) + guard state == KuzuSuccess else { + throw KuzuError.getValueFailed( + "Failed to get timestamp value with status \(state)" + ) + } + let microseconds = cTimestampValue.value + let seconds: Double = Double(microseconds) / MICROSECONDS_IN_A_SECOND + return Date(timeIntervalSince1970: seconds) + } + + static func kuzuTimestampNs(from container: consuming KuzuValue) throws -> Self { + var cTimestampValue = kuzu_timestamp_ns_t() + let state = kuzu_value_get_timestamp_ns(container.ptr, &cTimestampValue) + guard state == KuzuSuccess else { + throw KuzuError.getValueFailed( + "Failed to get timestamp value with status \(state)" + ) + } + let nanoseconds = cTimestampValue.value + let seconds: Double = Double(nanoseconds) / NANOSECONDS_IN_A_SECOND + return Date(timeIntervalSince1970: seconds) + } + + static func kuzuTimestampMs(from container: consuming KuzuValue) throws -> Self { + var cTimestampValue = kuzu_timestamp_ms_t() + let state = kuzu_value_get_timestamp_ms(container.ptr, &cTimestampValue) + guard state == KuzuSuccess else { + throw KuzuError.getValueFailed( + "Failed to get timestamp value with status \(state)" + ) + } + let milliseconds = cTimestampValue.value + let seconds: Double = Double(milliseconds) / MILLISECONDS_IN_A_SECOND + return Date(timeIntervalSince1970: seconds) + } + + static func kuzuTimestampSec(from container: consuming KuzuValue) throws -> Self { + var cTimestampValue = kuzu_timestamp_sec_t() + let state = kuzu_value_get_timestamp_sec(container.ptr, &cTimestampValue) + guard state == KuzuSuccess else { + throw KuzuError.getValueFailed( + "Failed to get timestamp value with status \(state)" + ) + } + let seconds = cTimestampValue.value + return Date(timeIntervalSince1970: Double(seconds)) + } + + static func kuzuTimestampTz(from container: consuming KuzuValue) throws -> Self { + var cTimestampValue = kuzu_timestamp_tz_t() + let state = kuzu_value_get_timestamp_tz(container.ptr, &cTimestampValue) + guard state == KuzuSuccess else { + throw KuzuError.getValueFailed( + "Failed to get timestamp value with status \(state)" + ) + } + let microseconds = cTimestampValue.value + let seconds: Double = Double(microseconds) / MICROSECONDS_IN_A_SECOND + return Date(timeIntervalSince1970: seconds) + } + + static func kuzuDate(from container: consuming KuzuValue) throws -> Self { + var cDateValue = kuzu_date_t() + let state = kuzu_value_get_date(container.ptr, &cDateValue) + guard state == KuzuSuccess else { + throw KuzuError.getValueFailed( + "Failed to get date value with status \(state)" + ) + } + let days = cDateValue.days + let seconds: Double = Double(days) * SECONDS_IN_A_DAY + return Date(timeIntervalSince1970: seconds) + } +} + +@_spi(Typed) +extension Decimal: KuzuDecodable { + public static let kuzuDataTypes: [KuzuDataType] = [.decimal, .int128] + public static func kuzuDecode(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + + let typeId = LogicalType(from: container).id + switch typeId { + case .decimal: return try kuzuDecimal(from: container) + case .int128: return try kuzuInt128(from: container) + default: + throw KuzuError.valueConversionFailed( + "Received \(typeId) type when expecting \(Self.kuzuDataTypes)" + ) + } + } + + static func kuzuDecimal(from container: consuming KuzuValue) throws -> Self { + var outString: UnsafeMutablePointer? + let state = kuzu_value_get_decimal_as_string(container.ptr, &outString) + defer { kuzu_destroy_string(outString) } + guard state == KuzuSuccess, let outString else { + throw KuzuError.getValueFailed( + "Failed to get string value of decimal type with status: \(state)" + ) + } + let decimalString = String(cString: outString) + guard let decimal = Decimal(string: decimalString) else { + throw KuzuError.valueConversionFailed( + "Failed to convert decimal value from string: \(decimalString)" + ) + } + return decimal + } + + static func kuzuInt128(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + var int128Value = kuzu_int128_t() + let getValueState = kuzu_value_get_int128(container.ptr, &int128Value) + guard getValueState == KuzuSuccess else { + throw KuzuError.getValueFailed( + "Failed to get int128 value with status \(getValueState)" + ) + } + var valueString: UnsafeMutablePointer? + let valueConversionState = kuzu_int128_t_to_string( + int128Value, + &valueString + ) + defer { kuzu_destroy_string(valueString) } + guard valueConversionState == KuzuSuccess, let valueString else { + throw KuzuError.getValueFailed( + "Failed to convert int128 to string with status \(valueConversionState)" + ) + } + let decimalString = String(cString: valueString) + guard let decimal = Decimal(string: decimalString) else { + throw KuzuError.valueConversionFailed( + "Failed to convert decimal value from string: \(decimalString)" + ) + } + return decimal + } +} + +@_spi(Typed) +extension KuzuInternalId: KuzuDecodable { + public static let kuzuDataTypes: [KuzuDataType] = [.internalId] + public static func kuzuDecode(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + var cInternalIdValue = kuzu_internal_id_t() + let state = kuzu_value_get_internal_id(container.ptr, &cInternalIdValue) + guard state == KuzuSuccess else { + throw KuzuError.getValueFailed( + "Failed to get internal id value with status \(state)" + ) + } + return KuzuInternalId( + tableId: cInternalIdValue.table_id, + offset: cInternalIdValue.offset + ) + } +} + +@_spi(Typed) +extension Data: KuzuDecodable { + public static let kuzuDataTypes: [KuzuDataType] = [.blob] + public static func kuzuDecode(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + var cBlobValue: UnsafeMutablePointer? + let state = kuzu_value_get_blob(container.ptr, &cBlobValue) + defer { kuzu_destroy_blob(cBlobValue) } + guard state == KuzuSuccess, let cBlobValue else { + throw KuzuError.getValueFailed( + "Failed to get blob value with status \(state)" + ) + } + let blobSize = strlen(cBlobValue) + let blobData = Data(bytes: cBlobValue, count: blobSize) + return blobData + } +} + +@_spi(Typed) +extension Optional: KuzuDecodable where Wrapped: KuzuDecodable { + public static var kuzuDataTypes: [KuzuDataType] { Wrapped.kuzuDataTypes } + public static func kuzuDecode(from container: consuming KuzuValue) throws -> Self { + if kuzu_value_is_null(container.ptr) { + return nil + } + + return try Wrapped.kuzuDecode(from: container) + } +} + +@_spi(Typed) +extension Array: KuzuDecodable where Element: KuzuDecodable { + public static var kuzuDataTypes: [KuzuDataType] { [.list, .array] } + public static func kuzuDecode(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + let logicalType = LogicalType(from: container) + let numElements: UInt64 + + switch logicalType.id { + case .array: + numElements = try logicalType.kuzuArrayElementCount() + case .list: + numElements = try container.kuzuListElementCount() + default: + throw KuzuError.valueConversionFailed( + "Failed to get number of elements - unknown list/array type \(logicalType.id)" + ) + } + + var result: [Element] = [] + for i in UInt64(0).. Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + let propertySize = try container.kuzuStructElementCount() + + var dict: [String: Value] = [:] + for i in UInt64(0).. Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + let mapSize = try container.kuzuMapElementCount() + + var result: [(Key, Value)] = [] + for i in UInt64(0).. Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + var idValue = kuzu_value() + let idState = kuzu_node_val_get_id_val(container.ptr, &idValue) + let kuzuId = KuzuValue(ptr: &idValue) + guard idState == KuzuSuccess else { + throw KuzuError.valueConversionFailed( + "Failed to get node ID with status: \(idState)" + ) + } + let id = try KuzuInternalId.kuzuDecode(from: kuzuId) + + var labelValue = kuzu_value() + let labelState = kuzu_node_val_get_label_val(container.ptr, &labelValue) + let kuzuLabel = KuzuValue(ptr: &labelValue) + guard labelState == KuzuSuccess else { + throw KuzuError.valueConversionFailed( + "Failed to get node label with status: \(labelState)" + ) + } + let label = try String.kuzuDecode(from: kuzuLabel) + + let propertySize = try container.kuzuNodeElementCount() + var properties: [String: any KuzuDecodable] = [:] + + for i in UInt64(0).. Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + var idValue = kuzu_value() + let idState = kuzu_rel_val_get_id_val(container.ptr, &idValue) + let idKuzu = KuzuValue(ptr: &idValue) + guard idState == KuzuSuccess else { + throw KuzuError.valueConversionFailed( + "Failed to get node ID with status: \(idState)" + ) + } + let id = try KuzuInternalId.kuzuDecode(from: idKuzu) + + var sourceValue = kuzu_value() + let sourceState = kuzu_rel_val_get_src_id_val(container.ptr, &sourceValue) + let sourceKuzu = KuzuValue(ptr: &sourceValue) + guard sourceState == KuzuSuccess else { + throw KuzuError.valueConversionFailed( + "Failed to get relationship source ID with status: \(sourceState)" + ) + } + let sourceId = try KuzuInternalId.kuzuDecode(from: sourceKuzu) + + var targetValue = kuzu_value() + let targetState = kuzu_rel_val_get_dst_id_val(container.ptr, &targetValue) + let targetKuzu = KuzuValue(ptr: &targetValue) + guard targetState == KuzuSuccess else { + throw KuzuError.valueConversionFailed( + "Failed to get relationship target ID with status: \(targetState)" + ) + } + let targetId = try KuzuInternalId.kuzuDecode(from: targetKuzu) + + var labelValue = kuzu_value() + let labelState = kuzu_rel_val_get_label_val(container.ptr, &labelValue) + let labelKuzu = KuzuValue(ptr: &labelValue) + guard labelState == KuzuSuccess else { + throw KuzuError.valueConversionFailed( + "Failed to get relationship label with status: \(labelState)" + ) + } + let label = try String.kuzuDecode(from: labelKuzu) + + let propertySize = try container.kuzuRelElementCount() + var properties: [String: any KuzuDecodable] = [:] + + for i in UInt64(0).. Self { + try container.nullCheck() + try container.typeCheck(Self.kuzuDataTypes) + + var nodesValue = kuzu_value() + let nodesState = kuzu_value_get_recursive_rel_node_list( + container.ptr, + &nodesValue + ) + let nodesKuzu = KuzuValue(ptr: &nodesValue) + guard nodesState == KuzuSuccess else { + throw KuzuError.valueConversionFailed( + "Failed to get recursive relationship nodes with status: \(nodesState)" + ) + } + + var relsValue = kuzu_value() + let relsState = kuzu_value_get_recursive_rel_rel_list( + container.ptr, + &relsValue + ) + let relsKuzu = KuzuValue(ptr: &relsValue) + guard relsState == KuzuSuccess else { + throw KuzuError.valueConversionFailed( + "Failed to get recursive relationship relationships with status: \(relsState)" + ) + } + + let nodes = try Array.kuzuDecode(from: nodesKuzu) + let rels = try Array.kuzuDecode(from: relsKuzu) + + return KuzuRecursiveRelationship_( + nodes: nodes, + relationships: rels + ) + } +} + +@_spi(Typed) +public struct KuzuAnyDecodable: KuzuDecodable { + public let value: any KuzuDecodable + + public static let kuzuDataTypes: [KuzuDataType] = [.any] + public static func kuzuDecode(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + let type = LogicalType(from: container) + let decoded = try type.id.decode(from: container) + return .init(value: decoded) + } + + public static func kuzuUnion(from container: consuming KuzuValue) throws -> Self { + try container.nullCheck() + try container.typeCheck([.union]) + + var unionValue = kuzu_value() + let state = kuzu_value_get_struct_field_value(container.ptr, 0, &unionValue) + let kuzu = KuzuValue(ptr: &unionValue) + guard state == KuzuSuccess else { + throw KuzuError.valueConversionFailed( + "Failed to get union value with status: \(state)" + ) + } + + return try Self.kuzuDecode(from: kuzu) + } +} + +// MARK: Helpers + +extension KuzuValue { + fileprivate func getListValue( + index: UInt64, + into cValue: inout kuzu_value + ) throws -> KuzuValue { + let state = kuzu_value_get_list_element(ptr, index, &cValue) + let kuzu = KuzuValue(ptr: &cValue) + guard state == KuzuSuccess else { + throw KuzuError.valueConversionFailed( + "Failed to get list element with status: \(state)" + ) + } + return kuzu + } + + fileprivate func getStructValue( + index: UInt64, + into cValue: inout kuzu_value + ) throws -> KuzuValue { + let state = kuzu_value_get_struct_field_value(ptr, index, &cValue) + let kuzu = KuzuValue(ptr: &cValue) + guard state == KuzuSuccess else { + throw KuzuError.valueConversionFailed( + "Failed to get struct field with status: \(state)" + ) + } + return kuzu + } + + fileprivate func getStructKey(index: UInt64) throws -> String { + var currentKey: UnsafeMutablePointer? + let keyState = kuzu_value_get_struct_field_name(ptr, index, ¤tKey) + defer { kuzu_destroy_string(currentKey) } + guard keyState == KuzuSuccess, let currentKey else { + throw KuzuError.valueConversionFailed( + "Failed to get struct field name with status: \(keyState)" + ) + } + return String(cString: currentKey) + } + + fileprivate func getMapValue( + index: UInt64, + into cValue: inout kuzu_value + ) throws -> KuzuValue { + let state = kuzu_value_get_map_value(ptr, index, &cValue) + let kuzu = KuzuValue(ptr: &cValue) + guard state == KuzuSuccess else { + throw KuzuError.valueConversionFailed( + "Failed to get map value with status: \(state)" + ) + } + return kuzu + } + + fileprivate func getMapKey( + index: UInt64, + into cValue: inout kuzu_value + ) throws -> KuzuValue { + let state = kuzu_value_get_map_key(ptr, index, &cValue) + let kuzu = KuzuValue(ptr: &cValue) + guard state == KuzuSuccess else { + throw KuzuError.valueConversionFailed( + "Failed to get map key with status: \(state)" + ) + } + return kuzu + } + + fileprivate func getNodeValue( + index: UInt64, + into cValue: inout kuzu_value + ) throws -> KuzuValue { + let state = kuzu_node_val_get_property_value_at(ptr, index, &cValue) + let kuzu = KuzuValue(ptr: &cValue) + guard state == KuzuSuccess else { + throw KuzuError.valueConversionFailed( + "Failed to get node property value with status: \(state)" + ) + } + return kuzu + } + + fileprivate func getNodeKey(index: UInt64) throws -> String { + var currentKey: UnsafeMutablePointer? + let keyState = kuzu_node_val_get_property_name_at(ptr, index, ¤tKey) + defer { kuzu_destroy_string(currentKey) } + guard keyState == KuzuSuccess, let currentKey else { + throw KuzuError.valueConversionFailed( + "Failed to get node property name with status: \(keyState)" + ) + } + return String(cString: currentKey) + } + + fileprivate func getRelName(index: UInt64) throws -> String { + var currentKey: UnsafeMutablePointer? + let keyState = kuzu_rel_val_get_property_name_at(ptr, index, ¤tKey) + defer { kuzu_destroy_string(currentKey) } + guard keyState == KuzuSuccess, let currentKey else { + throw KuzuError.valueConversionFailed( + "Failed to get rel property name with status: \(keyState)" + ) + } + return String(cString: currentKey) + } + + fileprivate func getRelValue( + index: UInt64, + into cValue: inout kuzu_value + ) throws -> KuzuValue { + let state = kuzu_rel_val_get_property_value_at(ptr, index, &cValue) + let kuzu = KuzuValue(ptr: &cValue) + guard state == KuzuSuccess else { + throw KuzuError.valueConversionFailed( + "Failed to get node property value with status: \(state)" + ) + } + return kuzu + } + + fileprivate func kuzuListElementCount() throws -> UInt64 { + var numElements: UInt64 = 0 + + let state = kuzu_value_get_list_size(ptr, &numElements) + guard state == KuzuSuccess else { + throw KuzuError.valueConversionFailed( + "Failed to get number of elements in list with status: \(state)" + ) + } + return numElements + } + + fileprivate func kuzuStructElementCount() throws -> UInt64 { + var propertySize: UInt64 = 0 + + let state = kuzu_value_get_struct_num_fields(ptr, &propertySize) + guard state == KuzuSuccess else { + throw KuzuError.valueConversionFailed( + "Failed to get number of elements in struct with status: \(state)" + ) + } + + return propertySize + } + + fileprivate func kuzuMapElementCount() throws -> UInt64 { + var mapSize: UInt64 = 0 + + let state = kuzu_value_get_map_size(ptr, &mapSize) + guard state == KuzuSuccess else { + throw KuzuError.valueConversionFailed( + "Failed to get number of elements in map with status: \(state)" + ) + } + + return mapSize + } + + fileprivate func kuzuNodeElementCount() throws -> UInt64 { + var propertySize: UInt64 = 0 + + let state = kuzu_node_val_get_property_size(ptr, &propertySize) + guard state == KuzuSuccess else { + throw KuzuError.valueConversionFailed( + "Failed to get number of elements in node with status: \(state)" + ) + } + + return propertySize + } + + fileprivate func kuzuRelElementCount() throws -> UInt64 { + var propertySize: UInt64 = 0 + + let state = kuzu_rel_val_get_property_size(ptr, &propertySize) + guard state == KuzuSuccess else { + throw KuzuError.valueConversionFailed( + "Failed to get number of elements in relationship with status: \(state)" + ) + } + + return propertySize + } +} + +extension LogicalType { + fileprivate func kuzuArrayElementCount() throws -> UInt64 { + var numElements: UInt64 = 0 + + let state = kuzu_data_type_get_num_elements_in_array( + ptr, + &numElements + ) + guard state == KuzuSuccess else { + throw KuzuError.valueConversionFailed( + "Failed to get number of elements in array with status: \(state)" + ) + } + + return numElements + } +} diff --git a/Sources/Kuzu/KuzuEncodable.swift b/Sources/Kuzu/KuzuEncodable.swift new file mode 100644 index 00000000..81141cc2 --- /dev/null +++ b/Sources/Kuzu/KuzuEncodable.swift @@ -0,0 +1,321 @@ +// +// kuzu-swift +// https://github.com/kuzudb/kuzu-swift +// +// Copyright © 2023 - 2025 Kùzu Inc. +// This code is licensed under MIT license (see LICENSE for details) + +import cxx_kuzu + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +#if os(macOS) || os(iOS) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif os(Windows) +import ucrt +#else +#error("Unknown platform") +#endif + +@_spi(Typed) +public protocol KuzuEncodable { + func kuzuValue() throws -> KuzuValue +} + +@_spi(Typed) +extension Optional: KuzuEncodable where Wrapped: KuzuEncodable { + public func kuzuValue() throws -> KuzuValue { + if let self { + try self.kuzuValue() + } else { + KuzuValue.null() + } + } +} + +@_spi(Typed) +extension Array: KuzuEncodable where Element: KuzuEncodable { + public func kuzuValue() throws -> KuzuValue { + try ListContainer(items: self).kuzuValue() + } +} + +@_spi(Typed) +extension Dictionary: KuzuEncodable where Key == String, Value: KuzuEncodable { + public func kuzuValue() throws -> KuzuValue { + try StructContainer(items: self).kuzuValue() + } +} + +@_spi(Typed) +extension KuzuMap: KuzuEncodable where Key: KuzuEncodable, Value: KuzuEncodable { + public func kuzuValue() throws -> KuzuValue { + try MapContainer(items: tuples).kuzuValue() + } +} + +@_spi(Typed) +extension Bool: KuzuEncodable { + public func kuzuValue() -> KuzuValue { + .init(ptr: kuzu_value_create_bool(self)) + } +} + +@_spi(Typed) +extension Int8: KuzuEncodable { + public func kuzuValue() -> KuzuValue { + .init(ptr: kuzu_value_create_int8(self)) + } +} + +@_spi(Typed) +extension UInt8: KuzuEncodable { + public func kuzuValue() -> KuzuValue { + .init(ptr: kuzu_value_create_uint8(self)) + } +} + +@_spi(Typed) +extension Int16: KuzuEncodable { + public func kuzuValue() -> KuzuValue { + .init(ptr: kuzu_value_create_int16(self)) + } +} + +@_spi(Typed) +extension UInt16: KuzuEncodable { + public func kuzuValue() -> KuzuValue { + .init(ptr: kuzu_value_create_uint16(self)) + } +} + +@_spi(Typed) +extension Int32: KuzuEncodable { + public func kuzuValue() -> KuzuValue { + .init(ptr: kuzu_value_create_int32(self)) + } +} + +@_spi(Typed) +extension UInt32: KuzuEncodable { + public func kuzuValue() -> KuzuValue { + .init(ptr: kuzu_value_create_uint32(self)) + } +} + +@_spi(Typed) +extension Int64: KuzuEncodable { + public func kuzuValue() -> KuzuValue { + .init(ptr: kuzu_value_create_int64(self)) + } +} + +@_spi(Typed) +extension UInt64: KuzuEncodable { + public func kuzuValue() -> KuzuValue { + .init(ptr: kuzu_value_create_uint64(self)) + } +} + +@_spi(Typed) +extension Float: KuzuEncodable { + public func kuzuValue() -> KuzuValue { + .init(ptr: kuzu_value_create_float(self)) + } +} + +@_spi(Typed) +extension Double: KuzuEncodable { + public func kuzuValue() -> KuzuValue { + .init(ptr: kuzu_value_create_double(self)) + } +} + +@_spi(Typed) +extension String: KuzuEncodable { + public func kuzuValue() -> KuzuValue { + .init(ptr: kuzu_value_create_string(self)) + } +} + +@_spi(Typed) +extension Date: KuzuEncodable { + public func kuzuValue() -> KuzuValue { + let timeInterval = self.timeIntervalSince1970 + let microseconds = timeInterval * MICROSECONDS_IN_A_SECOND + let cValue = kuzu_timestamp_t(value: Int64(microseconds)) + return .init(ptr: kuzu_value_create_timestamp(cValue)) + } +} + +@_spi(Typed) +extension UUID: KuzuEncodable { + public func kuzuValue() -> KuzuValue { + .init(ptr: kuzu_value_create_string(uuidString)) + } +} + +@_spi(Typed) +extension KuzuInterval: KuzuEncodable { + public func kuzuValue() -> KuzuValue { + .init(ptr: kuzu_value_create_interval(kuzu_interval_t(months: months, days: days, micros: micros))) + } +} + +private final class Box { + var value: T + init(_ value: consuming T) { + self.value = value + } +} + +private final class ListContainer: KuzuEncodable { + let count: Int + let items: [Box] + + init(items: [some KuzuEncodable]) throws { + self.count = items.count + self.items = try items.map { try Box($0.kuzuValue()) } + } + + var pointers: [UnsafeMutablePointer?] { + items.map { $0.value.ptr } + } + + func kuzuValue() throws -> KuzuValue { + guard count > 0 else { + throw KuzuError.valueConversionFailed( + "Cannot convert empty array to Kuzu list" + ) + } + + var outValue: UnsafeMutablePointer? + let state = pointers.withUnsafeBufferPointer { buffer in + kuzu_value_create_list( + UInt64(count), + UnsafeMutablePointer(mutating: buffer.baseAddress), + &outValue + ) + } + + guard state == KuzuSuccess, let outValue else { + throw KuzuError.valueConversionFailed( + "Failed to create list value with status: \(state)" + ) + } + + return KuzuValue(ptr: outValue) + } +} + +private final class StructContainer: KuzuEncodable { + let count: Int + let keys: [Box] + let values: [Box] + + init(items: [String: some KuzuEncodable]) throws { + self.count = items.count + self.keys = items.map { Box(CString($0.key)) } + self.values = try items.map { try Box($0.value.kuzuValue()) } + } + + var valuePointers: [UnsafeMutablePointer?] { + values.map { $0.value.ptr } + } + + var charPointers: [UnsafePointer?] { + keys.map { UnsafePointer($0.value.ptr) } + } + + struct CString: ~Copyable { + let ptr: UnsafeMutablePointer? + + init(_ string: String) { + self.ptr = strdup(string) + } + + deinit { + free(ptr) + } + } + + func kuzuValue() throws -> KuzuValue { + guard count > 0 else { + throw KuzuError.valueConversionFailed("Cannot convert empty dictionary to Kuzu struct") + } + + var outValue: UnsafeMutablePointer? + let state = charPointers.withUnsafeBufferPointer { namesBuffer in + valuePointers.withUnsafeBufferPointer { valuesBuffer in + kuzu_value_create_struct( + UInt64(count), + UnsafeMutablePointer(mutating: namesBuffer.baseAddress), + UnsafeMutablePointer(mutating: valuesBuffer.baseAddress), + &outValue + ) + } + } + + guard state == KuzuSuccess, let outValue else { + throw KuzuError.valueConversionFailed( + "Failed to create struct value with status: \(state)" + ) + } + + return KuzuValue(ptr: outValue) + } +} + +private final class MapContainer: KuzuEncodable { + let count: Int + let keys: [Box] + let values: [Box] + + init(items: [(some KuzuEncodable, some KuzuEncodable)]) throws { + self.count = items.count + self.keys = try items.map { try Box($0.0.kuzuValue()) } + self.values = try items.map { try Box($0.1.kuzuValue()) } + } + + var valuePointers: [UnsafeMutablePointer?] { + values.map { $0.value.ptr } + } + + var keyPointers: [UnsafeMutablePointer?] { + keys.map { $0.value.ptr } + } + + func kuzuValue() throws -> KuzuValue { + guard count > 0 else { + throw KuzuError.valueConversionFailed("Cannot convert empty dictionary to Kuzu struct") + } + + var outValue: UnsafeMutablePointer? + let state = keyPointers.withUnsafeBufferPointer { keysBuffer in + valuePointers.withUnsafeBufferPointer { valuesBuffer in + kuzu_value_create_map( + UInt64(count), + UnsafeMutablePointer(mutating: keysBuffer.baseAddress), + UnsafeMutablePointer(mutating: valuesBuffer.baseAddress), + &outValue + ) + } + } + + guard state == KuzuSuccess, let outValue else { + throw KuzuError.valueConversionFailed( + "Failed to create MAP value with status: \(state). Please make sure all the keys are of the same type and all the values are of the same type." + ) + } + + return KuzuValue(ptr: outValue) + } +} diff --git a/Sources/Kuzu/PreparedStatement.swift b/Sources/Kuzu/PreparedStatement.swift index 90ebb41e..47c5599b 100644 --- a/Sources/Kuzu/PreparedStatement.swift +++ b/Sources/Kuzu/PreparedStatement.swift @@ -5,7 +5,7 @@ // Copyright © 2023 - 2025 Kùzu Inc. // This code is licensed under MIT license (see LICENSE for details) -@_implementationOnly import cxx_kuzu +import cxx_kuzu /// A class representing a prepared statement in Kuzu. /// PreparedStatement can be used to execute a query with parameters. diff --git a/Sources/Kuzu/QueryResult.swift b/Sources/Kuzu/QueryResult.swift index e9982feb..4261ad16 100644 --- a/Sources/Kuzu/QueryResult.swift +++ b/Sources/Kuzu/QueryResult.swift @@ -5,8 +5,24 @@ // Copyright © 2023 - 2025 Kùzu Inc. // This code is licensed under MIT license (see LICENSE for details) +import cxx_kuzu +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation -@_implementationOnly import cxx_kuzu +#endif + +#if os(macOS) || os(iOS) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif os(Windows) +import ucrt +#else +#error("Unknown platform") +#endif /// A class representing the result of a query, which can be used to iterate over the result set. /// QueryResult is returned by the `query` and `execute` methods of Connection. diff --git a/Sources/Kuzu/SystemConfig.swift b/Sources/Kuzu/SystemConfig.swift index bc65f21e..2f98d89a 100644 --- a/Sources/Kuzu/SystemConfig.swift +++ b/Sources/Kuzu/SystemConfig.swift @@ -5,8 +5,12 @@ // Copyright © 2023 - 2025 Kùzu Inc. // This code is licensed under MIT license (see LICENSE for details) +import cxx_kuzu +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation -@_implementationOnly import cxx_kuzu +#endif /// Represents the configuration of Kuzu database system. /// diff --git a/Sources/Kuzu/Types+Extensions.swift b/Sources/Kuzu/Types+Extensions.swift new file mode 100644 index 00000000..1319240b --- /dev/null +++ b/Sources/Kuzu/Types+Extensions.swift @@ -0,0 +1,187 @@ +// +// kuzu-swift +// https://github.com/kuzudb/kuzu-swift +// +// Copyright © 2023 - 2025 Kùzu Inc. +// This code is licensed under MIT license (see LICENSE for details) + +import cxx_kuzu +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@_spi(Typed) +public struct KuzuValue: ~Copyable { + let ptr: UnsafeMutablePointer + + init(ptr: UnsafeMutablePointer) { + self.ptr = ptr + } + + static func null() -> KuzuValue { + KuzuValue(ptr: kuzu_value_create_null()) + } + + deinit { + kuzu_value_destroy(ptr) + } +} + +struct LogicalType: ~Copyable { + let ptr: UnsafeMutablePointer + + init(from value: borrowing KuzuValue) { + ptr = .allocate(capacity: 1) + ptr.initialize(to: kuzu_logical_type()) + kuzu_value_get_data_type(value.ptr, ptr) + } + + var id: KuzuDataType { + .init(id: kuzu_data_type_get_id(ptr)) + } + + deinit { + kuzu_data_type_destroy(ptr) + ptr.deinitialize(count: 1) + ptr.deallocate() + } +} + +@_spi(Typed) +public struct KuzuInterval { + public let months: Int32 + public let days: Int32 + public let micros: Int64 +} + +extension KuzuInterval { + public init(_ timeInterval: TimeInterval) { + self.init( + months: 0, + days: 0, + micros: Int64(timeInterval * MICROSECONDS_IN_A_SECOND) + ) + } +} + +@_spi(Typed) +public struct KuzuNode_ { + /// The internal ID of the node. + public let id: KuzuInternalId + /// The label of the node. + public let label: String + /// The properties of the node, where keys are property names and values are property values. + public let properties: [String: any KuzuDecodable] +} + +@_spi(Typed) +public struct KuzuRelationship_ { + /// The internal ID of the relationship + public let id: KuzuInternalId + /// The internal ID of the source node. + public let sourceId: KuzuInternalId + /// The internal ID of the target node. + public let targetId: KuzuInternalId + /// The label of the relationship. + public let label: String + /// The properties of the relationship, where keys are property names and values are property values. + public let properties: [String: any KuzuDecodable] +} + +/// Represents a recursive relationship retrieved from a path query in Kuzu. +/// A recursive relationship has a list of nodes and a list of relationships. +@_spi(Typed) +public struct KuzuRecursiveRelationship_ { + /// The list of nodes in the recursive relationship. + public let nodes: [KuzuNode_] + /// The list of relationships in the recursive relationship. + public let relationships: [KuzuRelationship_] +} + +// Tuples cannot conform to protocols so we need this wrapper +@_spi(Typed) +public struct KuzuMap { + public let tuples: [(Key, Value)] + + public init(_ tuples: [(Key, Value)]) { + self.tuples = tuples + } + + subscript(idx: Int) -> (Key, Value) { + tuples[idx] + } +} + +@_spi(Typed) +public struct KuzuDataType: Equatable, Sendable { + let id: kuzu_data_type_id + + public static let bool = KuzuDataType(id: KUZU_BOOL) + public static let serial = KuzuDataType(id: KUZU_SERIAL) + public static let int128 = KuzuDataType(id: KUZU_INT128) + public static let int64 = KuzuDataType(id: KUZU_INT64) + public static let uint64 = KuzuDataType(id: KUZU_UINT64) + public static let int32 = KuzuDataType(id: KUZU_INT32) + public static let uint32 = KuzuDataType(id: KUZU_UINT32) + public static let int16 = KuzuDataType(id: KUZU_INT16) + public static let uint16 = KuzuDataType(id: KUZU_UINT16) + public static let int8 = KuzuDataType(id: KUZU_INT8) + public static let uint8 = KuzuDataType(id: KUZU_UINT8) + public static let float = KuzuDataType(id: KUZU_FLOAT) + public static let double = KuzuDataType(id: KUZU_DOUBLE) + public static let interval = KuzuDataType(id: KUZU_INTERVAL) + public static let string = KuzuDataType(id: KUZU_STRING) + public static let uuid = KuzuDataType(id: KUZU_UUID) + public static let date = KuzuDataType(id: KUZU_DATE) + public static let timestamp = KuzuDataType(id: KUZU_TIMESTAMP) + public static let timestampSec = KuzuDataType(id: KUZU_TIMESTAMP_SEC) + public static let timestampNs = KuzuDataType(id: KUZU_TIMESTAMP_NS) + public static let timestampMs = KuzuDataType(id: KUZU_TIMESTAMP_MS) + public static let timestampTz = KuzuDataType(id: KUZU_TIMESTAMP_TZ) + public static let decimal = KuzuDataType(id: KUZU_DECIMAL) + public static let internalId = KuzuDataType(id: KUZU_INTERNAL_ID) + public static let blob = KuzuDataType(id: KUZU_BLOB) + public static let list = KuzuDataType(id: KUZU_LIST) + public static let array = KuzuDataType(id: KUZU_ARRAY) + public static let map = KuzuDataType(id: KUZU_MAP) + public static let `struct` = KuzuDataType(id: KUZU_STRUCT) + public static let node = KuzuDataType(id: KUZU_NODE) + public static let rel = KuzuDataType(id: KUZU_REL) + public static let recursiveRel = KuzuDataType(id: KUZU_RECURSIVE_REL) + public static let union = KuzuDataType(id: KUZU_UNION) + public static let any = KuzuDataType(id: KUZU_ANY) + + func decode(from container: consuming KuzuValue) throws -> any KuzuDecodable { + switch self { + case .bool: try Bool.kuzuDecode(from: container) + case .serial, .int64: try Int64.kuzuDecode(from: container) + case .uint64: try UInt64.kuzuDecode(from: container) + case .int32: try Int32.kuzuDecode(from: container) + case .uint32: try UInt32.kuzuDecode(from: container) + case .int16: try Int16.kuzuDecode(from: container) + case .uint16: try UInt16.kuzuDecode(from: container) + case .int8: try Int8.kuzuDecode(from: container) + case .uint8: try UInt8.kuzuDecode(from: container) + case .float: try Float.kuzuDecode(from: container) + case .double: try Double.kuzuDecode(from: container) + case .interval: try KuzuInterval.kuzuDecode(from: container) + case .string: try String.kuzuDecode(from: container) + case .uuid: try UUID.kuzuDecode(from: container) + case .date, .timestamp, .timestampSec, .timestampNs, .timestampMs, .timestampTz: try Date.kuzuDecode(from: container) + case .decimal, .int128: try Decimal.kuzuDecode(from: container) + case .internalId: try KuzuInternalId.kuzuDecode(from: container) + case .blob: try Data.kuzuDecode(from: container) + case .list, .array: try Array.kuzuDecode(from: container) + case .map: try Dictionary.kuzuDecode(from: container) + case .struct: try Dictionary.kuzuDecode(from: container) + case .node: try KuzuNode_.kuzuDecode(from: container) + case .rel: try KuzuRelationship_.kuzuDecode(from: container) + case .recursiveRel: try KuzuRecursiveRelationship_.kuzuDecode(from: container) + case .union: try KuzuAnyDecodable.kuzuUnion(from: container) + default: + throw KuzuError.valueConversionFailed("Unsupported type: \(self)") + } + } +} diff --git a/Sources/Kuzu/Util.swift b/Sources/Kuzu/Util.swift index 66c3d599..754bea22 100644 --- a/Sources/Kuzu/Util.swift +++ b/Sources/Kuzu/Util.swift @@ -5,22 +5,26 @@ // Copyright © 2023 - 2025 Kùzu Inc. // This code is licensed under MIT license (see LICENSE for details) +import cxx_kuzu +// #if canImport(FoundationEssentials) +// import FoundationEssentials +// #else import Foundation -@_implementationOnly import cxx_kuzu +// #endif /// Constants for time unit conversions -private let MILLISECONDS_IN_A_SECOND: Double = 1_000 -private let MICROSECONDS_IN_A_MILLISECOND: Double = 1_000 -private let MICROSECONDS_IN_A_SECOND: Double = +let MILLISECONDS_IN_A_SECOND: Double = 1_000 +let MICROSECONDS_IN_A_MILLISECOND: Double = 1_000 +let MICROSECONDS_IN_A_SECOND: Double = MILLISECONDS_IN_A_SECOND * MICROSECONDS_IN_A_MILLISECOND private let NANOSECONDS_IN_A_MICROSECOND: Double = 1_000 -private let NANOSECONDS_IN_A_SECOND: Double = +let NANOSECONDS_IN_A_SECOND: Double = MICROSECONDS_IN_A_SECOND * NANOSECONDS_IN_A_MICROSECOND private let SECONDS_IN_A_MINUTE: Double = 60 private let MINUTES_IN_AN_HOUR: Double = 60 private let HOURS_IN_A_DAY: Double = 24 private let DAYS_IN_A_MONTH: Double = 30 -private let SECONDS_IN_A_DAY = +let SECONDS_IN_A_DAY = HOURS_IN_A_DAY * MINUTES_IN_AN_HOUR * SECONDS_IN_A_MINUTE private let SECONDS_IN_A_MONTH = DAYS_IN_A_MONTH * SECONDS_IN_A_DAY diff --git a/Tests/kuzu-swiftTests/TypedTests.swift b/Tests/kuzu-swiftTests/TypedTests.swift new file mode 100644 index 00000000..aa7a45bd --- /dev/null +++ b/Tests/kuzu-swiftTests/TypedTests.swift @@ -0,0 +1,159 @@ +// +// kuzu-swift +// https://github.com/kuzudb/kuzu-swift +// +// Copyright © 2023 - 2025 Kùzu Inc. +// This code is licensed under MIT license (see LICENSE for details) + +import Foundation +import Testing +@testable @_spi(Typed) import Kuzu + +@Suite +struct TypedTests { + let conn: Connection + + init() throws { + let db = try Database() + self.conn = try Connection(db) + } + + @Test + func testExecutePrimatives() throws { + let stmt = try conn.prepare(""" + RETURN + $bool, + $int8, + $uint8, + $int16, + $uint16, + $int32, + $uint32, + $int64, + $uint64, + $float, + $double, + $string, + TIMESTAMP($date), + INTERVAL($interval), + UUID($uuid) + ; + """) + let result = try conn.execute_(stmt, [ + "bool": true, + "int8": Int8(8), + "uint8": UInt8(8), + "int16": Int16(16), + "uint16": UInt16(16), + "int32": Int32(32), + "uint32": UInt32(32), + "int64": Int64(64), + "uint64": UInt64(64), + "float": 3.14, + "double": 3.14159, + "string": "hello", + "date": Date(timeIntervalSince1970: 3600), + "interval": KuzuInterval(1000.0), + "uuid": UUID(uuidString: "83313081-D4D3-4175-B118-BCCE83E708D1") + ]) + + let tuple = try #require(try result.getNext()) + #expect(try tuple[0]) + #expect(try tuple[1, as: Int8.self] == 8) + #expect(try tuple[2, as: UInt8.self] == 8) + #expect(try tuple[3, as: Int16.self] == 16) + #expect(try tuple[4, as: UInt16.self] == 16) + #expect(try tuple[5, as: Int32.self] == 32) + #expect(try tuple[6, as: UInt32.self] == 32) + #expect(try tuple[7, as: Int64.self] == 64) + #expect(try tuple[8, as: UInt64.self] == 64) + #expect(try tuple[9, as: Double.self] == 3.14) + #expect(try tuple[10, as: Double.self] == 3.14159) + #expect(try tuple[11, as: String.self] == "hello") + #expect(try tuple[12, as: Date.self].timeIntervalSince1970 == 3600) + #expect(try tuple[13, as: KuzuInterval.self].micros == 1000000000) + #expect(try tuple[14, as: UUID.self] == UUID(uuidString: "83313081-D4D3-4175-B118-BCCE83E708D1")) + } + + @Test + func testExecuteNil() throws { + let stmt = try conn.prepare(""" + RETURN + $nilInt32, + $notNilInt32, + $nilString, + $notNilString + ; + """) + let result = try conn.execute_(stmt, [ + "nilInt32": nil as Int32?, + "notNilInt32": 1 as Int32?, + "nilString": nil as String?, + "notNilString": "hello" as String? + ]) + + let tuple = try #require(try result.getNext()) + #expect(try tuple[0, as: Int32?.self] == nil) + #expect(try tuple[1, as: Int32?.self] == 1) + #expect(try tuple[2, as: String?.self] == nil) + #expect(try tuple[3, as: String?.self] == "hello") + } + + @Test + func testExecuteList() throws { + let stmt = try conn.prepare(""" + RETURN + $intList, + $stringList + ; + """) + let result = try conn.execute_(stmt, [ + "intList": [1, 2, 3], + "stringList": ["hi", "there"], + ]) + + let tuple = try #require(try result.getNext()) + #expect(try tuple[0, as: [Double].self] == [1, 2, 3]) + #expect(try tuple[1, as: [String].self] == ["hi", "there"]) + } + + @Test + func testExecuteStruct() throws { + let stmt = try conn.prepare(""" + RETURN + $intStruct, + $stringStruct + ; + """) + let result = try conn.execute_(stmt, [ + "intStruct": ["1": 1, "2": 2], + "stringStruct": ["1": "1", "2": "2"] + ]) + + let tuple = try #require(try result.getNext()) + #expect(try tuple[0, as: [String: Double].self] == ["1": 1, "2": 2]) + #expect(try tuple[1, as: [String: String].self] == ["1": "1", "2": "2"]) + } + + @Test + func testExecuteMap() throws { + let stmt = try conn.prepare(""" + RETURN + $map1, + $map2 + ; + """) + let result = try conn.execute_(stmt, [ + "map1": KuzuMap([("1", 1), ("2", 2)]), + "map2": KuzuMap([("1", "1"), ("2", "2")]) + ]) + + let tuple = try #require(try result.getNext()) + let map1 = try tuple[0, as: [(String, Double)].self] + #expect(map1.map(\.0) == ["1", "2"]) + #expect(map1.map(\.1) == [1, 2]) + let map2 = try tuple[1, as: KuzuMap.self] + #expect(map2.tuples.map(\.0) == ["1", "2"]) + #expect(map2.tuples.map(\.1) == ["1", "2"]) + } +}