You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Implement missing DB-Library features: RPC, BCP, Transactions, and Parameterized queries
- Added full RPC support with OUTPUT parameter and return status handling.
- Implemented high-performance Bulk Copy (BCP) via bulkInsert().
- Added explicit transaction management (begin/commit/rollback).
- Added executeParameterized() using sp_executesql.
- Refactored SQLClient into focused extensions for better maintainability.
- Updated README.md with comprehensive documentation and usage samples.
- Verified build compatibility for Swift Package Index (SPI).
- Added 11 new integration tests covering all new functionality.
-**Affected-row counts** — `rowsAffected` from `INSERT` / `UPDATE` / `DELETE`
25
-
-**Parameterised queries** — built-in SQL injection protection via `?` placeholders
25
+
-**Remote Procedure Calls (RPC)** — efficient stored procedure execution with full `OUTPUT` parameter and return status support
26
+
-**Explicit Transactions** — `beginTransaction()`, `commitTransaction()`, and `rollbackTransaction()`
27
+
-**Bulk Copy (BCP)** — high-performance `bulkInsert()` for large data sets
28
+
-**Connection Pooling** — built-in `SQLClientPool` for high-concurrency applications
29
+
-**Parameterised queries** — built-in SQL injection protection via `?` placeholders or `executeParameterized()`
26
30
-**All SQL Server date types** — `date`, `time`, `datetime2`, `datetimeoffset` as native `Date`
27
31
-**Swift Package Manager** — single dependency, no Ruby tooling required
28
32
@@ -126,6 +130,26 @@ await client.disconnect()
126
130
127
131
---
128
132
133
+
## Connection Pooling
134
+
135
+
For server-side applications with high concurrency, use `SQLClientPool` to manage a pool of reusable connections:
136
+
137
+
```swift
138
+
let options =SQLClientConnectionOptions(server: "myserver", username: "sa", password: "pwd")
139
+
let pool =SQLClientPool(options: options, maxPoolSize: 10)
140
+
141
+
// Use a client from the pool
142
+
tryawait pool.withClient { client in
143
+
let rows =tryawait client.query("SELECT GETDATE()")
144
+
print(rows[0])
145
+
}
146
+
147
+
// Disconnect all clients when shutting down
148
+
await pool.disconnectAll()
149
+
```
150
+
151
+
---
152
+
129
153
## Usage
130
154
131
155
### Connecting
@@ -203,6 +227,9 @@ for row in rows {
203
227
if row.isNull("DiscontinuedDate") {
204
228
print("\(name ??"") is still available")
205
229
}
230
+
231
+
// Convert the entire row to a dictionary
232
+
let dict = row.toDictionary()
206
233
}
207
234
```
208
235
@@ -214,20 +241,17 @@ let firstColumn = row[0]
214
241
215
242
### Querying — `Decodable` Mapping
216
243
217
-
Map rows directly to your own `Decodable` structs. Column name matching is **case-insensitive** and handles `snake_case` ↔ `camelCase` automatically:
244
+
Map rows directly to your own `Decodable` structs. Column name matching is **case-insensitive** and handles `snake_case` ↔ `camelCase` automatically. It also supports automatic conversion from `String` to primitive types, `Date`, `URL`, and `Decimal`:
218
245
219
246
```swift
220
-
structProduct: Decodable {
221
-
letproductID: Int
222
-
letname: String
223
-
letprice: Decimal
224
-
letdateAdded: Date
247
+
structUserProfile: Decodable {
248
+
letuserID: Int// matches "user_id" or "UserID"
249
+
letdisplayName: String// matches "display_name"
250
+
letwebsite: URL?// automatically converted from string
251
+
letbalance: Decimal // automatically converted
225
252
}
226
253
227
-
// "product_id", "ProductID", and "productId" all match the `productID` property
228
-
let products: [Product] =tryawait client.query(
229
-
"SELECT product_id, name, price, date_added FROM Products"
230
-
)
254
+
let profiles: [UserProfile] =tryawait client.query("SELECT * FROM UserProfiles")
231
255
```
232
256
233
257
### Executing — `SQLClientResult`
@@ -256,6 +280,43 @@ let affected = try await client.run(
256
280
print("\(affected) row(s) updated")
257
281
```
258
282
283
+
### Explicit Transactions
284
+
285
+
Wrap multiple operations in a transaction to ensure atomicity:
286
+
287
+
```swift
288
+
tryawait client.beginTransaction()
289
+
do {
290
+
tryawait client.run("INSERT INTO Orders ...")
291
+
tryawait client.run("UPDATE Inventory ...")
292
+
tryawait client.commitTransaction()
293
+
} catch {
294
+
tryawait client.rollbackTransaction()
295
+
throw error
296
+
}
297
+
```
298
+
299
+
### Stored Procedures (RPC)
300
+
301
+
The `executeRPC` method is the most efficient way to call stored procedures and correctly supports `OUTPUT` parameters and return status:
Use `?` placeholders to pass values safely. Strings are automatically escaped to prevent SQL injection:
@@ -280,7 +341,36 @@ try await client.run(
280
341
)
281
342
```
282
343
283
-
> **Note:** This uses string-level escaping (single-quote doubling). For maximum security with untrusted user input, prefer stored procedures.
344
+
> **Note:** This uses string-level escaping (single-quote doubling). For maximum security or output parameters in ad-hoc queries, use `executeParameterized()`:
345
+
346
+
```swift
347
+
let params = [
348
+
SQLParameter(name: "@UserID", value: 42),
349
+
SQLParameter(name: "@Msg", value: "Hello World")
350
+
]
351
+
let result =tryawait client.executeParameterized(
352
+
"SELECT * FROM Users WHERE ID = @UserID; PRINT @Msg",
353
+
parameters: params
354
+
)
355
+
```
356
+
357
+
### Bulk Insert (BCP)
358
+
359
+
For high-performance loading of thousands of rows, use the Bulk Copy (BCP) interface:
360
+
361
+
```swift
362
+
var rows: [SQLRow] = []
363
+
for i in1...1000 {
364
+
let storage: [(key: String, value: Sendable)] = [
365
+
(key: "ID", value: i),
366
+
(key: "Name", value: "Bulk User \(i)")
367
+
]
368
+
rows.append(SQLRow(storage, columnTypes: [:]))
369
+
}
370
+
371
+
let inserted =tryawait client.bulkInsert(table: "LargeTable", rows: rows)
372
+
print("Bulk inserted \(inserted) rows")
373
+
```
284
374
285
375
### SQLDataTable & SQLDataSet
286
376
@@ -389,10 +479,14 @@ print(ds.count) // number of tables
389
479
390
480
#### Backward compatibility
391
481
392
-
`SQLDataTable` can be converted back to `[SQLRow]` if you need to pass it to existing code:
482
+
`SQLDataTable` can be converted back to `[SQLRow]` if you need to pass it to existing code or use `bulkInsert` with a table you just fetched:
393
483
394
484
```swift
395
485
let sqlRows: [SQLRow] = table.toSQLRows()
486
+
487
+
// Example: fetch from one table and bulk insert into another
488
+
let table =tryawait client.dataTable("SELECT * FROM SourceTable")
0 commit comments