Saved queries¶
The saved-queries API lets you register a query dataset at runtime without
editing datasets.toml. Useful for ad-hoc analysis, temporary datasets with
a TTL, or gradually promoting exploratory queries to config-file permanence.
All three routes are admin-gated: they require the X-Admin-Token header
(same token as POST .../reload), or — when the auth feature is enabled —
a bearer token that carries the datasets:manage scope.
Routes¶
POST /api/v1/queries — create¶
Request body
{
"name": "tx_severe",
"sql": "SELECT * FROM accidents WHERE state = 'TX' AND severity >= 3",
"kind": "temp",
"ttl": "2h",
"refresh": { "interval": "15m", "on_upstream_reload": true },
"materialize": { "residency": "auto", "sort_by": ["state"] },
"index": { "mode": "auto" }
}
| Field | Required | Default | Notes |
|---|---|---|---|
name |
yes | — | Dataset name. Must be unique across all registered datasets. 409 on conflict. The name "reload-all" is reserved and is rejected. |
sql |
yes | — | A single read-only SELECT (or WITH … SELECT). The same validator as POST /api/v1/sql applies. |
kind |
no | "temp" |
"temp" (ephemeral) or "query" (persisted). |
ttl |
no | (unset) | Humantime duration. Temp datasets only. After expiry the dataset is automatically unregistered. Absent = lives until restart or explicit delete. |
refresh |
no | (unset) | Same schema as [dataset.refresh] in datasets.toml. Accepted for both kinds. |
materialize |
no | (unset) | Same schema as [dataset.materialize]. |
index |
no | (unset) | DataFusion equality-index policy. Rejected if residency = "lazy". |
depends_on is inferred — the server parses the SQL, determines every
referenced dataset, and returns the list in the response. You do not supply it.
Query parameters
| Param | Default | Notes |
|---|---|---|
?async |
false |
When true, return 202 Accepted immediately with the dataset in building state rather than waiting for the build to complete. Poll GET /api/v1/datasets/{name}/status for the transition to published. |
Success response (sync 200)
{
"name": "tx_severe",
"kind": "temp",
"depends_on": ["accidents"],
"state": "published",
"rows": 12345
}
Success response (async 202)
Error responses
| Code | Reason |
|---|---|
400 |
Invalid SQL, unknown table reference, or invalid field value. |
401 / 403 |
Missing or invalid auth token / scope. |
409 |
Name conflict with an existing dataset. |
GET /api/v1/queries — list¶
Returns all datasets created via the saved-queries API (both temp and
query kinds).
Response
[
{
"name": "tx_severe",
"kind": "temp",
"depends_on": ["accidents"],
"state": "published"
},
{
"name": "daily_counts",
"kind": "query",
"depends_on": ["accidents"],
"state": "published"
}
]
DELETE /api/v1/queries/{name} — delete¶
curl -s -X DELETE \
-H "X-Admin-Token: $ADMIN_TOKEN" \
http://localhost:8080/api/v1/queries/tx_severe
| Code | Reason |
|---|---|
200 |
Dataset unregistered (and storage generations removed for storage-backed datasets). |
403 |
The named dataset was not created via POST /api/v1/queries — config-file datasets cannot be deleted at runtime. |
404 |
Dataset not found. |
409 |
Other registered datasets depend on this one. The body lists the dependents. |
In-flight queries finish against their captured generation snapshot (Arc
semantics for DataFusion; DuckDB DROP TABLE runs after the registry entry
is removed). Storage generations are wiped from disk as part of unregister.
kind = "temp" — ephemeral datasets¶
A temp dataset lives entirely in memory and is not written to any file.
It disappears on server restart.
# Create a temp dataset that expires in 2 hours
curl -s -X POST \
-H "X-Admin-Token: $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"recent_accidents","sql":"SELECT * FROM accidents WHERE year = 2023","kind":"temp","ttl":"2h"}' \
http://localhost:8080/api/v1/queries
When ttl is set, the scheduler fires a one-shot expiry at the deadline.
Expiry behaves identically to DELETE /api/v1/queries/{name}: the dataset
is unregistered and its storage tree (if any) is wiped. A 409 at expiry
time (dependents exist) is logged as a WARN — the dataset survives.
ttl is only meaningful for kind = "temp". Setting it on kind = "query"
is a 400 error.
kind = "query" — persisted datasets¶
A query dataset is written to a TOML file under the managed directory so
it survives server restart.
Default managed directory: <config_dir>/datasets.d/ — a sibling
directory next to the main datasets.toml. Override with:
At startup DataPress loads all *.toml files from this directory after
datasets.toml. A name collision between the two sources fails startup.
The persisted TOML has the same schema as a [[dataset]] entry plus an
internal managed = true marker. The user's datasets.toml is never
rewritten.
# Create a persisted saved query
curl -s -X POST \
-H "X-Admin-Token: $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "daily_counts",
"sql": "SELECT state, COUNT(*) AS n FROM accidents GROUP BY state",
"kind": "query",
"refresh": {"interval":"1h","on_upstream_reload":true}
}' \
http://localhost:8080/api/v1/queries | jq
Bulk reload — POST /api/v1/datasets/reload-all¶
Enqueues every reloadable dataset for refresh in a single wave:
curl -s -X POST \
-H "X-Admin-Token: $ADMIN_TOKEN" \
http://localhost:8080/api/v1/datasets/reload-all | jq
Response 202 Accepted
skipped contains datasets that are already building. Datasets in
pending state with on_start = "lazy" or on_start = "skip" are excluded
(nothing to reload). Dependents within the wave absorb their upstreams'
cascade trigger via debounce — no dataset builds twice within one wave.
The reload-all dataset name is reserved and rejected at config validation.
Auth¶
| Scenario | Required |
|---|---|
ADMIN_TOKEN set, auth feature off |
X-Admin-Token: $ADMIN_TOKEN header |
auth feature on, OIDC configured |
Bearer token with datasets:manage scope |
| Neither token set, no OIDC | Routes return 404 (not even their existence is revealed) |
The datasets:manage scope is separate from datasets:reload — users with
read or reload access cannot create or delete runtime datasets.
Explorer integration¶
When DataPress is built with --features explorer, the browser UI gains:
- Save as dataset action on any query result — opens a form to create a
temporquerydataset from the current SQL. The server re-validates before building. The inferreddepends_onlist is shown for confirmation. - State badges in the dataset list showing
pending / building / published / failedin real time (pollsGET .../status). - Per-dataset reload button calling
POST /api/v1/datasets/{name}/reload. - Reload all button with a confirmation dialog calling
POST /api/v1/datasets/reload-all. - Delete action on runtime-created (
managed) datasets, surfacing409errors verbatim if dependents exist.
The save/delete actions are hidden entirely when the saved-queries routes
return 404 (auth not configured or feature not compiled in).
Interaction with the dependency graph¶
Runtime datasets participate fully in the dependency graph:
- A
querydataset created viaPOST /api/v1/queriesmay list config-file datasets in itsdepends_on(inferred automatically). - Config-file datasets cannot
depends_ona runtime dataset (config is validated before runtime state exists). on_upstream_reload = truein therefreshblock triggers cascade exactly as for a config-file query dataset.
Deleting a dataset that other registered datasets depend on returns 409
with the list of dependents. The dataset survives.
[server] saved_queries_dir¶
[server]
saved_queries_dir = "/var/lib/datapress/saved" # absolute path
# or relative to the config file directory:
saved_queries_dir = "datasets.d"
| Value | Behaviour |
|---|---|
| (unset) | Default: <config_dir>/datasets.d/ |
| absolute path | Used as-is. Directory is created on first write if absent. |
| relative path | Resolved relative to the directory containing datasets.toml. |