Skip to content

Commit 1281379

Browse files
committed
feat: DataTable support
1 parent d67872b commit 1281379

2 files changed

Lines changed: 584 additions & 20 deletions

File tree

README.md

Lines changed: 134 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This is a Swift rewrite and modernisation of [martinrybak/SQLClient](https://git
1414
- **Swift `actor`** — connection state is safe across concurrent callers by design
1515
- **`Decodable` row mapping** — map query results directly to your Swift structs
1616
- **Typed `SQLRow`** — access columns as `String`, `Int`, `Date`, `UUID`, `Decimal`, and more
17+
- **`SQLDataTable` & `SQLDataSet`** — typed, named tables with JSON serialisation and Markdown rendering
1718
- **Full TLS support**`off`, `request`, `require`, and `strict` (TDS 8.0 / Azure SQL)
1819
- **FreeTDS 1.5** — NTLMv2, read-only AG routing, Kerberos auth, IPv6, cluster failover
1920
- **Affected-row counts**`rowsAffected` from `INSERT` / `UPDATE` / `DELETE`
@@ -234,6 +235,119 @@ try await client.run(
234235

235236
> **Note:** This uses string-level escaping (single-quote doubling). For maximum security with untrusted user input, prefer stored procedures.
236237
238+
### SQLDataTable & SQLDataSet
239+
240+
`SQLDataTable` is a typed, named result table — the Swift equivalent of .NET's `DataTable`. Each cell is a strongly-typed `SQLCellValue` enum, the table is `Codable` for JSON serialisation, and it can render itself as a Markdown table.
241+
242+
`SQLDataSet` is a collection of `SQLDataTable` instances, used when a query or stored procedure returns multiple result sets.
243+
244+
#### Fetching a single table
245+
246+
```swift
247+
let table = try await client.dataTable("SELECT * FROM Users")
248+
249+
print(table.rowCount) // number of rows
250+
print(table.columnCount) // number of columns
251+
```
252+
253+
#### Cell access
254+
255+
```swift
256+
// By row index and column name (case-insensitive)
257+
let cell: SQLCellValue = table[0, "Name"]
258+
259+
// By row and column index
260+
let cell: SQLCellValue = table[0, 0]
261+
262+
// As a typed value
263+
switch table[0, "Age"] {
264+
case .int32(let age): print("Age:", age)
265+
case .null: print("Age unknown")
266+
default: break
267+
}
268+
269+
// As Any? for interop with existing code
270+
let raw: Any? = table[0, "Name"].anyValue
271+
272+
// Whole row as a dictionary
273+
let dict: [String: SQLCellValue] = table.row(at: 0)
274+
275+
// All values in a column
276+
let names: [SQLCellValue] = table.column(named: "Name")
277+
```
278+
279+
#### Markdown rendering
280+
281+
```swift
282+
print(table.toMarkdown())
283+
```
284+
285+
Output example:
286+
287+
```
288+
| ID | Name | Email |
289+
|---|---|---|
290+
| 1 | Alice | alice@example.com |
291+
| 2 | Bob | bob@example.com |
292+
```
293+
294+
#### Decoding rows into a `Decodable` struct
295+
296+
```swift
297+
struct User: Decodable {
298+
let id: Int
299+
let name: String
300+
let email: String
301+
}
302+
303+
let users: [User] = try table.decode()
304+
```
305+
306+
#### JSON serialisation
307+
308+
`SQLDataTable` and `SQLDataSet` are fully `Codable`:
309+
310+
```swift
311+
let json = try JSONEncoder().encode(table)
312+
let restored = try JSONDecoder().decode(SQLDataTable.self, from: json)
313+
```
314+
315+
#### Converting an existing `SQLClientResult`
316+
317+
```swift
318+
let result = try await client.execute("SELECT * FROM Orders")
319+
320+
// First result set as SQLDataTable
321+
let table = result.asDataTable(name: "Orders")
322+
323+
// All result sets as SQLDataSet
324+
let ds = result.asSQLDataSet()
325+
```
326+
327+
#### Multi-table — `SQLDataSet`
328+
329+
Use `dataSet()` when a stored procedure or batch returns more than one result set:
330+
331+
```swift
332+
let ds = try await client.dataSet("EXEC sp_GetDashboard")
333+
334+
// Access by index
335+
let summary = ds[0]
336+
337+
// Access by name (case-insensitive, uses the table name assigned by the procedure)
338+
let details = ds["Details"]
339+
340+
print(ds.count) // number of tables
341+
```
342+
343+
#### Backward compatibility
344+
345+
`SQLDataTable` can be converted back to `[SQLRow]` if you need to pass it to existing code:
346+
347+
```swift
348+
let sqlRows: [SQLRow] = table.toSQLRows()
349+
```
350+
237351
### Error Handling
238352

239353
All errors are thrown as `SQLClientError`, which conforms to `LocalizedError`:
@@ -304,26 +418,26 @@ try await client.connect(options: options)
304418

305419
## Type Mapping
306420

307-
| SQL Server type | Swift type |
308-
|---|---|
309-
| `tinyint` | `NSNumber` (UInt8) |
310-
| `smallint` | `NSNumber` (Int16) |
311-
| `int` | `NSNumber` (Int32) |
312-
| `bigint` | `NSNumber` (Int64) |
313-
| `bit` | `NSNumber` (Bool) |
314-
| `real` | `NSNumber` (Float) |
315-
| `float` | `NSNumber` (Double) |
316-
| `decimal`, `numeric` | `NSDecimalNumber` |
317-
| `money`, `smallmoney` | `NSDecimalNumber` (4 decimal places) |
318-
| `char`, `varchar`, `nchar`, `nvarchar` | `String` |
319-
| `text`, `ntext`, `xml` | `String` |
320-
| `binary`, `varbinary`, `image` | `Data` |
321-
| `timestamp` | `Data` |
322-
| `datetime`, `smalldatetime` | `Date` |
323-
| `date`, `time`, `datetime2`, `datetimeoffset` | `Date` (TDS 7.3+) or `String` (TDS 7.1) |
324-
| `uniqueidentifier` | `UUID` |
325-
| `null` | `NSNull` |
326-
| `sql_variant`, `cursor`, `table` | ⚠️ Not supported |
421+
| SQL Server type | Swift type | `SQLCellValue` case |
422+
|---|---|---|
423+
| `tinyint` | `NSNumber` (UInt8) | `.int16` |
424+
| `smallint` | `NSNumber` (Int16) | `.int16` |
425+
| `int` | `NSNumber` (Int32) | `.int32` |
426+
| `bigint` | `NSNumber` (Int64) | `.int64` |
427+
| `bit` | `NSNumber` (Bool) | `.bool` |
428+
| `real` | `NSNumber` (Float) | `.float` |
429+
| `float` | `NSNumber` (Double) | `.double` |
430+
| `decimal`, `numeric` | `NSDecimalNumber` | `.decimal` |
431+
| `money`, `smallmoney` | `NSDecimalNumber` (4 dp) | `.decimal` |
432+
| `char`, `varchar`, `nchar`, `nvarchar` | `String` | `.string` |
433+
| `text`, `ntext`, `xml` | `String` | `.string` |
434+
| `binary`, `varbinary`, `image` | `Data` | `.bytes` |
435+
| `timestamp` | `Data` | `.bytes` |
436+
| `datetime`, `smalldatetime` | `Date` | `.date` |
437+
| `date`, `time`, `datetime2`, `datetimeoffset` | `Date` | `.date` |
438+
| `uniqueidentifier` | `UUID` | `.uuid` |
439+
| `null` | `NSNull` | `.null` |
440+
| `sql_variant`, `cursor`, `table` | ⚠️ Not supported ||
327441

328442
> **Date types note:** `date`, `time`, `datetime2`, and `datetimeoffset` are returned as `Date` when using TDS 7.3 or higher. FreeTDS 1.x defaults to `auto` protocol negotiation, which will select 7.3+ automatically for modern SQL Server versions. If you see strings instead of dates on an older server, set the `TDSVER` environment variable in your Xcode scheme to `7.3` or `auto`.
329443

0 commit comments

Comments
 (0)