morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
filter.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5/// @file table/filter.hpp
6/// @brief Filter specs as data, compiled once and evaluated column at a time.
7///
8/// A `FilterSpec` is plain data in the JSON shape of spec 7 ยง6, so a document
9/// can hold it, a user can save it and a server can receive it. Compiling it
10/// against a table's columns parses every value once for its column's kind;
11/// evaluating it narrows a candidate list one column at a time.
12///
13/// Semantics, normative for every client and server:
14/// - the `include` entries of a column combine by `combine` (`any`, the
15/// default, or `all`); a match on an `exclude` entry rejects the row;
16/// columns, and groups, combine with each other by AND;
17/// - a group matches a row when its entries match on any of its columns;
18/// - no operator but `isEmpty` and `notEmpty` matches an empty or invalid cell;
19/// - text operators compare through the collator's primary-strength fold
20/// unless the entry sets `caseSensitive`; `%` and `_` are ordinary characters.
21///
22/// See `docs/spec/table/engine.md`.
23
24#include <algorithm>
25#include <array>
26#include <core/async/StopToken.hpp>
27#include <cstddef>
28#include <cstdint>
29#include <expected>
30#include <map>
32#include <morph/table/sort.hpp>
33#include <optional>
34#include <span>
35#include <string>
36#include <string_view>
37#include <utility>
38#include <vector>
39
40namespace morph::table {
41
42// 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
43
44/// @brief One filter condition. Exactly one operator member is set.
45///
46/// Values are exact text, parsed for the column's kind when the filter is
47/// compiled: `"1.5"` for a decimal, `"2026-10-08"` for a date, `"true"` for a
48/// boolean.
50 /// @brief Equal to (text: at primary strength unless `caseSensitive`).
51 std::optional<std::string> eq;
52 /// @brief Not equal to.
53 std::optional<std::string> ne;
54 /// @brief Less than. Numeric, date and date-time columns.
55 std::optional<std::string> lt;
56 /// @brief Less than or equal to. Numeric, date and date-time columns.
57 std::optional<std::string> le;
58 /// @brief Greater than. Numeric, date and date-time columns.
59 std::optional<std::string> gt;
60 /// @brief Greater than or equal to. Numeric, date and date-time columns.
61 std::optional<std::string> ge;
62 /// @brief Between two values, both inclusive. Numeric, date and date-time columns.
63 std::optional<std::array<std::string, 2>> between;
64 /// @brief Text contains. Text columns.
65 std::optional<std::string> contains;
66 /// @brief Text starts with. Text columns.
67 std::optional<std::string> startsWith;
68 /// @brief Text ends with. Text columns.
69 std::optional<std::string> endsWith;
70 /// @brief `true`: the cell is empty. Any column.
71 std::optional<bool> isEmpty;
72 /// @brief `true`: the cell is not empty. Any column.
73 std::optional<bool> notEmpty;
74 /// @brief Text operators compare the text as given rather than folded.
75 std::optional<bool> caseSensitive;
76};
77
78/// @brief How a column's `include` entries combine.
79enum class Combine : std::uint8_t {
80 Any, ///< A row matches when any entry matches.
81 All, ///< A row matches when every entry matches.
82};
83
84/// @brief The filter on one column.
86 /// @brief How `include` entries combine.
88 /// @brief Conditions a row must meet (combined by `combine`); empty means no condition.
89 std::vector<FilterEntry> include;
90 /// @brief Conditions that reject a row when any matches.
91 std::vector<FilterEntry> exclude;
92};
93
94/// @brief A filter over several columns: a row matches when the entries match
95/// on any of them, which is what a quick-search box needs.
97 /// @brief The columns searched.
98 std::vector<std::string> columns;
99 /// @brief How `include` entries combine on one column.
101 /// @brief The conditions.
102 std::vector<FilterEntry> include;
103};
104
105/// @brief A table's whole filter.
107 /// @brief Per-column filters, by column id.
108 std::map<std::string, ColumnFilter> columns;
109 /// @brief Multi-column groups.
110 std::vector<GroupFilter> groups;
111
112 /// @brief Whether the spec filters nothing.
113 /// @return `true` when it has no column filter and no group.
114 [[nodiscard]] bool empty() const noexcept { return columns.empty() && groups.empty(); }
115};
116
117/// @brief A filter compiled against a table's columns: every value parsed.
119public:
120 /// @brief An operator.
121 enum class Op : std::uint8_t {
122 Eq,
123 Ne,
124 Lt,
125 Le,
126 Gt,
127 Ge,
128 Between,
129 Contains,
130 StartsWith,
131 EndsWith,
132 IsEmpty,
133 NotEmpty
134 };
135
136 /// @brief One compiled condition.
137 struct Entry {
138 /// @brief The operator.
139 Op op = Op::IsEmpty;
140 /// @brief Text operators compare raw text.
141 bool caseSensitive = false;
142 /// @brief Integer operands (dates, date-times, booleans).
143 std::array<std::int64_t, 2> ints{};
144 /// @brief Exact operands (integers, keys, decimals, quantities).
145 std::array<math::Rational, 2> exact{};
146 /// @brief Floating-point operands (`Number`).
147 std::array<double, 2> reals{};
148 /// @brief Text operand: folded, or raw when `caseSensitive`.
149 std::string text;
150 };
151
152 /// @brief One column's compiled filter.
153 struct Column {
154 /// @brief Column index.
155 std::size_t column = 0;
156 /// @brief How `include` combines.
158 /// @brief Compiled `include` entries.
159 std::vector<Entry> include;
160 /// @brief Compiled `exclude` entries.
161 std::vector<Entry> exclude;
162 };
163
164 /// @brief One compiled group: per column, the entries that apply to it.
165 struct Group {
166 /// @brief The group's columns, each with the entries that compiled for it.
167 std::vector<Column> columns;
168 };
169
170 /// @brief Whether the filter matches every row.
171 /// @return `true` when there is nothing to evaluate.
172 [[nodiscard]] bool empty() const noexcept { return _columns.empty() && _groups.empty(); }
173
174 /// @brief The column indices whose keys evaluation reads.
175 /// @return Sorted, unique column indices.
176 [[nodiscard]] std::vector<std::size_t> columnsUsed() const {
177 std::vector<std::size_t> out;
178 out.reserve(_columns.size());
179 for (auto const& column : _columns) {
180 out.push_back(column.column);
181 }
182 for (auto const& group : _groups) {
183 for (auto const& column : group.columns) {
184 out.push_back(column.column);
185 }
186 }
187 std::ranges::sort(out);
188 auto const [first, last] = std::ranges::unique(out);
189 out.erase(first, last);
190 return out;
191 }
192
193 /// @brief The compiled column filters, in evaluation order.
194 /// @return The filters.
195 [[nodiscard]] std::span<Column const> columns() const noexcept { return _columns; }
196
197 /// @brief The compiled groups, in evaluation order.
198 /// @return The groups.
199 [[nodiscard]] std::span<Group const> groups() const noexcept { return _groups; }
200
201 /// @brief Whether a row passes one column filter.
202 /// @param filter The column filter.
203 /// @param keys That column's keys.
204 /// @param row The row.
205 /// @return `true` when the row passes.
206 [[nodiscard]] static bool passes(Column const& filter, KeyColumn const& keys, std::uint32_t row) {
207 if (!filter.include.empty()) {
208 bool const all = filter.combine == Combine::All;
209 bool result = all;
210 for (auto const& entry : filter.include) {
211 if (matches(entry, keys, row) != all) {
212 result = !all;
213 break;
214 }
215 }
216 if (!result) {
217 return false;
218 }
219 }
220 return std::ranges::none_of(filter.exclude, [&](Entry const& entry) { return matches(entry, keys, row); });
221 }
222
223 /// @brief Whether one entry matches one cell.
224 /// @param entry The entry.
225 /// @param keys The column's keys.
226 /// @param row The row.
227 /// @return `true` on a match.
228 [[nodiscard]] static bool matches(Entry const& entry, KeyColumn const& keys, std::uint32_t row) {
229 auto const state = keys.states[row];
230 if (entry.op == Op::IsEmpty) {
231 return state == CellState::Empty;
232 }
233 if (entry.op == Op::NotEmpty) {
234 return state != CellState::Empty;
235 }
236 if (state != CellState::Valid) {
237 return false;
238 }
239 switch (keys.kind) {
241 case ColumnKind::Key:
242 return compare(entry, math::Rational{keys.ints[row], math::DecimalPlaces{0}}, entry.exact);
245 return compare(entry, keys.exact[row], entry.exact);
247 return compare(entry, keys.reals[row], entry.reals);
248 case ColumnKind::Date:
250 case ColumnKind::Bool:
251 return compare(entry, keys.ints[row], entry.ints);
252 case ColumnKind::Text:
253 return matchText(entry, entry.caseSensitive ? keys.raw[row] : keys.folded[row]);
255 default:
256 return false;
257 }
258 }
259
260private:
261 friend std::expected<CompiledFilter, TableError> compileFilter(FilterSpec const& spec,
262 std::span<ColumnInfo const> columns,
263 Services const& services);
264
265 template <typename T>
266 [[nodiscard]] static bool compare(Entry const& entry, T const& value, std::array<T, 2> const& operand) {
267 switch (entry.op) {
268 case Op::Eq:
269 return value == operand[0];
270 case Op::Ne:
271 return !(value == operand[0]);
272 case Op::Lt:
273 return value < operand[0];
274 case Op::Le:
275 return value <= operand[0];
276 case Op::Gt:
277 return value > operand[0];
278 case Op::Ge:
279 return value >= operand[0];
280 case Op::Between:
281 return operand[0] <= value && value <= operand[1];
282 case Op::Contains:
283 case Op::StartsWith:
284 case Op::EndsWith:
285 case Op::IsEmpty:
286 case Op::NotEmpty:
287 default:
288 return false;
289 }
290 }
291
292 [[nodiscard]] static bool matchText(Entry const& entry, std::string_view text) {
293 switch (entry.op) {
294 case Op::Eq:
295 return text == entry.text;
296 case Op::Ne:
297 return text != entry.text;
298 case Op::Contains:
299 return text.contains(entry.text);
300 case Op::StartsWith:
301 return text.starts_with(entry.text);
302 case Op::EndsWith:
303 return text.ends_with(entry.text);
304 case Op::Lt:
305 case Op::Le:
306 case Op::Gt:
307 case Op::Ge:
308 case Op::Between:
309 case Op::IsEmpty:
310 case Op::NotEmpty:
311 default:
312 return false;
313 }
314 }
315
316 std::vector<Column> _columns;
317 std::vector<Group> _groups;
318};
319
320namespace detail {
321
322/// @brief The operator an entry sets and its operand text.
323struct RawEntry {
324 /// @brief The operator.
325 CompiledFilter::Op op = CompiledFilter::Op::IsEmpty;
326 /// @brief Its operands; the second only for `between`.
327 std::array<std::string_view, 2> values{};
328};
329
330/// @brief Reads which operator an entry sets.
331/// @param entry The entry.
332/// @param column The column, for the error.
333/// @return The operator and operands, or `InvalidSpec` when the entry sets none or several.
334[[nodiscard]] inline std::expected<RawEntry, TableError> readEntry(FilterEntry const& entry, std::string_view column) {
335 using Op = CompiledFilter::Op;
336 RawEntry out;
337 int set = 0;
338 auto const take = [&](std::optional<std::string> const& member, Op op) {
339 if (member) {
340 ++set;
341 out.op = op;
342 out.values[0] = *member;
343 }
344 };
345 take(entry.eq, Op::Eq);
346 take(entry.ne, Op::Ne);
347 take(entry.lt, Op::Lt);
348 take(entry.le, Op::Le);
349 take(entry.gt, Op::Gt);
350 take(entry.ge, Op::Ge);
351 take(entry.contains, Op::Contains);
352 take(entry.startsWith, Op::StartsWith);
353 take(entry.endsWith, Op::EndsWith);
354 if (entry.between) {
355 ++set;
356 out.op = Op::Between;
357 out.values = {(*entry.between)[0], (*entry.between)[1]};
358 }
359 if (entry.isEmpty.value_or(false)) {
360 ++set;
361 out.op = Op::IsEmpty;
362 }
363 if (entry.notEmpty.value_or(false)) {
364 ++set;
365 out.op = Op::NotEmpty;
366 }
367 if (set != 1) {
368 return std::unexpected(TableError{.code = TableErrorCode::InvalidSpec,
369 .column = std::string{column},
370 .message = "a filter entry sets exactly one operator"});
371 }
372 return out;
373}
374
375/// @brief Whether a column kind defines an operator.
376/// @param kind The column's kind.
377/// @param op The operator.
378/// @return `true` when the operator applies to the kind.
379[[nodiscard]] inline bool operatorDefined(ColumnKind kind, CompiledFilter::Op op) {
380 using Op = CompiledFilter::Op;
381 if (op == Op::IsEmpty || op == Op::NotEmpty) {
382 return true;
383 }
384 bool const textOp = op == Op::Contains || op == Op::StartsWith || op == Op::EndsWith;
385 switch (kind) {
386 case ColumnKind::Text:
387 return textOp || op == Op::Eq || op == Op::Ne;
388 case ColumnKind::Bool:
389 return op == Op::Eq || op == Op::Ne;
391 return false;
393 case ColumnKind::Key:
397 case ColumnKind::Date:
399 default:
400 return !textOp;
401 }
402}
403
404/// @brief Parses one operand for the column's kind into a slot of @p entry.
405/// @param info The column.
406/// @param services Collator (text is folded with it) and date parser.
407/// @param text The operand text.
408/// @param slot 0, or 1 for the upper bound of `between`.
409/// @param entry Receives the parsed operand.
410/// @return `false` when the text does not parse for the kind.
411[[nodiscard]] inline bool parseOperand(ColumnInfo const& info, Services const& services, std::string_view text,
412 std::size_t slot, CompiledFilter::Entry& entry) {
413 switch (info.kind) {
415 case ColumnKind::Key:
418 auto const value = parseDecimal(text);
419 if (!value) {
420 return false;
421 }
422 entry.exact.at(slot) = *value;
423 return true;
424 }
425 case ColumnKind::Number: {
426 auto const value = parseReal(text);
427 if (!value) {
428 return false;
429 }
430 entry.reals.at(slot) = *value;
431 return true;
432 }
433 case ColumnKind::Date:
435 auto const& parser = *services.datesOrDefault();
436 auto const value = info.kind == ColumnKind::Date ? parser.parseDate(text, info.format)
437 : parser.parseDateTime(text, info.format);
438 if (!value) {
439 return false;
440 }
441 entry.ints.at(slot) = *value;
442 return true;
443 }
444 case ColumnKind::Bool:
445 if (text != "true" && text != "false") {
446 return false;
447 }
448 entry.ints.at(slot) = text == "true" ? 1 : 0;
449 return true;
450 case ColumnKind::Text:
451 entry.text = entry.caseSensitive ? std::string{text} : services.collatorOrDefault()->fold(text);
452 return true;
454 default:
455 return false;
456 }
457}
458
459/// @brief Compiles one entry for one column.
460/// @param entry The entry.
461/// @param info The column.
462/// @param services Collator and date parser.
463/// @return The compiled entry, or the error naming the column.
464[[nodiscard]] inline std::expected<CompiledFilter::Entry, TableError> compileEntry(FilterEntry const& entry,
465 ColumnInfo const& info,
466 Services const& services) {
467 auto const raw = readEntry(entry, info.id);
468 if (!raw) {
469 return std::unexpected(raw.error());
470 }
471 if (!operatorDefined(info.kind, raw->op)) {
472 return std::unexpected(TableError{.code = TableErrorCode::UnsupportedOperator,
473 .column = info.id,
474 .message = "operator not defined for column " + info.id});
475 }
477 out.op = raw->op;
478 out.caseSensitive = entry.caseSensitive.value_or(false);
479 if (out.op == CompiledFilter::Op::IsEmpty || out.op == CompiledFilter::Op::NotEmpty) {
480 return out;
481 }
482 std::size_t const operands = out.op == CompiledFilter::Op::Between ? 2 : 1;
483 for (std::size_t slot = 0; slot < operands; ++slot) {
484 if (!parseOperand(info, services, raw->values.at(slot), slot, out)) {
485 return std::unexpected(TableError{.code = TableErrorCode::InvalidValue,
486 .column = info.id,
487 .message = "value \"" + std::string{raw->values.at(slot)} +
488 "\" does not parse for column " + info.id});
489 }
490 }
491 return out;
492}
493
494/// @brief The index of the column with id @p columnId.
495/// @param columns The table's columns.
496/// @param columnId A column id.
497/// @return Its index, or nothing.
498[[nodiscard]] inline std::optional<std::size_t> findColumn(std::span<ColumnInfo const> columns,
499 std::string_view columnId) {
500 for (std::size_t i = 0; i < columns.size(); ++i) {
501 if (columns[i].id == columnId) {
502 return i;
503 }
504 }
505 return std::nullopt;
506}
507
508/// @brief An `UnknownColumn` error.
509/// @param columnId The column id that names no column.
510/// @return The error.
511[[nodiscard]] inline TableError unknownColumn(std::string const& columnId) {
512 return TableError{.code = TableErrorCode::UnknownColumn, .column = columnId, .message = "no column " + columnId};
513}
514
515/// @brief Compiles entries for one column, appending them to @p out.
516/// @param entries The entries.
517/// @param info The column.
518/// @param services Collator and date parser.
519/// @param out Receives the compiled entries.
520/// @return Nothing, or the first entry's error.
521[[nodiscard]] inline std::expected<void, TableError> compileEntries(std::span<FilterEntry const> entries,
522 ColumnInfo const& info, Services const& services,
523 std::vector<CompiledFilter::Entry>& out) {
524 for (auto const& entry : entries) {
525 auto compiled = compileEntry(entry, info, services);
526 if (!compiled) {
527 return std::unexpected(std::move(compiled.error()));
528 }
529 out.push_back(std::move(*compiled));
530 }
531 return {};
532}
533
534/// @brief Compiles the filter on one column.
535/// @param columnId The column's id.
536/// @param filter Its filter.
537/// @param columns The table's columns.
538/// @param services Collator and date parser.
539/// @return The compiled filter, or the first error.
540[[nodiscard]] inline std::expected<CompiledFilter::Column, TableError> compileColumnFilter(
541 std::string const& columnId, ColumnFilter const& filter, std::span<ColumnInfo const> columns,
542 Services const& services) {
543 auto const index = findColumn(columns, columnId);
544 if (!index) {
545 return std::unexpected(unknownColumn(columnId));
546 }
547 CompiledFilter::Column out{.column = *index, .combine = filter.combine, .include = {}, .exclude = {}};
548 if (auto included = compileEntries(filter.include, columns[*index], services, out.include); !included) {
549 return std::unexpected(std::move(included.error()));
550 }
551 if (auto excluded = compileEntries(filter.exclude, columns[*index], services, out.exclude); !excluded) {
552 return std::unexpected(std::move(excluded.error()));
553 }
554 return out;
555}
556
557/// @brief Compiles a group: per column, the entries that apply to it.
558/// @param group The group.
559/// @param columns The table's columns.
560/// @param services Collator and date parser.
561/// @return The compiled group, or an error when a column is unknown, an entry
562/// is malformed, or an entry applies to none of the group's columns.
563[[nodiscard]] inline std::expected<CompiledFilter::Group, TableError> compileGroup(GroupFilter const& group,
564 std::span<ColumnInfo const> columns,
565 Services const& services) {
567 std::vector<std::size_t> appliesTo(group.include.size(), 0);
568 std::optional<TableError> lastError;
569 for (auto const& columnId : group.columns) {
570 auto const index = findColumn(columns, columnId);
571 if (!index) {
572 return std::unexpected(unknownColumn(columnId));
573 }
574 CompiledFilter::Column column{.column = *index, .combine = group.combine, .include = {}, .exclude = {}};
575 for (std::size_t i = 0; i < group.include.size(); ++i) {
576 auto compiled = compileEntry(group.include[i], columns[*index], services);
577 if (compiled) {
578 ++appliesTo[i];
579 column.include.push_back(std::move(*compiled));
580 } else if (compiled.error().code == TableErrorCode::InvalidSpec) {
581 return std::unexpected(std::move(compiled.error()));
582 } else {
583 lastError = std::move(compiled.error());
584 }
585 }
586 // With `all`, a column that cannot evaluate every entry cannot
587 // match; with `any`, one that evaluates none cannot either.
588 bool const usable =
589 group.combine == Combine::All ? column.include.size() == group.include.size() : !column.include.empty();
590 if (usable) {
591 out.columns.push_back(std::move(column));
592 }
593 }
594 if (std::ranges::find(appliesTo, std::size_t{0}) != appliesTo.end()) {
595 return std::unexpected(lastError.value_or(TableError{
596 .code = TableErrorCode::InvalidSpec, .column = {}, .message = "a filter group names no column"}));
597 }
598 return out;
599}
600
601} // namespace detail
602
603/// @brief Compiles @p spec against @p columns.
604///
605/// In a group, an entry whose operator a column's kind does not define, or
606/// whose value does not parse for it, does not apply to that column; it is an
607/// error only when it applies to none of the group's columns. A quick-search
608/// text therefore searches the text columns of a group that also holds numbers.
609/// @param spec The filter.
610/// @param columns The table's columns.
611/// @param services The collator (values of text filters are folded with it)
612/// and date parser; null members mean their defaults.
613/// @return The compiled filter, or the first error, naming its column.
614[[nodiscard]] inline std::expected<CompiledFilter, TableError> compileFilter(FilterSpec const& spec,
615 std::span<ColumnInfo const> columns,
616 Services const& services) {
617 CompiledFilter out;
618 for (auto const& [columnId, filter] : spec.columns) {
619 auto compiled = detail::compileColumnFilter(columnId, filter, columns, services);
620 if (!compiled) {
621 return std::unexpected(std::move(compiled.error()));
622 }
623 if (!compiled->include.empty() || !compiled->exclude.empty()) {
624 out._columns.push_back(std::move(*compiled));
625 }
626 }
627 for (auto const& group : spec.groups) {
628 auto compiled = detail::compileGroup(group, columns, services);
629 if (!compiled) {
630 return std::unexpected(std::move(compiled.error()));
631 }
632 if (!group.include.empty()) {
633 out._groups.push_back(std::move(*compiled));
634 }
635 }
636 return out;
637}
638
639/// @brief Evaluates a compiled filter over a candidate list, column at a time,
640/// in steps that yield at a deadline or a stop request.
641///
642/// Each column filter, then each group, compacts the surviving candidates in
643/// place, so later columns test fewer rows. The order of the candidates is
644/// kept, which is how a filter over a cached sorted order stays sorted.
646public:
647 /// @brief Prepares to filter @p candidates.
648 /// @param filter The compiled filter; must outlive the run.
649 /// @param keys Key columns by column index; every index in
650 /// `filter.columnsUsed()` must be non-null and outlive the run.
651 /// @param candidates The rows to filter, in the order to keep.
652 FilterRun(CompiledFilter const& filter, std::vector<KeyColumn const*> keys, std::vector<std::uint32_t> candidates)
653 : _filter{&filter}, _keys{std::move(keys)}, _rows{std::move(candidates)}, _size{_rows.size()} {}
654
655 /// @brief Filters until done, the deadline passes, or a stop is requested.
656 /// @param deadline When to yield.
657 /// @param stop A stop request ends the step early.
658 /// @return `true` when every stage has run.
659 bool run(Deadline deadline, ::core::async::StopToken const& stop) {
660 auto const stages = _filter->columns().size() + _filter->groups().size();
661 while (_stage < stages) {
662 while (_read < _size) {
663 auto const end = std::min(_read + detail::kCheckEvery, _size);
664 for (; _read < end; ++_read) {
665 auto const row = _rows[_read];
666 if (keep(row)) {
667 _rows[_write++] = row;
668 }
669 }
670 if (_read < _size && detail::shouldYield(deadline, stop)) {
671 return false;
672 }
673 }
674 _size = _write;
675 _read = 0;
676 _write = 0;
677 ++_stage;
678 if (_stage < stages && detail::shouldYield(deadline, stop)) {
679 return false;
680 }
681 }
682 return true;
683 }
684
685 /// @brief The surviving rows. Valid once `run` returned `true`.
686 /// @return The rows, moved out.
687 [[nodiscard]] std::vector<std::uint32_t> take() && {
688 _rows.resize(_size);
689 return std::move(_rows);
690 }
691
692private:
693 [[nodiscard]] bool keep(std::uint32_t row) const {
694 auto const columns = _filter->columns();
695 if (_stage < columns.size()) {
696 auto const& column = columns[_stage];
697 return CompiledFilter::passes(column, *_keys[column.column], row);
698 }
699 auto const& group = _filter->groups()[_stage - columns.size()];
700 return std::ranges::any_of(group.columns, [&](CompiledFilter::Column const& column) {
701 return CompiledFilter::passes(column, *_keys[column.column], row);
702 });
703 }
704
705 CompiledFilter const* _filter;
706 std::vector<KeyColumn const*> _keys;
707 std::vector<std::uint32_t> _rows;
708 std::size_t _size;
709 std::size_t _stage = 0;
710 std::size_t _read = 0;
711 std::size_t _write = 0;
712};
713
714// NOLINTEND(cppcoreguidelines-pro-bounds-avoid-unchecked-container-access)
715
716} // namespace morph::table
717
718/// @brief JSON names of `morph::table::Combine`: `any` and `all`.
719template <>
720struct glz::meta<morph::table::Combine> {
721 using enum morph::table::Combine;
722 /// @brief Each enumerator after its JSON name.
723 static constexpr auto value = glz::enumerate("any", Any, "all", All);
724};
A filter compiled against a table's columns: every value parsed.
Definition filter.hpp:118
std::span< Group const > groups() const noexcept
The compiled groups, in evaluation order.
Definition filter.hpp:199
std::span< Column const > columns() const noexcept
The compiled column filters, in evaluation order.
Definition filter.hpp:195
static bool matches(Entry const &entry, KeyColumn const &keys, std::uint32_t row)
Whether one entry matches one cell.
Definition filter.hpp:228
std::vector< std::size_t > columnsUsed() const
The column indices whose keys evaluation reads.
Definition filter.hpp:176
bool empty() const noexcept
Whether the filter matches every row.
Definition filter.hpp:172
static bool passes(Column const &filter, KeyColumn const &keys, std::uint32_t row)
Whether a row passes one column filter.
Definition filter.hpp:206
friend std::expected< CompiledFilter, TableError > compileFilter(FilterSpec const &spec, std::span< ColumnInfo const > columns, Services const &services)
Compiles spec against columns.
Definition filter.hpp:614
std::vector< std::uint32_t > take() &&
The surviving rows. Valid once run returned true.
Definition filter.hpp:687
FilterRun(CompiledFilter const &filter, std::vector< KeyColumn const * > keys, std::vector< std::uint32_t > candidates)
Prepares to filter candidates.
Definition filter.hpp:652
bool run(Deadline deadline, ::core::async::StopToken const &stop)
Filters until done, the deadline passes, or a stop is requested.
Definition filter.hpp:659
The rows a table engine reads, and the services it is given.
ColumnKind
What a column holds, which decides how its keys are built and compared.
@ Date
Days since 1970-01-01.
@ Quantity
An exact Rational in the column's canonical unit.
@ Key
An int64 key, compared numerically.
@ DateTime
UTC seconds since 1970-01-01T00:00:00Z.
@ Custom
Ordered by a named comparator from Services::comparators.
@ 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.
@ InvalidValue
A filter value that does not parse for its column's kind.
@ UnsupportedOperator
A filter operator the column's kind does not define.
@ InvalidSpec
A filter entry with no operator or more than one.
@ 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::expected< RawEntry, TableError > readEntry(FilterEntry const &entry, std::string_view column)
Reads which operator an entry sets.
Definition filter.hpp:334
TableError unknownColumn(std::string const &columnId)
An UnknownColumn error.
Definition filter.hpp:511
std::expected< CompiledFilter::Group, TableError > compileGroup(GroupFilter const &group, std::span< ColumnInfo const > columns, Services const &services)
Compiles a group: per column, the entries that apply to it.
Definition filter.hpp:563
bool parseOperand(ColumnInfo const &info, Services const &services, std::string_view text, std::size_t slot, CompiledFilter::Entry &entry)
Parses one operand for the column's kind into a slot of entry.
Definition filter.hpp:411
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
std::expected< void, TableError > compileEntries(std::span< FilterEntry const > entries, ColumnInfo const &info, Services const &services, std::vector< CompiledFilter::Entry > &out)
Compiles entries for one column, appending them to out.
Definition filter.hpp:521
Combine
How a column's include entries combine.
Definition filter.hpp:79
@ All
A row matches when every entry matches.
Definition filter.hpp:81
@ Any
A row matches when any entry matches.
Definition filter.hpp:80
bool operatorDefined(ColumnKind kind, CompiledFilter::Op op)
Whether a column kind defines an operator.
Definition filter.hpp:379
std::expected< CompiledFilter::Column, TableError > compileColumnFilter(std::string const &columnId, ColumnFilter const &filter, std::span< ColumnInfo const > columns, Services const &services)
Compiles the filter on one column.
Definition filter.hpp:540
std::expected< CompiledFilter::Entry, TableError > compileEntry(FilterEntry const &entry, ColumnInfo const &info, Services const &services)
Compiles one entry for one column.
Definition filter.hpp:464
Typed sort keys and the chunked, stoppable merge sort over them.
@ Valid
The cell has a key.
Definition sort.hpp:75
@ Empty
The cell is empty; it sorts with invalid cells.
Definition sort.hpp:76
constexpr std::size_t kCheckEvery
Elements of work between two looks at the clock and the stop token: often enough to keep an owner ste...
Definition sort.hpp:91
std::chrono::steady_clock::time_point Deadline
A deadline for one step of work; kNoDeadline runs to completion.
Definition sort.hpp:81
bool shouldYield(Deadline deadline, ::core::async::StopToken const &stop)
Whether a step should return now.
Definition sort.hpp:97
std::optional< double > parseReal(std::string_view text)
Parses a floating-point number, all of text.
Definition sort.hpp:153
Strong type for a decimal-precision count.
Definition rational.hpp:121
Exact rational number with a runtime decimal-precision tag.
Definition rational.hpp:375
The filter on one column.
Definition filter.hpp:85
Combine combine
How include entries combine.
Definition filter.hpp:87
std::vector< FilterEntry > include
Conditions a row must meet (combined by combine); empty means no condition.
Definition filter.hpp:89
std::vector< FilterEntry > exclude
Conditions that reject a row when any matches.
Definition filter.hpp:91
A column's identity and kind.
One column's compiled filter.
Definition filter.hpp:153
Combine combine
How include combines.
Definition filter.hpp:157
std::size_t column
Column index.
Definition filter.hpp:155
std::vector< Entry > exclude
Compiled exclude entries.
Definition filter.hpp:161
std::vector< Entry > include
Compiled include entries.
Definition filter.hpp:159
One compiled condition.
Definition filter.hpp:137
bool caseSensitive
Text operators compare raw text.
Definition filter.hpp:141
std::array< math::Rational, 2 > exact
Exact operands (integers, keys, decimals, quantities).
Definition filter.hpp:145
std::string text
Text operand: folded, or raw when caseSensitive.
Definition filter.hpp:149
std::array< std::int64_t, 2 > ints
Integer operands (dates, date-times, booleans).
Definition filter.hpp:143
std::array< double, 2 > reals
Floating-point operands (Number).
Definition filter.hpp:147
One compiled group: per column, the entries that apply to it.
Definition filter.hpp:165
std::vector< Column > columns
The group's columns, each with the entries that compiled for it.
Definition filter.hpp:167
One filter condition. Exactly one operator member is set.
Definition filter.hpp:49
std::optional< bool > caseSensitive
Text operators compare the text as given rather than folded.
Definition filter.hpp:75
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< bool > isEmpty
true: the cell is empty. Any column.
Definition filter.hpp:71
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< bool > notEmpty
true: the cell is not empty. Any column.
Definition filter.hpp:73
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
bool empty() const noexcept
Whether the spec filters nothing.
Definition filter.hpp:114
A filter over several columns: a row matches when the entries match on any of them,...
Definition filter.hpp:96
std::vector< FilterEntry > include
The conditions.
Definition filter.hpp:102
Combine combine
How include entries combine on one column.
Definition filter.hpp:100
std::vector< std::string > columns
The columns searched.
Definition filter.hpp:98
One column's keys, built once and shared, immutable, between jobs.
Definition sort.hpp:178
std::vector< double > reals
Floating-point keys.
Definition sort.hpp:188
ColumnKind kind
The column's kind.
Definition sort.hpp:180
std::vector< math::Rational > exact
Exact keys.
Definition sort.hpp:186
std::vector< std::int64_t > ints
Integer keys.
Definition sort.hpp:184
std::vector< CellState > states
The state of each row's cell.
Definition sort.hpp:182
std::vector< std::string > folded
Primary-strength folds, which text filters match.
Definition sort.hpp:192
std::vector< std::string > raw
The text as the source gave it, for case-sensitive filters.
Definition sort.hpp:194
The services an engine sorts and filters with. A null member is replaced by its default.
std::shared_ptr< DateParser const > datesOrDefault() const
The date parser to use.
std::shared_ptr< TextCollator const > collatorOrDefault() const
The collator to use.
The error every fallible table call returns.
std::string message
A human-readable description.
The operator an entry sets and its operand text.
Definition filter.hpp:323
CompiledFilter::Op op
The operator.
Definition filter.hpp:325
std::array< std::string_view, 2 > values
Its operands; the second only for between.
Definition filter.hpp:327