> ## Documentation Index
> Fetch the complete documentation index at: https://docs.exmergo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Amazon Redshift connector

> Connect dex to Amazon Redshift Serverless or provisioned clusters: IAM temporary credentials, compute-seconds priced as RPU-hours, read-only access.

The AWS warehouse, Serverless-first. Namespace: `database.schema.table` (one
connection, one database; the first component is always the connected
database). Cost paradigm: **compute-seconds**, translated to RPU-hours on
Serverless. Read-only against data, enforced in depth. Provisioned clusters
ride the same SQL and metadata paths with seconds-only accounting (node-hours
bill flat, so there is nothing to translate); the cost gate is designed for
Serverless.

## Authentication: discover, don't ask

The engine discovers a connection at runtime and never prompts for or
persists a password. Discovery order:

1. `redshift.workgroup` in `.dex/config.yml`: the Serverless pin. The AWS
   default credential chain (`aws configure`, `AWS_*` env, SSO, an assumed
   role, instance metadata; `redshift.aws_profile` pins a named profile)
   resolves the endpoint, port, and database from the control plane, and the
   driver mints IAM **temporary database credentials** (`GetCredentials`) at
   connect time. Nothing durable exists to leak.
2. `redshift.cluster_identifier` plus `dbname`/`user`: the provisioned IAM
   analogue (`GetClusterCredentials`).
3. the `REDSHIFT_*` environment (`REDSHIFT_HOST`, `REDSHIFT_DATABASE`,
   `REDSHIFT_USER`, password via `REDSHIFT_PASSWORD`)
4. the committed non-secret `redshift.host`/`port`/`dbname`/`user` config
   target (password still supplied by `REDSHIFT_PASSWORD`)
5. the `host` of a `type: redshift` target in a discovered dbt `profiles.yml`

Only a coarse method is ever surfaced (for example `iam_serverless:profile`
or `config_target:password`); identities, keys, and passwords never cross the
envelope. Every discovery failure names all the fixes, including the IAM
permissions the workgroup path needs (`redshift-serverless:GetWorkgroup`,
`GetCredentials`, and `GetNamespace` for database discovery).

## Config

```yaml theme={null}
# .dex/config.yml
connector: redshift
redshift:
  workgroup: dex-wg              # the Serverless pin; IAM auth + RPU facts
  aws_profile: null              # a named ~/.aws profile; default chain if unset
  region: null                   # when the chain's default region is wrong
  schemas:                       # source allowlist; empty means all visible
    - shop
  dev_schema: dbt_dev            # where dbt dev builds write (never a source)
  rpu_price_usd: null            # your region's RPU-hour price; set it to see dollars
budget:
  ceiling: 300                   # per-command compute-seconds; --budget overrides
  session_ceiling: 3600          # cumulative seconds per UTC day
```

Point dex at a read-only database user (or let IAM mint one); the documented
grant shape is USAGE on source schemas plus SELECT on their tables and on
`svv_table_info` (an IAM-minted user starts without it, and without it table
sizes are unknown and estimates degrade to minimums), with write access only
on the dedicated dev schema for the dbt user.

## Scoping a command

`redshift.schemas` is the committed source allowlist. `--scope` narrows it
for one command and is repeatable; a Redshift scope is a bare `<schema>` in
the connected database (dbt refuses cross-database references, so there is
nothing to qualify). Resolution is a catalog lookup and happens before
anything is estimated: a scope that names no schema is refused with the
schemas that do exist listed, never quietly dropped, and `--scope` narrows
the committed allowlist, never widens it. `--project` and `--dataset` are
BigQuery vocabulary and error here.

## Cost model: compute-seconds, honestly floored

Redshift Serverless charges RPU-hours whenever compute is active, with a
**60-second minimum** each time an idle workgroup wakes, and AWS treats every
incoming query as billable activity. Two consequences dex states plainly
rather than hides:

* **Metadata is cheap, not free.** Inventory, `connect test`, estimation,
  and the free axes of `maintain check` read only catalogs
  (`pg_catalog`, `SVV_TABLE_INFO`, `SVV_COLUMNS`) and are not gated, but
  touching an idle workgroup can incur the wake minimum; `connect test`
  carries that warning on Serverless.
* **Estimates carry the wake floor once per command.** There is no API that
  says whether compute is warm (and probing would itself bill), so every
  estimate on Serverless includes the 60-second minimum exactly once,
  labeled an upper bound that actuals waive when compute was already active.

**Metered:** profiling aggregates, `explore query`, relationship
verification probes, distinct-count escalations, and `transform build`.

