Skip to content

Commit 073b97d

Browse files
committed
Robust FreeTDS integration: single init, strict serialization, and UTF-8 support
1 parent c43bdc9 commit 073b97d

1 file changed

Lines changed: 37 additions & 40 deletions

File tree

Sources/SQLClientSwift/SQLClient.swift

Lines changed: 37 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,28 @@ private struct TDSHandle: @unchecked Sendable {
129129

130130
public actor SQLClient {
131131
public static let shared = SQLClient()
132-
public init() {}
132+
133+
private static let initializeFreeTDS: Void = {
134+
dbinit()
135+
dberrhandle(SQLClient_errorHandler)
136+
dbmsghandle(SQLClient_messageHandler)
137+
}()
138+
139+
public init() {
140+
_ = SQLClient.initializeFreeTDS
141+
}
133142

134143
private let queue = DispatchQueue(label: "com.sqlclient.serial")
135144
private var activeTask: Task<Void, Never>?
136145

137-
private func awaitPrevious() async {
138-
_ = await activeTask?.result
146+
private func serialize<T: Sendable>(_ operation: @escaping @Sendable () async throws -> T) async throws -> T {
147+
let previousTask = activeTask
148+
let newTask: Task<T, Error> = Task {
149+
_ = await previousTask?.result
150+
return try await operation()
151+
}
152+
activeTask = Task { _ = await newTask.result }
153+
return try await newTask.value
139154
}
140155

141156
public var maxTextSize: Int = 4096
@@ -148,70 +163,44 @@ public actor SQLClient {
148163
}
149164

150165
public func connect(options: SQLClientConnectionOptions) async throws {
151-
print("DEBUG SQL: connect start")
152-
await awaitPrevious()
153-
print("DEBUG SQL: connect entering task")
154-
let task = Task {
155-
print("DEBUG SQL: connect execution start")
166+
try await serialize {
156167
guard !self.connected else { throw SQLClientError.alreadyConnected }
157168

158169
let result = try await self.runBlocking {
159-
print("DEBUG SQL: connect blocking start")
160170
return try self._connectSync(options: options)
161171
}
162172

163173
self.login = result.login.pointer
164174
self.connection = result.connection.pointer
165175
self.connected = true
166-
print("DEBUG SQL: connect execution complete")
167176
}
168-
activeTask = Task { _ = await task.result }
169-
try await task.value
170177
}
171178

172179
public func disconnect() async {
173-
print("DEBUG SQL: disconnect start")
174-
await awaitPrevious()
175-
print("DEBUG SQL: disconnect entering task")
176-
let task = Task {
177-
print("DEBUG SQL: disconnect execution start")
180+
_ = try? await serialize {
178181
guard self.connected else { return }
179182
let lgn = self.login.map { TDSHandle(pointer: $0) }
180183
let conn = self.connection.map { TDSHandle(pointer: $0) }
181184
await self.runBlockingVoid {
182-
print("DEBUG SQL: disconnect blocking start")
183185
self._disconnectSync(login: lgn, connection: conn)
184186
}
185187
self.login = nil
186188
self.connection = nil
187189
self.connected = false
188-
print("DEBUG SQL: disconnect execution complete")
189190
}
190-
activeTask = Task { _ = await task.result }
191-
_ = await task.result
192191
}
193192

194193
public func execute(_ sql: String) async throws -> SQLClientResult {
195-
let snippet = String(sql.prefix(30)).replacingOccurrences(of: "\n", with: " ")
196-
print("DEBUG SQL: execute start [\(snippet)...]")
197-
await awaitPrevious()
198-
print("DEBUG SQL: execute entering task [\(snippet)...]")
199-
let task = Task {
200-
print("DEBUG SQL: execute execution start [\(snippet)...]")
194+
try await serialize {
201195
guard self.connected, let conn = self.connection else { throw SQLClientError.notConnected }
202196
guard !sql.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { throw SQLClientError.noCommandText }
203197
let maxText = self.maxTextSize
204198
let handle = TDSHandle(pointer: conn)
205199

206-
let res = try await self.runBlocking {
207-
print("DEBUG SQL: execute blocking start [\(snippet)...]")
200+
return try await self.runBlocking {
208201
return try self._executeSync(sql: sql, connection: handle, maxTextSize: maxText)
209202
}
210-
print("DEBUG SQL: execute execution complete [\(snippet)...]")
211-
return res
212203
}
213-
activeTask = Task { _ = await task.result }
214-
return try await task.value
215204
}
216205

217206
public func query(_ sql: String) async throws -> [SQLRow] { try await execute(sql).rows }
@@ -265,8 +254,8 @@ public actor SQLClient {
265254
}
266255
Thread.sleep(forTimeInterval: 0.1)
267256
}
268-
CFReadStreamOpen(read)
269-
CFWriteStreamOpen(write)
257+
CFReadStreamClose(read)
258+
CFWriteStreamClose(write)
270259

271260
if connected {
272261
cont.resume()
@@ -278,15 +267,14 @@ public actor SQLClient {
278267
}
279268

280269
private nonisolated func _connectSync(options: SQLClientConnectionOptions) throws -> (login: TDSHandle, connection: TDSHandle) {
281-
dbinit()
282-
dberrhandle(SQLClient_errorHandler)
283-
dbmsghandle(SQLClient_messageHandler)
284-
285270
guard let lgn = dblogin() else { throw SQLClientError.loginAllocationFailed }
286271

287272
dbsetlname(lgn, options.username, 2) // DBSETUSER
288273
dbsetlname(lgn, options.password, 3) // DBSETPWD
289274
dbsetlname(lgn, "SQLClientSwift", 5) // DBSETAPP
275+
276+
// Ensure we get UTF-8 from the server for N-types
277+
dbsetlcharset(lgn, "UTF-8")
290278

291279
if let port = options.port { dbsetlshort(lgn, Int32(port), 13) } // DBSETPORT
292280
if options.encryption != .request { dbsetlname(lgn, options.encryption.rawValue, 17) } // DBSETENCRYPTION
@@ -320,6 +308,10 @@ public actor SQLClient {
320308

321309
private nonisolated func _executeSync(sql: String, connection: TDSHandle, maxTextSize: Int) throws -> SQLClientResult {
322310
let conn = connection.pointer
311+
312+
// Ensure any previous results are cancelled before a new command
313+
dbcancel(conn)
314+
323315
_ = dbsetopt(conn, DBTEXTSIZE, "\(maxTextSize)", -1)
324316

325317
guard dbcmd(conn, sql) != FAIL, dbsqlexec(conn) != FAIL else { throw SQLClientError.executionFailed }
@@ -382,7 +374,12 @@ public actor SQLClient {
382374
return NSNumber(value: data.load(as: UInt8.self) != 0)
383375
case 47, 39, 102, 103, 35, 99, 241: // SYBCHAR, SYBVARCHAR, SYBTEXT, SYBNTEXT, SYBXML, SYBNCHAR, SYBNVARCHAR
384376
let buf = UnsafeBufferPointer<UInt8>(start: data.assumingMemoryBound(to: UInt8.self), count: Int(len))
385-
return String(bytes: buf, encoding: .utf8) ?? String(bytes: buf, encoding: .windowsCP1252) ?? ""
377+
// Try UTF-8 first, then windowsCP1252 as fallback
378+
if let str = String(bytes: buf, encoding: .utf8) { return str }
379+
if let str = String(bytes: buf, encoding: .windowsCP1252) { return str }
380+
// If it's UCS-2 (type 103/SYBNVARCHAR usually), try UTF-16
381+
if let str = String(bytes: buf, encoding: .utf16LittleEndian) { return str }
382+
return ""
386383
case 45, 37, 34, 173, 174, 167: // SYBBINARY, SYBVARBINARY, SYBIMAGE, SYBBIGBINARY, SYBBIGVARBINARY, SYBBLOB
387384
return Data(bytes: dataPtr, count: Int(len))
388385
case 61, 58, 111: // SYBDATETIME, SYBDATETIME4, SYBDATETIMN

0 commit comments

Comments
 (0)