ADR 0026: Trino as a native connection type¶
- Status: Accepted (2026-10-19)
- Backlog: roadmap item #76 (Trino / Presto as a native connection type). Launches
trinoexperimental (#64), to graduate against the connection-type criteria (databases.GRADUATION, #66). Implemented, with fully-qualifiedallowed_tables(§2), tested against a real Trino in CI, and reviewed for security (THREAT_MODEL.md, "Trino"); a release of stable shape and real-deployment use remain before it graduates.
Context¶
Trino (and its ancestor Presto) is a distributed SQL engine that federates many catalogs behind one endpoint - Hive, Iceberg, Delta, Kafka, object stores, other databases. One Trino connection could turn every one of those into a source QueryAPIGate publishes as a governed REST and MCP tool, through a single well-understood engine.
Today Trino is reachable only through the generic JDBC connection type, which is deliberately minimal: no query
time limit, no schema browser, and allowed_tables and masking off (JDBC can't be traced by dialect). That is the
worst of both worlds for a federation engine - broad reach, little governance.
A native type can do better, and the pieces exist:
- The codebase has a clean "add a dialect" shape: a
runners._Driversubclass plus entries in a handful of per-dialect registries (RUNNERS,schema._QUERIES,tableguard._DIALECT_MAP,masking._DIALECT_MAP, …). - sqlglot ships a
trinodialect, so the table guard, column masking and the flow diagram work by mappingtrino → 'trino', no new parser. - The
trinoPython client (DB-API) supports parameter binding,information_schemaper catalog, and cancellation of a running query.
Three things about Trino make it different from the single-database types already supported, and each needs a
decision: its names are three-part (catalog.schema.table); it has no read-only session mode; and it has
no server-enforced timeout unless we ask for one.
Decision¶
Add trino as a native, experimental connection type, mirroring how H2/JDBC and (before it graduated) MongoDB
were introduced.
1. The connection is a Trino endpoint; database means catalog¶
A trino connection carries host, port, user, auth and http_scheme, plus a catalog (mapped to the generic
database field) and an optional default schema. "Switch database" in the Console switches catalog
(LIST_DATABASES_QUERIES['trino'] = 'SHOW CATALOGS'). The schema browser reads the catalog's own
information_schema.
The connection reaches whatever that Trino user is allowed to reach. We treat Trino's own access control as the outer boundary and recommend a read-only, catalog/schema-scoped Trino user per connection - QueryAPIGate's grants then narrow within that.
2. allowed_tables is matched fully qualified¶
For every other dialect a table matcher extracts a bare, lower-cased table name (sqlglot's .name), dropping
catalog and schema. For a federation engine that would be dangerous: a grant of orders would match orders
in every catalog the connection can see.
So for trino, extraction and matching are fully qualified. sqlglot with read='trino' parses
exp.Table.catalog / .db; tableguard emits each real table as catalog.schema.table, filling the parts a
reference omits from the connection's defaults (the connection's catalog = its database field, and schema). A
grant is qualified the same way (tableguard.qualify_grant): orders → <catalog>.<schema>.orders,
sales.orders → <catalog>.sales.orders, hive.sales.orders as written. So a grant of orders reaches only the
default catalog+schema's orders, never orders in another catalog or schema. The same qualified matching is used
by the schema browser (schema._only_allowed). A scoped, read-only Trino user is still recommended as an outer
boundary, but allowed_tables is now catalog/schema-accurate - not the schema-blind limitation other dialects carry.
3. Read-only rests on the SQL guard, not the driver¶
Trino has no read-only transaction or session mode, so - like DuckDB, H2 and JDBC - the read-only guarantee is the
server's SQL check (sqltools.validate_sql / write_operations): a non-read-only statement is refused with
read_only before it runs. trino is not a member of databases.DRIVER_READ_ONLY. We verify the write-keyword
set covers Trino's write surface (INSERT/UPDATE/DELETE/MERGE, CREATE TABLE AS, CALL, and ANALYZE, which
collects statistics and is treated as a write). Trino uses ANSI string quoting, so it correctly stays out of
BACKSLASH_ESCAPE_DIALECTS. A read-only Trino user is recommended as defense in depth. Agent writes (ADR 0008)
are out of scope for the first cut - trino is not in WRITE_RUNNERS.
4. The query time limit is enforced by cancellation¶
There is no engine-level timeout; each driver enforces its own. For trino we start a daemon timer that cancels
the running query on the client (cursor.cancel()), the same shape DuckDB uses, and also SET SESSION
query_max_run_time as a server-side backstop. With this, trino is in databases.TIMEOUT_ENFORCED and passes the
timeout integration test (unlike JDBC, which self-skips it).
5. Not in the first cut¶
- Qualification-aware
allowed_tables(see §2) - the graduation blocker. - Primary/foreign keys in the schema browser (Trino has no enforced constraints) and
SHOW CREATE TABLEDDL (three-part identifier handling) - both left off, as for ClickHouse. - Agent writes.
6. Experimental, with a graduation checklist¶
trino joins experimental.FEATURES['connection_types'].db_types. Already met: a real Trino runs in the
integration CI job, with the read-only check, the time limit and (fully-qualified) allowed_tables each tested
against it; and the federation boundary has a security review (THREAT_MODEL.md, "Trino"). Remaining to graduate (per
databases.GRADUATION): the shape unchanged for a release and real-deployment use; the docs record what may still
be added (writes, more metadata).
Consequences¶
- Trino's catalogs become first-class, governed sources - with masking, the schema browser, the flow diagram, a real time limit, and run/audit history - instead of the ungoverned generic-JDBC path.
- The federation reach that makes Trino valuable is also what makes bare-name
allowed_tablesrisky; we are explicit that, until qualification-aware matching lands, the scoped Trino user is the boundary, and the type stays experimental to say so. - No new parser or masking engine: the sqlglot
trinodialect carries the table guard, masking and the diagram. trinois an optional extra (queryapigate[trino]); a server without it is unaffected.
Alternatives considered¶
- Leave it to generic JDBC. Works today, but with no timeout, no schema browser, and no
allowed_tables/masking - unacceptable governance for an engine that reaches everything.
- Build our own federation layer (query several connections at once). Far larger, and it reinvents exactly what Trino is for; the backlog already parks that idea behind this one.
- Pin each connection to a single catalog so bare names are scoped. Safer, but it throws away Trino's reason to exist (one connection, many catalogs); rejected in favour of qualification-aware matching as the graduation path.
Delivery¶
Roughly: a _Trino driver + RUNNERS/STREAM_RUNNERS entries and the trino extra; the membership trio
(config.SUPPORTED_DB_TYPES, databases.TYPES, experimental db_types); the sqlglot-dialect maps
(tableguard, masking, sqlflow, tracing) and the frontend lists; schema._QUERIES['trino'] and
SHOW CATALOGS; a TrinoTests integration class with a trinodb/trino CI service; the regenerated support matrix
and a ## Trino connections doc section. Launched experimental; qualification-aware allowed_tables and the CI
security review are the work that graduates it.