`explore query` and `explore cluster` bill an auto-profile of an object this connection has that the `.dex/` cache cannot adjudicate, priced into the same handshake as the statements: see [�3](/dex/guardrails/cost-controls). Pass `--no-auto-profile` (or set `auto_profile: false` in `.dex/config.yml`) to be refused instead.

Budgets (`budget.ceiling`, `--budget`, `budget.session_ceiling`) are
compute-seconds: the number you budget is the number the server enforces.
Every cost surface also carries the translation to **RPU-hours**
(`seconds x base_capacity / 3600`, base capacity from the workgroup via the
control plane, labeled approximate because Serverless can scale above it)
and to dollars when `redshift.rpu_price_usd` is set. dex never guesses a
dollar price: the RPU-hour rate varies by region and agreement.

**Estimates are a heuristic, honestly labeled.** Redshift has no dry-run,
and its EXPLAIN costs are relative units with no honest translation to
seconds, so estimates come from table bytes (`SVV_TABLE_INFO`) over a
conservative capacity-scaled scan rate; every handshake payload carries
`estimate_quality: "heuristic"`. The same estimator prices `transform build`:
each compiled model, snapshot, and test is estimated and summed into the
build's upfront cost.

`transform build --verify` costs only where the warehouse keeps no row
count, so a table's count is free metadata; how the counts are priced into
the build's own estimate is in
[�1](/dex/guardrails/cost-controls).

`transform test --mutate` prices its whole batch as one number and confirms it
once, then runs one dbt invocation per mutant. Nothing is materialized: a mutant
builds as an ephemeral model, so the dev namespace holds exactly the relations it
held before. A budget that runs out partway stops the run, and the remaining
mutants are reported `not_run` rather than the budget being exceeded. Survivors
are not yet compared with the model on the dev data on this connector, so each
reports `equivalence.status: not_checked` with the reason, and nothing is
priced or run for it.

`--verify` also reads `redshift.dev_schema` for the length of that one command,
because dbt writes the relations it is judging there and that schema is refused
as a source everywhere else. The schema need not exist yet: dbt creates it on a
first build, and dex lists it after dbt has run. How that read is bounded is
under **Read scope** in [�2](/dex/reference/command-contract).

**The budget is hard-enforced regardless of estimate quality.** Before every
metered statement the session's `statement_timeout` is set to the remaining
budget, so a wrong heuristic cannot overrun the ceiling: Redshift kills the
statement and dex reports the over-ceiling refusal. Actual spend is
wall-clock seconds per statement (a killed statement still bills what ran),
recorded as `billed_seconds` in the ledger. Every session connects as `application_name = 'dex'`
(`SYS_CONNECTION_LOG`) and sets `query_group = 'dex'` for attribution.

The estimate carries its RPU translation; the handshake itself is in
[�0](/dex/guardrails/cost-controls).

## Read-only, enforced in depth

* Every data statement passes the SELECT-only guard parsed in the redshift
  dialect through one execution door (DML, DDL, `COPY`, `UNLOAD`, and
  multi-statement forms all refuse).
* The adapter issues no mutating statements: catalog SELECTs and session
  `SET`s only.
* The session attempts `default_transaction_read_only = on`; whether Redshift
  honors it is probed rather than assumed, and `connect test` reports the
  truth as `session_read_only` (the guard and grants hold either way).
* The documented grant shape is a least-privilege read-only user; dbt dev
  builds write to `dev_schema` (default `dbt_dev`), which is refused as a
  source, and the generated profile renders one thread and IAM or an
  `env_var` password reference, never a value.

## Profiling behavior

Profiling is batched aggregates in single passes: `COUNT(*)`, per-column
non-null counts, `HLL(...)` approximate distincts (Redshift refuses more
than three `APPROXIMATE COUNT(DISTINCT)` expressions per statement, so the
equivalent HLL aggregate keeps a wide batch a single pass), and min/max only
for engine-cleared safe columns; never row values. Redshift keeps no usable
planner distincts, so approximate distincts ride the billed batch, and an
approximation is never a uniqueness verdict: near-unique keys escalate to an
exact `COUNT(DISTINCT)` inside the confirmed budget, and that scan also
upgrades the `SVV_TABLE_INFO` row estimate to an exact figure. When no single
column proves unique, the composite-key probe (a bounded batch of exact
distinct-combination counts) spends inside the same confirmed budget, and
like the escalation it carries the pending Serverless wake minimum when it
bills first; when the remaining budget cannot cover every pair, the probe
narrows to the best-ranked pairs it can afford, and skips with a note only
when it cannot afford one, in which case the grain stays unknown. `SUPER`,
`VARBYTE`, `GEOMETRY`, `GEOGRAPHY`, and `HLLSKETCH` columns degrade to
non-null counts. There is **no sampled-profiling threshold**: Redshift has
no TABLESAMPLE, so a sampling knob would be a lie; the budget is the only
bound. Empty tables are inventoried from `pg_class` even though
`SVV_TABLE_INFO` omits them.

