Skip to content

feat(plugin-trino): add Trino driver over the REST client protocol#1910

Open
datlechin wants to merge 6 commits into
mainfrom
feat/trino-driver
Open

feat(plugin-trino): add Trino driver over the REST client protocol#1910
datlechin wants to merge 6 commits into
mainfrom
feat/trino-driver

Conversation

@datlechin

@datlechin datlechin commented Jul 18, 2026

Copy link
Copy Markdown
Member

Adds Trino as a registry-only, pure-Swift driver plugin. Part of #1906.

Trino speaks a documented HTTP client protocol (POST /v1/statement then follow nextUri until it is gone), so the driver needs no C bridge. It is split into a tested core (TableProTrinoCore) and a thin plugin adapter, matching the existing HTTP drivers.

What's included

  • Wire client (TableProTrinoCore): the statement lifecycle (POST, poll nextUri, drain), session state threaded from the X-Trino-Set-* response headers, precision-safe value decoding, transient-error backoff (502/503/504 and 429), and cancel via DELETE. A quote and comment aware statement splitter runs multi-statement DDL correctly (Trino accepts one statement per request).
  • Browsing: catalog to schema to table, sourced from SHOW CATALOGS, SHOW SCHEMAS, and each catalog's information_schema. Materialized views appear in the tree, table row counts come from SHOW STATS, and table and column comments come from information_schema and system.metadata.
  • Editing rows: INSERT/UPDATE/DELETE from the grid, keyed on the primary key or the row's other values, with Trino-typed literals (a varchar holding digits stays quoted, a number stays unquoted) and array/map/row columns left out of the WHERE.
  • Editing schema: create tables, and add, drop, rename, and retype columns. Runs in autocommit, matching how most Trino connectors handle DDL.
  • Streaming export: exports stream page by page over nextUri instead of buffering the whole result set.
  • Auth: Username and Password (HTTP Basic), JWT access token, and client certificate (mutual TLS).
  • TLS: Required, Verify CA, and Verify Identity modes with a custom CA certificate.
  • Editor: EXPLAIN variants (Logical, Distributed, IO, Validate, Analyze), a Trino SQL dialect, and a session time zone.
  • Wiring: DatabaseType.trino, the plugin build target, the registry defaults, the brand icon, build-plugin.yml, release-all-plugins.sh, docs, and CHANGELOG. A small shared ColumnTypeClassifier change shows array/map/row as JSON (also improves ClickHouse and DuckDB nested types).

Deliberately deferred, with reasons

  • Transactions: the schema-apply path wraps statements in a transaction when a driver reports supportsTransactions, but Trino DDL is autocommit-only on most connectors, so enabling it would break DDL. Trino runs in autocommit.
  • Query progress: the supportsQueryProgress flag has no working producer anywhere in the app (the ClickHouse path is dead scaffolding). Doing it right means building app-wide progress plumbing, a separate change.
  • Roles, Kerberos, OAuth 2.0: each is a sizable, deployment-specific effort. X-Trino-Role is catalog scoped, Kerberos needs SPNEGO, and OAuth 2.0 needs the external browser flow. Left for follow-ups.

Testing

  • 85 unit tests in TableProTrinoCoreTests cover the poll loop, the bigint precision trap, session threading, retry, error surfacing, the statement splitter, typed literals, DDL and row-edit SQL, streaming, and introspection SQL. swift test passes.
  • TrinoDriverPlugin and the full TablePro app both build. SwiftLint is clean on the new code.

@mintlify

mintlify Bot commented Jul 18, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
TablePro 🟢 Ready View Preview Jul 18, 2026, 2:40 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a940f66d7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

let initialRequest = makeRequest(method: .post, url: statementURL, headers: initialHeaders(), body: Data(sql.utf8))
var httpResponse = try await sendWithRetry(initialRequest)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid retrying non-idempotent statement POSTs

sendWithRetry retries 502/503/504/429, and using it for the initial POST /v1/statement can resubmit the same SQL after a gateway or proxy returns a transient error even though the coordinator already accepted the statement. For INSERT/DDL or connector side-effecting statements this can execute twice; keep retries for nextUri GETs or require an idempotency mechanism before retrying the POST.

Useful? React with 👍 / 👎.

static let supportsDropIndex = false
static let supportsModifyPrimaryKey = false
static let structureColumnFields: [StructureColumnField] = [.name, .type, .nullable, .defaultValue]
static let postConnectActions: [PostConnectAction] = [.selectSchemaFromLastSession]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore the last selected Trino catalog

