From 9f85435ca3412978e705d276f21172469b872b68 Mon Sep 17 00:00:00 2001 From: Vishesh Yadav Date: Thu, 11 Sep 2025 20:02:26 -0700 Subject: [PATCH 01/16] 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 02/16] 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 03/16] 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 04/16] 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 05/16] 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") From 3fe5a4ffe0df3f3b277ca546f4417fc025fdd799 Mon Sep 17 00:00:00 2001 From: Vishesh Yadav Date: Thu, 2 Oct 2025 23:16:40 -0700 Subject: [PATCH 06/16] Implement StackTester --- CMakeLists.txt | 18 +- Package.swift | 8 +- Sources/CFoundationDB/fdb_c_wrapper.h | 2 +- Sources/FoundationDB/Client.swift | 2 +- Sources/FoundationDB/Future.swift | 4 +- Sources/FoundationDB/Network.swift | 37 +- .../Sources/StackTester/StackTester.swift | 693 ++++++++++++++++++ .../Sources/StackTester/main.swift | 77 ++ 8 files changed, 815 insertions(+), 26 deletions(-) create mode 100644 Tests/StackTester/Sources/StackTester/StackTester.swift create mode 100644 Tests/StackTester/Sources/StackTester/main.swift diff --git a/CMakeLists.txt b/CMakeLists.txt index d9236b6..9dd8697 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ endif() set(FDB_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/bindings/c) set(SWIFT_SDK_DIR "/usr/lib/swift/linux/") -set(SWIFT_SDK_STATIC_DIR "/usr/libexec/swift/6.0.3/lib/swift_static/linux") +set(SWIFT_SDK_STATIC_DIR "/usr/lib/swift_static/linux") file(GLOB_RECURSE SWIFT_BINDING_SOURCES "Sources/**/*.swift") add_library(FoundationDB-Swift ${SWIFT_BINDING_SOURCES}) @@ -71,10 +71,22 @@ set_target_properties(FoundationDB-Swift-Tests PROPERTIES Swift_MODULE_NAME "FoundationDBTests" ) -install(TARGETS FoundationDB-Swift +add_executable(stacktester_swift + Tests/StackTester/Sources/StackTester/StackTester.swift + Tests/StackTester/Sources/StackTester/main.swift +) +add_dependencies(stacktester_swift FoundationDB-Swift) +target_link_libraries(stacktester_swift PRIVATE FoundationDB-Swift) + +set_target_properties(stacktester_swift PROPERTIES + Swift_MODULE_NAME "StackTester" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bindings/swift/bin" +) + +install(TARGETS FoundationDB-Swift stacktester_swift + RUNTIME DESTINATION bindings/swift/bin ARCHIVE DESTINATION lib LIBRARY DESTINATION lib ) -# Configure test to run with Swift Testing add_test(NAME FoundationDB-Swift-Tests COMMAND FoundationDB-Swift-Tests) diff --git a/Package.swift b/Package.swift index 41343d2..f5e7eb2 100644 --- a/Package.swift +++ b/Package.swift @@ -27,7 +27,8 @@ let package = Package( .macOS(.v14) ], products: [ - .library(name: "FoundationDB", targets: ["FoundationDB"]) + .library(name: "FoundationDB", targets: ["FoundationDB"]), + .executable(name: "stacktester", targets: ["StackTester"]) ], targets: [ .systemLibrary( @@ -45,5 +46,10 @@ let package = Package( name: "FoundationDBTests", dependencies: ["FoundationDB"] ), + .executableTarget( + name: "StackTester", + dependencies: ["FoundationDB"], + path: "Tests/StackTester/Sources/StackTester" + ), ] ) diff --git a/Sources/CFoundationDB/fdb_c_wrapper.h b/Sources/CFoundationDB/fdb_c_wrapper.h index 77515ab..eb8f344 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 730 +#define FDB_API_VERSION 740 #include #endif diff --git a/Sources/FoundationDB/Client.swift b/Sources/FoundationDB/Client.swift index 88c82b3..0ef7f4a 100644 --- a/Sources/FoundationDB/Client.swift +++ b/Sources/FoundationDB/Client.swift @@ -38,7 +38,7 @@ public class FdbClient { /// FoundationDB API version constants. public enum APIVersion { /// The current supported API version (730). - public static let current: Int32 = 730 + public static let current: Int32 = 740 } /// Initializes the FoundationDB client with the specified API version. diff --git a/Sources/FoundationDB/Future.swift b/Sources/FoundationDB/Future.swift index 8ca9bae..4c4841a 100644 --- a/Sources/FoundationDB/Future.swift +++ b/Sources/FoundationDB/Future.swift @@ -230,9 +230,9 @@ struct ResultValue: FutureResult { /// with information about whether more data is available. public struct ResultRange: FutureResult { /// The array of key-value pairs returned by the range operation. - let records: Fdb.KeyValueArray + public let records: Fdb.KeyValueArray /// Indicates whether there are more records beyond this result. - let more: Bool + public let more: Bool /// Extracts key-value pairs from a range future. /// diff --git a/Sources/FoundationDB/Network.swift b/Sources/FoundationDB/Network.swift index 756e557..ed9c84d 100644 --- a/Sources/FoundationDB/Network.swift +++ b/Sources/FoundationDB/Network.swift @@ -36,16 +36,15 @@ import CFoundationDB /// let network = FdbNetwork.shared /// try network.initialize(version: 740) /// ``` -// TODO: stopNetwork at deinit. @MainActor class FdbNetwork { /// The shared singleton instance of the network manager. static let shared = FdbNetwork() /// Indicates whether the network has been set up. - private var networkSetup = false + private nonisolated(unsafe) var networkSetup = false /// The pthread handle for the network thread. - private var networkThread: pthread_t? = nil + private nonisolated(unsafe) var networkThread: pthread_t? = nil /// Initializes the FoundationDB network with the specified API version. /// @@ -65,6 +64,23 @@ class FdbNetwork { startNetwork() } + /// Stops the FoundationDB network and waits for the network thread to complete. + deinit { + if !networkSetup { + return + } + + // Call stop_network and wait for network thread to complete + let error = fdb_stop_network() + if error != 0 { + print("Failed to stop network in deinit: \(FdbError(code: error).description)") + } + + if let thread = networkThread { + pthread_join(thread, nil) + } + } + /// Selects the FoundationDB API version. /// /// - Parameter version: The API version to select. @@ -117,21 +133,6 @@ class FdbNetwork { } } - /// Stops the FoundationDB network and waits for the network thread to complete. - /// - /// - Throws: `FdbError` if the network cannot be stopped cleanly. - func stopNetwork() throws { - let error = fdb_stop_network() - if error != 0 { - throw FdbError(code: error) - } - - if networkSetup { - pthread_join(networkThread!, nil) - } - networkSetup = false - } - /// Sets a network option with an optional byte array value. /// /// - Parameters: diff --git a/Tests/StackTester/Sources/StackTester/StackTester.swift b/Tests/StackTester/Sources/StackTester/StackTester.swift new file mode 100644 index 0000000..896fd01 --- /dev/null +++ b/Tests/StackTester/Sources/StackTester/StackTester.swift @@ -0,0 +1,693 @@ +/* + * StackTester.swift + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2013-2024 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 +import FoundationDB + +// Simple stack entry - equivalent to Go's stackEntry +struct StackEntry { + let item: Any + let idx: Int +} + +class StackMachine { + private let prefix: [UInt8] + private var stack: [StackEntry] = [] + private var database: FdbDatabase + private let verbose: Bool + private var transaction: (any ITransaction)? + private var transactionMap: [String: any ITransaction] = [:] + private var transactionName: String = "MAIN" + private var lastVersion: Int64 = 0 + + init(prefix: [UInt8], database: FdbDatabase, verbose: Bool) { + self.prefix = prefix + self.database = database + self.verbose = verbose + } + + // Equivalent to Go's waitAndPop with error handling + func waitAndPop() -> StackEntry { + guard !stack.isEmpty else { + fatalError("Stack is empty") + } + + let ret = stack.removeLast() + + // Handle futures and convert types like in Go + switch ret.item { + case let data as [UInt8]: + return StackEntry(item: data, idx: ret.idx) + case let string as String: + return StackEntry(item: Array(string.utf8), idx: ret.idx) + case let int as Int64: + return StackEntry(item: int, idx: ret.idx) + default: + return ret + } + } + + // Equivalent to Go's store + func store(_ idx: Int, _ item: Any) { + stack.append(StackEntry(item: item, idx: idx)) + } + + // Get current transaction (create if needed) + func currentTransaction() throws -> any ITransaction { + if let existingTransaction = transactionMap[transactionName] { + return existingTransaction + } + + // Create new transaction if it doesn't exist + let newTransaction = try database.createTransaction() + transactionMap[transactionName] = newTransaction + return newTransaction + } + + // Create a new transaction for the current transaction name + func newTransaction() throws { + let newTransaction = try database.createTransaction() + transactionMap[transactionName] = newTransaction + } + + // Switch to a different transaction by name + func switchTransaction(_ name: [UInt8]) throws { + let nameString = String(bytes: name, encoding: .utf8) ?? "MAIN" + transactionName = nameString + + // Create transaction if it doesn't exist + if transactionMap[transactionName] == nil { + try newTransaction() + } + } + + // Helper method to pack range results like Python's push_range + func pushRange(_ idx: Int, _ records: [(key: [UInt8], value: [UInt8])], prefixFilter: [UInt8]? = nil) { + var kvs: [any TupleElement] = [] + for (key, value) in records { + if let prefix = prefixFilter { + if key.starts(with: prefix) { + kvs.append(key) + kvs.append(value) + } + } else { + kvs.append(key) + kvs.append(value) + } + } + let tuple = Tuple(kvs) + store(idx, tuple.encode()) + } + + // Helper method to filter key results with prefix + func filterKeyResult(_ key: [UInt8], prefix: [UInt8]) -> [UInt8] { + if key.starts(with: prefix) { + return key + } else if key.lexicographicallyPrecedes(prefix) { + return prefix + } else { + return prefix + [0xFF] + } + } + + // Process a single instruction - subset of Go's processInst + func processInstruction(_ idx: Int, _ instruction: [Any]) async throws { + guard let op = instruction.first as? String else { + fatalError("Invalid instruction format") + } + + if verbose { + print("\(idx). Instruction is \(op)") + print("Stack: [\(stack.map { "\($0.item)" }.joined(separator: ", "))] (\(stack.count))") + } + + switch op { + case "PUSH": + if instruction.count > 1 { + store(idx, instruction[1]) + } + + case "POP": + if !stack.isEmpty { + let _ = waitAndPop() + } + + case "DUP": + if !stack.isEmpty { + let entry = stack.last! + store(entry.idx, entry.item) + } + + case "EMPTY_STACK": + stack.removeAll() + + case "SWAP": + if !stack.isEmpty { + let swapIdx = waitAndPop().item as! Int64 + let lastIdx = stack.count - 1 + let targetIdx = lastIdx - Int(swapIdx) + if targetIdx >= 0 && targetIdx < stack.count { + stack.swapAt(lastIdx, targetIdx) + } + } + + case "SUB": + if stack.count >= 2 { + let x = waitAndPop().item as! Int64 + let y = waitAndPop().item as! Int64 + store(idx, x - y) + } + + case "CONCAT": + if stack.count >= 2 { + let str1 = waitAndPop().item + let str2 = waitAndPop().item + + if let s1 = str1 as? String, let s2 = str2 as? String { + store(idx, s1 + s2) + } else if let d1 = str1 as? [UInt8], let d2 = str2 as? [UInt8] { + store(idx, d1 + d2) + } else { + fatalError("Invalid CONCAT parameters") + } + } + + case "SET": + if stack.count >= 2 { + let key = waitAndPop().item as! [UInt8] + let value = waitAndPop().item as! [UInt8] + + try await database.withTransaction { transaction in + transaction.setValue(value, for: key) + return () + } + } + + case "GET": + if !stack.isEmpty { + let key = waitAndPop().item as! [UInt8] + + let result = try await database.withTransaction { transaction in + return try await transaction.getValue(for: key, snapshot: false) + } + + if let value = result { + store(idx, value) + } else { + store(idx, Array("RESULT_NOT_PRESENT".utf8)) + } + } + + case "LOG_STACK": + if !stack.isEmpty { + let logPrefix = waitAndPop().item as! [UInt8] + + try await database.withTransaction { transaction in + var stackIndex = 0 + for entry in stack.reversed() { + // Create key: logPrefix + tuple(stackIndex, entry.idx) + let keyTuple = Tuple([Int64(stackIndex), Int64(entry.idx)]) + var key = logPrefix + key.append(contentsOf: keyTuple.encode()) + + // Create value from entry.item + var value: [UInt8] + if let data = entry.item as? [UInt8] { + value = data + } else if let str = entry.item as? String { + value = Array(str.utf8) + } else { + value = Array("STACK_ITEM".utf8) + } + + // Limit value size like in Go + let maxSize = 40000 + if value.count > maxSize { + value = Array(value.prefix(maxSize)) + } + + transaction.setValue(value, for: key) + stackIndex += 1 + } + + // Clear stack after logging + stack.removeAll() + return () + } + } + + case "NEW_TRANSACTION": + try newTransaction() + + case "GET_READ_VERSION": + let transaction = try currentTransaction() + lastVersion = try await transaction.getReadVersion() + store(idx, Array("GOT_READ_VERSION".utf8)) + + case "USE_TRANSACTION": + let name = waitAndPop().item as! [UInt8] + try switchTransaction(name) + + case "COMMIT": + let transaction = try currentTransaction() + let success = try await transaction.commit() + // In async Swift, we store the result directly since it's already awaited + store(idx, Array("COMMIT_RESULT".utf8)) + + case "RESET": + if let transaction = transactionMap[transactionName] as? FdbTransaction { + // Create a new transaction to replace the reset one + try newTransaction() + } + + case "CANCEL": + if let transaction = transactionMap[transactionName] { + transaction.cancel() + } + + case "ON_ERROR": + let errorCode = waitAndPop().item as! Int64 + // For now, just create a new transaction as error handling + try newTransaction() + store(idx, Array("RESULT_NOT_PRESENT".utf8)) + + case "GET_KEY": + // Python order: key, or_equal, offset, prefix = inst.pop(4) + let prefix = waitAndPop().item as! [UInt8] + let offset = Int32(waitAndPop().item as! Int64) + let orEqual = (waitAndPop().item as! Int64) != 0 + let key = waitAndPop().item as! [UInt8] + + let selector = Fdb.KeySelector(key: key, orEqual: orEqual, offset: offset) + let transaction = try currentTransaction() + + if let resultKey = try await transaction.getKey(selector: selector, snapshot: false) { + let filteredKey = filterKeyResult(resultKey, prefix: prefix) + store(idx, filteredKey) + } else { + store(idx, Array("RESULT_NOT_PRESENT".utf8)) + } + + case "GET_RANGE": + // Python/Go order: begin, end, limit, reverse, mode (but Go pops in reverse) + // Go pops: mode, reverse, limit, endKey, beginKey + let mode = waitAndPop().item as! Int64 // Streaming mode, ignore for now + let reverse = (waitAndPop().item as! Int64) != 0 + let limit = Int32(waitAndPop().item as! Int64) + let endKey = waitAndPop().item as! [UInt8] + let beginKey = waitAndPop().item as! [UInt8] + let transaction = try currentTransaction() + + let result = try await transaction.getRange( + beginKey: beginKey, + endKey: endKey, + limit: limit, + snapshot: false + ) + + pushRange(idx, result.records) + + case "GET_RANGE_STARTS_WITH": + // Python order: prefix, limit, reverse, mode (pops 4 parameters) + // Go order: same but pops in reverse + let mode = waitAndPop().item as! Int64 // Streaming mode, ignore for now + let reverse = (waitAndPop().item as! Int64) != 0 + let limit = Int32(waitAndPop().item as! Int64) + let prefix = waitAndPop().item as! [UInt8] + let transaction = try currentTransaction() + + var endKey = prefix + endKey.append(0xFF) + + let result = try await transaction.getRange( + beginKey: prefix, + endKey: endKey, + limit: limit, + snapshot: false + ) + + pushRange(idx, result.records) + + case "GET_RANGE_SELECTOR": + // Python pops 10 parameters: begin_key, begin_or_equal, begin_offset, end_key, end_or_equal, end_offset, limit, reverse, mode, prefix + // Go pops in reverse order + let prefix = waitAndPop().item as! [UInt8] + let mode = waitAndPop().item as! Int64 // Streaming mode, ignore for now + let reverse = (waitAndPop().item as! Int64) != 0 + let limit = Int32(waitAndPop().item as! Int64) + let endOffset = Int32(waitAndPop().item as! Int64) + let endOrEqual = (waitAndPop().item as! Int64) != 0 + let endKey = waitAndPop().item as! [UInt8] + let beginOffset = Int32(waitAndPop().item as! Int64) + let beginOrEqual = (waitAndPop().item as! Int64) != 0 + let beginKey = waitAndPop().item as! [UInt8] + + let beginSelector = Fdb.KeySelector(key: beginKey, orEqual: beginOrEqual, offset: beginOffset) + let endSelector = Fdb.KeySelector(key: endKey, orEqual: endOrEqual, offset: endOffset) + let transaction = try currentTransaction() + + let result = try await transaction.getRange( + beginSelector: beginSelector, + endSelector: endSelector, + limit: limit, + snapshot: false + ) + + pushRange(idx, result.records, prefixFilter: prefix) + + case "GET_ESTIMATED_RANGE_SIZE": + // Python order: begin, end = inst.pop(2) + let endKey = waitAndPop().item as! [UInt8] + let beginKey = waitAndPop().item as! [UInt8] + // Not available in Swift bindings, store placeholder + store(idx, Array("GOT_ESTIMATED_RANGE_SIZE".utf8)) + + case "GET_RANGE_SPLIT_POINTS": + // Python order: begin, end, chunkSize = inst.pop(3) + let chunkSize = waitAndPop().item as! Int64 + let endKey = waitAndPop().item as! [UInt8] + let beginKey = waitAndPop().item as! [UInt8] + // Not available in Swift bindings, store placeholder + store(idx, Array("GOT_RANGE_SPLIT_POINTS".utf8)) + + case "CLEAR": + let key = waitAndPop().item as! [UInt8] + let transaction = try currentTransaction() + transaction.clear(key: key) + + case "CLEAR_RANGE": + let beginKey = waitAndPop().item as! [UInt8] + let endKey = waitAndPop().item as! [UInt8] + let transaction = try currentTransaction() + transaction.clearRange(beginKey: beginKey, endKey: endKey) + + case "CLEAR_RANGE_STARTS_WITH": + let prefix = waitAndPop().item as! [UInt8] + let transaction = try currentTransaction() + var endKey = prefix + endKey.append(0xFF) + transaction.clearRange(beginKey: prefix, endKey: endKey) + + case "ATOMIC_OP": + // Python order: opType, key, value = inst.pop(3) + let param = waitAndPop().item as! [UInt8] // value/param + let key = waitAndPop().item as! [UInt8] // key + let opType = waitAndPop().item as! [UInt8] // opType + let transaction = try currentTransaction() + + // Convert opType string to MutationType + let opTypeString = String(bytes: opType, encoding: .utf8) ?? "" + let mutationType: Fdb.MutationType + switch opTypeString { + case "ADD": + mutationType = .add + case "BIT_AND": + mutationType = .bitAnd + case "BIT_OR": + mutationType = .bitOr + case "BIT_XOR": + mutationType = .bitXor + default: + mutationType = .add // Default fallback + } + + transaction.atomicOp(key: key, param: param, mutationType: mutationType) + + case "SET_READ_VERSION": + let version = waitAndPop().item as! Int64 + let transaction = try currentTransaction() + transaction.setReadVersion(version) + + case "GET_COMMITTED_VERSION": + // Not available in Swift bindings, store lastVersion instead + store(idx, lastVersion) + store(idx, Array("GOT_COMMITTED_VERSION".utf8)) + + case "GET_APPROXIMATE_SIZE": + // Not available in Swift bindings, store placeholder + store(idx, Array("GOT_APPROXIMATE_SIZE".utf8)) + + case "GET_VERSIONSTAMP": + let transaction = try currentTransaction() + if let versionstamp = try await transaction.getVersionstamp() { + store(idx, versionstamp) + } else { + store(idx, Array("RESULT_NOT_PRESENT".utf8)) + } + + case "READ_CONFLICT_RANGE": + let beginKey = waitAndPop().item as! [UInt8] + let endKey = waitAndPop().item as! [UInt8] + // Conflict ranges not exposed in Swift bindings, just consume parameters + + case "WRITE_CONFLICT_RANGE": + let beginKey = waitAndPop().item as! [UInt8] + let endKey = waitAndPop().item as! [UInt8] + // Conflict ranges not exposed in Swift bindings, just consume parameters + + case "READ_CONFLICT_KEY": + let key = waitAndPop().item as! [UInt8] + // Conflict ranges not exposed in Swift bindings, just consume parameters + + case "WRITE_CONFLICT_KEY": + let key = waitAndPop().item as! [UInt8] + // Conflict ranges not exposed in Swift bindings, just consume parameters + + case "DISABLE_WRITE_CONFLICT": + // Not directly available in Swift bindings, could use transaction option + let transaction = try currentTransaction() + try transaction.setOption(.nextWriteNoWriteConflictRange, value: nil) + + case "TUPLE_PACK": + let numElements = waitAndPop().item as! Int64 + var elements: [any TupleElement] = [] + + for _ in 0..= 4 { + let floatValue = data.withUnsafeBytes { $0.load(as: Float.self) } + store(idx, Int64(floatValue.bitPattern)) + } else { + store(idx, Int64(0)) + } + + case "DECODE_DOUBLE": + let data = waitAndPop().item as! [UInt8] + if data.count >= 8 { + let doubleValue = data.withUnsafeBytes { $0.load(as: Double.self) } + store(idx, Int64(doubleValue.bitPattern)) + } else { + store(idx, Int64(0)) + } + + case "WAIT_FUTURE": + // In async context, futures are automatically awaited, just pass through the item + let oldIdx = stack.count > 0 ? stack.last!.idx : idx + let item = waitAndPop().item + store(oldIdx, item) + + case "START_THREAD": + // Threading not supported in current implementation, just consume the instruction + let instruction = waitAndPop().item + // Could implement this with Task.detached in the future + + case "WAIT_EMPTY": + // Wait until stack is empty - already satisfied since we process sequentially + break + + case "UNIT_TESTS": + // Unit tests placeholder - could implement tuple/encoding tests here + store(idx, Array("UNIT_TESTS_COMPLETED".utf8)) + + default: + fatalError("Unhandled operation: \(op)") + } + + if verbose { + print(" -> [\(stack.map { "\($0.item)" }.joined(separator: ", "))] (\(stack.count))") + print() + } + } + + // Main run function - equivalent to Go's Run() + func run() async throws { + // Read instructions from database using the prefix, like Go version + let instructions = try await database.withTransaction { transaction -> [(key: [UInt8], value: [UInt8])] in + // Create range starting with our prefix + let prefixTuple = Tuple([prefix]) + let beginKey = prefixTuple.encode() + let endKey = beginKey + [0xFF] // Simple range end + + let result = try await transaction.getRange( + beginKey: beginKey, + endKey: endKey, + limit: 0, + snapshot: false + ) + + return result.records + } + + if verbose { + print("Found \(instructions.count) instructions") + } + + // Process each instruction + for (i, (_, value)) in instructions.enumerated() { + // Unpack the instruction tuple from the value + let elements = try Tuple.decode(from: value) + + // Convert tuple elements to array for processing + var instruction: [Any] = [] + for element in elements { + if let stringElement = element as? String { + instruction.append(stringElement) + } else if let bytesElement = element as? [UInt8] { + instruction.append(bytesElement) + } else if let intElement = element as? Int64 { + instruction.append(intElement) + } else { + instruction.append(element) + } + } + + if verbose { + print("Instruction \(i): \(instruction)") + } + + try await processInstruction(i, instruction) + } + + print("StackTester completed successfully with \(instructions.count) instructions") + } +} + diff --git a/Tests/StackTester/Sources/StackTester/main.swift b/Tests/StackTester/Sources/StackTester/main.swift new file mode 100644 index 0000000..1701667 --- /dev/null +++ b/Tests/StackTester/Sources/StackTester/main.swift @@ -0,0 +1,77 @@ +/* + * main.swift + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2013-2024 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 +import FoundationDB + +// Entry point - using RunLoop to keep the process alive +guard CommandLine.arguments.count >= 3 else { + print("Usage: stacktester [cluster_file]") + exit(1) +} + +let prefix = Array(CommandLine.arguments[1].utf8) +let apiVersionString = CommandLine.arguments[2] +let clusterFile = CommandLine.arguments.count > 3 ? CommandLine.arguments[3] : nil + +guard let apiVersion = Int32(apiVersionString) else { + print("Invalid API version: \(apiVersionString)") + exit(1) +} + +// Use a serial queue for thread-safe communication +let syncQueue = DispatchQueue(label: "stacktester.sync") +var finished = false +var finalError: Error? = nil + +Task { + do { + try await FdbClient.initialize(version: apiVersion) + let database = try FdbClient.openDatabase(clusterFilePath: clusterFile) + let stackMachine = StackMachine(prefix: prefix, database: database, verbose: false) + try await stackMachine.run() + print("StackMachine completed successfully") + + syncQueue.sync { + finished = true + } + } catch { + print("Error occurred: \(error)") + syncQueue.sync { + finalError = error + finished = true + } + } +} + +while true { + let isFinished = syncQueue.sync { finished } + if isFinished { break } + RunLoop.current.run(mode: .default, before: Date(timeIntervalSinceNow: 0.1)) +} + +let error = syncQueue.sync { finalError } + +if let error = error { + print("Final error: \(error)") + exit(1) +} + +exit(0) From c014aade6a3000c4da389d9d1ea7737c286cd616 Mon Sep 17 00:00:00 2001 From: Vishesh Yadav Date: Tue, 7 Oct 2025 16:28:22 -0700 Subject: [PATCH 07/16] Update some instruction processing with errors --- .../Sources/StackTester/StackTester.swift | 207 ++++++++---------- 1 file changed, 97 insertions(+), 110 deletions(-) diff --git a/Tests/StackTester/Sources/StackTester/StackTester.swift b/Tests/StackTester/Sources/StackTester/StackTester.swift index 896fd01..a814a7b 100644 --- a/Tests/StackTester/Sources/StackTester/StackTester.swift +++ b/Tests/StackTester/Sources/StackTester/StackTester.swift @@ -140,140 +140,134 @@ class StackMachine { switch op { case "PUSH": - if instruction.count > 1 { - store(idx, instruction[1]) - } + assert(instruction.coung > 1) + store(idx, instruction[1]) case "POP": - if !stack.isEmpty { - let _ = waitAndPop() - } + assert(!stack.isEmpty) + let _ = waitAndPop() case "DUP": - if !stack.isEmpty { - let entry = stack.last! - store(entry.idx, entry.item) - } + assert(!stack.isEmpty) + let entry = stack.last! + store(entry.idx, entry.item) case "EMPTY_STACK": stack.removeAll() case "SWAP": - if !stack.isEmpty { - let swapIdx = waitAndPop().item as! Int64 - let lastIdx = stack.count - 1 - let targetIdx = lastIdx - Int(swapIdx) - if targetIdx >= 0 && targetIdx < stack.count { - stack.swapAt(lastIdx, targetIdx) - } - } + assert(!stack.isEmpty) + let swapIdx = waitAndPop().item as! Int64 + let lastIdx = stack.count - 1 + let targetIdx = lastIdx - Int(swapIdx) + assert(targetIdx >= 0 && targetIdx < stack.count) + stack.swapAt(lastIdx, targetIdx) case "SUB": - if stack.count >= 2 { - let x = waitAndPop().item as! Int64 - let y = waitAndPop().item as! Int64 - store(idx, x - y) - } + assert(stack.count >= 2) + let x = waitAndPop().item as! Int64 + let y = waitAndPop().item as! Int64 + store(idx, x - y) case "CONCAT": - if stack.count >= 2 { - let str1 = waitAndPop().item - let str2 = waitAndPop().item - - if let s1 = str1 as? String, let s2 = str2 as? String { - store(idx, s1 + s2) - } else if let d1 = str1 as? [UInt8], let d2 = str2 as? [UInt8] { - store(idx, d1 + d2) - } else { - fatalError("Invalid CONCAT parameters") - } + assert(stack.count >= 2) + let str1 = waitAndPop().item + let str2 = waitAndPop().item + + if let s1 = str1 as? String, let s2 = str2 as? String { + store(idx, s1 + s2) + } else if let d1 = str1 as? [UInt8], let d2 = str2 as? [UInt8] { + store(idx, d1 + d2) + } else { + fatalError("Invalid CONCAT parameters") } + case "NEW_TRANSACTION": + try newTransaction() + + case "USE_TRANSACTION": + let name = waitAndPop().item as! [UInt8] + try switchTransaction(name) + + case "ON_ERROR": // TODO + let errorCode = waitAndPop().item as! Int64 + // For now, just create a new transaction as error handling + try newTransaction() + store(idx, Array("RESULT_NOT_PRESENT".utf8)) + + case "GET_READ_VERSION": + let transaction = try currentTransaction() + lastVersion = try await transaction.getReadVersion() + store(idx, Array("GOT_READ_VERSION".utf8)) + case "SET": - if stack.count >= 2 { - let key = waitAndPop().item as! [UInt8] - let value = waitAndPop().item as! [UInt8] + assert(stack.count >= 2) + let key = waitAndPop().item as! [UInt8] + let value = waitAndPop().item as! [UInt8] - try await database.withTransaction { transaction in - transaction.setValue(value, for: key) - return () - } + try await database.withTransaction { transaction in + transaction.setValue(value, for: key) + return () } case "GET": - if !stack.isEmpty { - let key = waitAndPop().item as! [UInt8] + assert(!stack.isEmpty) + let key = waitAndPop().item as! [UInt8] - let result = try await database.withTransaction { transaction in - return try await transaction.getValue(for: key, snapshot: false) - } + let result = try await database.withTransaction { transaction in + return try await transaction.getValue(for: key, snapshot: false) + } - if let value = result { - store(idx, value) - } else { - store(idx, Array("RESULT_NOT_PRESENT".utf8)) - } + if let value = result { + store(idx, value) + } else { + store(idx, Array("RESULT_NOT_PRESENT".utf8)) } - case "LOG_STACK": - if !stack.isEmpty { - let logPrefix = waitAndPop().item as! [UInt8] - - try await database.withTransaction { transaction in - var stackIndex = 0 - for entry in stack.reversed() { - // Create key: logPrefix + tuple(stackIndex, entry.idx) - let keyTuple = Tuple([Int64(stackIndex), Int64(entry.idx)]) - var key = logPrefix - key.append(contentsOf: keyTuple.encode()) - - // Create value from entry.item - var value: [UInt8] - if let data = entry.item as? [UInt8] { - value = data - } else if let str = entry.item as? String { - value = Array(str.utf8) - } else { - value = Array("STACK_ITEM".utf8) - } - - // Limit value size like in Go - let maxSize = 40000 - if value.count > maxSize { - value = Array(value.prefix(maxSize)) - } - - transaction.setValue(value, for: key) - stackIndex += 1 + case "LOG_STACK": // TODO + assert(!stack.isEmpty) + let logPrefix = waitAndPop().item as! [UInt8] + + try await database.withTransaction { transaction in + var stackIndex = 0 + for entry in stack.reversed() { + // Create key: logPrefix + tuple(stackIndex, entry.idx) + let keyTuple = Tuple([Int64(stackIndex), Int64(entry.idx)]) + var key = logPrefix + key.append(contentsOf: keyTuple.encode()) + + // Create value from entry.item + var value: [UInt8] + if let data = entry.item as? [UInt8] { + value = data + } else if let str = entry.item as? String { + value = Array(str.utf8) + } else { + value = Array("STACK_ITEM".utf8) } - // Clear stack after logging - stack.removeAll() - return () - } - } - - case "NEW_TRANSACTION": - try newTransaction() + // Limit value size like in Go + let maxSize = 40000 + if value.count > maxSize { + value = Array(value.prefix(maxSize)) + } - case "GET_READ_VERSION": - let transaction = try currentTransaction() - lastVersion = try await transaction.getReadVersion() - store(idx, Array("GOT_READ_VERSION".utf8)) + transaction.setValue(value, for: key) + stackIndex += 1 + } - case "USE_TRANSACTION": - let name = waitAndPop().item as! [UInt8] - try switchTransaction(name) + // Clear stack after logging + stack.removeAll() + return () + } case "COMMIT": let transaction = try currentTransaction() let success = try await transaction.commit() - // In async Swift, we store the result directly since it's already awaited store(idx, Array("COMMIT_RESULT".utf8)) case "RESET": if let transaction = transactionMap[transactionName] as? FdbTransaction { - // Create a new transaction to replace the reset one try newTransaction() } @@ -282,12 +276,6 @@ class StackMachine { transaction.cancel() } - case "ON_ERROR": - let errorCode = waitAndPop().item as! Int64 - // For now, just create a new transaction as error handling - try newTransaction() - store(idx, Array("RESULT_NOT_PRESENT".utf8)) - case "GET_KEY": // Python order: key, or_equal, offset, prefix = inst.pop(4) let prefix = waitAndPop().item as! [UInt8] @@ -372,14 +360,14 @@ class StackMachine { pushRange(idx, result.records, prefixFilter: prefix) - case "GET_ESTIMATED_RANGE_SIZE": + case "GET_ESTIMATED_RANGE_SIZE": // TODO // Python order: begin, end = inst.pop(2) let endKey = waitAndPop().item as! [UInt8] let beginKey = waitAndPop().item as! [UInt8] // Not available in Swift bindings, store placeholder store(idx, Array("GOT_ESTIMATED_RANGE_SIZE".utf8)) - case "GET_RANGE_SPLIT_POINTS": + case "GET_RANGE_SPLIT_POINTS": // TODO // Python order: begin, end, chunkSize = inst.pop(3) let chunkSize = waitAndPop().item as! Int64 let endKey = waitAndPop().item as! [UInt8] @@ -435,12 +423,12 @@ class StackMachine { let transaction = try currentTransaction() transaction.setReadVersion(version) - case "GET_COMMITTED_VERSION": + case "GET_COMMITTED_VERSION": // TODO // Not available in Swift bindings, store lastVersion instead store(idx, lastVersion) store(idx, Array("GOT_COMMITTED_VERSION".utf8)) - case "GET_APPROXIMATE_SIZE": + case "GET_APPROXIMATE_SIZE": // TODO // Not available in Swift bindings, store placeholder store(idx, Array("GOT_APPROXIMATE_SIZE".utf8)) @@ -624,8 +612,7 @@ class StackMachine { // Wait until stack is empty - already satisfied since we process sequentially break - case "UNIT_TESTS": - // Unit tests placeholder - could implement tuple/encoding tests here + case "UNIT_TESTS": // TODO store(idx, Array("UNIT_TESTS_COMPLETED".utf8)) default: From 91fc94e67bcd210c90562a1056e206000aa147b7 Mon Sep 17 00:00:00 2001 From: Vishesh Yadav Date: Tue, 7 Oct 2025 16:40:59 -0700 Subject: [PATCH 08/16] Add ON_ERROR instruction support --- Sources/FoundationDB/FoundationdDB.swift | 9 ++++++++ Sources/FoundationDB/Transaction.swift | 6 ++++++ .../Sources/StackTester/StackTester.swift | 21 ++++++++++++++----- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/Sources/FoundationDB/FoundationdDB.swift b/Sources/FoundationDB/FoundationdDB.swift index 4ff90ff..8602f32 100644 --- a/Sources/FoundationDB/FoundationdDB.swift +++ b/Sources/FoundationDB/FoundationdDB.swift @@ -218,6 +218,15 @@ public protocol ITransaction: Sendable { /// - Throws: `FdbError` if the operation fails. func getReadVersion() async throws -> Int64 + /// Handles transaction errors and implements retry logic with exponential backoff. + /// + /// If this method returns successfully, the transaction has been reset and can be retried. + /// If it throws an error, the transaction should not be retried. + /// + /// - Parameter error: The error encountered during transaction execution. + /// - Throws: `FdbError` if the error is not retryable or retry limits have been exceeded. + func onError(_ error: FdbError) async throws + /// Performs an atomic operation on a key. /// /// - Parameters: diff --git a/Sources/FoundationDB/Transaction.swift b/Sources/FoundationDB/Transaction.swift index 7d6e4f3..9aa272e 100644 --- a/Sources/FoundationDB/Transaction.swift +++ b/Sources/FoundationDB/Transaction.swift @@ -157,6 +157,12 @@ public class FdbTransaction: ITransaction, @unchecked Sendable { ).getAsync()?.value ?? 0 } + public func onError(_ error: FdbError) async throws { + try await Future( + fdb_transaction_on_error(transaction, error.code) + ).getAsync() + } + public func getRange( beginSelector: Fdb.KeySelector, endSelector: Fdb.KeySelector, limit: Int32 = 0, snapshot: Bool diff --git a/Tests/StackTester/Sources/StackTester/StackTester.swift b/Tests/StackTester/Sources/StackTester/StackTester.swift index a814a7b..199c30b 100644 --- a/Tests/StackTester/Sources/StackTester/StackTester.swift +++ b/Tests/StackTester/Sources/StackTester/StackTester.swift @@ -140,7 +140,7 @@ class StackMachine { switch op { case "PUSH": - assert(instruction.coung > 1) + assert(instruction.count > 1) store(idx, instruction[1]) case "POP": @@ -189,11 +189,22 @@ class StackMachine { let name = waitAndPop().item as! [UInt8] try switchTransaction(name) - case "ON_ERROR": // TODO + case "ON_ERROR": let errorCode = waitAndPop().item as! Int64 - // For now, just create a new transaction as error handling - try newTransaction() - store(idx, Array("RESULT_NOT_PRESENT".utf8)) + let transaction = try currentTransaction() + + // Create FdbError from the error code + let error = FdbError(code: Int32(errorCode)) + + // Call onError which will wait and handle the error appropriately + do { + try await transaction.onError(error) + // If onError succeeds, the transaction has been reset and is ready to retry + store(idx, Array("RESULT_NOT_PRESENT".utf8)) + } catch { + // If onError fails, store the error (transaction should not be retried) + throw error + } case "GET_READ_VERSION": let transaction = try currentTransaction() From ea7258776c5013dbf7e6582b140c501716891ffe Mon Sep 17 00:00:00 2001 From: Vishesh Yadav Date: Tue, 7 Oct 2025 16:46:50 -0700 Subject: [PATCH 09/16] Implement LOG_STACK instruction --- .../Sources/StackTester/StackTester.swift | 78 ++++++++++++------- 1 file changed, 50 insertions(+), 28 deletions(-) diff --git a/Tests/StackTester/Sources/StackTester/StackTester.swift b/Tests/StackTester/Sources/StackTester/StackTester.swift index 199c30b..852ebc7 100644 --- a/Tests/StackTester/Sources/StackTester/StackTester.swift +++ b/Tests/StackTester/Sources/StackTester/StackTester.swift @@ -127,6 +127,41 @@ class StackMachine { } } + // Helper method to log a batch of stack entries + func logStackBatch(_ entries: [(stackIndex: Int, entry: StackEntry)], prefix: [UInt8]) async throws { + try await database.withTransaction { transaction in + for (stackIndex, entry) in entries { + // Create key: prefix + tuple(stackIndex, entry.idx) + let keyTuple = Tuple([Int64(stackIndex), Int64(entry.idx)]) + var key = prefix + key.append(contentsOf: keyTuple.encode()) + + // Pack value as a tuple (matching Python/Go behavior) + let valueTuple: Tuple + if let data = entry.item as? [UInt8] { + valueTuple = Tuple([data]) + } else if let str = entry.item as? String { + valueTuple = Tuple([str]) + } else if let int = entry.item as? Int64 { + valueTuple = Tuple([int]) + } else { + valueTuple = Tuple([Array("UNKNOWN_ITEM".utf8)]) + } + + var packedValue = valueTuple.encode() + + // Limit value size to 40000 bytes + let maxSize = 40000 + if packedValue.count > maxSize { + packedValue = Array(packedValue.prefix(maxSize)) + } + + transaction.setValue(packedValue, for: key) + } + return () + } + } + // Process a single instruction - subset of Go's processInst func processInstruction(_ idx: Int, _ instruction: [Any]) async throws { guard let op = instruction.first as? String else { @@ -235,41 +270,28 @@ class StackMachine { store(idx, Array("RESULT_NOT_PRESENT".utf8)) } - case "LOG_STACK": // TODO + case "LOG_STACK": assert(!stack.isEmpty) let logPrefix = waitAndPop().item as! [UInt8] - try await database.withTransaction { transaction in - var stackIndex = 0 - for entry in stack.reversed() { - // Create key: logPrefix + tuple(stackIndex, entry.idx) - let keyTuple = Tuple([Int64(stackIndex), Int64(entry.idx)]) - var key = logPrefix - key.append(contentsOf: keyTuple.encode()) - - // Create value from entry.item - var value: [UInt8] - if let data = entry.item as? [UInt8] { - value = data - } else if let str = entry.item as? String { - value = Array(str.utf8) - } else { - value = Array("STACK_ITEM".utf8) - } + // Process stack in batches of 100 like Python/Go implementations + var entries: [(stackIndex: Int, entry: StackEntry)] = [] + var stackIndex = stack.count - 1 - // Limit value size like in Go - let maxSize = 40000 - if value.count > maxSize { - value = Array(value.prefix(maxSize)) - } + while !stack.isEmpty { + let entry = waitAndPop() + entries.append((stackIndex: stackIndex, entry: entry)) + stackIndex -= 1 - transaction.setValue(value, for: key) - stackIndex += 1 + if entries.count == 100 { + try await logStackBatch(entries, prefix: logPrefix) + entries.removeAll() } + } - // Clear stack after logging - stack.removeAll() - return () + // Log remaining entries + if !entries.isEmpty { + try await logStackBatch(entries, prefix: logPrefix) } case "COMMIT": From 890ca1a7c15fb22167c88dcc86f0514b04d1787a Mon Sep 17 00:00:00 2001 From: Vishesh Yadav Date: Tue, 7 Oct 2025 16:54:03 -0700 Subject: [PATCH 10/16] Add support for some other API calls --- Sources/FoundationDB/FoundationdDB.swift | 41 +++++++ Sources/FoundationDB/Future.swift | 58 ++++++++++ Sources/FoundationDB/Transaction.swift | 48 ++++++++ .../FoundationDBTests/FoundationDBTests.swift | 107 ++++++++++++++++++ .../Sources/StackTester/StackTester.swift | 25 ++-- 5 files changed, 268 insertions(+), 11 deletions(-) diff --git a/Sources/FoundationDB/FoundationdDB.swift b/Sources/FoundationDB/FoundationdDB.swift index 8602f32..d23ad1a 100644 --- a/Sources/FoundationDB/FoundationdDB.swift +++ b/Sources/FoundationDB/FoundationdDB.swift @@ -227,6 +227,47 @@ public protocol ITransaction: Sendable { /// - Throws: `FdbError` if the error is not retryable or retry limits have been exceeded. func onError(_ error: FdbError) async throws + /// Returns an estimated byte size of the specified key range. + /// + /// The estimate is calculated based on sampling done by FDB server. Larger key-value pairs + /// are more likely to be sampled. For accuracy, use on large ranges (>3MB recommended). + /// + /// - Parameters: + /// - beginKey: The start of the range (inclusive). + /// - endKey: The end of the range (exclusive). + /// - Returns: The estimated size in bytes. + /// - Throws: `FdbError` if the operation fails. + func getEstimatedRangeSizeBytes(beginKey: Fdb.Key, endKey: Fdb.Key) async throws -> Int64 + + /// Returns a list of keys that can split the given range into roughly equal chunks. + /// + /// The returned split points include the start and end keys of the range. + /// + /// - Parameters: + /// - beginKey: The start of the range. + /// - endKey: The end of the range. + /// - chunkSize: The desired size of each chunk in bytes. + /// - Returns: An array of keys representing split points. + /// - Throws: `FdbError` if the operation fails. + func getRangeSplitPoints(beginKey: Fdb.Key, endKey: Fdb.Key, chunkSize: Int64) async throws -> [[UInt8]] + + /// Returns the version number at which a committed transaction modified the database. + /// + /// Must only be called after a successful commit. Read-only transactions return -1. + /// + /// - Returns: The committed version number. + /// - Throws: `FdbError` if called before commit or if the operation fails. + func getCommittedVersion() throws -> Int64 + + /// Returns the approximate transaction size so far. + /// + /// This is the sum of estimated sizes of mutations, read conflict ranges, and write conflict ranges. + /// Can be called multiple times before commit. + /// + /// - Returns: The approximate size in bytes. + /// - Throws: `FdbError` if the operation fails. + func getApproximateSize() async throws -> Int64 + /// Performs an atomic operation on a key. /// /// - Parameters: diff --git a/Sources/FoundationDB/Future.swift b/Sources/FoundationDB/Future.swift index 4c4841a..b94b78f 100644 --- a/Sources/FoundationDB/Future.swift +++ b/Sources/FoundationDB/Future.swift @@ -163,6 +163,28 @@ struct ResultVersion: FutureResult { } } +/// A result type for futures that return 64-bit integer values. +/// +/// Used for operations that return size estimates or counts. +struct ResultInt64: FutureResult { + /// The extracted integer value. + let value: Int64 + + /// Extracts an Int64 from the future. + /// + /// - Parameter fromFuture: The C future containing the integer. + /// - Returns: A `ResultInt64` with the extracted value. + /// - Throws: `FdbError` if the future contains an error. + static func extract(fromFuture: CFuturePtr) throws -> Self? { + var value: Int64 = 0 + let err = fdb_future_get_int64(fromFuture, &value) + if err != 0 { + throw FdbError(code: err) + } + return Self(value: value) + } +} + /// A result type for futures that return key data. /// /// Used for operations like key selectors that resolve to actual keys. @@ -264,3 +286,39 @@ public struct ResultRange: FutureResult { return Self(records: keyValueArray, more: more > 0) } } + +/// A result type for futures that return arrays of keys. +/// +/// Used for operations like get range split points that return multiple keys. +struct ResultKeyArray: FutureResult { + /// The array of keys returned by the operation. + let value: [[UInt8]] + + /// Extracts an array of keys from the future. + /// + /// - Parameter fromFuture: The C future containing the key array. + /// - Returns: A `ResultKeyArray` with the extracted keys. + /// - Throws: `FdbError` if the future contains an error. + static func extract(fromFuture: CFuturePtr) throws -> Self? { + var keysPtr: UnsafePointer? + var count: Int32 = 0 + + let err = fdb_future_get_key_array(fromFuture, &keysPtr, &count) + if err != 0 { + throw FdbError(code: err) + } + + guard let keysPtr = keysPtr, count > 0 else { + return Self(value: []) + } + + var keyArray: [[UInt8]] = [] + for i in 0 ..< Int(count) { + let fdbKey = keysPtr[i] + let key = Array(UnsafeBufferPointer(start: fdbKey.key, count: Int(fdbKey.key_length))) + keyArray.append(key) + } + + return Self(value: keyArray) + } +} diff --git a/Sources/FoundationDB/Transaction.swift b/Sources/FoundationDB/Transaction.swift index 9aa272e..0a54306 100644 --- a/Sources/FoundationDB/Transaction.swift +++ b/Sources/FoundationDB/Transaction.swift @@ -163,6 +163,54 @@ public class FdbTransaction: ITransaction, @unchecked Sendable { ).getAsync() } + public func getEstimatedRangeSizeBytes(beginKey: Fdb.Key, endKey: Fdb.Key) async throws -> Int64 { + try await beginKey.withUnsafeBytes { beginKeyBytes in + endKey.withUnsafeBytes { endKeyBytes in + Future( + fdb_transaction_get_estimated_range_size_bytes( + transaction, + beginKeyBytes.bindMemory(to: UInt8.self).baseAddress, + Int32(beginKey.count), + endKeyBytes.bindMemory(to: UInt8.self).baseAddress, + Int32(endKey.count) + ) + ) + } + }.getAsync()?.value ?? 0 + } + + public func getRangeSplitPoints(beginKey: Fdb.Key, endKey: Fdb.Key, chunkSize: Int64) async throws -> [[UInt8]] { + try await beginKey.withUnsafeBytes { beginKeyBytes in + endKey.withUnsafeBytes { endKeyBytes in + Future( + fdb_transaction_get_range_split_points( + transaction, + beginKeyBytes.bindMemory(to: UInt8.self).baseAddress, + Int32(beginKey.count), + endKeyBytes.bindMemory(to: UInt8.self).baseAddress, + Int32(endKey.count), + chunkSize + ) + ) + } + }.getAsync()?.value ?? [] + } + + public func getCommittedVersion() throws -> Int64 { + var version: Int64 = 0 + let err = fdb_transaction_get_committed_version(transaction, &version) + if err != 0 { + throw FdbError(code: err) + } + return version + } + + public func getApproximateSize() async throws -> Int64 { + try await Future( + fdb_transaction_get_approximate_size(transaction) + ).getAsync()?.value ?? 0 + } + public func getRange( beginSelector: Fdb.KeySelector, endSelector: Fdb.KeySelector, limit: Int32 = 0, snapshot: Bool diff --git a/Tests/FoundationDBTests/FoundationDBTests.swift b/Tests/FoundationDBTests/FoundationDBTests.swift index 6240f03..5631d45 100644 --- a/Tests/FoundationDBTests/FoundationDBTests.swift +++ b/Tests/FoundationDBTests/FoundationDBTests.swift @@ -1452,3 +1452,110 @@ extension String { return String(repeating: pad, count: toLength - count) + self } } + +@Test("getEstimatedRangeSizeBytes returns size estimate") +func testGetEstimatedRangeSizeBytes() async throws { + try await FdbClient.initialize() + let database = try FdbClient.openDatabase() + + // Write some test data + try await database.withTransaction { transaction in + transaction.clearRange(beginKey: "test_size_", endKey: "test_size`") + for i in 0..<100 { + let key = "test_size_\(String.padded(i))" + let value = String(repeating: "x", count: 1000) + transaction.setValue(value, for: key) + } + return () + } + + // Get estimated size + let transaction = try database.createTransaction() + let beginKey: Fdb.Key = Array("test_size_".utf8) + let endKey: Fdb.Key = Array("test_size`".utf8) + let estimatedSize = try await transaction.getEstimatedRangeSizeBytes(beginKey: beginKey, endKey: endKey) + + // Size should be positive (may not be exact due to sampling) + #expect(estimatedSize >= 0, "Estimated size should be non-negative") +} + +@Test("getRangeSplitPoints returns split keys") +func testGetRangeSplitPoints() async throws { + try await FdbClient.initialize() + let database = try FdbClient.openDatabase() + + // Write some test data + try await database.withTransaction { transaction in + transaction.clearRange(beginKey: "test_split_", endKey: "test_split`") + for i in 0..<200 { + let key = "test_split_\(String.padded(i))" + let value = String(repeating: "y", count: 500) + transaction.setValue(value, for: key) + } + return () + } + + // Get split points with 10KB chunks + let transaction = try database.createTransaction() + let beginKey: Fdb.Key = Array("test_split_".utf8) + let endKey: Fdb.Key = Array("test_split`".utf8) + let splitPoints = try await transaction.getRangeSplitPoints( + beginKey: beginKey, + endKey: endKey, + chunkSize: 10000 + ) + + // Should return at least begin and end keys + #expect(splitPoints.count >= 2, "Should return at least begin and end keys") + #expect(splitPoints.first == beginKey, "First split point should be begin key") + #expect(splitPoints.last == endKey, "Last split point should be end key") +} + +@Test("getCommittedVersion returns version after commit") +func testGetCommittedVersion() async throws { + try await FdbClient.initialize() + let database = try FdbClient.openDatabase() + let transaction = try database.createTransaction() + + transaction.setValue("test_value", for: "test_version_key") + _ = try await transaction.commit() + + let committedVersion = try transaction.getCommittedVersion() + #expect(committedVersion > 0, "Committed version should be positive for write transaction") +} + +@Test("getCommittedVersion returns -1 for read-only transaction") +func testGetCommittedVersionReadOnly() async throws { + try await FdbClient.initialize() + let database = try FdbClient.openDatabase() + let transaction = try database.createTransaction() + + // Read-only transaction + _ = try await transaction.getValue(for: "test_readonly_key") + _ = try await transaction.commit() + + let committedVersion = try transaction.getCommittedVersion() + #expect(committedVersion == -1, "Read-only transaction should return -1") +} + +@Test("getApproximateSize returns transaction size") +func testGetApproximateSize() async throws { + try await FdbClient.initialize() + let database = try FdbClient.openDatabase() + let transaction = try database.createTransaction() + + // Initial size + let initialSize = try await transaction.getApproximateSize() + #expect(initialSize >= 0, "Initial size should be non-negative") + + // Add some mutations + for i in 0..<10 { + let key = "test_approx_\(i)" + let value = String(repeating: "z", count: 100) + transaction.setValue(value, for: key) + } + + // Size should increase + let sizeAfterMutations = try await transaction.getApproximateSize() + #expect(sizeAfterMutations > initialSize, "Size should increase after mutations") +} diff --git a/Tests/StackTester/Sources/StackTester/StackTester.swift b/Tests/StackTester/Sources/StackTester/StackTester.swift index 852ebc7..c0d4b5d 100644 --- a/Tests/StackTester/Sources/StackTester/StackTester.swift +++ b/Tests/StackTester/Sources/StackTester/StackTester.swift @@ -393,19 +393,21 @@ class StackMachine { pushRange(idx, result.records, prefixFilter: prefix) - case "GET_ESTIMATED_RANGE_SIZE": // TODO - // Python order: begin, end = inst.pop(2) + case "GET_ESTIMATED_RANGE_SIZE": let endKey = waitAndPop().item as! [UInt8] let beginKey = waitAndPop().item as! [UInt8] - // Not available in Swift bindings, store placeholder + let transaction = try currentTransaction() + + _ = try await transaction.getEstimatedRangeSizeBytes(beginKey: beginKey, endKey: endKey) store(idx, Array("GOT_ESTIMATED_RANGE_SIZE".utf8)) - case "GET_RANGE_SPLIT_POINTS": // TODO - // Python order: begin, end, chunkSize = inst.pop(3) + case "GET_RANGE_SPLIT_POINTS": let chunkSize = waitAndPop().item as! Int64 let endKey = waitAndPop().item as! [UInt8] let beginKey = waitAndPop().item as! [UInt8] - // Not available in Swift bindings, store placeholder + let transaction = try currentTransaction() + + _ = try await transaction.getRangeSplitPoints(beginKey: beginKey, endKey: endKey, chunkSize: chunkSize) store(idx, Array("GOT_RANGE_SPLIT_POINTS".utf8)) case "CLEAR": @@ -456,13 +458,14 @@ class StackMachine { let transaction = try currentTransaction() transaction.setReadVersion(version) - case "GET_COMMITTED_VERSION": // TODO - // Not available in Swift bindings, store lastVersion instead - store(idx, lastVersion) + case "GET_COMMITTED_VERSION": + let transaction = try currentTransaction() + lastVersion = try transaction.getCommittedVersion() store(idx, Array("GOT_COMMITTED_VERSION".utf8)) - case "GET_APPROXIMATE_SIZE": // TODO - // Not available in Swift bindings, store placeholder + case "GET_APPROXIMATE_SIZE": + let transaction = try currentTransaction() + _ = try await transaction.getApproximateSize() store(idx, Array("GOT_APPROXIMATE_SIZE".utf8)) case "GET_VERSIONSTAMP": From dad8ccb6d0df80a65cca7af3150cbf22cc82bc0e Mon Sep 17 00:00:00 2001 From: Vishesh Yadav Date: Tue, 7 Oct 2025 17:16:55 -0700 Subject: [PATCH 11/16] Change few things --- Sources/FoundationDB/Client.swift | 2 +- .../FoundationDBTests/FoundationDBTests.swift | 25 -------- .../FoundationDBTupleTests.swift | 62 +++++++++---------- 3 files changed, 32 insertions(+), 57 deletions(-) diff --git a/Sources/FoundationDB/Client.swift b/Sources/FoundationDB/Client.swift index 0ef7f4a..3ca6772 100644 --- a/Sources/FoundationDB/Client.swift +++ b/Sources/FoundationDB/Client.swift @@ -37,7 +37,7 @@ import CFoundationDB public class FdbClient { /// FoundationDB API version constants. public enum APIVersion { - /// The current supported API version (730). + /// The current supported API version (740). public static let current: Int32 = 740 } diff --git a/Tests/FoundationDBTests/FoundationDBTests.swift b/Tests/FoundationDBTests/FoundationDBTests.swift index 5631d45..4801d05 100644 --- a/Tests/FoundationDBTests/FoundationDBTests.swift +++ b/Tests/FoundationDBTests/FoundationDBTests.swift @@ -1136,31 +1136,6 @@ func atomicOpByteMax() async throws { #expect(resultString == "zebra", "byte_max should choose lexicographically larger value") } -@Test("network option setting - method validation") -func networkOptionMethods() throws { - // Test that network option methods accept different parameter types - // Note: These tests verify the API works but don't actually set options - // since network initialization happens globally - - // Test Data parameter - let data = [UInt8]("test_value".utf8) - // This would normally throw if the method signature was wrong - - // Test String parameter - _ = "test_string" - // This would normally throw if the method signature was wrong - - // Test Int parameter - _ = 1_048_576 - // This would normally throw if the method signature was wrong - - // Test no parameter (for boolean options) - // This would normally throw if the method signature was wrong - - // If we get here, the method signatures are correct - #expect(data.count > 0, "Network option method signatures are valid") -} - @Test("network option enum values") func networkOptionEnumValues() { // Test that network option enum has expected values diff --git a/Tests/FoundationDBTests/FoundationDBTupleTests.swift b/Tests/FoundationDBTests/FoundationDBTupleTests.swift index 9aa77da..84ea8d5 100644 --- a/Tests/FoundationDBTests/FoundationDBTupleTests.swift +++ b/Tests/FoundationDBTests/FoundationDBTupleTests.swift @@ -307,34 +307,34 @@ func testTupleNestedDeep() throws { #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)") -} +// @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 c93d487b3b44437aab7d4313028e51deb3a0f11a Mon Sep 17 00:00:00 2001 From: Vishesh Yadav Date: Tue, 7 Oct 2025 17:34:37 -0700 Subject: [PATCH 12/16] add setoption --- Sources/FoundationDB/Database.swift | 49 +++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/Sources/FoundationDB/Database.swift b/Sources/FoundationDB/Database.swift index 354b487..f31a63f 100644 --- a/Sources/FoundationDB/Database.swift +++ b/Sources/FoundationDB/Database.swift @@ -66,4 +66,53 @@ public class FdbDatabase: IDatabase { return FdbTransaction(transaction: tr) } + + /// Sets a database option with a byte array value. + /// + /// - Parameters: + /// - option: The database option to set. + /// - value: The value for the option (optional). + /// - Throws: `FdbError` if the option cannot be set. + public func setOption(_ option: Fdb.DatabaseOption, value: Fdb.Value? = nil) throws { + let error: Int32 + if let value = value { + error = value.withUnsafeBytes { bytes in + fdb_database_set_option( + database, + FDBDatabaseOption(option.rawValue), + bytes.bindMemory(to: UInt8.self).baseAddress, + Int32(value.count) + ) + } + } else { + error = fdb_database_set_option(database, FDBDatabaseOption(option.rawValue), nil, 0) + } + + if error != 0 { + throw FdbError(code: error) + } + } + + /// Sets a database option with a string value. + /// + /// - Parameters: + /// - option: The database option to set. + /// - value: The string value for the option. + /// - Throws: `FdbError` if the option cannot be set. + public func setOption(_ option: Fdb.DatabaseOption, value: String) throws { + try setOption(option, value: Array(value.utf8)) + } + + /// Sets a database option with an integer value. + /// + /// - Parameters: + /// - option: The database option to set. + /// - value: The integer value for the option. + /// - Throws: `FdbError` if the option cannot be set. + public func setOption(_ option: Fdb.DatabaseOption, value: Int) throws { + var val = Int64(value).littleEndian + try withUnsafeBytes(of: &val) { bytes in + try setOption(option, value: Array(bytes)) + } + } } From 4077065a588208083e4e16a515887c086b1a60c5 Mon Sep 17 00:00:00 2001 From: Vishesh Yadav Date: Tue, 7 Oct 2025 19:09:07 -0700 Subject: [PATCH 13/16] addConflictRange support --- Sources/FoundationDB/Client.swift | 2 +- Sources/FoundationDB/Fdb+Options.swift | 2 +- Sources/FoundationDB/FoundationdDB.swift | 12 ++ Sources/FoundationDB/Transaction.swift | 19 +++ .../FoundationDBTests/FoundationDBTests.swift | 128 ++++++++++++++++++ 5 files changed, 161 insertions(+), 2 deletions(-) diff --git a/Sources/FoundationDB/Client.swift b/Sources/FoundationDB/Client.swift index 3ca6772..0e8a19b 100644 --- a/Sources/FoundationDB/Client.swift +++ b/Sources/FoundationDB/Client.swift @@ -37,7 +37,7 @@ import CFoundationDB public class FdbClient { /// FoundationDB API version constants. public enum APIVersion { - /// The current supported API version (740). + /// The current supported API version (710). public static let current: Int32 = 740 } diff --git a/Sources/FoundationDB/Fdb+Options.swift b/Sources/FoundationDB/Fdb+Options.swift index 74341a0..deb9b86 100644 --- a/Sources/FoundationDB/Fdb+Options.swift +++ b/Sources/FoundationDB/Fdb+Options.swift @@ -473,7 +473,7 @@ public extension Fdb { } /** Conflict range types used internally by the C API. */ - enum ConflictRangeType: UInt32, @unchecked Sendable { + public enum ConflictRangeType: UInt32, @unchecked Sendable { /** Used to add a read conflict range */ case read = 0 diff --git a/Sources/FoundationDB/FoundationdDB.swift b/Sources/FoundationDB/FoundationdDB.swift index d23ad1a..7d53c44 100644 --- a/Sources/FoundationDB/FoundationdDB.swift +++ b/Sources/FoundationDB/FoundationdDB.swift @@ -276,6 +276,18 @@ public protocol ITransaction: Sendable { /// - mutationType: The type of atomic operation to perform. func atomicOp(key: Fdb.Key, param: Fdb.Value, mutationType: Fdb.MutationType) + /// Adds a conflict range to the transaction. + /// + /// Conflict ranges are used to manually declare the read and write sets of the transaction. + /// This can be useful for ensuring serializability when certain keys are accessed indirectly. + /// + /// - Parameters: + /// - beginKey: The start of the range (inclusive) as a byte array. + /// - endKey: The end of the range (exclusive) as a byte array. + /// - type: The type of conflict range (read or write). + /// - Throws: `FdbError` if the operation fails. + func addConflictRange(beginKey: Fdb.Key, endKey: Fdb.Key, type: Fdb.ConflictRangeType) throws + // MARK: - Transaction option methods /// Sets a transaction option with an optional value. diff --git a/Sources/FoundationDB/Transaction.swift b/Sources/FoundationDB/Transaction.swift index 0a54306..c0c52f6 100644 --- a/Sources/FoundationDB/Transaction.swift +++ b/Sources/FoundationDB/Transaction.swift @@ -211,6 +211,25 @@ public class FdbTransaction: ITransaction, @unchecked Sendable { ).getAsync()?.value ?? 0 } + public func addConflictRange(beginKey: Fdb.Key, endKey: Fdb.Key, type: Fdb.ConflictRangeType) throws { + let error = beginKey.withUnsafeBytes { beginKeyBytes in + endKey.withUnsafeBytes { endKeyBytes in + fdb_transaction_add_conflict_range( + transaction, + beginKeyBytes.bindMemory(to: UInt8.self).baseAddress, + Int32(beginKey.count), + endKeyBytes.bindMemory(to: UInt8.self).baseAddress, + Int32(endKey.count), + FDBConflictRangeType(rawValue: type.rawValue) + ) + } + } + + if error != 0 { + throw FdbError(code: error) + } + } + public func getRange( beginSelector: Fdb.KeySelector, endSelector: Fdb.KeySelector, limit: Int32 = 0, snapshot: Bool diff --git a/Tests/FoundationDBTests/FoundationDBTests.swift b/Tests/FoundationDBTests/FoundationDBTests.swift index 4801d05..e6ebc48 100644 --- a/Tests/FoundationDBTests/FoundationDBTests.swift +++ b/Tests/FoundationDBTests/FoundationDBTests.swift @@ -1534,3 +1534,131 @@ func testGetApproximateSize() async throws { let sizeAfterMutations = try await transaction.getApproximateSize() #expect(sizeAfterMutations > initialSize, "Size should increase after mutations") } + +@Test("addConflictRange read conflict") +func testAddReadConflictRange() async throws { + try await FdbClient.initialize() + let database = try FdbClient.openDatabase() + + // Clear test key range + let clearTransaction = try database.createTransaction() + clearTransaction.clearRange(beginKey: "test_conflict_", endKey: "test_conflict`") + _ = try await clearTransaction.commit() + + // Set up initial data + let setupTransaction = try database.createTransaction() + setupTransaction.setValue("initial_value", for: "test_conflict_a") + _ = try await setupTransaction.commit() + + // Test adding read conflict range + let transaction = try database.createTransaction() + let beginKey: Fdb.Key = Array("test_conflict_a".utf8) + let endKey: Fdb.Key = Array("test_conflict_b".utf8) + + // Add read conflict range - should succeed + try transaction.addConflictRange(beginKey: beginKey, endKey: endKey, type: .read) + + // Should be able to commit successfully + let result = try await transaction.commit() + #expect(result == true, "Transaction with read conflict range should commit successfully") +} + +@Test("addConflictRange write conflict") +func testAddWriteConflictRange() async throws { + try await FdbClient.initialize() + let database = try FdbClient.openDatabase() + + // Clear test key range + let clearTransaction = try database.createTransaction() + clearTransaction.clearRange(beginKey: "test_write_conflict_", endKey: "test_write_conflict`") + _ = try await clearTransaction.commit() + + // Test adding write conflict range + let transaction = try database.createTransaction() + let beginKey: Fdb.Key = Array("test_write_conflict_a".utf8) + let endKey: Fdb.Key = Array("test_write_conflict_b".utf8) + + // Add write conflict range - should succeed + try transaction.addConflictRange(beginKey: beginKey, endKey: endKey, type: .write) + + // Should be able to commit successfully + let result = try await transaction.commit() + #expect(result == true, "Transaction with write conflict range should commit successfully") +} + +@Test("addConflictRange detects concurrent write conflicts") +func testConflictRangeDetectsConcurrentWrites() async throws { + try await FdbClient.initialize() + let database = try FdbClient.openDatabase() + + // Clear test key range + let clearTransaction = try database.createTransaction() + clearTransaction.clearRange(beginKey: "test_concurrent_", endKey: "test_concurrent`") + _ = try await clearTransaction.commit() + + // Set up initial data + let setupTransaction = try database.createTransaction() + setupTransaction.setValue("initial", for: "test_concurrent_key") + _ = try await setupTransaction.commit() + + // Create first transaction and add a write conflict range + let transaction1 = try database.createTransaction() + let beginKey: Fdb.Key = Array("test_concurrent_key".utf8) + var endKey: Fdb.Key = beginKey + endKey.append(0x00) + + try transaction1.addConflictRange(beginKey: beginKey, endKey: endKey, type: .write) + + // Create second transaction that writes to the same key + let transaction2 = try database.createTransaction() + transaction2.setValue("modified_by_tr2", for: "test_concurrent_key") + _ = try await transaction2.commit() + + // Now try to commit transaction1 - it should detect a conflict + do { + _ = try await transaction1.commit() + // If it succeeds, that's also acceptable behavior + } catch let error as FdbError { + // Expected to fail with a conflict error (not_committed) + #expect(error.isRetryable, "Conflict should be a retryable error") + } +} + +@Test("addConflictRange multiple ranges") +func testAddMultipleConflictRanges() async throws { + try await FdbClient.initialize() + let database = try FdbClient.openDatabase() + + // Clear test key range + let clearTransaction = try database.createTransaction() + clearTransaction.clearRange(beginKey: "test_multi_", endKey: "test_multi`") + _ = try await clearTransaction.commit() + + // Test adding multiple conflict ranges + let transaction = try database.createTransaction() + + // Add multiple read conflict ranges + let beginKey1: Fdb.Key = Array("test_multi_a".utf8) + let endKey1: Fdb.Key = Array("test_multi_b".utf8) + try transaction.addConflictRange(beginKey: beginKey1, endKey: endKey1, type: .read) + + let beginKey2: Fdb.Key = Array("test_multi_c".utf8) + let endKey2: Fdb.Key = Array("test_multi_d".utf8) + try transaction.addConflictRange(beginKey: beginKey2, endKey: endKey2, type: .read) + + // Add write conflict range + let beginKey3: Fdb.Key = Array("test_multi_x".utf8) + let endKey3: Fdb.Key = Array("test_multi_y".utf8) + try transaction.addConflictRange(beginKey: beginKey3, endKey: endKey3, type: .write) + + // Should be able to commit with multiple conflict ranges + let result = try await transaction.commit() + #expect(result == true, "Transaction with multiple conflict ranges should commit successfully") +} + +@Test("ConflictRangeType enum values") +func testConflictRangeTypeValues() { + // Test that conflict range type enum has expected values + #expect(Fdb.ConflictRangeType.read.rawValue == 0, "read should have value 0") + #expect(Fdb.ConflictRangeType.write.rawValue == 1, "write should have value 1") +} From c77f736791d75b21d7e46c3d0da0009a759b8bb1 Mon Sep 17 00:00:00 2001 From: Vishesh Yadav Date: Tue, 7 Oct 2025 19:09:16 -0700 Subject: [PATCH 14/16] conflict range in stacktester --- .../Sources/StackTester/StackTester.swift | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Tests/StackTester/Sources/StackTester/StackTester.swift b/Tests/StackTester/Sources/StackTester/StackTester.swift index c0d4b5d..48fd172 100644 --- a/Tests/StackTester/Sources/StackTester/StackTester.swift +++ b/Tests/StackTester/Sources/StackTester/StackTester.swift @@ -477,22 +477,32 @@ class StackMachine { } case "READ_CONFLICT_RANGE": - let beginKey = waitAndPop().item as! [UInt8] let endKey = waitAndPop().item as! [UInt8] - // Conflict ranges not exposed in Swift bindings, just consume parameters + let beginKey = waitAndPop().item as! [UInt8] + let transaction = try currentTransaction() + try transaction.addConflictRange(beginKey: beginKey, endKey: endKey, type: .read) case "WRITE_CONFLICT_RANGE": - let beginKey = waitAndPop().item as! [UInt8] let endKey = waitAndPop().item as! [UInt8] - // Conflict ranges not exposed in Swift bindings, just consume parameters + let beginKey = waitAndPop().item as! [UInt8] + let transaction = try currentTransaction() + try transaction.addConflictRange(beginKey: beginKey, endKey: endKey, type: .write) case "READ_CONFLICT_KEY": let key = waitAndPop().item as! [UInt8] - // Conflict ranges not exposed in Swift bindings, just consume parameters + let transaction = try currentTransaction() + // For a single key, create a range [key, key+\x00) + var endKey = key + endKey.append(0x00) + try transaction.addConflictRange(beginKey: key, endKey: endKey, type: .read) case "WRITE_CONFLICT_KEY": let key = waitAndPop().item as! [UInt8] - // Conflict ranges not exposed in Swift bindings, just consume parameters + let transaction = try currentTransaction() + // For a single key, create a range [key, key+\x00) + var endKey = key + endKey.append(0x00) + try transaction.addConflictRange(beginKey: key, endKey: endKey, type: .write) case "DISABLE_WRITE_CONFLICT": // Not directly available in Swift bindings, could use transaction option From 7186e9ef7d9f6c0f32bbf3f096860de05d2cd2ba Mon Sep 17 00:00:00 2001 From: Vishesh Yadav Date: Tue, 7 Oct 2025 19:11:33 -0700 Subject: [PATCH 15/16] reduce apiversion to 710 --- Sources/CFoundationDB/fdb_c_wrapper.h | 2 +- Sources/FoundationDB/Client.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CFoundationDB/fdb_c_wrapper.h b/Sources/CFoundationDB/fdb_c_wrapper.h index eb8f344..b32367b 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 710 #include #endif diff --git a/Sources/FoundationDB/Client.swift b/Sources/FoundationDB/Client.swift index 0e8a19b..e5d05a7 100644 --- a/Sources/FoundationDB/Client.swift +++ b/Sources/FoundationDB/Client.swift @@ -38,7 +38,7 @@ public class FdbClient { /// FoundationDB API version constants. public enum APIVersion { /// The current supported API version (710). - public static let current: Int32 = 740 + public static let current: Int32 = 710 } /// Initializes the FoundationDB client with the specified API version. From f5d4f3941712d96c48a49ac1271f06b3f77c4781 Mon Sep 17 00:00:00 2001 From: Vishesh Yadav Date: Tue, 7 Oct 2025 19:13:31 -0700 Subject: [PATCH 16/16] Use FDB_LATEST_API_VERSION in header --- Sources/CFoundationDB/fdb_c_wrapper.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CFoundationDB/fdb_c_wrapper.h b/Sources/CFoundationDB/fdb_c_wrapper.h index b32367b..4f61c56 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 710 +#define FDB_API_VERSION FDB_LATEST_API_VERSION #include #endif