Skip to content

Commit db95e57

Browse files
vkuttypCopilot
andcommitted
perf/feat: port JSONChunkAssembler byte-scan + SQLValue.toAny() from C#
JSONChunkAssembler (mirrors C# v1.4.6): - Replace String-based pending buffer with Data - feed() appends UTF-8 bytes directly (no String concat or re-encoding) - Scan bytes by index using ASCII constants — safe in UTF-8 since {, }, ", \ are all single-byte - results.append(pending[start...i]) is a zero-copy Data slice; no String(combined[start...i]) + Data(jsonStr.utf8) double-copy - Eliminates one String allocation per chunk and one re-encoding per found object SQLValue.toAny() (mirrors C# v1.3.8 ToClrObject()): - Returns the unwrapped underlying Swift value as Any?, nil for .null - Useful for JSON serialization, generic containers, and any code that needs the value without switching on every case C# fixes NOT ported (not applicable to Swift): - EnvChange string-type detection: Swift switch already handles each type correctly and ignores unknowns — the bug did not exist here - QueryStreamAsync lastBufEnd: Swift uses NIO frame-based delivery, no raw byte buffer management - FillDataTableAsync / DataTable: System.Data is .NET-only Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 319cf08 commit db95e57

2 files changed

Lines changed: 67 additions & 36 deletions

File tree

Sources/CosmoSQLCore/JSONChunkAssembler.swift

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -24,59 +24,67 @@ import Foundation
2424
/// }
2525
/// ```
2626
public struct JSONChunkAssembler {
27-
private var pending: String = "" // partial JSON object buffered across chunks
28-
private var depth: Int = 0 // current `{` nesting depth
29-
private var inString: Bool = false
30-
private var escaped: Bool = false
27+
// Working buffer — accumulated UTF-8 bytes across calls.
28+
// Using Data instead of String avoids per-chunk String concatenation and
29+
// the subsequent re-encoding when extracting found objects.
30+
private var pending: Data = Data()
31+
private var depth: Int = 0 // current { nesting depth
32+
private var inString: Bool = false
33+
private var escaped: Bool = false
34+
35+
// ASCII byte constants — all single-byte, safe to scan in UTF-8.
36+
private static let openBrace: UInt8 = 0x7B // {
37+
private static let closeBrace: UInt8 = 0x7D // }
38+
private static let quote: UInt8 = 0x22 // "
39+
private static let backslash: UInt8 = 0x5C // \
3140

3241
public init() {}
3342

3443
/// Feed a text chunk and return all complete JSON objects found.
3544
///
3645
/// Each returned `Data` value is valid UTF-8 JSON representing one top-level object.
3746
public mutating func feed(_ chunk: String) -> [Data] {
38-
let combined = pending.isEmpty ? chunk : pending + chunk
39-
pending = ""
47+
// Append UTF-8 bytes directly — one allocation, no intermediate String copy.
48+
pending.append(contentsOf: chunk.utf8)
4049

41-
var results: [Data] = []
42-
var objStart: String.Index? = nil
43-
var i = combined.startIndex
50+
var results: [Data] = []
51+
var objStart: Int? = nil
4452

45-
while i < combined.endIndex {
46-
let ch = combined[i]
53+
for i in 0 ..< pending.count {
54+
let ch = pending[i]
4755

4856
if escaped {
4957
escaped = false
50-
} else if inString {
51-
switch ch {
52-
case "\\": escaped = true
53-
case "\"": inString = false
54-
default: break
55-
}
56-
} else {
57-
switch ch {
58-
case "{":
59-
if depth == 0 { objStart = i }
60-
depth += 1
61-
case "}":
62-
depth -= 1
63-
if depth == 0, let start = objStart {
64-
let jsonStr = String(combined[start...i])
65-
results.append(Data(jsonStr.utf8))
66-
objStart = nil
67-
}
68-
case "\"":
69-
inString = true
70-
default:
71-
break
58+
continue
59+
}
60+
if inString {
61+
if ch == Self.backslash { escaped = true }
62+
else if ch == Self.quote { inString = false }
63+
continue
64+
}
65+
switch ch {
66+
case Self.openBrace:
67+
if depth == 0 { objStart = i }
68+
depth += 1
69+
case Self.closeBrace:
70+
depth -= 1
71+
if depth == 0, let start = objStart {
72+
// Zero-copy slice into the existing buffer — no re-encoding.
73+
results.append(pending[start ... i])
74+
objStart = nil
7275
}
76+
case Self.quote:
77+
inString = true
78+
default:
79+
break
7380
}
74-
i = combined.index(after: i)
7581
}
7682

77-
// Buffer any partial object for the next call
83+
// Retain only the partial object (if any) for the next call.
7884
if let start = objStart {
79-
pending = String(combined[start...])
85+
pending = Data(pending[start...])
86+
} else {
87+
pending = Data()
8088
}
8189

8290
return results

Sources/CosmoSQLCore/SQLValue.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,29 @@ public extension SQLValue {
7777
default: return nil
7878
}
7979
}
80+
81+
/// Returns the underlying Swift value as `Any`, or `nil` for `.null`.
82+
///
83+
/// Useful for JSON serialization, generic containers, and any code that needs
84+
/// the unwrapped value without pattern-matching on each `SQLValue` case.
85+
func toAny() -> Any? {
86+
switch self {
87+
case .null: return nil
88+
case .bool(let v): return v
89+
case .int(let v): return v
90+
case .int8(let v): return v
91+
case .int16(let v): return v
92+
case .int32(let v): return v
93+
case .int64(let v): return v
94+
case .float(let v): return v
95+
case .double(let v): return v
96+
case .decimal(let v): return v
97+
case .string(let v): return v
98+
case .bytes(let v): return v
99+
case .uuid(let v): return v
100+
case .date(let v): return v
101+
}
102+
}
80103
}
81104

82105
// MARK: - ExpressibleBy literals

0 commit comments

Comments
 (0)