docs(page): clarify cursor size and timestamp encoding

This commit is contained in:
end committed 2026-10-27 11:55:43 -07:00
1 parent ea5d57f626
commit 2318c4fa39
1 file changed
+3 -3
@@ -45,11 +45,11 @@ Cursor v1 uses a strict envelope with version, sort ID, ordered column IDs/direc
{"v":1,"sort":"orders/recent/v1","columns":[{"id":"created","direction":"desc","unique":false},{"id":"id","direction":"asc","unique":true}],"values":[{"type":"time","value":"2026-10-27T12:00:00Z"},{"type":"int64","value":"9007199254740993"}]} {"v":1,"sort":"orders/recent/v1","columns":[{"id":"created","direction":"desc","unique":false},{"id":"id","direction":"asc","unique":true}],"values":[{"type":"time","value":"2026-10-27T12:00:00Z"},{"type":"int64","value":"9007199254740993"}]}
``` ```
All payloads are strings. Fixed type tags distinguish `int`, `int8`, `int16`, `int32`, `int64`, `string`, `time` and `uuid`. A type switch encodes exactly these concrete Go types; a fixed switch reconstructs them using range-checked integer parsing, valid UTF-8 strings, `time.Time` via RFC3339Nano, and standard-library `uuid.UUID` via canonical lowercase dashed text. Integer decimal text is canonical and never passes through float64. `int` must fit the consuming platform. Timestamp round trips preserve SQL-relevant instant/precision, not monotonic metadata or named timezone identity. Reject invalid UTF-8 rather than silently replacing bytes. All payloads are strings. Fixed type tags distinguish `int`, `int8`, `int16`, `int32`, `int64`, `string`, `time` and `uuid`. A type switch encodes exactly these concrete Go types; a fixed switch reconstructs them using range-checked integer parsing, valid UTF-8 strings, `time.Time` via RFC3339Nano, and standard-library `uuid.UUID` via canonical lowercase dashed text. Integer decimal text is canonical and never passes through float64. `int` must fit the consuming platform. Normalize timestamps to UTC before RFC3339Nano encoding, preserving the instant and nanoseconds, not monotonic metadata or named timezone identity; reject years unrepresentable in RFC3339 with a typed `ValueError`. Reject invalid UTF-8 rather than silently replacing bytes.
UUID support specifically means Go 1.27.1's standard `uuid.UUID`: its Parse/String methods support this representation and database/sql's default converter converts it to a string for drivers. Third-party UUID types require explicit caller conversion to this type. Named user-defined scalar types, unsigned integers, floats, booleans, pointers, nullable wrappers, byte slices and arbitrary custom types return a typed `ValueError`; no reflection, implicit coercion, custom binder or registry is proposed. A true Go type alias retains its underlying supported identity. Additional types require an explicit format/support decision. V0 sort columns must be non-null and ordered consistently by WHERE and ORDER BY; callers extract actual stored/scanned values, including timestamp precision. UUID support specifically means Go 1.27.1's standard `uuid.UUID`: its Parse/String methods support this representation and database/sql's default converter converts it to a string for drivers. Third-party UUID types require explicit caller conversion to this type. Named user-defined scalar types, unsigned integers, floats, booleans, pointers, nullable wrappers, byte slices and arbitrary custom types return a typed `ValueError`; no reflection, implicit coercion, custom binder or registry is proposed. A true Go type alias retains its underlying supported identity. Additional types require an explicit format/support decision. V0 sort columns must be non-null and ordered consistently by WHERE and ORDER BY; callers extract actual stored/scanned values, including timestamp precision.
Decode and bound the token before constructing a mod (proposed maximum: 16 KiB encoded). Reject bad base64/JSON, unknown/duplicate/missing fields, trailing data, version mismatch, sort descriptor mismatch, wrong value count, nulls, unknown tags and invalid/range-overflowing payloads. An exported `CursorError`, inspectable with `errors.As`, distinguishes malformed, version, sort and unsupported-value failures. Encoding unsupported key values returns `ValueError` identifying the position and type, without leaking values. Keys and token shape are validated before producing a Page; errors return no partial page. Cursors are not authenticated: a well-formed edit to a supported value or tag remains a seek request, not a tamper-detection failure. Without expected type metadata in Sort, the library cannot prove an edited supported tag matches a SQL column; driver/database errors propagate normally. Authorization stays in the query. Apply the same proposed maximum of 16 KiB (16,384 bytes) to both emitted and received tokens, measured after final unpadded base64url encoding, including the full envelope. Check received token bytes before decoding, and decode before constructing a mod. Encoding a key or descriptor that cannot fit returns a typed `CursorError` and no partial Page. Reject bad base64/JSON, unknown/duplicate/missing fields, trailing data, version mismatch, sort descriptor mismatch, wrong value count, nulls, unknown tags and invalid/range-overflowing payloads. An exported `CursorError`, inspectable with `errors.As`, distinguishes malformed, version, sort and unsupported-value failures. Encoding unsupported key values returns `ValueError` identifying the position and type, without leaking values. Keys and token shape are validated before producing a Page; errors return no partial page. Cursors are not authenticated: a well-formed edit to a supported value or tag remains a seek request, not a tamper-detection failure. Without expected type metadata in Sort, the library cannot prove an edited supported tag matches a SQL column; driver/database errors propagate normally. Authorization stays in the query.
Prepared pagination exposes one generic Bob `Mod[Q]` targeting native `AppendWhere(...any)`, `AppendOrder(bob.Expression)` and `SetLimit(any)` methods shared by PostgreSQL, SQLite and MySQL SelectQuery. Use Bob expressions and `clause.OrderDef`, fixed operators/directions, and an integer limit. Prepare validates requests/cursors first because ordinary Bob Apply has no error return. Cursor values only enter `expr.Arg(value)`; Bob's existing `any` boundary retains their concrete dynamic type. No custom SQL renderer is needed. Apply once to a query without ORDER BY, LIMIT, OFFSET or FETCH; callers may append their dynamic/authorization filters but must not override pagination/order. Prepared pagination exposes one generic Bob `Mod[Q]` targeting native `AppendWhere(...any)`, `AppendOrder(bob.Expression)` and `SetLimit(any)` methods shared by PostgreSQL, SQLite and MySQL SelectQuery. Use Bob expressions and `clause.OrderDef`, fixed operators/directions, and an integer limit. Prepare validates requests/cursors first because ordinary Bob Apply has no error return. Cursor values only enter `expr.Arg(value)`; Bob's existing `any` boundary retains their concrete dynamic type. No custom SQL renderer is needed. Apply once to a query without ORDER BY, LIMIT, OFFSET or FETCH; callers may append their dynamic/authorization filters but must not override pagination/order.
@@ -85,4 +85,4 @@ Bob v0.50.0 is source-feasible for all three dialects, with two reported limitat
* Explicit tags retain supported concrete types without reflection, precision loss or a registry. Unsupported custom/third-party types require explicit caller conversion or a later support decision. * Explicit tags retain supported concrete types without reflection, precision loss or a registry. Unsupported custom/third-party types require explicit caller conversion or a later support decision.
* Version and sort IDs become compatibility commitments; changes may deliberately reject retained cursors. No legacy decoder is needed before another version exists. * Version and sort IDs become compatibility commitments; changes may deliberately reject retained cursors. No legacy decoder is needed before another version exists.
* Cursors expose values and permit valid edits; queries, counts and every request must preserve authorization. Concurrent writes have no snapshot guarantee. Nothing runs when unused. * Cursors expose values and permit valid edits; queries, counts and every request must preserve authorization. Concurrent writes have no snapshot guarantee. Nothing runs when unused.
* Implementation tests must cover tagged round trips (including large integers, time and UUID), unsupported/malformed/tampered/mismatched cursors, Sort/key disagreement, mixed-direction ties, both directions, boundaries and size clamping, plus a package Example of the key function. Execute our pagination and compliant binding/composition tests on in-process SQLite and PostgreSQL CI; do not assert Bob SQL formatting/rendering guarantees. MySQL remains source-feasible until runtime coverage exists. * Implementation tests must cover tagged round trips (including large integers, time with an unusual second-offset timezone, and UUID), unrepresentable timestamp years, encoded-token boundaries at and above 16 KiB for both emission and receipt, oversized-string rejection with a typed error and no partial Page, unsupported/malformed/tampered/mismatched cursors, Sort/key disagreement, mixed-direction ties, both directions, boundaries and size clamping, plus a package Example of the key function. Execute our pagination and compliant binding/composition tests on in-process SQLite and PostgreSQL CI; do not assert Bob SQL formatting/rendering guarantees. MySQL remains source-feasible until runtime coverage exists.