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

# Query Provider Pools with SQL

> Create a SQL token, connect DBeaver or Python, and run your first query against your Workspace's Provider Pools.

## Sources

Docs: [Flight SQL](/protocols/sql/flight-sql.md) · [Provider Pool](/protocols/sql/data-catalog/provider-pool.md) · [API tokens](/protocols/workspace/authentication#api-tokens)

Web App: **Protection → Access Tokens** in your Workspace.

This guide takes you from no access to a first query result against the data
your Providers sync into Parable. You create a token in your Workspace, connect
DBeaver or Python to Parable's SQL endpoint, and query the Provider Pools with
read-only SQL.

You need:

* a Workspace with at least one connected Provider;
* a Workspace Administrator who can give you a role;
* DBeaver, or Python 3.10 or later.

Every connection uses these values:

| Setting | Value |
| - | - |
| Host | `sql.parable.work` |
| Port | `443`, with TLS |
| Token | A SQL token you create in [step 2](#2-create-a-sql-token). It starts with `pat_`. |
| Workspace | Your Workspace slug ([step 3](#3-find-your-workspace-slug)), sent as `x-tenant`. |

## 1. Get the Provider Pools Reader role

Ask a Workspace Administrator to give you the **Provider Pools Reader** role,
described as "Query the Provider Pools over SQL". It lets you query the
Provider Pools over SQL, and nothing else. You find tables with SQL
([step 7](#7-find-tables-and-columns)).

The administrator:

1. Opens **People → Users**.
2. Opens the actions on your row and chooses **Edit Roles**.
3. Selects **Provider Pools Reader** and chooses **Save**.

The **Data Access** and **Workspace Administrator** roles also let you query,
and they reach the Workspace's other pools too. A member who holds only
**Viewer** cannot query.

The same roles decide SQL that runs inside your Workspace and names no
Parable, such as SQL Ponder runs for you outside a Parable: Provider Pools
Reader reads the Provider Pools, Data Access and Workspace Administrator read
every pool, and Viewer alone reads nothing. You do not need Viewer beside the
data role for that SQL to run. SQL inside a Parable, such as a Plot's Preview
or Preview Report, follows the Parable's rule instead: it needs a seat on the
Parable and a role that opens Parables, which Provider Pools Reader is not,
and it reads the Parable's Plots; only someone who can edit the Parable's
Plots reads every pool there.

## 2. Create a SQL token

1. Sign in to your Workspace and open **Protection → Access Tokens**. Its
   address is `https://<workspace-slug>.parable.work/platform/protection/access-tokens`.
   A Provider Pools Reader or a Data Access member lands there when they
   open the Workspace.
2. In **Your tokens**, choose **Create token**.
3. Under **Use it for**, keep **SQL**. The other choice, **MCP**, creates a
   token for an MCP host; SQL refuses it.
4. Enter a **Name** that says where you will use it, such as
   `DBeaver on my laptop`.
5. Under **Expires in**, choose 30 days, 90 days, 180 days, or 1 year. The
   default is 90 days.
6. Choose **Create token**.
7. Copy the token and store it in your password manager. Parable shows it
   once and cannot show it again. Then choose **I have saved it**.

The token:

* works only with SQL, and only in the Workspace where you created it;
* runs each query with your roles at that moment, so it stops working when
  your role or your membership is removed;
* stops working when it expires or is revoked. After a revocation or a
  removed role, SQL can keep answering for up to 30 seconds.

To revoke a token, open **Protection → Access Tokens**, open the token's
actions in **Your tokens**, and choose **Revoke token**. Workspace
Administrators see every member's token under **Members' tokens** and can
revoke any of them. People who manage the Workspace's sign-in can turn tokens
off; while they are off, every token is refused and nobody can create one.

Treat the token like a password. Create one token for each tool or script, so
you can revoke one without breaking the others.

## 3. Find your Workspace slug

The slug is the first part of your Workspace's web address. For
`https://acme.parable.work`, the slug is `acme`. Your SQL
tool sends it as `x-tenant` on every call. A token works only with the slug of
the Workspace that created it.

## 4. Connect DBeaver

DBeaver connects through the Apache Arrow Flight SQL JDBC driver. Other JDBC
tools, such as DataGrip, use the same driver, class, URL, and properties.

### Run DBeaver on Java 21

Recent DBeaver releases run on Java 25. The driver does not support Java 25,
and on it **Test Connection** fails with `ExceptionInInitializerError` and
`Could not initialize class org.apache.arrow.memory.RootAllocator`. Point
DBeaver at Java 21 before you add the driver:

1. Install Java 21, for example Eclipse Temurin 21 from
   [Adoptium](https://adoptium.net/temurin/releases/?version=21), or run
   `brew install openjdk@21` on macOS.

2. Quit DBeaver.

3. Open `dbeaver.ini` in a text editor. It is in the DBeaver installation:

   | System | File |
   | - | - |
   | macOS | `/Applications/DBeaver.app/Contents/Eclipse/dbeaver.ini` |
   | Windows | `C:\Program Files\DBeaver\dbeaver.ini` |
   | Linux | `/usr/share/dbeaver-ce/dbeaver.ini` |

4. Find the line that reads `-vm`. Replace the line below it with the path to
   Java 21's `java` program (`javaw.exe` on Windows). If the file has no `-vm`
   line, add `-vm` and the path as two lines at the top of the file. Both
   lines must come before `-vmargs`. With Homebrew on macOS, this command
   prints the path:

   ```bash theme={null}
   echo "$(brew --prefix openjdk@21)/libexec/openjdk.jdk/Contents/Home/bin/java"
   ```

5. On the line after `-vmargs`, add
   `--add-opens=java.base/java.nio=ALL-UNNAMED` if the file does not already
   have it.

6. Save the file and start DBeaver.

The start of the file then looks like this, with your own Java path:

```text dbeaver.ini theme={null}
-vm
/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home/bin/java
```

A DBeaver upgrade replaces `dbeaver.ini`, so repeat these steps after each
upgrade.

### Add the driver

1. Download the `flight-sql-jdbc-driver` jar, version 18 or 19, from
   [Maven Central](https://central.sonatype.com/artifact/org.apache.arrow/flight-sql-jdbc-driver).

2. In DBeaver, open **Database → Driver Manager** and choose **New**.

3. On **Settings**, enter:

   | Field | Value |
   | - | - |
   | Driver Name | `Arrow Flight SQL` |
   | Class Name | `org.apache.arrow.driver.jdbc.ArrowFlightJdbcDriver` |
   | URL Template | `jdbc:arrow-flight-sql://{host}:{port}` |
   | Default Port | `443` |

4. On **Libraries**, choose **Add File** and pick the jar. If DBeaver offers a
   list of driver classes, choose `org.apache.arrow.driver.jdbc.ArrowFlightJdbcDriver`.

5. Choose **OK**.

### Create the connection

1. Choose **Database → New Database Connection**, pick **Arrow Flight SQL**,
   and choose **Next**.

2. Set **Host** to `sql.parable.work` and **Port** to `443`. Leave
   **Username** and **Password** empty.

3. Open **Driver properties**. Right-click in the list, choose
   **Add new property**, and add these two properties:

   | Property | Value |
   | - | - |
   | `token` | Your SQL token, `pat_...`, without `Bearer` |
   | `x-tenant` | Your Workspace slug |

4. Choose **Test Connection**. DBeaver reports that it is connected.

5. Choose **Finish**.

The driver uses TLS by default. Do not set `useEncryption=false`.

## 5. Connect with Python

Install the ADBC Flight SQL driver and PyArrow:

```bash theme={null}
pip install adbc-driver-flightsql pyarrow
```

Put your token and Workspace slug in environment variables, so the token stays
out of your code:

```bash theme={null}
export PARABLE_SQL_TOKEN='pat_...'
export PARABLE_WORKSPACE='acme'
```

Save this script as `first_query.py`:

```python first_query.py theme={null}
import os

import adbc_driver_flightsql.dbapi as flightsql

url = os.environ.get("PARABLE_SQL_URL", "grpc+tls://sql.parable.work:443")
token = os.environ["PARABLE_SQL_TOKEN"]
workspace = os.environ["PARABLE_WORKSPACE"]

with flightsql.connect(
    url,
    db_kwargs={
        "adbc.flight.sql.authorization_header": f"Bearer {token}",
        "adbc.flight.sql.rpc.call_header.x-tenant": workspace,
    },
    autocommit=True,
) as connection:
    with connection.cursor() as cursor:
        cursor.execute(
            """
            SELECT table_schema, table_name
            FROM information_schema.tables
            WHERE table_catalog = 'providers'
              AND table_schema <> 'information_schema'
            ORDER BY table_schema, table_name
            """
        )
        tables = cursor.fetchall()
        for schema, table in tables:
            print(f"providers.{schema}.{table}")

        if not tables:
            print("No Provider tables yet. Connect a Provider and let it sync.")
        else:
            schema, table = tables[0]
            cursor.execute(f"SELECT * FROM providers.{schema}.{table} LIMIT 10")
            print(cursor.fetch_arrow_table())
```

Run it:

```bash theme={null}
python first_query.py
```

It prints the address of every Provider table you can query, then the first 10
rows of the first one.

`grpc+tls://` connects with TLS. ADBC requires header names in lowercase, so
write `x-tenant` exactly as shown. `autocommit=True` tells the driver not to
open a transaction, since SQL access is read-only and has none.

## 6. Run a first query

Every Provider table has a three-part address:

```text theme={null}
providers.{connector}.{table}
```

`{connector}` is the Provider's technical name, such as `google` or `github`,
and `{table}` is one of its tables, such as `users`. There is no `USE`
statement, so always write the full address.

In DBeaver, select the connection and open **SQL Editor → New SQL script**. In
Python, pass the SQL to `cursor.execute`. Run:

```sql theme={null}
SELECT *
FROM providers.<connector>.<table>
LIMIT 10
```

Replace `<connector>` and `<table>` with a table you can query, for example
`providers.google.users`. DBeaver's Database Navigator lists them under the
`providers` catalog, and the Python script above prints them.

A table can exist before its Provider's first sync writes rows. An empty
result, or the error `No data has been written for this table yet`, means the
table has no data yet, not that your connection is wrong.

## 7. Find tables and columns

List a Provider's tables and their columns with SQL. A table's address, such as
`providers.google.users`, is its catalog, schema and name:

```sql theme={null}
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_catalog = 'providers'
  AND table_schema = 'google'
  AND table_name = 'users'
ORDER BY ordinal_position
```

To find which tables hold some kind of data, search by meaning with
[`search_available_data`](/protocols/sql/data-catalog/search-available-data).
To find the records themselves, search with
[`search_everything`](/protocols/sql/data-catalog/search-everything).
[Discover tables and columns](/protocols/sql/data-catalog/discover-tables-and-columns.md)
covers the other discovery calls.

## What you can query

The Provider Pools Reader role reaches three catalogs:

| Catalog | Holds |
| - | - |
| `providers` | Your Providers' data, at `providers.{connector}.{table}`. See [Provider Pool](/protocols/sql/data-catalog/provider-pool.md). |
| `providers_quarantine` | Records a Provider sent that failed validation, under the same `{connector}.{table}` names. `_parable_raw` holds the record as it arrived, and `_parable_validation` says why it failed. |
| `providers_quality` | The health of each Provider table. See [Provider quality](/protocols/sql/data-catalog/provider-quality.md). |

The Workspace's other pools, `workspace`, `artifacts`, and `parables`, need the
Data Access role. With Provider Pools Reader, a query that names them fails
with `Query references an unknown relation`.

Within these catalogs, the Workspace's
[Provider Policies](/protection/provider-policies.md) decide which rows of each
Provider table your roles read. A table hidden from your roles reads as empty,
and its quarantine and quality tables follow it. If your roles hold
[All data access](/protection/provider-policies.md#all-data-access), as
Workspace Administrator does, you read every row, and each query is recorded
in the Workspace's audit events.

## System columns

Every table in the `providers` catalog has these columns beside the
Provider's own fields:

| Column | Meaning |
| - | - |
| `dt` | The load day that last wrote the row. |
| `_parable_observed` | `true` when the latest load contained the row. |
| `_parable_last_observed_at` | When a load last contained the row. |
| `_parable_deleted` | `true` when the Provider reported the row deleted, or a full load no longer contained it. |
| `_parable_deleted_at` | When the row was first marked deleted. |
| `_parable_seal_id` | The load that last wrote the row. |
| `_parable_event_date` | Event tables laid out by their event time only: the UTC day of the row's event time, which the table is partitioned by. Filter on it to read whole days, for example `WHERE _parable_event_date = DATE '2026-10-01'`. Null when the row has no event time. |

A row is written only when a load changes it, so `WHERE dt = '<day>'` returns
the rows that changed that day, not the whole table. To read current records,
read the whole table and filter on the state columns:

```sql theme={null}
SELECT *
FROM providers.google.users
WHERE _parable_deleted = false
  AND _parable_observed = true
```

[Read row state](/protocols/sql/data-catalog/provider-pool.md#read-row-state)
explains these columns in more detail.

## Limits

* **Read-only.** Only `SELECT` statements run. Any other statement fails with
  `Only read-only SELECT queries are supported`. There are no transactions.
* **Plain `EXPLAIN`.** `EXPLAIN` followed by a `SELECT` returns the query's
  plan and runs nothing: one `logical_plan` row and one `physical_plan` row.
  The physical plan names each step, its partitions and its keys. It shows no
  storage path and no filter condition, because a filter can hold a data
  policy. `EXPLAIN ANALYZE` runs the query, so it fails with error code
  `QL-SQ-008`. `EXPLAIN VERBOSE` and the other forms fail with `QL-SQ-009`.
* **One hour per query.** The hour covers planning and the whole result,
  including the time your tool spends reading it. A longer query fails with
  `DEADLINE_EXCEEDED`, error code `QL-TO-002`, and
  `query <id> exceeded timeout of 3600s`. Set your tool's own timeout to at
  least an hour if you run long queries.
* **2,000,000 rows per result.** A query whose result has more rows fails
  with `RESOURCE_EXHAUSTED` and error code `QL-SQ-005`; nothing is cut off
  silently. The error can arrive after some rows, so discard any rows your tool
  read before it. Add `LIMIT`, filter with `WHERE`, or aggregate in SQL. To read
  more rows than that, run one query per range of a column, such as one month
  at a time.
* **Paid AI calls.** One query may send up to 1,000,000 calls of `ponder()`,
  `embed()` and the Jev functions, and spend up to \$1,000 on them. A call that
  would pass a limit is not sent, and the query fails with
  `RESOURCE_EXHAUSTED` and error code `QL-SQ-010` (calls) or `QL-SQ-011`
  (cost). Call the function over fewer rows, or publish the SQL as a Plot.
  See the [AI call limits](/protocols/sql/flight-sql.md#ai-call-limits).
* **Shared memory.** Queries that run at the same time can share
  memory and temporary disk. A query that asks for more than is
  left fails with `RESOURCE_EXHAUSTED` and error code `QL-IN-009`, whether its
  own size or the queries beside it used the space. Provider tables are
  partitioned by `dt`, the day a load wrote the row, so a `WHERE dt` bound
  skips the files of other days. Grouping, `DISTINCT`, joins, and sorts over
  every day of a large table need the most memory: bound `dt` first, and run
  large queries one at a time rather than together.
* **600 requests a minute from one IP address.** A query takes at least two
  requests, and DBeaver sends more while you browse. Requests over the limit
  are refused with HTTP status 429, which most tools report as `UNAVAILABLE`.
* **The Provider Pools only.** The Provider Pools Reader role reaches the three
  catalogs above and nothing else.
* **One Workspace per token.** To query another Workspace, create a token
  there.

## ODBC tools

Parable has no ODBC driver. Tools that need one, such as Power BI and
Tableau, are not supported. Use DBeaver, another JDBC tool, or Python.

## Troubleshooting

Tools show an error as a gRPC status and a message, such as
`UNAUTHENTICATED: token expired`, and each tool wraps them in its own words.
Python ADBC, for example, reports `PERMISSION_DENIED: insufficient permissions`
as `UNAUTHORIZED: [FlightSQL] insufficient permissions (PermissionDenied; Prepare)`.
Match on the message.

| Error | Cause | Fix |
| - | - | - |
| `ExceptionInInitializerError`, `Could not initialize class org.apache.arrow.memory.RootAllocator` | DBeaver runs on Java 25. | [Run DBeaver on Java 21](#run-dbeaver-on-java-21). |
| `UNAUTHENTICATED: authentication required` | The tool sent no token. | Set the `token` driver property, or the `authorization_header` option in Python. |
| `UNAUTHENTICATED: invalid token` | The token was not copied whole, or Parable does not know it. | Copy the token again, or [create a new one](#2-create-a-sql-token). |
| `UNAUTHENTICATED: token expired` | The token passed its expiry date. | Create a new token. |
| `UNAUTHENTICATED: token has been revoked` | You or an administrator revoked it. | Create a new token. |
| `UNAUTHENTICATED: user not found` | Your Parable account was deactivated. | Ask a Workspace Administrator. |
| `INVALID_ARGUMENT: X-Tenant header required` | The tool sent no Workspace. | Add the `x-tenant` property with your Workspace slug. |
| `NOT_FOUND: tenant not found` | No Workspace has that slug. | [Check the slug](#3-find-your-workspace-slug). |
| `PERMISSION_DENIED: insufficient permissions` | Your role in that Workspace cannot query, or you are not a member of it. | Ask for the [Provider Pools Reader role](#1-get-the-provider-pools-reader-role), and check the slug. |
| `PERMISSION_DENIED: tenant not in token scope` | The token was created in another Workspace. | Create a token in the Workspace you want to query. |
| `PERMISSION_DENIED: personal access tokens are turned off for this Workspace` | Someone who manages the Workspace's sign-in turned tokens off. | Ask them to turn tokens back on in **Protection → Access Tokens**. |
| **Create token** is missing, or the page says `You do not have access to personal access tokens in this Workspace` | Your role cannot query, or tokens are turned off. | Ask for the Provider Pools Reader role, or ask for tokens to be turned on. |
| `Query references an unknown relation` | The address is misspelled, the table does not exist, or it is outside the Provider Pools. | List the tables in `information_schema.tables` (step 7). Other pools need the Data Access role. |
| `Query references an unknown column "…"` | The named column is misspelled. | List the table's columns in `information_schema.columns` (step 7). Names are lowercase, and nested fields use `__`, such as `assignee__email`. |
| `Column "…" in SELECT must be in GROUP BY or an aggregate function` | The query groups rows and selects the named column without grouping or aggregating it. The clause can also be `HAVING`, `QUALIFY` or `ORDER BY`. | Add the column to `GROUP BY`, or aggregate it, such as `max(title)`. |
| `A column in SELECT that a subquery computed without an alias must be in GROUP BY or an aggregate function` | The same mistake on a subquery column that has no name of its own, such as `SELECT t.*, count(*) FROM (SELECT 'open', id FROM …) AS t GROUP BY t.id`. | Name the expression in the subquery with `AS`, then add it to `GROUP BY` or aggregate it. |
| `Query references a struct field that does not exist` | The field is misspelled, or it has capital letters and is not quoted: SQL lowercases `profile.userType` to `profile.usertype`. | Double-quote the field, such as `profile."userType"`. See [Column names](/protocols/sql/sql-behavior/fully-qualified-names.md#column-names). |
| `Query calls function "…", which does not exist` | Parable's SQL has no function of that name. The message names the closest one when there is one. | Use the suggested function or an equivalent. For `date_diff`, subtract `extract(epoch FROM …)` values. For `approx_percentile(x, 0.5)`, use `approx_median(x)`. |
| `No data has been written for this table yet` | The Provider has not synced this table yet. | Try again after its first sync. |
| `Only read-only SELECT queries are supported` | The statement writes or changes data. | Run `SELECT` statements only. |
| `EXPLAIN ANALYZE runs the query, so it is not supported. Use EXPLAIN.` | `EXPLAIN ANALYZE` would run the query. | Remove `ANALYZE` and run `EXPLAIN` again. |
| `Only plain EXPLAIN followed by a query is supported` | The statement uses `EXPLAIN VERBOSE`, a `FORMAT`, or another form of `EXPLAIN`. | Write `EXPLAIN` followed by the `SELECT`, with no other option. |
| `UNIMPLEMENTED` when DBeaver connects or runs a query | DBeaver tried to start a transaction. | Turn on auto-commit for the connection. |
| `DEADLINE_EXCEEDED: query <id> exceeded timeout of 3600s` | The query ran longer than an hour. | Filter or aggregate more, or split the query. |
| `RESOURCE_EXHAUSTED: Query concurrency limit reached; retry after a short backoff` | Too many queries are running at once. | Wait a few seconds and try again. |
| `RESOURCE_EXHAUSTED: Query ran out of the memory or temporary disk space that running queries share.` | Your query and the others running at the same time needed more memory or temporary disk space than they share. | Wait a few seconds and try again once. If it fails again, bound `dt`, add `LIMIT`, filter with `WHERE`, or sort, join, and group fewer rows at once, and do not run it beside other large queries. Discard any rows read before the error. |
| `UNAVAILABLE` with HTTP status 429 | You sent more than 600 requests in a minute. | Wait a minute, and send fewer requests. |
| `UNAVAILABLE: This table is temporarily unavailable` | Parable could not read the table's storage. | Try again. |
| `RESOURCE_EXHAUSTED: Query result has more than 2,000,000 rows, the most one query can return.` | The result is larger than one query may return. | Add `LIMIT`, filter with `WHERE`, or aggregate. Discard any rows read before the error. |
| **Test Connection** hangs or is refused | The host or port is wrong, or encryption is off. | Use `sql.parable.work`, port `443`, and leave `useEncryption` unset. |


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