morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
data_source.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5/// @file table/data_source.hpp
6/// @brief The rows a table engine reads, and the services it is given.
7///
8/// A `DataSource` publishes a table's columns, an immutable `RowSnapshot` of
9/// its rows, and notifications when rows change. The engine reads cells only
10/// through a snapshot, never through the source, so a snapshot can be handed
11/// to a worker thread while the owner keeps changing the source.
12///
13/// The injected services (`TextCollator`, `DateParser`, `ProgressSink`, named
14/// comparators) are the only locale- and toolkit-dependent parts of sorting
15/// and filtering; each has a deterministic default here.
16///
17/// See `docs/spec/table/engine.md`.
18
19#include <algorithm>
20#include <array>
21#include <chrono>
22#include <compare>
23#include <cstddef>
24#include <cstdint>
25#include <expected>
26#include <format>
27#include <functional>
28#include <limits>
29#include <map>
30#include <memory>
31#include <morph/core/callback_scope.hpp>
35#include <optional>
36#include <span>
37#include <string>
38#include <string_view>
39#include <unordered_map>
40#include <utility>
41#include <variant>
42#include <vector>
43
44namespace morph::table {
45
46// 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
47
48/// @brief What a column holds, which decides how its keys are built and compared.
49enum class ColumnKind : std::uint8_t {
50 Integer, ///< `int64`, compared numerically.
51 Decimal, ///< An exact `Rational`.
52 Quantity, ///< An exact `Rational` in the column's canonical unit.
53 Number, ///< A `double`; the only kind that holds one.
54 Text, ///< Text, ordered and matched through the `TextCollator`.
55 Date, ///< Days since 1970-01-01.
56 DateTime, ///< UTC seconds since 1970-01-01T00:00:00Z.
57 Bool, ///< `false` before `true`.
58 Key, ///< An `int64` key, compared numerically.
59 Custom, ///< Ordered by a named comparator from `Services::comparators`.
60};
61
62/// @brief A row's identity: the table's key, stable across reloads.
63using RowId = std::variant<std::int64_t, std::string>;
64
65/// @brief A quantity in its own unit, with the factor that converts it to the
66/// column's canonical unit.
67///
68/// The engine multiplies the two with checked arithmetic; a product that does
69/// not fit is an invalid cell, never a saturated one. A plain `Rational` in a
70/// `Quantity` column is taken as already canonical.
72 /// @brief The amount in the cell's own unit.
74 /// @brief Multiplier from the cell's unit to the column's canonical unit.
76};
77
78/// @brief One cell as a data source hands it out.
79///
80/// `std::monostate` is an empty cell. A `std::string` in a non-text column is
81/// parsed for the column's kind when its key is built; text that does not parse
82/// is an invalid cell.
83using Cell = std::variant<std::monostate, std::int64_t, math::Rational, double, std::string, bool, QuantityCell>;
84
85/// @brief A column's identity and kind.
86struct ColumnInfo {
87 /// @brief The column's id, unique within the table (the row member's name).
88 std::string id;
89 /// @brief How the column's keys are built and compared.
91 /// @brief For a `Custom` column, the name of its comparator in `Services::comparators`.
92 std::string comparator;
93 /// @brief For a `Date` or `DateTime` column holding text, the format handed to the `DateParser`.
94 std::string format;
95
96 /// @brief Member-wise equality.
97 /// @param other The column to compare with.
98 /// @return `true` when id, kind, comparator and format match.
99 [[nodiscard]] bool operator==(ColumnInfo const& other) const = default;
100};
101
102/// @brief What went wrong in a fallible table call.
103enum class TableErrorCode : std::uint8_t {
104 UnknownColumn, ///< A sort or filter names a column the table does not have.
105 UnsupportedOperator, ///< A filter operator the column's kind does not define.
106 InvalidValue, ///< A filter value that does not parse for its column's kind.
107 InvalidSpec, ///< A filter entry with no operator or more than one.
108 LimitExceeded, ///< A `TableQuery` over one of the server's bounds.
109 ColumnNotReadable, ///< A sort or filter on a column the principal may not read.
110 Unavailable, ///< An operation the table's mode does not offer.
111 ServiceFailed, ///< An injected service (collator, date parser, comparator) threw.
112};
113
114/// @brief The error every fallible table call returns.
116 /// @brief What went wrong.
118 /// @brief The column concerned, or empty when the error is not about one column.
119 std::string column;
120 /// @brief A human-readable description.
121 std::string message;
122};
123
124/// @brief Parses exact decimal text (`"-12.50"`) into a `Rational`.
125///
126/// Accepts an optional sign, digits, and an optional fraction of at most 18
127/// digits; nothing else (no exponent, no grouping, no surrounding space). The
128/// result carries as many decimal places as the text had.
129/// @param text The decimal text.
130/// @return The exact value, or `InvalidValue` when the text is not a decimal or
131/// does not fit an `int64` numerator.
132[[nodiscard]] inline std::expected<math::Rational, TableError> parseDecimal(std::string_view text) {
133 auto const fail = [&] {
134 return std::unexpected(TableError{
135 .code = TableErrorCode::InvalidValue, .column = {}, .message = "not a decimal: " + std::string{text}});
136 };
137 std::size_t cursor = 0;
138 bool negative = false;
139 if (cursor < text.size() && (text[cursor] == '-' || text[cursor] == '+')) {
140 negative = text[cursor] == '-';
141 ++cursor;
142 }
143 std::uint64_t magnitude = 0;
144 std::uint32_t fractionDigits = 0;
145 std::size_t digits = 0;
146 bool inFraction = false;
147 // The largest magnitude whose negation still fits: |INT64_MIN| is one more
148 // than INT64_MAX, and parsing to that bound keeps "-9223372036854775808" exact.
149 auto const bound = negative ? static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max()) + 1U
150 : static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max());
151 for (; cursor < text.size(); ++cursor) {
152 char const character = text[cursor];
153 if (character == '.' && !inFraction) {
154 inFraction = true;
155 continue;
156 }
157 if (character < '0' || character > '9') {
158 return fail();
159 }
160 auto const digit = static_cast<std::uint64_t>(character - '0');
161 if (magnitude > (bound - digit) / 10U) {
162 return fail();
163 }
164 magnitude = (magnitude * 10U) + digit;
165 ++digits;
166 if (inFraction) {
167 ++fractionDigits;
168 }
169 }
170 if (digits == 0 || fractionDigits > math::kMaxDecimalPlaces) {
171 return fail();
172 }
173 std::int64_t const numerator =
174 negative ? static_cast<std::int64_t>(0U - magnitude) : static_cast<std::int64_t>(magnitude);
175 if (fractionDigits == 0) {
176 // The whole-number constructor keeps INT64_MIN; the canonicalising
177 // one would have to negate it and clamps it instead.
178 return math::Rational{numerator, math::DecimalPlaces{0}};
179 }
180 if (numerator == std::numeric_limits<std::int64_t>::min()) {
181 return fail();
182 }
183 std::int64_t denominator = 1;
184 for (std::uint32_t i = 0; i < fractionDigits; ++i) {
185 denominator *= 10;
186 }
187 return math::Rational{math::Numerator{numerator}, math::Denominator{denominator},
188 math::DecimalPlaces{fractionDigits}};
189}
190
191/// @brief The text a cell shows when it has to be ordered or matched as text.
192/// @param cell The cell.
193/// @return Its text: the string itself, a number in decimal form (a decimal
194/// to its own places, `"12.5"`), `true` or `false`, or empty for an
195/// empty cell.
196[[nodiscard]] inline std::string displayText(Cell const& cell) {
197 return std::visit(
198 []<typename T>(T const& value) -> std::string {
199 if constexpr (std::is_same_v<T, std::monostate>) {
200 return {};
201 } else if constexpr (std::is_same_v<T, std::string>) {
202 return value;
203 } else if constexpr (std::is_same_v<T, bool>) {
204 return value ? "true" : "false";
205 } else if constexpr (std::is_same_v<T, QuantityCell>) {
206 return units::detail::formatRationalDecimal(value.amount);
207 } else if constexpr (std::is_same_v<T, math::Rational>) {
208 return units::detail::formatRationalDecimal(value);
209 } else {
210 return std::format("{}", value);
211 }
212 },
213 cell);
214}
215
216// ── Services ────────────────────────────────────────────────────────────────
217
218/// @brief Orders and matches text. The renderer injects the toolkit's collator
219/// for the user's locale; `CodePointCollator` is the default.
220///
221/// Called from the engine's worker. An implementation either is safe to call
222/// concurrently, and returns null from `cloneForTask`, or returns a fresh
223/// instance there that one task uses alone.
224// NOLINTBEGIN(cppcoreguidelines-special-member-functions)
226public:
227 virtual ~TextCollator() = default;
228
229 /// @brief A key whose byte order is the collation order of @p text.
230 /// @param text The text to order.
231 /// @return The collation key.
232 [[nodiscard]] virtual std::string sortKey(std::string_view text) const = 0;
233
234 /// @brief @p text folded to primary strength: two texts a user would call
235 /// equal ignoring case and accents fold to the same bytes.
236 /// @param text The text to fold.
237 /// @return The folded text, which filters match against.
238 [[nodiscard]] virtual std::string fold(std::string_view text) const = 0;
239
240 /// @brief An instance for one task to use alone.
241 /// @return Null when this instance is safe to call concurrently, else a copy.
242 [[nodiscard]] virtual std::shared_ptr<TextCollator const> cloneForTask() const { return nullptr; }
243};
244// NOLINTEND(cppcoreguidelines-special-member-functions)
245
246/// @brief The default collator: code-point order, with ASCII and Latin-1
247/// letters case-folded. It does not fold accents.
248///
249/// Stateless and safe to call concurrently.
250class CodePointCollator final : public TextCollator {
251public:
252 /// @brief The case-folded text; its bytes order as its code points.
253 /// @param text UTF-8 text.
254 /// @return The collation key.
255 [[nodiscard]] std::string sortKey(std::string_view text) const override { return fold(text); }
256
257 /// @brief ASCII `A`-`Z` and Latin-1 `À`-`Þ` (except `×`) lowered; every
258 /// other byte copied as it is.
259 /// @param text UTF-8 text.
260 /// @return The folded text.
261 [[nodiscard]] std::string fold(std::string_view text) const override {
262 std::string out;
263 out.reserve(text.size());
264 for (std::size_t i = 0; i < text.size(); ++i) {
265 auto const byte = static_cast<unsigned char>(text[i]);
266 if (byte >= 'A' && byte <= 'Z') {
267 out.push_back(static_cast<char>(byte + ('a' - 'A')));
268 continue;
269 }
270 // U+00C0..U+00DE encode as 0xC3 0x80..0x9E; lowering adds 0x20 to
271 // the second byte. U+00D7 (multiplication sign) has no lower case.
272 if (byte == 0xC3 && i + 1 < text.size()) {
273 auto const next = static_cast<unsigned char>(text[i + 1]);
274 if (next >= 0x80 && next <= 0x9E && next != 0x97) {
275 out.push_back(text[i]);
276 out.push_back(static_cast<char>(next + 0x20));
277 ++i;
278 continue;
279 }
280 }
281 out.push_back(text[i]);
282 }
283 return out;
284 }
285};
286
287/// @brief Parses text to dates and date-times for `Date` and `DateTime`
288/// columns that hold text, and for filter values on those columns.
289///
290/// The same thread-safety contract as `TextCollator`.
291// NOLINTBEGIN(cppcoreguidelines-special-member-functions)
293public:
294 virtual ~DateParser() = default;
295
296 /// @brief Parses a date.
297 /// @param text The text.
298 /// @param format The column's format; empty for the parser's default.
299 /// @return Days since 1970-01-01, or nothing when @p text is not a date.
300 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) - text then format, as every parser reads them
301 [[nodiscard]] virtual std::optional<std::int64_t> parseDate(std::string_view text,
302 std::string_view format) const = 0;
303
304 /// @brief Parses a date-time.
305 /// @param text The text.
306 /// @param format The column's format; empty for the parser's default.
307 /// @return UTC seconds since the epoch, or nothing when @p text is not a date-time.
308 [[nodiscard]] virtual std::optional<std::int64_t> parseDateTime(std::string_view text,
309 std::string_view format) const = 0;
310
311 /// @brief An instance for one task to use alone.
312 /// @return Null when this instance is safe to call concurrently, else a copy.
313 [[nodiscard]] virtual std::shared_ptr<DateParser const> cloneForTask() const { return nullptr; }
314};
315// NOLINTEND(cppcoreguidelines-special-member-functions)
316
317/// @brief The default date parser: ISO 8601 only, whatever the format.
318///
319/// A date is `YYYY-MM-DD`. A date-time is what `morph::time::DateTime::fromIso8601`
320/// reads, or a bare date, which is midnight UTC. Stateless and safe to call
321/// concurrently.
322class IsoDateParser final : public DateParser {
323public:
324 /// @brief Parses `YYYY-MM-DD`.
325 /// @param text The text.
326 /// @param format Ignored.
327 /// @return Days since 1970-01-01, or nothing.
328 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) - text then format, as every parser reads them
329 [[nodiscard]] std::optional<std::int64_t> parseDate(std::string_view text,
330 std::string_view format) const override {
331 static_cast<void>(format);
332 if (text.size() != 10 || text[4] != '-' || text[7] != '-') {
333 return std::nullopt;
334 }
335 auto const number = [&](std::size_t offset, std::size_t count) -> std::optional<int> {
336 int value = 0;
337 for (std::size_t i = offset; i < offset + count; ++i) {
338 if (text[i] < '0' || text[i] > '9') {
339 return std::nullopt;
340 }
341 value = (value * 10) + (text[i] - '0');
342 }
343 return value;
344 };
345 auto const year = number(0, 4);
346 auto const month = number(5, 2);
347 auto const day = number(8, 2);
348 if (!year || !month || !day) {
349 return std::nullopt;
350 }
351 std::chrono::year_month_day const date{std::chrono::year{*year},
352 std::chrono::month{static_cast<unsigned>(*month)},
353 std::chrono::day{static_cast<unsigned>(*day)}};
354 if (!date.ok()) {
355 return std::nullopt;
356 }
357 return std::chrono::sys_days{date}.time_since_epoch().count();
358 }
359
360 /// @brief Parses an ISO 8601 date-time, or a bare date as midnight UTC.
361 /// @param text The text.
362 /// @param format Ignored.
363 /// @return UTC seconds since the epoch, or nothing.
364 [[nodiscard]] std::optional<std::int64_t> parseDateTime(std::string_view text,
365 std::string_view format) const override {
366 if (auto const days = parseDate(text, format)) {
367 return *days * std::int64_t{86400};
368 }
369 auto const instant = time::DateTime::fromIso8601(text);
370 if (!instant) {
371 return std::nullopt;
372 }
373 return std::chrono::floor<std::chrono::seconds>(instant->value).time_since_epoch().count();
374 }
375};
376
377/// @brief Receives progress of a long sort or filter, on the owner thread.
378// NOLINTBEGIN(cppcoreguidelines-special-member-functions)
380public:
381 virtual ~ProgressSink() = default;
382
383 /// @brief Reports progress through the current job.
384 /// @param done Work units finished.
385 /// @param total Work units in the job.
386 virtual void progress(std::size_t done, std::size_t total) = 0;
387};
388// NOLINTEND(cppcoreguidelines-special-member-functions)
389
390/// @brief Orders two cells of a `Custom` column. Called from the worker, so it
391/// must be safe to call concurrently.
392using CellComparator = std::function<std::strong_ordering(Cell const&, Cell const&)>;
393
394/// @brief The services an engine sorts and filters with. A null member is
395/// replaced by its default.
396struct Services {
397 /// @brief Text order and matching; null means `CodePointCollator`.
398 std::shared_ptr<TextCollator const> collator;
399 /// @brief Date parsing; null means `IsoDateParser`.
400 std::shared_ptr<DateParser const> dates;
401 /// @brief Comparators for `Custom` columns, by the name `ColumnInfo::comparator` gives.
402 std::map<std::string, CellComparator, std::less<>> comparators;
403 /// @brief Optional progress receiver.
404 std::shared_ptr<ProgressSink> progress;
405
406 /// @brief The collator to use.
407 /// @return `collator`, or the shared default.
408 [[nodiscard]] std::shared_ptr<TextCollator const> collatorOrDefault() const {
409 static auto const fallback = std::make_shared<CodePointCollator const>();
410 return collator != nullptr ? collator : fallback;
411 }
412
413 /// @brief The date parser to use.
414 /// @return `dates`, or the shared default.
415 [[nodiscard]] std::shared_ptr<DateParser const> datesOrDefault() const {
416 static auto const fallback = std::make_shared<IsoDateParser const>();
417 return dates != nullptr ? dates : fallback;
418 }
419
420 /// @brief The services one task uses: collator and parser cloned when they
421 /// are not safe to share, defaults filled in.
422 /// @return A copy fit to hand to a worker.
423 [[nodiscard]] Services forTask() const {
424 Services out = *this;
426 if (auto clone = out.collator->cloneForTask()) {
427 out.collator = std::move(clone);
428 }
429 out.dates = datesOrDefault();
430 if (auto clone = out.dates->cloneForTask()) {
431 out.dates = std::move(clone);
432 }
433 return out;
434 }
435};
436
437// ── Snapshots ───────────────────────────────────────────────────────────────
438
439/// @brief Receives a run of one column's cells.
440// NOLINTBEGIN(cppcoreguidelines-special-member-functions)
442public:
443 virtual ~ColumnSink() = default;
444
445 /// @brief Receives consecutive cells of one column.
446 /// @param firstRow The row of `cells[0]`.
447 /// @param cells The cells of rows `firstRow`, `firstRow + 1`, ….
448 virtual void cells(std::size_t firstRow, std::span<Cell const> cells) = 0;
449};
450// NOLINTEND(cppcoreguidelines-special-member-functions)
451
452/// @brief An immutable view of a table's rows at one moment.
453///
454/// Nothing changes a snapshot after it is published, so it is safe to read
455/// from any thread, concurrently, for as long as it is held.
456// NOLINTBEGIN(cppcoreguidelines-special-member-functions)
457class RowSnapshot {
458public:
459 RowSnapshot() = default;
460 virtual ~RowSnapshot() = default;
461
462 /// @brief The number of rows.
463 /// @return Row count.
464 [[nodiscard]] virtual std::size_t rowCount() const = 0;
465
466 /// @brief A row's key.
467 /// @param row Row index, below `rowCount()`.
468 /// @return The row's `RowId`.
469 [[nodiscard]] virtual RowId rowId(std::size_t row) const = 0;
470
471 /// @brief One cell.
472 /// @param row Row index, below `rowCount()`.
473 /// @param column Column index, below the source's column count.
474 /// @return The cell, by value.
475 [[nodiscard]] virtual Cell cell(std::size_t row, std::size_t column) const = 0;
476
477 /// @brief Hands rows `[first, last)` of @p column to @p sink.
478 ///
479 /// The default calls `cell` per row. A snapshot that stores cells
480 /// contiguously overrides it to hand whole runs, which is how the engine
481 /// reads a column of 100,000 rows without as many virtual calls.
482 /// @param column Column index.
483 /// @param sink Receives the cells.
484 /// @param first First row.
485 /// @param last One past the last row.
486 virtual void readColumn(std::size_t column, ColumnSink& sink, std::size_t first, std::size_t last) const {
487 for (std::size_t row = first; row < last; ++row) {
488 Cell const value = cell(row, column);
489 sink.cells(row, std::span<Cell const>{&value, 1});
490 }
491 }
492
493protected:
494 /// @brief Copies the base of a snapshot: a derived snapshot makes a changed
495 /// copy of itself this way.
496 RowSnapshot(RowSnapshot const&) = default;
497 /// @brief Moves the base of a snapshot.
498 RowSnapshot(RowSnapshot&&) = default;
499 /// @brief Copy-assigns the base of a snapshot.
500 /// @return `*this`.
501 RowSnapshot& operator=(RowSnapshot const&) = default;
502 /// @brief Move-assigns the base of a snapshot.
503 /// @return `*this`.
504 RowSnapshot& operator=(RowSnapshot&&) = default;
505};
506// NOLINTEND(cppcoreguidelines-special-member-functions)
507
508/// @brief A `RowSnapshot` that stores rows in shared, immutable chunks.
509///
510/// Changing one row produces a new snapshot that shares every chunk but the
511/// one holding the row, so a stream of updates against a table a worker is
512/// still reading costs one chunk copy per update, not one table copy.
513class TableSnapshot final : public RowSnapshot {
514public:
515 /// @brief Rows per chunk: small enough that copying one is cheap, large
516 /// enough that a column read hands long runs.
517 static constexpr std::size_t kChunkRows = 256;
518
519 /// @brief Constructs an empty snapshot.
520 /// @param columnCount Number of columns every row has.
521 explicit TableSnapshot(std::size_t columnCount) : _columnCount{columnCount} {}
522
523 /// @brief Builds a snapshot from rows.
524 /// @param columnCount Number of columns.
525 /// @param ids The rows' keys.
526 /// @param rows The rows' cells; each row has @p columnCount cells
527 /// (missing cells are empty, extra cells are dropped).
528 /// @return The snapshot.
529 [[nodiscard]] static std::shared_ptr<TableSnapshot const> fromRows(std::size_t columnCount, std::vector<RowId> ids,
530 std::vector<std::vector<Cell>> rows) {
531 auto out = std::make_shared<TableSnapshot>(columnCount);
532 out->assign(std::move(ids), std::move(rows), 0);
533 return out;
534 }
535
536 /// @brief The number of rows.
537 /// @return Row count.
538 [[nodiscard]] std::size_t rowCount() const override { return _rows; }
539
540 /// @brief The number of columns.
541 /// @return Column count.
542 [[nodiscard]] std::size_t columnCount() const noexcept { return _columnCount; }
543
544 /// @brief A row's key.
545 /// @param row Row index.
546 /// @return The key.
547 [[nodiscard]] RowId rowId(std::size_t row) const override {
548 return _chunks[row / kChunkRows]->ids[row % kChunkRows];
549 }
550
551 /// @brief One cell.
552 /// @param row Row index.
553 /// @param column Column index.
554 /// @return The cell.
555 [[nodiscard]] Cell cell(std::size_t row, std::size_t column) const override {
556 return _chunks[row / kChunkRows]->columns[column][row % kChunkRows];
557 }
558
559 /// @brief Hands rows `[first, last)` of @p column to @p sink, one run per chunk.
560 /// @param column Column index.
561 /// @param sink Receives the cells.
562 /// @param first First row.
563 /// @param last One past the last row.
564 void readColumn(std::size_t column, ColumnSink& sink, std::size_t first, std::size_t last) const override {
565 while (first < last) {
566 auto const& chunk = *_chunks[first / kChunkRows];
567 auto const offset = first % kChunkRows;
568 auto const count = std::min(last - first, chunk.ids.size() - offset);
569 sink.cells(first, std::span<Cell const>{chunk.columns[column]}.subspan(offset, count));
570 first += count;
571 }
572 }
573
574 /// @brief A copy of this snapshot with one row's cells replaced. Only that
575 /// row's chunk is copied.
576 /// @param row Row index.
577 /// @param cells The new cells.
578 /// @return The new snapshot.
579 [[nodiscard]] std::shared_ptr<TableSnapshot const> withRow(std::size_t row, std::vector<Cell> cells) const {
580 auto out = std::make_shared<TableSnapshot>(*this);
581 auto chunk = std::make_shared<Chunk>(*_chunks[row / kChunkRows]);
582 cells.resize(_columnCount);
583 for (std::size_t column = 0; column < _columnCount; ++column) {
584 chunk->columns[column][row % kChunkRows] = std::move(cells[column]);
585 }
586 out->_chunks[row / kChunkRows] = std::move(chunk);
587 return out;
588 }
589
590 /// @brief A copy with a row inserted before @p row. Chunks before it are shared.
591 /// @param row Insert position, at most `rowCount()`.
592 /// @param key The new row's key.
593 /// @param cells The new row's cells.
594 /// @return The new snapshot.
595 [[nodiscard]] std::shared_ptr<TableSnapshot const> withInserted(std::size_t row, RowId key,
596 std::vector<Cell> cells) const {
597 auto [ids, rows] = tail(row);
598 ids.insert(ids.begin(), std::move(key));
599 rows.insert(rows.begin(), std::move(cells));
600 auto out = std::make_shared<TableSnapshot>(*this);
601 out->assign(std::move(ids), std::move(rows), row);
602 return out;
603 }
604
605 /// @brief A copy with row @p row removed. Chunks before it are shared.
606 /// @param row Row index.
607 /// @return The new snapshot.
608 [[nodiscard]] std::shared_ptr<TableSnapshot const> withRemoved(std::size_t row) const {
609 auto [ids, rows] = tail(row);
610 ids.erase(ids.begin());
611 rows.erase(rows.begin());
612 auto out = std::make_shared<TableSnapshot>(*this);
613 out->assign(std::move(ids), std::move(rows), row);
614 return out;
615 }
616
617 /// @brief Whether this snapshot and @p other share the chunk holding @p row.
618 /// @param other Another snapshot.
619 /// @param row Row index valid in both.
620 /// @return `true` when the chunk is the same object.
621 [[nodiscard]] bool sharesChunk(TableSnapshot const& other, std::size_t row) const noexcept {
622 return _chunks[row / kChunkRows] == other._chunks[row / kChunkRows];
623 }
624
625private:
626 struct Chunk {
627 std::vector<RowId> ids;
628 std::vector<std::vector<Cell>> columns; // columns[column][row within chunk]
629 };
630
631 // Rows from `first` on, as plain vectors, for rebuilding the tail.
632 [[nodiscard]] std::pair<std::vector<RowId>, std::vector<std::vector<Cell>>> tail(std::size_t first) const {
633 std::vector<RowId> ids;
634 std::vector<std::vector<Cell>> rows;
635 ids.reserve(_rows - first);
636 rows.reserve(_rows - first);
637 for (std::size_t row = first; row < _rows; ++row) {
638 ids.push_back(rowId(row));
639 std::vector<Cell> cells;
640 cells.reserve(_columnCount);
641 for (std::size_t column = 0; column < _columnCount; ++column) {
642 cells.push_back(cell(row, column));
643 }
644 rows.push_back(std::move(cells));
645 }
646 return {std::move(ids), std::move(rows)};
647 }
648
649 // Replaces rows from `first` on with the given rows, keeping the chunks
650 // wholly before `first` and the rows of `first`'s chunk that precede it.
651 void assign(std::vector<RowId> ids, std::vector<std::vector<Cell>> rows, std::size_t first) {
652 auto const keepChunks = first / kChunkRows;
653 std::vector<RowId> headIds;
654 std::vector<std::vector<Cell>> headRows;
655 if (keepChunks < _chunks.size()) {
656 headIds.reserve(first - (keepChunks * kChunkRows));
657 headRows.reserve(first - (keepChunks * kChunkRows));
658 for (std::size_t row = keepChunks * kChunkRows; row < first; ++row) {
659 headIds.push_back(rowId(row));
660 std::vector<Cell> cells;
661 cells.reserve(_columnCount);
662 for (std::size_t column = 0; column < _columnCount; ++column) {
663 cells.push_back(cell(row, column));
664 }
665 headRows.push_back(std::move(cells));
666 }
667 }
668 _chunks.resize(std::min(keepChunks, _chunks.size()));
669 headIds.insert(headIds.end(), std::make_move_iterator(ids.begin()), std::make_move_iterator(ids.end()));
670 headRows.insert(headRows.end(), std::make_move_iterator(rows.begin()), std::make_move_iterator(rows.end()));
671 _rows = (_chunks.size() * kChunkRows) + headIds.size();
672 for (std::size_t at = 0; at < headIds.size(); at += kChunkRows) {
673 auto chunk = std::make_shared<Chunk>();
674 auto const end = std::min(at + kChunkRows, headIds.size());
675 chunk->ids.assign(std::make_move_iterator(headIds.begin() + static_cast<std::ptrdiff_t>(at)),
676 std::make_move_iterator(headIds.begin() + static_cast<std::ptrdiff_t>(end)));
677 chunk->columns.resize(_columnCount);
678 for (auto& column : chunk->columns) {
679 column.reserve(end - at);
680 }
681 for (std::size_t row = at; row < end; ++row) {
682 auto& cells = headRows[row];
683 cells.resize(_columnCount);
684 for (std::size_t column = 0; column < _columnCount; ++column) {
685 chunk->columns[column].push_back(std::move(cells[column]));
686 }
687 }
688 _chunks.push_back(std::move(chunk));
689 }
690 }
691
692 std::size_t _columnCount = 0;
693 std::size_t _rows = 0;
694 std::vector<std::shared_ptr<Chunk const>> _chunks;
695};
696
697// ── Sources ─────────────────────────────────────────────────────────────────
698
699/// @brief How rows changed.
700enum class ChangeKind : std::uint8_t {
701 Updated, ///< `rows` changed their cells; row indices are unchanged.
702 Inserted, ///< `rows` are the new rows' indices in the new snapshot.
703 Removed, ///< `rows` are the removed rows' indices in the old snapshot.
704 Reset, ///< Everything may have changed, the columns included.
705};
706
707/// @brief One change notification.
708struct RowChange {
709 /// @brief What happened.
711 /// @brief The rows concerned; empty for `Reset`.
712 std::vector<std::size_t> rows;
713};
714
715/// @brief Receives a `DataSource`'s change notifications, on the owner thread.
716// NOLINTBEGIN(cppcoreguidelines-special-member-functions)
718public:
719 virtual ~ChangeListener() = default;
720
721 /// @brief Called after the source changed; its `snapshot()` already shows the change.
722 /// @param change What changed.
723 virtual void rowsChanged(RowChange const& change) = 0;
724};
725// NOLINTEND(cppcoreguidelines-special-member-functions)
726
727/// @brief The rows of a table, as the engine reads them.
728///
729/// Owner-affine: every member is called on the owner thread. The engine reads
730/// cells only from `snapshot()`, which it may hand to a worker.
731// NOLINTBEGIN(cppcoreguidelines-special-member-functions)
733public:
734 virtual ~DataSource() = default;
735
736 /// @brief The table's columns.
737 /// @return The columns, in source order.
738 [[nodiscard]] virtual std::span<ColumnInfo const> columns() const = 0;
739
740 /// @brief The current rows as an immutable snapshot. Taking one is cheap.
741 /// @return The snapshot.
742 [[nodiscard]] virtual std::shared_ptr<RowSnapshot const> snapshot() const = 0;
743
744 /// @brief Registers @p listener for change notifications.
745 /// @param listener Receives notifications until `unsubscribe`; must outlive the subscription.
746 virtual void subscribe(ChangeListener& listener) = 0;
747
748 /// @brief Removes @p listener.
749 /// @param listener A listener previously subscribed.
750 virtual void unsubscribe(ChangeListener& listener) = 0;
751};
752// NOLINTEND(cppcoreguidelines-special-member-functions)
753
754namespace detail {
755
756/// @brief A source's listeners, notified so that a listener may unsubscribe
757/// itself or another, or destroy the source, from inside a notification.
758///
759/// A listener unsubscribed during a notification is not called after that,
760/// even if it was registered when the notification began: unsubscribing is
761/// how its owner says it is about to be destroyed. A source destroyed during
762/// a notification ends it.
764public:
765 /// @brief Registers @p listener.
766 /// @param listener The listener.
767 void add(ChangeListener& listener) { _listeners.push_back(&listener); }
768
769 /// @brief Removes @p listener.
770 /// @param listener The listener.
771 void remove(ChangeListener& listener) { std::erase(_listeners, &listener); }
772
773 /// @brief Calls every listener registered now that is still registered when its turn comes.
774 /// @param change What changed.
775 void notify(RowChange const& change) {
776 auto const alive = _alive.token();
777 auto const listeners = _listeners;
778 for (auto* const listener : listeners) {
779 if (alive.expired()) {
780 return;
781 }
782 if (std::ranges::find(_listeners, listener) != _listeners.end()) {
783 listener->rowsChanged(change);
784 }
785 }
786 }
787
788private:
789 std::vector<ChangeListener*> _listeners;
791};
792
793} // namespace detail
794
795/// @brief Replaces a row by key; what a committed cell edit patches.
796// NOLINTBEGIN(cppcoreguidelines-special-member-functions)
798public:
799 virtual ~RowPatcher() = default;
800
801 /// @brief Replaces the cells of the row keyed @p id.
802 /// @param key The row's key.
803 /// @param cells The new cells.
804 /// @return `false` when no row has that key.
805 virtual bool patchRow(RowId const& key, std::vector<Cell> cells) = 0;
806};
807// NOLINTEND(cppcoreguidelines-special-member-functions)
808
809/// @brief A `DataSource` over rows held in memory, for tests, servers and
810/// sources that cannot share their own storage.
811class VectorSource final : public DataSource, public RowPatcher {
812public:
813 /// @brief Constructs an empty source with @p columns.
814 /// @param columns The table's columns.
815 explicit VectorSource(std::vector<ColumnInfo> columns)
816 : _columns{std::move(columns)}, _snapshot{std::make_shared<TableSnapshot const>(_columns.size())} {}
817
818 /// @brief The table's columns.
819 /// @return The columns.
820 [[nodiscard]] std::span<ColumnInfo const> columns() const override { return _columns; }
821
822 /// @brief The current rows; a pointer copy.
823 /// @return The snapshot.
824 [[nodiscard]] std::shared_ptr<RowSnapshot const> snapshot() const override { return _snapshot; }
825
826 /// @brief The current rows, typed.
827 /// @return The snapshot.
828 [[nodiscard]] std::shared_ptr<TableSnapshot const> const& tableSnapshot() const noexcept { return _snapshot; }
829
830 /// @brief Registers @p listener.
831 /// @param listener The listener.
832 void subscribe(ChangeListener& listener) override { _listeners.add(listener); }
833
834 /// @brief Removes @p listener; safe from inside a notification.
835 /// @param listener The listener.
836 void unsubscribe(ChangeListener& listener) override { _listeners.remove(listener); }
837
838 /// @brief Replaces every row (and optionally the columns), then notifies `Reset`.
839 /// @param ids The rows' keys.
840 /// @param rows The rows' cells.
841 /// @param columns New columns, or nothing to keep the current ones.
842 void setRows(std::vector<RowId> ids, std::vector<std::vector<Cell>> rows,
843 std::optional<std::vector<ColumnInfo>> columns = std::nullopt) {
844 if (columns) {
845 _columns = std::move(*columns);
846 }
847 _snapshot = TableSnapshot::fromRows(_columns.size(), std::move(ids), std::move(rows));
848 reindex();
849 _listeners.notify(RowChange{.kind = ChangeKind::Reset, .rows = {}});
850 }
851
852 /// @brief Replaces one row's cells, then notifies `Updated`.
853 /// @param row Row index.
854 /// @param cells The new cells.
855 void updateRow(std::size_t row, std::vector<Cell> cells) {
856 _snapshot = _snapshot->withRow(row, std::move(cells));
857 _listeners.notify(RowChange{.kind = ChangeKind::Updated, .rows = {row}});
858 }
859
860 /// @brief Replaces the cells of the row keyed @p id, then notifies `Updated`.
861 /// @param key The row's key.
862 /// @param cells The new cells.
863 /// @return `false` when no row has that key.
864 bool patchRow(RowId const& key, std::vector<Cell> cells) override {
865 auto const found = _index.find(key);
866 if (found == _index.end()) {
867 return false;
868 }
869 updateRow(found->second, std::move(cells));
870 return true;
871 }
872
873 /// @brief Inserts a row before @p row, then notifies `Inserted`.
874 /// @param row Insert position.
875 /// @param key The new row's key.
876 /// @param cells The new row's cells.
877 void insertRow(std::size_t row, RowId key, std::vector<Cell> cells) {
878 _snapshot = _snapshot->withInserted(row, std::move(key), std::move(cells));
879 reindex();
880 _listeners.notify(RowChange{.kind = ChangeKind::Inserted, .rows = {row}});
881 }
882
883 /// @brief Removes row @p row, then notifies `Removed`.
884 /// @param row Row index.
885 void removeRow(std::size_t row) {
886 _snapshot = _snapshot->withRemoved(row);
887 reindex();
888 _listeners.notify(RowChange{.kind = ChangeKind::Removed, .rows = {row}});
889 }
890
891 /// @brief The index of the row keyed @p id.
892 /// @param key The key.
893 /// @return Its row index, or nothing.
894 [[nodiscard]] std::optional<std::size_t> indexOf(RowId const& key) const {
895 auto const found = _index.find(key);
896 return found == _index.end() ? std::nullopt : std::optional{found->second};
897 }
898
899private:
900 void reindex() {
901 _index.clear();
902 for (std::size_t row = 0; row < _snapshot->rowCount(); ++row) {
903 _index.emplace(_snapshot->rowId(row), row);
904 }
905 }
906
907 std::vector<ColumnInfo> _columns;
908 std::shared_ptr<TableSnapshot const> _snapshot;
909 std::unordered_map<RowId, std::size_t> _index;
910 detail::Listeners _listeners;
911};
912
913// NOLINTEND(cppcoreguidelines-pro-bounds-avoid-unchecked-container-access)
914
915} // namespace morph::table
916
917/// @brief JSON names of `morph::table::ColumnKind`: `integer`, `decimal`, ….
918template <>
919struct glz::meta<morph::table::ColumnKind> {
920 using enum morph::table::ColumnKind;
921 /// @brief Each enumerator after its JSON name.
922 static constexpr auto value =
923 glz::enumerate("integer", Integer, "decimal", Decimal, "quantity", Quantity, "number", Number, "text", Text,
924 "date", Date, "dateTime", DateTime, "bool", Bool, "key", Key, "custom", Custom);
925};
926
927/// @brief JSON names of `morph::table::TableErrorCode`, for a typed error on the wire.
928template <>
929struct glz::meta<morph::table::TableErrorCode> {
931 /// @brief Each enumerator after its JSON name.
932 static constexpr auto value =
933 glz::enumerate("unknownColumn", UnknownColumn, "unsupportedOperator", UnsupportedOperator, "invalidValue",
934 InvalidValue, "invalidSpec", InvalidSpec, "limitExceeded", LimitExceeded, "columnNotReadable",
935 ColumnNotReadable, "unavailable", Unavailable, "serviceFailed", ServiceFailed);
936};
Node column(Nodes const &children, ContainerOptions const &options={})
A column node.
Definition author.hpp:3394
Handle< T > row()
The innermost row, {"row": ""}.
Definition author.hpp:1145
std::vector< TableColumn > cells(std::vector< Node > const &cells)
Columns from cells alone, without ids or labels.
Definition author.hpp:3686
Owned lifetime-and-stop gate for asynchronous callbacks — a data member, not a base class.
Receives a DataSource's change notifications, on the owner thread.
virtual void rowsChanged(RowChange const &change)=0
Called after the source changed; its snapshot() already shows the change.
The default collator: code-point order, with ASCII and Latin-1 letters case-folded....
std::string fold(std::string_view text) const override
ASCII A-Z and Latin-1 À-Þ (except ×) lowered; every other byte copied as it is.
std::string sortKey(std::string_view text) const override
The case-folded text; its bytes order as its code points.
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.
virtual void unsubscribe(ChangeListener &listener)=0
Removes listener.
virtual void subscribe(ChangeListener &listener)=0
Registers listener for change notifications.
Parses text to dates and date-times for Date and DateTime columns that hold text, and for filter valu...
virtual std::optional< std::int64_t > parseDateTime(std::string_view text, std::string_view format) const =0
Parses a date-time.
virtual std::shared_ptr< DateParser const > cloneForTask() const
An instance for one task to use alone.
virtual std::optional< std::int64_t > parseDate(std::string_view text, std::string_view format) const =0
Parses a date.
The default date parser: ISO 8601 only, whatever the format.
std::optional< std::int64_t > parseDateTime(std::string_view text, std::string_view format) const override
Parses an ISO 8601 date-time, or a bare date as midnight UTC.
std::optional< std::int64_t > parseDate(std::string_view text, std::string_view format) const override
Parses YYYY-MM-DD.
Receives progress of a long sort or filter, on the owner thread.
virtual void progress(std::size_t done, std::size_t total)=0
Reports progress through the current job.
Replaces a row by key; what a committed cell edit patches.
virtual bool patchRow(RowId const &key, std::vector< Cell > cells)=0
Replaces the cells of the row keyed id.
RowSnapshot & operator=(RowSnapshot &&)=default
Move-assigns the base of a snapshot.
virtual Cell cell(std::size_t row, std::size_t column) const =0
One cell.
virtual RowId rowId(std::size_t row) const =0
A row's key.
RowSnapshot & operator=(RowSnapshot const &)=default
Copy-assigns the base of a snapshot.
RowSnapshot(RowSnapshot const &)=default
Copies the base of a snapshot: a derived snapshot makes a changed copy of itself this way.
RowSnapshot(RowSnapshot &&)=default
Moves the base of a snapshot.
virtual void readColumn(std::size_t column, ColumnSink &sink, std::size_t first, std::size_t last) const
Hands rows [first, last) of column to sink.
virtual std::size_t rowCount() const =0
The number of rows.
A RowSnapshot that stores rows in shared, immutable chunks.
std::shared_ptr< TableSnapshot const > withRow(std::size_t row, std::vector< Cell > cells) const
A copy of this snapshot with one row's cells replaced. Only that row's chunk is copied.
static constexpr std::size_t kChunkRows
Rows per chunk: small enough that copying one is cheap, large enough that a column read hands long ru...
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, one run per chunk.
Cell cell(std::size_t row, std::size_t column) const override
One cell.
std::size_t rowCount() const override
The number of rows.
std::shared_ptr< TableSnapshot const > withRemoved(std::size_t row) const
A copy with row row removed. Chunks before it are shared.
bool sharesChunk(TableSnapshot const &other, std::size_t row) const noexcept
Whether this snapshot and other share the chunk holding row.
std::size_t columnCount() const noexcept
The number of columns.
std::shared_ptr< TableSnapshot const > withInserted(std::size_t row, RowId key, std::vector< Cell > cells) const
A copy with a row inserted before row. Chunks before it are shared.
static std::shared_ptr< TableSnapshot const > fromRows(std::size_t columnCount, std::vector< RowId > ids, std::vector< std::vector< Cell > > rows)
Builds a snapshot from rows.
RowId rowId(std::size_t row) const override
A row's key.
TableSnapshot(std::size_t columnCount)
Constructs an empty snapshot.
Orders and matches text. The renderer injects the toolkit's collator for the user's locale; CodePoint...
virtual std::string sortKey(std::string_view text) const =0
A key whose byte order is the collation order of text.
virtual std::string fold(std::string_view text) const =0
text folded to primary strength: two texts a user would call equal ignoring case and accents fold to ...
virtual std::shared_ptr< TextCollator const > cloneForTask() const
An instance for one task to use alone.
std::optional< std::size_t > indexOf(RowId const &key) const
The index of the row keyed id.
bool patchRow(RowId const &key, std::vector< Cell > cells) override
Replaces the cells of the row keyed id, then notifies Updated.
void updateRow(std::size_t row, std::vector< Cell > cells)
Replaces one row's cells, then notifies Updated.
std::shared_ptr< RowSnapshot const > snapshot() const override
The current rows; a pointer copy.
void setRows(std::vector< RowId > ids, std::vector< std::vector< Cell > > rows, std::optional< std::vector< ColumnInfo > > columns=std::nullopt)
Replaces every row (and optionally the columns), then notifies Reset.
std::span< ColumnInfo const > columns() const override
The table's columns.
void unsubscribe(ChangeListener &listener) override
Removes listener; safe from inside a notification.
void removeRow(std::size_t row)
Removes row row, then notifies Removed.
std::shared_ptr< TableSnapshot const > const & tableSnapshot() const noexcept
The current rows, typed.
void subscribe(ChangeListener &listener) override
Registers listener.
void insertRow(std::size_t row, RowId key, std::vector< Cell > cells)
Inserts a row before row, then notifies Inserted.
VectorSource(std::vector< ColumnInfo > columns)
Constructs an empty source with columns.
A source's listeners, notified so that a listener may unsubscribe itself or another,...
void remove(ChangeListener &listener)
Removes listener.
void add(ChangeListener &listener)
Registers listener.
void notify(RowChange const &change)
Calls every listener registered now that is still registered when its turn comes.
std::variant< std::monostate, std::int64_t, math::Rational, double, std::string, bool, QuantityCell > Cell
One cell as a data source hands it out.
ChangeKind
How rows changed.
@ Inserted
rows are the new rows' indices in the new snapshot.
@ Reset
Everything may have changed, the columns included.
@ Removed
rows are the removed rows' indices in the old snapshot.
@ Updated
rows changed their cells; row indices are unchanged.
std::function< std::strong_ordering(Cell const &, Cell const &)> CellComparator
Orders two cells of a Custom column. Called from the worker, so it must be safe to call concurrently.
ColumnKind
What a column holds, which decides how its keys are built and compared.
@ Custom
Ordered by a named comparator from Services::comparators.
@ Text
Text, ordered and matched through the TextCollator.
@ Bool
false before true.
TableErrorCode
What went wrong in a fallible table call.
@ InvalidValue
A filter value that does not parse for its column's kind.
@ LimitExceeded
A TableQuery over one of the server's bounds.
@ Unavailable
An operation the table's mode does not offer.
@ UnsupportedOperator
A filter operator the column's kind does not define.
@ ServiceFailed
An injected service (collator, date parser, comparator) threw.
@ InvalidSpec
A filter entry with no operator or more than one.
@ ColumnNotReadable
A sort or filter on a column the principal may not read.
@ UnknownColumn
A sort or filter names a column the table does not have.
std::expected< math::Rational, TableError > parseDecimal(std::string_view text)
Parses exact decimal text ("-12.50") into a Rational.
std::string displayText(Cell const &cell)
The text a cell shows when it has to be ordered or matched as text.
std::variant< std::int64_t, std::string > RowId
A row's identity: the table's key, stable across reloads.
UTC timestamps for morph actions.
Unit-tagged, optionally-empty exact values with a derivation trail.
Exact rational arithmetic for morph model/action values.
constexpr std::uint32_t kMaxDecimalPlaces
Largest decimal precision the type supports: 10^18 is the greatest power of ten that fits in std::int...
Definition rational.hpp:167
Strong type for a decimal-precision count.
Definition rational.hpp:121
Strong type for a Rational denominator.
Definition rational.hpp:154
Strong type for a Rational numerator.
Definition rational.hpp:140
Exact rational number with a runtime decimal-precision tag.
Definition rational.hpp:375
A column's identity and kind.
std::string comparator
For a Custom column, the name of its comparator in Services::comparators.
std::string format
For a Date or DateTime column holding text, the format handed to the DateParser.
bool operator==(ColumnInfo const &other) const =default
Member-wise equality.
std::string id
The column's id, unique within the table (the row member's name).
ColumnKind kind
How the column's keys are built and compared.
A quantity in its own unit, with the factor that converts it to the column's canonical unit.
math::Rational amount
The amount in the cell's own unit.
math::Rational toCanonical
Multiplier from the cell's unit to the column's canonical unit.
One change notification.
ChangeKind kind
What happened.
std::vector< std::size_t > rows
The rows concerned; empty for Reset.
The services an engine sorts and filters with. A null member is replaced by its default.
std::shared_ptr< DateParser const > dates
Date parsing; null means IsoDateParser.
Services forTask() const
The services one task uses: collator and parser cloned when they are not safe to share,...
std::shared_ptr< DateParser const > datesOrDefault() const
The date parser to use.
std::shared_ptr< TextCollator const > collatorOrDefault() const
The collator to use.
std::shared_ptr< ProgressSink > progress
Optional progress receiver.
std::shared_ptr< TextCollator const > collator
Text order and matching; null means CodePointCollator.
std::map< std::string, CellComparator, std::less<> > comparators
Comparators for Custom columns, by the name ColumnInfo::comparator gives.
The error every fallible table call returns.
TableErrorCode code
What went wrong.
std::string message
A human-readable description.
std::string column
The column concerned, or empty when the error is not about one column.
static std::optional< DateTime > fromIso8601(std::string_view text) noexcept
Strictly parses ISO-8601 UTC: YYYY-MM-DDTHH:MM:SS, optionally followed by .m/.mm/....
Definition datetime.hpp:148