morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
query.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5/// @file table/query.hpp
6/// @brief `TableQuery`, `Page` and `table::apply`: a table's sort, filter and
7/// page as a request body, and the server's reference implementation.
8///
9/// In `server` mode a list action's body embeds a `TableQuery` and its reply
10/// is a `Page<Row>`. A server implements the action with `table::apply` over
11/// its own rows, or with a database query that honours the same semantics
12/// (spec 7 §5–6). `validate` applies the limits every server applies to an
13/// untrusted query. On the client, `PageWindow` holds the pages fetched
14/// around the visible rows.
15///
16/// See `docs/spec/table/engine.md`.
17
18#include <algorithm>
19#include <array>
20#include <chrono>
21#include <concepts>
22#include <cstddef>
23#include <cstdint>
24#include <cstdlib>
25#include <expected>
26#include <functional>
27#include <limits>
28#include <map>
29#include <memory>
34#include <morph/table/sort.hpp>
37#include <optional>
38#include <set>
39#include <span>
40#include <string>
41#include <string_view>
42#include <type_traits>
43#include <utility>
44#include <vector>
45
46namespace morph::table {
47
48// NOLINTBEGIN(cppcoreguidelines-pro-bounds-avoid-unchecked-container-access) -- row and column indices are bounded by the snapshot's own counts, and the sort and filter loops index once per comparison, where at() would check every access
49
50/// @brief Which rows of the view a request wants.
52 /// @brief First view row.
53 std::int64_t offset = 0;
54 /// @brief Rows wanted.
55 std::int64_t limit = 200;
56
57 /// @brief Member-wise equality.
58 /// @param other The request to compare with.
59 /// @return `true` when offset and limit match.
60 [[nodiscard]] bool operator==(PageRequest const& other) const = default;
61};
62
63/// @brief A table's sort, filters and page, as a list action's body carries them.
64///
65/// The member that holds one is marked `x-table` in the action's schema, which
66/// is how a client knows to send the table's state in it.
67struct TableQuery {
68 /// @brief The sort chain.
69 std::vector<SortKey> sort;
70 /// @brief The filter.
72 /// @brief The page.
74
75 /// @brief The schema keyword that marks a member of this type.
76 /// @return `"x-table"`.
77 [[nodiscard]] static constexpr std::string_view schemaMarker() noexcept { return "x-table"; }
78};
79
80/// @brief One page of a list action's reply.
81/// @tparam Row The row type.
82template <typename Row>
83struct Page {
84 /// @brief The rows, in view order.
85 std::vector<Row> rows;
86 /// @brief Rows in the whole filtered view; absent when the server did not
87 /// count them or the count is above its cap.
88 std::optional<std::int64_t> total;
89 /// @brief The view row of `rows[0]`.
90 std::int64_t offset = 0;
91};
92
93/// @brief The bounds a server applies to every `TableQuery`, an untrusted body.
95 /// @brief `page.limit` above this is clamped to it.
96 std::int64_t maxLimit = 1000;
97 /// @brief `page.offset` above this is refused.
98 std::int64_t maxOffset = 10'000'000;
99 /// @brief Sort keys.
100 std::size_t maxSortKeys = 8;
101 /// @brief Filtered columns.
102 std::size_t maxFilterColumns = 32;
103 /// @brief `include` plus `exclude` entries of one column or group.
104 std::size_t maxEntries = 32;
105 /// @brief Groups.
106 std::size_t maxGroups = 8;
107 /// @brief Columns of one group.
108 std::size_t maxGroupColumns = 16;
109 /// @brief Bytes of one filter value.
110 std::size_t maxTextLength = 256;
111 /// @brief A total above this is not reported.
112 std::int64_t totalCap = 100'000;
113};
114
115namespace detail {
116
117/// @brief A `LimitExceeded` error.
118/// @param column The column concerned, or empty.
119/// @param message The description.
120/// @return The error.
121[[nodiscard]] inline TableError limitError(std::string column, std::string message) {
122 return TableError{
123 .code = TableErrorCode::LimitExceeded, .column = std::move(column), .message = std::move(message)};
124}
125
126/// @brief Whether any operand of an entry is longer than @p max bytes.
127/// @param entry The entry.
128/// @param max The bound.
129/// @return `true` when one is.
130[[nodiscard]] inline bool entryTooLong(FilterEntry const& entry, std::size_t max) {
131 auto const tooLong = [max](std::optional<std::string> const& value) { return value && value->size() > max; };
132 return tooLong(entry.eq) || tooLong(entry.ne) || tooLong(entry.lt) || tooLong(entry.le) || tooLong(entry.gt) ||
133 tooLong(entry.ge) || tooLong(entry.contains) || tooLong(entry.startsWith) || tooLong(entry.endsWith) ||
134 (entry.between && ((*entry.between)[0].size() > max || (*entry.between)[1].size() > max));
135}
136
137/// @brief Whether any of @p entries has an operand longer than @p max bytes.
138/// @param entries The entries.
139/// @param max The bound.
140/// @return `true` when one has.
141[[nodiscard]] inline bool anyTooLong(std::span<FilterEntry const> entries, std::size_t max) {
142 return std::ranges::any_of(entries, [max](FilterEntry const& entry) { return entryTooLong(entry, max); });
143}
144
145/// @brief Checks that a column exists and the principal may read it.
146/// @param columnId The column's id.
147/// @param columns The row schema's columns.
148/// @param mayRead Whether the principal may read a column; empty allows every column.
149/// @return The error, or nothing.
150[[nodiscard]] inline std::optional<TableError> checkColumn(std::string const& columnId,
151 std::span<ColumnInfo const> columns,
152 std::function<bool(std::string_view)> const& mayRead) {
153 if (!findColumn(columns, columnId)) {
154 return unknownColumn(columnId);
155 }
156 if (mayRead && !mayRead(columnId)) {
158 .column = columnId,
159 .message = "column " + columnId + " is not readable"};
160 }
161 return std::nullopt;
162}
163
164/// @brief Checks a filter's columns, entry counts and value lengths.
165/// @param filters The filter.
166/// @param columns The row schema's columns.
167/// @param limits The bounds.
168/// @param mayRead Whether the principal may read a column.
169/// @return The first violation, or nothing.
170[[nodiscard]] inline std::optional<TableError> checkFilters(FilterSpec const& filters,
171 std::span<ColumnInfo const> columns,
172 QueryLimits const& limits,
173 std::function<bool(std::string_view)> const& mayRead) {
174 if (filters.columns.size() > limits.maxFilterColumns) {
175 return limitError({}, "too many filtered columns");
176 }
177 for (auto const& [columnId, filter] : filters.columns) {
178 if (auto error = checkColumn(columnId, columns, mayRead)) {
179 return error;
180 }
181 if (filter.include.size() + filter.exclude.size() > limits.maxEntries) {
182 return limitError(columnId, "too many filter entries");
183 }
184 if (anyTooLong(filter.include, limits.maxTextLength) || anyTooLong(filter.exclude, limits.maxTextLength)) {
185 return limitError(columnId, "filter value too long");
186 }
187 }
188 if (filters.groups.size() > limits.maxGroups) {
189 return limitError({}, "too many filter groups");
190 }
191 for (auto const& group : filters.groups) {
192 if (group.columns.size() > limits.maxGroupColumns || group.include.size() > limits.maxEntries) {
193 return limitError({}, "filter group too large");
194 }
195 for (auto const& columnId : group.columns) {
196 if (auto error = checkColumn(columnId, columns, mayRead)) {
197 return error;
198 }
199 }
200 if (anyTooLong(group.include, limits.maxTextLength)) {
201 return limitError({}, "filter value too long");
202 }
203 }
204 return std::nullopt;
205}
206
207} // namespace detail
208
209/// @brief Checks @p query against the table's columns and @p limits.
210///
211/// Every column named must be one of @p columns, a closed set, and one the
212/// principal may read: a filter on a hidden column is an oracle for its value,
213/// so hiding a column is not a security control. `page.limit` is clamped;
214/// every other bound is a refusal.
215/// @param query The query, as received.
216/// @param columns The row schema's columns.
217/// @param limits The bounds.
218/// @param mayRead Whether the principal may read a column; empty allows every column.
219/// @return The query with its limit clamped, or the first violation.
220[[nodiscard]] inline std::expected<TableQuery, TableError> validate(
221 TableQuery query, std::span<ColumnInfo const> columns, QueryLimits const& limits = {},
222 std::function<bool(std::string_view)> const& mayRead = {}) {
223 if (query.page.offset < 0 || query.page.offset > limits.maxOffset) {
224 return std::unexpected(detail::limitError({}, "page.offset out of range"));
225 }
226 if (query.page.limit < 0) {
227 return std::unexpected(detail::limitError({}, "page.limit is negative"));
228 }
229 query.page.limit = std::min(query.page.limit, limits.maxLimit);
230 if (query.sort.size() > limits.maxSortKeys) {
231 return std::unexpected(detail::limitError({}, "too many sort keys"));
232 }
233 for (auto const& key : query.sort) {
234 if (auto error = detail::checkColumn(key.column, columns, mayRead)) {
235 return std::unexpected(std::move(*error));
236 }
237 }
238 if (auto error = detail::checkFilters(query.filters, columns, limits, mayRead)) {
239 return std::unexpected(std::move(*error));
240 }
241 return query;
242}
243
244/// @brief Escapes `%`, `_` and the escape character for a SQL `LIKE`
245/// pattern, so a database-backed server keeps them ordinary characters
246/// as the filter semantics require.
247/// @param text A filter value.
248/// @param escape The `ESCAPE` character the query declares.
249/// @return The escaped text; wrap it in `%` for `contains` and so on.
250[[nodiscard]] inline std::string escapeLikePattern(std::string_view text, char escape = '\\') {
251 std::string out;
252 out.reserve(text.size());
253 for (char const character : text) {
254 if (character == '%' || character == '_' || character == escape) {
255 out.push_back(escape);
256 }
257 out.push_back(character);
258 }
259 return out;
260}
261
262/// @brief How `apply` runs.
264 /// @brief The bounds of `validate`.
266 /// @brief The server's collator, date parser and comparators. Its collator
267 /// decides the order of text in the pages it returns.
269 /// @brief Whether the principal may read a column; empty allows every column.
270 std::function<bool(std::string_view)> mayRead;
271};
272
273/// @brief The rows of one page, as source row indices.
274struct PageRows {
275 /// @brief Source rows, in view order.
276 std::vector<std::size_t> rows;
277 /// @brief Rows in the whole filtered view, unless above `QueryLimits::totalCap`.
278 std::optional<std::int64_t> total;
279 /// @brief The view row of `rows[0]`.
280 std::int64_t offset = 0;
281};
282
283/// @brief Runs @p query over @p source in the calling thread: the reference
284/// implementation of `server` mode.
285/// @param source The server's rows.
286/// @param query The request body's table query.
287/// @param options Limits, services and column access.
288/// @return The page, or the typed error the client shows.
289[[nodiscard]] inline std::expected<PageRows, TableError> apply(DataSource const& source, TableQuery const& query,
290 ApplyOptions const& options = {}) {
291 auto const columns = source.columns();
292 auto checked = validate(query, columns, options.limits, options.mayRead);
293 if (!checked) {
294 return std::unexpected(std::move(checked.error()));
295 }
296 // One copy of the services for this call, the filter's operands included:
297 // a collator that is not safe to share clones itself here, so concurrent
298 // calls never fold on the one the caller passed.
299 auto services = options.services.forTask();
300 auto filter = compileFilter(checked->filters, columns, services);
301 if (!filter) {
302 return std::unexpected(std::move(filter.error()));
303 }
304 detail::JobInput input;
305 input.snapshot = source.snapshot();
306 input.columns.assign(columns.begin(), columns.end());
307 input.services = std::move(services);
308 input.keys.resize(columns.size());
309 for (auto const& key : checked->sort) {
310 if (auto const column = detail::findColumn(columns, key.column)) {
311 input.needed.push_back(*column);
312 }
313 }
314 for (auto const column : filter->columnsUsed()) {
315 input.needed.push_back(column);
316 }
317 std::ranges::sort(input.needed);
318 auto const [first, last] = std::ranges::unique(input.needed);
319 input.needed.erase(first, last);
320 input.sort = checked->sort;
321 input.filter = std::make_shared<CompiledFilter const>(std::move(*filter));
322 input.previousSnapshot = input.snapshot;
323 detail::ViewJob job{std::move(input)};
324 static_cast<void>(job.step(kNoDeadline, ::core::async::StopToken{}));
325 if (job.failure()) {
326 return std::unexpected(TableError{
327 .code = TableErrorCode::ServiceFailed, .column = {}, .message = "a service failed: " + *job.failure()});
328 }
329 auto const view = job.takeResult().view;
330
331 PageRows out;
332 out.offset = checked->page.offset;
333 auto const count = static_cast<std::int64_t>(view.size());
334 if (count <= options.limits.totalCap) {
335 out.total = count;
336 }
337 auto const begin = std::min(checked->page.offset, count);
338 // Compared, not added: `maxLimit` may be as large as INT64_MAX.
339 auto const end = checked->page.limit >= count - begin ? count : begin + checked->page.limit;
340 for (auto i = begin; i < end; ++i) {
341 out.rows.push_back(view[static_cast<std::size_t>(i)]);
342 }
343 return out;
344}
345
346// ── Typed rows ──────────────────────────────────────────────────────────────
347
348namespace detail {
349
350/// @brief `true` for `std::optional<T>`.
351/// @tparam T A type.
352template <typename T>
353struct IsOptional : std::false_type {};
354/// @brief `true` for `std::optional<T>`.
355/// @tparam T The optional's value type.
356template <typename T>
357struct IsOptional<std::optional<T>> : std::true_type {};
358
359/// @brief A type glaze writes as one of its own members, as morph's `Ranged`,
360/// `Choice`, `Multiline` and `Tagged` are: its `glz::meta` value is a
361/// member pointer.
362template <typename V>
363concept MemberWrapper = requires { glz::meta<V>::value; } &&
364 std::is_member_object_pointer_v<std::remove_cvref_t<decltype(glz::meta<V>::value)>>;
365
366/// @brief The member a `MemberWrapper` is written as.
367/// @tparam V The wrapper type.
368template <typename V>
369using WrappedMember = std::remove_cvref_t<decltype(std::declval<V const&>().*(glz::meta<V>::value))>;
370
371/// @brief The column kind a member type gives.
372/// @tparam T The member type.
373/// @return The kind.
374template <typename T>
375[[nodiscard]] constexpr ColumnKind kindOf() {
376 using V = std::remove_cvref_t<T>;
377 if constexpr (IsOptional<V>::value) {
379 } else if constexpr (std::is_same_v<V, bool>) {
380 return ColumnKind::Bool;
381 } else if constexpr (std::integral<V>) {
382 return ColumnKind::Integer;
383 } else if constexpr (std::floating_point<V>) {
384 return ColumnKind::Number;
385 } else if constexpr (std::is_same_v<V, math::Rational>) {
386 return ColumnKind::Decimal;
387 } else if constexpr (units::isQuantity<V>) {
389 } else if constexpr (std::is_same_v<V, time::DateTime> || std::is_same_v<V, time::Timestamp>) {
391 } else if constexpr (MemberWrapper<V>) {
392 return kindOf<WrappedMember<V>>();
393 } else {
394 return ColumnKind::Text;
395 }
396}
397
398/// @brief An integer member as a cell.
399/// @tparam T The integer type.
400/// @param value The member.
401/// @return The cell: an `int64`, or text that an integer column reads as
402/// invalid when no `int64` holds the value (rather than a wrapped
403/// negative number).
404template <typename T>
405[[nodiscard]] Cell integerCell(T value) {
406 if constexpr (std::is_unsigned_v<T> && sizeof(T) >= sizeof(std::int64_t)) {
407 if (value > static_cast<T>(std::numeric_limits<std::int64_t>::max())) {
408 return Cell{std::to_string(value)};
409 }
410 }
411 return Cell{static_cast<std::int64_t>(value)};
412}
413
414/// @brief A member as the text glaze writes for it: an enum by its glaze name,
415/// a JSON string decoded, `null` as an empty cell.
416/// @tparam T The member type.
417/// @param value The member.
418/// @return The cell.
419template <typename T>
420[[nodiscard]] Cell writtenCell(T const& value) {
421 std::string text;
422 if (glz::write_json(value, text) || text == "null") {
423 return Cell{};
424 }
425 if (text.size() >= 2 && text.front() == '"' && text.back() == '"') {
426 std::string decoded;
427 if (!glz::read_json(decoded, text)) {
428 return Cell{std::move(decoded)};
429 }
430 }
431 return Cell{std::move(text)};
432}
433
434/// @brief A member value as a cell.
435/// @tparam T The member type.
436/// @param value The member.
437/// @return The cell.
438template <typename T>
439[[nodiscard]] Cell toCell(T const& value) {
440 using V = std::remove_cvref_t<T>;
441 if constexpr (IsOptional<V>::value) {
442 return value ? toCell(*value) : Cell{};
443 } else if constexpr (std::is_same_v<V, bool> || std::is_same_v<V, std::string> ||
444 std::is_same_v<V, math::Rational>) {
445 return Cell{value};
446 } else if constexpr (std::integral<V>) {
447 return integerCell(value);
448 } else if constexpr (std::floating_point<V>) {
449 return Cell{static_cast<double>(value)};
450 } else if constexpr (units::isQuantity<V>) {
451 return value.value() ? Cell{*value.value()} : Cell{};
452 } else if constexpr (std::is_same_v<V, time::DateTime>) {
453 return Cell{std::chrono::floor<std::chrono::seconds>(value.value).time_since_epoch().count()};
454 } else if constexpr (std::is_same_v<V, time::Timestamp>) {
455 return value.value ? toCell(*value.value) : Cell{};
456 } else if constexpr (MemberWrapper<V>) {
457 return toCell(value.*(glz::meta<V>::value));
458 } else {
459 return writtenCell(value);
460 }
461}
462
463/// @brief Reads one column's cell from a row.
464/// @tparam Row The row type.
465template <typename Row>
466using CellReader = Cell (*)(Row const&);
467
468/// @brief One cell reader per key glaze writes, in key order.
469/// @tparam Row The row type.
470/// @return The readers.
471template <typename Row>
472[[nodiscard]] std::vector<CellReader<Row>> cellReaders() {
473 constexpr auto count = glz::reflect<Row>::size;
474 return [&]<std::size_t... I>(std::index_sequence<I...>) {
475 return std::vector<CellReader<Row>>{
476 +[](Row const& row) -> Cell { return toCell(morph::detail::reflectedMember<I>(row)); }...};
477 }(std::make_index_sequence<count>{});
478}
479
480/// @brief One column per key glaze writes, its kind from the value's type.
481/// @tparam Row The row type.
482/// @return The columns.
483template <typename Row>
484[[nodiscard]] std::vector<ColumnInfo> reflectedColumns() {
485 constexpr auto count = glz::reflect<Row>::size;
486 std::vector<ColumnInfo> out;
487 [&]<std::size_t... I>(std::index_sequence<I...>) {
488 (out.push_back(
489 ColumnInfo{.id = std::string{glz::reflect<Row>::keys[I]},
490 .kind = kindOf<decltype(morph::detail::reflectedMember<I>(std::declval<Row const&>()))>(),
491 .comparator = {},
492 .format = {}}),
493 ...);
494 }(std::make_index_sequence<count>{});
495 return out;
496}
497
498} // namespace detail
499
500/// @brief A key projection: the `RowId` of a typed row.
501/// @tparam Row The row type.
502template <typename Row>
503using RowKey = std::function<RowId(Row const&)>;
504
505/// @brief A snapshot over a shared, immutable vector of typed rows.
506/// @tparam Row A glaze-reflectable aggregate.
507template <typename Row>
508class RowsSnapshot final : public RowSnapshot {
509public:
510 /// @brief Wraps @p rows.
511 /// @param rows The rows; shared, never modified.
512 /// @param key The key projection.
513 /// @param readers One cell reader per column.
514 RowsSnapshot(std::shared_ptr<std::vector<Row> const> rows, RowKey<Row> key,
515 std::shared_ptr<std::vector<detail::CellReader<Row>> const> readers)
516 : _rows{std::move(rows)}, _key{std::move(key)}, _readers{std::move(readers)} {}
517
518 /// @brief The number of rows.
519 /// @return Row count.
520 [[nodiscard]] std::size_t rowCount() const override { return _rows->size(); }
521
522 /// @brief A row's key.
523 /// @param row Row index.
524 /// @return The key.
525 [[nodiscard]] RowId rowId(std::size_t row) const override { return _key((*_rows)[row]); }
526
527 /// @brief One cell.
528 /// @param row Row index.
529 /// @param column Column index.
530 /// @return The cell.
531 [[nodiscard]] Cell cell(std::size_t row, std::size_t column) const override {
532 return (*_readers)[column]((*_rows)[row]);
533 }
534
535 /// @brief Hands rows `[first, last)` of @p column to @p sink in runs.
536 /// @param column Column index.
537 /// @param sink Receives the cells.
538 /// @param first First row.
539 /// @param last One past the last row.
540 void readColumn(std::size_t column, ColumnSink& sink, std::size_t first, std::size_t last) const override {
541 constexpr std::size_t kRun = 256;
542 std::vector<Cell> run;
543 run.reserve(kRun);
544 auto const reader = (*_readers)[column];
545 while (first < last) {
546 auto const end = std::min(first + kRun, last);
547 run.clear();
548 for (auto row = first; row < end; ++row) {
549 run.push_back(reader((*_rows)[row]));
550 }
551 sink.cells(first, run);
552 first = end;
553 }
554 }
555
556private:
557 std::shared_ptr<std::vector<Row> const> _rows;
558 RowKey<Row> _key;
559 std::shared_ptr<std::vector<detail::CellReader<Row>> const> _readers;
560};
561
562/// @brief A `DataSource` over typed rows: one column per key glaze writes,
563/// its kind inferred from the value's type.
564///
565/// `bool` is `Bool`; other integers `Integer` (an unsigned value above
566/// `INT64_MAX` is an invalid cell); floating point `Number`; `Rational`
567/// `Decimal`; a `Quantity` `Quantity`; `DateTime` and `Timestamp`
568/// `DateTime`; a type glaze writes as one of its members (`Ranged`,
569/// `Choice`, `Tagged`) that member's kind; everything else `Text` (an enum by
570/// its glaze name). A `std::optional` member's empty state, or anything glaze
571/// writes as `null`, is an empty cell. A row with a `glz::meta` has the
572/// columns its meta names. `setRows` replaces the rows and notifies `Reset`.
573/// @tparam Row A glaze-reflectable aggregate.
574template <typename Row>
575class RowsSource final : public DataSource {
576public:
577 /// @brief Builds the source.
578 /// @param rows The rows.
579 /// @param key The key projection.
580 /// @param kinds Column kinds that replace the inferred ones, by column
581 /// id (for example `Date` for an `int64` of epoch days).
582 /// @param comparators Comparator names by column id, from
583 /// `Services::comparators`; a column named here is `Custom`.
584 RowsSource(std::shared_ptr<std::vector<Row> const> rows, RowKey<Row> key,
585 std::map<std::string, ColumnKind, std::less<>> const& kinds = {},
586 std::map<std::string, std::string, std::less<>> const& comparators = {})
587 : _columns{detail::reflectedColumns<Row>()},
588 _readers{std::make_shared<std::vector<detail::CellReader<Row>> const>(detail::cellReaders<Row>())},
589 _key{std::move(key)},
590 _snapshot{std::make_shared<RowsSnapshot<Row>>(std::move(rows), _key, _readers)} {
591 for (auto& column : _columns) {
592 if (auto const found = kinds.find(column.id); found != kinds.end()) {
593 column.kind = found->second;
594 }
595 if (auto const found = comparators.find(column.id); found != comparators.end()) {
596 column.kind = ColumnKind::Custom;
597 column.comparator = found->second;
598 }
599 }
600 }
601
602 /// @brief The columns.
603 /// @return One per reflected member.
604 [[nodiscard]] std::span<ColumnInfo const> columns() const override { return _columns; }
605
606 /// @brief The current rows; a pointer copy.
607 /// @return The snapshot.
608 [[nodiscard]] std::shared_ptr<RowSnapshot const> snapshot() const override { return _snapshot; }
609
610 /// @brief Registers @p listener.
611 /// @param listener The listener.
612 void subscribe(ChangeListener& listener) override { _listeners.add(listener); }
613
614 /// @brief Removes @p listener; safe from inside a notification.
615 /// @param listener The listener.
616 void unsubscribe(ChangeListener& listener) override { _listeners.remove(listener); }
617
618 /// @brief Replaces the rows and notifies `Reset`.
619 /// @param rows The new rows.
620 void setRows(std::shared_ptr<std::vector<Row> const> rows) {
621 _snapshot = std::make_shared<RowsSnapshot<Row>>(std::move(rows), _key, _readers);
622 _listeners.notify(RowChange{.kind = ChangeKind::Reset, .rows = {}});
623 }
624
625private:
626 std::vector<ColumnInfo> _columns;
627 std::shared_ptr<std::vector<detail::CellReader<Row>> const> _readers;
628 RowKey<Row> _key;
629 std::shared_ptr<RowsSnapshot<Row> const> _snapshot;
630 detail::Listeners _listeners;
631};
632
633/// @brief Runs @p query over typed rows and returns the page of rows: what a
634/// server's list action returns.
635/// @tparam Row A glaze-reflectable aggregate.
636/// @param rows The server's rows; read during the call only.
637/// @param key The key projection.
638/// @param query The request body's table query.
639/// @param options Limits, services and column access.
640/// @param kinds Column kinds that replace the inferred ones.
641/// @param comparators Comparator names by column id; a column named here is `Custom`.
642/// @return The page, or the typed error the client shows.
643template <typename Row>
644[[nodiscard]] std::expected<Page<Row>, TableError> apply(
645 std::vector<Row> const& rows, RowKey<Row> key, TableQuery const& query, ApplyOptions const& options = {},
646 std::map<std::string, ColumnKind, std::less<>> const& kinds = {},
647 std::map<std::string, std::string, std::less<>> const& comparators = {}) {
648 // Borrowed for the call: an aliasing pointer that owns nothing.
649 std::shared_ptr<std::vector<Row> const> const borrowed{std::shared_ptr<void>{}, &rows};
650 RowsSource<Row> const source{borrowed, std::move(key), kinds, comparators};
651 auto page = apply(source, query, options);
652 if (!page) {
653 return std::unexpected(std::move(page.error()));
654 }
655 Page<Row> out;
656 out.total = page->total;
657 out.offset = page->offset;
658 out.rows.reserve(page->rows.size());
659 for (auto const row : page->rows) {
660 out.rows.push_back(rows[row]);
661 }
662 return out;
663}
664
665// ── Server mode on the client ───────────────────────────────────────────────
666
667/// @brief A request a `PageWindow` asks the client to send, tagged with the
668/// query it belongs to.
669struct PageFetch {
670 /// @brief The rows to request.
672 /// @brief The window's query when the fetch was made (`PageWindow::query`).
673 std::uint64_t query = 0;
674
675 /// @brief Member-wise equality.
676 /// @param other The fetch to compare with.
677 /// @return `true` when request and query match.
678 [[nodiscard]] bool operator==(PageFetch const& other) const = default;
679};
680
681/// @brief The pages a `server`-mode table holds: those around the visible
682/// rows, up to a bound, plus the request state and the last error.
683///
684/// The client calls `visible` as the view scrolls and sends the request of
685/// each fetch it returns, then hands the fetch back with its reply to
686/// `accept` (or with its error to `fail`). A change of sort or filter is a
687/// new query: call `reset`. A reply to a fetch made before the last `reset`
688/// is ignored, so a late page of the old query never shows under the new one.
689///
690/// A server may return fewer rows than a fetch asked for (`validate` clamps
691/// the limit to its `maxLimit`): the window then fetches the rest of the page
692/// from where the reply ended. A page is complete when it is full, reaches the
693/// known total, or a reply to it is empty.
694/// @tparam Row The row type.
695template <typename Row>
697public:
698 /// @brief An empty window.
699 /// @param pageSize Rows per page (default 200).
700 /// @param maxPages Pages held at most; the farthest from the visible rows go first.
701 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) - page size then page count, both documented above
702 explicit PageWindow(std::int64_t pageSize = 200, std::size_t maxPages = 8)
703 : _pageSize{std::max<std::int64_t>(pageSize, 1)}, _maxPages{std::max<std::size_t>(maxPages, 1)} {}
704
705 /// @brief Declares the visible rows; returns the fetches to send.
706 ///
707 /// Covers the visible rows and one page either side, skipping complete
708 /// pages, pages in flight, and pages past a known total. A page held in
709 /// part is fetched from where it ends. The returned pages are marked in
710 /// flight.
711 /// @param first First visible view row.
712 /// @param count Visible rows.
713 /// @return The fetches to send.
714 [[nodiscard]] std::vector<PageFetch> visible(std::int64_t first, std::int64_t count) {
715 _visibleFirst = std::max<std::int64_t>(first, 0);
716 _visibleCount = std::max<std::int64_t>(count, 0);
717 auto const firstPage = std::max<std::int64_t>((_visibleFirst / _pageSize) - 1, 0);
718 auto const lastPage = ((_visibleFirst + std::max<std::int64_t>(_visibleCount, 1) - 1) / _pageSize) + 1;
719 std::vector<PageFetch> out;
720 for (auto page = firstPage; page <= lastPage; ++page) {
721 if (_total && page * _pageSize >= *_total) {
722 break;
723 }
724 if (_complete.contains(page) || std::ranges::find(_inFlight, page) != _inFlight.end()) {
725 continue;
726 }
727 auto const held = _pages.find(page);
728 auto const have = held == _pages.end() ? std::int64_t{0} : static_cast<std::int64_t>(held->second.size());
729 _inFlight.push_back(page);
730 out.push_back(
731 PageFetch{.request = PageRequest{.offset = (page * _pageSize) + have, .limit = _pageSize - have},
732 .query = _query});
733 }
734 return out;
735 }
736
737 /// @brief Stores a reply.
738 /// @param fetch The fetch the reply answers, as `visible` returned it.
739 /// @param page The page the server returned.
740 /// @return `false` when the reply was ignored: its fetch belongs to an
741 /// earlier query, or is not awaited.
742 bool accept(PageFetch const& fetch, Page<Row> page) {
743 if (fetch.query != _query) {
744 return false;
745 }
746 auto const index = fetch.request.offset / _pageSize;
747 auto const flight = std::ranges::find(_inFlight, index);
748 if (flight == _inFlight.end()) {
749 return false;
750 }
751 _inFlight.erase(flight);
752 if (page.total) {
753 _total = page.total;
754 }
755 _error.reset();
756 auto& rows = _pages[index];
757 bool const replyEmpty = page.rows.empty();
758 if (fetch.request.offset == (index * _pageSize) + static_cast<std::int64_t>(rows.size())) {
759 auto const room = static_cast<std::size_t>(_pageSize) - rows.size();
760 auto const take = std::min(room, page.rows.size());
761 rows.insert(rows.end(), std::make_move_iterator(page.rows.begin()),
762 std::make_move_iterator(page.rows.begin() + static_cast<std::ptrdiff_t>(take)));
763 }
764 auto const end = (index * _pageSize) + static_cast<std::int64_t>(rows.size());
765 if (std::cmp_greater_equal(rows.size(), _pageSize) || (_total && end >= *_total) || replyEmpty) {
766 _complete.insert(index);
767 }
768 evict();
769 return true;
770 }
771
772 /// @brief Records a failed request; the error is shown until a reply arrives.
773 /// @param fetch The fetch that failed, as `visible` returned it.
774 /// @param error The server's error.
775 /// @return `false` when the failure was ignored: its fetch belongs to an earlier query.
776 bool fail(PageFetch const& fetch, TableError error) {
777 if (fetch.query != _query) {
778 return false;
779 }
780 std::erase(_inFlight, fetch.request.offset / _pageSize);
781 _error = std::move(error);
782 return true;
783 }
784
785 /// @brief Drops every page, request and error and starts a new query: the
786 /// sort or filter changed.
787 void reset() {
788 ++_query;
789 _pages.clear();
790 _complete.clear();
791 _inFlight.clear();
792 _total.reset();
793 _error.reset();
794 }
795
796 /// @brief The current query, which `reset` advances.
797 /// @return The query's number.
798 [[nodiscard]] std::uint64_t query() const noexcept { return _query; }
799
800 /// @brief The row at a view row, when it is held.
801 /// @param index View row.
802 /// @return The row, or null.
803 [[nodiscard]] Row const* row(std::int64_t index) const {
804 if (index < 0) {
805 return nullptr;
806 }
807 auto const found = _pages.find(index / _pageSize);
808 if (found == _pages.end()) {
809 return nullptr;
810 }
811 auto const offset = static_cast<std::size_t>(index % _pageSize);
812 return offset < found->second.size() ? &found->second[offset] : nullptr;
813 }
814
815 /// @brief The view's total rows, when a reply of this query reported it.
816 /// @return The total.
817 [[nodiscard]] std::optional<std::int64_t> total() const noexcept { return _total; }
818
819 /// @brief The last error, until a reply clears it.
820 /// @return The error, or nothing.
821 [[nodiscard]] std::optional<TableError> const& error() const noexcept { return _error; }
822
823 /// @brief Pages held, complete or in part.
824 /// @return Page count.
825 [[nodiscard]] std::size_t pagesHeld() const noexcept { return _pages.size(); }
826
827 /// @brief Whether any request is in flight.
828 /// @return `true` while a page is awaited.
829 [[nodiscard]] bool pending() const noexcept { return !_inFlight.empty(); }
830
831private:
832 void evict() {
833 auto const center = (_visibleFirst + (_visibleCount / 2)) / _pageSize;
834 while (_pages.size() > _maxPages) {
835 auto farthest = _pages.begin();
836 for (auto at = _pages.begin(); at != _pages.end(); ++at) {
837 if (std::abs(at->first - center) > std::abs(farthest->first - center)) {
838 farthest = at;
839 }
840 }
841 _complete.erase(farthest->first);
842 _pages.erase(farthest);
843 }
844 }
845
846 std::int64_t _pageSize;
847 std::size_t _maxPages;
848 std::uint64_t _query = 0;
849 std::map<std::int64_t, std::vector<Row>> _pages;
850 std::set<std::int64_t> _complete;
851 std::vector<std::int64_t> _inFlight;
852 std::optional<std::int64_t> _total;
853 std::optional<TableError> _error;
854 std::int64_t _visibleFirst = 0;
855 std::int64_t _visibleCount = 0;
856};
857
858// NOLINTEND(cppcoreguidelines-pro-bounds-avoid-unchecked-container-access)
859
860} // namespace morph::table
Handle< std::int64_t > count(AnyHandle const &list)
count: a list's length.
Definition author.hpp:1000
AnyHandle filter(AnyHandle const &source, std::string const &name, std::type_identity_t< std::function< Handle< bool >(Handle< E > const &)> > const &where)
A filter over a list, {"filter": e, "as": "x", "where": e}.
Definition author.hpp:1187
Receives a DataSource's change notifications, on the owner thread.
Receives a run of one column's cells.
virtual void cells(std::size_t firstRow, std::span< Cell const > cells)=0
Receives consecutive cells of one column.
The rows of a table, as the engine reads them.
virtual std::span< ColumnInfo const > columns() const =0
The table's columns.
virtual std::shared_ptr< RowSnapshot const > snapshot() const =0
The current rows as an immutable snapshot. Taking one is cheap.
bool accept(PageFetch const &fetch, Page< Row > page)
Stores a reply.
Definition query.hpp:742
void reset()
Drops every page, request and error and starts a new query: the sort or filter changed.
Definition query.hpp:787
std::optional< std::int64_t > total() const noexcept
The view's total rows, when a reply of this query reported it.
Definition query.hpp:817
std::vector< PageFetch > visible(std::int64_t first, std::int64_t count)
Declares the visible rows; returns the fetches to send.
Definition query.hpp:714
Row const * row(std::int64_t index) const
The row at a view row, when it is held.
Definition query.hpp:803
std::uint64_t query() const noexcept
The current query, which reset advances.
Definition query.hpp:798
std::size_t pagesHeld() const noexcept
Pages held, complete or in part.
Definition query.hpp:825
PageWindow(std::int64_t pageSize=200, std::size_t maxPages=8)
An empty window.
Definition query.hpp:702
bool fail(PageFetch const &fetch, TableError error)
Records a failed request; the error is shown until a reply arrives.
Definition query.hpp:776
std::optional< TableError > const & error() const noexcept
The last error, until a reply clears it.
Definition query.hpp:821
bool pending() const noexcept
Whether any request is in flight.
Definition query.hpp:829
RowId rowId(std::size_t row) const override
A row's key.
Definition query.hpp:525
Cell cell(std::size_t row, std::size_t column) const override
One cell.
Definition query.hpp:531
void readColumn(std::size_t column, ColumnSink &sink, std::size_t first, std::size_t last) const override
Hands rows [first, last) of column to sink in runs.
Definition query.hpp:540
RowsSnapshot(std::shared_ptr< std::vector< Row > const > rows, RowKey< Row > key, std::shared_ptr< std::vector< detail::CellReader< Row > > const > readers)
Wraps rows.
Definition query.hpp:514
std::size_t rowCount() const override
The number of rows.
Definition query.hpp:520
A DataSource over typed rows: one column per key glaze writes, its kind inferred from the value's typ...
Definition query.hpp:575
std::span< ColumnInfo const > columns() const override
The columns.
Definition query.hpp:604
void subscribe(ChangeListener &listener) override
Registers listener.
Definition query.hpp:612
void unsubscribe(ChangeListener &listener) override
Removes listener; safe from inside a notification.
Definition query.hpp:616
RowsSource(std::shared_ptr< std::vector< Row > const > rows, RowKey< Row > key, std::map< std::string, ColumnKind, std::less<> > const &kinds={}, std::map< std::string, std::string, std::less<> > const &comparators={})
Builds the source.
Definition query.hpp:584
std::shared_ptr< RowSnapshot const > snapshot() const override
The current rows; a pointer copy.
Definition query.hpp:608
void setRows(std::shared_ptr< std::vector< Row > const > rows)
Replaces the rows and notifies Reset.
Definition query.hpp:620
A source's listeners, notified so that a listener may unsubscribe itself or another,...
One recomputation of a view, runnable in steps.
Definition engine.hpp:283
JobResult takeResult()
Moves the result out. Valid once done(); the input stays readable.
Definition engine.hpp:336
Node id(Element const &element) const
The element id, by which relations read its values.
Definition author.hpp:1818
A type glaze writes as one of its own members, as morph's Ranged, Choice, Multiline and Tagged are: i...
Definition query.hpp:363
The rows a table engine reads, and the services it is given.
std::variant< std::monostate, std::int64_t, math::Rational, double, std::string, bool, QuantityCell > Cell
One cell as a data source hands it out.
@ Reset
Everything may have changed, the columns included.
ColumnKind
What a column holds, which decides how its keys are built and compared.
@ Quantity
An exact Rational in the column's canonical unit.
@ DateTime
UTC seconds since 1970-01-01T00:00:00Z.
@ Decimal
An exact Rational.
@ Text
Text, ordered and matched through the TextCollator.
@ Integer
int64, compared numerically.
@ Number
A double; the only kind that holds one.
@ Bool
false before true.
@ LimitExceeded
A TableQuery over one of the server's bounds.
@ ServiceFailed
An injected service (collator, date parser, comparator) threw.
@ ColumnNotReadable
A sort or filter on a column the principal may not read.
std::variant< std::int64_t, std::string > RowId
A row's identity: the table's key, stable across reloads.
UTC timestamps for morph actions.
The table engine: sort, filter and view mapping, on and off the owner.
Filter specs as data, compiled once and evaluated column at a time.
TableError unknownColumn(std::string const &columnId)
An UnknownColumn error.
Definition filter.hpp:511
std::optional< std::size_t > findColumn(std::span< ColumnInfo const > columns, std::string_view columnId)
The index of the column with id columnId.
Definition filter.hpp:498
Unit-tagged, optionally-empty exact values with a derivation trail.
constexpr bool isQuantity
true when T (cvref-stripped) is a morph::units::Quantity.
Definition quantity.hpp:950
std::string escapeLikePattern(std::string_view text, char escape='\\')
Escapes %, _ and the escape character for a SQL LIKE pattern, so a database-backed server keeps them ...
Definition query.hpp:250
std::remove_cvref_t< decltype(std::declval< V const & >().*(glz::meta< V >::value))> WrappedMember
The member a MemberWrapper is written as.
Definition query.hpp:369
std::vector< ColumnInfo > reflectedColumns()
One column per key glaze writes, its kind from the value's type.
Definition query.hpp:484
std::expected< TableQuery, TableError > validate(TableQuery query, std::span< ColumnInfo const > columns, QueryLimits const &limits={}, std::function< bool(std::string_view)> const &mayRead={})
Checks query against the table's columns and limits.
Definition query.hpp:220
TableError limitError(std::string column, std::string message)
A LimitExceeded error.
Definition query.hpp:121
Cell writtenCell(T const &value)
A member as the text glaze writes for it: an enum by its glaze name, a JSON string decoded,...
Definition query.hpp:420
std::function< RowId(Row const &)> RowKey
A key projection: the RowId of a typed row.
Definition query.hpp:503
std::optional< TableError > checkFilters(FilterSpec const &filters, std::span< ColumnInfo const > columns, QueryLimits const &limits, std::function< bool(std::string_view)> const &mayRead)
Checks a filter's columns, entry counts and value lengths.
Definition query.hpp:170
Cell toCell(T const &value)
A member value as a cell.
Definition query.hpp:439
std::optional< TableError > checkColumn(std::string const &columnId, std::span< ColumnInfo const > columns, std::function< bool(std::string_view)> const &mayRead)
Checks that a column exists and the principal may read it.
Definition query.hpp:150
Cell(*)(Row const &) CellReader
Reads one column's cell from a row.
Definition query.hpp:466
bool entryTooLong(FilterEntry const &entry, std::size_t max)
Whether any operand of an entry is longer than max bytes.
Definition query.hpp:130
bool anyTooLong(std::span< FilterEntry const > entries, std::size_t max)
Whether any of entries has an operand longer than max bytes.
Definition query.hpp:141
constexpr ColumnKind kindOf()
The column kind a member type gives.
Definition query.hpp:375
Cell integerCell(T value)
An integer member as a cell.
Definition query.hpp:405
std::expected< PageRows, TableError > apply(DataSource const &source, TableQuery const &query, ApplyOptions const &options={})
Runs query over source in the calling thread: the reference implementation of server mode.
Definition query.hpp:289
std::vector< CellReader< Row > > cellReaders()
One cell reader per key glaze writes, in key order.
Definition query.hpp:472
The value glaze reads and writes under a reflected key.
constexpr decltype(auto) reflectedMember(T &object)
The value glaze reads and writes under glz::reflect<T>::keys[I].
Typed sort keys and the chunked, stoppable merge sort over them.
constexpr Deadline kNoDeadline
The deadline that never passes.
Definition sort.hpp:84
How apply runs.
Definition query.hpp:263
Services services
The server's collator, date parser and comparators. Its collator decides the order of text in the pag...
Definition query.hpp:268
QueryLimits limits
The bounds of validate.
Definition query.hpp:265
std::function< bool(std::string_view)> mayRead
Whether the principal may read a column; empty allows every column.
Definition query.hpp:270
A column's identity and kind.
std::string comparator
For a Custom column, the name of its comparator in Services::comparators.
One filter condition. Exactly one operator member is set.
Definition filter.hpp:49
std::optional< std::string > contains
Text contains. Text columns.
Definition filter.hpp:65
std::optional< std::string > ge
Greater than or equal to. Numeric, date and date-time columns.
Definition filter.hpp:61
std::optional< std::string > ne
Not equal to.
Definition filter.hpp:53
std::optional< std::array< std::string, 2 > > between
Between two values, both inclusive. Numeric, date and date-time columns.
Definition filter.hpp:63
std::optional< std::string > eq
Equal to (text: at primary strength unless caseSensitive).
Definition filter.hpp:51
std::optional< std::string > endsWith
Text ends with. Text columns.
Definition filter.hpp:69
std::optional< std::string > gt
Greater than. Numeric, date and date-time columns.
Definition filter.hpp:59
std::optional< std::string > lt
Less than. Numeric, date and date-time columns.
Definition filter.hpp:55
std::optional< std::string > le
Less than or equal to. Numeric, date and date-time columns.
Definition filter.hpp:57
std::optional< std::string > startsWith
Text starts with. Text columns.
Definition filter.hpp:67
A table's whole filter.
Definition filter.hpp:106
std::map< std::string, ColumnFilter > columns
Per-column filters, by column id.
Definition filter.hpp:108
std::vector< GroupFilter > groups
Multi-column groups.
Definition filter.hpp:110
A request a PageWindow asks the client to send, tagged with the query it belongs to.
Definition query.hpp:669
std::uint64_t query
The window's query when the fetch was made (PageWindow::query).
Definition query.hpp:673
PageRequest request
The rows to request.
Definition query.hpp:671
bool operator==(PageFetch const &other) const =default
Member-wise equality.
Which rows of the view a request wants.
Definition query.hpp:51
std::int64_t limit
Rows wanted.
Definition query.hpp:55
bool operator==(PageRequest const &other) const =default
Member-wise equality.
std::int64_t offset
First view row.
Definition query.hpp:53
The rows of one page, as source row indices.
Definition query.hpp:274
std::int64_t offset
The view row of rows[0].
Definition query.hpp:280
std::vector< std::size_t > rows
Source rows, in view order.
Definition query.hpp:276
std::optional< std::int64_t > total
Rows in the whole filtered view, unless above QueryLimits::totalCap.
Definition query.hpp:278
One page of a list action's reply.
Definition query.hpp:83
std::int64_t offset
The view row of rows[0].
Definition query.hpp:90
std::optional< std::int64_t > total
Rows in the whole filtered view; absent when the server did not count them or the count is above its ...
Definition query.hpp:88
std::vector< Row > rows
The rows, in view order.
Definition query.hpp:85
The bounds a server applies to every TableQuery, an untrusted body.
Definition query.hpp:94
std::size_t maxGroups
Groups.
Definition query.hpp:106
std::size_t maxSortKeys
Sort keys.
Definition query.hpp:100
std::size_t maxGroupColumns
Columns of one group.
Definition query.hpp:108
std::size_t maxTextLength
Bytes of one filter value.
Definition query.hpp:110
std::int64_t maxOffset
page.offset above this is refused.
Definition query.hpp:98
std::int64_t totalCap
A total above this is not reported.
Definition query.hpp:112
std::size_t maxFilterColumns
Filtered columns.
Definition query.hpp:102
std::size_t maxEntries
include plus exclude entries of one column or group.
Definition query.hpp:104
std::int64_t maxLimit
page.limit above this is clamped to it.
Definition query.hpp:96
One change notification.
The services an engine sorts and filters with. A null member is replaced by its default.
The error every fallible table call returns.
A table's sort, filters and page, as a list action's body carries them.
Definition query.hpp:67
static constexpr std::string_view schemaMarker() noexcept
The schema keyword that marks a member of this type.
Definition query.hpp:77
std::vector< SortKey > sort
The sort chain.
Definition query.hpp:69
FilterSpec filters
The filter.
Definition query.hpp:71
PageRequest page
The page.
Definition query.hpp:73
true for std::optional<T>.
Definition query.hpp:353
Everything one view job reads. Immutable once the job starts.
Definition engine.hpp:220
std::shared_ptr< RowSnapshot const > snapshot
The rows.
Definition engine.hpp:222
std::vector< std::uint32_t > view
The sorted, filtered view.
Definition engine.hpp:264