Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/build-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ jobs:
DISPLAY_NAME="Teradata Driver"; SUMMARY="Teradata Vantage driver via a native Swift TD2 client"
DB_TYPE_IDS='["Teradata"]'; ICON="teradata-icon"; BUNDLE_NAME="TeradataDriver"
CATEGORY="database-driver"; HOMEPAGE="https://docs.tablepro.app/databases/teradata" ;;
trino)
TARGET="TrinoDriverPlugin"; BUNDLE_ID="com.TablePro.TrinoDriverPlugin"
DISPLAY_NAME="Trino Driver"; SUMMARY="Trino distributed SQL engine driver via the REST client protocol"
DB_TYPE_IDS='["Trino"]'; ICON="trino-icon"; BUNDLE_NAME="TrinoDriverPlugin"
CATEGORY="database-driver"; HOMEPAGE="https://docs.tablepro.app/databases/trino" ;;
mongodb)
TARGET="MongoDBDriver"; BUNDLE_ID="com.TablePro.MongoDBDriver"
DISPLAY_NAME="MongoDB Driver"; SUMMARY="MongoDB document database driver via libmongoc"
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Trino support through a downloadable native Swift driver. Connect over the HTTP client protocol with a username and password or a JWT token, browse catalogs, schemas, tables, materialized views, and columns, run SQL and EXPLAIN, edit rows, and create or alter tables. TLS supports Verify CA and Verify Identity with a custom CA certificate, and client certificates for mutual TLS. Large exports stream page by page. (#1906)

## [0.58.0] - 2026-07-18

### Added
Expand Down
13 changes: 12 additions & 1 deletion Packages/TableProCore/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ let package = Package(
.library(name: "TableProSync", targets: ["TableProSync"]),
.library(name: "TableProAnalytics", targets: ["TableProAnalytics"]),
.library(name: "TableProMSSQLCore", targets: ["TableProMSSQLCore"]),
.library(name: "TableProTeradataCore", targets: ["TableProTeradataCore"])
.library(name: "TableProTeradataCore", targets: ["TableProTeradataCore"]),
.library(name: "TableProTrinoCore", targets: ["TableProTrinoCore"])
],
targets: [
.target(
Expand Down Expand Up @@ -72,6 +73,11 @@ let package = Package(
dependencies: [],
path: "Sources/TableProTeradataCore"
),
.target(
name: "TableProTrinoCore",
dependencies: [],
path: "Sources/TableProTrinoCore"
),
.testTarget(
name: "TableProModelsTests",
dependencies: ["TableProModels", "TableProPluginKit"],
Expand Down Expand Up @@ -107,6 +113,11 @@ let package = Package(
dependencies: ["TableProTeradataCore"],
path: "Tests/TableProTeradataCoreTests"
),
.testTarget(
name: "TableProTrinoCoreTests",
dependencies: ["TableProTrinoCore"],
path: "Tests/TableProTrinoCoreTests"
),
.testTarget(
name: "TableProSyncTests",
dependencies: ["TableProSync", "TableProModels"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ public struct DatabaseType: Hashable, Codable, Sendable, RawRepresentable {
public static let turso = DatabaseType(rawValue: "Turso")
public static let surrealdb = DatabaseType(rawValue: "SurrealDB")
public static let teradata = DatabaseType(rawValue: "Teradata")
public static let trino = DatabaseType(rawValue: "Trino")

public static let allKnownTypes: [DatabaseType] = [
.mysql, .mariadb, .postgresql, .sqlite, .redis, .mongodb,
.clickhouse, .mssql, .oracle, .duckdb, .cassandra, .redshift,
.etcd, .cloudflareD1, .dynamodb, .bigquery, .snowflake, .libsql, .beancount,
.surrealdb, .teradata
.surrealdb, .teradata, .trino
]

/// Icon name for this database type — asset catalog name (e.g. "mysql-icon") or SF Symbol fallback
Expand All @@ -65,6 +66,7 @@ public struct DatabaseType: Hashable, Codable, Sendable, RawRepresentable {
case .beancount: return "beancount-icon"
case .surrealdb: return "surrealdb-icon"
case .teradata: return "teradata-icon"
case .trino: return "trino-icon"
default: return "externaldrive"
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import Foundation

public enum TrinoAuth: Sendable, Equatable {
case none
case basic(password: String)
case jwt(token: String)
}

public struct TrinoTLSOptions: Sendable, Equatable {
public enum VerificationMode: Sendable, Equatable {
case full
case caOnly
case insecure
}

public var mode: VerificationMode
public var caCertificatePath: String
public var clientCertificatePath: String
public var clientKeyPath: String

public init(
mode: VerificationMode = .full,
caCertificatePath: String = "",
clientCertificatePath: String = "",
clientKeyPath: String = ""
) {
self.mode = mode
self.caCertificatePath = caCertificatePath
self.clientCertificatePath = clientCertificatePath
self.clientKeyPath = clientKeyPath
}

public static let systemDefault = TrinoTLSOptions(mode: .full)
public static let insecure = TrinoTLSOptions(mode: .insecure)
}

public struct TrinoClientConfig: Sendable {
public var host: String
public var port: Int
public var useTLS: Bool
public var tls: TrinoTLSOptions
public var user: String
public var source: String
public var catalog: String?
public var schema: String?
public var timeZone: String?
public var auth: TrinoAuth
public var clientTags: [String]
public var protocolHeaders: TrinoProtocolHeaders
public var requestTimeoutSeconds: Int

public init(
host: String,
port: Int = 8_080,
useTLS: Bool = false,
tls: TrinoTLSOptions = .systemDefault,
user: String,
source: String = "TablePro",
catalog: String? = nil,
schema: String? = nil,
timeZone: String? = nil,
auth: TrinoAuth = .none,
clientTags: [String] = [],
protocolHeaders: TrinoProtocolHeaders = .trino,
requestTimeoutSeconds: Int = 60
) {
self.host = host
self.port = port
self.useTLS = useTLS
self.tls = tls
self.user = user
self.source = source
self.catalog = catalog
self.schema = schema
self.timeZone = timeZone
self.auth = auth
self.clientTags = clientTags
self.protocolHeaders = protocolHeaders
self.requestTimeoutSeconds = requestTimeoutSeconds
}

public var scheme: String { useTLS ? "https" : "http" }

public var statementURL: URL? {
var components = URLComponents()
components.scheme = scheme
components.host = host
components.port = port
components.path = "/v1/statement"
return components.url
}

public var authorizationHeader: String? {
switch auth {
case .none:
return nil
case .basic(let password):
let credentials = "\(user):\(password)"
return "Basic \(Data(credentials.utf8).base64EncodedString())"
case .jwt(let token):
return "Bearer \(token)"
}
}
}
78 changes: 78 additions & 0 deletions Packages/TableProCore/Sources/TableProTrinoCore/TrinoDDLSQL.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import Foundation

public struct TrinoColumnSpec: Sendable, Equatable {
public let name: String
public let type: String
public let nullable: Bool
public let comment: String?

public init(name: String, type: String, nullable: Bool, comment: String?) {
self.name = name
self.type = type
self.nullable = nullable
self.comment = comment
}
}

public enum TrinoDDLSQL {
public static func columnDefinition(_ column: TrinoColumnSpec) -> String {
var definition = "\(TrinoIntrospectionSQL.quoteIdentifier(column.name)) \(column.type)"
if !column.nullable {
definition += " NOT NULL"
}
if let comment = column.comment, !comment.isEmpty {
definition += " COMMENT \(TrinoIntrospectionSQL.quoteLiteral(comment))"
}
return definition
}

public static func createTable(
qualifiedTable: String,
columns: [TrinoColumnSpec],
tableComment: String?,
ifNotExists: Bool
) -> String? {
guard !columns.isEmpty else { return nil }
let existsClause = ifNotExists ? "IF NOT EXISTS " : ""
let body = columns.map(columnDefinition).joined(separator: ",\n ")
var statement = "CREATE TABLE \(existsClause)\(qualifiedTable) (\n \(body)\n)"
if let tableComment, !tableComment.isEmpty {
statement += " COMMENT \(TrinoIntrospectionSQL.quoteLiteral(tableComment))"
}
return statement
}

public static func addColumn(qualifiedTable: String, column: TrinoColumnSpec) -> String {
"ALTER TABLE \(qualifiedTable) ADD COLUMN \(columnDefinition(column))"
}

public static func dropColumn(qualifiedTable: String, name: String) -> String {
"ALTER TABLE \(qualifiedTable) DROP COLUMN \(TrinoIntrospectionSQL.quoteIdentifier(name))"
}

public static func renameColumn(qualifiedTable: String, from: String, to: String) -> String {
"ALTER TABLE \(qualifiedTable) RENAME COLUMN "
+ "\(TrinoIntrospectionSQL.quoteIdentifier(from)) TO \(TrinoIntrospectionSQL.quoteIdentifier(to))"
}

public static func setColumnType(qualifiedTable: String, name: String, type: String) -> String {
"ALTER TABLE \(qualifiedTable) ALTER COLUMN \(TrinoIntrospectionSQL.quoteIdentifier(name)) SET DATA TYPE \(type)"
}

public static func setColumnComment(qualifiedTable: String, name: String, comment: String?) -> String {
let reference = "\(qualifiedTable).\(TrinoIntrospectionSQL.quoteIdentifier(name))"
let value = comment.flatMap { $0.isEmpty ? nil : $0 }
guard let value else {
return "COMMENT ON COLUMN \(reference) IS NULL"
}
return "COMMENT ON COLUMN \(reference) IS \(TrinoIntrospectionSQL.quoteLiteral(value))"
}

public static func setTableComment(qualifiedTable: String, comment: String?) -> String {
let value = comment.flatMap { $0.isEmpty ? nil : $0 }
guard let value else {
return "COMMENT ON TABLE \(qualifiedTable) IS NULL"
}
return "COMMENT ON TABLE \(qualifiedTable) IS \(TrinoIntrospectionSQL.quoteLiteral(value))"
}
}
57 changes: 57 additions & 0 deletions Packages/TableProCore/Sources/TableProTrinoCore/TrinoError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Foundation

public struct TrinoQueryError: Decodable, Sendable, Equatable {
public let message: String
public let errorCode: Int?
public let errorName: String?
public let errorType: String?

public init(message: String, errorCode: Int? = nil, errorName: String? = nil, errorType: String? = nil) {
self.message = message
self.errorCode = errorCode
self.errorName = errorName
self.errorType = errorType
}

private enum CodingKeys: String, CodingKey {
case message, errorCode, errorName, errorType
}
}

public enum TrinoError: Error, LocalizedError, Equatable {
case invalidConfiguration(String)
case notConnected
case transport(String)
case httpStatus(code: Int, body: String)
case authenticationFailed(String)
case query(TrinoQueryError)
case invalidResponse(String)
case cancelled
case timedOut

public var errorDescription: String? {
switch self {
case .invalidConfiguration(let detail):
return detail
case .notConnected:
return "Not connected to Trino"
case .transport(let detail):
return detail
case .httpStatus(let code, let body):
return body.isEmpty ? "HTTP \(code)" : "HTTP \(code): \(body)"
case .authenticationFailed(let detail):
return detail
case .query(let error):
if let name = error.errorName, !name.isEmpty {
return "\(name): \(error.message)"
}
return error.message
case .invalidResponse(let detail):
return detail
case .cancelled:
return "Query was cancelled"
case .timedOut:
return "Timed out waiting for Trino"
}
}
}
Loading