Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### 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)
- SSH tunnels can authenticate with no password or key, for hosts that handle SSH auth themselves like Tailscale SSH. Pick **None** as the SSH auth method. (#1907)

## [0.58.0] - 2026-07-18
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
Loading