@@ -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 )
0 commit comments