catalog.schema.table, three parts, always written in full, because dex
connects with no session catalog or schema. The cost paradigm is database
load in cluster-seconds: a self-hosted cluster bills no currency, but an
unbounded scan on a shared cluster slows everyone else on it and, through a
federated catalog, the database behind that catalog too. Read-only against
data, enforced in depth.
Starburst Galaxy bills differently and is not supported yet (see
Deployment). Amazon Athena is Trino-derived but has its own API,
driver and bytes-scanned pricing, so it would be a separate connector.
Install with the [trino] extra, which brings the trino client and
dbt-trino.
Authentication: discover, don’t ask
The engine discovers a connection at runtime and never prompts for or persists a credential. Discovery order:TRINO_URL:http://user@host:port,https://user[:password]@host:port, ortrino://user@host:port. A catalog or schema in the URL path is ignored with a warning, since dex never sets one.- the
TRINO_*environment (TRINO_HOST,_PORT,_USER,_HTTP_SCHEME,_PASSWORD,_JWT_TOKEN) - the committed non-secret
trino.host/port/user/http_schemeconfig target, with the credential still supplied byTRINO_PASSWORDorTRINO_JWT_TOKEN - a
type: trinooutput in a discovered dbtprofiles.yml, with{{ env_var(...) }}references expanded
- OAuth2 opens a browser, or waits for a sign-in at the console
- Kerberos needs a ticket cache dex does not manage
- certificate auth needs client key files dex does not manage
allow_insecure_auth. The scheme defaults to
https whenever a credential is present or the port is 443.
Only a coarse method (environment:dsn, config_target:password,
dbt_profile:jwt) is ever surfaced. Users, passwords and tokens never cross the
envelope.
Splitting dex’s identity from dbt’s. dex reads and dbt writes, and they
should be different users. TRINO_URL is read only by dex; the profile
transform init renders reads TRINO_USER and TRINO_PASSWORD (or
TRINO_JWT_TOKEN) for dbt. So the URL carries the read-only identity and the
variables carry the one that writes the dev schema.
Attribution. Every connection dex opens sends source=dex and
client_tags=dex, so an operator can see dex’s queries in the UI and route them
to a constrained resource group with a selector on either.
Config
credit_price_usd is Galaxy’s dollar translation, and is refused under
self_hosted, where it would be accepted and never used.
Scoping a command
A scope is acatalog or a catalog.schema, never a table:
--scope narrows the committed trino.catalogs allowlist and can never widen
it. Resolution is free (system.metadata.catalogs and each catalog’s
information_schema.schemata) and happens before anything is estimated, so a
typo is refused for nothing, with the catalogs or schemas that do exist listed.
Identifiers are three parts, and always qualified
lake.sales.orders, folded to lower case, which is how Trino stores an unquoted
name. dex opens every connection with no session catalog or schema, and
refuses any relation that is not fully qualified (a bare name is allowed only
where the statement defines it as a CTE). That is a safety property, not a
style: the query firewall resolves an agent’s partial name to a cached relation
and applies that relation’s PII verdict, and with a session catalog Trino could
resolve the same spelling to a different table than the one that was checked.
Cost model
- Free:
connect test,explore inventory, every schema fact (information_schema, no scan),EXPLAIN (TYPE IO),maintain schema, every dev-target preflight, anddbt compile-based build pricing. - Metered: profiling, relationship verification,
explore query,explore cluster,maintain grain, the cardinality half ofmaintain semantic,transform build, and the row counts behindmaintain volumeandbuild --verify.
information_schema carries no row or byte
figures, and SHOW STATS is never used because its low_value and
high_value columns are raw column values, PII included. So an inventory
reports no sizes, an exact count arrives with the first profile’s COUNT(*),
and maintain volume names each object it could not compare in warnings
rather than reporting no drift.
The estimate is free and executes nothing. EXPLAIN (TYPE IO, FORMAT JSON)
plans the statement on the coordinator and reports each input table’s estimated
bytes after column pruning and predicate pushdown. Bytes become seconds through
a deliberately conservative scan rate (200 MiB/s). A catalog with no statistics
(most JDBC catalogs, a Hive table never ANALYZEd) answers NaN;
such a table is priced at a fixed floor and named in a note. The handshake’s
estimate_basis says explain_io when every table was priced from the plan and
floor when at least one was not, so you know which part of the quote to
distrust. A statement that cannot be planned at all, almost always a build’s
test over a relation the build has not created yet, is priced at the 0.5-second
minimum and counted in a note, as every connector prices a cold dev target: the
estimate is then a lower bound, and the run-time cap still binds each statement. estimate_quality is heuristic, because the number you confirm is
seconds from a constant.
The cap is two session properties, set through one door. Every billed
statement runs under query_max_run_time, wound down to what remains of the
budget, and query_max_scan_physical_bytes, derived from the same scan rate so
the cap and the quote are two views of one number. Both are applied with
SET SESSION / RESET SESSION by the adapter itself, so they hold on a
connection a host supplied too, and each door states its whole set: a free
metadata read runs under a fixed 60-second wall cap and never inherits a billed
statement’s tight one.
Time is the cap that always binds. The byte cap is checked on an interval of
about a second, and some catalogs report no physical input at all (tpch
reports none). Run time rather than execution time is capped because run time
includes queueing and planning, which is what the settlement measures.
Settlement is the server’s own elapsedTimeMillis once a statement
finishes, and the client’s wall clock when it fails or is cancelled (the client
stops polling stats on an error, so its last figure would be stale). A partially
fetched result is cancelled server-side, so nothing keeps running unrecorded.
There is no currency translation, because nothing is billed in one.
Deployment
trino.deployment declares which Trino this is, and it is a cost decision rather
than a connection one:
self_hosted(default): Trino, Starburst Enterprise, and Trino on EMR, guarded asdb_loadin cluster-seconds.galaxy: Starburst Galaxy, which bills credits for every second a cluster runs, resume and idle tail included. It is accepted by config validation so the committed shape is final, and refused by name onconnect, the dev-target preflight, andtransform init, withcompute_timeas the paradigm in the envelope, because dex does not model that uptime yet.
self_hosted declaration
whose host, committed or discovered, ends in .galaxy.starburst.io is refused,
since the guard would otherwise count load on a cluster that bills uptime.
Read-only, enforced in depth
Trino has no read-only session mode a client can hold, soconnect test
reports session_read_only: false rather than claiming one. Read-only rests on:
- A guard on the billed door and the explain door: one read-only SELECT,
every relation fully qualified, and no table functions (
TABLE(...), including a federated catalog’ssystem.querypassthrough, which would send opaque SQL to the remote database), hidden$columns, connector metadata tables ("orders$files","orders$snapshots"), time travel (FOR VERSION AS OF,FOR TIMESTAMP AS OF), orDESCRIBE. - Refusal of every non-query statement:
CALL,EXECUTE,SET/RESET SESSION,USE,ANALYZE,EXPLAIN ANALYZE, and every write. The safety spine pins each one, so a parser upgrade cannot quietly admit it. - An adapter that issues only
SELECT,EXPLAIN, and its ownSET/RESET SESSIONfor the caps. Inventory filters out$tables too. - Server-side access control for the dex identity, which is the layer
nothing inside dex can vouch for, so set it up. With file-based access
control (
access-control.name=file), a shape that gives dex read-only access everywhere and dbt write access to its dev schema only:
GRANT_SELECT for the dbt identity is not optional: Trino views run with their
owner’s rights, so when dex (or anyone else) reads a dbt view, Trino checks that
the view’s owner may pass on read access to everything the view reads. Without
it a staging view over a source reads fine and a mart over those staging views
is refused (“View owner ‘dbt_dev’ cannot create view that selects from …”).
The same intent maps onto Ranger or OPA policies. scripts/trino/rules.json is the
version the integration suite runs against, with an admin identity for
seeding.
A resource group selected by source: dex (or the dex client tag) with a
softCpuLimit or hardConcurrencyLimit is the server-side backstop on load
that dex cannot raise from inside a session.
Profiling behavior
- Nested and special types (
array,map,row,json,varbinary, the sketch types such ashyperloglogandtdigest, and the geo types) degrade to a non-null count before any per-column statistic is requested. Their spellings contain the substrings the shared type predicates look for (array(varchar)reads as a string), and aregexp_likeover one would fail the whole batch. - Fractions are computed with double literals. Trino types
1.0asdecimal(2,1)andavgkeeps that scale, which would round every null fraction to one decimal place; the profiles match DuckDB’s on the same data. max_full_profile_bytessamples a table whose planned scan exceeds it withTABLESAMPLE BERNOULLI (pct), written natively because the transpiled form is one Trino rejects. BERNOULLI keeps each row with that probability, so it still reads every split: it reduces CPU and aggregation work, not the scan, and it is not repeatable. The table note says so, and a sampled profile never judges uniqueness.explore clustersamples the same way, with the same caveat.- Trino enforces no keys, so every relationship dex reports here is name-and-shape inference, verified by an overlap probe when you ask.
CROSS JOIN UNNEST(...)
over an array or map column and over CAST(json_parse(c) AS ARRAY(JSON)) or
MAP(VARCHAR, JSON), with the outputs inheriting the source column’s PII flags.
dbt builds
The dbt adapter isdbt-trino, which ships with the [trino] extra. Its
database is the Trino catalog. transform init renders a dev-only profile
with no secret in it:
cert: true is explicit because dbt-trino otherwise hands the client
verify=None, which turns certificate checking off for the identity whose
password the profile sends.
The build cap rides session_properties. dbt-trino sends each entry with
every statement it issues, CTAS and MERGE included, and transform build fills
the two variables from the confirmed budget. The literal defaults are real caps
for a hand-run dbt build, because on Trino 0s fails every statement at once
rather than meaning unlimited. A hand-written profile without those references
builds fine but cannot be capped; dex warns rather than refusing, and the build
result says the run was uncapped. transform preflight reports which you have.
The dev-target preflight asks about the catalog, not the schema. dbt-trino
creates a missing dev schema but never a catalog, and a catalog is only as
writable as its connector. So dev_catalog must exist, and is refused before
dbt runs when it is a read-only kind (tpch, tpcds, jmx, system,
faker). A memory catalog (lost on restart) and a blackhole catalog (keeps
nothing) are allowed with a warning. Whether the dbt identity may write is not
asked: Trino authorization is pluggable and exposes no grant table, so dbt finds
out.
Scaffolded sources name their catalog. dbt-trino resolves a source with no
database in the profile’s, which is the dev catalog, so transform plan --scaffold writes each source with database: <catalog> and names it
<catalog>_<schema> (memory_app), since federated catalogs often share schema
names. Two relations sharing a table name in different catalogs would both map
to stg_<table>; the scaffold refuses rather than repoint an existing model, and
names transform rename model as the way through.
A lakehouse dev catalog cannot hold every source type. Iceberg rejects
char(n), timestamp(p) below microseconds, and a few others, so a staging
model that passes a federated source’s column straight through fails to build
there (Type not supported for Iceberg: char(10), Timestamp precision (3) not supported for Iceberg). Cast in the staging model (cast(zip as varchar),
cast(paid_at as timestamp(6))). The scaffold does not cast for you, because the
right target type depends on the dev catalog’s connector: Hive, for one, rejects
timestamp(6) under its default precision.
transform build --verify reads dev_catalog.dev_schema for that one command.
Its row counts are priced into the build’s own estimate, and since Trino keeps
no free count, every relation verified is a count query.
transform test --mutate prices its batch as one number and confirms it once.
Survivors are not yet compared with the model on the dev data here, so each
reports equivalence.status: not_checked with the reason.
The shipped unpivot_json_object macro has a Trino implementation that casts the
object to map(varchar, json) and unnests it, so it is top-level only and keeps
nested values as JSON, matching the other adapters.
Semantic layer
explore semantic query --local renders with MetricFlow’s Trino renderer and
runs through dex’s own connector, firewall and cost handshake.
A native Apache Ossie layer reads expressions in the portable ANSI_SQL
dialect, since Ossie’s dialect enum has no Trino token. A SLayer layer is
refused on Trino for now: SLayer has a Trino dialect, but a connector joins only
once a golden of its rendered SQL has been read (see
Querying the semantic layer).
Testing
Offline, deterministic, and free:tests/fakes/trino.py is a stateful fake of
the client surface, driving tests/adapters/test_connect_trino.py and the Trino
block of the safety spine.
The live suite runs against a container pair you start yourself (Trino, plus the
Postgres database its keyless Iceberg catalog keeps table pointers in), with the
tpch, memory and iceberg catalogs and the file-based access control above.
The same script CI runs stands it up:
trino-iceberg-integration environment, through an OIDC role that may
write only the dbt dev database. scripts/setup_trino_iceberg_ci.sh provisions
it; locally, scripts/setup_trino_dev.sh --aws attaches the same catalog from
the current AWS CLI session:
references/trino.md (edit on main). This page is generated; change the source, not the site.