TRINO
Trino is a distributed SQL engine that queries data where it already lives — one coordinator, many catalogs, from Iceberg to Postgres to Kafka. In DVT it plays both roles an engine can play: a default target that builds models into a writable catalog, and a live federation connection any model can read through meta.connection while writing somewhere else entirely.
CONNECT — THE COMMON CASE
One output in profiles.yml is all it takes. Most internal clusters run plain HTTP on port 8080 with a username and no password — this is a complete, working profile for exactly that:
# ~/.dbt/profiles.yml
my_project:
target: trino_dev
outputs:
trino_dev:
type: trino
host: trino-coordinator.internal.com
port: 8080
user: analyst
catalog: iceberg # where models build — pick a writable one
schema: analytics
threads: 4Run dvt debugand DVT tests the connection on both of its layers — the dbt layer that builds models and the Sling layer that moves data. If the Trino driver isn't installed yet, dvt sync reads this profile and installs it for you.
CATALOG AND SCHEMA, ALWAYS IN PAIRS
Every Trino relation is catalog.schema.table. The profile's catalog (required — database is an accepted alias) and schema (defaults to default) set the session context your models build in. Reading across other catalogs inside a model works exactly as Trino intends — just qualify the full three-part name.
PASSWORD AUTH (LDAP) — HTTPS BY RULE
Trino itself refuses password authentication over plain HTTP — a hard server-side rule, not a preference. So a password-protected cluster (LDAP, file-based users, Starburst built-in auth) means http_scheme: https:
trino_prod:
type: trino
host: trino.example.com
port: 443
user: analyst
password: "{{ env_var('TRINO_PASSWORD') }}"
http_scheme: https
catalog: iceberg
schema: analyticsDVT knows the rule too: the moment a password appears on a Trino output, the data lane connects over HTTPS even if you forgot to spell http_scheme out. Spell it out anyway — the profile should say what it does.
JWT — NOT ON THE MENU
Username/password over HTTPS is the one auth shape DVT supports on Trino — it is the only method both of DVT's lanes speak. method: jwt is not in that intersection, and DVT refuses it by name rather than failing somewhere strange:
trino: method 'jwt' is unsupported in the Sling lane (dbt lane only) — use user/password over http_scheme: https for federation
That's the guidance, verbatim: give the output a user and password over http_scheme: https, like the LDAP example above.
HOW TRINO BEHAVES IN DVT
Models build where the connector allows.Trino delegates writes to the catalog's connector, so pick a writable catalog (Iceberg, Delta, Hive with a writable metastore) as your target context. Materializations cover table, view, incremental, ephemeral and materialized_view.
Session properties and multi-catalog SQLwork as documented for dbt-trino — DVT's model-building lane is that adapter. Starburst clusters (Galaxy included) speak the same protocol and connect with the same profiles.
Data movement. Federation extraction and loading ride the same coordinator endpoint you configured — same host, same scheme. Trino streams results rather than staging them, so extracts start flowing as soon as the query does.
Names land lowercase. Trino catalogs store schema and table names folded to lowercase — send unquoted public_STG in SQL and the server downfolds it to public_stg anyway. DVT commits to that one casing convention on every lane: federated loads target the lowercase spelling directly, so a schema created on one visit is found — not collided with — on the next, however your project spells it. One engine, one casing rule, decided once.
Reading it live. To use this connection as a federation source, point a source at the output by name:
# models/sources.yml
sources:
- name: lake
meta:
connection: trino_dev # the profiles.yml output above
schema: raw # schema within the output's catalog
tables:
- name: web_logs
- name: app_eventsLocal dev in one line. The official image starts a single-node cluster on 8080 with in-memory catalogs to play against — user anything, no password:
docker run -d --name trino -p 8080:8080 trinodb/trino
INCREMENTAL MODELS — THE CONNECTOR HAS THE FINAL WORD
Four strategies are accepted by name: append, merge, delete+insert and microbatch. insert_overwriteis not one of them — it is a dbt builtin this adapter does not declare, so asking for it stops the run before any SQL reaches the coordinator. A name that isn't a builtin at all fails one step later, at macro lookup. Trino declares neither message itself; both come from the shared adapter layer:
The incremental strategy 'insert_overwrite' is not valid for this adapter dbt could not find an incremental strategy macro with the name "get_incremental_upsert_sql" in my_project
Set no incremental_strategy at all and the materialization substitutes the literal default— and Trino ships no default-strategy macro of its own, so that sentinel resolves through dbt-core's, which is append. Take that fact the way it was established: read off the dispatch chain in the installed packages, not observed on a live cluster. Appending and upserting are a data-shape difference, so if it matters to a model, name the strategy rather than inherit it.
The part that actually bites is unique_key. Both merge and delete+insert degrade to a plain INSERT when no unique_key is configured: the DELETE half of one and the MERGE body of the other are wrapped in the key check, while the INSERT runs unconditionally. No error, no warning — just a table that grows where you expected it to update. Both accept a composite key (a list of columns) when you give them one.
{{ config(
materialized='incremental',
incremental_strategy='merge', -- append | merge | delete+insert | microbatch
unique_key=['order_id', 'line_no'] -- omit this and merge becomes a plain INSERT
) }}
select order_id, line_no, status, updated_at
from {{ source('lake', 'orders') }}
{% if is_incremental() %}
where updated_at > (select max(updated_at) from {{ this }})
{% endif %}The remaining conditions are small but real. Incremental models on Trino are SQL only — the materialization declares no Python support. microbatch builds its predicates from the model's event_time and dbt's batch bounds, and from nothing else. delete+insert with a unique_key forces the temporary relation to be a real table; every other combination may use a view, unless views_enabled: false forces a table everywhere. merge honours merge_update_columns and merge_exclude_columns. And on_table_exists must be rename, drop or replace — any other value is logged and silently reset to rename.
What the adapter does not do is check the connector. There is no table-format, catalog-type or engine-version gate anywhere in it: ask for mergeon a catalog whose connector cannot perform a row-level MERGE and the refusal comes back from Trino, not from a validator. Worse, an incremental model's first run is a plain create — so a strategy the connector cannot perform stays invisible until build #2.
WHICH CATALOGS CAN ACTUALLY MERGE
DVT keeps a per-connector map and detects the connector live, by querying system.metadata.catalogs on your coordinator: Iceberg and Delta Lake can MERGE; Hive and Hudi cannot. A connector absent from the map is left alone and proceeds — the map refuses only on positive evidence. The Hive entry is there because a federated model died on the real thing: USER_ERROR: Modifying Hive table rows is only supported for transactional tables. If your Hive table genuinely is transactional (ACID), set table_is_transactional: true and DVT stands aside. dvt debugprints each catalog's connector during its engine preflight, so you learn this at setup time rather than on the second build.
When Trino is the target of a federated f_incremental, the strategy stops compiling into Trino SQL and starts selecting a load mode instead: DVT computes the model in DuckDB and lands rows through its data-movement layer. The vocabulary shrinks to append, merge and delete+insert. insert_overwrite and microbatch are refused outright — DVT025, "has no Sling equivalent — f_incremental supports append, merge and delete+insert" — even though the native adapter accepts microbatch perfectly well. merge and delete+insert resolve to the same thing here, an upsert on unique_key; there is no delete-then-insert on the federated lane. Without a unique_key, either one falls back to a full refresh with a warning rather than an error, and append without a watermark_column does the same.
Before any of that runs, the connector check above applies — a MERGE the catalog cannot perform is refused as DVT064 rather than attempted:
[{model}] refusing a MERGE this connector cannot perform. Attempted: apply
the incremental delta to {target} ({type}) as a row-level MERGE on
unique_key ({keys}). The connector cannot: {reason}.The way out is the one DVT names: use an append-class materialization — f_table, or f_incremental with incremental_strategy='append' and a watermark_column — or point the model at a transactional catalog, which on Trino means Iceberg or Delta Lake. That append-with-watermark route is available here because Trino is one of the engines whose connector has been verified to honour an update-key-only load: a keyless f_incremental is admitted on Postgres, Redshift, MySQL, DuckDB, SQLite, SQL Server, Oracle, ClickHouse and Trino, and refused with DVT026 on engines still unmeasured — StarRocks, Snowflake, Databricks, BigQuery, Fabric and Athena.
REFERENCE — EVERY PROFILE FIELD
Everything the Trino profile reads. type, host, user and catalog are the hard requirements — the rest has sensible defaults.
| FIELD | TYPE | REQUIRED | DEFAULT | NOTES |
|---|---|---|---|---|
| type | string | yes | — | Must be trino. |
| host | string | yes | localhost | The coordinator's hostname — not a worker. |
| port | integer | no | 8080 | Coordinator port. Typically 443 behind HTTPS. |
| user | string | yes | — | Trino username; becomes the query's identity. |
| password | string | no | — | LDAP / file-based / Starburst password. Setting one implies HTTPS — Trino refuses passwords over plain HTTP. Use {{ env_var('TRINO_PASSWORD') }} rather than a literal. |
| http_scheme | string | no | http | Set https for TLS clusters. Required (implicitly or explicitly) whenever a password is set. |
| method | string | no | — | Auth method. jwt is refused by name — 'unsupported in the Sling lane' — use user/password over http_scheme: https instead. |
| catalog | string | yes | — | The catalog models build in — pick a writable connector. database is accepted as an alias for this same field. |
| schema | string | no | default | Schema within the catalog for built models. |
| threads | integer | no | 4 | Parallel threads for model builds. |