26#include <core/async/StopToken.hpp>
40namespace morph::table {
51 std::optional<std::string>
eq;
53 std::optional<std::string>
ne;
55 std::optional<std::string>
lt;
57 std::optional<std::string>
le;
59 std::optional<std::string>
gt;
61 std::optional<std::string>
ge;
63 std::optional<std::array<std::string, 2>>
between;
121 enum class Op : std::uint8_t {
143 std::array<std::int64_t, 2>
ints{};
145 std::array<math::Rational, 2>
exact{};
172 [[nodiscard]]
bool empty() const noexcept {
return _columns.empty() && _groups.empty(); }
177 std::vector<std::size_t> out;
178 out.reserve(_columns.size());
179 for (
auto const& column : _columns) {
180 out.push_back(column.column);
182 for (
auto const& group : _groups) {
183 for (
auto const& column : group.columns) {
184 out.push_back(column.column);
187 std::ranges::sort(out);
188 auto const [first, last] = std::ranges::unique(out);
189 out.erase(first, last);
195 [[nodiscard]] std::span<Column const>
columns() const noexcept {
return _columns; }
199 [[nodiscard]] std::span<Group const>
groups() const noexcept {
return _groups; }
207 if (!filter.include.empty()) {
210 for (
auto const& entry : filter.include) {
211 if (
matches(entry, keys, row) != all) {
220 return std::ranges::none_of(filter.exclude, [&](
Entry const& entry) { return matches(entry, keys, row); });
229 auto const state = keys.
states[row];
230 if (entry.
op == Op::IsEmpty) {
233 if (entry.
op == Op::NotEmpty) {
245 return compare(entry, keys.
exact[row], entry.
exact);
247 return compare(entry, keys.
reals[row], entry.
reals);
251 return compare(entry, keys.
ints[row], entry.
ints);
262 std::span<ColumnInfo const>
columns,
265 template <
typename T>
266 [[nodiscard]]
static bool compare(Entry
const& entry, T
const& value, std::array<T, 2>
const& operand) {
269 return value == operand[0];
271 return !(value == operand[0]);
273 return value < operand[0];
275 return value <= operand[0];
277 return value > operand[0];
279 return value >= operand[0];
281 return operand[0] <= value && value <= operand[1];
292 [[nodiscard]]
static bool matchText(Entry
const& entry, std::string_view text) {
295 return text == entry.text;
297 return text != entry.text;
299 return text.contains(entry.text);
301 return text.starts_with(entry.text);
303 return text.ends_with(entry.text);
316 std::vector<Column> _columns;
317 std::vector<Group> _groups;
327 std::array<std::string_view, 2>
values{};
334[[nodiscard]]
inline std::expected<RawEntry, TableError>
readEntry(
FilterEntry const& entry, std::string_view column) {
338 auto const take = [&](std::optional<std::string>
const& member, Op op) {
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);
356 out.
op = Op::Between;
359 if (entry.
isEmpty.value_or(
false)) {
361 out.
op = Op::IsEmpty;
363 if (entry.
notEmpty.value_or(
false)) {
365 out.
op = Op::NotEmpty;
369 .column = std::string{column},
370 .message =
"a filter entry sets exactly one operator"});
381 if (op == Op::IsEmpty || op == Op::NotEmpty) {
384 bool const textOp = op == Op::Contains || op == Op::StartsWith || op == Op::EndsWith;
387 return textOp || op == Op::Eq || op == Op::Ne;
389 return op == Op::Eq || op == Op::Ne;
422 entry.
exact.at(slot) = *value;
430 entry.
reals.at(slot) = *value;
436 auto const value = info.kind ==
ColumnKind::Date ? parser.parseDate(text, info.format)
437 : parser.parseDateTime(text, info.format);
441 entry.
ints.at(slot) = *value;
445 if (text !=
"true" && text !=
"false") {
448 entry.
ints.at(slot) = text ==
"true" ? 1 : 0;
467 auto const raw =
readEntry(entry, info.id);
469 return std::unexpected(raw.error());
474 .message =
"operator not defined for column " + info.id});
479 if (out.
op == CompiledFilter::Op::IsEmpty || out.
op == CompiledFilter::Op::NotEmpty) {
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)) {
487 .message =
"value \"" + std::string{raw->values.at(slot)} +
488 "\" does not parse for column " + info.id});
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) {
521[[nodiscard]]
inline std::expected<void, TableError>
compileEntries(std::span<FilterEntry const> entries,
523 std::vector<CompiledFilter::Entry>& out) {
524 for (
auto const& entry : entries) {
527 return std::unexpected(std::move(compiled.error()));
529 out.push_back(std::move(*compiled));
541 std::string
const& columnId,
ColumnFilter const& filter, std::span<ColumnInfo const> columns,
543 auto const index =
findColumn(columns, columnId);
548 if (
auto included =
compileEntries(filter.include, columns[*index], services, out.
include); !included) {
549 return std::unexpected(std::move(included.error()));
551 if (
auto excluded =
compileEntries(filter.exclude, columns[*index], services, out.
exclude); !excluded) {
552 return std::unexpected(std::move(excluded.error()));
564 std::span<ColumnInfo const> columns,
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);
575 for (std::size_t i = 0; i < group.
include.size(); ++i) {
579 column.include.push_back(std::move(*compiled));
581 return std::unexpected(std::move(compiled.error()));
583 lastError = std::move(compiled.error());
591 out.
columns.push_back(std::move(column));
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"}));
615 std::span<ColumnInfo const>
columns,
618 for (
auto const& [columnId, filter] : spec.
columns) {
621 return std::unexpected(std::move(compiled.error()));
623 if (!compiled->include.empty() || !compiled->exclude.empty()) {
624 out._columns.push_back(std::move(*compiled));
627 for (
auto const& group : spec.
groups) {
630 return std::unexpected(std::move(compiled.error()));
632 if (!group.include.empty()) {
633 out._groups.push_back(std::move(*compiled));
653 : _filter{&filter}, _keys{std::move(keys)}, _rows{std::move(candidates)}, _size{_rows.size()} {}
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) {
664 for (; _read < end; ++_read) {
665 auto const row = _rows[_read];
667 _rows[_write++] = row;
687 [[nodiscard]] std::vector<std::uint32_t>
take() && {
689 return std::move(_rows);
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];
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);
705 CompiledFilter
const* _filter;
706 std::vector<KeyColumn const*> _keys;
707 std::vector<std::uint32_t> _rows;
709 std::size_t _stage = 0;
710 std::size_t _read = 0;
711 std::size_t _write = 0;
720struct glz::meta<morph::table::Combine> {
723 static constexpr auto value = glz::enumerate(
"any", Any,
"all", All);
A filter compiled against a table's columns: every value parsed.
std::span< Group const > groups() const noexcept
The compiled groups, in evaluation order.
std::span< Column const > columns() const noexcept
The compiled column filters, in evaluation order.
static bool matches(Entry const &entry, KeyColumn const &keys, std::uint32_t row)
Whether one entry matches one cell.
std::vector< std::size_t > columnsUsed() const
The column indices whose keys evaluation reads.
bool empty() const noexcept
Whether the filter matches every row.
static bool passes(Column const &filter, KeyColumn const &keys, std::uint32_t row)
Whether a row passes one column filter.
friend std::expected< CompiledFilter, TableError > compileFilter(FilterSpec const &spec, std::span< ColumnInfo const > columns, Services const &services)
Compiles spec against columns.
std::vector< std::uint32_t > take() &&
The surviving rows. Valid once run returned true.
FilterRun(CompiledFilter const &filter, std::vector< KeyColumn const * > keys, std::vector< std::uint32_t > candidates)
Prepares to filter candidates.
bool run(Deadline deadline, ::core::async::StopToken const &stop)
Filters until done, the deadline passes, or a stop is requested.
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.
@ 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.
TableError unknownColumn(std::string const &columnId)
An UnknownColumn error.
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.
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.
std::optional< std::size_t > findColumn(std::span< ColumnInfo const > columns, std::string_view columnId)
The index of the column with id columnId.
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.
Combine
How a column's include entries combine.
@ All
A row matches when every entry matches.
@ Any
A row matches when any entry matches.
bool operatorDefined(ColumnKind kind, CompiledFilter::Op op)
Whether a column kind defines an operator.
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.
std::expected< CompiledFilter::Entry, TableError > compileEntry(FilterEntry const &entry, ColumnInfo const &info, Services const &services)
Compiles one entry for one column.
Typed sort keys and the chunked, stoppable merge sort over them.
@ Valid
The cell has a key.
@ Empty
The cell is empty; it sorts with invalid cells.
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...
std::chrono::steady_clock::time_point Deadline
A deadline for one step of work; kNoDeadline runs to completion.
bool shouldYield(Deadline deadline, ::core::async::StopToken const &stop)
Whether a step should return now.
std::optional< double > parseReal(std::string_view text)
Parses a floating-point number, all of text.
Strong type for a decimal-precision count.
Exact rational number with a runtime decimal-precision tag.
The filter on one column.
Combine combine
How include entries combine.
std::vector< FilterEntry > include
Conditions a row must meet (combined by combine); empty means no condition.
std::vector< FilterEntry > exclude
Conditions that reject a row when any matches.
A column's identity and kind.
One column's compiled filter.
Combine combine
How include combines.
std::size_t column
Column index.
std::vector< Entry > exclude
Compiled exclude entries.
std::vector< Entry > include
Compiled include entries.
bool caseSensitive
Text operators compare raw text.
std::array< math::Rational, 2 > exact
Exact operands (integers, keys, decimals, quantities).
std::string text
Text operand: folded, or raw when caseSensitive.
std::array< std::int64_t, 2 > ints
Integer operands (dates, date-times, booleans).
std::array< double, 2 > reals
Floating-point operands (Number).
One compiled group: per column, the entries that apply to it.
std::vector< Column > columns
The group's columns, each with the entries that compiled for it.
One filter condition. Exactly one operator member is set.
std::optional< bool > caseSensitive
Text operators compare the text as given rather than folded.
std::optional< std::string > contains
Text contains. Text columns.
std::optional< std::string > ge
Greater than or equal to. Numeric, date and date-time columns.
std::optional< std::string > ne
Not equal to.
std::optional< std::array< std::string, 2 > > between
Between two values, both inclusive. Numeric, date and date-time columns.
std::optional< bool > isEmpty
true: the cell is empty. Any column.
std::optional< std::string > eq
Equal to (text: at primary strength unless caseSensitive).
std::optional< std::string > endsWith
Text ends with. Text columns.
std::optional< std::string > gt
Greater than. Numeric, date and date-time columns.
std::optional< std::string > lt
Less than. Numeric, date and date-time columns.
std::optional< std::string > le
Less than or equal to. Numeric, date and date-time columns.
std::optional< bool > notEmpty
true: the cell is not empty. Any column.
std::optional< std::string > startsWith
Text starts with. Text columns.
std::map< std::string, ColumnFilter > columns
Per-column filters, by column id.
std::vector< GroupFilter > groups
Multi-column groups.
bool empty() const noexcept
Whether the spec filters nothing.
A filter over several columns: a row matches when the entries match on any of them,...
std::vector< FilterEntry > include
The conditions.
Combine combine
How include entries combine on one column.
std::vector< std::string > columns
The columns searched.
One column's keys, built once and shared, immutable, between jobs.
std::vector< double > reals
Floating-point keys.
ColumnKind kind
The column's kind.
std::vector< math::Rational > exact
Exact keys.
std::vector< std::int64_t > ints
Integer keys.
std::vector< CellState > states
The state of each row's cell.
std::vector< std::string > folded
Primary-strength folds, which text filters match.
std::vector< std::string > raw
The text as the source gave it, for case-sensitive filters.
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.
CompiledFilter::Op op
The operator.
std::array< std::string_view, 2 > values
Its operands; the second only for between.