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

# Connect a Warehouse

> Turn read-only queries against your database or warehouse into tools that run inside your network

Connecting a database takes the engine and a reference to its connection string. Your agents get three tools at once: **List tables**, **Describe table** and **Query**, which runs one read-only statement an agent writes, on the tables you allow, capped and masked. The query runs where the connection string lives, which on the hosted service is your organization's [data-plane agent](/organizations/data-plane); Danube stores a *reference* to the connection string and never the string. Named query tools come later, promoted from the statements agents actually run, or written as templates on the advanced path.

## Engines

| Engine       | Driver in the agent image     | Parameters     | Read-only enforcement                                           |
| ------------ | ----------------------------- | -------------- | --------------------------------------------------------------- |
| `postgres`   | `psycopg`                     | `%(name)s`     | `BEGIN READ ONLY` plus `statement_timeout`                      |
| `redshift`   | `psycopg` (Postgres protocol) | `%(name)s`     | `BEGIN READ ONLY` plus `statement_timeout`                      |
| `mysql`      | `pymysql`                     | `%(name)s`     | `SET SESSION TRANSACTION READ ONLY` plus `max_execution_time`   |
| `snowflake`  | `snowflake-connector-python`  | `%(name)s`     | validator plus a read-only role; `STATEMENT_TIMEOUT_IN_SECONDS` |
| `bigquery`   | `google-cloud-bigquery`       | `@name`        | validator plus a viewer role; job timeout                       |
| `databricks` | `databricks-sql-connector`    | `:name`        | validator plus a read-only grant; `STATEMENT_TIMEOUT`           |
| `clickhouse` | `clickhouse-connect`          | `%(name)s`     | `readonly=2` plus `max_execution_time`                          |
| `trino`      | `trino`                       | positional `?` | validator plus a read-only role; `query_max_execution_time`     |

You write every template the same way, with `:name` placeholders; the engine rewrites them per driver. Whatever the engine, the template is validated before it runs: one `SELECT` (or `WITH ... SELECT`), no comments, no second statement, no data-modifying keyword anywhere.

## The connection reference

`connection_ref` names where the connection string is, in one of three forms:

| Form                             | Resolved by                                                                                |
| -------------------------------- | ------------------------------------------------------------------------------------------ |
| `env://ANALYTICS_DSN`            | An environment variable on the data-plane agent's host (or on a self-hosted control plane) |
| `vault://<mount>/<path>#<field>` | HashiCorp Vault KV v2, with the agent's `VAULT_ADDR` and `VAULT_TOKEN`                     |
| `ANALYTICS_DSN`                  | A bare variable name, read as `env://ANALYTICS_DSN`                                        |

A value in that field (anything with a scheme, a host or a password) is refused at registration, by the dashboard and by the agent. What the variable holds, per engine:

| Engine       | The variable holds                                                                                                                                                |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `postgres`   | `postgresql://user:password@host:5432/database`                                                                                                                   |
| `redshift`   | `postgresql://user:password@cluster.region.redshift.amazonaws.com:5439/database`                                                                                  |
| `mysql`      | `mysql://user:password@host:3306/database`, `?ssl=true` for TLS                                                                                                   |
| `snowflake`  | `snowflake://user:password@account/database/schema?warehouse=WH&role=ROLE` (`authenticator`, `token` and `private_key_file` pass through as query parameters)     |
| `bigquery`   | `bigquery://project` with default credentials on the host, `bigquery://project?credentials=/path/sa.json&location=EU`, or the path of a service-account JSON file |
| `databricks` | `databricks://token:TOKEN@workspace-host?http_path=/sql/1.0/warehouses/ID&catalog=main&schema=gold`                                                               |
| `clickhouse` | `https://user:password@host:8443/database` (`http://` for port 8123)                                                                                              |
| `trino`      | `trino://user:password@host:443/catalog/schema` (`https` whenever a password is set)                                                                              |

## A read-only role

The validator refuses writes, and Postgres, Redshift, MySQL and ClickHouse refuse them a second time on the connection. For the others the role is the second layer, so create one that can only read:

