diff --git a/CHANGELOG.md b/CHANGELOG.md index fb49750..51ffefc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.3.4] - 2026-06-15 + +### Changed + +- feat(queries): add preview and total row count fields + ## [0.3.3] - 2026-06-10 ### Changed diff --git a/docs/QueryApi.md b/docs/QueryApi.md index fad7fc1..9f8340e 100644 --- a/docs/QueryApi.md +++ b/docs/QueryApi.md @@ -104,6 +104,7 @@ Name | Type | Description | Notes **202** | Query submitted asynchronously | - | **400** | Invalid request (no database specified, or header/body database_id conflict) | - | **404** | Database not found | - | +**429** | Too many concurrent queries; retry after the Retry-After delay | - | **500** | Internal server error | - | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) diff --git a/docs/QueryResponse.md b/docs/QueryResponse.md index fb66678..1eb521a 100644 --- a/docs/QueryResponse.md +++ b/docs/QueryResponse.md @@ -9,10 +9,13 @@ Name | Type | Description | Notes **columns** | **List[str]** | | **execution_time_ms** | **int** | | **nullable** | **List[bool]** | Nullable flags for each column (parallel to columns vec). True if the column allows NULL values, false if NOT NULL. | +**preview_row_count** | **int** | Number of rows in *this* response body (`rows.len()`). Always present. For a large result this is a bounded preview, not the grand total — see `total_row_count` and `truncated` (#640). | **query_run_id** | **str** | Unique identifier for the query run record (qrun...). | **result_id** | **str** | Unique identifier for retrieving this result via GET /results/{id}. Null if catalog registration failed (see `warning` field for details). When non-null, the result is being persisted asynchronously. | [optional] -**row_count** | **int** | | +**row_count** | **int** | **Deprecated** — use `preview_row_count` (rows in this body) and `total_row_count` (grand total) instead. Retained for backward compatibility and currently always equal to `preview_row_count`; it will be removed in a future release once clients migrate to the count fields below (#640). | **rows** | **List[List[object]]** | Array of rows, where each row is an array of column values. Values can be strings, numbers, booleans, or null. | +**total_row_count** | **int** | 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}` (#640). | [optional] +**truncated** | **bool** | True when `rows` is a bounded preview of a larger result. Fetch the full result via `result_id` (#640). Always `false` until bounded streaming is enabled; clients should still branch on it so no code change is needed when truncation goes live. | **warning** | **str** | Warning message if result persistence could not be initiated. When present, `result_id` will be null and the result cannot be retrieved later. The query results are still returned in this response. | [optional] ## Example diff --git a/hotdata/api/query_api.py b/hotdata/api/query_api.py index 6500446..481bfd5 100644 --- a/hotdata/api/query_api.py +++ b/hotdata/api/query_api.py @@ -102,6 +102,7 @@ def query( '202': "AsyncQueryResponse", '400': "ApiErrorResponse", '404': "ApiErrorResponse", + '429': "ApiErrorResponse", '500': "ApiErrorResponse", } response_data = self.api_client.call_api( @@ -177,6 +178,7 @@ def query_with_http_info( '202': "AsyncQueryResponse", '400': "ApiErrorResponse", '404': "ApiErrorResponse", + '429': "ApiErrorResponse", '500': "ApiErrorResponse", } response_data = self.api_client.call_api( @@ -252,6 +254,7 @@ def query_without_preload_content( '202': "AsyncQueryResponse", '400': "ApiErrorResponse", '404': "ApiErrorResponse", + '429': "ApiErrorResponse", '500': "ApiErrorResponse", } response_data = self.api_client.call_api( diff --git a/hotdata/api_client.py b/hotdata/api_client.py index eb55840..465fa0b 100644 --- a/hotdata/api_client.py +++ b/hotdata/api_client.py @@ -91,7 +91,7 @@ def __init__( self.default_headers[header_name] = header_value self.cookie = cookie # Set default User-Agent. - self.user_agent = 'OpenAPI-Generator/0.3.3/python' + self.user_agent = 'OpenAPI-Generator/0.3.4/python' self.client_side_validation = configuration.client_side_validation def __enter__(self): diff --git a/hotdata/models/query_response.py b/hotdata/models/query_response.py index c24eb73..429740a 100644 --- a/hotdata/models/query_response.py +++ b/hotdata/models/query_response.py @@ -18,7 +18,7 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, StrictStr from typing import Any, ClassVar, Dict, List, Optional from typing_extensions import Annotated from typing import Optional, Set @@ -31,12 +31,15 @@ class QueryResponse(BaseModel): columns: List[StrictStr] execution_time_ms: Annotated[int, Field(strict=True, ge=0)] nullable: List[StrictBool] = Field(description="Nullable flags for each column (parallel to columns vec). True if the column allows NULL values, false if NOT NULL.") + preview_row_count: StrictInt = Field(description="Number of rows in *this* response body (`rows.len()`). Always present. For a large result this is a bounded preview, not the grand total — see `total_row_count` and `truncated` (#640).") query_run_id: StrictStr = Field(description="Unique identifier for the query run record (qrun...).") result_id: Optional[StrictStr] = Field(default=None, description="Unique identifier for retrieving this result via GET /results/{id}. Null if catalog registration failed (see `warning` field for details). When non-null, the result is being persisted asynchronously.") - row_count: Annotated[int, Field(strict=True, ge=0)] + row_count: Annotated[int, Field(strict=True, ge=0)] = Field(description="**Deprecated** — use `preview_row_count` (rows in this body) and `total_row_count` (grand total) instead. Retained for backward compatibility and currently always equal to `preview_row_count`; it will be removed in a future release once clients migrate to the count fields below (#640).") rows: List[List[Any]] = Field(description="Array of rows, where each row is an array of column values. Values can be strings, numbers, booleans, or null.") + total_row_count: Optional[StrictInt] = Field(default=None, description="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}` (#640).") + truncated: StrictBool = Field(description="True when `rows` is a bounded preview of a larger result. Fetch the full result via `result_id` (#640). Always `false` until bounded streaming is enabled; clients should still branch on it so no code change is needed when truncation goes live.") warning: Optional[StrictStr] = Field(default=None, description="Warning message if result persistence could not be initiated. When present, `result_id` will be null and the result cannot be retrieved later. The query results are still returned in this response.") - __properties: ClassVar[List[str]] = ["columns", "execution_time_ms", "nullable", "query_run_id", "result_id", "row_count", "rows", "warning"] + __properties: ClassVar[List[str]] = ["columns", "execution_time_ms", "nullable", "preview_row_count", "query_run_id", "result_id", "row_count", "rows", "total_row_count", "truncated", "warning"] model_config = ConfigDict( populate_by_name=True, @@ -82,6 +85,11 @@ def to_dict(self) -> Dict[str, Any]: if self.result_id is None and "result_id" in self.model_fields_set: _dict['result_id'] = None + # set to None if total_row_count (nullable) is None + # and model_fields_set contains the field + if self.total_row_count is None and "total_row_count" in self.model_fields_set: + _dict['total_row_count'] = None + # set to None if warning (nullable) is None # and model_fields_set contains the field if self.warning is None and "warning" in self.model_fields_set: @@ -102,10 +110,13 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "columns": obj.get("columns"), "execution_time_ms": obj.get("execution_time_ms"), "nullable": obj.get("nullable"), + "preview_row_count": obj.get("preview_row_count"), "query_run_id": obj.get("query_run_id"), "result_id": obj.get("result_id"), "row_count": obj.get("row_count"), "rows": obj.get("rows"), + "total_row_count": obj.get("total_row_count"), + "truncated": obj.get("truncated"), "warning": obj.get("warning") }) return _obj diff --git a/pyproject.toml b/pyproject.toml index 2d691f9..da875cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "hotdata" -version = "0.3.3" +version = "0.3.4" description = "Hotdata API" authors = [ {name = "Hotdata",email = "developers@hotdata.dev"}, diff --git a/test/test_query_response.py b/test/test_query_response.py index b45a175..33ba5c2 100644 --- a/test/test_query_response.py +++ b/test/test_query_response.py @@ -43,6 +43,7 @@ def make_instance(self, include_optional) -> QueryResponse: nullable = [ True ], + preview_row_count = 56, query_run_id = '', result_id = '', row_count = 0, @@ -51,6 +52,8 @@ def make_instance(self, include_optional) -> QueryResponse: null ] ], + total_row_count = 56, + truncated = True, warning = '' ) else: @@ -62,6 +65,7 @@ def make_instance(self, include_optional) -> QueryResponse: nullable = [ True ], + preview_row_count = 56, query_run_id = '', row_count = 0, rows = [ @@ -69,6 +73,7 @@ def make_instance(self, include_optional) -> QueryResponse: null ] ], + truncated = True, ) """