Everett
Loading...
Searching...
No Matches
sqlite_catalog.h
Go to the documentation of this file.
1
13#pragma once
14
15#include <everett/mapped_blob.h>
16#include <everett/mapped_cola.h>
18#include <sqlite3.h>
19
20#include <algorithm>
21#include <cstddef>
22#include <cstdint>
23#include <filesystem>
24#include <limits>
25#include <optional>
26#include <span>
27#include <stdexcept>
28#include <string>
29#include <string_view>
30#include <type_traits>
31#include <unordered_set>
32#include <utility>
33#include <vector>
34
35static_assert(SQLITE_VERSION_NUMBER >= 3051003, "Everett requires SQLite 3.51.3 or later");
36
37namespace everett {
40 int busy_timeout_ms = 250;
41 // The caller owns this scope's process lease. Only its new reservations
42 // are charged to the scope; existing published owners remain independent.
43 std::optional<object_id> private_scope{};
44 };
46 struct catalog_saved_root { blob_identity head; std::string owner; };
48 // Ordered physical inputs to the library's KV03 replacement merge. Schema
49 // bytes are application-owned; the operation and physical policy are not.
55 // Every generation has its own permanent root owner. Names and owners are
56 // arbitrary nonempty byte strings; generations never wrap or get reused.
58 std::string name;
59 std::uint64_t generation;
61 std::string owner;
62 bool operator==(catalog_timeline_head const &) const = default;
63 };
66 catalog_timeline_head head; // New generation, or the observed head on conflict.
67 bool operator==(catalog_timeline_publication const &) const = default;
68 };
69 // The runtime's small, versioned continuation travels atomically with its
70 // immutable root. SQLite does not interpret the checkpoint's codec.
72 std::vector<blob_identity> pairs;
73 std::vector<object_id> natives;
74 bool operator==(catalog_auxiliary_roots const &) const = default;
75 };
88 std::string kind;
89 std::vector<std::byte> request;
90 std::vector<std::byte> outcome;
91 };
92 struct catalog_error : std::runtime_error {
93 catalog_error(std::string message, int code, std::string operation = {}, bool uncertain = false)
94 : std::runtime_error(std::move(message)), code(code), operation(std::move(operation)),
96 int code; // SQLite extended result code; custom COMMIT hooks preserve their result.
97 std::string operation;
99 };
100
101 // This seam models an unacknowledged COMMIT, not storage or power failure.
102 // A custom hook must be noexcept and either execute COMMIT or return an error.
104 int commit(sqlite3 * db) noexcept { return sqlite3_exec(db, "COMMIT", nullptr, nullptr, nullptr); }
105 };
106
107 namespace catalog_detail {
108 using bytes = std::vector<std::byte>;
109 inline void number(bytes & out, std::uint64_t value) {
110 for (unsigned i = 0; i != 8; ++i) out.push_back(std::byte(value >> (8 * i)));
111 }
112 inline void field(bytes & out, std::string_view value) {
113 number(out, value.size());
114 auto data = std::as_bytes(std::span(value.data(), value.size()));
115 out.insert(out.end(), data.begin(), data.end());
116 }
117 inline void identity(bytes & out, object_id const & value) {
118 if (value.hex().size() != 32) throw std::invalid_argument("invalid Everett catalog identity");
119 field(out, value.hex());
120 }
121 inline void pair(bytes & out, blob_identity const & value) {
122 identity(out, value.native); identity(out, value.index);
123 }
124 inline void timeline(bytes & out, catalog_timeline_head const & value) {
125 field(out, value.name); number(out, value.generation); pair(out, value.head); field(out, value.owner);
126 }
127 inline void binary(bytes & out, std::span<std::byte const> value) {
128 number(out, value.size()); out.insert(out.end(), value.begin(), value.end());
129 }
130 inline void auxiliary(bytes & out, catalog_auxiliary_roots const & value) {
131 number(out, value.pairs.size());
132 for (auto const & root : value.pairs) pair(out, root);
133 number(out, value.natives.size());
134 for (auto const & native : value.natives) identity(out, native);
135 }
137 auto less = [](blob_identity const & a, blob_identity const & b) {
138 return a.native.hex() < b.native.hex() || (a.native == b.native && a.index.hex() < b.index.hex());
139 };
140 std::sort(value.pairs.begin(), value.pairs.end(), less);
141 value.pairs.erase(std::unique(value.pairs.begin(), value.pairs.end()), value.pairs.end());
142 std::erase(value.pairs, primary);
143 std::sort(value.natives.begin(), value.natives.end(), [](auto const & a, auto const & b) { return a.hex() < b.hex(); });
144 value.natives.erase(std::unique(value.natives.begin(), value.natives.end()), value.natives.end());
145 return value;
146 }
147 inline void session(bytes & out, catalog_session_head const & value) {
148 timeline(out, value.timeline); binary(out, value.checkpoint); auxiliary(out, value.auxiliary);
149 }
150 // Outcomes are decoded, rather than looking up today's mutable head during
151 // replay. These bounds also reject a malformed stored operation outcome.
153 std::span<std::byte const> data;
154 [[noreturn]] static void invalid() { throw catalog_error("invalid Everett timeline outcome", SQLITE_CORRUPT); }
155 std::uint64_t number() {
156 if (data.size() < 8) invalid();
157 std::uint64_t value = 0;
158 for (unsigned i = 0; i != 8; ++i) value |= std::uint64_t(std::to_integer<unsigned>(data[i])) << (8 * i);
159 data = data.subspan(8); return value;
160 }
161 std::string field() {
162 auto size = number();
163 if (size > data.size()) invalid();
164 std::string value(reinterpret_cast<char const *>(data.data()), static_cast<std::size_t>(size));
165 data = data.subspan(static_cast<std::size_t>(size)); return value;
166 }
168 auto name = field(); auto generation = number();
169 auto native = field(); auto index = field(); auto owner = field();
170 if (name.empty() || owner.empty() || generation > std::uint64_t(std::numeric_limits<std::int64_t>::max())) invalid();
171 try { return {std::move(name), generation, {object_id(native), object_id(index)}, std::move(owner)}; }
172 catch (std::invalid_argument const &) { invalid(); }
173 }
176 auto count = number();
177 if (count > data.size() / 80) invalid();
178 for (std::uint64_t i = 0; i != count; ++i) {
179 auto native = field(), index = field();
180 value.pairs.push_back({object_id(native), object_id(index)});
181 }
182 count = number();
183 if (count > data.size() / 40) invalid();
184 for (std::uint64_t i = 0; i != count; ++i) value.natives.emplace_back(field());
185 return value;
186 }
188 auto head = timeline();
189 auto size = number();
190 if (size > data.size()) invalid();
191 bytes checkpoint(data.begin(), data.begin() + static_cast<std::size_t>(size));
192 data = data.subspan(static_cast<std::size_t>(size));
193 auto retained = auxiliary();
194 return {std::move(head), std::move(checkpoint), std::move(retained)};
195 }
196 void end() const { if (!data.empty()) invalid(); }
197 };
198 inline void name(std::string_view value) {
199 if (value.empty()) throw std::invalid_argument("empty Everett catalog name");
200 }
201 inline std::int64_t integer(std::uint64_t value) {
202 if (value > std::uint64_t(std::numeric_limits<std::int64_t>::max()))
203 throw std::length_error("Everett catalog count exceeds SQLite integer range");
204 return static_cast<std::int64_t>(value);
205 }
206 inline bool storage_error(int code) noexcept {
207 switch (code & 255) {
208 case SQLITE_IOERR: case SQLITE_FULL: case SQLITE_CORRUPT: case SQLITE_NOTADB:
209 case SQLITE_NOMEM: case SQLITE_CANTOPEN: case SQLITE_PROTOCOL: return true;
210 default: return false;
211 }
212 }
213 [[noreturn]] inline void fail(sqlite3 * db, int code) {
214 throw catalog_error(db && sqlite3_errcode(db) != SQLITE_OK ? sqlite3_errmsg(db) : sqlite3_errstr(code), code);
215 }
216 inline void exec(sqlite3 * db, char const * sql) {
217 auto code = sqlite3_exec(db, sql, nullptr, nullptr, nullptr);
218 if (code != SQLITE_OK) fail(db, code);
219 }
220 struct statement {
221 sqlite3 * db;
222 sqlite3_stmt * value = nullptr;
223 statement(sqlite3 * db, char const * sql) : db(db) {
224 auto code = sqlite3_prepare_v2(db, sql, -1, &value, nullptr);
225 if (code != SQLITE_OK) {
226 if (value) sqlite3_finalize(value);
227 fail(db, code);
228 }
229 }
230 ~statement() { if (value) sqlite3_finalize(value); }
231 statement(statement const &) = delete;
232 statement & operator=(statement const &) = delete;
233 void text(int index, std::string_view data) {
234 auto code = sqlite3_bind_text64(value, index, data.data(), data.size(), SQLITE_TRANSIENT, SQLITE_UTF8);
235 if (code != SQLITE_OK) fail(db, code);
236 }
237 void blob(int index, std::span<std::byte const> data) {
238 // A null pointer denotes SQL NULL, including when the length is zero.
239 static constexpr std::byte empty{};
240 auto code = sqlite3_bind_blob64(value, index, data.empty() ? &empty : data.data(), data.size(), SQLITE_TRANSIENT);
241 if (code != SQLITE_OK) fail(db, code);
242 }
243 void key(int index, std::string_view data) {
244 blob(index, std::as_bytes(std::span(data.data(), data.size())));
245 }
246 void integer(int index, std::int64_t number) {
247 auto code = sqlite3_bind_int64(value, index, number);
248 if (code != SQLITE_OK) fail(db, code);
249 }
250 void null(int index) {
251 auto code = sqlite3_bind_null(value, index);
252 if (code != SQLITE_OK) fail(db, code);
253 }
254 bool row() {
255 auto code = sqlite3_step(value);
256 if (code == SQLITE_ROW) return true;
257 if (code == SQLITE_DONE) return false;
258 fail(db, code);
259 }
260 void done() { if (row()) throw std::logic_error("unexpected SQLite result row"); }
261 std::string text(int column) const {
262 auto data = sqlite3_column_text(value, column);
263 auto size = sqlite3_column_bytes(value, column);
264 if (!data && (size || sqlite3_errcode(db) == SQLITE_NOMEM)) fail(db, SQLITE_NOMEM);
265 return size ? std::string(reinterpret_cast<char const *>(data), std::size_t(size)) : std::string{};
266 }
267 std::string key(int column) const {
268 auto data = static_cast<char const *>(sqlite3_column_blob(value, column));
269 auto size = sqlite3_column_bytes(value, column);
270 if (!data && (size || sqlite3_errcode(db) == SQLITE_NOMEM)) fail(db, SQLITE_NOMEM);
271 return size ? std::string(data, std::size_t(size)) : std::string{};
272 }
273 bytes blob(int column) const {
274 auto data = static_cast<std::byte const *>(sqlite3_column_blob(value, column));
275 auto size = sqlite3_column_bytes(value, column);
276 if (!data && (size || sqlite3_errcode(db) == SQLITE_NOMEM)) fail(db, SQLITE_NOMEM);
277 return size ? bytes(data, data + size) : bytes{};
278 }
279 std::int64_t integer(int column) const { return sqlite3_column_int64(value, column); }
280 bool is_null(int column) const { return sqlite3_column_type(value, column) == SQLITE_NULL; }
281 };
282 inline constexpr char schema[] = R"sql(
283CREATE TABLE catalog_info(singleton INTEGER PRIMARY KEY CHECK(singleton=1), version INTEGER NOT NULL CHECK(version=1), identity TEXT NOT NULL CHECK(length(identity)=32), policy BLOB NOT NULL) STRICT;
284CREATE TABLE operations(id BLOB PRIMARY KEY, kind TEXT NOT NULL, request BLOB NOT NULL, outcome BLOB NOT NULL) STRICT;
285CREATE TABLE owners(kind TEXT NOT NULL CHECK(kind IN('attempt','save','reader')), id BLOB NOT NULL, PRIMARY KEY(kind,id)) STRICT;
286CREATE TABLE attempts(id TEXT PRIMARY KEY CHECK(length(id)=32), owner BLOB NOT NULL UNIQUE) STRICT;
287CREATE TABLE objects(id TEXT PRIMARY KEY CHECK(length(id)=32), kind INTEGER NOT NULL CHECK(kind IN(0,1)), attempt TEXT NOT NULL REFERENCES attempts(id), bytes INTEGER, crc INTEGER, barrier INTEGER,
288 CHECK((bytes IS NULL AND crc IS NULL AND barrier IS NULL) OR (bytes IS NOT NULL AND crc IS NOT NULL AND barrier IS NOT NULL AND bytes>=96 AND crc>=0 AND crc<=4294967295 AND barrier IN(0,1)))) STRICT;
289CREATE TABLE pairs(index_id TEXT PRIMARY KEY REFERENCES objects(id), native_id TEXT NOT NULL REFERENCES objects(id), target_native TEXT, target_index TEXT, native_count INTEGER NOT NULL CHECK(native_count>=0), borrowed_count INTEGER NOT NULL CHECK(borrowed_count>=0), virtual_count INTEGER NOT NULL CHECK(virtual_count>=0),
290 UNIQUE(native_id,index_id), FOREIGN KEY(target_native,target_index) REFERENCES pairs(native_id,index_id), CHECK((target_native IS NULL)=(target_index IS NULL)), CHECK(native_count<=virtual_count AND borrowed_count=virtual_count-native_count)) STRICT;
291CREATE TABLE owner_objects(owner_kind TEXT NOT NULL, owner_id BLOB NOT NULL, object_id TEXT NOT NULL REFERENCES objects(id), PRIMARY KEY(owner_kind,owner_id,object_id), FOREIGN KEY(owner_kind,owner_id) REFERENCES owners(kind,id)) STRICT;
292CREATE TABLE owner_roots(owner_kind TEXT NOT NULL, owner_id BLOB NOT NULL, native_id TEXT NOT NULL, index_id TEXT NOT NULL, PRIMARY KEY(owner_kind,owner_id,native_id,index_id), FOREIGN KEY(owner_kind,owner_id) REFERENCES owners(kind,id), FOREIGN KEY(native_id,index_id) REFERENCES pairs(native_id,index_id)) STRICT;
293CREATE TABLE saves(name BLOB PRIMARY KEY, native_id TEXT NOT NULL, index_id TEXT NOT NULL, FOREIGN KEY(native_id,index_id) REFERENCES pairs(native_id,index_id)) STRICT;
294CREATE TRIGGER sealed_immutable BEFORE UPDATE ON objects WHEN OLD.bytes IS NOT NULL OR NEW.id<>OLD.id OR NEW.kind<>OLD.kind OR NEW.attempt<>OLD.attempt BEGIN SELECT RAISE(ABORT,'immutable object'); END;
295)sql";
296 // Optional advisory extension. Core v4 readers can ignore it: its durable
297 // pins use their existing reader owners, and no logical root depends on it.
298 inline constexpr char native_merges_schema[] =
299 "CREATE TABLE completed_native_merges(domain BLOB NOT NULL,older TEXT NOT NULL REFERENCES objects(id),newer TEXT NOT NULL REFERENCES objects(id),output TEXT NOT NULL REFERENCES objects(id),owner_kind TEXT NOT NULL CHECK(owner_kind='reader'),owner_id BLOB NOT NULL UNIQUE,PRIMARY KEY(domain,older,newer),FOREIGN KEY(owner_kind,owner_id,output) REFERENCES owner_objects(owner_kind,owner_id,object_id)) STRICT";
300 inline constexpr char private_scopes_schema[] =
301 "CREATE TABLE private_scopes(id TEXT PRIMARY KEY CHECK(length(id)=32)) STRICT";
302 inline constexpr char private_attempts_schema[] =
303 "CREATE TABLE private_attempts(attempt TEXT PRIMARY KEY REFERENCES attempts(id),scope TEXT NOT NULL REFERENCES private_scopes(id)) STRICT";
304 inline constexpr char released_private_scopes_schema[] =
305 "CREATE TABLE released_private_scopes(scope TEXT PRIMARY KEY REFERENCES private_scopes(id)) STRICT";
306 // Release is an append-only event. Seals and operation receipts remain
307 // available after an abort, including an uncertain publication outcome.
308 inline constexpr char live_owner_objects_schema[] =
309 "CREATE VIEW live_owner_objects AS SELECT p.* FROM owner_objects p WHERE p.owner_kind<>'attempt' OR NOT EXISTS(SELECT 1 FROM attempts a JOIN private_attempts s ON s.attempt=a.id JOIN released_private_scopes r ON r.scope=s.scope WHERE a.owner=p.owner_id)";
310 inline constexpr char live_owner_roots_schema[] =
311 "CREATE VIEW live_owner_roots AS SELECT p.* FROM owner_roots p WHERE p.owner_kind<>'attempt' OR NOT EXISTS(SELECT 1 FROM attempts a JOIN private_attempts s ON s.attempt=a.id JOIN released_private_scopes r ON r.scope=s.scope WHERE a.owner=p.owner_id)";
312 // Version 1 is retained verbatim for old-catalog validation. No open path
313 // rewrites it. Creation explicitly chooses the timeline or COLA extension.
314 inline std::string schema_for(unsigned version) {
315 std::string result = schema;
316 if (version == 1) return result;
317 if (version < 2 || version > 4) throw std::invalid_argument("unsupported Everett catalog version");
318 auto replace = [&](std::string_view before, std::string_view after) {
319 result.replace(result.find(before), before.size(), after);
320 };
321 replace("CHECK(version=1)", "CHECK(version=" + std::to_string(version) + ")");
322 replace("IN('attempt','save','reader')", "IN('attempt','save','reader','timeline')");
323 result.insert(result.find("CREATE TRIGGER sealed_immutable"), R"sql(CREATE TABLE timelines(name BLOB PRIMARY KEY, source_name BLOB, source_generation INTEGER,
324 CHECK((source_name IS NULL)=(source_generation IS NULL)), FOREIGN KEY(source_name,source_generation) REFERENCES timeline_generations(name,generation)) STRICT;
325CREATE TABLE timeline_generations(name BLOB NOT NULL REFERENCES timelines(name), generation INTEGER NOT NULL CHECK(generation>=0), native_id TEXT NOT NULL, index_id TEXT NOT NULL, owner_kind TEXT NOT NULL CHECK(owner_kind='timeline'), owner_id BLOB NOT NULL UNIQUE,
326 PRIMARY KEY(name,generation), FOREIGN KEY(owner_kind,owner_id,native_id,index_id) REFERENCES owner_roots(owner_kind,owner_id,native_id,index_id)) STRICT;
327)sql");
328 if (version >= 3) {
329 auto first = result.find("CREATE TABLE pairs(");
330 auto last = result.find(';', first);
331 result.replace(first, last + 1 - first, R"sql(CREATE TABLE pairs(index_id TEXT PRIMARY KEY REFERENCES objects(id), native_id TEXT NOT NULL REFERENCES objects(id), target_native TEXT, target_index TEXT, native_count INTEGER NOT NULL CHECK(native_count>=0), borrowed_count INTEGER NOT NULL CHECK(borrowed_count>=0), virtual_count INTEGER NOT NULL CHECK(virtual_count>=0),
332 layout INTEGER NOT NULL DEFAULT 2 CHECK(layout IN(2,3)), secondary_native TEXT REFERENCES objects(id), secondary_native_count INTEGER NOT NULL DEFAULT 0 CHECK(secondary_native_count>=0), secondary_borrowed_count INTEGER NOT NULL DEFAULT 0 CHECK(secondary_borrowed_count>=0),
333 UNIQUE(native_id,index_id), FOREIGN KEY(target_native,target_index) REFERENCES pairs(native_id,index_id), CHECK((target_native IS NULL)=(target_index IS NULL)),
334 CHECK(native_count<=virtual_count AND secondary_borrowed_count<=virtual_count-native_count AND borrowed_count=virtual_count-native_count-secondary_borrowed_count),
335 CHECK(secondary_native IS NOT NULL OR (secondary_native_count=0 AND secondary_borrowed_count=0)), CHECK(layout=3 OR (secondary_native IS NULL AND secondary_native_count=0 AND secondary_borrowed_count=0))) STRICT;)sql");
336 }
337 if (version >= 4)
338 result.insert(result.find("CREATE TRIGGER sealed_immutable"), R"sql(CREATE TABLE session_checkpoints(name BLOB NOT NULL, generation INTEGER NOT NULL, checkpoint BLOB NOT NULL,
339PRIMARY KEY(name,generation), FOREIGN KEY(name,generation) REFERENCES timeline_generations(name,generation)) STRICT;
340CREATE TABLE session_saves(name BLOB PRIMARY KEY REFERENCES saves(name), session_name BLOB NOT NULL, generation INTEGER NOT NULL,
341 FOREIGN KEY(session_name,generation) REFERENCES session_checkpoints(name,generation)) STRICT;
342)sql");
343 return result;
344 }
345 }
346
347 // Insert-only durable metadata over an existing trusted local directory.
348 // Separate connections may contend; a single handle must not be used by
349 // concurrent callers. No method deletes an object, owner, operation or save.
350 // An uncertain storage/COMMIT outcome permanently disables this handle.
351 template <class P, class Ops = sqlite_catalog_ops> struct sqlite_catalog {
352 static_assert(std::is_nothrow_move_constructible_v<Ops>);
353 static_assert(noexcept(std::declval<Ops &>().commit(std::declval<sqlite3 *>())));
354 using policy_type = P;
359 : db_(std::exchange(other.db_, nullptr)), root_(std::move(other.root_)),
360 ops_(std::move(other.ops_)), poisoned_(other.poisoned_), schema_version_(other.schema_version_),
361 identity_(std::move(other.identity_)), private_scope_(std::move(other.private_scope_)) {}
363 ~sqlite_catalog() { if (db_) sqlite3_close_v2(db_); }
364
365 static sqlite_catalog create(std::filesystem::path const & root, object_id const & identity,
366 catalog_options options = {}, Ops ops = {}) {
367 return create_version(root, identity, options, std::move(ops), 2);
368 }
369 // Explicit schema opt-in. Existing catalogs are never migrated on open.
370 // Version 3 admits both linear IX02 and dual-target IX03 graphs.
371 static sqlite_catalog create_cola(std::filesystem::path const & root, object_id const & identity,
372 catalog_options options = {}, Ops ops = {}) {
373 return create_version(root, identity, options, std::move(ops), 3);
374 }
375 static sqlite_catalog create_sessions(std::filesystem::path const & root, object_id const & identity,
376 catalog_options options = {}, Ops ops = {}) {
377 return create_version(root, identity, options, std::move(ops), 4);
378 }
379 static sqlite_catalog open(std::filesystem::path const & root, catalog_options options = {}, Ops ops = {}) {
380 auto result = connect(std::filesystem::canonical(root), options, std::move(ops));
381 result.schema_version_ = result.detect_schema_version();
382 result.validate_schema();
383 catalog_detail::statement info(result.db_, "SELECT version,identity,policy FROM catalog_info WHERE singleton=1");
384 if (!info.row() || info.integer(0) != result.schema_version_ || !compatible_policy(info.blob(2)))
385 throw std::invalid_argument("Everett catalog schema or policy mismatch");
386 result.identity_.emplace(info.text(1));
387 if (info.row()) throw std::invalid_argument("multiple Everett catalog identities");
388 if (options.private_scope) result.set_private_scope(*options.private_scope);
389 return result;
390 }
391 bool poisoned() const noexcept { return poisoned_; }
392 bool private_construction() const noexcept { return private_scope_.has_value(); }
393 unsigned schema_version() const noexcept { return schema_version_; }
394 static char const * runtime_version() noexcept { return sqlite3_libversion(); }
395 static char const * source_id() noexcept { return sqlite3_sourceid(); }
396 std::filesystem::path const & root() const & noexcept { return root_; }
397 std::filesystem::path const & root() const && = delete;
398 object_id const & identity() const & {
400 if (!identity_) throw std::logic_error("uninitialized Everett catalog identity");
401 return *identity_;
402 }
403 object_id const & identity() const && = delete;
404
405 void begin_private_scope(std::string_view op, object_id const & id) {
408 transaction(op, "private-begin", request, [&] {
410 catalog_detail::statement insert(db_, "INSERT INTO private_scopes VALUES(?)");
411 insert.text(1, id.hex()); insert.done();
412 return catalog_detail::bytes{};
413 });
414 }
415 void set_private_scope(object_id const & id) {
417 read([&] { require_private_scope(id); return 0; });
418 private_scope_ = id;
419 }
420 // A process lease establishes that no writer can add more scope work.
421 // Publication must acquire its permanent owners before calling this.
422 void release_private_scope(std::string_view op, object_id const & id) {
425 transaction(op, "private-release", request, [&] {
427 catalog_detail::statement exists(db_, "SELECT 1 FROM private_scopes WHERE id=?");
428 exists.text(1, id.hex());
429 if (!exists.row()) throw std::invalid_argument("unknown private construction scope");
430 catalog_detail::statement insert(db_, "INSERT OR IGNORE INTO released_private_scopes VALUES(?)");
431 insert.text(1, id.hex()); insert.done();
432 return catalog_detail::bytes{};
433 });
434 }
438 return read([&] {
439 if (!has_table("private_scopes")) return private_scope_state::missing;
441 catalog_detail::statement query(db_, "SELECT EXISTS(SELECT 1 FROM released_private_scopes WHERE scope=id) FROM private_scopes WHERE id=?");
442 query.text(1, id.hex());
443 if (!query.row()) return private_scope_state::missing;
445 });
446 }
447 std::vector<object_id> private_scopes() const {
449 return read([&] {
450 std::vector<object_id> result;
451 if (!has_table("private_scopes")) return result;
453 catalog_detail::statement query(db_, "SELECT id FROM private_scopes WHERE NOT EXISTS(SELECT 1 FROM released_private_scopes WHERE scope=id) ORDER BY id");
454 while (query.row()) result.emplace_back(query.text(0));
455 return result;
456 });
457 }
458
459 // Recover acknowledged seal evidence for an existing immutable owner.
460 // The catalog supplies the attempt and barrier; the file supplies only
461 // checked envelope evidence, never a new durability claim.
463 require_active(); (void)file_extension(expected);
464 return read([&] {
465 catalog_detail::statement query(db_, "SELECT kind,attempt,bytes,crc,barrier FROM objects WHERE id=?");
466 query.text(1, id.hex());
467 if (!query.row() || query.integer(0) != kind(expected) || query.is_null(2))
468 throw std::invalid_argument("object has no completed seal in this catalog");
469 auto size = query.integer(2), crc = query.integer(3), barrier = query.integer(4);
470 if (size < 96 || crc < 0 || crc > 0xffffffffLL || barrier < 0 || barrier > 1)
471 throw std::invalid_argument("invalid completed seal metadata");
472 object_seal_receipt receipt{id, object_attempt_id(query.text(1)), root_ / object_path(id, expected),
473 std::uint64_t(size), std::uint32_t(crc), static_cast<object_sync_barrier>(barrier)};
474 verify_object_envelope(id, expected, receipt.bytes, receipt.body_crc32c);
475 return receipt;
476 });
477 }
478
479 // A completed receipt is an attestation of barriers, not a request to
480 // repeat them. Verify its exact immutable catalog row and object envelope;
481 // recovery payload checks remain explicit.
482 void verify_sealed(object_seal_receipt const & receipt, file_kind expected) const {
483 require_active(); (void)file_extension(expected);
484 auto barrier = static_cast<unsigned>(receipt.barrier);
485 if (barrier > 1) throw std::invalid_argument("unsupported seal barrier");
486 if (std::filesystem::canonical(receipt.path) != root_ / object_path(receipt.object, expected))
487 throw std::invalid_argument("seal receipt names another catalog path");
488 read([&] {
489 catalog_detail::statement query(db_, "SELECT kind,attempt,bytes,crc,barrier FROM objects WHERE id=?");
490 query.text(1, receipt.object.hex());
491 if (!query.row() || query.integer(0) != kind(expected) || query.text(1) != receipt.attempt.hex() ||
492 query.is_null(2) || query.integer(2) != catalog_detail::integer(receipt.bytes) ||
493 query.integer(3) != receipt.body_crc32c || query.integer(4) != barrier)
494 throw std::invalid_argument("seal receipt disagrees with catalog reservation");
495 verify_object_envelope(receipt.object, expected, receipt.bytes, receipt.body_crc32c);
496 });
497 }
498
499 std::optional<catalog_operation> lookup_operation(std::string_view op) const {
501 return read([&]() -> std::optional<catalog_operation> {
502 catalog_detail::statement query(db_, "SELECT kind,request,outcome FROM operations WHERE id=?");
503 query.key(1, op);
504 if (!query.row()) return std::nullopt;
505 return catalog_operation{query.text(0), query.blob(1), query.blob(2)};
506 });
507 }
508 std::optional<blob_identity> find_save(std::string_view name) const {
510 return read([&]() -> std::optional<blob_identity> {
511 catalog_detail::statement query(db_, "SELECT native_id,index_id FROM saves WHERE name=?");
512 query.key(1, name);
513 if (!query.row()) return std::nullopt;
514 return blob_identity{object_id(query.text(0)), object_id(query.text(1))};
515 });
516 }
517
518 void reserve(std::string_view op, object_attempt_id const & attempt, std::string_view owner,
519 std::span<blob_identity const> inputs, std::span<catalog_object_reservation const> outputs) {
521 if (outputs.empty()) throw std::invalid_argument("reservation has no outputs");
522 catalog_detail::bytes request;
523 catalog_detail::identity(request, object_id(attempt.hex())); catalog_detail::field(request, owner);
524 catalog_detail::number(request, inputs.size());
525 for (auto const & input : inputs) catalog_detail::pair(request, input);
526 catalog_detail::number(request, outputs.size());
527 for (auto const & output : outputs) {
528 catalog_detail::identity(request, output.object); (void)file_extension(output.kind);
529 catalog_detail::number(request, kind(output.kind));
530 }
532 transaction(op, "reserve", request, [&] {
534 add_owner("attempt", owner);
535 catalog_detail::statement job(db_, "INSERT INTO attempts VALUES(?,?)");
536 job.text(1, attempt.hex()); job.key(2, owner); job.done();
537 if (private_scope_) {
538 catalog_detail::statement scope(db_, "INSERT INTO private_attempts VALUES(?,?)");
539 scope.text(1, attempt.hex()); scope.text(2, private_scope_->hex()); scope.done();
540 }
541 for (auto const & input : inputs) add_root("attempt", owner, input);
542 for (auto const & output : outputs) {
543 catalog_detail::statement insert(db_, "INSERT INTO objects(id,kind,attempt) VALUES(?,?,?)");
544 insert.text(1, output.object.hex()); insert.integer(2, kind(output.kind)); insert.text(3, attempt.hex()); insert.done();
545 catalog_detail::statement pin(db_, "INSERT INTO owner_objects VALUES('attempt',?,?)");
546 pin.key(1, owner); pin.text(2, output.object.hex()); pin.done();
547 }
548 return catalog_detail::bytes{};
549 });
550 }
551
552 void record_sealed(std::string_view op, object_seal_receipt const & receipt) {
554 auto path = std::filesystem::canonical(receipt.path);
555 catalog_detail::bytes request; append_seal_request(request, receipt, path);
556 transaction(op, "seal", request, [&] {
557 record_sealed_row(receipt, path);
558 return catalog_detail::bytes{};
559 });
560 }
561
562 // Trusted semantic attestation: the caller must have performed the exact
563 // built-in KV03 replacement merge of these ordered inputs under this schema.
564 // Receipts prove durable bytes, not that semantic relationship. The runtime
565 // context supplies it only for its known replacement kernel and registry.
566 // First acknowledgment wins; valid competing completions keep their own
567 // sealed outputs. Replay also rechecks the supplied envelope.
568 void record_native_merge(std::string_view op, catalog_native_merge const & key,
569 object_seal_receipt const & receipt, std::string_view owner) {
571 auto domain = native_merge_domain(key);
572 auto path = std::filesystem::canonical(receipt.path);
573 catalog_detail::bytes request; append_native_merge(request, domain, key);
574 append_seal_request(request, receipt, path); catalog_detail::field(request, owner);
575 transaction(op, "remember_native_merge", request, [&] {
579 if (!has_table("completed_native_merges")) {
581 for (auto action : {"UPDATE", "DELETE"}) {
582 std::string sql = "CREATE TRIGGER immutable_completed_native_merges_" + std::string(action) +
583 " BEFORE " + action + " ON completed_native_merges BEGIN SELECT RAISE(ABORT,'immutable catalog row'); END";
584 catalog_detail::exec(db_, sql.c_str());
585 }
586 } else validate_native_merges();
587 if (auto old = native_merge_output(domain, key)) {
589 catalog_detail::bytes result; catalog_detail::identity(result, *old); return result;
590 }
591 add_native_pin(owner, receipt.object);
592 catalog_detail::statement row(db_, "INSERT INTO completed_native_merges VALUES(?,?,?,?,'reader',?)");
593 row.blob(1, domain); row.text(2, key.older.hex()); row.text(3, key.newer.hex());
594 row.text(4, receipt.object.hex()); row.key(5, owner); row.done();
595 catalog_detail::bytes result; catalog_detail::identity(result, receipt.object); return result;
596 });
599 }
600
601 // A missing hint is a read-only miss. A hit acquires its own durable pin in
602 // the same acknowledged transaction that selects the exact native result.
603 std::optional<object_seal_receipt> acquire_native_merge(std::string_view op,
604 catalog_native_merge const & key, std::string_view owner) {
606 auto domain = native_merge_domain(key);
607 catalog_detail::bytes request; append_native_merge(request, domain, key);
608 catalog_detail::field(request, owner);
609 if (!read([&] {
610 // Even a miss must reject reuse of an acknowledged operation ID for
611 // different arguments; the transaction performs that exact check.
612 if (lookup_operation(op)) return true;
613 if (!has_table("completed_native_merges")) return false;
615 return bool(native_merge_output(domain, key));
616 })) return {};
617 auto outcome = transaction(op, "acquire_native_merge", request, [&] {
618 auto output = native_merge_output(domain, key);
619 if (!output) throw std::invalid_argument("completed native merge disappeared");
621 add_native_pin(owner, *output);
622 catalog_detail::bytes result; catalog_detail::identity(result, *output); return result;
623 });
624 catalog_detail::outcome_reader reader{outcome};
625 object_id output(reader.field()); reader.end();
627 }
628
629 // The supplied head names catalog paths, which are reopened and pinned for
630 // this admission. Normal admission checks metadata; scan explicitly checks
631 // readable contents and exact target samples before taking the writer lock.
632 void register_chain(std::string_view op, mapped_query_root<P> const & source,
635 if (admission != catalog_admission::trusted && admission != catalog_admission::scan)
636 throw std::invalid_argument("unsupported catalog admission mode");
637 auto head = source.head();
638 if (!head) throw std::invalid_argument("empty catalog query root");
639 auto opened = open_mapped_query<P>(root_, head->identity());
640 if (admission == catalog_admission::scan) opened.head()->scan();
641 std::vector<typename mapped_blob<P>::pair_type> chain;
642 catalog_detail::bytes request;
643 catalog_detail::number(request, static_cast<unsigned>(admission));
644 for (auto current = opened.head(); current; current = current->target()) {
645 chain.push_back(current);
646 catalog_detail::pair(request, current->identity());
647 catalog_detail::number(request, current->native().size());
648 catalog_detail::number(request, current->borrowed().size());
649 catalog_detail::number(request, current->virtual_size());
650 }
651 transaction(op, "register_chain", request, [&] {
652 for (auto entry = chain.rbegin(); entry != chain.rend(); ++entry) {
653 auto const & pair = **entry;
654 require_sealed(pair.identity().native, file_kind::native_blob);
655 require_sealed(pair.identity().index, file_kind::fractional_index);
656 auto target = pair.target();
658 "SELECT native_id,target_native,target_index,native_count,borrowed_count,virtual_count,layout FROM pairs WHERE index_id=?" :
659 "SELECT native_id,target_native,target_index,native_count,borrowed_count,virtual_count,2 FROM pairs WHERE index_id=?");
660 old.text(1, pair.identity().index.hex());
661 if (old.row()) {
662 if (old.integer(6) != 2 || old.text(0) != pair.identity().native.hex() || old.is_null(1) != !target ||
663 (target && (old.text(1) != target->identity().native.hex() || old.text(2) != target->identity().index.hex())) ||
664 old.integer(3) != catalog_detail::integer(pair.native().size()) ||
665 old.integer(4) != catalog_detail::integer(pair.borrowed().size()) ||
666 old.integer(5) != catalog_detail::integer(pair.virtual_size()))
667 throw std::invalid_argument("registered index identity already has different contents");
668 continue;
669 }
670 catalog_detail::statement insert(db_, "INSERT INTO pairs(index_id,native_id,target_native,target_index,native_count,borrowed_count,virtual_count) VALUES(?,?,?,?,?,?,?)");
671 insert.text(1, pair.identity().index.hex()); insert.text(2, pair.identity().native.hex());
672 if (target) { insert.text(3, target->identity().native.hex()); insert.text(4, target->identity().index.hex()); }
673 else { insert.null(3); insert.null(4); }
674 insert.integer(5, catalog_detail::integer(pair.native().size()));
675 insert.integer(6, catalog_detail::integer(pair.borrowed().size()));
676 insert.integer(7, catalog_detail::integer(pair.virtual_size())); insert.done();
677 }
678 catalog_detail::bytes result; catalog_detail::pair(result, opened.head()->identity()); return result;
679 });
680 }
681
682 // COLA admission is an explicit schema-3 operation. The terminal secondary
683 // edge retains only its native object; only the main edge recurses. As for
684 // linear admission, trusted mode reads fixed metadata, not payload contents.
685 void register_chain(std::string_view op, mapped_cola_query_root<P> const & source,
687 register_graph(op, source.head(), admission);
688 }
689 // A hidden completed artifact can be a large pair without a prepared
690 // bounded query head. Register its exact graph without manufacturing one.
691 template <class Mapped> void register_graph(std::string_view op, std::shared_ptr<Mapped const> const & source,
693 std::array roots{source}; register_graphs<Mapped>(op, roots, admission);
694 }
695 // A checkpoint can have several hidden roots sharing a long main chain.
696 // Intern and register their union once, with children before their parents.
697 template <class Mapped> void register_graphs(std::string_view op,
698 std::span<std::shared_ptr<Mapped const> const> roots,
700 static_assert(std::is_same_v<P, typename Mapped::policy_type>);
702 if (schema_version_ < 3)
703 throw std::logic_error("COLA admission requires catalog version 3; no automatic migration");
704 if (admission != catalog_admission::trusted && admission != catalog_admission::scan)
705 throw std::invalid_argument("unsupported catalog admission mode");
706 if (roots.empty()) throw std::invalid_argument("empty COLA graph set");
707 std::vector<std::shared_ptr<Mapped const>> canonical(roots.begin(), roots.end());
708 for (auto const & root : canonical) if (!root) throw std::invalid_argument("empty COLA catalog graph");
709 std::sort(canonical.begin(), canonical.end(), [](auto const & a, auto const & b) {
710 return a->identity().index.hex() < b->identity().index.hex();
711 });
712 for (std::size_t i = 1; i != canonical.size(); ++i)
713 if (canonical[i - 1]->identity().index == canonical[i]->identity().index &&
714 canonical[i - 1]->identity().native != canonical[i]->identity().native)
715 throw std::invalid_argument("conflicting COLA graph root identities");
716 canonical.erase(std::unique(canonical.begin(), canonical.end(), [](auto const & a, auto const & b) {
717 return a->identity() == b->identity();
718 }), canonical.end());
721 std::vector<std::shared_ptr<Mapped const>> chain;
722 std::unordered_set<std::string> seen;
723 catalog_detail::bytes request;
724 catalog_detail::number(request, static_cast<unsigned>(admission));
725 catalog_detail::number(request, canonical.size());
726 for (auto const & root : canonical) {
727 catalog_detail::pair(request, root->identity());
728 auto opened = resolver.pair(root->identity());
729 if (admission == catalog_admission::scan) scan(*opened);
730 std::vector<std::shared_ptr<Mapped const>> pending;
731 for (auto current = opened; current && seen.insert(current->identity().index.hex()).second;
732 current = current->main_target()) pending.push_back(current);
733 chain.insert(chain.end(), pending.rbegin(), pending.rend());
734 }
735 for (auto const & current : chain) {
736 auto view = current->view();
737 auto main = current->main_target();
738 auto secondary = current->secondary_target();
739 catalog_detail::pair(request, current->identity());
740 catalog_detail::number(request, bool(main));
741 if (main) catalog_detail::pair(request, main->identity());
742 catalog_detail::number(request, bool(secondary));
743 if (secondary) catalog_detail::identity(request, *current->index_object()->secondary_id());
744 for (auto count : {view.native().size(), view.borrowed(0).size(), view.borrowed(1).size(),
745 secondary ? secondary->size() : 0, view.virtual_size()})
746 catalog_detail::number(request, count);
747 }
748 transaction(op, "register_cola_graphs", request, [&] {
749 for (auto const & entry : chain) {
750 auto const & pair = *entry;
751 auto view = pair.view();
752 auto main = pair.main_target();
753 auto secondary = pair.secondary_target();
754 auto const & secondary_id = pair.index_object()->secondary_id();
755 auto secondary_count = secondary ? secondary->size() : 0;
756 register_cola_row({pair.identity(), main ? std::optional{main->identity()} : std::nullopt, secondary_id,
757 view.native().size(), view.borrowed(0).size(), view.borrowed(1).size(), secondary_count, view.virtual_size()});
758 }
760 catalog_detail::number(result, canonical.size());
761 for (auto const & root : canonical) catalog_detail::pair(result, root->identity());
762 return result;
763 });
764 }
765
766 // Admit one new pair over a registered immutable main suffix. Reading the
767 // main row is sufficient: its files and descendants are never reopened.
768 // This is metadata admission; explicit recovery scans remain separate.
769 template <class Mapped> void register_pair(std::string_view op, blob_identity const & id) {
771 auto prepared = prepare_cola_pair<Mapped>(id);
772 catalog_detail::bytes request; append_pair_request(request, prepared.descriptor);
773 transaction(op, "register_cola_pair", request, [&] {
774 insert_cola_row(prepared.descriptor);
775 catalog_detail::bytes result; catalog_detail::pair(result, id); return result;
776 });
777 }
778
779 // File barriers have already completed. Acknowledge the index seal and
780 // its exact registered pair in one transaction, then return the mapping
781 // used for metadata validation. No binding escapes an uncertain commit.
782 template <class Mapped> auto seal_pair(std::string_view op, blob_identity const & id,
783 object_seal_receipt const & receipt) {
785 if (receipt.object != id.index) throw std::invalid_argument("pair receipt names another index");
786 auto path = std::filesystem::canonical(receipt.path);
787 catalog_detail::bytes request; append_seal_request(request, receipt, path);
788 if (path != root_ / object_path(receipt.object, file_kind::fractional_index))
789 throw std::invalid_argument("seal receipt names another catalog path");
790 auto prepared = prepare_cola_pair<Mapped>(id, &receipt);
791 append_pair_request(request, prepared.descriptor);
792 transaction(op, "seal_cola_pair", request, [&] {
793 record_sealed_row(receipt, path, &prepared.files.index);
794 insert_cola_row(prepared.descriptor);
795 catalog_detail::bytes result; catalog_detail::pair(result, id); return result;
796 });
797 return std::move(prepared.index);
798 }
799
800 // Both files have completed their barriers. Prepare and pin their metadata
801 // before taking the writer lock; the joint acknowledgment performs only
802 // SQL work. Neither mapping escapes an uncertain commit.
803 template <class Mapped> auto seal_native_pair(std::string_view op, blob_identity const & id,
804 object_seal_receipt const & native_receipt, object_seal_receipt const & index_receipt) {
806 if (native_receipt.object != id.native || index_receipt.object != id.index || id.native == id.index)
807 throw std::invalid_argument("pair receipts name different objects");
808 auto native_path = std::filesystem::canonical(native_receipt.path);
809 auto index_path = std::filesystem::canonical(index_receipt.path);
810 if (native_path != root_ / object_path(id.native, file_kind::native_blob) ||
811 index_path != root_ / object_path(id.index, file_kind::fractional_index))
812 throw std::invalid_argument("seal receipt names another catalog path");
813 catalog_detail::bytes request;
814 append_seal_request(request, native_receipt, native_path);
815 append_seal_request(request, index_receipt, index_path);
816 auto prepared = prepare_cola_pair<Mapped>(id, &index_receipt, &native_receipt);
817 append_pair_request(request, prepared.descriptor);
818 auto native = std::make_shared<typename Mapped::native_type const>(std::move(prepared.native));
819 transaction(op, "seal_native_cola_pair", request, [&] {
822 insert_cola_row(prepared.descriptor);
823 catalog_detail::bytes result; catalog_detail::pair(result, id); return result;
824 });
825 return std::pair{std::move(native), std::move(prepared.index)};
826 }
827
828 void save(std::string_view op, std::string_view name, blob_identity const & head) {
830 catalog_detail::bytes request; catalog_detail::field(request, name); catalog_detail::pair(request, head);
831 transaction(op, "save", request, [&] {
832 catalog_detail::statement prepared(db_, "SELECT virtual_count FROM pairs WHERE native_id=? AND index_id=?");
833 prepared.text(1, head.native.hex()); prepared.text(2, head.index.hex());
834 if (!prepared.row() || std::uint64_t(prepared.integer(0)) > P::group_size)
835 throw std::invalid_argument("save requires a registered prepared head");
836 add_owner("save", name); add_root("save", name, head);
837 catalog_detail::statement insert(db_, "INSERT INTO saves VALUES(?,?,?)");
838 insert.key(1, name); insert.text(2, head.native.hex()); insert.text(3, head.index.hex()); insert.done();
839 catalog_detail::bytes result; catalog_detail::pair(result, head); return result;
840 });
841 }
842 catalog_saved_root acquire_save(std::string_view op, std::string_view name, std::string_view reader_owner) {
844 catalog_detail::bytes request; catalog_detail::field(request, name); catalog_detail::field(request, reader_owner);
845 transaction(op, "acquire_save", request, [&] {
846 auto head = find_save(name);
847 if (!head) throw std::invalid_argument("unknown Everett save");
848 add_owner("reader", reader_owner); add_root("reader", reader_owner, *head);
849 catalog_detail::bytes result; catalog_detail::pair(result, *head); return result;
850 });
851 // Saves are immutable. Exact replay therefore resolves the same head.
852 auto head = find_save(name);
853 if (!head) throw std::logic_error("committed save disappeared");
854 return {*head, std::string(reader_owner)};
855 }
856
857 std::optional<catalog_timeline_head> find_timeline(std::string_view name) const {
859 return read([&] { return timeline_at(name); });
860 }
861
862 catalog_timeline_head create_timeline(std::string_view op, std::string_view name,
863 blob_identity const & head) {
865 catalog_detail::bytes request; catalog_detail::field(request, name); catalog_detail::pair(request, head);
866 auto outcome = transaction(op, "create_timeline", request, [&] {
867 require_prepared(head);
868 catalog_detail::statement insert(db_, "INSERT INTO timelines(name) VALUES(?)");
869 insert.key(1, name); insert.done();
870 auto result = add_generation(name, 0, head);
871 catalog_detail::bytes bytes; catalog_detail::timeline(bytes, result); return bytes;
872 });
873 return decode_timeline(outcome);
874 }
875
876 // Fork precisely the supplied historical generation. Advancing its source
877 // later cannot change the selected root or the result of operation replay.
878 catalog_timeline_head fork_timeline(std::string_view op, std::string_view name,
879 catalog_timeline_head const & source) {
881 validate_timeline(source);
882 catalog_detail::bytes request; catalog_detail::field(request, name); catalog_detail::timeline(request, source);
883 auto outcome = transaction(op, "fork_timeline", request, [&] {
884 auto actual = timeline_at(source.name, source.generation);
885 if (!actual || *actual != source) throw std::invalid_argument("fork source is not this exact timeline generation");
886 require_prepared(source.head);
887 catalog_detail::statement insert(db_, "INSERT INTO timelines VALUES(?,?,?)");
888 insert.key(1, name); insert.key(2, source.name); insert.integer(3, catalog_detail::integer(source.generation)); insert.done();
889 auto result = add_generation(name, 0, source.head);
890 catalog_detail::bytes bytes; catalog_detail::timeline(bytes, result); return bytes;
891 });
892 return decode_timeline(outcome);
893 }
894
895 // Compare the complete expected generation under the SQLite writer lock.
896 // A stale comparison is itself a committed, replay-stable outcome. Every
897 // successful publication appends a generation, even if the root is unchanged.
899 catalog_timeline_head const & expected, blob_identity const & candidate) {
901 catalog_detail::bytes request; catalog_detail::timeline(request, expected); catalog_detail::pair(request, candidate);
902 auto outcome = transaction(op, "publish_timeline", request, [&] {
903 auto current = timeline_at(expected.name);
904 if (!current) throw std::invalid_argument("unknown Everett timeline");
905 if (schema_version_ >= 4 && checkpoint_at(current->name, current->generation))
906 throw std::invalid_argument("publish a named session together with its checkpoint");
907 bool published = *current == expected;
908 if (published) {
909 require_prepared(candidate);
910 if (expected.generation == std::uint64_t(std::numeric_limits<std::int64_t>::max()))
911 throw std::length_error("Everett timeline generation exhausted");
912 current = add_generation(expected.name, expected.generation + 1, candidate);
913 }
914 catalog_detail::bytes bytes; catalog_detail::number(bytes, published); catalog_detail::timeline(bytes, *current); return bytes;
915 });
916 return read([&] {
917 catalog_detail::outcome_reader reader{outcome};
918 auto published = reader.number();
919 if (published > 1) reader.invalid();
920 auto head = reader.timeline(); reader.end();
921 return catalog_timeline_publication{published != 0, std::move(head)};
922 });
923 }
924
925 std::optional<catalog_session_head> find_session(std::string_view name) const {
927 return read([&]() -> std::optional<catalog_session_head> {
928 auto head = timeline_at(name);
929 if (!head) return std::nullopt;
930 return attach_checkpoint(std::move(*head));
931 });
932 }
933
934 catalog_session_head create_session(std::string_view op, std::string_view name,
935 blob_identity const & head, std::span<std::byte const> checkpoint, catalog_auxiliary_roots auxiliary = {}) {
936 auxiliary = catalog_detail::canonical_auxiliary(std::move(auxiliary), head);
938 catalog_detail::bytes request; catalog_detail::field(request, name);
939 catalog_detail::pair(request, head); catalog_detail::binary(request, checkpoint); catalog_detail::auxiliary(request, auxiliary);
940 auto outcome = transaction(op, "create_session", request, [&] {
941 require_prepared(head);
942 catalog_detail::statement insert(db_, "INSERT INTO timelines(name) VALUES(?)");
943 insert.key(1, name); insert.done();
944 auto result = add_session_generation(name, 0, head, checkpoint, auxiliary);
945 catalog_detail::bytes bytes; catalog_detail::session(bytes, result); return bytes;
946 });
947 return decode_session(outcome);
948 }
949
951 blob_identity const & candidate, std::span<std::byte const> checkpoint, catalog_auxiliary_roots auxiliary = {}) {
952 auxiliary = catalog_detail::canonical_auxiliary(std::move(auxiliary), candidate);
954 catalog_detail::bytes request; catalog_detail::session(request, expected);
955 catalog_detail::pair(request, candidate); catalog_detail::binary(request, checkpoint); catalog_detail::auxiliary(request, auxiliary);
956 auto outcome = transaction(op, "publish_session", request, [&] {
957 auto timeline = timeline_at(expected.timeline.name);
958 if (!timeline) throw std::invalid_argument("unknown Everett session");
959 auto current = attach_checkpoint(std::move(*timeline));
960 bool published = current == expected;
961 if (published) {
962 require_prepared(candidate);
963 if (current.timeline.generation == std::uint64_t(std::numeric_limits<std::int64_t>::max()))
964 throw std::length_error("Everett session generation exhausted");
965 current = add_session_generation(current.timeline.name, current.timeline.generation + 1,
966 candidate, checkpoint, auxiliary);
967 }
969 catalog_detail::session(bytes, current); return bytes;
970 });
971 return read([&] {
972 catalog_detail::outcome_reader reader{outcome};
973 auto published = reader.number();
974 if (published > 1) reader.invalid();
975 auto head = reader.session(); reader.end();
976 return catalog_session_publication{published != 0, std::move(head)};
977 });
978 }
979
980 catalog_session_head fork_session(std::string_view op, std::string_view name,
981 catalog_session_head const & source) {
984 catalog_detail::bytes request; catalog_detail::field(request, name); catalog_detail::session(request, source);
985 auto outcome = transaction(op, "fork_session", request, [&] {
986 auto actual = timeline_at(source.timeline.name, source.timeline.generation);
987 if (!actual || attach_checkpoint(std::move(*actual)) != source)
988 throw std::invalid_argument("fork source is not this exact session generation");
990 catalog_detail::statement insert(db_, "INSERT INTO timelines VALUES(?,?,?)");
991 insert.key(1, name); insert.key(2, source.timeline.name);
992 insert.integer(3, catalog_detail::integer(source.timeline.generation)); insert.done();
993 auto result = add_session_generation(name, 0, source.timeline.head, source.checkpoint, source.auxiliary);
994 catalog_detail::bytes bytes; catalog_detail::session(bytes, result); return bytes;
995 });
996 return decode_session(outcome);
997 }
998
999 void save_session(std::string_view op, std::string_view name, catalog_session_head const & source) {
1002 catalog_detail::bytes request; catalog_detail::field(request, name); catalog_detail::session(request, source);
1003 transaction(op, "save_session", request, [&] {
1004 auto actual = timeline_at(source.timeline.name, source.timeline.generation);
1005 if (!actual || attach_checkpoint(std::move(*actual)) != source)
1006 throw std::invalid_argument("save source is not this exact session generation");
1007 auto const & head = source.timeline.head;
1008 add_owner("save", name); add_root("save", name, head); add_auxiliary("save", name, source.auxiliary);
1009 catalog_detail::statement saved(db_, "INSERT INTO saves VALUES(?,?,?)");
1010 saved.key(1, name); saved.text(2, head.native.hex()); saved.text(3, head.index.hex()); saved.done();
1011 catalog_detail::statement metadata(db_, "INSERT INTO session_saves VALUES(?,?,?)");
1012 metadata.key(1, name); metadata.key(2, source.timeline.name);
1013 metadata.integer(3, catalog_detail::integer(source.timeline.generation)); metadata.done();
1014 return catalog_detail::bytes{};
1015 });
1016 }
1017
1018 std::optional<catalog_session_head> find_saved_session(std::string_view name) const {
1020 return read([&]() -> std::optional<catalog_session_head> {
1021 catalog_detail::statement saved(db_, "SELECT session_name,generation FROM session_saves WHERE name=?");
1022 saved.key(1, name);
1023 if (!saved.row()) return std::nullopt;
1024 auto head = timeline_at(saved.key(0), std::uint64_t(saved.integer(1)));
1025 if (!head) throw catalog_error("saved session generation disappeared", SQLITE_CORRUPT);
1026 return attach_checkpoint(std::move(*head));
1027 });
1028 }
1029
1030 private:
1033 std::optional<blob_identity> main;
1034 std::optional<object_id> secondary;
1036 };
1038 template <class Mapped> struct prepared_cola_pair {
1039 std::shared_ptr<typename Mapped::index_type const> index;
1042 typename Mapped::native_type native;
1043 };
1045 auto mapping = mapped_file::open(root_ / object_path(id, expected));
1046 return mapping.slice(0, mapping.size());
1047 }
1049 object_seal_receipt const * receipt = nullptr, object_seal_receipt const * native_receipt = nullptr) const {
1050 return read([&]() -> prepared_cola_pair<Mapped> {
1051 static_assert(std::is_same_v<P, typename Mapped::policy_type>);
1052 if (schema_version_ < 3) throw std::logic_error("COLA admission requires catalog version 3");
1053 using native_type = typename Mapped::native_type;
1054 using index_type = typename Mapped::index_type;
1055 using view_type = decltype(std::declval<Mapped const &>().view());
1056 cola_pair_files files;
1058 auto index = std::make_shared<index_type const>(index_type::open(file<P>::from_slice(files.index)));
1059 if (index->native_id() != id.native) throw std::invalid_argument("COLA pair native identity mismatch");
1060 if (receipt) check_object_envelope(file_kind::fractional_index, receipt->bytes, receipt->body_crc32c, files.index.bytes());
1061 else require_sealed(id.index, file_kind::fractional_index, &files.index);
1063 auto native = native_type::open(file<P>::from_slice(files.native));
1064 if (native_receipt)
1065 check_object_envelope(file_kind::native_blob, native_receipt->bytes, native_receipt->body_crc32c, files.native.bytes());
1067 std::optional<native_type> secondary;
1068 if (index->secondary_id()) {
1069 files.secondary = open_object_bytes(*index->secondary_id(), file_kind::native_blob);
1070 secondary.emplace(native_type::open(file<P>::from_slice(files.secondary)));
1071 require_sealed(*index->secondary_id(), file_kind::native_blob, &files.secondary);
1072 }
1073 auto main_samples = read([&]() -> std::uint64_t {
1074 if (!index->main_id()) return 0;
1075 auto const & main = *index->main_id();
1076 catalog_detail::statement row(db_, "SELECT native_id,virtual_count,layout FROM pairs WHERE index_id=?");
1077 row.text(1, main.index.hex());
1078 if (!row.row() || row.text(0) != main.native.hex() || row.integer(2) != 3 || row.integer(1) < 0)
1079 throw std::invalid_argument("COLA main target is not this registered pair");
1080 auto count = std::uint64_t(row.integer(1));
1081 return count / P::group_size + (count % P::group_size != 0);
1082 });
1083 auto secondary_count = secondary ? secondary->size() : 0;
1084 if (index->borrowed(0).size() != main_samples || index->borrowed(1).size() !=
1085 secondary_count / P::group_size + (secondary_count % P::group_size != 0))
1086 throw std::invalid_argument("COLA pair sample count disagrees with registered target");
1087 view_type view{native.view(), {index->borrowed(0), index->borrowed(1)},
1088 {index->interleave(0), index->interleave(1)}, {index->false_borrow_bits(0), index->false_borrow_bits(1)},
1089 {index->cut_lcps(0), index->cut_lcps(1)}, index->virtual_size()};
1090 cola_pair_descriptor descriptor{id, index->main_id(), index->secondary_id(), view.native().size(),
1091 view.borrowed(0).size(), view.borrowed(1).size(), secondary_count, view.virtual_size()};
1092 return {std::move(index), std::move(files), std::move(descriptor), std::move(native)};
1093 });
1094 }
1095 static void append_pair_request(catalog_detail::bytes & request, cola_pair_descriptor const & descriptor) {
1096 catalog_detail::pair(request, descriptor.id); catalog_detail::number(request, bool(descriptor.main));
1097 if (descriptor.main) catalog_detail::pair(request, *descriptor.main);
1098 catalog_detail::number(request, bool(descriptor.secondary));
1099 if (descriptor.secondary) catalog_detail::identity(request, *descriptor.secondary);
1100 for (auto count : {descriptor.native_count, descriptor.main_samples, descriptor.secondary_samples,
1101 descriptor.secondary_count, descriptor.virtual_count}) catalog_detail::number(request, count);
1102 }
1104 std::filesystem::path const & path) const {
1105 catalog_detail::identity(request, receipt.object);
1106 catalog_detail::identity(request, object_id(receipt.attempt.hex()));
1107 catalog_detail::number(request, receipt.bytes); catalog_detail::number(request, receipt.body_crc32c);
1108 auto barrier = static_cast<unsigned>(receipt.barrier);
1109 if (barrier > 1) throw std::invalid_argument("unsupported seal barrier");
1110 catalog_detail::number(request, barrier);
1111 // Relative replay descriptors survive relocation of the backing store.
1112 catalog_detail::field(request, path.lexically_relative(root_).generic_string());
1113 }
1114 void record_sealed_row(object_seal_receipt const & receipt, std::filesystem::path const & path,
1115 mapped_slice const * evidence = nullptr) {
1116 catalog_detail::statement reserved(db_, "SELECT kind,attempt,bytes FROM objects WHERE id=?");
1117 reserved.text(1, receipt.object.hex());
1118 if (!reserved.row() || reserved.text(1) != receipt.attempt.hex() || !reserved.is_null(2))
1119 throw std::invalid_argument("object is not this attempt's unsealed reservation");
1120 auto expected = reserved.integer(0) == 0 ? file_kind::native_blob : file_kind::fractional_index;
1121 if (path != root_ / object_path(receipt.object, expected))
1122 throw std::invalid_argument("seal receipt names another catalog path");
1123 // A barrier receipt is an attestation; re-reading cannot prove durability.
1124 auto opened = evidence ? mapped_slice{} : open_object_bytes(receipt.object, expected);
1125 check_object_envelope(expected, receipt.bytes, receipt.body_crc32c, (evidence ? *evidence : opened).bytes());
1126 write_seal(receipt);
1127 }
1128 // Used only after the exact path and envelope were checked against a pinned
1129 // immutable mapping outside this transaction.
1130 void record_prepared_seal(object_seal_receipt const & receipt, file_kind expected) {
1131 catalog_detail::statement reserved(db_, "SELECT kind,attempt,bytes FROM objects WHERE id=?");
1132 reserved.text(1, receipt.object.hex());
1133 if (!reserved.row() || reserved.integer(0) != kind(expected) ||
1134 reserved.text(1) != receipt.attempt.hex() || !reserved.is_null(2))
1135 throw std::invalid_argument("object is not this attempt's unsealed reservation");
1136 write_seal(receipt);
1137 }
1138 void write_seal(object_seal_receipt const & receipt) {
1139 catalog_detail::statement update(db_, "UPDATE objects SET bytes=?,crc=?,barrier=? WHERE id=?");
1140 update.integer(1, catalog_detail::integer(receipt.bytes)); update.integer(2, receipt.body_crc32c);
1141 update.integer(3, static_cast<unsigned>(receipt.barrier)); update.text(4, receipt.object.hex()); update.done();
1142 }
1144 require_sealed(value.id.native, file_kind::native_blob);
1146 if (value.secondary) require_sealed(*value.secondary, file_kind::native_blob);
1147 insert_cola_row(value);
1148 }
1149 // Call only with sealed rows checked against pinned immutable envelopes.
1150 // New index seal rows may have been written earlier in this transaction.
1152 catalog_detail::statement old(db_, "SELECT native_id,target_native,target_index,native_count,borrowed_count,virtual_count,layout,secondary_native,secondary_native_count,secondary_borrowed_count FROM pairs WHERE index_id=?");
1153 old.text(1, value.id.index.hex());
1154 if (old.row()) {
1155 if (old.integer(6) != 3 || old.text(0) != value.id.native.hex() ||
1156 old.is_null(1) != !value.main || old.is_null(2) != !value.main ||
1157 (value.main && (old.text(1) != value.main->native.hex() || old.text(2) != value.main->index.hex())) ||
1158 old.is_null(7) != !value.secondary || (value.secondary && old.text(7) != value.secondary->hex()) ||
1159 old.integer(3) != catalog_detail::integer(value.native_count) ||
1160 old.integer(4) != catalog_detail::integer(value.main_samples) ||
1161 old.integer(5) != catalog_detail::integer(value.virtual_count) ||
1162 old.integer(8) != catalog_detail::integer(value.secondary_count) ||
1163 old.integer(9) != catalog_detail::integer(value.secondary_samples))
1164 throw std::invalid_argument("registered COLA index identity already has different contents");
1165 return;
1166 }
1167 catalog_detail::statement insert(db_, "INSERT INTO pairs(index_id,native_id,target_native,target_index,native_count,borrowed_count,virtual_count,layout,secondary_native,secondary_native_count,secondary_borrowed_count) VALUES(?,?,?,?,?,?,?,3,?,?,?)");
1168 insert.text(1, value.id.index.hex()); insert.text(2, value.id.native.hex());
1169 if (value.main) { insert.text(3, value.main->native.hex()); insert.text(4, value.main->index.hex()); }
1170 else { insert.null(3); insert.null(4); }
1171 insert.integer(5, catalog_detail::integer(value.native_count));
1172 insert.integer(6, catalog_detail::integer(value.main_samples));
1173 insert.integer(7, catalog_detail::integer(value.virtual_count));
1174 if (value.secondary) insert.text(8, value.secondary->hex()); else insert.null(8);
1175 insert.integer(9, catalog_detail::integer(value.secondary_count));
1176 insert.integer(10, catalog_detail::integer(value.secondary_samples)); insert.done();
1177 }
1178 sqlite3 * db_ = nullptr;
1179 std::filesystem::path root_;
1180 Ops ops_;
1181 mutable bool poisoned_ = false;
1182 mutable bool native_merges_validated_ = false;
1183 unsigned schema_version_ = 2;
1184 std::optional<object_id> identity_;
1185 std::optional<object_id> private_scope_;
1186 inline static constexpr char const * immutable_tables[] = {
1187 "catalog_info", "operations", "owners", "attempts", "pairs", "owner_objects", "owner_roots", "saves",
1188 "timelines", "timeline_generations", "session_checkpoints", "session_saves", "completed_native_merges",
1189 "private_scopes", "private_attempts", "released_private_scopes"
1190 };
1191 sqlite_catalog(sqlite3 * db, std::filesystem::path root, Ops ops)
1192 : db_(db), root_(std::move(root)), ops_(std::move(ops)) {}
1193 static sqlite_catalog create_version(std::filesystem::path const & root, object_id const & identity,
1194 catalog_options options, Ops ops, unsigned version) {
1195 validate_options(options);
1197 auto location = std::filesystem::canonical(root);
1198#if defined(__APPLE__) || defined(__linux__)
1199 // Reserve this catalog name without following a pre-existing symlink.
1200 auto path = location / "catalog.sqlite3";
1201 int fd = ::open(path.c_str(), O_CREAT | O_EXCL | O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0600);
1202 if (fd < 0) throw std::system_error(errno, std::generic_category(), "create Everett catalog");
1203 if (::close(fd)) throw std::system_error(errno, std::generic_category(), "close new Everett catalog");
1204 auto result = connect(location, options, std::move(ops), true);
1205 result.schema_version_ = version;
1206 result.transaction("", "initialize", {}, [&] {
1207 catalog_detail::exec(result.db_, catalog_detail::schema_for(version).c_str());
1208 catalog_detail::statement insert(result.db_, "INSERT INTO catalog_info VALUES(1,?,?,?)");
1209 insert.integer(1, version); insert.text(2, identity.hex()); insert.blob(3, policy()); insert.done();
1210 // Immutable tables remain readable through ordinary SQL tooling.
1211 for (auto table : immutable_tables) {
1212 if (!result.has_table(table)) continue;
1213 for (auto action : {"UPDATE", "DELETE"}) {
1214 std::string sql = "CREATE TRIGGER immutable_" + std::string(table) + "_" + action +
1215 " BEFORE " + action + " ON " + table + " BEGIN SELECT RAISE(ABORT,'immutable catalog row'); END";
1216 catalog_detail::exec(result.db_, sql.c_str());
1217 }
1218 }
1219 catalog_detail::exec(result.db_, "CREATE TRIGGER objects_no_delete BEFORE DELETE ON objects BEGIN SELECT RAISE(ABORT,'retained object'); END");
1220 return catalog_detail::bytes{};
1221 }, false);
1222 result.identity_ = identity;
1223 // SQLite's transaction is not a substitute for retaining this new name.
1224 posix_object_ops barriers;
1225 int directory = barriers.open_root(location);
1226 if (directory < 0) throw std::system_error(errno, std::generic_category(), "open catalog directory");
1227 if (barriers.sync_directory(directory)) {
1228 int error = errno; barriers.close(directory);
1229 throw std::system_error(error, std::generic_category(), "sync catalog directory; initialization outcome unknown");
1230 }
1231 if (barriers.close(directory))
1232 throw std::system_error(errno, std::generic_category(), "close catalog directory; initialization outcome unknown");
1233 return result;
1234#else
1235 (void)location; (void)options; (void)ops; (void)version;
1236 throw std::system_error(std::make_error_code(std::errc::operation_not_supported), "Everett catalog creation requires POSIX directory barriers");
1237#endif
1238 }
1239 void require_active() const {
1240 if (!db_ || poisoned_) throw std::logic_error("Everett catalog handle is inactive or poisoned");
1241 }
1242 void require_timelines() const {
1244 if (schema_version_ < 2) throw std::logic_error("Everett timelines require catalog version 2 or later; no automatic migration");
1245 }
1246 void require_sessions() const {
1248 if (schema_version_ < 4)
1249 throw std::logic_error("Everett named sessions require catalog version 4; no automatic migration");
1250 }
1251 bool has_table(std::string_view table) const {
1252 if (table == "completed_native_merges" || table == "private_scopes" ||
1253 table == "private_attempts" || table == "released_private_scopes") {
1254 // Unlike core version-selected tables, this advisory extension can
1255 // appear later through another connection. Do not cache an absence.
1256 catalog_detail::statement query(db_, "SELECT 1 FROM sqlite_schema WHERE name=?");
1257 query.text(1, table); return query.row();
1258 }
1259 if (table == "session_checkpoints" || table == "session_saves") return schema_version_ >= 4;
1260 if (table == "timelines" || table == "timeline_generations") return schema_version_ >= 2;
1261 return true;
1262 }
1263 static void validate_timeline(catalog_timeline_head const & value) {
1264 catalog_detail::name(value.name); catalog_detail::name(value.owner);
1265 (void)catalog_detail::integer(value.generation);
1266 }
1268 return read([&] {
1269 catalog_detail::outcome_reader reader{bytes};
1270 auto head = reader.timeline(); reader.end(); return head;
1271 });
1272 }
1274 return read([&] {
1275 catalog_detail::outcome_reader reader{bytes};
1276 auto head = reader.session(); reader.end(); return head;
1277 });
1278 }
1279 std::optional<catalog_detail::bytes> checkpoint_at(std::string_view name, std::uint64_t generation) const {
1280 catalog_detail::statement query(db_, "SELECT checkpoint FROM session_checkpoints WHERE name=? AND generation=?");
1281 query.key(1, name); query.integer(2, catalog_detail::integer(generation));
1282 if (!query.row()) return std::nullopt;
1283 return query.blob(0);
1284 }
1286 auto checkpoint = checkpoint_at(head.name, head.generation);
1287 if (!checkpoint) throw std::invalid_argument("timeline is not a named Everett session");
1288 auto retained = auxiliary_at(head);
1289 return {std::move(head), std::move(*checkpoint), std::move(retained)};
1290 }
1293 catalog_detail::statement pairs(db_, "SELECT native_id,index_id FROM owner_roots WHERE owner_kind='timeline' AND owner_id=? ORDER BY native_id,index_id");
1294 pairs.key(1, head.owner);
1295 while (pairs.row()) {
1296 blob_identity pair{object_id(pairs.text(0)), object_id(pairs.text(1))};
1297 if (pair != head.head) value.pairs.push_back(std::move(pair));
1298 }
1299 catalog_detail::statement natives(db_, "SELECT object_id FROM owner_objects WHERE owner_kind='timeline' AND owner_id=? ORDER BY object_id");
1300 natives.key(1, head.owner);
1301 while (natives.row()) value.natives.emplace_back(natives.text(0));
1302 return value;
1303 }
1304 void add_auxiliary(std::string_view kind, std::string_view owner, catalog_auxiliary_roots const & value) {
1305 for (auto const & pair : value.pairs) add_root(kind, owner, pair);
1306 for (auto const & native : value.natives) {
1308 catalog_detail::statement pin(db_, "INSERT INTO owner_objects VALUES(?,?,?)");
1309 pin.text(1, kind); pin.key(2, owner); pin.text(3, native.hex()); pin.done();
1310 }
1311 }
1312 std::optional<catalog_timeline_head> timeline_at(std::string_view name,
1313 std::optional<std::uint64_t> generation = {}) const {
1314 catalog_detail::statement query(db_, generation ?
1315 "SELECT generation,native_id,index_id,owner_id FROM timeline_generations WHERE name=? AND generation=?" :
1316 "SELECT generation,native_id,index_id,owner_id FROM timeline_generations WHERE name=? ORDER BY generation DESC LIMIT 1");
1317 query.key(1, name);
1318 if (generation) query.integer(2, catalog_detail::integer(*generation));
1319 if (!query.row()) return std::nullopt;
1320 return catalog_timeline_head{std::string(name), std::uint64_t(query.integer(0)),
1321 {object_id(query.text(1)), object_id(query.text(2))}, query.key(3)};
1322 }
1323 void require_prepared(blob_identity const & head) const {
1324 catalog_detail::statement query(db_, "SELECT virtual_count FROM pairs WHERE native_id=? AND index_id=?");
1325 query.text(1, head.native.hex()); query.text(2, head.index.hex());
1326 if (!query.row() || std::uint64_t(query.integer(0)) > P::group_size)
1327 throw std::invalid_argument("timeline requires a registered prepared head");
1328 }
1329 catalog_timeline_head add_generation(std::string_view name, std::uint64_t generation, blob_identity const & head) {
1330 catalog_detail::bytes encoded; catalog_detail::field(encoded, name); catalog_detail::number(encoded, generation);
1331 std::string owner(reinterpret_cast<char const *>(encoded.data()), encoded.size());
1332 add_owner("timeline", owner); add_root("timeline", owner, head);
1333 catalog_detail::statement insert(db_, "INSERT INTO timeline_generations VALUES(?,?,?,?, 'timeline',?)");
1334 insert.key(1, name); insert.integer(2, catalog_detail::integer(generation));
1335 insert.text(3, head.native.hex()); insert.text(4, head.index.hex()); insert.key(5, owner); insert.done();
1336 return {std::string(name), generation, head, std::move(owner)};
1337 }
1338 catalog_session_head add_session_generation(std::string_view name, std::uint64_t generation,
1339 blob_identity const & head, std::span<std::byte const> checkpoint, catalog_auxiliary_roots const & auxiliary) {
1340 auto timeline = add_generation(name, generation, head);
1341 add_auxiliary("timeline", timeline.owner, auxiliary);
1342 catalog_detail::statement insert(db_, "INSERT INTO session_checkpoints VALUES(?,?,?)");
1343 insert.key(1, name); insert.integer(2, catalog_detail::integer(generation));
1344 insert.blob(3, checkpoint); insert.done();
1345 return {std::move(timeline), {checkpoint.begin(), checkpoint.end()}, auxiliary};
1346 }
1347 template<class F> auto read(F && action) const {
1348 try { return action(); }
1349 catch (catalog_error const & error) {
1350 if (catalog_detail::storage_error(error.code)) poisoned_ = true;
1351 throw;
1352 }
1353 }
1354 static std::int64_t kind(file_kind value) { return value == file_kind::native_blob ? 0 : 1; }
1356 catalog_detail::bytes result;
1357 for (auto value : {std::uint64_t(P::unit), P::group_size, P::codec_block_size,
1358 std::uint64_t(P::backspace_code), P::backspace_parameter,
1359 std::uint64_t(P::fixed_width), P::value_width.value_or(0)})
1360 catalog_detail::number(result, value);
1361 return result;
1362 }
1363 // The descriptor keeps the creation-time value-layout annotation, but a
1364 // registry extension can widen its defaults without changing file framing.
1365 // Every native object retains and validates its own actual common width.
1366 static bool compatible_policy(std::span<std::byte const> stored) {
1367 if (stored.size() != 7 * sizeof(std::uint64_t)) return false;
1368 auto fixed = file_detail::get(stored, 40, 8);
1369 auto width = file_detail::get(stored, 48, 8);
1370 if (fixed > 1 || (!fixed && width)) return false;
1371 auto expected = policy();
1372 return std::equal(stored.begin(), stored.begin() + 40, expected.begin());
1373 }
1374 static void validate_options(catalog_options options) {
1375 if (sqlite3_libversion_number() < 3051003 || !sqlite3_threadsafe())
1376 throw std::runtime_error("Everett requires thread-safe SQLite 3.51.3 or later");
1377 if (options.busy_timeout_ms < 0) throw std::invalid_argument("negative SQLite busy timeout");
1378 }
1379 static sqlite_catalog connect(std::filesystem::path const & root, catalog_options options, Ops ops,
1380 bool initialize = false) {
1381 validate_options(options);
1382 sqlite3 * db = nullptr;
1383 auto path = (root / "catalog.sqlite3").string();
1384 auto code = sqlite3_open_v2(path.c_str(), &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_EXRESCODE | SQLITE_OPEN_NOFOLLOW, nullptr);
1385 sqlite_catalog result(db, root, std::move(ops));
1386 if (code != SQLITE_OK) catalog_detail::fail(db, code);
1387 if (!sqlite3_db_mutex(db))
1388 throw std::runtime_error("Everett requires a serialized SQLite connection");
1389 sqlite3_extended_result_codes(db, 1);
1390 code = sqlite3_busy_timeout(db, options.busy_timeout_ms);
1391 if (code != SQLITE_OK) catalog_detail::fail(db, code);
1392 int setting = 0;
1393 code = sqlite3_db_config(db, SQLITE_DBCONFIG_DEFENSIVE, 1, &setting);
1394 if (code != SQLITE_OK || setting != 1) catalog_detail::fail(db, code == SQLITE_OK ? SQLITE_ERROR : code);
1395 catalog_detail::exec(db, "PRAGMA trusted_schema=OFF; PRAGMA foreign_keys=ON; PRAGMA synchronous=FULL; PRAGMA fullfsync=ON; PRAGMA checkpoint_fullfsync=ON;");
1396 // Opening must not format an incomplete file or change another database's
1397 // persistent journal mode before its Everett schema has been established.
1398 // Only the exclusive create path is allowed to enable WAL.
1399 catalog_detail::statement journal(db, initialize ? "PRAGMA journal_mode=WAL" : "PRAGMA journal_mode");
1400 if (!journal.row() || journal.text(0) != "wal") throw std::runtime_error("Everett requires SQLite WAL mode");
1401 for (auto sql : {"PRAGMA foreign_keys", "PRAGMA synchronous", "PRAGMA fullfsync", "PRAGMA checkpoint_fullfsync", "PRAGMA trusted_schema"}) {
1402 catalog_detail::statement check(db, sql);
1403 auto expected = std::string_view(sql) == "PRAGMA synchronous" ? 2 :
1404 std::string_view(sql) == "PRAGMA trusted_schema" ? 0 : 1;
1405 if (!check.row() || check.integer(0) != expected)
1406 throw std::runtime_error("Everett SQLite persistence setting rejected");
1407 }
1408 return result;
1409 }
1410 void definition(std::string_view type, std::string_view name, std::string_view expected) const {
1411 catalog_detail::statement query(db_, "SELECT sql FROM sqlite_schema WHERE type=? AND name=?");
1412 query.text(1, type); query.text(2, name);
1413 if (!query.row() || query.text(0) != expected)
1414 throw std::invalid_argument("incompatible Everett catalog schema definition");
1415 }
1416 unsigned detect_schema_version() const {
1417 // Do not query an unrecognized replacement table/view to discover its
1418 // version: establish the canonical bounded singleton shape first.
1419 catalog_detail::statement query(db_, "SELECT sql FROM sqlite_schema WHERE type='table' AND name='catalog_info'");
1420 if (query.row()) {
1421 auto actual = query.text(0);
1422 for (unsigned version : {1, 2, 3, 4}) {
1423 auto expected = catalog_detail::schema_for(version);
1424 auto first = expected.find("CREATE TABLE catalog_info");
1425 auto last = expected.find(';', first);
1426 if (actual == expected.substr(first, last - first)) return version;
1427 }
1428 }
1429 throw std::invalid_argument("incompatible Everett catalog version table");
1430 }
1431 void validate_schema() const {
1432 // Validate only a bounded schema description, never external payloads or
1433 // all catalog rows. SQLite preserves these canonical CREATE definitions.
1435 std::string_view source = schema;
1436 while (true) {
1437 auto begin = source.find("CREATE TABLE ");
1438 if (begin == std::string_view::npos) break;
1439 source.remove_prefix(begin);
1440 auto end = source.find(';');
1441 auto name_end = source.find('(');
1442 definition("table", source.substr(13, name_end - 13), source.substr(0, end));
1443 source.remove_prefix(end + 1);
1444 }
1445 auto first = source.find("CREATE TRIGGER sealed_immutable");
1446 auto last = source.rfind("END;");
1447 definition("trigger", "sealed_immutable", source.substr(first, last + 3 - first));
1448 for (auto table : immutable_tables) {
1449 if (!has_table(table)) continue;
1450 for (auto action : {"UPDATE", "DELETE"}) {
1451 std::string name = "immutable_" + std::string(table) + "_" + action;
1452 std::string sql = "CREATE TRIGGER " + name + " BEFORE " + action + " ON " + table +
1453 " BEGIN SELECT RAISE(ABORT,'immutable catalog row'); END";
1454 definition("trigger", name, sql);
1455 }
1456 }
1457 if (has_table("completed_native_merges")) validate_native_merges();
1458 if (has_table("private_scopes") || has_table("private_attempts") || has_table("released_private_scopes"))
1460 definition("trigger", "objects_no_delete", "CREATE TRIGGER objects_no_delete BEFORE DELETE ON objects BEGIN SELECT RAISE(ABORT,'retained object'); END");
1461 std::string trigger_query = "SELECT count(*) FROM sqlite_schema WHERE type='trigger' AND tbl_name IN('objects'";
1462 std::int64_t trigger_count = 2;
1463 for (auto table : immutable_tables) if (has_table(table)) {
1464 trigger_query += ",'" + std::string(table) + "'"; trigger_count += 2;
1465 }
1466 trigger_query += ")";
1467 catalog_detail::statement triggers(db_, trigger_query.c_str());
1468 if (!triggers.row() || triggers.integer(0) != trigger_count)
1469 throw std::invalid_argument("unexpected trigger on Everett catalog tables");
1470 }
1472 catalog_detail::name(key.schema);
1473 catalog_detail::bytes result;
1474 switch (key.kind) {
1476 catalog_detail::field(result, "everett.KV03.right-biased-native-merge/1"); break;
1478 catalog_detail::field(result, "everett.KV03.conservative-tombstone-merge/1"); break;
1479 default: throw std::invalid_argument("unknown native merge kernel");
1480 }
1481 catalog_detail::field(result, key.schema);
1482 auto physical = policy(); result.insert(result.end(), physical.begin(), physical.end());
1483 return result;
1484 }
1485 static void append_native_merge(catalog_detail::bytes & out, std::span<std::byte const> domain,
1486 catalog_native_merge const & key) {
1487 catalog_detail::number(out, domain.size()); out.insert(out.end(), domain.begin(), domain.end());
1488 catalog_detail::identity(out, key.older); catalog_detail::identity(out, key.newer);
1489 }
1490 std::optional<object_id> native_merge_output(std::span<std::byte const> domain,
1491 catalog_native_merge const & key) const {
1492 catalog_detail::statement query(db_, "SELECT output FROM completed_native_merges WHERE domain=? AND older=? AND newer=?");
1493 query.blob(1, domain); query.text(2, key.older.hex()); query.text(3, key.newer.hex());
1494 if (!query.row()) return {};
1495 return object_id(query.text(0));
1496 }
1498 if (native_merges_validated_) return;
1499 definition("table", "completed_native_merges", catalog_detail::native_merges_schema);
1500 for (auto action : {"UPDATE", "DELETE"}) {
1501 auto name = "immutable_completed_native_merges_" + std::string(action);
1502 definition("trigger", name, "CREATE TRIGGER " + name + " BEFORE " + action +
1503 " ON completed_native_merges BEGIN SELECT RAISE(ABORT,'immutable catalog row'); END");
1504 }
1505 catalog_detail::statement count(db_, "SELECT count(*) FROM sqlite_schema WHERE type='trigger' AND tbl_name='completed_native_merges'");
1506 if (!count.row() || count.integer(0) != 2) throw std::invalid_argument("unexpected native merge hint trigger");
1508 }
1510 if (has_table("private_scopes")) { validate_private_scopes(); return; }
1514 for (auto table : {"private_scopes", "private_attempts", "released_private_scopes"})
1515 for (auto action : {"UPDATE", "DELETE"}) {
1516 auto name = "immutable_" + std::string(table) + "_" + action;
1517 auto sql = "CREATE TRIGGER " + name + " BEFORE " + action + " ON " + table +
1518 " BEGIN SELECT RAISE(ABORT,'immutable catalog row'); END";
1519 catalog_detail::exec(db_, sql.c_str());
1520 }
1521 }
1523 definition("table", "private_scopes", catalog_detail::private_scopes_schema);
1524 definition("table", "private_attempts", catalog_detail::private_attempts_schema);
1525 definition("table", "released_private_scopes", catalog_detail::released_private_scopes_schema);
1526 definition("view", "live_owner_objects", catalog_detail::live_owner_objects_schema);
1527 definition("view", "live_owner_roots", catalog_detail::live_owner_roots_schema);
1528 for (auto table : {"private_scopes", "private_attempts", "released_private_scopes"})
1529 for (auto action : {"UPDATE", "DELETE"}) {
1530 auto name = "immutable_" + std::string(table) + "_" + action;
1531 definition("trigger", name, "CREATE TRIGGER " + name + " BEFORE " + action + " ON " + table +
1532 " BEGIN SELECT RAISE(ABORT,'immutable catalog row'); END");
1533 }
1534 }
1535 void require_private_scope(object_id const & id) const {
1537 catalog_detail::statement query(db_, "SELECT 1 FROM private_scopes WHERE id=? AND NOT EXISTS(SELECT 1 FROM released_private_scopes WHERE scope=?)");
1538 query.text(1, id.hex()); query.text(2, id.hex());
1539 if (!query.row()) throw std::invalid_argument("inactive private construction scope");
1540 }
1541 void add_native_pin(std::string_view owner, object_id const & native) {
1542 add_owner("reader", owner);
1543 catalog_detail::statement pin(db_, "INSERT INTO owner_objects VALUES('reader',?,?)");
1544 pin.key(1, owner); pin.text(2, native.hex()); pin.done();
1545 }
1546 void add_owner(std::string_view kind, std::string_view name) {
1547 catalog_detail::statement insert(db_, "INSERT INTO owners VALUES(?,?)");
1548 insert.text(1, kind); insert.key(2, name); insert.done();
1549 }
1550 void add_root(std::string_view kind, std::string_view owner, blob_identity const & pair) {
1551 catalog_detail::statement insert(db_, "INSERT INTO owner_roots VALUES(?,?,?,?)");
1552 insert.text(1, kind); insert.key(2, owner); insert.text(3, pair.native.hex()); insert.text(4, pair.index.hex()); insert.done();
1553 }
1554 void require_sealed(object_id const & id, file_kind expected, mapped_slice const * evidence = nullptr) const {
1555 catalog_detail::statement query(db_, "SELECT kind,bytes,crc FROM objects WHERE id=?");
1556 query.text(1, id.hex());
1557 if (!query.row() || query.integer(0) != kind(expected) || query.is_null(1))
1558 throw std::invalid_argument("chain refers to an unsealed or wrongly typed object");
1559 if (evidence) check_object_envelope(expected, std::uint64_t(query.integer(1)), std::uint64_t(query.integer(2)), evidence->bytes());
1560 else verify_object_envelope(id, expected, std::uint64_t(query.integer(1)), std::uint64_t(query.integer(2)));
1561 }
1562 void verify_object_envelope(object_id const & id, file_kind expected, std::uint64_t size, std::uint64_t crc) const {
1563 auto bytes = open_object_bytes(id, expected);
1564 check_object_envelope(expected, size, crc, bytes.bytes());
1565 }
1566 static void check_object_envelope(file_kind expected, std::uint64_t size, std::uint64_t crc,
1567 std::span<std::byte const> bytes) {
1568 auto header = decode_file_header<P>(bytes);
1569 if (header.kind != expected || size != bytes.size() ||
1570 file_detail::total_bytes<P>(header.extent) != bytes.size() || crc != file_detail::get(bytes, 64, 4))
1571 throw std::invalid_argument("sealed catalog metadata disagrees with object");
1572 }
1573 template <class F> catalog_detail::bytes transaction(std::string_view op, std::string_view kind,
1574 catalog_detail::bytes const & request, F && apply, bool record = true) {
1576 if (record) catalog_detail::name(op);
1577 bool began = false, committing = false;
1578 try {
1579 catalog_detail::exec(db_, "BEGIN IMMEDIATE"); began = true;
1580 if (record) {
1581 if (auto old = lookup_operation(op)) {
1582 if (old->kind != kind || old->request != request)
1583 throw std::invalid_argument("Everett operation identity reused with a different request");
1584 catalog_detail::exec(db_, "ROLLBACK");
1585 return std::move(old->outcome);
1586 }
1587 }
1588 auto result = apply();
1589 if (record) {
1590 catalog_detail::statement insert(db_, "INSERT INTO operations VALUES(?,?,?,?)");
1591 insert.key(1, op); insert.text(2, kind); insert.blob(3, request); insert.blob(4, result); insert.done();
1592 }
1593 committing = true;
1594 auto code = ops_.commit(db_);
1595 if (code != SQLITE_OK) catalog_detail::fail(db_, code);
1596 if (!sqlite3_get_autocommit(db_)) catalog_detail::fail(db_, SQLITE_PROTOCOL);
1597 return result;
1598 } catch (catalog_error const & error) {
1599 bool uncertain = committing || catalog_detail::storage_error(error.code);
1600 if (began && !sqlite3_get_autocommit(db_) && sqlite3_exec(db_, "ROLLBACK", nullptr, nullptr, nullptr) != SQLITE_OK)
1601 uncertain = true;
1603 throw catalog_error(error.what(), error.code, std::string(op), uncertain);
1604 } catch (...) {
1605 if (committing) poisoned_ = true;
1606 if (began && !sqlite3_get_autocommit(db_) && sqlite3_exec(db_, "ROLLBACK", nullptr, nullptr, nullptr) != SQLITE_OK)
1607 poisoned_ = true;
1608 throw;
1609 }
1610 }
1611 };
1612}
Binds immutable mapped blobs to exact dependency chains and validates their samples.
Binds mapped COLA main and secondary targets with exact immutable pins.
void session(bytes &out, catalog_session_head const &value)
Definition sqlite_catalog.h:147
std::string schema_for(unsigned version)
Definition sqlite_catalog.h:314
void number(bytes &out, std::uint64_t value)
Definition sqlite_catalog.h:109
void fail(sqlite3 *db, int code)
Definition sqlite_catalog.h:213
void name(std::string_view value)
Definition sqlite_catalog.h:198
constexpr char schema[]
Definition sqlite_catalog.h:282
constexpr char private_attempts_schema[]
Definition sqlite_catalog.h:302
void pair(bytes &out, blob_identity const &value)
Definition sqlite_catalog.h:121
catalog_auxiliary_roots canonical_auxiliary(catalog_auxiliary_roots value, blob_identity const &primary)
Definition sqlite_catalog.h:136
constexpr char live_owner_roots_schema[]
Definition sqlite_catalog.h:310
void timeline(bytes &out, catalog_timeline_head const &value)
Definition sqlite_catalog.h:124
constexpr char live_owner_objects_schema[]
Definition sqlite_catalog.h:308
void identity(bytes &out, object_id const &value)
Definition sqlite_catalog.h:117
constexpr char native_merges_schema[]
Definition sqlite_catalog.h:298
std::int64_t integer(std::uint64_t value)
Definition sqlite_catalog.h:201
void binary(bytes &out, std::span< std::byte const > value)
Definition sqlite_catalog.h:127
bool storage_error(int code) noexcept
Definition sqlite_catalog.h:206
void auxiliary(bytes &out, catalog_auxiliary_roots const &value)
Definition sqlite_catalog.h:130
void exec(sqlite3 *db, char const *sql)
Definition sqlite_catalog.h:216
constexpr char private_scopes_schema[]
Definition sqlite_catalog.h:300
std::vector< std::byte > bytes
Definition sqlite_catalog.h:108
constexpr char released_private_scopes_schema[]
Definition sqlite_catalog.h:304
void field(bytes &out, std::string_view value)
Definition sqlite_catalog.h:112
std::uint64_t get(std::span< std::byte const > bytes, std::size_t at, unsigned width) noexcept
Definition file.h:65
Definition active_engine.h:18
catalog_admission
Definition sqlite_catalog.h:38
object_sync_barrier
Definition object_writer.h:47
catalog_native_merge_kind
Definition sqlite_catalog.h:47
std::string_view file_extension(file_kind kind)
Definition object_path.h:27
std::filesystem::path object_path(object_id const &id, file_kind kind)
Definition object_path.h:52
file_kind
Definition object_path.h:25
Streams and seals immutable object files without catalog publication.
Definition sections.h:34
object_id index
Definition sections.h:36
object_id native
Definition sections.h:35
Definition sqlite_catalog.h:71
std::vector< blob_identity > pairs
Definition sqlite_catalog.h:72
bool operator==(catalog_auxiliary_roots const &) const =default
std::vector< object_id > natives
Definition sqlite_catalog.h:73
Definition sqlite_catalog.h:152
std::uint64_t number()
Definition sqlite_catalog.h:155
catalog_session_head session()
Definition sqlite_catalog.h:187
catalog_timeline_head timeline()
Definition sqlite_catalog.h:167
void end() const
Definition sqlite_catalog.h:196
std::span< std::byte const > data
Definition sqlite_catalog.h:153
static void invalid()
Definition sqlite_catalog.h:154
std::string field()
Definition sqlite_catalog.h:161
catalog_auxiliary_roots auxiliary()
Definition sqlite_catalog.h:174
Definition sqlite_catalog.h:220
std::string text(int column) const
Definition sqlite_catalog.h:261
sqlite3_stmt * value
Definition sqlite_catalog.h:222
void key(int index, std::string_view data)
Definition sqlite_catalog.h:243
void text(int index, std::string_view data)
Definition sqlite_catalog.h:233
statement(sqlite3 *db, char const *sql)
Definition sqlite_catalog.h:223
sqlite3 * db
Definition sqlite_catalog.h:221
void integer(int index, std::int64_t number)
Definition sqlite_catalog.h:246
bool is_null(int column) const
Definition sqlite_catalog.h:280
statement(statement const &)=delete
bytes blob(int column) const
Definition sqlite_catalog.h:273
std::string key(int column) const
Definition sqlite_catalog.h:267
bool row()
Definition sqlite_catalog.h:254
statement & operator=(statement const &)=delete
std::int64_t integer(int column) const
Definition sqlite_catalog.h:279
void null(int index)
Definition sqlite_catalog.h:250
void blob(int index, std::span< std::byte const > data)
Definition sqlite_catalog.h:237
void done()
Definition sqlite_catalog.h:260
~statement()
Definition sqlite_catalog.h:230
Definition sqlite_catalog.h:92
std::string operation
Definition sqlite_catalog.h:97
int code
Definition sqlite_catalog.h:96
bool outcome_unknown
Definition sqlite_catalog.h:98
catalog_error(std::string message, int code, std::string operation={}, bool uncertain=false)
Definition sqlite_catalog.h:93
Definition sqlite_catalog.h:50
catalog_native_merge_kind kind
Definition sqlite_catalog.h:53
object_id older
Definition sqlite_catalog.h:52
std::string schema
Definition sqlite_catalog.h:51
object_id newer
Definition sqlite_catalog.h:52
Definition sqlite_catalog.h:45
object_id object
Definition sqlite_catalog.h:45
file_kind kind
Definition sqlite_catalog.h:45
Definition sqlite_catalog.h:87
std::string kind
Definition sqlite_catalog.h:88
std::vector< std::byte > request
Definition sqlite_catalog.h:89
std::vector< std::byte > outcome
Definition sqlite_catalog.h:90
Definition sqlite_catalog.h:39
std::optional< object_id > private_scope
Definition sqlite_catalog.h:43
int busy_timeout_ms
Definition sqlite_catalog.h:40
Definition sqlite_catalog.h:46
std::string owner
Definition sqlite_catalog.h:46
blob_identity head
Definition sqlite_catalog.h:46
Definition sqlite_catalog.h:76
catalog_auxiliary_roots auxiliary
Definition sqlite_catalog.h:79
catalog_timeline_head timeline
Definition sqlite_catalog.h:77
bool operator==(catalog_session_head const &) const =default
std::vector< std::byte > checkpoint
Definition sqlite_catalog.h:78
Definition sqlite_catalog.h:82
bool published
Definition sqlite_catalog.h:83
catalog_session_head head
Definition sqlite_catalog.h:84
bool operator==(catalog_session_publication const &) const =default
Definition sqlite_catalog.h:57
std::string owner
Definition sqlite_catalog.h:61
blob_identity head
Definition sqlite_catalog.h:60
bool operator==(catalog_timeline_head const &) const =default
std::uint64_t generation
Definition sqlite_catalog.h:59
std::string name
Definition sqlite_catalog.h:58
Definition sqlite_catalog.h:64
bool published
Definition sqlite_catalog.h:65
bool operator==(catalog_timeline_publication const &) const =default
catalog_timeline_head head
Definition sqlite_catalog.h:66
Definition cola_query.h:40
pair_type head() const noexcept
Definition cola_query.h:63
Definition file.h:242
Definition mapped_cola.h:214
pair_type pair(blob_identity const &head)
Definition mapped_cola.h:227
Definition mapped_cola.h:91
static mapped_file open(std::filesystem::path const &path)
Definition mapped_file.h:131
Definition mapped_file.h:81
std::span< std::byte const > bytes() const &noexcept
Definition mapped_file.h:99
Definition object_writer.h:39
std::string const & hex() const noexcept
Definition object_writer.h:41
Definition object_path.h:38
std::string const & hex() const noexcept
Definition object_path.h:46
Definition object_writer.h:88
object_attempt_id attempt
Definition object_writer.h:90
std::filesystem::path path
Definition object_writer.h:91
object_id object
Definition object_writer.h:89
std::uint32_t body_crc32c
Definition object_writer.h:93
std::uint64_t bytes
Definition object_writer.h:92
object_sync_barrier barrier
Definition object_writer.h:94
Definition object_writer.h:100
Definition query.h:51
pair_type head() const noexcept
Definition query.h:66
Definition session.h:71
Definition sqlite_catalog.h:1031
std::uint64_t main_samples
Definition sqlite_catalog.h:1035
std::optional< object_id > secondary
Definition sqlite_catalog.h:1034
std::uint64_t secondary_count
Definition sqlite_catalog.h:1035
std::optional< blob_identity > main
Definition sqlite_catalog.h:1033
blob_identity id
Definition sqlite_catalog.h:1032
std::uint64_t native_count
Definition sqlite_catalog.h:1035
std::uint64_t secondary_samples
Definition sqlite_catalog.h:1035
std::uint64_t virtual_count
Definition sqlite_catalog.h:1035
Definition sqlite_catalog.h:1037
mapped_slice secondary
Definition sqlite_catalog.h:1037
mapped_slice index
Definition sqlite_catalog.h:1037
mapped_slice native
Definition sqlite_catalog.h:1037
Definition sqlite_catalog.h:1038
std::shared_ptr< typename Mapped::index_type const > index
Definition sqlite_catalog.h:1039
cola_pair_files files
Definition sqlite_catalog.h:1040
Mapped::native_type native
Definition sqlite_catalog.h:1042
cola_pair_descriptor descriptor
Definition sqlite_catalog.h:1041
Definition sqlite_catalog.h:103
int commit(sqlite3 *db) noexcept
Definition sqlite_catalog.h:104
Definition sqlite_catalog.h:351
catalog_auxiliary_roots auxiliary_at(catalog_timeline_head const &head) const
Definition sqlite_catalog.h:1291
sqlite_catalog & operator=(sqlite_catalog const &)=delete
~sqlite_catalog()
Definition sqlite_catalog.h:363
bool native_merges_validated_
Definition sqlite_catalog.h:1182
std::optional< catalog_detail::bytes > checkpoint_at(std::string_view name, std::uint64_t generation) const
Definition sqlite_catalog.h:1279
sqlite_catalog(sqlite3 *db, std::filesystem::path root, Ops ops)
Definition sqlite_catalog.h:1191
bool has_table(std::string_view table) const
Definition sqlite_catalog.h:1251
std::optional< catalog_operation > lookup_operation(std::string_view op) const
Definition sqlite_catalog.h:499
private_scope_state
Definition sqlite_catalog.h:435
auto seal_pair(std::string_view op, blob_identity const &id, object_seal_receipt const &receipt)
Definition sqlite_catalog.h:782
static void append_native_merge(catalog_detail::bytes &out, std::span< std::byte const > domain, catalog_native_merge const &key)
Definition sqlite_catalog.h:1485
unsigned schema_version_
Definition sqlite_catalog.h:1183
prepared_cola_pair< Mapped > prepare_cola_pair(blob_identity const &id, object_seal_receipt const *receipt=nullptr, object_seal_receipt const *native_receipt=nullptr) const
Definition sqlite_catalog.h:1048
static bool compatible_policy(std::span< std::byte const > stored)
Definition sqlite_catalog.h:1366
static void validate_options(catalog_options options)
Definition sqlite_catalog.h:1374
void verify_sealed(object_seal_receipt const &receipt, file_kind expected) const
Definition sqlite_catalog.h:482
catalog_timeline_head create_timeline(std::string_view op, std::string_view name, blob_identity const &head)
Definition sqlite_catalog.h:862
auto read(F &&action) const
Definition sqlite_catalog.h:1347
void verify_object_envelope(object_id const &id, file_kind expected, std::uint64_t size, std::uint64_t crc) const
Definition sqlite_catalog.h:1562
catalog_detail::bytes transaction(std::string_view op, std::string_view kind, catalog_detail::bytes const &request, F &&apply, bool record=true)
Definition sqlite_catalog.h:1573
catalog_timeline_head add_generation(std::string_view name, std::uint64_t generation, blob_identity const &head)
Definition sqlite_catalog.h:1329
std::optional< object_id > identity_
Definition sqlite_catalog.h:1184
std::vector< object_id > private_scopes() const
Definition sqlite_catalog.h:447
static sqlite_catalog create_version(std::filesystem::path const &root, object_id const &identity, catalog_options options, Ops ops, unsigned version)
Definition sqlite_catalog.h:1193
static sqlite_catalog create_cola(std::filesystem::path const &root, object_id const &identity, catalog_options options={}, Ops ops={})
Definition sqlite_catalog.h:371
void record_native_merge(std::string_view op, catalog_native_merge const &key, object_seal_receipt const &receipt, std::string_view owner)
Definition sqlite_catalog.h:568
void add_native_pin(std::string_view owner, object_id const &native)
Definition sqlite_catalog.h:1541
void require_timelines() const
Definition sqlite_catalog.h:1242
static char const * runtime_version() noexcept
Definition sqlite_catalog.h:394
sqlite_catalog & operator=(sqlite_catalog &&)=delete
void ensure_private_scopes()
Definition sqlite_catalog.h:1509
void save(std::string_view op, std::string_view name, blob_identity const &head)
Definition sqlite_catalog.h:828
Ops ops_
Definition sqlite_catalog.h:1180
std::optional< catalog_session_head > find_session(std::string_view name) const
Definition sqlite_catalog.h:925
catalog_session_head decode_session(catalog_detail::bytes const &bytes) const
Definition sqlite_catalog.h:1273
void write_seal(object_seal_receipt const &receipt)
Definition sqlite_catalog.h:1138
void append_seal_request(catalog_detail::bytes &request, object_seal_receipt const &receipt, std::filesystem::path const &path) const
Definition sqlite_catalog.h:1103
static sqlite_catalog create_sessions(std::filesystem::path const &root, object_id const &identity, catalog_options options={}, Ops ops={})
Definition sqlite_catalog.h:375
catalog_timeline_head fork_timeline(std::string_view op, std::string_view name, catalog_timeline_head const &source)
Definition sqlite_catalog.h:878
static constexpr char const * immutable_tables[]
Definition sqlite_catalog.h:1186
void validate_native_merges() const
Definition sqlite_catalog.h:1497
catalog_timeline_publication publish_timeline(std::string_view op, catalog_timeline_head const &expected, blob_identity const &candidate)
Definition sqlite_catalog.h:898
bool private_construction() const noexcept
Definition sqlite_catalog.h:392
static sqlite_catalog open(std::filesystem::path const &root, catalog_options options={}, Ops ops={})
Definition sqlite_catalog.h:379
void require_private_scope(object_id const &id) const
Definition sqlite_catalog.h:1535
static sqlite_catalog create(std::filesystem::path const &root, object_id const &identity, catalog_options options={}, Ops ops={})
Definition sqlite_catalog.h:365
std::filesystem::path root_
Definition sqlite_catalog.h:1179
catalog_session_head add_session_generation(std::string_view name, std::uint64_t generation, blob_identity const &head, std::span< std::byte const > checkpoint, catalog_auxiliary_roots const &auxiliary)
Definition sqlite_catalog.h:1338
bool poisoned_
Definition sqlite_catalog.h:1181
object_id const & identity() const &&=delete
static char const * source_id() noexcept
Definition sqlite_catalog.h:395
std::filesystem::path const & root() const &noexcept
Definition sqlite_catalog.h:396
std::optional< object_seal_receipt > acquire_native_merge(std::string_view op, catalog_native_merge const &key, std::string_view owner)
Definition sqlite_catalog.h:603
void record_sealed_row(object_seal_receipt const &receipt, std::filesystem::path const &path, mapped_slice const *evidence=nullptr)
Definition sqlite_catalog.h:1114
void validate_private_scopes() const
Definition sqlite_catalog.h:1522
static void append_pair_request(catalog_detail::bytes &request, cola_pair_descriptor const &descriptor)
Definition sqlite_catalog.h:1095
void register_graph(std::string_view op, std::shared_ptr< Mapped const > const &source, catalog_admission admission=catalog_admission::trusted)
Definition sqlite_catalog.h:691
unsigned schema_version() const noexcept
Definition sqlite_catalog.h:393
void record_sealed(std::string_view op, object_seal_receipt const &receipt)
Definition sqlite_catalog.h:552
mapped_slice open_object_bytes(object_id const &id, file_kind expected) const
Definition sqlite_catalog.h:1044
object_seal_receipt sealed_receipt(object_id const &id, file_kind expected) const
Definition sqlite_catalog.h:462
void save_session(std::string_view op, std::string_view name, catalog_session_head const &source)
Definition sqlite_catalog.h:999
void add_root(std::string_view kind, std::string_view owner, blob_identity const &pair)
Definition sqlite_catalog.h:1550
static catalog_detail::bytes policy()
Definition sqlite_catalog.h:1355
private_scope_state scope_state(object_id const &id) const
Definition sqlite_catalog.h:436
bool poisoned() const noexcept
Definition sqlite_catalog.h:391
catalog_session_head create_session(std::string_view op, std::string_view name, blob_identity const &head, std::span< std::byte const > checkpoint, catalog_auxiliary_roots auxiliary={})
Definition sqlite_catalog.h:934
catalog_session_head attach_checkpoint(catalog_timeline_head head) const
Definition sqlite_catalog.h:1285
std::optional< catalog_timeline_head > timeline_at(std::string_view name, std::optional< std::uint64_t > generation={}) const
Definition sqlite_catalog.h:1312
std::optional< catalog_session_head > find_saved_session(std::string_view name) const
Definition sqlite_catalog.h:1018
auto seal_native_pair(std::string_view op, blob_identity const &id, object_seal_receipt const &native_receipt, object_seal_receipt const &index_receipt)
Definition sqlite_catalog.h:803
std::optional< object_id > native_merge_output(std::span< std::byte const > domain, catalog_native_merge const &key) const
Definition sqlite_catalog.h:1490
void require_sealed(object_id const &id, file_kind expected, mapped_slice const *evidence=nullptr) const
Definition sqlite_catalog.h:1554
void register_pair(std::string_view op, blob_identity const &id)
Definition sqlite_catalog.h:769
object_id const & identity() const &
Definition sqlite_catalog.h:398
void reserve(std::string_view op, object_attempt_id const &attempt, std::string_view owner, std::span< blob_identity const > inputs, std::span< catalog_object_reservation const > outputs)
Definition sqlite_catalog.h:518
void set_private_scope(object_id const &id)
Definition sqlite_catalog.h:415
void definition(std::string_view type, std::string_view name, std::string_view expected) const
Definition sqlite_catalog.h:1410
void add_auxiliary(std::string_view kind, std::string_view owner, catalog_auxiliary_roots const &value)
Definition sqlite_catalog.h:1304
std::optional< blob_identity > find_save(std::string_view name) const
Definition sqlite_catalog.h:508
catalog_session_head fork_session(std::string_view op, std::string_view name, catalog_session_head const &source)
Definition sqlite_catalog.h:980
static void validate_timeline(catalog_timeline_head const &value)
Definition sqlite_catalog.h:1263
static sqlite_catalog connect(std::filesystem::path const &root, catalog_options options, Ops ops, bool initialize=false)
Definition sqlite_catalog.h:1379
catalog_saved_root acquire_save(std::string_view op, std::string_view name, std::string_view reader_owner)
Definition sqlite_catalog.h:842
std::optional< catalog_timeline_head > find_timeline(std::string_view name) const
Definition sqlite_catalog.h:857
catalog_timeline_head decode_timeline(catalog_detail::bytes const &bytes) const
Definition sqlite_catalog.h:1267
std::filesystem::path const & root() const &&=delete
static void check_object_envelope(file_kind expected, std::uint64_t size, std::uint64_t crc, std::span< std::byte const > bytes)
Definition sqlite_catalog.h:1566
void register_chain(std::string_view op, mapped_query_root< P > const &source, catalog_admission admission=catalog_admission::trusted)
Definition sqlite_catalog.h:632
void require_sessions() const
Definition sqlite_catalog.h:1246
unsigned detect_schema_version() const
Definition sqlite_catalog.h:1416
P policy_type
Definition sqlite_catalog.h:354
void release_private_scope(std::string_view op, object_id const &id)
Definition sqlite_catalog.h:422
catalog_session_publication publish_session(std::string_view op, catalog_session_head const &expected, blob_identity const &candidate, std::span< std::byte const > checkpoint, catalog_auxiliary_roots auxiliary={})
Definition sqlite_catalog.h:950
void register_graphs(std::string_view op, std::span< std::shared_ptr< Mapped const > const > roots, catalog_admission admission=catalog_admission::trusted)
Definition sqlite_catalog.h:697
static std::int64_t kind(file_kind value)
Definition sqlite_catalog.h:1354
void require_active() const
Definition sqlite_catalog.h:1239
void record_prepared_seal(object_seal_receipt const &receipt, file_kind expected)
Definition sqlite_catalog.h:1130
void validate_schema() const
Definition sqlite_catalog.h:1431
std::optional< object_id > private_scope_
Definition sqlite_catalog.h:1185
void add_owner(std::string_view kind, std::string_view name)
Definition sqlite_catalog.h:1546
sqlite3 * db_
Definition sqlite_catalog.h:1178
void register_chain(std::string_view op, mapped_cola_query_root< P > const &source, catalog_admission admission=catalog_admission::trusted)
Definition sqlite_catalog.h:685
static catalog_detail::bytes native_merge_domain(catalog_native_merge const &key)
Definition sqlite_catalog.h:1471
sqlite_catalog(sqlite_catalog &&other) noexcept
Definition sqlite_catalog.h:358
void require_prepared(blob_identity const &head) const
Definition sqlite_catalog.h:1323
void begin_private_scope(std::string_view op, object_id const &id)
Definition sqlite_catalog.h:405
void register_cola_row(cola_pair_descriptor const &value)
Definition sqlite_catalog.h:1143
sqlite_catalog(sqlite_catalog const &)=delete
void insert_cola_row(cola_pair_descriptor const &value)
Definition sqlite_catalog.h:1151
Definition multiverse.h:42
Definition transaction.h:28