# Bulk import computed tables from CSV

Source: https://userguide.qualytics.io/cli/examples/bulk-import-computed-tables/

## Goal

Register many computed tables (SQL-defined virtual tables) at once from a CSV or XLSX file. Common scenario: the customer has a library of dozens of metric definitions, business rules, or anomaly-detection queries that they currently run as ad-hoc SQL, and they want each of them registered as a Qualytics computed table with an associated quality check.

## Permissions

| Step | Endpoint | Role | Team permission |
| --- | --- | --- | --- |
| Create computed container | `POST /api/containers` | `Member` | `Editor` on the datastore's team |
| Create satisfiesExpression check (per row) | `POST /api/quality-checks` | `Member` | `Author` |
| Profile the new container | `POST /api/operations/run` (profile) | `Member` | `Editor` |

> **Warning: Profile must complete before checks become useful**
>
> The CLI waits for the post-import profile to finish by default. If you skip the wait with `--skip-profile-wait`, the checks are still created but will fail until profiling completes.

## Prerequisites

- The CLI is installed and authenticated.
- A datastore exists where the computed tables will live.
- A CSV, XLSX, or TXT file with three columns: `name`, `description`, `query`. Header row required.

```text
name,description,query
high_value_orders,Orders over $10k,SELECT * FROM orders WHERE total > 10000
recent_signups,Customers from the last 30 days,SELECT * FROM customers WHERE created_at > NOW() - INTERVAL '30 days'
churned_customers,Customers inactive for 90+ days,SELECT * FROM customers WHERE last_seen < NOW() - INTERVAL '90 days'
```

## CLI workflow

```mermaid
graph LR
    File[CSV / XLSX] --> Preview[containers preview]
    Preview --> Import[containers import]
    Import --> CT[(Computed tables)]
    Import --> Profile[Auto profile]
    Profile --> Checks[(satisfiesExpression checks)]
```

### 1. Preview without writing

```bash
qualytics containers preview --input ./computed_tables.csv
```

The CLI parses the file, applies the prefix, normalizes the queries, and shows what it would create, without making any API calls.

### 2. Import

```bash
qualytics containers import \
    --datastore 42 \
    --input ./computed_tables.csv \
    --prefix "metric_" \
    --as-active \
    --tags "metrics,dashboard"
```

Each row becomes:
- A computed table named `metric_<name>` in datastore 42.
- A `satisfiesExpression` check that fails when the query returns any rows (so `query` is meant to express "rows that should not exist").

### 3. (Optional) Skip checks

If you want the computed tables but not the auto-generated checks:

```bash
qualytics containers import --datastore 42 --input ./computed_tables.csv --skip-checks
```

## Behind the scenes

| CLI step | Method | Path | Notes |
| --- | --- | --- | --- |
| Parse the file | (local) | — | Validates header row, applies prefix, adds aliases to SELECT columns missing them. |
| Skip existing | GET | `/api/containers?datastore_id={id}` | The CLI lists existing containers and skips rows whose name (after prefix) already exists. |
| Create computed table | POST | `/api/containers` | One per new row. Type is `computed_table`. |
| Trigger profile | POST | `/api/operations/run` (profile) | One profile call covering the new containers. |
| Wait for profile | GET | `/api/operations/{id}` | Polls until the profile finishes (unless `--skip-profile-wait`). |
| Create check (per row) | POST | `/api/quality-checks` | One satisfiesExpression check per new computed table. |

## Python equivalent

```python
import csv
import os
import time
import httpx

BASE_URL     = os.environ["QUALYTICS_URL"].rstrip("/")
TOKEN        = os.environ["QUALYTICS_TOKEN"]
HEADERS      = {"Authorization": f"Bearer {TOKEN}"}
DATASTORE_ID = 42
PREFIX       = "metric_"

with httpx.Client(headers=HEADERS, timeout=60.0) as client:
    # Read the CSV
    with open("./computed_tables.csv") as f:
        reader = csv.DictReader(f)
        rows = list(reader)

    # Create computed tables
    created_ids = []
    for row in rows:
        body = {
            "type":         "computed_table",
            "name":         f"{PREFIX}{row['name']}",
            "datastore_id": DATASTORE_ID,
            "query":        row["query"],
            "description":  row.get("description"),
            "tags":         ["metrics", "dashboard"],
        }
        r = client.post(f"{BASE_URL}/api/containers", json=body)
        r.raise_for_status()
        created_ids.append(r.json()["id"])

    # Profile the new containers and wait
    r = client.post(f"{BASE_URL}/api/operations/run", json={
        "type":          "profile",
        "datastore_ids": [DATASTORE_ID],
        "container_ids": created_ids,
    })
    r.raise_for_status()
    op_id = r.json()["id"]
    while True:
        op = client.get(f"{BASE_URL}/api/operations/{op_id}").json()
        if op["result"] in ("success", "failure", "aborted"):
            break
        time.sleep(10)

    # Create one satisfiesExpression check per container
    for row, cid in zip(rows, created_ids):
        client.post(f"{BASE_URL}/api/quality-checks", json={
            "container_id": cid,
            "rule_type":    "satisfiesExpression",
            "fields":       [],
            "properties":   {"expression": "1=0"},  # fail if any rows present
            "description":  row.get("description"),
            "status":       "Active",
        }).raise_for_status()
```

## Variations and advanced usage

### Use TXT or XLSX

```bash
qualytics containers import --datastore 42 --input ./computed_tables.tsv --delimiter $'\t'
qualytics containers import --datastore 42 --input ./computed_tables.xlsx
```

### Create as drafts, review later

```bash
qualytics containers import --datastore 42 --input ./computed_tables.csv --as-draft
# After review:
qualytics checks list --datastore-id 42 --status Draft
qualytics checks activate --ids "501,502,503"
```

### Debug logs for stuck imports

```bash
qualytics containers import --datastore 42 --input ./computed_tables.csv --debug
# Logs land in ~/.qualytics/logs/
```

### Re-run safely

The CLI skips computed tables whose name (after prefix) already exists. Re-running on the same CSV won't duplicate.

## Troubleshooting

| Symptom | Likely cause | Fix |
| --- | --- | --- |
| `columns missing aliases` warning | The SELECT has unnamed expressions | The CLI adds `expr_1`, `expr_2`, ... aliases automatically; the warning is informational. |
| Profile never finishes | Source database is slow or query is heavy | Run with `--skip-profile-wait` and poll `qualytics operations list` separately. |
| Some checks fail to create | The corresponding computed table was skipped (already existed), so the check check would be a duplicate | Check the import summary; the CLI logs what was skipped. |
| Wrong delimiter detected | TXT files default to `\t`; CSV defaults to `,` | Pass `--delimiter` explicitly. |
| Header row is missing or in wrong position | The CLI assumes first row is a header | Add the header row (`name,description,query`). |

## Related

- [Containers command reference](https://userguide.qualytics.io/cli/containers/): every flag for `containers import`, `preview`, `validate`.
- [Build a computed join](https://userguide.qualytics.io/cli/examples/build-computed-join/): for joining two existing containers without writing SQL.
- [Bulk-create quality checks](https://userguide.qualytics.io/cli/examples/bulk-create-quality-checks/): the regular path for non-computed-table checks.

*Last updated: May 12, 2026.*
