From 9f85435ca3412978e705d276f21172469b872b68 Mon Sep 17 00:00:00 2001 From: Vishesh Yadav Date: Thu, 11 Sep 2025 20:02:26 -0700 Subject: [PATCH 1/5] fix build in macos --- Package.swift | 10 ++++++++-- Sources/CFoundationDB/fdb_c_wrapper.h | 2 +- Sources/FoundationDB/Client.swift | 4 ++-- Sources/FoundationDB/Network.swift | 10 +++++----- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Package.swift b/Package.swift index a91215a..41343d2 100644 --- a/Package.swift +++ b/Package.swift @@ -23,8 +23,11 @@ import PackageDescription let package = Package( name: "FoundationDB", + platforms: [ + .macOS(.v14) + ], products: [ - .library(name: "FoundationDB", targets: ["FoundationDB"]), + .library(name: "FoundationDB", targets: ["FoundationDB"]) ], targets: [ .systemLibrary( @@ -33,7 +36,10 @@ let package = Package( .target( name: "FoundationDB", dependencies: ["CFoundationDB"], - path: "Sources/FoundationDB" + path: "Sources/FoundationDB", + linkerSettings: [ + .unsafeFlags(["-Xlinker", "-rpath", "-Xlinker", "/usr/local/lib"]) + ] ), .testTarget( name: "FoundationDBTests", diff --git a/Sources/CFoundationDB/fdb_c_wrapper.h b/Sources/CFoundationDB/fdb_c_wrapper.h index eb8f344..77515ab 100644 --- a/Sources/CFoundationDB/fdb_c_wrapper.h +++ b/Sources/CFoundationDB/fdb_c_wrapper.h @@ -21,7 +21,7 @@ #ifndef FDB_C_WRAPPER_H #define FDB_C_WRAPPER_H -#define FDB_API_VERSION 740 +#define FDB_API_VERSION 730 #include #endif diff --git a/Sources/FoundationDB/Client.swift b/Sources/FoundationDB/Client.swift index 3ca6772..88c82b3 100644 --- a/Sources/FoundationDB/Client.swift +++ b/Sources/FoundationDB/Client.swift @@ -37,8 +37,8 @@ import CFoundationDB public class FdbClient { /// FoundationDB API version constants. public enum APIVersion { - /// The current supported API version (740). - public static let current: Int32 = 740 + /// The current supported API version (730). + public static let current: Int32 = 730 } /// Initializes the FoundationDB client with the specified API version. diff --git a/Sources/FoundationDB/Network.swift b/Sources/FoundationDB/Network.swift index e187189..756e557 100644 --- a/Sources/FoundationDB/Network.swift +++ b/Sources/FoundationDB/Network.swift @@ -45,7 +45,7 @@ class FdbNetwork { /// Indicates whether the network has been set up. private var networkSetup = false /// The pthread handle for the network thread. - private var networkThread: pthread_t = .init() + private var networkThread: pthread_t? = nil /// Initializes the FoundationDB network with the specified API version. /// @@ -80,7 +80,7 @@ class FdbNetwork { /// /// This method must be called before starting the network thread. /// - /// - Throws: `FdbError` if network setup fails or if already set up. + /// - Throws: `FdbError` if network setup fails or if already set up.X func setupNetwork() throws { guard !networkSetup else { throw FdbError(.networkError) @@ -100,10 +100,10 @@ class FdbNetwork { /// The network must be set up before calling this method. func startNetwork() { guard networkSetup else { - fatalError("Network must be setup before starting network thread") + fatalError("Network must be setup before starting thread network") } - var thread = pthread_t() + var thread = pthread_t(bitPattern: 0) let result = pthread_create(&thread, nil, { _ in let error = fdb_run_network() if error != 0 { @@ -127,7 +127,7 @@ class FdbNetwork { } if networkSetup { - pthread_join(networkThread, nil) + pthread_join(networkThread!, nil) } networkSetup = false } From de73165fed4f9d296cf2fc34b6fade86ad044a99 Mon Sep 17 00:00:00 2001 From: Vishesh Yadav Date: Sat, 27 Sep 2025 16:54:58 -0700 Subject: [PATCH 2/5] Implement Tuple Layer --- CMakeLists.txt | 15 +- Sources/FoundationDB/Tuple.swift | 296 ++++++++++++++++++ .../FoundationDBTupleTests.swift | 126 ++++++++ 3 files changed, 433 insertions(+), 4 deletions(-) create mode 100644 Sources/FoundationDB/Tuple.swift create mode 100644 Tests/FoundationDBTests/FoundationDBTupleTests.swift diff --git a/CMakeLists.txt b/CMakeLists.txt index c8d4234..d9236b6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,10 +24,17 @@ target_include_directories(FoundationDB-Swift PUBLIC ) set(STATIC_SWIFT_LIBS - # ${SWIFT_SDK_STATIC_DIR}/libswiftCore.a - # ${SWIFT_SDK_STATIC_DIR}/libswiftDispatch.a - # ${SWIFT_SDK_STATIC_DIR}/libswiftSynchronization.a - # ${SWIFT_SDK_STATIC_DIR}/libswift_Concurrency.a + ${SWIFT_SDK_STATIC_DIR}/libswiftCore.a + ${SWIFT_SDK_STATIC_DIR}/libswiftDispatch.a + ${SWIFT_SDK_STATIC_DIR}/libswiftSynchronization.a + ${SWIFT_SDK_STATIC_DIR}/libswift_Concurrency.a + + ${SWIFT_SDK_STATIC_DIR}/libFoundation.a + ${SWIFT_SDK_STATIC_DIR}/libCoreFoundation.a + ${SWIFT_SDK_STATIC_DIR}/libFoundationInternationalization.a + ${SWIFT_SDK_STATIC_DIR}/libswiftCore.a + ${SWIFT_SDK_STATIC_DIR}/libswift_RegexParser.a + ${SWIFT_SDK_STATIC_DIR}/libswift_StringProcessing.a ) target_link_directories(FoundationDB-Swift PUBLIC diff --git a/Sources/FoundationDB/Tuple.swift b/Sources/FoundationDB/Tuple.swift new file mode 100644 index 0000000..73088f4 --- /dev/null +++ b/Sources/FoundationDB/Tuple.swift @@ -0,0 +1,296 @@ +/* + * Tuple.swift + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2016-2025 Apple Inc. and the FoundationDB project authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +public enum TupleError: Error, Sendable { + case invalidTupleElement + case invalidEncoding + case invalidDecoding(String) + case unsupportedType +} + +public enum TupleTypeCode: UInt8, CaseIterable { + case null = 0x00 + case bytes = 0x01 + case string = 0x02 + case nested = 0x05 + case intZero = 0x14 + case positiveIntEnd = 0x1D + case negativeIntStart = 0x1B + case float = 0x20 + case double = 0x21 + case boolFalse = 0x26 + case boolTrue = 0x27 + case uuid = 0x30 + case versionstamp = 0x33 +} + +public protocol TupleElement: Sendable { + func encodeTuple() -> Fdb.Bytes + static func decodeTuple(from bytes: Fdb.Bytes, at offset: inout Int) throws -> Self +} + +public struct Tuple: Sendable { + private let elements: [any TupleElement] + + public init(_ elements: any TupleElement...) { + self.elements = elements + } + + public init(_ elements: [any TupleElement]) { + self.elements = elements + } + + public subscript(index: Int) -> (any TupleElement)? { + guard index >= 0 && index < elements.count else { return nil } + return elements[index] + } + + public var count: Int { + return elements.count + } + + public func encode() -> Fdb.Bytes { + var result = Fdb.Bytes() + for element in elements { + result.append(contentsOf: element.encodeTuple()) + } + return result + } + + public static func decode(from bytes: Fdb.Bytes) throws -> [any TupleElement] { + var elements: [any TupleElement] = [] + var offset = 0 + + while offset < bytes.count { + let typeCode = bytes[offset] + offset += 1 + + switch typeCode { + case TupleTypeCode.null.rawValue: + elements.append(TupleNil()) + case TupleTypeCode.bytes.rawValue: + let element = try Fdb.Bytes.decodeTuple(from: bytes, at: &offset) + elements.append(element) + case TupleTypeCode.string.rawValue: + let element = try String.decodeTuple(from: bytes, at: &offset) + elements.append(element) + case TupleTypeCode.boolFalse.rawValue, TupleTypeCode.boolTrue.rawValue: + let element = try Bool.decodeTuple(from: bytes, at: &offset) + elements.append(element) + case TupleTypeCode.float.rawValue: + let element = try Float.decodeTuple(from: bytes, at: &offset) + elements.append(element) + case TupleTypeCode.double.rawValue: + let element = try Double.decodeTuple(from: bytes, at: &offset) + elements.append(element) + case TupleTypeCode.uuid.rawValue: + let element = try UUID.decodeTuple(from: bytes, at: &offset) + elements.append(element) + default: + throw TupleError.invalidDecoding("Unknown type code: \(typeCode)") + } + } + + return elements + } +} + +public struct TupleNil: TupleElement { + public func encodeTuple() -> Fdb.Bytes { + return [TupleTypeCode.null.rawValue] + } + + public static func decodeTuple(from bytes: Fdb.Bytes, at offset: inout Int) throws -> TupleNil { + return TupleNil() + } +} + +extension String: TupleElement { + public func encodeTuple() -> Fdb.Bytes { + var encoded = [TupleTypeCode.string.rawValue] + let utf8Bytes = Array(self.utf8) + + for byte in utf8Bytes { + if byte == 0x00 { + encoded.append(contentsOf: [0x00, 0xFF]) + } else { + encoded.append(byte) + } + } + encoded.append(0x00) + return encoded + } + + public static func decodeTuple(from bytes: Fdb.Bytes, at offset: inout Int) throws -> String { + var decoded = Fdb.Bytes() + + while offset < bytes.count { + let byte = bytes[offset] + offset += 1 + + if byte == 0x00 { + if offset < bytes.count && bytes[offset] == 0xFF { + offset += 1 + decoded.append(0x00) + } else { + break + } + } else { + decoded.append(byte) + } + } + + return String(bytes: decoded, encoding: .utf8)! + } +} + +extension Fdb.Bytes: TupleElement { + public func encodeTuple() -> Fdb.Bytes { + var encoded = [TupleTypeCode.bytes.rawValue] + for byte in self { + if byte == 0x00 { + encoded.append(contentsOf: [0x00, 0xFF]) + } else { + encoded.append(byte) + } + } + encoded.append(0x00) + return encoded + } + + public static func decodeTuple(from bytes: Fdb.Bytes, at offset: inout Int) throws -> Fdb.Bytes { + var decoded = Fdb.Bytes() + + while offset < bytes.count { + let byte = bytes[offset] + offset += 1 + + if byte == 0x00 { + if offset < bytes.count && bytes[offset] == 0xFF { + offset += 1 + decoded.append(0x00) + } else { + break + } + } else { + decoded.append(byte) + } + } + + return decoded + } +} + +extension Bool: TupleElement { + public func encodeTuple() -> Fdb.Bytes { + return self ? [TupleTypeCode.boolTrue.rawValue] : [TupleTypeCode.boolFalse.rawValue] + } + + public static func decodeTuple(from bytes: Fdb.Bytes, at offset: inout Int) throws -> Bool { + guard offset > 0 else { throw TupleError.invalidDecoding("Bool decoding requires type code") } + let typeCode = bytes[offset - 1] + + switch typeCode { + case TupleTypeCode.boolTrue.rawValue: + return true + case TupleTypeCode.boolFalse.rawValue: + return false + default: + throw TupleError.invalidDecoding("Invalid bool type code: \(typeCode)") + } + } +} + +extension Float: TupleElement { + public func encodeTuple() -> Fdb.Bytes { + var encoded = [TupleTypeCode.float.rawValue] + let bitPattern = self.bitPattern + let bytes = withUnsafeBytes(of: bitPattern.bigEndian) { Array($0) } + encoded.append(contentsOf: bytes) + return encoded + } + + public static func decodeTuple(from bytes: Fdb.Bytes, at offset: inout Int) throws -> Float { + guard offset + 4 <= bytes.count else { + throw TupleError.invalidDecoding("Not enough bytes for Float") + } + + let floatBytes = Array(bytes[offset.. Fdb.Bytes { + var encoded = [TupleTypeCode.double.rawValue] + let bitPattern = self.bitPattern + let bytes = withUnsafeBytes(of: bitPattern.bigEndian) { Array($0) } + encoded.append(contentsOf: bytes) + return encoded + } + + public static func decodeTuple(from bytes: Fdb.Bytes, at offset: inout Int) throws -> Double { + guard offset + 8 <= bytes.count else { + throw TupleError.invalidDecoding("Not enough bytes for Double") + } + + let doubleBytes = Array(bytes[offset.. Fdb.Bytes { + var encoded = [TupleTypeCode.uuid.rawValue] + let (u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u11, u12, u13, u14, u15, u16) = self.uuid + encoded.append(contentsOf: [u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u11, u12, u13, u14, u15, u16]) + return encoded + } + + public static func decodeTuple(from bytes: Fdb.Bytes, at offset: inout Int) throws -> UUID { + guard offset + 16 <= bytes.count else { + throw TupleError.invalidDecoding("Not enough bytes for UUID") + } + + let uuidBytes = Array(bytes[offset.. Date: Mon, 29 Sep 2025 19:40:14 -0700 Subject: [PATCH 3/5] Integer arithmatic and nested Tuples --- Sources/FoundationDB/Tuple.swift | 253 +++++++++++++++++- .../FoundationDBTupleTests.swift | 171 ++++++++++++ 2 files changed, 423 insertions(+), 1 deletion(-) diff --git a/Sources/FoundationDB/Tuple.swift b/Sources/FoundationDB/Tuple.swift index 73088f4..3e253e0 100644 --- a/Sources/FoundationDB/Tuple.swift +++ b/Sources/FoundationDB/Tuple.swift @@ -32,9 +32,9 @@ public enum TupleTypeCode: UInt8, CaseIterable { case bytes = 0x01 case string = 0x02 case nested = 0x05 + case negativeIntStart = 0x0B case intZero = 0x14 case positiveIntEnd = 0x1D - case negativeIntStart = 0x1B case float = 0x20 case double = 0x21 case boolFalse = 0x26 @@ -105,6 +105,14 @@ public struct Tuple: Sendable { case TupleTypeCode.uuid.rawValue: let element = try UUID.decodeTuple(from: bytes, at: &offset) elements.append(element) + case TupleTypeCode.intZero.rawValue: + elements.append(0) + case TupleTypeCode.negativeIntStart.rawValue...TupleTypeCode.positiveIntEnd.rawValue: + let element = try Int64.decodeTuple(from: bytes, at: &offset) + elements.append(element) + case TupleTypeCode.nested.rawValue: + let element = try Tuple.decodeTuple(from: bytes, at: &offset) + elements.append(element) default: throw TupleError.invalidDecoding("Unknown type code: \(typeCode)") } @@ -294,3 +302,246 @@ extension UUID: TupleElement { return UUID(uuid: uuidTuple) } } + +private let sizeLimits: [UInt64] = [ + (1 << (0 * 8)) - 1, + (1 << (1 * 8)) - 1, + (1 << (2 * 8)) - 1, + (1 << (3 * 8)) - 1, + (1 << (4 * 8)) - 1, + (1 << (5 * 8)) - 1, + (1 << (6 * 8)) - 1, + (1 << (7 * 8)) - 1, + UInt64.max // (1 << (8 * 8)) - 1 would overflow, so use UInt64.max instead +] + +private func bisectLeft(_ value: UInt64) -> Int { + var n = 0 + while n < sizeLimits.count && sizeLimits[n] < value { + n += 1 + } + return n +} + +extension Int64: TupleElement { + public func encodeTuple() -> Fdb.Bytes { + if self == 0 { + return [TupleTypeCode.intZero.rawValue] + } + + if self >= 0 { + return encodeUint(UInt64(self)) + } else { + return encodeInt(self) + } + } + + private func encodeUint(_ value: UInt64) -> Fdb.Bytes { + let n = bisectLeft(value) + + if n >= 8 { + var encoded = [TupleTypeCode.positiveIntEnd.rawValue, UInt8(8)] + let bigEndianValue = value.bigEndian + let bytes = withUnsafeBytes(of: bigEndianValue) { Array($0) } + encoded.append(contentsOf: bytes) + return encoded + } + + var encoded = [TupleTypeCode.intZero.rawValue + UInt8(n)] + let bigEndianValue = value.bigEndian + let bytes = withUnsafeBytes(of: bigEndianValue) { Array($0) } + encoded.append(contentsOf: bytes.suffix(n)) + return encoded + } + + private func encodeInt(_ value: Int64) -> Fdb.Bytes { + let absValue = value == Int64.min ? UInt64(Int64.max) &+ 1 : UInt64(-value) + let n = bisectLeft(absValue) + + if n >= 8 { + var encoded = [TupleTypeCode.negativeIntStart.rawValue, UInt8(8) ^ 0xFF] + let onesComplement = value &- 1 + let bigEndianValue = UInt64(bitPattern: onesComplement).bigEndian + let bytes = withUnsafeBytes(of: bigEndianValue) { Array($0) } + encoded.append(contentsOf: bytes) + return encoded + } + + var encoded = [TupleTypeCode.intZero.rawValue - UInt8(n)] + let maxValue = Int64(sizeLimits[n]) + let offsetEncoded = maxValue &+ value + let bigEndianValue = UInt64(bitPattern: offsetEncoded).bigEndian + let bytes = withUnsafeBytes(of: bigEndianValue) { Array($0) } + encoded.append(contentsOf: bytes.suffix(n)) + return encoded + } + + public static func decodeTuple(from bytes: Fdb.Bytes, at offset: inout Int) throws -> Int64 { + guard offset > 0 else { throw TupleError.invalidDecoding("Int64 decoding requires type code") } + let typeCode = bytes[offset - 1] + + if typeCode == TupleTypeCode.intZero.rawValue { + return 0 + } + + if typeCode == TupleTypeCode.positiveIntEnd.rawValue { + guard offset < bytes.count else { + throw TupleError.invalidDecoding("Not enough bytes for large positive integer length") + } + let byteLength = Int(bytes[offset]) + offset += 1 + return try decodePositiveInt(from: bytes, at: &offset, byteLength: byteLength) + } + + if typeCode == TupleTypeCode.negativeIntStart.rawValue { + guard offset < bytes.count else { + throw TupleError.invalidDecoding("Not enough bytes for large negative integer length") + } + let byteLength = Int(bytes[offset] ^ 0xFF) + offset += 1 + return try decodeNegativeInt(from: bytes, at: &offset, byteLength: byteLength) + } + + let n = Int(typeCode) - Int(TupleTypeCode.intZero.rawValue) + let isNegative = n < 0 + let byteLength = abs(n) + + guard offset + byteLength <= bytes.count else { + throw TupleError.invalidDecoding("Not enough bytes for integer") + } + + let intBytes = Array(bytes[offset.. Int64 { + guard offset + byteLength <= bytes.count else { + throw TupleError.invalidDecoding("Not enough bytes for positive integer") + } + + let intBytes = Array(bytes[offset.. Int64 { + guard offset + byteLength <= bytes.count else { + throw TupleError.invalidDecoding("Not enough bytes for negative integer") + } + + let intBytes = Array(bytes[offset.. Fdb.Bytes { + var encoded = [TupleTypeCode.nested.rawValue] + for element in elements { + let elementBytes = element.encodeTuple() + for byte in elementBytes { + if byte == 0x00 { + encoded.append(contentsOf: [0x00, 0xFF]) + } else { + encoded.append(byte) + } + } + } + encoded.append(0x00) + return encoded + } + + public static func decodeTuple(from bytes: Fdb.Bytes, at offset: inout Int) throws -> Tuple { + var nestedBytes = Fdb.Bytes() + + while offset < bytes.count { + let byte = bytes[offset] + offset += 1 + + if byte == 0x00 { + if offset < bytes.count && bytes[offset] == 0xFF { + offset += 1 + nestedBytes.append(0x00) + } else { + break + } + } else { + nestedBytes.append(byte) + } + } + + let nestedElements = try Tuple.decode(from: nestedBytes) + return Tuple(nestedElements) + } +} + +extension Int: TupleElement { + public func encodeTuple() -> Fdb.Bytes { + return Int64(self).encodeTuple() + } + + public static func decodeTuple(from bytes: Fdb.Bytes, at offset: inout Int) throws -> Int { + let value = try Int64.decodeTuple(from: bytes, at: &offset) + guard value >= Int.min && value <= Int.max else { + throw TupleError.invalidDecoding("Int64 value \(value) out of range for Int") + } + return Int(value) + } +} + +extension Int32: TupleElement { + public func encodeTuple() -> Fdb.Bytes { + return Int64(self).encodeTuple() + } + + public static func decodeTuple(from bytes: Fdb.Bytes, at offset: inout Int) throws -> Int32 { + let value = try Int64.decodeTuple(from: bytes, at: &offset) + guard value >= Int32.min && value <= Int32.max else { + throw TupleError.invalidDecoding("Int64 value \(value) out of range for Int32") + } + return Int32(value) + } +} + +extension UInt64: TupleElement { + public func encodeTuple() -> Fdb.Bytes { + if self <= Int64.max { + return Int64(self).encodeTuple() + } else { + return Int64.max.encodeTuple() + } + } + + public static func decodeTuple(from bytes: Fdb.Bytes, at offset: inout Int) throws -> UInt64 { + let value = try Int64.decodeTuple(from: bytes, at: &offset) + guard value >= 0 else { + throw TupleError.invalidDecoding("Negative value \(value) cannot be converted to UInt64") + } + return UInt64(value) + } +} diff --git a/Tests/FoundationDBTests/FoundationDBTupleTests.swift b/Tests/FoundationDBTests/FoundationDBTupleTests.swift index 38ec453..f963b4e 100644 --- a/Tests/FoundationDBTests/FoundationDBTupleTests.swift +++ b/Tests/FoundationDBTests/FoundationDBTupleTests.swift @@ -124,3 +124,174 @@ func testTupleUUID() throws { #expect(decoded == testUUID, "Should decode back to original UUID") #expect(offset == encoded.count, "Offset should advance to end of encoded data") } + +@Test("TupleInt64 encoding and decoding - Zero") +func testTupleInt64Zero() throws { + let testInt: Int64 = 0 + let encoded = testInt.encodeTuple() + + #expect(encoded == [TupleTypeCode.intZero.rawValue], "Zero should encode to intZero type code") + + var offset = 1 + let decoded = try Int64.decodeTuple(from: encoded, at: &offset) + #expect(decoded == testInt, "Should decode back to original zero") + #expect(offset == encoded.count, "Offset should advance to end of encoded data") +} + +@Test("TupleInt64 encoding and decoding - Small positive") +func testTupleInt64SmallPositive() throws { + let testInt: Int64 = 42 + let encoded = testInt.encodeTuple() + + #expect(encoded.first == 0x15, "Small positive should use 0x15 type code (positiveInt1)") + + var offset = 1 + let decoded = try Int64.decodeTuple(from: encoded, at: &offset) + #expect(decoded == testInt, "Should decode back to original positive integer") + #expect(offset == encoded.count, "Offset should advance to end of encoded data") +} + +@Test("TupleInt64 encoding and decoding - Large negative") +func testTupleInt64LargeNegative() throws { + let testInt: Int64 = -89_034_333_444 + let encoded = testInt.encodeTuple() + + // #expect(encoded.first == 0x13, "Small negative should use 0x13 type code (negativeInt1)") + + var offset = 1 + let decoded = try Int64.decodeTuple(from: encoded, at: &offset) + #expect(decoded == testInt, "Should decode back to original negative integer") + #expect(offset == encoded.count, "Offset should advance to end of encoded data") +} + +@Test("TupleInt64 encoding and decoding - Very Large negative") +func testTupleInt64VeryLargeNegative() throws { + let testInt: Int64 = -(1 << 55) - 34897432 + let encoded = testInt.encodeTuple() + + var offset = 1 + let decoded = try Int64.decodeTuple(from: encoded, at: &offset) + #expect(decoded == testInt, "Should decode back to original negative integer") + #expect(offset == encoded.count, "Offset should advance to end of encoded data") +} + +@Test("TupleInt64 encoding and decoding - Very Large negative") +func testTupleInt64VeryLargeNegative2() throws { + let testInt: Int64 = -(1 << 60) - 34897432 + let encoded = testInt.encodeTuple() + + var offset = 1 + let decoded = try Int64.decodeTuple(from: encoded, at: &offset) + #expect(decoded == testInt, "Should decode back to original negative integer") + #expect(offset == encoded.count, "Offset should advance to end of encoded data") +} + +@Test("TupleInt64 encoding and decoding - Large values") +func testTupleInt64LargeValues() throws { + let largePositive: Int64 = Int64.max + let largeNegative: Int64 = Int64.min + 1 + + let encodedPos = largePositive.encodeTuple() + // let encodedNeg = largeNegative.encodeTuple() + + var offsetPos = 1 + var offsetNeg = 1 + + let decodedPos = try Int64.decodeTuple(from: encodedPos, at: &offsetPos) + // let decodedNeg = try Int64.decodeTuple(from: encodedNeg, at: &offsetNeg) + + #expect(decodedPos == largePositive, "Should decode back to Int64.max") + // #expect(decodedNeg == largeNegative, "Should decode back to Int64.min") +} + +@Test("TupleInt32 encoding and decoding") +func testTupleInt32() throws { + let testInt: Int32 = -2_034_333_444 + let encoded = testInt.encodeTuple() + + var offset = 1 + let decoded = try Int32.decodeTuple(from: encoded, at: &offset) + #expect(decoded == testInt, "Should decode back to original Int32") +} + +@Test("TupleInt encoding and decoding") +func testTupleInt() throws { + let testInt: Int = 123456 + let encoded = testInt.encodeTuple() + + var offset = 1 + let decoded = try Int.decodeTuple(from: encoded, at: &offset) + #expect(decoded == testInt, "Should decode back to original Int") +} + +@Test("TupleUInt64 encoding and decoding") +func testTupleUInt64() throws { + let testUInt: UInt64 = 999999 + let encoded = testUInt.encodeTuple() + + var offset = 1 + let decoded = try UInt64.decodeTuple(from: encoded, at: &offset) + #expect(decoded == testUInt, "Should decode back to original UInt64") +} + +@Test("TupleNested encoding and decoding") +func testTupleNested() throws { + let innerTuple = Tuple("hello", 42, true) + let outerTuple = Tuple("outer", innerTuple, "end") + + let encoded = outerTuple.encode() + let decoded = try Tuple.decode(from: encoded) + + #expect(decoded.count == 3, "Should have 3 elements") + + let decodedString1 = decoded[0] as? String + #expect(decodedString1 == "outer", "First element should be 'outer'") + + let decodedNested = decoded[1] as? Tuple + #expect(decodedNested != nil, "Second element should be a Tuple") + #expect(decodedNested?.count == 3, "Nested tuple should have 3 elements") + + let decodedString2 = decoded[2] as? String + #expect(decodedString2 == "end", "Third element should be 'end'") +} + +@Test("Tuple with a zero integer") +func testTupleWithZero() throws { + let tuple = Tuple("hello", 0, "foo") + + let encoded = tuple.encode() + let decoded = try Tuple.decode(from: encoded) + + #expect(decoded.count == 3, "Should have 3 elements") + let decodedString1 = decoded[0] as? String + #expect(decodedString1 == "hello") + + let decodedInt = decoded[1] as? Int + #expect(decodedInt == 0) + + let decodedString2 = decoded[2] as? String + #expect(decodedString2 == "foo") +} + + +@Test("TupleNested deep nesting") +func testTupleNestedDeep() throws { + let level3 = Tuple("deep", 123) + let level2 = Tuple("middle", level3) + let level1 = Tuple("top", level2, "bottom") + + let encoded = level1.encode() + let decoded = try Tuple.decode(from: encoded) + + #expect(decoded.count == 3, "Top level should have 3 elements") + + let topString = decoded[0] as? String + #expect(topString == "top", "First element should be 'top'") + + let middleTuple = decoded[1] as? Tuple + #expect(middleTuple != nil, "Second element should be a Tuple") + #expect(middleTuple?.count == 2, "Middle tuple should have 2 elements") + + let bottomString = decoded[2] as? String + #expect(bottomString == "bottom", "Third element should be 'bottom'") +} From 86a06e165fcf03536e903b8c673fbc343865a885 Mon Sep 17 00:00:00 2001 From: Vishesh Yadav Date: Thu, 2 Oct 2025 18:15:50 -0700 Subject: [PATCH 4/5] make integer encoding consistent with other bindings taking Go binding as reference --- Sources/FoundationDB/Tuple.swift | 182 +++++++----------- .../FoundationDBTupleTests.swift | 49 ++++- 2 files changed, 113 insertions(+), 118 deletions(-) diff --git a/Sources/FoundationDB/Tuple.swift b/Sources/FoundationDB/Tuple.swift index 3e253e0..76d6b3c 100644 --- a/Sources/FoundationDB/Tuple.swift +++ b/Sources/FoundationDB/Tuple.swift @@ -73,7 +73,7 @@ public struct Tuple: Sendable { for element in elements { result.append(contentsOf: element.encodeTuple()) } - return result + return result } public static func decode(from bytes: Fdb.Bytes) throws -> [any TupleElement] { @@ -185,7 +185,8 @@ extension Fdb.Bytes: TupleElement { return encoded } - public static func decodeTuple(from bytes: Fdb.Bytes, at offset: inout Int) throws -> Fdb.Bytes { + public static func decodeTuple(from bytes: Fdb.Bytes, at offset: inout Int) throws -> Fdb.Bytes + { var decoded = Fdb.Bytes() while offset < bytes.count { @@ -214,7 +215,9 @@ extension Bool: TupleElement { } public static func decodeTuple(from bytes: Fdb.Bytes, at offset: inout Int) throws -> Bool { - guard offset > 0 else { throw TupleError.invalidDecoding("Bool decoding requires type code") } + guard offset > 0 else { + throw TupleError.invalidDecoding("Bool decoding requires type code") + } let typeCode = bytes[offset - 1] switch typeCode { @@ -282,7 +285,9 @@ extension UUID: TupleElement { public func encodeTuple() -> Fdb.Bytes { var encoded = [TupleTypeCode.uuid.rawValue] let (u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u11, u12, u13, u14, u15, u16) = self.uuid - encoded.append(contentsOf: [u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u11, u12, u13, u14, u15, u16]) + encoded.append(contentsOf: [ + u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, u11, u12, u13, u14, u15, u16, + ]) return encoded } @@ -294,10 +299,12 @@ extension UUID: TupleElement { let uuidBytes = Array(bytes[offset.. Int { @@ -325,137 +332,81 @@ private func bisectLeft(_ value: UInt64) -> Int { extension Int64: TupleElement { public func encodeTuple() -> Fdb.Bytes { - if self == 0 { - return [TupleTypeCode.intZero.rawValue] - } - - if self >= 0 { - return encodeUint(UInt64(self)) - } else { - return encodeInt(self) - } - } - - private func encodeUint(_ value: UInt64) -> Fdb.Bytes { - let n = bisectLeft(value) - - if n >= 8 { - var encoded = [TupleTypeCode.positiveIntEnd.rawValue, UInt8(8)] - let bigEndianValue = value.bigEndian - let bytes = withUnsafeBytes(of: bigEndianValue) { Array($0) } - encoded.append(contentsOf: bytes) - return encoded - } - - var encoded = [TupleTypeCode.intZero.rawValue + UInt8(n)] - let bigEndianValue = value.bigEndian - let bytes = withUnsafeBytes(of: bigEndianValue) { Array($0) } - encoded.append(contentsOf: bytes.suffix(n)) - return encoded + return encodeInt(self) } private func encodeInt(_ value: Int64) -> Fdb.Bytes { - let absValue = value == Int64.min ? UInt64(Int64.max) &+ 1 : UInt64(-value) - let n = bisectLeft(absValue) + if value == 0 { + return [TupleTypeCode.intZero.rawValue] + } - if n >= 8 { - var encoded = [TupleTypeCode.negativeIntStart.rawValue, UInt8(8) ^ 0xFF] - let onesComplement = value &- 1 - let bigEndianValue = UInt64(bitPattern: onesComplement).bigEndian + var encoded = Fdb.Bytes() + if value > 0 { + let n = bisectLeft(UInt64(value)) + encoded.append(TupleTypeCode.intZero.rawValue + UInt8(n)) + let bigEndianValue = UInt64(bitPattern: value).bigEndian let bytes = withUnsafeBytes(of: bigEndianValue) { Array($0) } - encoded.append(contentsOf: bytes) - return encoded + encoded.append(contentsOf: bytes.suffix(n)) + } else { + let n = bisectLeft(UInt64(-value)) + encoded.append(TupleTypeCode.intZero.rawValue - UInt8(n)) + + if n < 8 { + let offset = UInt64(sizeLimits[n]) &+ UInt64(bitPattern: value) + let bigEndianValue = offset.bigEndian + let bytes = withUnsafeBytes(of: bigEndianValue) { Array($0) } + encoded.append(contentsOf: bytes.suffix(n)) + } else { + // n == 8 case + let offset = UInt64(bitPattern: value) + let bigEndianValue = offset.bigEndian + let bytes = withUnsafeBytes(of: bigEndianValue) { Array($0) } + encoded.append(contentsOf: bytes) + } } - var encoded = [TupleTypeCode.intZero.rawValue - UInt8(n)] - let maxValue = Int64(sizeLimits[n]) - let offsetEncoded = maxValue &+ value - let bigEndianValue = UInt64(bitPattern: offsetEncoded).bigEndian - let bytes = withUnsafeBytes(of: bigEndianValue) { Array($0) } - encoded.append(contentsOf: bytes.suffix(n)) return encoded } public static func decodeTuple(from bytes: Fdb.Bytes, at offset: inout Int) throws -> Int64 { - guard offset > 0 else { throw TupleError.invalidDecoding("Int64 decoding requires type code") } + guard offset > 0 else { + throw TupleError.invalidDecoding("Int64 decoding requires type code") + } let typeCode = bytes[offset - 1] if typeCode == TupleTypeCode.intZero.rawValue { return 0 } - if typeCode == TupleTypeCode.positiveIntEnd.rawValue { - guard offset < bytes.count else { - throw TupleError.invalidDecoding("Not enough bytes for large positive integer length") - } - let byteLength = Int(bytes[offset]) - offset += 1 - return try decodePositiveInt(from: bytes, at: &offset, byteLength: byteLength) - } - - if typeCode == TupleTypeCode.negativeIntStart.rawValue { - guard offset < bytes.count else { - throw TupleError.invalidDecoding("Not enough bytes for large negative integer length") - } - let byteLength = Int(bytes[offset] ^ 0xFF) - offset += 1 - return try decodeNegativeInt(from: bytes, at: &offset, byteLength: byteLength) + var n = Int(typeCode) - Int(TupleTypeCode.intZero.rawValue) + var neg = false + if n < 0 { + n = -n + neg = true } - let n = Int(typeCode) - Int(TupleTypeCode.intZero.rawValue) - let isNegative = n < 0 - let byteLength = abs(n) + var bp = [UInt8](repeating: 0, count: 8) + bp.replaceSubrange((8 - n)..<8, with: bytes[offset...(offset+n-1)]) + offset += n - guard offset + byteLength <= bytes.count else { - throw TupleError.invalidDecoding("Not enough bytes for integer") + var ret: Int64 = 0 + for byte in bp { + ret = (ret << 8) | Int64(byte) } - let intBytes = Array(bytes[offset.. Int64 { - guard offset + byteLength <= bytes.count else { - throw TupleError.invalidDecoding("Not enough bytes for positive integer") - } - - let intBytes = Array(bytes[offset.. Int64 { - guard offset + byteLength <= bytes.count else { - throw TupleError.invalidDecoding("Not enough bytes for negative integer") - } - - let intBytes = Array(bytes[offset.. 0 { + return ret } - // The encoding does onesComplement = value &- 1, so we need to add 1 back when decoding - let maxValue = Int64(1) << (byteLength * 8) - return value - maxValue + 1 + return ret } } @@ -540,7 +491,8 @@ extension UInt64: TupleElement { public static func decodeTuple(from bytes: Fdb.Bytes, at offset: inout Int) throws -> UInt64 { let value = try Int64.decodeTuple(from: bytes, at: &offset) guard value >= 0 else { - throw TupleError.invalidDecoding("Negative value \(value) cannot be converted to UInt64") + throw TupleError.invalidDecoding( + "Negative value \(value) cannot be converted to UInt64") } return UInt64(value) } diff --git a/Tests/FoundationDBTests/FoundationDBTupleTests.swift b/Tests/FoundationDBTests/FoundationDBTupleTests.swift index f963b4e..77f03b7 100644 --- a/Tests/FoundationDBTests/FoundationDBTupleTests.swift +++ b/Tests/FoundationDBTests/FoundationDBTupleTests.swift @@ -151,13 +151,24 @@ func testTupleInt64SmallPositive() throws { #expect(offset == encoded.count, "Offset should advance to end of encoded data") } +@Test("TupleInt64 encoding and decoding - Very small negative") +func testTupleInt64VerySmallNegative() throws { + let testInt: Int64 = -42 + let encoded = testInt.encodeTuple() + + #expect(encoded.first == 0x13) + + var offset = 1 + let decoded = try Int64.decodeTuple(from: encoded, at: &offset) + #expect(decoded == testInt, "Should decode back to original positive integer") + #expect(offset == encoded.count, "Offset should advance to end of encoded data") +} + @Test("TupleInt64 encoding and decoding - Large negative") func testTupleInt64LargeNegative() throws { let testInt: Int64 = -89_034_333_444 let encoded = testInt.encodeTuple() - // #expect(encoded.first == 0x13, "Small negative should use 0x13 type code (negativeInt1)") - var offset = 1 let decoded = try Int64.decodeTuple(from: encoded, at: &offset) #expect(decoded == testInt, "Should decode back to original negative integer") @@ -175,7 +186,7 @@ func testTupleInt64VeryLargeNegative() throws { #expect(offset == encoded.count, "Offset should advance to end of encoded data") } -@Test("TupleInt64 encoding and decoding - Very Large negative") +@Test("TupleInt64 encoding and decoding - VeryVery Large negative") func testTupleInt64VeryLargeNegative2() throws { let testInt: Int64 = -(1 << 60) - 34897432 let encoded = testInt.encodeTuple() @@ -295,3 +306,35 @@ func testTupleNestedDeep() throws { let bottomString = decoded[2] as? String #expect(bottomString == "bottom", "Third element should be 'bottom'") } + +@Test("TupleInt64 encoding and decoding - 1 million distributed integers") +func testTupleInt64DistributedIntegers() throws { + // Deterministic random number generator using LCG algorithm + var seed: UInt64 = 12345 + func nextRandom() -> Int64 { + // Generate full 64-bit value + seed = seed &* 6364136223846793005 &+ 1442695040888963407 + return Int64(bitPattern: seed) + } + + // Test 10000 integers + var positive: Int = 0 + var negative: Int = 0 + for _ in 0..<1000000 { + let testInt = nextRandom() + let encoded = testInt.encodeTuple() + + if testInt > 0 { + positive += 1 + } else if testInt < 0 { + negative += 1 + } + + var offset = 1 + let decoded = try Int64.decodeTuple(from: encoded, at: &offset) + #expect(decoded == testInt, "Integer \(testInt) should encode and decode correctly") + #expect(offset == encoded.count, "Offset should advance to end of encoded data") + } + + print("tested with n_positives = \(positive), n_negatives = \(negative)") +} From d58307684c9228093821b2a59e1cce88992e6446 Mon Sep 17 00:00:00 2001 From: Vishesh Yadav Date: Thu, 2 Oct 2025 18:20:44 -0700 Subject: [PATCH 5/5] Test the max negative value encoding --- Tests/FoundationDBTests/FoundationDBTupleTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/FoundationDBTests/FoundationDBTupleTests.swift b/Tests/FoundationDBTests/FoundationDBTupleTests.swift index 77f03b7..9aa77da 100644 --- a/Tests/FoundationDBTests/FoundationDBTupleTests.swift +++ b/Tests/FoundationDBTests/FoundationDBTupleTests.swift @@ -203,16 +203,16 @@ func testTupleInt64LargeValues() throws { let largeNegative: Int64 = Int64.min + 1 let encodedPos = largePositive.encodeTuple() - // let encodedNeg = largeNegative.encodeTuple() + let encodedNeg = largeNegative.encodeTuple() var offsetPos = 1 var offsetNeg = 1 let decodedPos = try Int64.decodeTuple(from: encodedPos, at: &offsetPos) - // let decodedNeg = try Int64.decodeTuple(from: encodedNeg, at: &offsetNeg) + let decodedNeg = try Int64.decodeTuple(from: encodedNeg, at: &offsetNeg) #expect(decodedPos == largePositive, "Should decode back to Int64.max") - // #expect(decodedNeg == largeNegative, "Should decode back to Int64.min") + #expect(decodedNeg == largeNegative, "Should decode back to Int64.min") } @Test("TupleInt32 encoding and decoding")