**Every `CAST` in a generated aggregate is total, and has to stay that way.**
Redshift evaluates a `CASE` branch's cast for rows the `WHEN` never selects,
so guarding a cast behind a shape predicate inside a `CASE` does not protect
it: the declared-type-vs-content probe did exactly that and died on any table
with one non-numeric string in a profiled column (`Invalid digit, Value 'p',
Pos 0, Type: Long`). There is no `TRY_CAST` here and the Python UDF escape
hatch is end-of-support after 2026-06-30, so the discipline is in the
expression shape: a cast's argument is itself a `CASE` yielding a digit-only
string on every row, with a sentinel every predicate built on the cast
rejects. It is enforced offline for all six adapters (see
`assert_every_cast_is_total`), because no unit test can catch a dialect that
disagrees with the standard about evaluation order.

**Two more spellings the server refuses outright, both verified live.**
`DATEDIFF` resolves to `pg_catalog.date_diff`, which is declared over
`DATE`/`TIME`/`TIMETZ`/`TIMESTAMP` and has no `TIMESTAMPTZ` overload, while
`DATE_TRUNC` over a `TIMESTAMPTZ` column returns `TIMESTAMPTZ`: the periods
the temporal-continuity probe diffs are exactly the shape it rejects
(`function pg_catalog.date_diff("unknown", timestamp with time zone,
timestamp with time zone) does not exist`), which failed the whole profiling
statement for any table carrying a `TIMESTAMPTZ` column. Both operands are
therefore cast to `TIMESTAMP`, a conversion that is total for every type
reaching that path and shifts nothing, since it applies to both sides by the
same rule. And `SUBSTR`, the spelling the shared expressions use, is refused
by name (`SUBSTR() function is not supported (Hint: use SUBSTRING instead)`)
at execution over a real table, not only in a leader-node-only query, so this
adapter emits `SUBSTRING` for the slash-date component extraction. Neither
spelling is universal in the other direction: BigQuery has `SUBSTR` and no
`SUBSTRING`, which is why the idiom is per-connector rather than swapped in
the shared builder.

## dbt builds

`transform init` renders a `type: redshift` dev profile from whichever
source won discovery: IAM discovery renders `method: iam` (temporary
credentials minted by dbt-redshift at runtime, nothing persisted), password
discovery renders `password: "{{ env_var('REDSHIFT_PASSWORD') }}"` (a Jinja
reference, not a value), both with `schema` pinned to the dedicated dev
schema and one thread. Before the cost gate, and for free, `transform build`
refuses config that has drifted from the rendered profile, and a dev schema
the profile's user lacks the privilege to create or write (the Postgres
question: dbt creates the schema, but only if the user may). `transform init`
additionally content-checks the dev schema (and, with `--layered-schemas`,
the `staging_dev` / `intermediate_dev` / `marts_dev` layer schemas) with one
catalog lookup per schema, warning when one already holds tables or views;
this works on IAM profiles too, since content, unlike privilege, is not a
per-user question. dbt has no
dry-run and dex cannot inject a per-build server cap through dbt-redshift,
so the honest layering is: per-node execution time summed into
`billed_seconds` on the ledger, a durable `ALTER USER dbt_dev SET
statement_timeout` applied by the setup script, and a workgroup usage limit
(RPU-hours per day) as the account-level backstop. Keep one dev schema per
building user: dbt swaps a rebuilt relation in place, and a relation another
user owns breaks the swap with a bare `relation already exists` (verified
live), so two identities sharing `dbt_dev` will trip over each other's
views.

## Testing

Offline: a stateful fake connection (`tests/fakes/redshift.py`) that records
statement ordering, serves the catalog census (including SVV\_TABLE\_INFO's
empty-table omission), simulates timing on an injected clock, and enforces
`statement_timeout` with redshift\_connector's real error shape. Live: an
env-gated integration suite (`DEX_TEST_REDSHIFT_*`) against a seeded
Serverless workgroup, run in CI on a schedule via GitHub OIDC role assumption
(no stored keys), never as a merge gate; `scripts/setup_redshift_ci.sh`
provisions the workgroup, users, and the usage-limit backstop.

Source: [`references/redshift.md`](https://github.com/exmergo/dex/blob/v1.14.1/references/redshift.md) ([edit on main](https://github.com/exmergo/dex/edit/main/references/redshift.md)). This page is generated; change the source, not the site.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.