Skip to content

Commit 2e0d764

Browse files
committed
fix: improve Cassandra plugin SSL, type extraction, column ordering, and UX
1 parent a339dda commit 2e0d764

2 files changed

Lines changed: 91 additions & 19 deletions

File tree

Plugins/CassandraDriverPlugin/CassandraPlugin.swift

Lines changed: 88 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import TableProPluginKit
1515

1616
// MARK: - Plugin Entry Point
1717

18-
final class CassandraPlugin: NSObject, TableProPlugin, DriverPlugin {
18+
internal final class CassandraPlugin: NSObject, TableProPlugin, DriverPlugin {
1919
static let pluginName = "Cassandra Driver"
2020
static let pluginVersion = "1.0.0"
2121
static let pluginDescription = "Apache Cassandra and ScyllaDB support via DataStax C driver"
@@ -25,7 +25,14 @@ final class CassandraPlugin: NSObject, TableProPlugin, DriverPlugin {
2525
static let databaseDisplayName = "Cassandra / ScyllaDB"
2626
static let iconName = "cassandra-icon"
2727
static let defaultPort = 9042
28-
static let additionalConnectionFields: [ConnectionField] = []
28+
static let additionalConnectionFields: [ConnectionField] = [
29+
ConnectionField(
30+
id: "sslCaCertPath",
31+
label: "CA Certificate",
32+
placeholder: "/path/to/ca-cert.pem",
33+
section: .advanced
34+
),
35+
]
2936
static let additionalDatabaseTypeIds: [String] = []
3037

3138
func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver {
@@ -44,6 +51,13 @@ private actor CassandraConnectionActor {
4451
return f
4552
}()
4653

54+
nonisolated(unsafe) private static let dateFormatter: DateFormatter = {
55+
let f = DateFormatter()
56+
f.dateFormat = "yyyy-MM-dd"
57+
f.timeZone = TimeZone(identifier: "UTC")
58+
return f
59+
}()
60+
4761
private var cluster: OpaquePointer? // CassCluster*
4862
private var session: OpaquePointer? // CassSession*
4963
private var currentKeyspace: String?
@@ -82,7 +96,12 @@ private actor CassandraConnectionActor {
8296
}
8397

8498
if sslMode == "Verify CA" || sslMode == "Verify Identity" {
85-
cass_ssl_set_verify_flags(ssl, Int32(CASS_SSL_VERIFY_PEER_CERT.rawValue))
99+
if sslMode == "Verify Identity" {
100+
let flags = Int32(CASS_SSL_VERIFY_PEER_CERT.rawValue | CASS_SSL_VERIFY_PEER_IDENTITY.rawValue)
101+
cass_ssl_set_verify_flags(ssl, flags)
102+
} else {
103+
cass_ssl_set_verify_flags(ssl, Int32(CASS_SSL_VERIFY_PEER_CERT.rawValue))
104+
}
86105

87106
if let caCertPath = sslCaCertPath, !caCertPath.isEmpty,
88107
let certData = FileManager.default.contents(atPath: caCertPath),
@@ -482,6 +501,44 @@ private actor CassandraConnectionActor {
482501
case CASS_VALUE_TYPE_TUPLE:
483502
return extractCollectionString(value, open: "(", close: ")")
484503

504+
case CASS_VALUE_TYPE_DATE:
505+
var dateVal: UInt32 = 0
506+
if cass_value_get_uint32(value, &dateVal) == CASS_OK {
507+
let daysSinceEpoch = Int64(dateVal) - Int64(1 << 31)
508+
let epochSeconds = daysSinceEpoch * 86400
509+
let date = Date(timeIntervalSince1970: Double(epochSeconds))
510+
return dateFormatter.string(from: date)
511+
}
512+
return nil
513+
514+
case CASS_VALUE_TYPE_TIME:
515+
var timeVal: Int64 = 0
516+
if cass_value_get_int64(value, &timeVal) == CASS_OK {
517+
// Cassandra time is nanoseconds since midnight
518+
let totalSeconds = timeVal / 1_000_000_000
519+
let hours = totalSeconds / 3600
520+
let minutes = (totalSeconds % 3600) / 60
521+
let seconds = totalSeconds % 60
522+
let nanos = timeVal % 1_000_000_000
523+
if nanos > 0 {
524+
let millis = nanos / 1_000_000
525+
return String(format: "%02lld:%02lld:%02lld.%03lld", hours, minutes, seconds, millis)
526+
}
527+
return String(format: "%02lld:%02lld:%02lld", hours, minutes, seconds)
528+
}
529+
return nil
530+
531+
case CASS_VALUE_TYPE_DECIMAL, CASS_VALUE_TYPE_VARINT:
532+
// Read as bytes and display as hex since proper numeric decoding
533+
// requires BigInteger support not available in the C driver API
534+
var bytes: UnsafePointer<UInt8>?
535+
var length: Int = 0
536+
if cass_value_get_bytes(value, &bytes, &length) == CASS_OK, let bytes {
537+
let data = Data(bytes: bytes, count: length)
538+
return "0x" + data.map { String(format: "%02x", $0) }.joined()
539+
}
540+
return nil
541+
485542
default:
486543
// Fallback: try reading as string
487544
var output: UnsafePointer<CChar>?
@@ -597,7 +654,7 @@ private struct CassandraRawResult: Sendable {
597654

598655
// MARK: - Plugin Driver
599656

600-
final class CassandraPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
657+
internal final class CassandraPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
601658
private let config: DriverConnectionConfig
602659
private let connectionActor = CassandraConnectionActor()
603660
private let stateLock = NSLock()
@@ -770,27 +827,39 @@ final class CassandraPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
770827
"""
771828
let result = try await execute(query: query)
772829

773-
return result.rows.compactMap { row in
830+
// Parse and sort by kind order then position before mapping to PluginColumnInfo
831+
struct RawColumn {
832+
let name: String
833+
let dataType: String
834+
let kind: String
835+
let position: Int
836+
let isPrimaryKey: Bool
837+
}
838+
839+
let rawColumns = result.rows.compactMap { row -> RawColumn? in
774840
guard let name = row[safe: 0] ?? nil,
775841
let dataType = row[safe: 1] ?? nil else {
776842
return nil
777843
}
778-
let kind = row[safe: 2] ?? nil // partition_key, clustering, regular, static
844+
let kind = (row[safe: 2] ?? nil) ?? "regular"
845+
let position = Int((row[safe: 4] ?? nil) ?? "0") ?? 0
779846
let isPrimaryKey = kind == "partition_key" || kind == "clustering"
847+
return RawColumn(name: name, dataType: dataType, kind: kind, position: position, isPrimaryKey: isPrimaryKey)
848+
}.sorted { lhs, rhs in
849+
let lhsOrder = columnKindOrder(lhs.kind)
850+
let rhsOrder = columnKindOrder(rhs.kind)
851+
if lhsOrder != rhsOrder { return lhsOrder < rhsOrder }
852+
return lhs.position < rhs.position
853+
}
780854

781-
return PluginColumnInfo(
782-
name: name,
783-
dataType: dataType,
784-
isNullable: !isPrimaryKey,
785-
isPrimaryKey: isPrimaryKey,
855+
return rawColumns.map { col in
856+
PluginColumnInfo(
857+
name: col.name,
858+
dataType: col.dataType,
859+
isNullable: !col.isPrimaryKey,
860+
isPrimaryKey: col.isPrimaryKey,
786861
defaultValue: nil
787862
)
788-
}.sorted { lhs, rhs in
789-
// Sort: partition keys first, then clustering, then regular
790-
let lhsOrder = columnKindOrder(lhs.isPrimaryKey ? "key" : "regular")
791-
let rhsOrder = columnKindOrder(rhs.isPrimaryKey ? "key" : "regular")
792-
if lhsOrder != rhsOrder { return lhsOrder < rhsOrder }
793-
return lhs.name < rhs.name
794863
}
795864
}
796865

@@ -976,7 +1045,7 @@ final class CassandraPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
9761045
let safeKs = escapeIdentifier(name)
9771046
let query = """
9781047
CREATE KEYSPACE "\(safeKs)"
979-
WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}
1048+
WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3}
9801049
"""
9811050
_ = try await execute(query: query)
9821051
}
@@ -1036,7 +1105,7 @@ final class CassandraPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
10361105

10371106
// MARK: - Errors
10381107

1039-
enum CassandraPluginError: Error {
1108+
internal enum CassandraPluginError: Error {
10401109
case connectionFailed(String)
10411110
case notConnected
10421111
case queryFailed(String)

TablePro/Views/Main/MainContentCoordinator.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,9 @@ final class MainContentCoordinator {
710710

711711
// Cassandra/ScyllaDB don't support EXPLAIN
712712
if connection.type == .cassandra || connection.type == .scylladb {
713+
if let index = tabManager.selectedTabIndex {
714+
tabManager.tabs[index].errorMessage = String(localized: "EXPLAIN is not supported for this database type.")
715+
}
713716
return
714717
}
715718

0 commit comments

Comments
 (0)