With Trino the catalog is the database dimension: switchDatabase(to:) stores it in TrinoSessionState, and the app persists that selection as the last database. On reconnect, DatabaseManager+Sessions.executePostConnectActions restores the saved database only when .selectDatabaseFromLastSession is present; this metadata restores only the schema, so connections whose catalog was selected in the UI come back with no catalog and schema/table browsing stays empty until the user reselects a catalog. Include the database action before the schema action, and mirror it in the registry default.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5e284fe66a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +81 to +82
if upper.hasPrefix("TIME") {
return .timestamp(rawType: rawTypeName)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Narrow TIME fallback so timeuuid stays textual

For Cassandra timeuuid columns, the classifier reaches this fallback with upper == "TIMEUUID" because that raw type is not in typeLookup; this new broad prefix check now classifies UUID strings as timestamps, giving them date/time formatting and filtering instead of textual/UUID behavior. Restrict this branch to actual Trino time spellings such as TIME WITH TIME ZONE rather than every type beginning with TIME.

Useful? React with 👍 / 👎.

public func executeStreamed(_ sql: String) -> AsyncThrowingStream<TrinoStreamElement, Error> {
AsyncThrowingStream { continuation in
let client = self
Task {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cancel streamed Trino statements when consumers stop

When a streaming export or other caller stops consuming the returned AsyncThrowingStream, this unstructured producer task is not tied to continuation.onTermination, so it can keep polling nextUri and leave the Trino query running on the cluster after the UI/export was cancelled. Store the task and cancel it from the termination handler so runStatement observes cancellation and deletes the current URI.

Useful? React with 👍 / 👎.

Comment on lines +19 to +20
func columnTypeKey(schema: String?, table: String) -> String {
"\(resolveSchema(schema) ?? "").\(table)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include the catalog in column type cache keys

After switching catalogs, two tables with the same schema and table name share this cache entry even though Trino catalogs are independent. Since fetchColumns populates the cached types and generateStatements uses them to decide whether edited values should be quoted or cast, editing schema.table in a second catalog can reuse stale types from the first catalog; include currentCatalog in the key or clear this cache on catalog switch.

Useful? React with 👍 / 👎.

Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50d5cbef3d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +169 to +180
let useTLS = config.ssl.isEnabled
let port = config.port > 0 ? config.port : (useTLS ? 8_443 : 8_080)
return TrinoClientConfig(
host: config.host.isEmpty ? "localhost" : config.host,
port: port,
useTLS: useTLS,
tls: TrinoSSLMapping.tlsOptions(for: config.ssl),
user: config.username,
catalog: config.database.isEmpty ? nil : config.database,
schema: trimmedField(config.additionalFields["trinoSchema"]),
timeZone: trimmedField(config.additionalFields["trinoTimeZone"]),
auth: resolveAuth(config)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject authenticated Trino connections without TLS

When a user leaves SSL disabled (the registry default) and uses the default password authentication or JWT option, this constructs an HTTP client configuration while resolveAuth still supplies an Authorization header. The resulting Basic password or bearer token is transmitted in cleartext to the coordinator and any network intermediary; reject this configuration or require TLS before creating the client.

Useful? React with 👍 / 👎.

Comment on lines +47 to +49
func switchDatabase(to database: String) async throws {
session.setCatalog(database)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Clear the schema when switching catalogs

Switching catalogs leaves session.schema set to the schema selected in the prior catalog. Every later statement POST includes both the new catalog and this stale schema, so if that schema does not exist in the new catalog Trino rejects even SHOW SCHEMAS/sidebar requests before the user can select a replacement. Clear the schema here (or validate it against the target catalog) when the catalog changes.

Useful? React with 👍 / 👎.

Comment on lines +24 to +26
if oldColumn.dataType != newColumn.dataType {
statements.append(TrinoDDLSQL.setColumnType(qualifiedTable: target, name: newColumn.name, type: newColumn.dataType))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Generate SQL for nullability-only column edits

A Structure-tab edit that changes only isNullable produces no statement because this method only checks the name, data type, and comment. Since the plugin exposes the nullable field and emits NOT NULL when creating columns, such edits are silently discarded; generate the connector-supported SET NOT NULL or DROP NOT NULL alteration when the flag changes.

Useful? React with 👍 / 👎.

Comment on lines +46 to +52
private func columnSpec(_ column: PluginColumnDefinition) -> TrinoColumnSpec {
TrinoColumnSpec(
name: column.name,
type: column.dataType,
nullable: column.isNullable,
comment: normalizedComment(column.comment)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve entered column default values in DDL

The plugin advertises a Default Value field, but columnSpec drops PluginColumnDefinition.defaultValue, and TrinoColumnSpec has no place to carry it. Consequently, creating a table or adding a column with a default silently creates a column without that default; include it in the column definition (and handle default changes in modify-column SQL).

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant