Skip to content

Commit caba0f3

Browse files
authored
Merge pull request #84 from hotdata-dev/openapi-update-28839841740
feat: async table loads + database-scoped results/query runs
2 parents 55c73dd + 431c195 commit caba0f3

34 files changed

Lines changed: 438 additions & 96 deletions

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- feat: support async table loads and append mode
13+
- **Breaking:** results and query runs are now scoped to a database via the
14+
required `X-Database-Id` header. The ergonomic wrappers gain a `database_id`
15+
argument to match: `Client::get_result`, `Client::list_results`,
16+
`Client::list_query_runs`, `Client::await_result`, `Client::get_result_arrow`,
17+
`Client::stream_result_arrow`, `Client::query_to_arrow`, and the
18+
`results()` / `query_runs()` resource handles. `Client::query`'s truncation
19+
auto-follow now forwards the query's database scope (the `X-Database-Id`
20+
header, or the request-body `database_id` when no header is set) to the
21+
follow-up result and query-run fetches.
22+
1023
## [0.7.0] - 2026-06-30
1124

1225
### Added

README.md

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
6464
// .base_url("https://api.hotdata.dev") // optional
6565
.build()?;
6666

67-
// Submit a query. `query` transparently retries HTTP 429 (server overload)
67+
// Queries, results, and query runs are scoped to a database (the required
68+
// `X-Database-Id` header), so pick one first.
69+
let database_id = "your_database_id";
70+
71+
// Submit a query. `query_in` transparently retries HTTP 429 (server overload)
6872
// and auto-follows a truncated large result to its full row set. Rows come
6973
// back inline, plus a result_id persisted for later retrieval.
7074
let response = client
71-
.query(QueryRequest::new("SELECT 1 AS n".to_string()))
75+
.query_in(QueryRequest::new("SELECT 1 AS n".to_string()), database_id)
7276
.await?;
7377

7478
if let Some(result_id) = response.result_id.flatten() {
7579
// Poll the persisted result to `ready` without hand-rolling a loop.
76-
let result = client.await_result(&result_id, PollConfig::default()).await?;
80+
let result = client
81+
.await_result(&result_id, database_id, PollConfig::default())
82+
.await?;
7783
println!("result {} is {}", result.result_id, result.status);
7884
}
7985

@@ -91,7 +97,8 @@ ergonomic, workspace-scoped handles so you never pass a `Configuration` around:
9197
let connections = client.connections().list().await?;
9298
let connection = client.connections().get(&connections.connections[0].id).await?;
9399
let secrets = client.secrets().list().await?;
94-
let runs = client.query_runs().list(Some(50), None, None, None).await?;
100+
// Query runs are database-scoped: pass the database id first.
101+
let runs = client.query_runs().list(database_id, Some(50), None, None, None).await?;
95102
```
96103

97104
Handles exist for every resource — `connections`, `connection_types`,
@@ -101,7 +108,9 @@ Handles exist for every resource — `connections`, `connection_types`,
101108
operations also have flat shortcuts directly on `Client` (`query` — with
102109
`query_in` to scope to a database, `query_preview` to skip auto-follow, and
103110
`query_with` for a per-call `QueryConfig` — plus `get_result`, `list_results`,
104-
`list_query_runs`, `list_workspaces`).
111+
`list_query_runs`, `list_workspaces`). The result and query-run operations take
112+
a `database_id` argument — they are scoped to a database via the required
113+
`X-Database-Id` header.
105114

106115
For anything not yet wrapped, the full generated surface is one call away via
107116
`client.configuration()`:
@@ -120,12 +129,12 @@ them with the typed [`ResultStatus`] / [`QueryRunStatus`] enums via the
120129
```rust
121130
use hotdata::prelude::*;
122131

123-
let result = client.await_result(&result_id, PollConfig::default()).await?;
132+
let result = client.await_result(&result_id, database_id, PollConfig::default()).await?;
124133
if result.result_status().is_ready() {
125134
// ...
126135
}
127136

128-
let run = client.query_runs().get(&query_run_id).await?;
137+
let run = client.query_runs().get(&query_run_id, database_id).await?;
129138
if run.run_status().is_terminal() { /* ... */ }
130139
```
131140

@@ -183,16 +192,19 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
183192
.workspace_id("your_workspace_id")
184193
.build()?;
185194

195+
// Results are database-scoped (the required `X-Database-Id` header).
196+
let database_id = "your_database_id";
197+
186198
// Buffered: decodes every batch into a Vec<RecordBatch>.
187-
let result = client.get_result_arrow(&result_id, None, None).await?;
199+
let result = client.get_result_arrow(&result_id, database_id, None, None).await?;
188200
println!("schema: {:?}", result.schema);
189201
println!("total rows: {:?}", result.total_row_count);
190202
for batch in &result.batches {
191203
// work with each arrow_array::RecordBatch
192204
}
193205

194206
// Streaming: yields batches lazily without holding them all at once.
195-
let mut stream = client.stream_result_arrow(&result_id, None, None).await?;
207+
let mut stream = client.stream_result_arrow(&result_id, database_id, None, None).await?;
196208
for batch in stream.by_ref() {
197209
let batch = batch?;
198210
// ...
@@ -202,7 +214,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
202214
}
203215
```
204216

