All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- feat(indexes): add vector_precision field to index operations
open_result_arrowandArrowResultStream: fetch a result as an Arrow IPC stream decoded directly off the socket, so peak memory is one record batch rather than the whole result.get_result_arrowandstream_result_arrowboth collect the entire body first and are unchanged. Also available asClient::open_result_arrow.
- Query results no longer round wide numbers. A JSON number in a result row
was parsed into a
serde_json::Value, which has no arbitrary-precision number: anything needing more than ~17 significant digits — aDECIMAL(38,2)at full width, for instance — arrived already rounded, so a value the service sent as99999999999999999999.99reached callers as1e20. Rows now carry each cell's JSON text.
- Breaking: result rows are
Vec<Vec<JsonCell>>, notVec<Vec<serde_json::Value>>.QueryResponse::rowsandGetResultResponse::rows(andQueryResponse::new) carry the newJsonCell, which holds a cell's JSON text and serializes back to exactly those bytes. Read a cell withas_json_str()(lossless),as_str(),as_array(),is_null()orkind();to_value()returns the oldserde_json::Value, with the old rounding, for code not ready to move. Comparisons against a literal becomeJsonCell::from(serde_json::json!(...)).serde_jsonis the only supported format for aJsonCell— it round-trips through aRawValue, which other serde formats do not recognise.
arrowfeature: arrow 55 -> 59. Breaking for callers of that feature:ArrowResulthands backRecordBatchandSchemaRef, so a crate naming those types must move to arrow 59 too, or the two majors will not compile together. Without the feature, nothing changes.
- Fork lineage.
databases().lineage(database_id, forks_limit)wraps the newGET /v1/databases/{database_id}/lineage— a database's ancestor chain and the databases forked directly from it (DatabaseLineageResponse,LineageAncestorInfo,LineageForkInfo). Database create/fork/get/list responses carry an optionalforked_from: ForkedFromInforecording the source's id, its label at fork time, the copiedsnapshot_id, andforked_at. Forks created before the server recorded lineage carry none. databases().lookup_by_name(name)wrapsGET /v1/databases/by-name: fetch a database by its exact name — 404 when none, 409 when the name is shared.CreateDatabaseRequest.if_not_existsand theCreateDatabaseResponse.createdflag it reports through;GetResultResponse.total_row_count; optionalkey_determineson managed-table declarations.
- Fork naming: an omitted fork
namenow defaults to the source's label plus a short suffix derived from the fork's own id, so the two stay distinguishable. - docs: adopt "instant database" terminology in doc comments and test comments;
public API names (e.g.
load_managed_table,source_type: managed) are unchanged.
- Breaking: the Secrets, Refresh, and Connection Types endpoints were retired
from the HotData API and are gone from the SDK:
client.secrets(),client.refresh(),client.connection_types(), theSecretsApi,RefreshApi, andConnectionTypesApihandles, the underlyingapis::secrets_api,apis::refresh_api, andapis::connection_types_apimodules, and their request/response models. - Breaking:
connections().check_health/connections_api::check_connection_healthandConnectionHealthResponseare removed. - Breaking: the table/connection cache-purge endpoints are gone:
connections().purge_cache,connections().purge_table_cache, and the generatedconnections_api::purge_connection_cache/connections_api::purge_table_cachefunctions. - Breaking: the
JobType::DataRefreshTable/JobType::DataRefreshConnectionandJobResult::TableRefreshResult/JobResult::ConnectionRefreshResultvariants are removed. NoteJobResult::default()now producesIndexInfoResponse(previouslyTableRefreshResult).
- feat(query): add dialect parameter to query request
-
The pluggable per-request bearer hook is back, for consumers that own their own credential lifecycle.
Configuration::token_providertakes anOption<Arc<dyn hotdata::auth::BearerTokenProvider>>, andConfiguration::resolve_bearer_tokenresolves the credential once per request — every generated op plus the hand-writtensubmit_query,upload_file(create-session and finalize), and Arrow fetch. A host whose access token is short-lived (e.g. the CLI's PKCE browser-login session, which expires in minutes) can now refresh mid-command instead of 401ing on a long call — a multi-gigabyte upload whose finalize lands after the TTL, a slow query, a large parallel batch. The trait's error type isBearerTokenError(Transport/Status/Malformed,#[non_exhaustive]); a provider that returns an error is logged via thelogfacade and the request proceeds unauthenticated; that warning is the only trace of the cause, and it requires alogimplementation installed in the host binary to be visible."Per request" includes each attempt of a 429 retry chain: the retry helper re-resolves rather than replaying the
Authorizationheader from the first attempt, which could otherwise outlive a short-lived token (the retry deadline defaults to 120s and an honoredRetry-Afteris uncapped). Presigned storagePUTs are excluded by construction — they authorize via the signed URL and must never carry a bearer.Additive and non-breaking.
bearer_access_tokenkeeps working exactly as it did in 0.12.0 when no provider is installed, andClientBuilder::api_tokenstill installs the API token there — no caller needs to change anything.This does not reintroduce the API-token to JWT exchange removed in 0.12.0.
TokenManager, the/v1/auth/jwtcall,ClientBuilder::client_id,PersistCallback, andHOTDATA_DISABLE_JWT_EXCHANGEstay removed. A provider is a hook, not an exchange: the SDK never trades one credential for another.
- Breaking: the API-token to JWT key exchange is deprecated and removed.
Clients now authenticate with the API token itself, sent verbatim as
Authorization: Bearer <token>; the SDK no longer calls/v1/auth/jwt, mints or refreshes short-lived JWTs, or holds a refresh token. This drops the wholehotdata::authmodule (TokenManager,TokenManagerOptions,BearerTokenProvider,TokenExchangeError,PersistCallback,CLIENT_ID), theConfiguration::token_providerfield, theConfiguration::resolve_bearer_tokenmethod,ClientBuilder::client_id, and theHOTDATA_DISABLE_JWT_EXCHANGEopt-out.ClientBuilder::api_tokennow installs the token asConfiguration::bearer_access_token; callers that only used the builder need no changes. Code that installed a customBearerTokenProvidershould setbearer_access_tokendirectly instead.
- feat(loads): add idempotency_key to load requests
- feat(loads): support inline csv data in table load requests
- Breaking: session scoping is gone from the API.
ClientBuilder::session_id, theclient::SESSION_ID_HEADERconstant, and theHOTDATA_SESSION_IDenvironment variable are removed, and no request sends theX-Session-Idheader any more. Drop the.session_id(..)builder call; nothing replaces it.
- feat(databases): add bulk operations and count endpoint
- Breaking:
list_databasesandDatabases::listtake abatchparameter, narrowing the listing to one bulk-creation batch; existing callers must pass an extraNone. Databases::countwrapsGET /v1/databases/countand returns the number of databases in the workspace across every page. Note this is not the listing'scountfield, which reports how many rows a single page returned — totalling a workspace from the listing means walking every page, and pages cap at 100.- chore: clarify sort and partition semantics in table schemas
- feat(tables): add partition_by and sorted_by configuration
QueryRunInfo::user_public_idnow reports the caller's stable account id (the access token's subject) instead of a fingerprint of the bearer token, which churned every few minutes as short-lived JWTs were reminted. Grouping a caller's query history by this field now holds across token refreshes. A request that carries no verified subject still records an opaqueuser_-prefixed fingerprint — stable for that credential, but not resolvable to an account. This is a server-side change; the SDK only carries the field.- feat(databases): add search parameter to list endpoint
- Breaking:
list_databasesandDatabases::listnow takelimitandcursorpagination parameters; existing callers must passNone, None. - Pagination metadata on
ListDatabasesResponse(count,limit,has_more) is now nullable so the client tolerates responses from a server that predates these fields (rolling deploy / version skew).
- Breaking: the
POST /v1/filesandGET /v1/filesendpoints are gone in favor of the presigned uploads flow. This drops the generatedupload_file/list_uploadsops, theUploadResponse/UploadInfo/ListUploadsResponsemodels, and the ergonomicClient::upload_stream,uploads().upload, anduploads().listwrappers. UseClient::upload_file(presigned direct-to-storage) instead.
LoadManagedTableRequestgains an optionalkeyfield naming the key columns fordelete/update/upsertloads.
- Ergonomic
Client::databases().fork()wrapper for thefork_databaseendpoint.
- Breaking: databases now report the schema that unqualified table names
resolve to, as a required
default_schemafield onDatabaseSummary,DatabaseDetailResponse, andCreateDatabaseResponse. Because the field is required, each type'snew()gained adefault_schemaparameter:DatabaseSummary::new(default_catalog, default_schema, id),DatabaseDetailResponse::new(attachments, default_catalog, default_connection_id, default_schema, id), andCreateDatabaseResponse::new(default_catalog, default_connection_id, default_schema, id). Callers that construct these types (or match on them exhaustively) must supply it; callers that only read them are unaffected. - feat(databases): add fork endpoint
- chore(api): exclude datasets from public OpenAPI spec + docs cleanup
CreateDatabaseRequestgains an optionaldefault_schemafield, naming the schema unqualified table names resolve to inside the new database's query scope. When omitted, a database declaring exactly one schema adopts that schema; otherwise unqualified names resolve tomain.AddManagedTableDecl,AddManagedTableRequest, andDatabaseDefaultTableDeclgain an optionalkeyfield naming the columns that uniquely identify a row. Declaring a key enables the key-based load modes (delete,update,upsert) on that table, which match rows by those columns; a table declared without one accepts onlyreplaceandappend.
- Breaking:
LoadManagedTableRequestcan now load from a persisted query result as well as an upload, soupload_idbecame optional and moved out of the constructor:new(mode, upload_id)is nownew(mode). Setupload_idorresult_idon the returned value — exactly one is required. Note this shipped in a patch release, which Cargo treats as compatible with 0.8.0 for a0.xcrate, so it could surface oncargo updaterather than an explicit upgrade. - feat(tables): support loading from query results
- feat(databases): expose created_at on list and detail endpoints
- feat: support async table loads and append mode
- Breaking: results and query runs are now scoped to a database via the
required
X-Database-Idheader. The ergonomic wrappers gain adatabase_idargument to match:Client::get_result,Client::list_results,Client::list_query_runs,Client::await_result,Client::get_result_arrow,Client::stream_result_arrow,Client::query_to_arrow, and theresults()/query_runs()resource handles.Client::query's truncation auto-follow now forwards the query's database scope (theX-Database-Idheader, or the request-bodydatabase_idwhen no header is set) to the follow-up result and query-run fetches.
- Multipart uploads now survive transient per-part failures. A single part exhausting its inner transport retries no longer aborts the whole transfer: an outer round loop re-sweeps just the failed parts (at decaying concurrency, with backoff) while completed parts keep their ETags, so a flaky or slow link recovers instead of discarding the work already done. Each part PUT also gets a part-size-scaled total timeout (bounded by an operational ceiling), so a silently stalled connection fails fast rather than hanging the upload.
- Streaming multipart uploads now mint one presigned part URL per part, on
demand immediately before each PUT, instead of pre-minting in batches. This
keeps each URL's age minimal so it can't expire mid-transfer on a slow link,
at the cost of more
POST /v1/uploads/{id}/partsrequests for large uploads.
uploads::MAX_MINT_BATCH(pub const), obsoleted by per-part minting. No longer part of the public API.
- Streaming uploads with just-in-time part minting for large files.
Client::upload_filenow opens a streaming multipart session for files past the multipart threshold and mints presigned part URLs on demand (viaPOST /v1/uploads/{id}/parts) immediately before each chunk uploads, instead of pre-minting every URL when the session opens. A part URL that expires mid-transfer (storage403) is transparently re-minted and the chunk retried, so large or slow uploads that outlive a presigned URL's ~30-minute TTL still complete within the session's 24-hour window rather than failing partway. Small files keep the single-PUTfast path. (Fixes #76.) - Low-level
POST /v1/uploads/{id}/partspart-minting endpoint with itsMintUploadPartsRequest/MintUploadPartsResponse/MintedUploadPartResponsemodels, generated from the OpenAPI spec.
- Token exchange (
POST /v1/auth/jwt) now retries transient failures before giving up: a momentary5xxor a transport error (connection/read failure) is retried with bounded exponential backoff + jitter (3 attempts total), so a brief server-side blip no longer fails the caller outright. A4xx(bad/expired credential) is never retried, and the last status/body is preserved once the budget is exhausted. Applies to both the initial mint and the refresh path.
- The default
User-Agentis now computed from the crate version at build time (CARGO_PKG_VERSION) instead of a hardcoded string, so it always reflects the published version.
- Ergonomic presigned (direct-to-storage) file uploads:
Client::upload_file(andclient.uploads().upload_file) open an upload session,PUTthe bytes straight to object storage — a singlePUTfor small files, bounded- concurrency multipartPUTs sliced by the server'spart_sizefor large ones — then finalize, returning theFinalizeUploadResponse. Configurable viaUploadOptions(content type/encoding, filename, part-size hint, and anUploadProgresscallback). Never falls back to the legacyPOST /v1/filesproxy; storagePUTs carry no SDK auth/scope headers. Multipart concurrency is tunable viaUploadOptions::max_concurrency(default 10), bounded by a 256 MiB peak-memory budget derived from the server's actual part size; when nopart_sizeis given, the SDK auto-scales the hint (8 MiB for normal files, larger only past ~72 GiB to keep the part count under S3's 10,000-part limit). Finalize is exactly-once (sent with retries disabled so an ambiguous failure can't be retried into a spurious "already finalized" error); partPUTs stay retryable. StoragePUTs use a dedicated header-bare reqwest client, so a host app's default headers on the SDK's main client never leak to object storage. The multipart session shape is validated (part_urlscount must match the file's part count) and pathological sizes (> i64::MAX) are rejected rather than silently wrapped. - Low-level file upload endpoints generated from the OpenAPI spec, including the
presigned upload-session and finalize operations that
upload_filebuilds on.
- Breaking: the datasets API and all related types, following their removal
from the Hotdata OpenAPI spec. This drops the
datasets_apimodule and theclient.datasets()resource handle; every dataset model (CreateDatasetRequest/CreateDatasetResponse,GetDatasetResponse,UpdateDatasetRequest/UpdateDatasetResponse,ListDatasetsResponse,ListDatasetVersionsResponse,RefreshDatasetResponse,DatasetSummary,DatasetVersionSummary,DatasetSourceand its*OneOf*variants,InlineDatasetSource,SavedQueryDatasetSource,SqlQueryDatasetSource,UploadDatasetSource,UrlDatasetSource,InlineData,ColumnTypeSpec); and the dataset-relatedJobTypevariants (DatasetRefresh,CreateDatasetIndex).
- Pre-response connection errors are now retried transparently on any method,
including
POST. A pooled keep-alive socket that an intermediary closed on its idle timeout surfaces, on the next reuse, as a connection reset before the request reaches the server; since the server did no work, the retry can't double-execute. This covers every generated op (viaexecute_retrying) and the hand-writtenClient::query/Client::submit_querypaths, governed by the sameRetryPolicybudget as 429. Response-phase transport errors are left un-retried so a non-idempotentPOSTcan't double-execute (#63).
- feat(indexes): add source_column field to index responses
Client::submit_query(the hand-written 202/async query path the CLI drives directly) now retries HTTP 429 (OVERLOADED) admission shedding perConfiguration::retry, like every generated op andClient::query. It was the one query path the 0.3.0 migration missed, so under admission shedding a submitted query surfaced the 429 as an error with no retry (#688).
- Every generated
apis::*operation now transparently retries HTTP 429 (OVERLOADED) admission shedding, honoringRetry-Afterwith backoff before the op returns (#58). The policy is the newConfiguration::retryfield (crate::query::RetryPolicy, defaulting toRetryPolicy::default); setmax_retriesto 0 to disable. The enhanced query path (crate::query) keeps using its own per-callQueryConfig::retryinstead.
- Enhanced
Client::query: transparently retries HTTP 429 (OVERLOADED) admission shedding honoringRetry-Afterunder a deadline budget, and auto-follows truncated results to materialize the full row set, guarded by configurablemax_auto_rows(default 1M) andmax_auto_bytes(default 64 MiB) ceilings (#688). The raw generated op remains reachable viaclient.queries().execute()/hotdata::apis::query_api::query. hotdata::querymodule withQueryConfig,RetryPolicy,PollPolicy, theQueryErrorenum, and theResultErrorfamily (Failed/Timeout/TooLarge/Incomplete/Unavailable).ClientBuilder::query_configsets the instance default;Client::query_withtakes a per-call override.Client::query_in(scope a query to a database) andClient::query_preview(return the bounded preview without auto-following), plusQueryConfig::with_*setters for fluent per-call overrides (client.query_config().clone().with_auto_follow(false)).
Client::querynow returnsResult<QueryResponse, hotdata::QueryError>instead ofResult<QueryResponse, Error<QueryError>>to carry the overload and result-lifecycle errors the bounded-memory query contract introduces. Migration: where you matched the old error, e.g.Err(Error::ResponseError(rc)), now matchErr(QueryError::Submit( Error::ResponseError(rc))); overload and auto-follow failures arrive as the newQueryError::Overloaded/QueryError::Result(..)variants.Client::queryrejects an explicitasync = truerequest up front withQueryError::AsyncRequested(it is the synchronous-results path); useClient::submit_queryfor asynchronous submissions.
- feat(queries): add preview and total row count fields
list_indexes_collection(GET /v1/indexes) inhotdata::apis::indexes_api— lists indexes across every table in a database, scoped by theX-Database-Idheader — along with theIndexEntryResponseandListIndexesPageResponsemodels.
- Sandbox endpoints and their models (
sandboxes_api,Sandbox,SandboxResponse,CreateSandboxRequest,UpdateSandboxRequest,ListSandboxesResponse,DeleteSandboxResponse), following their removal from the Hotdata OpenAPI spec.
- Regenerated the client from the updated Hotdata OpenAPI spec.
- Database and managed-catalog schema/table management endpoints:
add_database_schema,add_database_table,add_managed_schema, andadd_managed_table, with their request/response models (AddManagedSchemaRequest,AddManagedTableRequest,AddManagedTableDecl,ManagedSchemaResponse,ManagedTableResponse).
- Regenerated the client from the updated Hotdata OpenAPI spec, catching up on spec changes since 0.1.1. Generated with openapi-generator 7.22.0 (
useChrono=false, so date-time fields remainStringas before).
- Populate crate metadata (
repository,homepage,documentation,readme,keywords,categories) for the crates.io listing, and link the Hotdata CLI from the README. - Publishing to crates.io now uses Trusted Publishing (OIDC) instead of a stored API token.
- Ergonomic
ClientandClientBuilder(hotdata::Client) wrapping the generatedConfiguration: set an API token and workspace id, with thin async pass-throughs forquery,list_query_runs,list_results,get_result, andlist_workspaces. - Grouped resource handles on
Client(client.datasets(),client.secrets(),client.query_runs(), … one per API) so callers no longer importhotdata::apis::*_apifree functions or thread a&Configurationthrough every call. - Query convenience helpers:
Client::await_resultpolls a persisted result toready(configurable viaPollConfig) instead of a hand-rolled loop, andClient::query_to_arrowsubmits, awaits, and decodes a result as Arrow in one call (arrowfeature). hotdata::fieldhelpers (set/clear/unchanged) for theOption<Option<T>>nullable-and-optional update fields, soSome(Some(v))/Some(None)intents read clearly.- Typed
ResultStatus/QueryRunStatusenums (hotdata::status) withresult_status()/run_status()accessor traits on the response types. Each carries anOther(String)catch-all so unrecognized server statuses round-trip instead of failing — matching runtimedb's lenient parsing, where a generated closed enum would not. - Transparent API-token to JWT exchange via a hand-written
TokenManager(hotdata::auth):hd_API tokens are exchanged against/v1/auth/jwt, cached, refreshed, and re-minted automatically, with a 30s expiry leeway and single-flight concurrency. JWTs (eyJ…) are passed through unchanged. HonorsHOTDATA_DISABLE_JWT_EXCHANGE. - Pluggable
BearerTokenProviderhook onConfigurationso any async token source can drive bearer auth. - Optional
arrowfeature:get_result_arrow/stream_result_arrowdecode Apache Arrow IPC result streams intoRecordBatches, surface theX-Total-Row-Countandrel="next"Link headers, and map the202/409/404/400result states to typedArrowErrorvariants. - Flat re-export surface (
hotdata::Client,hotdata::Configuration,hotdata::prelude::*) alongside the namespacedhotdata::apis/hotdata::models. The prelude also re-exports the resource handles,PollConfig, thefieldhelpers, and (with thearrowfeature)ArrowError. - The SDK's own error enums (
ClientError,TokenExchangeError,ArrowError,AwaitResultError,QueryToArrowError) are#[non_exhaustive], so new variants can be added without a breaking change — match them with a wildcard arm. - Request/response debug logging (
hotdata::http_log) covering every HTTP call — generated ops plus the hand-writtensubmit_query/upload_stream/ Arrow fetch / JWT mint. Each emitslog::debug!records on thehotdata::httptarget (>>> METHOD url, headers, body;<<< status, body) so a host (e.g. the CLI's--debug) can render them with anylogbackend.Authorizationbearer tokens and sensitive JSON/form fields (api_token,secret,password, …) are masked before logging; the SDK installs no logger and stays silent without a backend. Theapi.mustachetemplate emits the hooks so they survive regeneration, and the regen-safety CI guard verifies they remain.
- Regeneration is now safe for the hand-written ergonomic layer: the generator only overwrites generated subtrees (
src/apis,src/models,docs) and skipssrc/lib.rs,src/auth.rs,src/http_log.rs,src/arrow.rs,src/client.rs,src/resources.rs,src/field.rs, andCargo.tomlvia.openapi-generator-ignore. The regen-safety CI guard verifies all of these survive and stay wired intolib.rs. - Initial generated client from the Hotdata OpenAPI spec.