Skip to content
Merged
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 apple/Packages/TableProCore/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,11 @@ let package = Package(
dependencies: ["TableProSpannerCore"],
path: "Tests/TableProSpannerCoreTests"
),
.testTarget(
name: "TableProTrinoCoreTests",
dependencies: ["TableProTrinoCore"],
path: "Tests/TableProTrinoCoreTests"
),
.testTarget(
name: "TableProTabularIOTests",
dependencies: ["TableProTabularIO", "TableProPluginKit"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ public struct SQLLexicalProfile: Sendable, Hashable {
let sqliteFamily = SQLLexicalProfile(grammar: sqlite)
let cqlFamily = SQLLexicalProfile(grammar: cql, undetermined: [.carriageReturnEndsLineComments])
let commandLineFamily = SQLLexicalProfile(grammar: commandLine)
let trinoFamily = SQLLexicalProfile(grammar: .ansi, undetermined: [.carriageReturnEndsLineComments])
return [
"PostgreSQL": postgreSQLFamily,
"Greenplum": postgreSQLFamily,
Expand Down Expand Up @@ -261,7 +262,8 @@ public struct SQLLexicalProfile: Sendable, Hashable {
alternatives: [spannerPostgreSQL],
undetermined: [.carriageReturnEndsLineComments]
),
"Trino": SQLLexicalProfile(grammar: .ansi, undetermined: [.carriageReturnEndsLineComments]),
"Trino": trinoFamily,
"Presto": trinoFamily,
"Teradata": SQLLexicalProfile(
grammar: .ansi,
undetermined: [.nestedBlockComments, .carriageReturnEndsLineComments]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public struct TrinoClientConfig: Sendable {
public var timeZone: String?
public var auth: TrinoAuth
public var clientTags: [String]
public var protocolHeaders: TrinoProtocolHeaders
public var flavor: TrinoServerFlavor
public var requestTimeoutSeconds: Int

public init(
Expand All @@ -58,7 +58,7 @@ public struct TrinoClientConfig: Sendable {
timeZone: String? = nil,
auth: TrinoAuth = .none,
clientTags: [String] = [],
protocolHeaders: TrinoProtocolHeaders = .trino,
flavor: TrinoServerFlavor = .trino,
requestTimeoutSeconds: Int = 60
) {
self.host = host
Expand All @@ -72,10 +72,12 @@ public struct TrinoClientConfig: Sendable {
self.timeZone = timeZone
self.auth = auth
self.clientTags = clientTags
self.protocolHeaders = protocolHeaders
self.flavor = flavor
self.requestTimeoutSeconds = requestTimeoutSeconds
}

public var protocolHeaders: TrinoProtocolHeaders { flavor.protocolHeaders }

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

public var statementURL: URL? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public enum TrinoError: Error, LocalizedError, Equatable {
case .invalidConfiguration(let detail):
return detail
case .notConnected:
return "Not connected to Trino"
return "Not connected to the coordinator"
case .transport(let detail):
return detail
case .httpStatus(let code, let body):
Expand All @@ -78,7 +78,7 @@ public enum TrinoError: Error, LocalizedError, Equatable {
case .cancelled:
return "Query was cancelled"
case .timedOut:
return "Timed out waiting for Trino"
return "Timed out waiting for the coordinator"
}
}

Expand Down Expand Up @@ -113,15 +113,15 @@ public enum TrinoError: Error, LocalizedError, Equatable {
private static func describeRedirect(statusCode: Int, location: String?, advice: TrinoRedirectAdvice) -> String {
guard let location else {
return "The server answered with HTTP \(statusCode) and no address. "
+ "Trino never redirects, so a proxy in front of it sent this. Check the host, port and SSL Mode."
+ "The coordinator never redirects, so a proxy in front of it sent this. Check the host, port and SSL Mode."
}
switch advice {
case .turnOnTLS(let port):
return "The server redirected the request to \(location), which needs HTTPS. "
+ "Set Port to \(port) and SSL Mode to Verify Identity."
case .checkAddress:
return "The server redirected the request to \(location). TablePro does not follow redirects, "
+ "and Trino never sends one, so a proxy in front of it did. Check the host, port and SSL Mode."
+ "and the coordinator never sends one, so a proxy in front of it did. Check the host, port and SSL Mode."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,32 +53,54 @@ public enum TrinoIntrospectionSQL {
"""
}

public static func tableComment(catalog: String, schema: String, table: String) -> String {
"""
SELECT comment FROM system.metadata.table_comments \
WHERE catalog_name = \(quoteLiteral(catalog)) AND schema_name = \(quoteLiteral(schema)) \
AND table_name = \(quoteLiteral(table))
"""
}

public static func listMaterializedViews(catalog: String, schema: String) -> String {
"""
SELECT name FROM system.metadata.materialized_views \
WHERE catalog_name = \(quoteLiteral(catalog)) AND schema_name = \(quoteLiteral(schema))
"""
// Presto has no `system.metadata.table_comments` and no other table that carries the comment.
public static func tableComment(
catalog: String,
schema: String,
table: String,
flavor: TrinoServerFlavor
) -> String? {
guard flavor == .trino else { return nil }
return """
SELECT comment FROM system.metadata.table_comments \
WHERE catalog_name = \(quoteLiteral(catalog)) AND schema_name = \(quoteLiteral(schema)) \
AND table_name = \(quoteLiteral(table))
"""
}

public static func listMaterializedViews(catalog: String, schema: String, flavor: TrinoServerFlavor) -> String {
switch flavor {
case .trino:
return """
SELECT name FROM system.metadata.materialized_views \
WHERE catalog_name = \(quoteLiteral(catalog)) AND schema_name = \(quoteLiteral(schema))
"""
case .presto:
return """
SELECT table_name FROM \(quoteIdentifier(catalog)).information_schema.materialized_views \
WHERE table_schema = \(quoteLiteral(schema))
"""
}
}

/// One relation's `table_type`, which says `VIEW` for a view. A materialized view is not told
/// apart here: ``materializedView(catalog:schema:name:)`` answers for it.
/// apart here: ``materializedView(catalog:schema:name:flavor:)`` answers for it.
public static func relationType(catalog: String, schema: String, table: String) -> String {
"""
SELECT table_type FROM \(quoteIdentifier(catalog)).information_schema.tables \
WHERE table_schema = \(quoteLiteral(schema)) AND table_name = \(quoteLiteral(table))
"""
}

public static func materializedView(catalog: String, schema: String, name: String) -> String {
listMaterializedViews(catalog: catalog, schema: schema) + " AND name = \(quoteLiteral(name))"
public static func materializedView(
catalog: String,
schema: String,
name: String,
flavor: TrinoServerFlavor
) -> String {
let nameColumn = flavor == .trino ? "name" : "table_name"
return listMaterializedViews(catalog: catalog, schema: schema, flavor: flavor)
+ " AND \(nameColumn) = \(quoteLiteral(name))"
}

public static func approximateRowCount(catalog: String?, schema: String?, table: String) -> String {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation

public enum TrinoServerFlavor: String, Sendable, Equatable, CaseIterable {
case trino = "Trino"
case presto = "Presto"

public init(driverVariant: String?) {
self = driverVariant == TrinoServerFlavor.presto.rawValue ? .presto : .trino
}

public var productName: String { rawValue }

public var protocolHeaders: TrinoProtocolHeaders {
switch self {
case .trino: .trino
case .presto: .presto
}
}

// Presto has no version() (measured on 0.299), so it connects on a statement any user can run.
public var connectStatement: String {
switch self {
case .trino: "SELECT version()"
case .presto: "SELECT 1"
}
}

public var versionStatement: String {
switch self {
case .trino: "SELECT version()"
case .presto: "SELECT node_version FROM system.runtime.nodes WHERE coordinator = true"
}
}

// Presto has no client capabilities header (PrestoHeaders, 0.299).
public var clientCapabilities: String? {
switch self {
case .trino: "PARAMETRIC_DATETIME"
case .presto: nil
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public final class TrinoStatementClient: @unchecked Sendable {
onPage: ([[TrinoValue]]) -> Void
) async throws -> StatementOutcome {
guard let statementURL = config.statementURL else {
throw TrinoError.invalidConfiguration("Invalid Trino server URL")
throw TrinoError.invalidConfiguration("Invalid \(config.flavor.productName) server URL")
}
if let credential = config.plaintextCredential {
throw TrinoError.credentialsRequireTLS(credential)
Expand Down Expand Up @@ -263,7 +263,7 @@ public final class TrinoStatementClient: @unchecked Sendable {
do {
return try JSONDecoder().decode(TrinoQueryResults.self, from: response.body)
} catch {
throw TrinoError.invalidResponse("Could not decode the Trino response")
throw TrinoError.invalidResponse("Could not decode the \(config.flavor.productName) response")
}
}

Expand Down Expand Up @@ -322,7 +322,9 @@ public final class TrinoStatementClient: @unchecked Sendable {
if !config.clientTags.isEmpty {
headers[protocolHeaders.clientTags] = config.clientTags.joined(separator: ",")
}
headers[protocolHeaders.clientCapabilities] = "PARAMETRIC_DATETIME"
if let capabilities = config.flavor.clientCapabilities {
headers[protocolHeaders.clientCapabilities] = capabilities
}
headers["Content-Type"] = "text/plain; charset=utf-8"
if let authorization = config.authorizationHeader {
headers["Authorization"] = authorization
Expand All @@ -340,11 +342,11 @@ public final class TrinoStatementClient: @unchecked Sendable {

private func followURL(_ uri: String) throws -> URL {
guard let url = URL(string: uri) else {
throw TrinoError.invalidResponse("Trino returned an invalid nextUri")
throw TrinoError.invalidResponse("\(config.flavor.productName) returned an invalid nextUri")
}
guard config.useTLS, url.scheme?.lowercased() != "https" else { return url }
throw TrinoError.invalidResponse(
"Trino answered an HTTPS request with a plain http:// address, so it was not followed. "
"\(config.flavor.productName) answered an HTTPS request with a plain http:// address, so it was not followed. "
+ "A coordinator behind a TLS proxy needs http-server.process-forwarded=true."
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ public enum TrinoTypeCategory: Sendable, Equatable {
}

public enum TrinoTypeMapper {
// Presto also sends `KHyperLogLog` and `kllsketch` as base64.
static let binaryTypes: Set<String> = [
"varbinary", "hyperloglog", "p4hyperloglog", "qdigest", "tdigest", "setdigest"
"varbinary", "hyperloglog", "p4hyperloglog", "khyperloglog", "kllsketch", "qdigest", "tdigest", "setdigest"
]

static let structuredTypes: Set<String> = ["array", "map", "row"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,21 @@ public enum TrinoValueDecoder {
}
return .text(json.scalarText ?? "")
case .structured:
return .text(json.jsonText())
return .text(structuredText(json))
}
}

// Presto 0.299 sends array, map and row values as their JSON text rather than as JSON.
private static func structuredText(_ json: TrinoJSONValue) -> String {
guard case .string(let text) = json,
let parsed = try? JSONDecoder().decode(TrinoJSONValue.self, from: Data(text.utf8)) else {
return json.jsonText()
}
switch parsed {
case .array, .object:
return parsed.jsonText()
default:
return json.jsonText()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ struct SQLLexicalReadingsTests {
let shipped = [
"MySQL", "MariaDB", "TiDB", "Databend", "OceanBase", "PostgreSQL", "Redshift", "CockroachDB", "PGlite",
"SQLite", "libSQL", "Turso", "Cloudflare D1", "DuckDB", "Oracle", "Dameng", "SQL Server", "ClickHouse",
"Snowflake", "BigQuery", "Spanner", "Trino", "Teradata", "SAP HANA", "Cassandra", "ScyllaDB", "DynamoDB",
"Snowflake", "BigQuery", "Spanner", "Trino", "Presto", "Teradata", "SAP HANA", "Cassandra", "ScyllaDB", "DynamoDB",
"SurrealDB", "Redis", "MongoDB", "etcd", "Elasticsearch", "Typesense", "Weaviate", "Kafka",
"Firestore", "Firebase Realtime Database", "Bigtable",
]
Expand Down
Loading
Loading