205-
Both methods accept `offset` and `limit` for pagination, and both honor the transparent JWT exchange. They return `ArrowError::NotReady` if the result is still pending or processing — poll `client.get_result(result_id)` until its status is `ready` first. `ArrowResult` also surfaces the `X-Total-Row-Count` header (`total_row_count`) and the `rel="next"` pagination `Link` (`next_link`).
217+
Both methods accept `offset` and `limit` for pagination, and both honor the transparent JWT exchange. They return `ArrowError::NotReady` if the result is still pending or processing — poll `client.get_result(result_id, database_id)` until its status is `ready` first. `ArrowResult` also surfaces the `X-Total-Row-Count` header (`total_row_count`) and the `rel="next"` pagination `Link` (`next_link`).
206218

207219
To run a query and get its result as Arrow in a single call — submit, await
208220
`ready`, and decode — use `query_to_arrow`:
@@ -211,6 +223,7 @@ To run a query and get its result as Arrow in a single call — submit, await
211223
let arrow = client
212224
.query_to_arrow(
213225
QueryRequest::new("SELECT * FROM big_table".to_string()),
226+
database_id,
214227
PollConfig::default(),
215228
None, // offset
216229
None, // limit

docs/AsyncQueryResponse.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Name | Type | Description | Notes
77
**query_run_id** | **String** | Unique identifier for the query run. |
88
**reason** | Option<**String**> | Human-readable reason why the query went async (e.g., caching tables for the first time). | [optional]
99
**status** | **String** | Current status of the query run. |
10-
**status_url** | **String** | URL to poll for query run status. |
10+
**status_url** | **String** | URL to poll for query run status. Requires the same `X-Database-Id` header used to submit the query. |
1111

1212
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
1313

docs/ConnectionsApi.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ This endpoint does not need any parameter.
298298
> models::LoadManagedTableResponse load_managed_table(connection_id, schema, table, load_managed_table_request)
299299
Load managed table from upload
300300

301-
Publish a previously-uploaded file as the new contents of a managed table. CSV, JSON, and Parquet uploads are supported; the format is auto-detected from the upload's `Content-Type` and file contents, or set explicitly via the `format` field. Only `mode = \"replace\"` is supported. Concurrent loads against the same upload return 409.
301+
Publish a previously-uploaded file as the new contents of a managed table. CSV, JSON, and Parquet uploads are supported; the format is auto-detected from the upload's `Content-Type` and file contents, or set explicitly via the `format` field. If the target table (or its schema) has not been declared yet, it is created automatically as part of the load — declaring tables up front is optional. `mode` selects how the upload is applied: `replace` overwrites the table's contents, `append` inserts the uploaded rows on top of the existing data. Concurrent loads against the same upload return 409. Set `async` to run the load in the background and get back a job ID to poll; add `async_after_ms` to wait briefly for it to finish before falling back to a job ID.
302302

303303
### Parameters
304304

docs/CreateIndexRequest.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
Name | Type | Description | Notes
66
------------ | ------------- | ------------- | -------------
77
**r#async** | Option<**bool**> | When true, create the index as a background job and return a job ID for polling. | [optional]
8+
**async_after_ms** | Option<**i32**> | If set (requires `async` = true), wait up to this many milliseconds for the index build to finish: if it completes in time the index is returned (201), otherwise a 202 with a job ID to poll. Must be between 1000 and the server maximum; a value out of that range, or set without `async` = true, is rejected with 400. | [optional]
89
**columns** | **Vec<String>** | Columns to index. Required for all index types. |
910
**description** | Option<**String**> | User-facing description of the embedding (e.g., \"product descriptions\"). | [optional]
1011
**dimensions** | Option<**i32**> | Output vector dimensions. Some models support multiple dimension sizes (e.g., OpenAI text-embedding-3-small supports 512 or 1536). If omitted, the model's default dimensions are used | [optional]

docs/DatabasesApi.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ This endpoint does not need any parameter.
259259
> models::LoadManagedTableResponse load_database_table(database_id, schema, table, load_managed_table_request)
260260
Load database table from upload
261261

262-
Publish a previously-uploaded file as the new contents of a table on the database's default catalog. The database-scoped equivalent of the connection-scoped managed-table load — addressed by `database_id`, so no `default_connection_id` is needed. CSV, JSON, and Parquet uploads are supported; the format is auto-detected or set via `format`. Only `mode = \"replace\"` is supported. Concurrent loads against the same upload return 409.
262+
Publish a previously-uploaded file as the new contents of a table on the database's default catalog. The database-scoped equivalent of the connection-scoped managed-table load — addressed by `database_id`, so no `default_connection_id` is needed. CSV, JSON, and Parquet uploads are supported; the format is auto-detected or set via `format`. If the target table (or its schema) has not been declared yet, it is created automatically as part of the load — declaring tables up front is optional. `mode` selects how the upload is applied: `replace` overwrites the table's contents, `append` inserts the uploaded rows on top of the existing data. Concurrent loads against the same upload return 409. Set `async` to run the load in the background and get back a job ID to poll; add `async_after_ms` to wait briefly for it to finish before falling back to a job ID.
263263

264264
### Parameters
265265

docs/JobResult.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
|---- | -----|
77
| ConnectionRefreshResult | Job-specific result payload. The shape depends on the job type. Null while the job is pending or running. |
88
| IndexInfoResponse | Job-specific result payload. The shape depends on the job type. Null while the job is pending or running. |
9+
| LoadManagedTableResponse | Job-specific result payload. The shape depends on the job type. Null while the job is pending or running. |
910
| TableRefreshResult | Job-specific result payload. The shape depends on the job type. Null while the job is pending or running. |
1011

1112
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

docs/LoadManagedTableRequest.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
Name | Type | Description | Notes
66
------------ | ------------- | ------------- | -------------
7+
**r#async** | Option<**bool**> | When true, run the load as a background job and return a job ID to poll instead of blocking until it finishes. Recommended for large uploads, which can take longer than an HTTP request should stay open. | [optional]
8+
**async_after_ms** | Option<**i32**> | If set (requires `async` = true), wait up to this many milliseconds for the load to finish: if it completes in time the full result is returned (200), otherwise a 202 with a job ID to poll. Must be between 1000 and the server maximum; a value out of that range, or set without `async` = true, is rejected with 400. | [optional]
79
**format** | Option<**String**> | File format of the upload: `\"csv\"`, `\"json\"`, or `\"parquet\"`. Optional — when omitted, the format is auto-detected from the upload's `Content-Type` and, failing that, from the file contents. Provide it explicitly to override detection or when the contents are ambiguous. `\"json\"` expects newline-delimited JSON (one object per line), not a JSON array. | [optional]
8-
**mode** | **String** | Load mode. Only `\"replace\"` is supported in this release. |
10+
**mode** | **String** | How the upload is applied: `\"replace\"` overwrites the table's contents, `\"append\"` inserts the uploaded rows on top of the existing data. |
911
**upload_id** | **String** | ID of a previously-staged upload (see `POST /v1/files`). The upload is claimed atomically; concurrent loads against the same `upload_id` return 409. |
1012

1113
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

docs/LoadManagedTableResponse.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Name | Type | Description | Notes
66
------------ | ------------- | ------------- | -------------
77
**arrow_schema_json** | **String** | Schema of the loaded table, as JSON. |
88
**connection_id** | **String** | |
9-
**row_count** | **i64** | Total rows in the published parquet file. |
9+
**row_count** | **i64** | Total number of rows in the table after the load. |
1010
**schema_name** | **String** | |
1111
**table_name** | **String** | |
1212

docs/QueryRequest.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
Name | Type | Description | Notes
66
------------ | ------------- | ------------- | -------------
77
**r#async** | Option<**bool**> | When true, execute the query asynchronously and return a query run ID for polling via GET /query-runs/{id}. The query results can be retrieved via GET /results/{id} once the query run status is \"succeeded\". | [optional]
8-
**async_after_ms** | Option<**i32**> | If set with async=true, wait up to this many milliseconds for the query to complete synchronously before returning an async response. Minimum 1000ms. Ignored if async is false. | [optional]
8+
**async_after_ms** | Option<**i32**> | If set (requires `async` = true), first attempt the query synchronously and wait up to this many milliseconds: if it finishes in time the full result is returned, otherwise an async response (a run id to poll) is returned. Must be at least 1000 and at most the server's configured maximum; a value out of that range, or set without `async` = true, is rejected with 400. | [optional]
99
**database_id** | Option<**String**> | Database to scope the query to (its id). Alternative to the `X-Database-Id` header — exactly one source must be provided. If both this field and the header are set and they disagree, the request is rejected with a 400. | [optional]
1010
**default_catalog** | Option<**String**> | Catalog that unqualified table references resolve against within the query's database scope. Must name a catalog visible in the database (`default`, an attached catalog alias, or a system catalog). Defaults to `default` when omitted. | [optional]
1111
**default_schema** | Option<**String**> | Schema that unqualified table references resolve against within the query's database scope. Defaults to `main` when omitted. Existence is not validated up front — an unknown schema surfaces as a \"table not found\" error at planning time. | [optional]

0 commit comments

Comments
 (0)