```sql theme={null}
-- Postgres and Redshift
CREATE ROLE danube_reader LOGIN PASSWORD '...';
GRANT USAGE ON SCHEMA public TO danube_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO danube_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO danube_reader;

-- MySQL
CREATE USER 'danube_reader'@'%' IDENTIFIED BY '...';
GRANT SELECT ON shop.* TO 'danube_reader'@'%';

-- Snowflake
CREATE ROLE danube_reader;
GRANT USAGE ON WAREHOUSE wh TO ROLE danube_reader;
GRANT USAGE ON DATABASE analytics TO ROLE danube_reader;
GRANT USAGE ON ALL SCHEMAS IN DATABASE analytics TO ROLE danube_reader;
GRANT SELECT ON ALL TABLES IN DATABASE analytics TO ROLE danube_reader;
GRANT ROLE danube_reader TO USER danube;

-- Databricks (Unity Catalog)
GRANT USE CATALOG ON CATALOG main TO `danube-reader`;
GRANT USE SCHEMA, SELECT ON SCHEMA main.gold TO `danube-reader`;

-- ClickHouse
CREATE USER danube_reader IDENTIFIED BY '...' SETTINGS readonly = 2;
GRANT SELECT ON events.* TO danube_reader;

-- Trino (file-based access control: a rule for the user)
-- {"catalog": "hive", "user": "danube", "allow": "read-only"}
```

BigQuery: grant the service account `roles/bigquery.dataViewer` on the dataset and `roles/bigquery.jobUser` on the project.

## Drivers in the agent image

The published image ships no database driver. Build your own from `dataplane-agent/` with the engines you need, comma-separated:

```bash theme={null}
docker build \
  --build-arg DANUBE_DB_ENGINES=postgres,snowflake \
  -t registry.example.com/danube-dataplane:0.3.0 \
  dataplane-agent
```

`install_drivers.sh` installs the pinned lines from `requirements-drivers.txt` for those engines and nothing else; an engine that is not built in fails a query with `driver_missing`. `postgres` and `redshift` share one driver.

## Connect

