@@ -189,4 +189,366 @@ final class SQLClientSwiftTests: XCTestCase {
189189 XCTFail ( " Expected notConnected " )
190190 } catch SQLClientError . notConnected { }
191191 }
192+
193+ // MARK: - SQLDataTable tests
194+
195+ func testDataTableRowAndColumnCount( ) async throws {
196+ let table = try await client. dataTable (
197+ " SELECT 1 AS A, 'hello' AS B UNION ALL SELECT 2, 'world' "
198+ )
199+ XCTAssertEqual ( table. rowCount, 2 )
200+ XCTAssertEqual ( table. columnCount, 2 )
201+ }
202+
203+ func testDataTableColumnNames( ) async throws {
204+ let table = try await client. dataTable ( " SELECT 42 AS Answer, 'hi' AS Greeting " )
205+ XCTAssertEqual ( table. columns [ 0 ] . name, " Answer " )
206+ XCTAssertEqual ( table. columns [ 1 ] . name, " Greeting " )
207+ }
208+
209+ func testDataTableSubscriptByName( ) async throws {
210+ let table = try await client. dataTable ( " SELECT 99 AS Score " )
211+ let cell = table [ 0 , " Score " ]
212+ if case . int32( let v) = cell {
213+ XCTAssertEqual ( v, 99 )
214+ } else {
215+ // Widen: some servers return tinyint/smallint for literals
216+ XCTAssertNotNil ( cell. anyValue, " Expected a non-null numeric value " )
217+ }
218+ }
219+
220+ func testDataTableSubscriptCaseInsensitive( ) async throws {
221+ let table = try await client. dataTable ( " SELECT 'test' AS MyColumn " )
222+ // Access using different casing
223+ let byLower = table [ 0 , " mycolumn " ]
224+ let byUpper = table [ 0 , " MYCOLUMN " ]
225+ XCTAssertEqual ( byLower. anyValue as? String , " test " )
226+ XCTAssertEqual ( byUpper. anyValue as? String , " test " )
227+ }
228+
229+ func testDataTableSubscriptByIndex( ) async throws {
230+ let table = try await client. dataTable ( " SELECT 7 AS N, 'x' AS S " )
231+ let second = table [ 0 , 1 ]
232+ XCTAssertEqual ( second. anyValue as? String , " x " )
233+ }
234+
235+ func testDataTableSubscriptOutOfBoundsReturnsNull( ) async throws {
236+ let table = try await client. dataTable ( " SELECT 1 AS A " )
237+ let oobRow = table [ 99 , " A " ]
238+ let oobCol = table [ 0 , " DoesNotExist " ]
239+ XCTAssertEqual ( oobRow, . null)
240+ XCTAssertEqual ( oobCol, . null)
241+ }
242+
243+ func testDataTableNullCell( ) async throws {
244+ let table = try await client. dataTable ( " SELECT NULL AS Val " )
245+ XCTAssertEqual ( table [ 0 , " Val " ] , . null)
246+ XCTAssertNil ( table [ 0 , " Val " ] . anyValue)
247+ }
248+
249+ func testDataTableRowAsDictionary( ) async throws {
250+ let table = try await client. dataTable ( " SELECT 5 AS ID, 'Alice' AS Name " )
251+ let dict = table. row ( at: 0 )
252+ XCTAssertEqual ( dict. count, 2 )
253+ XCTAssertNotNil ( dict [ " ID " ] )
254+ XCTAssertEqual ( dict [ " Name " ] ? . anyValue as? String , " Alice " )
255+ }
256+
257+ func testDataTableColumnValues( ) async throws {
258+ let table = try await client. dataTable (
259+ " SELECT 10 AS X UNION ALL SELECT 20 UNION ALL SELECT 30 "
260+ )
261+ let values = table. column ( named: " X " )
262+ XCTAssertEqual ( values. count, 3 )
263+ XCTAssertFalse ( values. contains ( . null) )
264+ }
265+
266+ func testDataTableNameAssignment( ) async throws {
267+ let table = try await client. dataTable ( " SELECT 1 AS A " , name: " MyTable " )
268+ XCTAssertEqual ( table. name, " MyTable " )
269+ }
270+
271+ func testDataTableStringCellValue( ) async throws {
272+ let table = try await client. dataTable ( " SELECT 'SQLClient' AS Lib " )
273+ if case . string( let s) = table [ 0 , " Lib " ] {
274+ XCTAssertEqual ( s, " SQLClient " )
275+ } else {
276+ XCTFail ( " Expected .string cell " )
277+ }
278+ }
279+
280+ func testDataTableBoolCellValue( ) async throws {
281+ let table = try await client. dataTable ( " SELECT CAST(1 AS BIT) AS Flag " )
282+ if case . bool( let b) = table [ 0 , " Flag " ] {
283+ XCTAssertTrue ( b)
284+ } else {
285+ XCTFail ( " Expected .bool cell " )
286+ }
287+ }
288+
289+ func testDataTableDateCellValue( ) async throws {
290+ let table = try await client. dataTable ( " SELECT GETDATE() AS Now " )
291+ if case . date( let d) = table [ 0 , " Now " ] {
292+ XCTAssertTrue ( d. timeIntervalSinceNow < 5 )
293+ } else {
294+ XCTFail ( " Expected .date cell " )
295+ }
296+ }
297+
298+ func testDataTableDecimalCellValue( ) async throws {
299+ let table = try await client. dataTable ( " SELECT CAST(3.14 AS DECIMAL(10,2)) AS Pi " )
300+ if case . decimal( let d) = table [ 0 , " Pi " ] {
301+ XCTAssertEqual ( d, Decimal ( string: " 3.14 " ) )
302+ } else {
303+ XCTFail ( " Expected .decimal cell " )
304+ }
305+ }
306+
307+ func testDataTableDisplayString( ) async throws {
308+ let table = try await client. dataTable ( " SELECT 'hello|world' AS Msg " )
309+ // Pipes in strings should be escaped for Markdown
310+ let display = table [ 0 , " Msg " ] . displayString
311+ XCTAssertTrue ( display. contains ( " \\ | " ) , " Pipe character should be escaped in displayString " )
312+ }
313+
314+ // MARK: - toMarkdown
315+
316+ func testDataTableToMarkdownContainsColumnNames( ) async throws {
317+ let table = try await client. dataTable ( " SELECT 1 AS ID, 'Alice' AS Name " )
318+ let md = table. toMarkdown ( )
319+ XCTAssertTrue ( md. contains ( " ID " ) )
320+ XCTAssertTrue ( md. contains ( " Name " ) )
321+ }
322+
323+ func testDataTableToMarkdownContainsValues( ) async throws {
324+ let table = try await client. dataTable ( " SELECT 42 AS Answer " )
325+ let md = table. toMarkdown ( )
326+ XCTAssertTrue ( md. contains ( " 42 " ) )
327+ }
328+
329+ func testDataTableToMarkdownHasHeaderSeparator( ) async throws {
330+ let table = try await client. dataTable ( " SELECT 1 AS A, 2 AS B " )
331+ let md = table. toMarkdown ( )
332+ // Every GFM table has a separator row with dashes
333+ XCTAssertTrue ( md. contains ( " ---| " ) , " Markdown should include a header separator row " )
334+ }
335+
336+ func testDataTableToMarkdownIncludesName( ) async throws {
337+ let table = try await client. dataTable ( " SELECT 1 AS A " , name: " Results " )
338+ let md = table. toMarkdown ( )
339+ XCTAssertTrue ( md. hasPrefix ( " # Results " ) )
340+ }
341+
342+ func testDataTableToMarkdownLineCount( ) async throws {
343+ let table = try await client. dataTable (
344+ " SELECT 1 AS N UNION ALL SELECT 2 UNION ALL SELECT 3 "
345+ )
346+ let lines = table. toMarkdown ( ) . components ( separatedBy: " \n " ) . filter { !$0. isEmpty }
347+ // header + separator + 3 data rows = 5 lines (no name prefix)
348+ XCTAssertEqual ( lines. count, 5 )
349+ }
350+
351+ // MARK: - decode<T>
352+
353+ func testDataTableDecodeDecodable( ) async throws {
354+ struct Row : Decodable {
355+ let id : Int
356+ let name : String
357+ }
358+ let table = try await client. dataTable (
359+ " SELECT 1 AS id, 'Alice' AS name UNION ALL SELECT 2, 'Bob' "
360+ )
361+ let rows : [ Row ] = try table. decode ( )
362+ XCTAssertEqual ( rows. count, 2 )
363+ XCTAssertEqual ( rows [ 0 ] . id, 1 )
364+ XCTAssertEqual ( rows [ 0 ] . name, " Alice " )
365+ XCTAssertEqual ( rows [ 1 ] . id, 2 )
366+ XCTAssertEqual ( rows [ 1 ] . name, " Bob " )
367+ }
368+
369+ func testDataTableDecodeOptionalField( ) async throws {
370+ struct Row : Decodable {
371+ let id : Int
372+ let note : String ?
373+ }
374+ let table = try await client. dataTable ( " SELECT 7 AS id, NULL AS note " )
375+ let rows : [ Row ] = try table. decode ( )
376+ XCTAssertEqual ( rows [ 0 ] . id, 7 )
377+ XCTAssertNil ( rows [ 0 ] . note)
378+ }
379+
380+ // MARK: - toSQLRows
381+
382+ func testDataTableToSQLRowsCount( ) async throws {
383+ let table = try await client. dataTable (
384+ " SELECT 1 AS A UNION ALL SELECT 2 UNION ALL SELECT 3 "
385+ )
386+ let sqlRows = table. toSQLRows ( )
387+ XCTAssertEqual ( sqlRows. count, 3 )
388+ }
389+
390+ func testDataTableToSQLRowsValues( ) async throws {
391+ let table = try await client. dataTable ( " SELECT 'hello' AS Msg " )
392+ let sqlRows = table. toSQLRows ( )
393+ XCTAssertEqual ( sqlRows [ 0 ] . string ( " Msg " ) , " hello " )
394+ }
395+
396+ // MARK: - JSON Codable
397+
398+ func testDataTableCodableRoundTrip( ) async throws {
399+ let table = try await client. dataTable (
400+ " SELECT 1 AS id, 'Alice' AS name, CAST(1 AS BIT) AS active "
401+ )
402+ let encoded = try JSONEncoder ( ) . encode ( table)
403+ let decoded = try JSONDecoder ( ) . decode ( SQLDataTable . self, from: encoded)
404+
405+ XCTAssertEqual ( decoded. rowCount, table. rowCount)
406+ XCTAssertEqual ( decoded. columnCount, table. columnCount)
407+ XCTAssertEqual ( decoded. columns [ 0 ] . name, " id " )
408+ XCTAssertEqual ( decoded. columns [ 1 ] . name, " name " )
409+ XCTAssertEqual ( decoded [ 0 , " name " ] . anyValue as? String , " Alice " )
410+ }
411+
412+ func testDataTableCodablePreservesNullCell( ) async throws {
413+ let table = try await client. dataTable ( " SELECT NULL AS Val " )
414+ let encoded = try JSONEncoder ( ) . encode ( table)
415+ let decoded = try JSONDecoder ( ) . decode ( SQLDataTable . self, from: encoded)
416+ XCTAssertEqual ( decoded [ 0 , " Val " ] , . null)
417+ }
418+
419+ // MARK: - asDataTable / asSQLDataSet on SQLClientResult
420+
421+ func testAsDataTableFromResult( ) async throws {
422+ let result = try await client. execute ( " SELECT 10 AS X, 20 AS Y " )
423+ let table = result. asDataTable ( name: " Test " )
424+ XCTAssertEqual ( table. name, " Test " )
425+ XCTAssertEqual ( table. rowCount, 1 )
426+ XCTAssertEqual ( table. columnCount, 2 )
427+ }
428+
429+ func testAsSQLDataSetFromResult( ) async throws {
430+ let result = try await client. execute ( " SELECT 1 AS A; SELECT 2 AS B; " )
431+ let ds = result. asSQLDataSet ( )
432+ XCTAssertEqual ( ds. count, 2 )
433+ XCTAssertNotNil ( ds [ 0 ] )
434+ XCTAssertNotNil ( ds [ 1 ] )
435+ }
436+
437+ // MARK: - SQLDataSet
438+
439+ func testDataSetCount( ) async throws {
440+ let ds = try await client. dataSet ( " SELECT 1 AS A; SELECT 2 AS B; SELECT 3 AS C; " )
441+ XCTAssertEqual ( ds. count, 3 )
442+ }
443+
444+ func testDataSetSubscriptByIndex( ) async throws {
445+ let ds = try await client. dataSet ( " SELECT 'first' AS V; SELECT 'second' AS V; " )
446+ XCTAssertEqual ( ds [ 0 ] ? [ 0 , " V " ] . anyValue as? String , " first " )
447+ XCTAssertEqual ( ds [ 1 ] ? [ 0 , " V " ] . anyValue as? String , " second " )
448+ }
449+
450+ func testDataSetSubscriptOutOfBoundsReturnsNil( ) async throws {
451+ let ds = try await client. dataSet ( " SELECT 1 AS A " )
452+ XCTAssertNil ( ds [ 99 ] )
453+ }
454+
455+ func testDataSetSubscriptByName( ) async throws {
456+ // Use a temp table with a named result via a stored proc isn't feasible here,
457+ // so we verify that name-based lookup works via asSQLDataSet with a named table.
458+ let result = try await client. execute ( " SELECT 42 AS Val " )
459+ var ds = result. asSQLDataSet ( )
460+ // The first table has no name by convention; rename via re-init for test purposes.
461+ // Instead, test that subscript by non-existent name returns nil gracefully.
462+ XCTAssertNil ( ds [ " NonExistent " ] )
463+ }
464+
465+ func testDataSetTablesAreAccessible( ) async throws {
466+ let ds = try await client. dataSet (
467+ " SELECT 1 AS ID, 'Alice' AS Name UNION ALL SELECT 2, 'Bob'; " +
468+ " SELECT 100 AS Score; "
469+ )
470+ let table0 = ds [ 0 ]
471+ let table1 = ds [ 1 ]
472+ XCTAssertEqual ( table0? . rowCount, 2 )
473+ XCTAssertEqual ( table1? . rowCount, 1 )
474+ XCTAssertEqual ( table1 ? [ 0 , " Score " ] . anyValue as? Int32 , 100 )
475+ }
476+
477+ // MARK: - SQLDataSet Codable
478+
479+ func testDataSetCodableRoundTrip( ) async throws {
480+ let ds = try await client. dataSet ( " SELECT 1 AS A; SELECT 'hello' AS B; " )
481+ let encoded = try JSONEncoder ( ) . encode ( ds)
482+ let decoded = try JSONDecoder ( ) . decode ( SQLDataSet . self, from: encoded)
483+ XCTAssertEqual ( decoded. count, ds. count)
484+ XCTAssertEqual ( decoded [ 0 ] ? . rowCount, ds [ 0 ] ? . rowCount)
485+ }
486+
487+ // MARK: - SQLCellValue
488+
489+ func testSQLCellValueEquality( ) {
490+ XCTAssertEqual ( SQLCellValue . null, SQLCellValue . null)
491+ XCTAssertEqual ( SQLCellValue . string ( " hi " ) , SQLCellValue . string ( " hi " ) )
492+ XCTAssertNotEqual ( SQLCellValue . string ( " hi " ) , SQLCellValue . string ( " bye " ) )
493+ XCTAssertNotEqual ( SQLCellValue . null, SQLCellValue . string ( " " ) )
494+ }
495+
496+ func testSQLCellValueAnyValueTypes( ) {
497+ XCTAssertNil ( SQLCellValue . null. anyValue)
498+ XCTAssertEqual ( SQLCellValue . string ( " x " ) . anyValue as? String , " x " )
499+ XCTAssertEqual ( SQLCellValue . int32 ( 7 ) . anyValue as? Int32 , 7 )
500+ XCTAssertEqual ( SQLCellValue . bool ( true ) . anyValue as? Bool , true )
501+ XCTAssertEqual ( SQLCellValue . double ( 3.14 ) . anyValue as? Double , 3.14 )
502+ }
503+
504+ func testSQLCellValueDisplayStringNull( ) {
505+ XCTAssertEqual ( SQLCellValue . null. displayString, " " )
506+ }
507+
508+ func testSQLCellValueDisplayStringBool( ) {
509+ XCTAssertEqual ( SQLCellValue . bool ( true ) . displayString, " true " )
510+ XCTAssertEqual ( SQLCellValue . bool ( false ) . displayString, " false " )
511+ }
512+
513+ func testSQLCellValueCodableRoundTrip( ) throws {
514+ let values : [ SQLCellValue ] = [
515+ . null,
516+ . string( " hello " ) ,
517+ . int32( 42 ) ,
518+ . int64( 9_999_999_999 ) ,
519+ . double( 3.14 ) ,
520+ . bool( true ) ,
521+ . decimal( Decimal ( string: " 123.456 " ) !) ,
522+ . uuid( UUID ( uuidString: " 550e8400-e29b-41d4-a716-446655440000 " ) !) ,
523+ ]
524+ for original in values {
525+ let data = try JSONEncoder ( ) . encode ( original)
526+ let restored = try JSONDecoder ( ) . decode ( SQLCellValue . self, from: data)
527+ XCTAssertEqual ( restored, original, " Round-trip failed for \( original) " )
528+ }
529+ }
530+
531+ func testSQLCellValueCodableDate( ) throws {
532+ let now = Date ( timeIntervalSince1970: 1_700_000_000 )
533+ let original = SQLCellValue . date ( now)
534+ let data = try JSONEncoder ( ) . encode ( original)
535+ let restored = try JSONDecoder ( ) . decode ( SQLCellValue . self, from: data)
536+ if case . date( let d) = restored {
537+ XCTAssertEqual ( d. timeIntervalSince1970, now. timeIntervalSince1970, accuracy: 0.001 )
538+ } else {
539+ XCTFail ( " Expected .date after round-trip " )
540+ }
541+ }
542+
543+ func testSQLCellValueCodableBytes( ) throws {
544+ let bytes = Data ( [ 0xDE , 0xAD , 0xBE , 0xEF ] )
545+ let original = SQLCellValue . bytes ( bytes)
546+ let data = try JSONEncoder ( ) . encode ( original)
547+ let restored = try JSONDecoder ( ) . decode ( SQLCellValue . self, from: data)
548+ if case . bytes( let b) = restored {
549+ XCTAssertEqual ( b, bytes)
550+ } else {
551+ XCTFail ( " Expected .bytes after round-trip " )
552+ }
553+ }
192554}
0 commit comments