Skip to content

Commit 96c4059

Browse files
authored
Add functions to retrieve C* column names (#71)
Provide a way to retrieve C* column names from a query result ### Motivation: - The underlying DataStax C++ driver already exposes column names via cass_result_column_name() - The Swift library already supports accessing columns by name via row.column(_ name: String) - However, there's no way to discover what column names are available in a result set - This is essential for building dynamic query tools, debug utilities, or any application that needs to work with unknown schemas at compile time ### Modifications: 2 new functions: - columnName(at index: Int) -> String? - Returns the column name for a given column index - columnNames() -> [String] - Returns an array of all column names in the result set ### Result: Use case example: ``` let rows = try await session.query("SELECT * FROM system_schema.tables", on: eventLoop) let columnNames = rows.columnNames() // ["keyspace_name", "table_name", "id", ...] for row in rows { for (index, columnName) in columnNames.enumerated() { if let column = row.column(index) { print("\(columnName): \(column.string ?? "<null>")") } } } ```
1 parent 08b45f6 commit 96c4059

File tree

2 files changed

+108
-0
lines changed

2 files changed

+108
-0
lines changed

Sources/CassandraClient/Data.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,36 @@ extension CassandraClient {
4444
cass_result_column_count(self.rawPointer)
4545
}
4646

47+
/// Get column name by index
48+
/// - Parameter index: The column index (0-based)
49+
/// - Returns: The column name
50+
/// - Throws: CassandraClient.Error if index is out of bounds or column name cannot be retrieved
51+
public func columnName(at index: Int) throws -> String {
52+
guard index >= 0 && index < self.columnsCount else {
53+
throw CassandraClient.Error(CASS_ERROR_LIB_INDEX_OUT_OF_BOUNDS)
54+
}
55+
56+
var namePtr: UnsafePointer<CChar>?
57+
var nameLength: Int = 0
58+
59+
let result = cass_result_column_name(self.rawPointer, index, &namePtr, &nameLength)
60+
guard result == CASS_OK, let name = namePtr else {
61+
throw CassandraClient.Error(result)
62+
}
63+
64+
let nameBuffer = UnsafeBufferPointer(start: name, count: nameLength)
65+
return nameBuffer.withMemoryRebound(to: UInt8.self) {
66+
String(decoding: $0, as: UTF8.self)
67+
}
68+
}
69+
70+
/// Get all column names
71+
/// - Returns: Array of column names
72+
/// - Throws: CassandraClient.Error if any column name cannot be retrieved
73+
public func columnNames() throws -> [String] {
74+
try (0..<self.columnsCount).map { try columnName(at: $0) }
75+
}
76+
4777
public func makeIterator() -> Iterator {
4878
Iterator(rows: self)
4979
}

Tests/CassandraClientTests/CassandraClientTests.swift

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,6 +1118,84 @@ final class Tests: XCTestCase {
11181118
}
11191119
}
11201120

1121+
func testColumnName() throws {
1122+
let tableName = "test_\(DispatchTime.now().uptimeNanoseconds)"
1123+
try self.cassandraClient.run(
1124+
"create table \(tableName) (id int primary key, name text, age int, email text);"
1125+
).wait()
1126+
1127+
try self.cassandraClient.run(
1128+
"insert into \(tableName) (id, name, age, email) values (1, 'Alice', 30, 'alice@example.com');"
1129+
).wait()
1130+
1131+
let rows = try self.cassandraClient.query("select id, name, age, email from \(tableName);").wait()
1132+
XCTAssertEqual(rows.count, 1, "expected exactly one row")
1133+
1134+
// Test valid column indices
1135+
XCTAssertEqual(try rows.columnName(at: 0), "id", "first column should be 'id'")
1136+
XCTAssertEqual(try rows.columnName(at: 1), "name", "second column should be 'name'")
1137+
XCTAssertEqual(try rows.columnName(at: 2), "age", "third column should be 'age'")
1138+
XCTAssertEqual(try rows.columnName(at: 3), "email", "fourth column should be 'email'")
1139+
1140+
// Test invalid indices - should throw errors
1141+
XCTAssertThrowsError(try rows.columnName(at: -1), "negative index should throw error")
1142+
XCTAssertThrowsError(try rows.columnName(at: 4), "out of bounds index should throw error")
1143+
XCTAssertThrowsError(try rows.columnName(at: 100), "large out of bounds index should throw error")
1144+
1145+
// Test with select *
1146+
let rowsStar = try self.cassandraClient.query("select * from \(tableName);").wait()
1147+
XCTAssertNoThrow(try rowsStar.columnName(at: 0), "select * should return valid column names")
1148+
XCTAssertEqual(rowsStar.columnsCount, 4, "select * should return all 4 columns")
1149+
}
1150+
1151+
func testColumnNames() throws {
1152+
let tableName = "test_\(DispatchTime.now().uptimeNanoseconds)"
1153+
try self.cassandraClient.run(
1154+
"create table \(tableName) (id int primary key, username text, score bigint, active boolean);"
1155+
).wait()
1156+
1157+
try self.cassandraClient.run(
1158+
"insert into \(tableName) (id, username, score, active) values (1, 'Bob', 9500, true);"
1159+
).wait()
1160+
1161+
// Test columnNames with explicit column selection
1162+
let rows = try self.cassandraClient.query("select id, username, score, active from \(tableName);").wait()
1163+
let columnNames = try rows.columnNames()
1164+
1165+
XCTAssertEqual(columnNames.count, 4, "should return 4 column names")
1166+
XCTAssertEqual(columnNames[0], "id", "first column name should be 'id'")
1167+
XCTAssertEqual(columnNames[1], "username", "second column name should be 'username'")
1168+
XCTAssertEqual(columnNames[2], "score", "third column name should be 'score'")
1169+
XCTAssertEqual(columnNames[3], "active", "fourth column name should be 'active'")
1170+
1171+
// Test columnNames with select *
1172+
let rowsStar = try self.cassandraClient.query("select * from \(tableName);").wait()
1173+
let columnNamesStar = try rowsStar.columnNames()
1174+
XCTAssertEqual(columnNamesStar.count, 4, "select * should return all 4 column names")
1175+
1176+
// Verify column names array matches individual columnName calls
1177+
for (index, name) in columnNames.enumerated() {
1178+
XCTAssertEqual(
1179+
try rows.columnName(at: index),
1180+
name,
1181+
"columnNames array should match columnName at index \(index)"
1182+
)
1183+
}
1184+
1185+
// Test columnNames with partial column selection
1186+
let rowsPartial = try self.cassandraClient.query("select username, active from \(tableName);").wait()
1187+
let columnNamesPartial = try rowsPartial.columnNames()
1188+
XCTAssertEqual(columnNamesPartial.count, 2, "partial select should return 2 column names")
1189+
XCTAssertEqual(columnNamesPartial[0], "username", "first column should be 'username'")
1190+
XCTAssertEqual(columnNamesPartial[1], "active", "second column should be 'active'")
1191+
1192+
// Test columnNames with single column
1193+
let rowsSingle = try self.cassandraClient.query("select id from \(tableName);").wait()
1194+
let columnNamesSingle = try rowsSingle.columnNames()
1195+
XCTAssertEqual(columnNamesSingle.count, 1, "single column select should return 1 column name")
1196+
XCTAssertEqual(columnNamesSingle[0], "id", "single column should be 'id'")
1197+
}
1198+
11211199
func testErrorMapping() {
11221200
XCTAssertThrowsError(try self.cassandraClient.run("boom!").wait()) { error in
11231201
XCTAssertEqual(

0 commit comments

Comments
 (0)