# Saved Queries Source: https://www.hotdata.dev/docs/api-reference/saved-queries Site index: https://www.hotdata.dev/llms.txt Save, version, and execute named SQL queries. Each update creates a new version, preserving the full history. Saved queries are automatically classified by category (e.g., aggregation, join, filtered scan) and can be executed by ID. ## List saved queries `GET /v1/queries` **Query parameters** - `limit` `integer` — Maximum number of results - `offset` `integer` — Pagination offset **Response** `200` — List of saved queries - `count` `integer` — **required**. Min: `0` - `has_more` `boolean` — **required** - `limit` `integer` — **required**. Min: `0` - `offset` `integer` — **required**. Min: `0` - `queries` `SavedQuerySummary`[] — **required** - `created_at` `string` — **required** - `description` `string` — **required** - `id` `string` — **required** - `latest_version` `integer` — **required** - `name` `string` — **required** - `tags` `string`[] — **required** - `updated_at` `string` — **required** ```json { "count": 0, "has_more": true, "limit": 0, "offset": 0, "queries": [ { "created_at": "2026-01-01T00:00:00Z", "description": "string", "id": "string", "latest_version": 0, "name": "string", "tags": [ "string" ], "updated_at": "2026-01-01T00:00:00Z" } ] } ``` ## Create saved query `POST /v1/queries` Save a named SQL query. The SQL is stored as version 1 and automatically analyzed for classification metadata (category, table count, predicate/join/aggregation flags). **Request body** - `description` `string,null` - `name` `string` — **required** - `sql` `string` — **required** - `tags` `string`[] | `null` ```json { "description": "Ten highest-spending customers by order total", "name": "top-customers", "sql": "SELECT customer_id, sum(amount) AS total FROM orders GROUP BY customer_id ORDER BY total DESC LIMIT 10", "tags": [ "sales", "weekly" ] } ``` **Response** `201` — Saved query created - `category` `string,null` - `created_at` `string` — **required** - `description` `string` — **required** - `has_aggregation` `boolean,null` - `has_group_by` `boolean,null` - `has_join` `boolean,null` - `has_limit` `boolean,null` - `has_order_by` `boolean,null` - `has_predicate` `boolean,null` - `id` `string` — **required** - `latest_version` `integer` — **required** - `name` `string` — **required** - `num_tables` `integer,null` - `sql` `string` — **required** - `sql_hash` `string` — **required** - `table_size` `string,null` - `tags` `string`[] — **required** - `updated_at` `string` — **required** ```json { "category": "string", "created_at": "2026-01-01T00:00:00Z", "description": "string", "has_aggregation": true, "has_group_by": true, "has_join": true, "has_limit": true, "has_order_by": true, "has_predicate": true, "id": "string", "latest_version": 0, "name": "string", "num_tables": 0, "sql": "string", "sql_hash": "string", "table_size": "string", "tags": [ "string" ], "updated_at": "2026-01-01T00:00:00Z" } ``` **Errors** | Status | Description | | ------ | ----------- | | `400` | Invalid request | ## Get saved query `GET /v1/queries/{id}` **Path parameters** - `id` `string` — **required**. Saved query ID **Response** `200` — Saved query details - `category` `string,null` - `created_at` `string` — **required** - `description` `string` — **required** - `has_aggregation` `boolean,null` - `has_group_by` `boolean,null` - `has_join` `boolean,null` - `has_limit` `boolean,null` - `has_order_by` `boolean,null` - `has_predicate` `boolean,null` - `id` `string` — **required** - `latest_version` `integer` — **required** - `name` `string` — **required** - `num_tables` `integer,null` - `sql` `string` — **required** - `sql_hash` `string` — **required** - `table_size` `string,null` - `tags` `string`[] — **required** - `updated_at` `string` — **required** ```json { "category": "string", "created_at": "2026-01-01T00:00:00Z", "description": "string", "has_aggregation": true, "has_group_by": true, "has_join": true, "has_limit": true, "has_order_by": true, "has_predicate": true, "id": "string", "latest_version": 0, "name": "string", "num_tables": 0, "sql": "string", "sql_hash": "string", "table_size": "string", "tags": [ "string" ], "updated_at": "2026-01-01T00:00:00Z" } ``` **Errors** | Status | Description | | ------ | ----------- | | `404` | Saved query not found | ## Update saved query `PUT /v1/queries/{id}` Update a saved query. If the SQL changes, a new version is created (previous versions are preserved). Name, tags, description, and classification overrides can also be updated. **Path parameters** - `id` `string` — **required**. Saved query ID **Request body** - `category_override` `string,null` — Override the auto-detected category. Send `null` to clear (revert to auto). - `description` `string,null` - `name` `string,null` — Optional new name. When omitted the existing name is preserved. - `sql` `string,null` — Optional new SQL. When omitted the existing SQL is preserved. - `table_size_override` `string,null` — User annotation for table size. Send `null` to clear. - `tags` `string`[] | `null` All fields are optional. Send only the ones you want to set. **Response** `200` — Saved query updated - `category` `string,null` - `created_at` `string` — **required** - `description` `string` — **required** - `has_aggregation` `boolean,null` - `has_group_by` `boolean,null` - `has_join` `boolean,null` - `has_limit` `boolean,null` - `has_order_by` `boolean,null` - `has_predicate` `boolean,null` - `id` `string` — **required** - `latest_version` `integer` — **required** - `name` `string` — **required** - `num_tables` `integer,null` - `sql` `string` — **required** - `sql_hash` `string` — **required** - `table_size` `string,null` - `tags` `string`[] — **required** - `updated_at` `string` — **required** ```json { "category": "string", "created_at": "2026-01-01T00:00:00Z", "description": "string", "has_aggregation": true, "has_group_by": true, "has_join": true, "has_limit": true, "has_order_by": true, "has_predicate": true, "id": "string", "latest_version": 0, "name": "string", "num_tables": 0, "sql": "string", "sql_hash": "string", "table_size": "string", "tags": [ "string" ], "updated_at": "2026-01-01T00:00:00Z" } ``` **Errors** | Status | Description | | ------ | ----------- | | `400` | Invalid request | | `404` | Saved query not found | ## Delete saved query `DELETE /v1/queries/{id}` **Path parameters** - `id` `string` — **required**. Saved query ID **Response** `204` — Saved query deleted **Errors** | Status | Description | | ------ | ----------- | | `404` | Saved query not found | ## Execute saved query `POST /v1/queries/{id}/execute` Execute a saved query, scoped to a database (required `X-Database-Id` header). By default runs the latest version. Optionally specify a version number to execute a previous version. The SQL runs inside the given database scope, the same way POST /v1/query does. Returns the same response format as POST /v1/query. **Path parameters** - `id` `string` — **required**. Saved query ID **Headers** - `X-Database-Id` `string` — **required**. Required. Scope execution to this database (its id). A missing or malformed value is a 400; an unknown database id is a 404. **Request body** - `version` `integer,null` All fields are optional. Send only the ones you want to set. **Response** `200` — Query executed - `columns` `string`[] — **required** - `execution_time_ms` `integer` — **required**. Min: `0` - `nullable` `boolean`[] — **required**. Nullable flags for each column (parallel to columns vec). True if the column allows NULL values, false if NOT NULL. - `preview_row_count` `integer` — **required**. Number of rows in *this* response body. Always present. For a large result this is a bounded preview, not the grand total — see `total_row_count` and `truncated`. - `query_run_id` `string` — **required**. Unique identifier for the query run record (qrun...). - `result_id` `string,null` — Unique identifier for retrieving this result via GET /results/{id}. When non-null, the result is being persisted asynchronously. Null only when the result fit entirely in this response (`truncated: false`) but could not be persisted for later retrieval — see the `warning` field. A `truncated: true` response ALWAYS carries a non-null, resolvable `result_id`: a truncated result that cannot be persisted fails the request with a retryable HTTP 503 (`PERSISTENCE_UNAVAILABLE`, with a `Retry-After` header) rather than returning a partial body with a dead ticket. - `row_count` `integer` — **required**. **Deprecated** — use `preview_row_count` (rows in this body) and `total_row_count` (grand total) instead. Retained as a back-compat alias and always equal to `preview_row_count`; for a truncated result it is the preview count, *not* the grand total — read `total_row_count` for that. Will be removed in a future release once clients migrate. Min: `0` - `rows` `any`[][] — **required**. Array of rows, where each row is an array of column values. Values can be strings, numbers, booleans, or null. - `total_row_count` `integer,null` — Grand total rows in the full result. Present (and equal to `preview_row_count`) when the whole result fit in this response; `null` while a truncated result is still being persisted. When `null`, read the authoritative total from `GET /v1/query-runs/{id}` (`row_count`) or the `X-Total-Row-Count` header on `GET /v1/results/{id}`. - `truncated` `boolean` — **required**. True when `rows` is a bounded preview of a larger result. Fetch the full result via `result_id`. - `warning` `string,null` — Warning message if result persistence could not be initiated. Present only when the full result is returned inline (`truncated: false`) but could not be persisted: `result_id` is then null and the result cannot be re-fetched later, though every row is in this response. A truncated result never carries a warning — if it cannot be persisted the request fails with a retryable HTTP 503 (`PERSISTENCE_UNAVAILABLE`, with a `Retry-After` header) instead. ```json { "columns": [ "string" ], "execution_time_ms": 0, "nullable": [ true ], "preview_row_count": 0, "query_run_id": "string", "result_id": "string", "row_count": 0, "rows": [ [ null ] ], "total_row_count": 0, "truncated": true, "warning": "string" } ``` **Errors** | Status | Description | | ------ | ----------- | | `400` | Invalid request (including a missing X-Database-Id header) | | `404` | Saved query or database not found | ## List saved query versions `GET /v1/queries/{id}/versions` **Path parameters** - `id` `string` — **required**. Saved query ID **Query parameters** - `limit` `integer` — Maximum number of versions - `offset` `integer` — Pagination offset **Response** `200` — List of versions - `count` `integer` — **required**. Min: `0` - `has_more` `boolean` — **required** - `limit` `integer` — **required**. Min: `0` - `offset` `integer` — **required**. Min: `0` - `saved_query_id` `string` — **required** - `versions` `SavedQueryVersionInfo`[] — **required** - `category` `string,null` - `created_at` `string` — **required** - `has_aggregation` `boolean,null` - `has_group_by` `boolean,null` - `has_join` `boolean,null` - `has_limit` `boolean,null` - `has_order_by` `boolean,null` - `has_predicate` `boolean,null` - `num_tables` `integer,null` - `sql` `string` — **required** - `sql_hash` `string` — **required** - `table_size` `string,null` - `version` `integer` — **required** ```json { "count": 0, "has_more": true, "limit": 0, "offset": 0, "saved_query_id": "string", "versions": [ { "category": "string", "created_at": "2026-01-01T00:00:00Z", "has_aggregation": true, "has_group_by": true, "has_join": true, "has_limit": true, "has_order_by": true, "has_predicate": true, "num_tables": 0, "sql": "string", "sql_hash": "string", "table_size": "string", "version": 0 } ] } ``` **Errors** | Status | Description | | ------ | ----------- | | `404` | Saved query not found |