From [Dashboard > Connect a system](https://danubeai.com/dashboard/connect), choose **Database**, the organization, the engine and the reference. **Preview** runs discovery through the agent and lists the tables and columns it found; **Connect** creates the service. Over the API:

```bash theme={null}
curl -X POST "https://api.danubeai.com/v1/organizations/ORG_ID/connect" \
  -H "danube-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "database",
    "name": "Warehouse",
    "description": "Read-only questions against the analytics warehouse",
    "database": {"db_type": "snowflake", "connection_ref": "env://ANALYTICS_DSN", "schema": "ANALYTICS"},
    "dry_run": true
  }'
```

The service is `local_only` and gets three `method: SQL` tools, none with a template of its own:

| Tool                         | Parameters                          | What it does                                                                                         |
| ---------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `Warehouse - List tables`    | `schema` (optional)                 | Lists the tables the policy allows, with their schema                                                |
| `Warehouse - Describe table` | `table` (`schema.table` or `table`) | The columns and types of one allowed table; masked columns are marked                                |
| `Warehouse - Query`          | `sql`                               | Runs one read-only `SELECT` the agent wrote, on allowed tables only, capped at `max_rows` and masked |

Discovery reads every non-system schema (or the one in `database.schema`; on BigQuery a dataset is required) through the agent and stores the tables and columns on the service as `metadata.discovery.tables`, capped at 500 tables and 200 columns per table. With no agent online the service is still created and `discovered.error` is `no_dataplane_agent`; start the agent, then **Discover again** on the service page (`POST /v1/organizations/{org_id}/connect/{service_id}/discover`).

### Allowlist and masking

The policy starts with **no table allowed**: a Query on any table answers `permission_denied` until you list some. On the service page, pick tables from what discovery found, or over the API:

```bash theme={null}
curl -X PUT "https://api.danubeai.com/v1/organizations/ORG_ID/services/SERVICE_ID/policy" \
  -H "danube-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sql": {
      "allowed_tables": ["public.customers", "public.orders"],
      "denied_tables": [],
      "masked_columns": ["customers.email", "customers.phone"],
      "max_rows": 500
    }
  }'
```

`allowed_tables` and `denied_tables` take `schema.table` or `table`, case-insensitive; a deny wins over an allow, and **List tables** shows only what is allowed. `masked_columns` (`table.column` or `column`) come back as `[REDACTED:<column>]` in every row and are marked in **Describe table**. `max_rows` caps every call at 1,000 or less. A statement that names a table outside the allowlist, or anything but a single `SELECT`, is refused with `error_type: permission_denied`, `fault: caller`, and the message names the table; the agent skill tells agents to read that as policy, not as an outage.

### Promote a statement

Once an agent has run a statement worth keeping, turn it into a named tool from the [Audit Log](https://danubeai.com/dashboard/organization/audit) (**Promote to tool** on the `tool.execute` row) or over the API, with the execution id or the statement itself:

```bash theme={null}
curl -X POST "https://api.danubeai.com/v1/organizations/ORG_ID/services/SERVICE_ID/promote" \
  -H "danube-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Customers by region",
    "sql": {
      "sql": "SELECT id, name, last_seen FROM customers WHERE region = :region ORDER BY last_seen DESC LIMIT 50",
      "parameters": [{"name": "region", "type": "string", "description": "Region code"}]
    }
  }'
```

The named tool is a `method: SQL` row at `/queries/<slug>` carrying the engine, the reference and the template, exactly like a template written by hand, and it is not subject to the allowlist: it does what its template says.

## Templates (advanced)

A connector that declares its own query templates is still registered through `POST /v1/organizations/{org_id}/services` with `source: database`, or from the [Connector Builder](https://danubeai.com/dashboard/tools/connector). Each template becomes one tool and no generic tools are created:

```bash theme={null}
curl -X POST "https://api.danubeai.com/v1/organizations/ORG_ID/services" \
  -H "danube-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "database",
    "visibility": "local_only",
    "database": {
      "name": "Warehouse",
      "description": "Read-only questions against the analytics warehouse",
      "db_type": "snowflake",
      "connection_ref": "env://ANALYTICS_DSN",
      "max_rows": 200,
      "queries": [
        {
          "name": "Customers by region",
          "description": "Customers in one region",
          "sql": "SELECT id, name, last_seen FROM customers WHERE region = :region ORDER BY last_seen DESC LIMIT 50",
          "parameters": [{"name": "region", "type": "string", "required": true}]
        }
      ]
    },
    "dry_run": true
  }'
```

Each query becomes a `method: SQL` tool at `/queries/<slug>` with the engine, the reference and the template in its metadata. A parameter used in the template and not declared is reported as a warning: an agent will not know to send it, and a call without it fails with `bad_request: missing parameter`.

## Limits

* One read statement per template. Comments, `;`, dollar quotes and `#` are refused.
* At most 1,000 rows per call (`max_rows` on the connector lowers it); `truncated: true` in the result says more matched.
* A 30 second statement timeout by default (`timeout_seconds`, up to 55 inside the 60 second dispatch budget) and a 10 second connect timeout.
* Cells are returned JSON-safe: dates as ISO 8601, decimals as numbers, bytes as base64. Credential-shaped values in rows are masked like any other tool result.
* `POST /v1/connectors/database/test`, `/introspect`, `/infer` and `/dry-run` connect from the control plane and are for self-hosted deployments; for a `local_only` connector they answer `400` and the test is a tool run after registration.

## Troubleshooting

| `error_type`                      | `fault`  | Meaning                                                                                                   | Fix                                                                                                   |
| --------------------------------- | -------- | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `dataplane_required`              | caller   | A database tool that is not `local_only` on the hosted service                                            | Register it for the organization as `local_only`                                                      |
| `no_dataplane_agent`              | caller   | No agent of the organization has polled in the last two minutes                                           | Start or enroll an agent; for a connect made while no agent was online, **Discover again** afterwards |
| `permission_denied`               | caller   | The statement names a table outside `allowed_tables` (or in `denied_tables`), or is not a single `SELECT` | Add the table to the policy on the service page, or promote the statement as a named tool             |
| `driver_missing`                  | caller   | The agent image was built without this engine's driver                                                    | Rebuild with `DANUBE_DB_ENGINES=<engine>`                                                             |
| `credential_reference_unresolved` | caller   | The `env://` or `vault://` reference could not be resolved on the agent's host                            | Set the variable on the agent, or check `VAULT_ADDR`, `VAULT_TOKEN` and the `#<field>` suffix         |
| `auth_required`                   | caller   | The database refused the credentials in the connection string                                             | Check the user, password, role or token in the referenced value                                       |
| `bad_request`                     | caller   | The template failed validation, a placeholder had no value, or the engine rejected the query              | Read the message; the engine's own text is passed through                                             |
| `timeout`                         | upstream | The statement timeout elapsed                                                                             | Narrow the query or raise `timeout_seconds`                                                           |
| `connection_error`                | upstream | The agent could not reach the database                                                                    | Network path, TLS and the host in the connection string                                               |
