diff --git a/reference/rest/headers.md b/reference/rest/headers.md index 818398eb..3b880d4a 100644 --- a/reference/rest/headers.md +++ b/reference/rest/headers.md @@ -20,6 +20,8 @@ These headers are included in all Harper REST API responses: | `etag` | `"abc123"` | Encoded version/last-modification time of the returned record. Used for conditional requests. | | `location` | `/MyTable/new-id` | Returned on `POST` responses. Contains the path to the newly created record. | +Collection responses to a [count request](./querying.md#pagination-and-total-count) additionally include `Content-Range`, `Range-Unit`, and `Preference-Applied` (). + ## Request Headers ### Content-Type @@ -67,6 +69,19 @@ Accept-Encoding: gzip, br Compression is particularly effective for JSON responses. For binary formats like CBOR, compression provides diminishing returns compared to the already-compact encoding. +### Prefer + + + +Opt in to a total match count on a collection `GET`/`HEAD`, returned via the `Content-Range` response header for pagination: + +```http +GET /Product/?category=software&limit(0,25) +Prefer: count=exact +``` + +Accepts `count=exact` (precise, scans the full matched set) or `count=estimated` (fast, approximate). See [Pagination and Total Count](./querying.md#pagination-and-total-count) for the full request/response contract. + ### Authorization Credentials for authenticating requests. See [Security Overview](../security/overview.md) for details on supported authentication mechanisms (Basic, JWT, mTLS). diff --git a/reference/rest/overview.md b/reference/rest/overview.md index 74b755f7..37c94502 100644 --- a/reference/rest/overview.md +++ b/reference/rest/overview.md @@ -33,8 +33,11 @@ rest: true rest: lastModified: true # enables Last-Modified response header support webSocket: false # disables automatic WebSocket support (enabled by default) + exactCount: true # opt in to Prefer: count=exact scans (off by default; otherwise served as estimates) ``` + `exactCount` (default `false`) controls whether the [pagination total-count](./querying.md#pagination-and-total-count) feature honors `Prefer: count=exact`. Because an exact count scans the full matched set, it is off by default; set this to `true` to enable exact counts for this application's REST interface. A `count=exact` request is otherwise served as a cheaper estimate. + ## URL Structure The REST interface follows a consistent URL structure: diff --git a/reference/rest/querying.md b/reference/rest/querying.md index b05a19c0..139176ea 100644 --- a/reference/rest/querying.md +++ b/reference/rest/querying.md @@ -173,6 +173,62 @@ GET /Product/?rating=gt=3&sort(+name) GET /Product/?sort(+rating,-price) ``` +## Pagination and Total Count + + + +Use `limit(start,end)` to page through a collection, and opt in to a total match count with the `Prefer` request header (`Prefer: count=exact`) so a client can render pagination (for example "1-25 of 1,234") without a second request. + +Counting is opt-in: without the header, no count is computed and no count headers are returned. It applies only to `GET`/`HEAD` requests and requires a `limit()` within a supported page size — a request with no `limit()`, an oversized one, or a non-numeric one is served normally with no count headers, since counting an unbounded page would defeat the point of paging. + +### Requesting a count + +Send a `Prefer` header on a `GET` (or `HEAD`) request to a collection: + +| Value | Meaning | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `count=exact` | The exact number of matching records. Scans the full matched set, so it is opt-in per mount (see below) and served as an estimate unless enabled. | +| `count=estimated` | A fast planner/table estimate. Cheap, approximate. | + +```http +GET /Product/?category=software&limit(0,25) +Prefer: count=exact +``` + +### Count response headers + +The count is returned in [RFC 7233](https://datatracker.ietf.org/doc/html/rfc7233)-style response headers alongside the page body: + +| Header | Example | Description | +| -------------------- | ----------------- | --------------------------------------------------------------------------------------------- | +| `Content-Range` | `items 0-24/1234` | The 0-based, inclusive range of records returned (`start-end`) out of the total matching set. | +| `Range-Unit` | `items` | The unit used by `Content-Range`. | +| `Preference-Applied` | `count=exact` | The count mode the server applied (`exact` or `estimated`). | + +```http +HTTP/1.1 200 OK +Content-Range: items 0-24/1234 +Range-Unit: items +Preference-Applied: count=exact +``` + +The response status is always `200` — `Content-Range` is informational (Harper does not use `206 Partial Content`). A `HEAD` request with `Prefer: count=` returns the count headers with no body, a cheap way to ask "how many match?" without transferring the page. When CORS is enabled, these three headers are added to `Access-Control-Expose-Headers` so browser clients can read them cross-origin. + +### Unavailable totals + +The total is reported as `*` (for example `Content-Range: items 0-24/*`) when it cannot be produced — an exact scan that reaches its internal work limit, or a query with no cardinality estimate (for example a `!=` or `=ct=` (contains) condition). `Preference-Applied` still echoes the requested mode, so an unavailable total (`.../*`) is distinct from a request that asked for no count. + +### Enabling exact counts + +Because an exact count scans the full matched set, it is **off by default**. Enable it in an application's REST configuration via the [`exactCount` option](./overview.md#configuration): + +```yaml +rest: + exactCount: true +``` + +Without it, a `count=exact` request is served as an estimate (the response reports `Preference-Applied: count=estimated`). Estimated counts are always available. + ## Relationships and Joins