dasmig-core 1.2.0
Shared random engine, distributions and data tiers for the dasmig generator libraries
Loading...
Searching...
No Matches
table.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2020-2026 Diego Dasso Migotto
3
4#ifndef DASMIG_CORE_TABLE_HPP
5#define DASMIG_CORE_TABLE_HPP
6
7#include "rng.hpp"
8#include "tsv.hpp"
9
10#include <cstddef>
11#include <cstdint>
12#include <functional>
13#include <initializer_list>
14#include <optional>
15#include <set>
16#include <stdexcept>
17#include <string>
18#include <string_view>
19#include <unordered_map>
20#include <utility>
21#include <vector>
22
23/// @file table.hpp
24/// @brief Data tables built from a base layer plus extension layers.
25/// @author Diego Dasso Migotto (diegomigotto at hotmail dot com)
26///
27/// Every library keeps its data in layered tables, so users can add to it,
28/// override it or replace it with files of the same TSV format, without
29/// touching the shipped data. See doc/usage.md ("Extending data").
30
31namespace dasmig::core
32{
33
34/// @brief How a layer combines with the rows already loaded.
35enum class layer_mode : std::uint8_t
36{
37 add, ///< New keys only; a key that already exists is an error.
38 override, ///< A row replaces the row with the same key; new keys are added.
39 replace ///< The layer replaces every row loaded before it.
40};
41
42// Members have {} initializers so designated initializers may leave them
43// out without -Wmissing-designated-field-initializers warnings.
44// NOLINTBEGIN(readability-redundant-member-init)
45
46/// @brief The columns of a table.
48{
49 /// Columns that identify a row, together. Must not be empty.
50 std::vector<std::string> key{};
51 /// Columns every layer must have (the key columns are always required).
52 std::vector<std::string> required{};
53 /// Other columns the library reads. Any column not listed here, in
54 /// `required` or in `key`, other than `disabled`, is reported as unknown.
55 std::vector<std::string> optional{};
56};
57
58/// @brief What one loaded layer did.
60{
61 std::string source{}; ///< The layer's source name.
62 layer_mode mode{layer_mode::override}; ///< How it was applied.
63 std::size_t rows{0}; ///< Data rows in the layer.
64 std::size_t added{0}; ///< Rows with new keys.
65 std::size_t replaced{0}; ///< Rows that replaced an existing key.
66 std::size_t removed{0}; ///< Keys removed by `disabled` rows.
67 std::uint64_t hash{0}; ///< tsv_document::content_hash().
68 std::vector<std::string> warnings{}; ///< Non-fatal problems (unknown columns, ...).
69};
70
71// NOLINTEND(readability-redundant-member-init)
72
73/// @brief A table assembled from layers of TSV data.
74///
75/// - Layers apply in the order loaded; later layers win.
76/// - Rows keep the order they first appeared in: an overriding row takes the
77/// place of the row it replaces, new rows are appended. Row order is part
78/// of the output (weighted choices depend on it).
79/// - A row with `disabled` set to `1` removes its key (override and replace
80/// modes). A layer needs the key columns; the other required columns are
81/// needed only by rows that add or replace data, so a layer that only
82/// removes rows can be just `key` and `disabled`.
83/// - A layer is validated completely before it is applied; when it fails,
84/// the table is unchanged.
85///
86/// @code
87/// dasmig::core::layered_table table{{.key = {"country_code"},
88/// .required = {"male_height_cm"}}};
89/// table.extend(dasmig::core::tsv_document::load("resources/lite/biodata.tsv"),
90/// dasmig::core::layer_mode::replace);
91/// table.extend(dasmig::core::tsv_document::load("mods/tall_people.tsv"));
92/// for (const auto& row : table.rows()) { ... row.get<double>("male_height_cm") ... }
93/// @endcode
95{
96 public:
97 /// @brief Name of the column that removes a key when set to 1.
98 static constexpr std::string_view disabled_column = "disabled";
99
100 /// @brief An empty table.
101 /// @param schema The columns.
102 /// @throws std::invalid_argument If the schema has no key column.
103 explicit layered_table(table_schema schema) : _schema{std::move(schema)}
104 {
105 if (_schema.key.empty())
106 {
107 throw std::invalid_argument("layered_table: the schema needs a key column");
108 }
109 }
110
111 /// @brief Apply a layer.
112 ///
113 /// @p validate is called for every row the layer adds or replaces
114 /// (not for `disabled` rows) before anything changes; it should parse
115 /// the row the way the library will and throw tsv_error on bad data.
116 ///
117 /// @param doc The layer's data.
118 /// @param mode How it combines with the rows loaded before.
119 /// @param validate Optional `void(const tsv_row&)` row check.
120 /// @return What the layer did.
121 /// @throws tsv_error On a missing key column, a missing required column
122 /// (for a row that adds or replaces data), an empty or repeated
123 /// key, an invalid `disabled` value, a clash in add mode, or an
124 /// error from @p validate. The table is then unchanged.
125 const layer_info& extend(tsv_document doc, layer_mode mode = layer_mode::override,
126 const std::function<void(const tsv_row&)>& validate = {})
127 {
128 layer_info info{.source = doc.source(), .mode = mode, .rows = doc.size(),
129 .hash = doc.content_hash()};
130 working w;
131 w.missing_required = check_columns(doc, info);
132 if (mode != layer_mode::replace)
133 {
134 w.rows = _rows;
135 w.index = _index;
136 }
137 w.removed.assign(w.rows.size(), false);
138 w.touched.assign(w.rows.size(), untouched);
139 w.rows.reserve(w.rows.size() + doc.size());
140 w.index.reserve(w.rows.size() + doc.size());
141 const bool has_disabled = doc.has_column(disabled_column);
142 bool any_removed = false;
143 for (const auto& row : doc.rows())
144 {
145 any_removed |= apply_row(row, mode, has_disabled, validate, w, info);
146 }
147
148 // Compact removed rows and rebuild the index; without removals the
149 // working index is already right.
150 std::vector<tsv_row> kept;
151 key_index kept_index;
152 if (any_removed)
153 {
154 kept.reserve(w.rows.size());
155 kept_index.reserve(w.rows.size());
156 for (std::size_t i = 0; i < w.rows.size(); ++i)
157 {
158 if (w.removed[i]) continue;
159 kept_index.insert(key_index::hash_of(key_of(w.rows[i])), kept.size());
160 kept.push_back(w.rows[i]);
161 }
162 }
163 else
164 {
165 kept = std::move(w.rows);
166 kept_index = std::move(w.index);
167 }
168
169 // Commit: nothing below throws except allocation. A replace layer
170 // discards everything before it, so the history (and data_hash())
171 // starts again: reloading the same base gives the same hash.
172 if (mode == layer_mode::replace)
173 {
174 _documents.clear();
175 _layers.clear();
176 }
177 _documents.push_back(std::move(doc));
178 _rows = std::move(kept);
179 _index = std::move(kept_index);
180 _layers.push_back(std::move(info));
181 return _layers.back();
182 }
183
184 /// @brief Remove every row and layer.
185 void clear() noexcept
186 {
187 _documents.clear();
188 _rows.clear();
189 _index.clear();
190 _layers.clear();
191 }
192
193 /// @brief The rows in effect, in order.
194 /// @return The rows.
195 [[nodiscard]] const std::vector<tsv_row>& rows() const noexcept { return _rows; }
196
197 /// @brief Number of rows in effect.
198 /// @return The count.
199 [[nodiscard]] std::size_t size() const noexcept { return _rows.size(); }
200
201 /// @brief Whether no rows are in effect.
202 /// @return True when empty.
203 [[nodiscard]] bool empty() const noexcept { return _rows.empty(); }
204
205 /// @brief Find a row by key.
206 /// @param key The key cells, in schema key order.
207 /// @return The row, or empty.
208 /// @throws std::invalid_argument If the number of cells is not the number
209 /// of key columns.
210 [[nodiscard]] std::optional<tsv_row> find(std::initializer_list<std::string_view> key) const
211 {
212 if (key.size() != _schema.key.size())
213 {
214 throw std::invalid_argument("layered_table::find: wrong number of key cells");
215 }
216 std::string joined;
217 std::size_t i = 0;
218 for (const auto part : key)
219 {
220 if (i++ > 0) joined += key_separator;
221 joined += part;
222 }
223 const auto at = locate(_index, _rows, *this, joined);
224 if (!at) return std::nullopt;
225 return _rows[*at];
226 }
227
228 /// @brief The layers in effect, in order: the last replace layer and
229 /// those after it.
230 /// @return The layer records.
231 [[nodiscard]] const std::vector<layer_info>& layers() const noexcept { return _layers; }
232
233 /// @brief Hash of the loaded layers: their content hashes and modes, in
234 /// order. 0 when nothing is loaded.
235 ///
236 /// Put it in the seed_signature of results so a replay can tell whether
237 /// it runs against the same data.
238 ///
239 /// @return The hash.
240 [[nodiscard]] std::uint64_t data_hash() const noexcept
241 {
242 std::uint64_t h = 0;
243 for (const auto& layer : _layers)
244 {
245 h = hash_combine(hash_combine(h, layer.hash), static_cast<std::uint64_t>(layer.mode));
246 }
247 return h;
248 }
249
250 /// @brief The schema.
251 /// @return The columns.
252 [[nodiscard]] const table_schema& schema() const noexcept { return _schema; }
253
254 private:
255 static constexpr char key_separator = '\x1f';
256
257 // Keys to row indices, as open addressing over 32-bit slots: 16 bytes
258 // per row instead of a node and a copy of the key. A slot keeps the
259 // key's hash and its row's index; the key itself is read from the row.
260 // Only looked up, never iterated, so the order does not reach the output.
261 class key_index
262 {
263 public:
264 [[nodiscard]] static std::uint32_t hash_of(std::string_view key) noexcept
265 {
266 return static_cast<std::uint32_t>(std::hash<std::string_view>{}(key));
267 }
268
269 // The row whose key satisfies @p matches(row_index), among those
270 // stored under @p hash.
271 template <typename Matches>
272 [[nodiscard]] std::optional<std::size_t> find(std::uint32_t hash, Matches&& matches) const
273 {
274 if (_slots.empty()) return std::nullopt;
275 const std::size_t mask = _slots.size() - 1;
276 for (std::size_t i = hash & mask;; i = (i + 1) & mask)
277 {
278 const slot& s = _slots[i];
279 if (s.row == vacant) return std::nullopt;
280 if (s.hash == hash && matches(static_cast<std::size_t>(s.row))) return s.row;
281 }
282 }
283
284 // Add a key known to be absent.
285 void insert(std::uint32_t hash, std::size_t row)
286 {
287 if (row >= vacant) throw std::length_error("layered_table: too many rows");
288 if ((_count + 1) * 2 > _slots.size()) grow(_slots.empty() ? 16 : _slots.size() * 2);
289 place(hash, static_cast<std::uint32_t>(row));
290 ++_count;
291 }
292
293 void clear() noexcept
294 {
295 _slots.clear();
296 _count = 0;
297 }
298
299 void reserve(std::size_t rows)
300 {
301 std::size_t want = 16;
302 while (want < rows * 2) want *= 2;
303 if (want > _slots.size()) grow(want);
304 }
305
306 private:
307 static constexpr std::uint32_t vacant = static_cast<std::uint32_t>(-1);
308 struct slot
309 {
310 std::uint32_t hash{0};
311 std::uint32_t row{vacant};
312 };
313
314 void place(std::uint32_t hash, std::uint32_t row)
315 {
316 const std::size_t mask = _slots.size() - 1;
317 std::size_t i = hash & mask;
318 while (_slots[i].row != vacant) i = (i + 1) & mask;
319 _slots[i] = {hash, row};
320 }
321
322 void grow(std::size_t slots)
323 {
324 std::vector<slot> old = std::move(_slots);
325 _slots.assign(slots, slot{});
326 for (const slot& s : old)
327 {
328 if (s.row != vacant) place(s.hash, s.row);
329 }
330 }
331
332 std::vector<slot> _slots;
333 std::size_t _count{0};
334 };
335
336 // Whether @p row has the key @p joined (its key cells joined by the
337 // separator), compared without building the row's own key.
338 [[nodiscard]] bool key_matches(const tsv_row& row, std::string_view joined) const
339 {
340 for (std::size_t i = 0; i < _schema.key.size(); ++i)
341 {
342 if (i > 0)
343 {
344 if (joined.empty() || joined.front() != key_separator) return false;
345 joined.remove_prefix(1);
346 }
347 const auto cell = row[_schema.key[i]];
348 if (!joined.starts_with(cell)) return false;
349 joined.remove_prefix(cell.size());
350 }
351 return joined.empty();
352 }
353
354 [[nodiscard]] static std::optional<std::size_t> locate(const key_index& index,
355 const std::vector<tsv_row>& rows,
356 const layered_table& self,
357 std::string_view joined)
358 {
359 return index.find(key_index::hash_of(joined), [&](std::size_t r) {
360 return self.key_matches(rows[r], joined);
361 });
362 }
363
364 static constexpr std::size_t untouched = static_cast<std::size_t>(-1);
365
366 // The table being built by extend(), applied only when every row passed.
367 struct working
368 {
369 std::vector<tsv_row> rows;
370 key_index index;
371 std::vector<bool> removed;
372 // Per row: the layer line that added, replaced or removed it, or
373 // `untouched`. Finds keys repeated within the layer.
374 std::vector<std::size_t> touched;
375 std::unordered_map<std::string, std::size_t> disabled_unloaded;
376 // keys the layer disabled that were not loaded -> line
377 std::string missing_required; // a required column the layer lacks, if any
378 };
379
380 // Returns true when the row removed a loaded row.
381 bool apply_row(const tsv_row& row, layer_mode mode, bool has_disabled,
382 const std::function<void(const tsv_row&)>& validate, working& w,
383 layer_info& info) const
384 {
385 std::string key = key_of(row);
386 const auto existing = locate(w.index, w.rows, *this, key);
387 const bool exists = existing.has_value();
388 // Keys are unique within a layer, so a key is never removed and added
389 // again by the same layer.
390 std::size_t first_line = untouched;
391 if (exists)
392 {
393 first_line = w.touched[*existing];
394 }
395 else if (const auto it = w.disabled_unloaded.find(key); it != w.disabled_unloaded.end())
396 {
397 first_line = it->second;
398 }
399 if (first_line != untouched)
400 {
401 throw row.error("", "key repeated (first on line " + std::to_string(first_line) + ")");
402 }
403
404 if (has_disabled && is_disabled(row))
405 {
406 if (mode == layer_mode::add)
407 {
408 throw row.error(disabled_column, "a layer in add mode cannot disable rows");
409 }
410 if (exists)
411 {
412 w.removed[*existing] = true;
413 w.touched[*existing] = row.line();
414 ++info.removed;
415 return true;
416 }
417 w.disabled_unloaded.emplace(std::move(key), row.line());
418 info.warnings.push_back(row.source() + ":" + std::to_string(row.line()) +
419 ": disabled key is not loaded");
420 return false;
421 }
422 // Only rows that add or replace data need the required columns; a
423 // layer that just disables rows needs the key alone.
424 if (!w.missing_required.empty())
425 {
426 throw row.error(w.missing_required, "required column missing");
427 }
428 if (validate) validate(row);
429 if (exists)
430 {
431 if (mode == layer_mode::add)
432 {
433 throw row.error("", "key already loaded (layer is in add mode)");
434 }
435 w.rows[*existing] = row;
436 w.touched[*existing] = row.line();
437 ++info.replaced;
438 return false;
439 }
440 w.index.insert(key_index::hash_of(key), w.rows.size());
441 w.rows.push_back(row);
442 w.removed.push_back(false);
443 w.touched.push_back(row.line());
444 ++info.added;
445 return false;
446 }
447
448 // Throws when a key column is missing; returns the first missing required
449 // column (empty when none), which only rows with data need.
450 std::string check_columns(const tsv_document& doc, layer_info& info) const
451 {
452 std::set<std::string_view> known{disabled_column};
453 std::string missing_required;
454 for (const auto& column : _schema.key)
455 {
456 known.insert(column);
457 if (!doc.has_column(column))
458 {
459 throw tsv_error{doc.source(), 0, column, "key column missing"};
460 }
461 }
462 for (const auto& column : _schema.required)
463 {
464 known.insert(column);
465 if (missing_required.empty() && !doc.has_column(column)) missing_required = column;
466 }
467 for (const auto& column : _schema.optional) known.insert(column);
468 for (const auto& column : doc.columns())
469 {
470 if (!known.contains(column))
471 {
472 info.warnings.push_back(doc.source() + ": column '" + column +
473 "' is not used and was ignored");
474 }
475 }
476 return missing_required;
477 }
478
479 [[nodiscard]] std::string key_of(const tsv_row& row) const
480 {
481 std::string key;
482 for (std::size_t i = 0; i < _schema.key.size(); ++i)
483 {
484 const auto cell = row[_schema.key[i]];
485 if (cell.empty())
486 {
487 throw row.error(_schema.key[i], "key cell is empty");
488 }
489 if (i > 0) key += key_separator;
490 key += cell;
491 }
492 return key;
493 }
494
495 [[nodiscard]] static bool is_disabled(const tsv_row& row)
496 {
497 const auto value = row[disabled_column];
498 if (value.empty() || value == "0") return false;
499 if (value == "1") return true;
500 throw row.error(disabled_column, "expected 0, 1 or empty, got '" + std::string{value} + "'");
501 }
502
503 table_schema _schema;
504 std::vector<tsv_document> _documents; // keeps the rows' data alive
505 std::vector<tsv_row> _rows;
506 key_index _index;
507 std::vector<layer_info> _layers;
508};
509
510} // namespace dasmig::core
511
512#endif // DASMIG_CORE_TABLE_HPP
A table assembled from layers of TSV data.
Definition table.hpp:95
static constexpr std::string_view disabled_column
Name of the column that removes a key when set to 1.
Definition table.hpp:98
layered_table(table_schema schema)
An empty table.
Definition table.hpp:103
const std::vector< layer_info > & layers() const noexcept
The layers in effect, in order: the last replace layer and those after it.
Definition table.hpp:231
const layer_info & extend(tsv_document doc, layer_mode mode=layer_mode::override, const std::function< void(const tsv_row &)> &validate={})
Apply a layer.
Definition table.hpp:125
std::uint64_t data_hash() const noexcept
Hash of the loaded layers: their content hashes and modes, in order. 0 when nothing is loaded.
Definition table.hpp:240
const table_schema & schema() const noexcept
The schema.
Definition table.hpp:252
std::optional< tsv_row > find(std::initializer_list< std::string_view > key) const
Find a row by key.
Definition table.hpp:210
bool empty() const noexcept
Whether no rows are in effect.
Definition table.hpp:203
const std::vector< tsv_row > & rows() const noexcept
The rows in effect, in order.
Definition table.hpp:195
void clear() noexcept
Remove every row and layer.
Definition table.hpp:185
std::size_t size() const noexcept
Number of rows in effect.
Definition table.hpp:199
A parsed tab-separated file.
Definition tsv.hpp:424
std::size_t size() const noexcept
Number of data rows.
Definition tsv.hpp:501
bool has_column(std::string_view name) const
Whether a column exists.
Definition tsv.hpp:494
const std::string & source() const noexcept
The source name used in errors.
Definition tsv.hpp:529
std::uint64_t content_hash() const noexcept
Hash of the parsed content: columns and cells, without comments, blank lines, line-end style or byte ...
Definition tsv.hpp:537
One row of a tsv_document. A light view: copy it freely while the document, or any copy of it,...
Definition tsv.hpp:290
The random engine every generator library uses: xoshiro256** seeded through SplitMix64,...
constexpr std::uint64_t hash_combine(std::uint64_t hash, std::uint64_t value) noexcept
Combine a hash with another 64-bit value, order-sensitively.
Definition rng.hpp:85
What one loaded layer did.
Definition table.hpp:60
layer_mode mode
How it was applied.
Definition table.hpp:62
std::vector< std::string > warnings
Non-fatal problems (unknown columns, ...).
Definition table.hpp:68
std::size_t replaced
Rows that replaced an existing key.
Definition table.hpp:65
std::size_t rows
Data rows in the layer.
Definition table.hpp:63
std::size_t added
Rows with new keys.
Definition table.hpp:64
std::string source
The layer's source name.
Definition table.hpp:61
std::size_t removed
Keys removed by disabled rows.
Definition table.hpp:66
std::uint64_t hash
tsv_document::content_hash().
Definition table.hpp:67
The columns of a table.
Definition table.hpp:48
std::vector< std::string > key
Columns that identify a row, together. Must not be empty.
Definition table.hpp:50
std::vector< std::string > optional
Definition table.hpp:55
std::vector< std::string > required
Columns every layer must have (the key columns are always required).
Definition table.hpp:52
layer_mode
How a layer combines with the rows already loaded.
Definition table.hpp:36
@ add
New keys only; a key that already exists is an error.
@ replace
The layer replaces every row loaded before it.
Tab-separated data files with typed access and located errors.