Arachne 1.0
Arachne - the perpetual stitcher of Wikidata entities.
Loading...
Searching...
No Matches
arachne::penelope::anonymous_namespace{store.cpp} Namespace Reference

Classes

class  statement
class  database
class  transaction
struct  staging_guard
struct  export_table

Typedefs

using json = nlohmann::json

Enumerations

enum class  database_access { ordinary , immutable_readonly , ordinary , immutable_readonly }
enum class  database_access { ordinary , immutable_readonly , ordinary , immutable_readonly }

Functions

void fail (std::string message)
std::string sqlite_message (sqlite3 *db, std::string_view operation)
std::string sqlite_immutable_uri (const fs::path &path)
std::string read_bytes (const fs::path &path)
json read_json (const fs::path &path)
void write_bytes (const fs::path &path, std::string_view bytes)
std::string lowercase (std::string value)
bool is_sha256 (std::string_view value)
void require_sha256 (std::string_view value, std::string_view field)
std::string canonical_json (const json &value)
std::string require_string (const json &object, std::string_view key, std::string_view context)
std::optional< int > optional_integer (const json &object, std::string_view key, std::string_view context)
std::optional< double > optional_number (const json &object, std::string_view key, std::string_view context)
const jsonarray_or_empty (const json &object, std::string_view key, std::string_view context)
void configure_connection (const database &db)
fs::path schema_path (std::string_view filename)
void create_candidate_schema (const database &db)
std::string domain_name (graph_domain)
fs::path domain_path (const fs::path &root, graph_domain domain)
std::string read_active_id (const fs::path &root, graph_domain domain)
void write_active_id (const fs::path &root, graph_domain domain, std::string_view id)
fs::path make_staging_directory (const fs::path &root, graph_domain domain, std::string_view seed)
void remove_staging_sqlite_sidecars (const fs::path &database_path, const fs::path &expected_staging_directory)
void validate_stable_contract_id (std::string_view value, std::string_view context)
int require_integer (const json &object, std::string_view key, std::string_view context)
double require_number (const json &object, std::string_view key, std::string_view context)
void require_only_fields (const json &object, std::initializer_list< std::string_view > fields, std::string_view context)
void import_candidate_plan (sqlite3 *db, const json &control, const json &payload)
bool query_has_row (sqlite3 *db, std::string_view sql)
void add_problem_if (integrity_report &report, sqlite3 *db, std::string_view sql, std::string message)
integrity_report inspect_database (graph_domain domain, const fs::path &database_path, const database_access access=database_access::ordinary)
void seal_and_validate_database (const graph_domain domain, const fs::path &database_path)
const std::vector< export_table > & export_tables (graph_domain domain)
json sqlite_value (sqlite3_stmt *statement_value, int column)
snapshot_result snapshot_from_directory (const fs::path &root, graph_domain domain, std::string id, bool verify_hashes)
snapshot_result finalize_snapshot (const fs::path &root, graph_domain domain, staging_guard &staging, std::string snapshot_id, std::string database_hash, std::string export_hash, std::size_t applied, std::size_t skipped)
void require_valid_contract (arachnespace::contracts::contract_name expected, const json &document, std::string_view context)
json read_candidate_payload (const fs::path &path)
std::string utc_now ()
json artifact (std::string storage_ref, std::string sha256, std::uintmax_t byte_length, std::string media_type)
json write_validation_report (const fs::path &staging_path, graph_domain domain, std::string_view snapshot_id)

Variables

constexpr std::string_view candidate_contract = "research_candidate_graph_snapshot_v1"

Typedef Documentation

◆ json

using arachne::penelope::anonymous_namespace{store.cpp}::json = nlohmann::json

Definition at line 36 of file store.cpp.

Enumeration Type Documentation

◆ database_access [1/2]

enum class arachne::penelope::anonymous_namespace{store.cpp}::database_access
strong
Enumerator
ordinary 
immutable_readonly 
ordinary 
immutable_readonly 

Definition at line 126 of file store.cpp.

◆ database_access [2/2]

enum class arachne::penelope::anonymous_namespace{store.cpp}::database_access
strong
Enumerator
ordinary 
immutable_readonly 
ordinary 
immutable_readonly 

Definition at line 126 of file store.cpp.

Function Documentation

◆ add_problem_if()

void arachne::penelope::anonymous_namespace{store.cpp}::add_problem_if ( integrity_report & report,
sqlite3 * db,
std::string_view sql,
std::string message )

Definition at line 1029 of file store.cpp.

1032 {
1033 if (query_has_row(db, sql)) {
1034 report.problems.push_back(std::move(message));
1035 }
1036 }
bool query_has_row(sqlite3 *db, std::string_view sql)
Definition store.cpp:1024
std::vector< std::string > problems
Definition store.hpp:36

◆ array_or_empty()

const json & arachne::penelope::anonymous_namespace{store.cpp}::array_or_empty ( const json & object,
std::string_view key,
std::string_view context )

Definition at line 364 of file store.cpp.

366 {
367 static const json empty = json::array();
368 const auto it = object.find(std::string(key));
369 if (it == object.end()) {
370 return empty;
371 }
372 if (!it->is_array()) {
373 fail(
374 std::string(context) + "." + std::string(key)
375 + " must be an array"
376 );
377 }
378 return *it;
379 }

◆ artifact()

json arachne::penelope::anonymous_namespace{store.cpp}::artifact ( std::string storage_ref,
std::string sha256,
std::uintmax_t byte_length,
std::string media_type )

Definition at line 1484 of file store.cpp.

1487 {
1488 return {
1489 { "storage_ref", std::move(storage_ref) },
1490 { "sha256", std::move(sha256) },
1491 { "byte_length", byte_length },
1492 { "media_type", std::move(media_type) },
1493 };
1494 }

◆ canonical_json()

std::string arachne::penelope::anonymous_namespace{store.cpp}::canonical_json ( const json & value)

Definition at line 300 of file store.cpp.

300 {
301 if (value.is_discarded()) {
302 fail("discarded JSON cannot be persisted");
303 }
304 try {
305 return value.dump();
306 } catch (const json::exception& error) {
307 fail(std::string("cannot serialize JSON: ") + error.what());
308 }
309 }

◆ configure_connection()

void arachne::penelope::anonymous_namespace{store.cpp}::configure_connection ( const database & db)

Definition at line 381 of file store.cpp.

381 {
382 db.exec("PRAGMA foreign_keys = ON");
383 db.exec("PRAGMA journal_mode = WAL");
384 db.exec("PRAGMA synchronous = NORMAL");
385 }

Referenced by arachne::penelope::store::replace_candidate_snapshot(), and seal_and_validate_database().

Here is the caller graph for this function:

◆ create_candidate_schema()

void arachne::penelope::anonymous_namespace{store.cpp}::create_candidate_schema ( const database & db)

Definition at line 405 of file store.cpp.

405 {
406 db.exec(read_bytes(schema_path("candidate_v1.sql")));
407 }
fs::path schema_path(std::string_view filename)
Definition store.cpp:387
std::string read_bytes(const fs::path &path)
Definition store.cpp:246

Referenced by arachne::penelope::store::replace_candidate_snapshot().

Here is the caller graph for this function:

◆ domain_name()

std::string arachne::penelope::anonymous_namespace{store.cpp}::domain_name ( graph_domain )

Definition at line 409 of file store.cpp.

409{ return "candidate"; }

Referenced by domain_path(), and write_validation_report().

Here is the caller graph for this function:

◆ domain_path()

fs::path arachne::penelope::anonymous_namespace{store.cpp}::domain_path ( const fs::path & root,
graph_domain domain )

Definition at line 411 of file store.cpp.

411 {
412 return root / domain_name(domain);
413 }

References domain_name().

Referenced by finalize_snapshot(), make_staging_directory(), read_active_id(), snapshot_from_directory(), and write_active_id().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ export_tables()

const std::vector< export_table > & arachne::penelope::anonymous_namespace{store.cpp}::export_tables ( graph_domain domain)

Definition at line 1153 of file store.cpp.

1153 {
1154 static const std::vector<export_table> candidate {
1155 { "candidate_graph_info", "singleton" },
1156 { "candidate_groups", "id" },
1157 { "candidate_nodes", "id" },
1158 { "candidate_edges", "id" },
1159 };
1160 static_cast<void>(domain);
1161 return candidate;
1162 }

◆ fail()

void arachne::penelope::anonymous_namespace{store.cpp}::fail ( std::string message)

Definition at line 42 of file store.cpp.

42 {
43 throw store_error(std::move(message));
44 }

Referenced by arachne::penelope::anonymous_namespace{store.cpp}::database::database().

Here is the caller graph for this function:

◆ finalize_snapshot()

snapshot_result arachne::penelope::anonymous_namespace{store.cpp}::finalize_snapshot ( const fs::path & root,
graph_domain domain,
staging_guard & staging,
std::string snapshot_id,
std::string database_hash,
std::string export_hash,
std::size_t applied,
std::size_t skipped )

Definition at line 1240 of file store.cpp.

1244 {
1245 const fs::path final_directory
1246 = domain_path(root, domain) / "snapshots" / snapshot_id;
1247 if (fs::exists(final_directory)) {
1248 const snapshot_result existing
1249 = snapshot_from_directory(root, domain, snapshot_id, true);
1250 if (existing.database_sha256 != database_hash
1251 || existing.export_sha256 != export_hash) {
1252 fail("snapshot ID collision with different content");
1253 }
1254 } else {
1255 std::error_code error;
1256 fs::rename(staging.path, final_directory, error);
1257 if (error) {
1258 fail(
1259 "cannot publish immutable snapshot directory: "
1260 + error.message()
1261 );
1262 }
1263 staging.keep = true;
1264 }
1265 const std::string previous = read_active_id(root, domain);
1266 const bool changed = previous != snapshot_id;
1267 if (changed) {
1268 write_active_id(root, domain, snapshot_id);
1269 }
1270 snapshot_result result = snapshot_from_directory(
1271 root, domain, std::move(snapshot_id), true
1272 );
1273 result.applied_inputs = applied;
1274 result.skipped_inputs = skipped;
1275 result.activated = changed;
1276 result.changed = changed;
1277 return result;
1278 }
void write_active_id(const fs::path &root, graph_domain domain, std::string_view id)
Definition store.cpp:434
snapshot_result snapshot_from_directory(const fs::path &root, graph_domain domain, std::string id, bool verify_hashes)
Definition store.cpp:1192
fs::path domain_path(const fs::path &root, graph_domain domain)
Definition store.cpp:411
std::string read_active_id(const fs::path &root, graph_domain domain)
Definition store.cpp:415

References arachne::penelope::snapshot_result::activated, arachne::penelope::snapshot_result::applied_inputs, arachne::penelope::snapshot_result::changed, arachne::penelope::snapshot_result::database_sha256, domain_path(), arachne::penelope::snapshot_result::export_sha256, arachne::penelope::anonymous_namespace{store.cpp}::staging_guard::keep, read_active_id(), arachne::penelope::snapshot_result::skipped_inputs, snapshot_from_directory(), and write_active_id().

Here is the call graph for this function:

◆ import_candidate_plan()

void arachne::penelope::anonymous_namespace{store.cpp}::import_candidate_plan ( sqlite3 * db,
const json & control,
const json & payload )

Definition at line 594 of file store.cpp.

596 {
597 if (!payload.is_object()) {
598 fail("candidate materialization payload must be a JSON object");
599 }
601 payload,
602 { "artifact_type", "format_version", "plan_id", "source_snapshot",
603 "algorithm", "groups", "candidates", "works", "relations" },
604 "candidate payload"
605 );
606 if (payload.value("artifact_type", std::string {})
607 != "research_candidate_graph_materialization_v1"
608 || payload.value("format_version", 0) != 1) {
609 fail(
610 "candidate payload must identify "
611 "research_candidate_graph_materialization_v1"
612 );
613 }
614 const std::string payload_plan_id
615 = require_string(payload, "plan_id", "candidate payload");
617 payload_plan_id, "candidate payload.plan_id"
618 );
619 if (payload_plan_id
620 != require_string(control, "plan_id", "candidate control")) {
621 fail(
622 "candidate payload plan_id does not match its control contract"
623 );
624 }
625 for (const std::string_view key :
626 { "groups", "candidates", "works", "relations" }) {
627 const auto it = payload.find(std::string(key));
628 if (it == payload.end() || !it->is_array()) {
629 fail(
630 "candidate payload." + std::string(key)
631 + " must be an array"
632 );
633 }
634 }
635 const auto& source = control.at("source_snapshot");
636 const std::string source_id
637 = require_string(source, "snapshot_id", "control.source_snapshot");
638 const std::string source_ref
639 = require_string(source, "storage_ref", "control.source_snapshot");
640 const std::string source_hash = lowercase(
641 require_string(source, "sha256", "control.source_snapshot")
642 );
643 const auto& payload_source = payload.at("source_snapshot");
645 payload_source, { "snapshot_id", "storage_ref", "sha256" },
646 "payload.source_snapshot"
647 );
648 if (require_string(
649 payload_source, "snapshot_id", "payload.source_snapshot"
650 ) != source_id
652 payload_source, "storage_ref", "payload.source_snapshot"
653 ) != source_ref
655 payload_source, "sha256", "payload.source_snapshot"
656 ))
657 != source_hash) {
658 fail(
659 "candidate payload source snapshot does not match its control "
660 "contract"
661 );
662 }
663 const auto& product = control.at("product_snapshot");
664 const std::string product_id = require_string(
665 product, "snapshot_id", "control.product_snapshot"
666 );
667 const std::string product_hash = lowercase(
668 require_string(product, "sha256", "control.product_snapshot")
669 );
670 const std::string algorithm
671 = require_string(control, "algorithm_version", "control");
672 const auto& configuration = control.at("configuration");
673 const std::string configuration_hash = lowercase(
674 require_string(configuration, "sha256", "control.configuration")
675 );
676 const json configuration_values = configuration.at("values");
677 if (crypto::sha256(canonical_json(configuration_values))
678 != configuration_hash) {
679 fail(
680 "candidate control configuration hash does not bind its "
681 "configuration values"
682 );
683 }
684 const auto& payload_algorithm = payload.at("algorithm");
686 payload_algorithm, { "name", "version", "configuration_sha256" },
687 "payload.algorithm"
688 );
689 const std::string payload_algorithm_name
690 = require_string(payload_algorithm, "name", "payload.algorithm");
691 const std::string payload_algorithm_version
692 = require_string(payload_algorithm, "version", "payload.algorithm");
693 if (algorithm
694 != payload_algorithm_name + "-" + payload_algorithm_version) {
695 fail(
696 "candidate payload algorithm identity/version differs from "
697 "its control"
698 );
699 }
701 payload_algorithm, "configuration_sha256", "payload.algorithm"
702 ))
703 != configuration_hash) {
704 fail(
705 "candidate payload configuration hash differs from its control"
706 );
707 }
708 statement graph_info(
709 db,
710 "INSERT INTO candidate_graph_info"
711 "(singleton,plan_version,source_snapshot_id,source_storage_ref,"
712 "source_snapshot_sha256,product_snapshot_id,product_snapshot_"
713 "sha256,"
714 "algorithm_version,configuration_sha256,configuration_json)"
715 " VALUES(1,1,?1,?2,?3,?4,?5,?6,?7,?8)"
716 );
717 graph_info.text(1, source_id);
718 graph_info.text(2, source_ref);
719 graph_info.text(3, source_hash);
720 graph_info.text(4, product_id);
721 graph_info.text(5, product_hash);
722 graph_info.text(6, algorithm);
723 graph_info.text(7, configuration_hash);
724 graph_info.text(8, canonical_json(configuration_values));
725 graph_info.done();
726
727 std::unordered_set<std::string> groups;
728 std::vector<json> ordered_groups;
729 for (const auto& group : array_or_empty(payload, "groups", "payload")) {
730 if (!group.is_object()) {
731 fail("payload.groups entries must be objects");
732 }
733 ordered_groups.push_back(group);
734 }
735 std::ranges::sort(ordered_groups, [](const json& lhs, const json& rhs) {
736 return lhs.value("group_id", std::string {})
737 < rhs.value("group_id", std::string {});
738 });
739 std::unordered_map<std::string, int> expected_group_counts;
740 for (const auto& group : ordered_groups) {
741 const std::string where
742 = "groups[" + std::to_string(groups.size()) + "]";
744 group,
745 { "group_id", "label", "order", "candidate_count", "rationale",
746 "attributes" },
747 where
748 );
749 const std::string id = require_string(group, "group_id", where);
750 validate_stable_contract_id(id, where + ".group_id");
751 if (!groups.emplace(id).second) {
752 fail(where + " duplicates group ID " + id);
753 }
754 const int candidate_count
755 = require_integer(group, "candidate_count", where);
756 if (candidate_count < 0) {
757 fail(where + ".candidate_count must be non-negative");
758 }
759 expected_group_counts.emplace(id, candidate_count);
760 json metadata { { "candidate_count", candidate_count },
761 { "rationale",
762 require_string(group, "rationale", where) } };
763 if (const auto attributes = group.find("attributes");
764 attributes != group.end()) {
765 if (!attributes->is_object()) {
766 fail(where + ".attributes must be an object");
767 }
768 metadata["attributes"] = *attributes;
769 }
770 statement insert(
771 db,
772 "INSERT INTO candidate_groups(id,label,ordinal,metadata_json)"
773 " VALUES(?1,?2,?3,?4)"
774 );
775 insert.text(1, id);
776 insert.text(2, require_string(group, "label", where));
777 insert.integer(3, require_integer(group, "order", where));
778 insert.text(4, canonical_json(metadata));
779 insert.done();
780 }
781
782 std::unordered_set<std::string> nodes;
783 std::unordered_set<std::string> candidates;
784 std::unordered_map<std::string, std::string> candidate_groups;
785 std::unordered_map<std::string, int> actual_group_counts;
786 std::size_t node_index = 0;
787 for (const auto& node :
788 array_or_empty(payload, "candidates", "payload")) {
789 const std::string where
790 = "candidates[" + std::to_string(node_index++) + "]";
791 if (!node.is_object()) {
792 fail(where + " must be an object");
793 }
795 node,
796 { "candidate_id", "external_id", "label", "kind", "rank",
797 "coverage", "group_id", "selection_reasons",
798 "source_snapshot_id", "attributes" },
799 where
800 );
801 const std::string id = require_string(node, "candidate_id", where);
802 validate_stable_contract_id(id, where + ".candidate_id");
803 if (!nodes.emplace(id).second) {
804 fail(where + " duplicates candidate node ID " + id);
805 }
806 candidates.emplace(id);
807 const std::string group_id
808 = require_string(node, "group_id", where);
809 if (!groups.contains(group_id)) {
810 fail(where + ".group_id references an unknown group");
811 }
812 candidate_groups.emplace(id, group_id);
813 ++actual_group_counts[group_id];
814 const std::string kind = require_string(node, "kind", where);
815 if (kind != "candidate" && kind != "grey") {
816 fail(where + ".kind must be candidate or grey");
817 }
818 const auto& reasons = node.at("selection_reasons");
819 if (!reasons.is_array() || reasons.empty()) {
820 fail(where + ".selection_reasons must be a non-empty array");
821 }
822 for (const auto& reason : reasons) {
823 if (!reason.is_string()
824 || reason.get_ref<const std::string&>().empty()) {
825 fail(
826 where
827 + ".selection_reasons must contain non-empty strings"
828 );
829 }
830 }
831 if (require_string(node, "source_snapshot_id", where)
832 != source_id) {
833 fail(where + " references the wrong source snapshot");
834 }
835 json source_metadata {
836 { "source_snapshot_id",
837 require_string(node, "source_snapshot_id", where) },
838 { "external_id", require_string(node, "external_id", where) }
839 };
840 if (const auto attributes = node.find("attributes");
841 attributes != node.end()) {
842 if (!attributes->is_object()) {
843 fail(where + ".attributes must be an object");
844 }
845 source_metadata["attributes"] = *attributes;
846 }
847 statement insert(
848 db,
849 "INSERT INTO candidate_nodes"
850 "(id,entity_ref,entity_type,label,rank,coverage,group_id,is_"
851 "grey,"
852 "selection_reason_json,source_metadata_json)"
853 " VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10)"
854 );
855 insert.text(1, id);
856 insert.text(2, require_string(node, "external_id", where));
857 insert.text(3, kind);
858 insert.text(4, require_string(node, "label", where));
859 insert.integer(5, require_integer(node, "rank", where));
860 insert.real(6, require_number(node, "coverage", where));
861 insert.text(7, group_id);
862 insert.integer(8, kind == "grey" ? 1 : 0);
863 insert.text(9, canonical_json(reasons));
864 insert.text(10, canonical_json(source_metadata));
865 insert.done();
866 }
867 for (const auto& [group, expected] : expected_group_counts) {
868 if (actual_group_counts[group] != expected) {
869 fail(
870 "group candidate_count does not match candidate "
871 "membership: "
872 + group
873 );
874 }
875 }
876
877 std::size_t work_index = 0;
878 for (const auto& work : array_or_empty(payload, "works", "payload")) {
879 const std::string where
880 = "works[" + std::to_string(work_index++) + "]";
881 if (!work.is_object()) {
882 fail(where + " must be an object");
883 }
885 work,
886 { "work_id", "candidate_id", "external_id", "label", "year",
887 "source_snapshot_id", "attributes" },
888 where
889 );
890 const std::string id = require_string(work, "work_id", where);
891 validate_stable_contract_id(id, where + ".work_id");
892 if (!nodes.emplace(id).second) {
893 fail(where + " duplicates a candidate/work node ID " + id);
894 }
895 const std::string candidate_id
896 = require_string(work, "candidate_id", where);
897 if (!candidates.contains(candidate_id)) {
898 fail(where + ".candidate_id references an unknown candidate");
899 }
900 if (require_string(work, "source_snapshot_id", where)
901 != source_id) {
902 fail(where + " references the wrong source snapshot");
903 }
904 json metadata { { "candidate_id", candidate_id },
905 { "source_snapshot_id", source_id } };
906 if (const auto year = optional_integer(work, "year", where)) {
907 metadata["year"] = *year;
908 }
909 if (const auto attributes = work.find("attributes");
910 attributes != work.end()) {
911 if (!attributes->is_object()) {
912 fail(where + ".attributes must be an object");
913 }
914 metadata["attributes"] = *attributes;
915 }
916 statement insert(
917 db,
918 "INSERT INTO candidate_nodes"
919 "(id,entity_ref,entity_type,label,rank,coverage,group_id,is_"
920 "grey,"
921 "selection_reason_json,source_metadata_json)"
922 " VALUES(?1,?2,'candidate_work',?3,NULL,0,?4,0,'[]',?5)"
923 );
924 insert.text(1, id);
925 insert.text(2, require_string(work, "external_id", where));
926 insert.text(3, require_string(work, "label", where));
927 insert.text(4, candidate_groups.at(candidate_id));
928 insert.text(5, canonical_json(metadata));
929 insert.done();
930 }
931
932 std::unordered_set<std::string> edges;
933 std::size_t edge_index = 0;
934 for (const auto& edge :
935 array_or_empty(payload, "relations", "payload")) {
936 const std::string where
937 = "relations[" + std::to_string(edge_index++) + "]";
938 if (!edge.is_object()) {
939 fail(where + " must be an object");
940 }
942 edge,
943 { "relation_id", "source_id", "target_id", "relation_type",
944 "weight", "provenance", "attributes" },
945 where
946 );
947 const std::string id = require_string(edge, "relation_id", where);
948 validate_stable_contract_id(id, where + ".relation_id");
949 if (!edges.emplace(id).second) {
950 fail(where + " duplicates candidate edge ID " + id);
951 }
952 const std::string subject
953 = require_string(edge, "source_id", where);
954 const std::string object = require_string(edge, "target_id", where);
955 if (!nodes.contains(subject) || !nodes.contains(object)) {
956 fail(where + " references an unknown candidate node");
957 }
958 const auto& provenance = edge.at("provenance");
960 provenance,
961 { "origin", "source_snapshot_id", "algorithm_version",
962 "explanation" },
963 where + ".provenance"
964 );
965 if (require_string(provenance, "origin", where + ".provenance")
966 != "algorithmic_external"
968 provenance, "source_snapshot_id", where + ".provenance"
969 ) != source_id
971 provenance, "algorithm_version", where + ".provenance"
972 ) != payload_algorithm_version) {
973 fail(where + ".provenance is inconsistent with payload inputs");
974 }
975 (void)require_string(
976 provenance, "explanation", where + ".provenance"
977 );
978 json edge_metadata = provenance;
979 if (const auto attributes = edge.find("attributes");
980 attributes != edge.end()) {
981 if (!attributes->is_object()) {
982 fail(where + ".attributes must be an object");
983 }
984 edge_metadata["attributes"] = *attributes;
985 }
986 statement insert(
987 db,
988 "INSERT INTO candidate_edges"
989 "(id,subject_id,relation_type,object_id,weight,metadata_json)"
990 " VALUES(?1,?2,?3,?4,?5,?6)"
991 );
992 insert.text(1, id);
993 insert.text(2, subject);
994 insert.text(3, require_string(edge, "relation_type", where));
995 insert.text(4, object);
996 insert.optional_real(5, optional_number(edge, "weight", where));
997 insert.text(6, canonical_json(edge_metadata));
998 insert.done();
999 }
1000
1001 const auto& summary = control.at("summary");
1002 if (require_integer(summary, "candidate_count", "control.summary")
1003 != static_cast<int>(
1004 array_or_empty(payload, "candidates", "payload").size()
1005 )
1006 || require_integer(summary, "edge_count", "control.summary")
1007 != static_cast<int>(
1008 array_or_empty(payload, "relations", "payload").size()
1009 )
1010 || require_integer(summary, "group_count", "control.summary")
1011 != static_cast<int>(ordered_groups.size())) {
1012 fail(
1013 "candidate control summary does not match the resolved payload"
1014 );
1015 }
1016 }
std::string sha256(std::span< const std::byte > bytes)
Definition crypto.cpp:209
std::optional< int > optional_integer(const json &object, std::string_view key, std::string_view context)
Definition store.cpp:325
std::string canonical_json(const json &value)
Definition store.cpp:300
std::string lowercase(std::string value)
Definition store.cpp:279
std::optional< double > optional_number(const json &object, std::string_view key, std::string_view context)
Definition store.cpp:341
void validate_stable_contract_id(std::string_view value, std::string_view context)
Definition store.cpp:524
double require_number(const json &object, std::string_view key, std::string_view context)
Definition store.cpp:556
const json & array_or_empty(const json &object, std::string_view key, std::string_view context)
Definition store.cpp:364
int require_integer(const json &object, std::string_view key, std::string_view context)
Definition store.cpp:536
void require_only_fields(const json &object, std::initializer_list< std::string_view > fields, std::string_view context)
Definition store.cpp:576
std::string require_string(const json &object, std::string_view key, std::string_view context)
Definition store.cpp:311

References arachne::penelope::anonymous_namespace{store.cpp}::statement::done(), and validate_stable_contract_id().

Here is the call graph for this function:

◆ inspect_database()

integrity_report arachne::penelope::anonymous_namespace{store.cpp}::inspect_database ( graph_domain domain,
const fs::path & database_path,
const database_access access = database_access::ordinary )

Definition at line 1038 of file store.cpp.

1041 {
1042 static_cast<void>(domain);
1043 integrity_report report;
1044 if (!fs::is_regular_file(database_path)) {
1045 report.problems.push_back("database file does not exist");
1046 return report;
1047 }
1048 try {
1049 database db(database_path, SQLITE_OPEN_READONLY, access);
1050 db.exec("PRAGMA foreign_keys = ON");
1051 {
1052 statement check(db.get(), "PRAGMA integrity_check");
1053 bool saw_result = false;
1054 while (check.row()) {
1055 saw_result = true;
1056 const auto* raw = sqlite3_column_text(check.get(), 0);
1057 const std::string value = raw == nullptr
1058 ? std::string {}
1059 : reinterpret_cast<const char*>(raw);
1060 if (value != "ok") {
1061 report.problems.push_back("SQLite integrity: " + value);
1062 }
1063 }
1064 if (!saw_result) {
1065 report.problems.push_back(
1066 "SQLite integrity_check returned no result"
1067 );
1068 }
1069 }
1070 {
1071 statement foreign_keys(db.get(), "PRAGMA foreign_key_check");
1072 while (foreign_keys.row()) {
1073 const auto* table
1074 = sqlite3_column_text(foreign_keys.get(), 0);
1075 report.problems.push_back(
1076 "foreign-key violation in table "
1077 + std::string(
1078 table == nullptr
1079 ? "unknown"
1080 : reinterpret_cast<const char*>(table)
1081 )
1082 );
1083 }
1084 }
1085
1087 report, db.get(),
1088 "SELECT 1 WHERE (SELECT count(*) FROM "
1089 "candidate_graph_info)<>1",
1090 "candidate database must contain exactly one graph-info row"
1091 );
1093 report, db.get(),
1094 "SELECT 1 FROM candidate_edges WHERE subject_id=object_id "
1095 "LIMIT 1",
1096 "candidate graph contains a self edge"
1097 );
1098 } catch (const std::exception& error) {
1099 report.problems.push_back(error.what());
1100 }
1101 report.ok = report.problems.empty();
1102 return report;
1103 }
void add_problem_if(integrity_report &report, sqlite3 *db, std::string_view sql, std::string message)
Definition store.cpp:1029

References arachne::penelope::anonymous_namespace{store.cpp}::database::database(), arachne::penelope::anonymous_namespace{store.cpp}::database::get(), arachne::penelope::anonymous_namespace{store.cpp}::statement::get(), arachne::penelope::integrity_report::ok, and arachne::penelope::anonymous_namespace{store.cpp}::statement::row().

Referenced by arachne::penelope::store::integrity_check(), and seal_and_validate_database().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ is_sha256()

bool arachne::penelope::anonymous_namespace{store.cpp}::is_sha256 ( std::string_view value)

Definition at line 286 of file store.cpp.

286 {
287 return value.size() == 64
288 && std::ranges::all_of(value, [](unsigned char c) {
289 return std::isdigit(c) != 0 || (c >= 'a' && c <= 'f')
290 || (c >= 'A' && c <= 'F');
291 });
292 }

◆ lowercase()

std::string arachne::penelope::anonymous_namespace{store.cpp}::lowercase ( std::string value)

Definition at line 279 of file store.cpp.

279 {
280 std::ranges::transform(value, value.begin(), [](unsigned char c) {
281 return static_cast<char>(std::tolower(c));
282 });
283 return value;
284 }

◆ make_staging_directory()

fs::path arachne::penelope::anonymous_namespace{store.cpp}::make_staging_directory ( const fs::path & root,
graph_domain domain,
std::string_view seed )

Definition at line 451 of file store.cpp.

453 {
454 const fs::path staging = domain_path(root, domain) / ".staging";
455 fs::create_directories(staging);
456 const std::string base = "stage-" + crypto::sha256(seed).substr(0, 20);
457 for (std::size_t suffix = 0; suffix < 1000; ++suffix) {
458 fs::path result = staging / base;
459 if (suffix != 0) {
460 result += "-" + std::to_string(suffix);
461 }
462 std::error_code error;
463 if (fs::create_directory(result, error)) {
464 return result;
465 }
466 if (error) {
467 fail("cannot create staging directory: " + error.message());
468 }
469 }
470 fail("cannot allocate a unique staging directory");
471 }

References domain_path(), and arachne::crypto::sha256().

Here is the call graph for this function:

◆ optional_integer()

std::optional< int > arachne::penelope::anonymous_namespace{store.cpp}::optional_integer ( const json & object,
std::string_view key,
std::string_view context )

Definition at line 325 of file store.cpp.

327 {
328 const auto it = object.find(std::string(key));
329 if (it == object.end() || it->is_null()) {
330 return std::nullopt;
331 }
332 if (!it->is_number_integer()) {
333 fail(
334 std::string(context) + "." + std::string(key)
335 + " must be an integer or null"
336 );
337 }
338 return it->get<int>();
339 }

◆ optional_number()

std::optional< double > arachne::penelope::anonymous_namespace{store.cpp}::optional_number ( const json & object,
std::string_view key,
std::string_view context )

Definition at line 341 of file store.cpp.

343 {
344 const auto it = object.find(std::string(key));
345 if (it == object.end() || it->is_null()) {
346 return std::nullopt;
347 }
348 if (!it->is_number()) {
349 fail(
350 std::string(context) + "." + std::string(key)
351 + " must be a number or null"
352 );
353 }
354 const double result = it->get<double>();
355 if (!std::isfinite(result)) {
356 fail(
357 std::string(context) + "." + std::string(key)
358 + " must be finite"
359 );
360 }
361 return result;
362 }

◆ query_has_row()

bool arachne::penelope::anonymous_namespace{store.cpp}::query_has_row ( sqlite3 * db,
std::string_view sql )

Definition at line 1024 of file store.cpp.

1024 {
1025 statement query(db, sql);
1026 return query.row();
1027 }

References arachne::penelope::anonymous_namespace{store.cpp}::statement::row().

Here is the call graph for this function:

◆ read_active_id()

std::string arachne::penelope::anonymous_namespace{store.cpp}::read_active_id ( const fs::path & root,
graph_domain domain )

Definition at line 415 of file store.cpp.

415 {
416 const fs::path pointer = domain_path(root, domain) / "ACTIVE";
417 std::error_code error;
418 if (!fs::exists(pointer, error)) {
419 return {};
420 }
421 std::string id = read_bytes(pointer);
422 while (!id.empty()
423 && std::isspace(static_cast<unsigned char>(id.back()))) {
424 id.pop_back();
425 }
426 if (id.empty() || !std::ranges::all_of(id, [](unsigned char c) {
427 return std::isalnum(c) != 0 || c == '-' || c == '_';
428 })) {
429 fail("invalid ACTIVE pointer content: " + pointer.string());
430 }
431 return id;
432 }

References domain_path(), and read_bytes().

Referenced by finalize_snapshot().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ read_bytes()

std::string arachne::penelope::anonymous_namespace{store.cpp}::read_bytes ( const fs::path & path)

Definition at line 246 of file store.cpp.

246 {
247 std::ifstream input(path, std::ios::binary);
248 if (!input) {
249 fail("cannot read file: " + path.string());
250 }
251 std::ostringstream data;
252 data << input.rdbuf();
253 if (!input.eof() && input.fail()) {
254 fail("failed while reading file: " + path.string());
255 }
256 return data.str();
257 }

Referenced by read_active_id(), and read_candidate_payload().

Here is the caller graph for this function:

◆ read_candidate_payload()

json arachne::penelope::anonymous_namespace{store.cpp}::read_candidate_payload ( const fs::path & path)

Definition at line 1400 of file store.cpp.

1400 {
1401 const std::string bytes = read_bytes(path);
1402 try {
1403 return json::parse(bytes);
1404 } catch (const json::parse_error&) {
1405 json result {
1406 { "groups", json::array() },
1407 { "candidates", json::array() },
1408 { "works", json::array() },
1409 { "relations", json::array() },
1410 };
1411 std::istringstream lines(bytes);
1412 std::string line;
1413 std::size_t line_number = 0;
1414 while (std::getline(lines, line)) {
1415 ++line_number;
1416 if (line.empty()) {
1417 continue;
1418 }
1419 json record;
1420 try {
1421 record = json::parse(line);
1422 } catch (const json::exception& error) {
1423 fail(
1424 "invalid candidate JSONL line "
1425 + std::to_string(line_number) + ": " + error.what()
1426 );
1427 }
1428 if (!record.is_object()) {
1429 fail("candidate JSONL records must be objects");
1430 }
1431 const std::string kind
1432 = record.value("record_type", std::string {});
1433 if (kind.empty() || kind == "header") {
1434 const json data
1435 = record.contains("data") ? record["data"] : record;
1436 if (!data.is_object()) {
1437 fail("candidate JSONL header data must be an object");
1438 }
1439 for (const auto& [key, value] : data.items()) {
1440 if (key != "record_type" && key != "data") {
1441 result[key] = value;
1442 }
1443 }
1444 continue;
1445 }
1446 static const std::map<std::string, std::string, std::less<>>
1447 arrays {
1448 { "group", "groups" },
1449 { "candidate", "candidates" },
1450 { "work", "works" },
1451 { "relation", "relations" },
1452 };
1453 const auto destination = arrays.find(kind);
1454 if (destination == arrays.end()) {
1455 fail("unknown candidate JSONL record_type: " + kind);
1456 }
1457 json data = record.contains("data") ? record["data"] : record;
1458 if (!data.is_object()) {
1459 fail("candidate JSONL record data must be an object");
1460 }
1461 data.erase("record_type");
1462 result[destination->second].push_back(std::move(data));
1463 }
1464 return result;
1465 }
1466 }

References read_bytes().

Here is the call graph for this function:

◆ read_json()

json arachne::penelope::anonymous_namespace{store.cpp}::read_json ( const fs::path & path)

Definition at line 259 of file store.cpp.

259 {
260 try {
261 return json::parse(read_bytes(path));
262 } catch (const json::exception& error) {
263 fail("invalid JSON in " + path.string() + ": " + error.what());
264 }
265 }

◆ remove_staging_sqlite_sidecars()

void arachne::penelope::anonymous_namespace{store.cpp}::remove_staging_sqlite_sidecars ( const fs::path & database_path,
const fs::path & expected_staging_directory )

Definition at line 473 of file store.cpp.

476 {
477 if (database_path.filename() != "graph.sqlite"
478 || database_path.parent_path() != expected_staging_directory) {
479 fail("refusing to clean SQLite sidecars outside expected staging");
480 }
481
482 const std::string directory_name
483 = expected_staging_directory.filename().string();
484 const bool snapshot_staging
485 = expected_staging_directory.parent_path().filename() == ".staging"
486 && directory_name.starts_with("stage-");
487 std::error_code error;
488 const fs::file_status staging_status
489 = fs::symlink_status(expected_staging_directory, error);
490 if (!snapshot_staging || error || !fs::is_directory(staging_status)) {
491 fail("refusing to clean SQLite sidecars outside safe staging");
492 }
493
494 for (const std::string_view suffix : { "-wal", "-shm", "-journal" }) {
495 fs::path sidecar = database_path;
496 sidecar += suffix;
497 error.clear();
498 const fs::file_status status = fs::symlink_status(sidecar, error);
499 if (error == std::errc::no_such_file_or_directory
500 || (!error && status.type() == fs::file_type::not_found)) {
501 continue;
502 }
503 if (error || !fs::is_regular_file(status)) {
504 fail(
505 "refusing to remove unsafe staging sidecar: "
506 + sidecar.string()
507 );
508 }
509 if (!fs::remove(sidecar, error) || error) {
510 fail(
511 "cannot remove checkpointed staging sidecar: "
512 + sidecar.string() + ": " + error.message()
513 );
514 }
515 }
516 }

Referenced by arachne::penelope::store::checkpoint_staging().

Here is the caller graph for this function:

◆ require_integer()

int arachne::penelope::anonymous_namespace{store.cpp}::require_integer ( const json & object,
std::string_view key,
std::string_view context )

Definition at line 536 of file store.cpp.

538 {
539 const auto value = object.find(std::string(key));
540 if (value == object.end() || !value->is_number_integer()) {
541 fail(
542 std::string(context) + "." + std::string(key)
543 + " must be an integer"
544 );
545 }
546 try {
547 return value->get<int>();
548 } catch (const json::exception&) {
549 fail(
550 std::string(context) + "." + std::string(key)
551 + " is outside the supported integer range"
552 );
553 }
554 }

◆ require_number()

double arachne::penelope::anonymous_namespace{store.cpp}::require_number ( const json & object,
std::string_view key,
std::string_view context )

Definition at line 556 of file store.cpp.

558 {
559 const auto value = object.find(std::string(key));
560 if (value == object.end() || !value->is_number()) {
561 fail(
562 std::string(context) + "." + std::string(key)
563 + " must be a number"
564 );
565 }
566 const double result = value->get<double>();
567 if (!std::isfinite(result)) {
568 fail(
569 std::string(context) + "." + std::string(key)
570 + " must be finite"
571 );
572 }
573 return result;
574 }

◆ require_only_fields()

void arachne::penelope::anonymous_namespace{store.cpp}::require_only_fields ( const json & object,
std::initializer_list< std::string_view > fields,
std::string_view context )

Definition at line 576 of file store.cpp.

579 {
580 if (!object.is_object()) {
581 fail(std::string(context) + " must be an object");
582 }
583 const std::set<std::string_view> allowed(fields);
584 for (const auto& [key, value] : object.items()) {
585 (void)value;
586 if (!allowed.contains(key)) {
587 fail(
588 std::string(context) + " contains unsupported field " + key
589 );
590 }
591 }
592 }

◆ require_sha256()

void arachne::penelope::anonymous_namespace{store.cpp}::require_sha256 ( std::string_view value,
std::string_view field )

Definition at line 294 of file store.cpp.

294 {
295 if (!is_sha256(value)) {
296 fail(std::string(field) + " must be a 64-character SHA-256 digest");
297 }
298 }
bool is_sha256(std::string_view value)
Definition store.cpp:286

Referenced by arachne::penelope::store::replace_candidate_snapshot(), and snapshot_from_directory().

Here is the caller graph for this function:

◆ require_string()

std::string arachne::penelope::anonymous_namespace{store.cpp}::require_string ( const json & object,
std::string_view key,
std::string_view context )

Definition at line 311 of file store.cpp.

313 {
314 const auto it = object.find(std::string(key));
315 if (it == object.end() || !it->is_string()
316 || it->get_ref<const std::string&>().empty()) {
317 fail(
318 std::string(context) + "." + std::string(key)
319 + " must be a non-empty string"
320 );
321 }
322 return it->get<std::string>();
323 }

◆ require_valid_contract()

void arachne::penelope::anonymous_namespace{store.cpp}::require_valid_contract ( arachnespace::contracts::contract_name expected,
const json & document,
std::string_view context )

Definition at line 1382 of file store.cpp.

1385 {
1386 const auto validation
1387 = arachnespace::contracts::validate(expected, document);
1388 if (validation.valid()) {
1389 return;
1390 }
1391 std::string message
1392 = std::string(context) + " contract validation failed";
1393 for (const auto& diagnostic : validation.diagnostics) {
1394 message += "\n- " + diagnostic.instance_path + " ["
1395 + diagnostic.code + "] " + diagnostic.message;
1396 }
1397 fail(std::move(message));
1398 }
validation_result validate(const nlohmann::json &document)

◆ schema_path()

fs::path arachne::penelope::anonymous_namespace{store.cpp}::schema_path ( std::string_view filename)

Definition at line 387 of file store.cpp.

387 {
388 const fs::path compiled
389 = fs::path(__FILE__).parent_path().parent_path().parent_path()
390 / "schema" / filename;
391 const std::array candidates {
392 compiled,
393 fs::current_path() / "schema" / filename,
394 fs::current_path() / "arachne" / "schema" / filename,
395 };
396 for (const auto& path : candidates) {
397 std::error_code error;
398 if (fs::is_regular_file(path, error)) {
399 return path;
400 }
401 }
402 fail("cannot locate schema file " + std::string(filename));
403 }

◆ seal_and_validate_database()

void arachne::penelope::anonymous_namespace{store.cpp}::seal_and_validate_database ( const graph_domain domain,
const fs::path & database_path )

Definition at line 1105 of file store.cpp.

1107 {
1108 {
1109 database db(database_path, SQLITE_OPEN_READWRITE);
1111 db.exec("PRAGMA optimize");
1112 {
1113 statement checkpoint(
1114 db.get(), "PRAGMA wal_checkpoint(TRUNCATE)"
1115 );
1116 if (!checkpoint.row()
1117 || sqlite3_column_int(checkpoint.get(), 0) != 0) {
1118 fail("SQLite WAL checkpoint did not complete");
1119 }
1120 }
1121 statement required_mode(db.get(), "PRAGMA journal_mode");
1122 if (!required_mode.row()) {
1123 fail("cannot inspect checkpointed database journal mode");
1124 }
1125 const auto* mode = sqlite3_column_text(required_mode.get(), 0);
1126 const std::string actual_mode = mode == nullptr
1127 ? std::string {}
1128 : lowercase(reinterpret_cast<const char*>(mode));
1129 if (actual_mode != "wal") {
1130 fail(
1131 "checkpointed database did not retain required WAL mode: "
1132 + actual_mode
1133 );
1134 }
1135 }
1136 const auto report = inspect_database(
1137 domain, database_path, database_access::immutable_readonly
1138 );
1139 if (!report.ok) {
1140 std::string message = "database integrity validation failed";
1141 for (const auto& problem : report.problems) {
1142 message += "\n- " + problem;
1143 }
1144 fail(std::move(message));
1145 }
1146 }
integrity_report inspect_database(graph_domain domain, const fs::path &database_path, const database_access access=database_access::ordinary)
Definition store.cpp:1038
void configure_connection(const database &db)
Definition store.cpp:381

References configure_connection(), arachne::penelope::anonymous_namespace{store.cpp}::database::database(), arachne::penelope::anonymous_namespace{store.cpp}::database::get(), arachne::penelope::anonymous_namespace{store.cpp}::statement::get(), immutable_readonly, inspect_database(), arachne::penelope::integrity_report::ok, and arachne::penelope::anonymous_namespace{store.cpp}::statement::row().

Referenced by arachne::penelope::store::checkpoint_staging().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ snapshot_from_directory()

snapshot_result arachne::penelope::anonymous_namespace{store.cpp}::snapshot_from_directory ( const fs::path & root,
graph_domain domain,
std::string id,
bool verify_hashes )

Definition at line 1192 of file store.cpp.

1195 {
1196 const fs::path directory = domain_path(root, domain) / "snapshots" / id;
1197 snapshot_result result;
1198 result.domain = domain;
1199 result.snapshot_id = std::move(id);
1200 result.database_path = directory / "graph.sqlite";
1201 result.export_path = directory / "graph.jsonl";
1202 result.metadata_path = directory / "metadata.json";
1203 const json metadata = read_json(result.metadata_path);
1204 const auto validation = arachnespace::contracts::validate(
1206 research_candidate_graph_snapshot,
1207 metadata
1208 );
1209 if (!validation.valid()
1210 || metadata.value("contract", std::string {})
1212 || metadata.value("snapshot_id", std::string {})
1213 != result.snapshot_id) {
1214 fail("snapshot metadata does not match ACTIVE pointer");
1215 }
1217 metadata.at("database"), "sha256", "metadata.database"
1218 ));
1219 const auto& exports = metadata.at("exports");
1220 if (exports.empty() || !exports.front().is_object()
1221 || !exports.front().contains("artifact")) {
1222 fail("snapshot metadata has no deterministic export artifact");
1223 }
1225 exports.front().at("artifact"), "sha256",
1226 "metadata.exports[0].artifact"
1227 ));
1228 require_sha256(result.database_sha256, "metadata.database_sha256");
1229 require_sha256(result.export_sha256, "metadata.export_sha256");
1230 if (verify_hashes
1232 != result.database_sha256
1234 != result.export_sha256)) {
1235 fail("active snapshot content hash does not match metadata");
1236 }
1237 return result;
1238 }
std::string sha256_file(const std::filesystem::path &path)
Definition crypto.cpp:221
void require_sha256(std::string_view value, std::string_view field)
Definition store.cpp:294
constexpr std::string_view candidate_contract
Definition store.cpp:40
std::filesystem::path metadata_path
Definition store.hpp:45
std::filesystem::path export_path
Definition store.hpp:44
std::filesystem::path database_path
Definition store.hpp:43

References arachne::penelope::snapshot_result::database_sha256, arachne::penelope::snapshot_result::domain, domain_path(), arachne::penelope::snapshot_result::export_sha256, require_sha256(), arachnespace::contracts::research_candidate_graph_snapshot, arachne::crypto::sha256_file(), and arachne::penelope::snapshot_result::snapshot_id.

Referenced by finalize_snapshot().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ sqlite_immutable_uri()

std::string arachne::penelope::anonymous_namespace{store.cpp}::sqlite_immutable_uri ( const fs::path & path)

Definition at line 128 of file store.cpp.

128 {
129 static constexpr char hex[] = "0123456789ABCDEF";
130 const std::string native
131 = fs::absolute(path).lexically_normal().generic_string();
132 std::string result = "file:";
133 result.reserve(native.size() + 24U);
134 for (const char raw_value : native) {
135 const auto value = static_cast<unsigned char>(raw_value);
136 if (std::isalnum(value) != 0 || value == '/' || value == ':'
137 || value == '-' || value == '.' || value == '_'
138 || value == '~') {
139 result.push_back(static_cast<char>(value));
140 } else {
141 result.push_back('%');
142 result.push_back(hex[value >> 4U]);
143 result.push_back(hex[value & 0x0FU]);
144 }
145 }
146 result += "?immutable=1";
147 return result;
148 }

Referenced by arachne::penelope::anonymous_namespace{store.cpp}::database::database().

Here is the caller graph for this function:

◆ sqlite_message()

std::string arachne::penelope::anonymous_namespace{store.cpp}::sqlite_message ( sqlite3 * db,
std::string_view operation )

Definition at line 46 of file store.cpp.

46 {
47 return std::string(operation) + ": "
48 + (db == nullptr ? "unknown SQLite error" : sqlite3_errmsg(db));
49 }

◆ sqlite_value()

json arachne::penelope::anonymous_namespace{store.cpp}::sqlite_value ( sqlite3_stmt * statement_value,
int column )

Definition at line 1164 of file store.cpp.

1164 {
1165 switch (sqlite3_column_type(statement_value, column)) {
1166 case SQLITE_NULL:
1167 return nullptr;
1168 case SQLITE_INTEGER:
1169 return sqlite3_column_int64(statement_value, column);
1170 case SQLITE_FLOAT: {
1171 const double value = sqlite3_column_double(statement_value, column);
1172 if (!std::isfinite(value)) {
1173 fail("database contains a non-finite number");
1174 }
1175 return value;
1176 }
1177 case SQLITE_TEXT: {
1178 const auto* value = sqlite3_column_text(statement_value, column);
1179 const int bytes = sqlite3_column_bytes(statement_value, column);
1180 return std::string(
1181 reinterpret_cast<const char*>(value),
1182 static_cast<std::size_t>(bytes)
1183 );
1184 }
1185 case SQLITE_BLOB:
1186 fail("canonical graph tables must not contain BLOB values");
1187 default:
1188 fail("unknown SQLite column type");
1189 }
1190 }

◆ utc_now()

std::string arachne::penelope::anonymous_namespace{store.cpp}::utc_now ( )

Definition at line 1468 of file store.cpp.

1468 {
1469 const std::time_t now = std::time(nullptr);
1470 std::tm broken_down {};
1471 if (gmtime_r(&now, &broken_down) == nullptr) {
1472 fail("cannot create snapshot activation timestamp");
1473 }
1474 std::array<char, 32> buffer {};
1475 if (std::strftime(
1476 buffer.data(), buffer.size(), "%Y-%m-%dT%H:%M:%SZ", &broken_down
1477 )
1478 == 0) {
1479 fail("cannot format snapshot activation timestamp");
1480 }
1481 return buffer.data();
1482 }

◆ validate_stable_contract_id()

void arachne::penelope::anonymous_namespace{store.cpp}::validate_stable_contract_id ( std::string_view value,
std::string_view context )

Definition at line 524 of file store.cpp.

526 {
527 if (value.empty() || value.size() > 128
528 || !std::ranges::all_of(value, [](unsigned char c) {
529 return std::isalnum(c) != 0 || c == '_' || c == '-'
530 || c == '.' || c == ':';
531 })) {
532 fail(std::string(context) + " must be a safe, non-empty stable ID");
533 }
534 }

Referenced by import_candidate_plan().

Here is the caller graph for this function:

◆ write_active_id()

void arachne::penelope::anonymous_namespace{store.cpp}::write_active_id ( const fs::path & root,
graph_domain domain,
std::string_view id )

Definition at line 434 of file store.cpp.

436 {
437 const fs::path parent = domain_path(root, domain);
438 const fs::path pointer = parent / "ACTIVE";
439 const fs::path temporary
440 = parent / (".ACTIVE-" + crypto::sha256(id).substr(0, 16) + ".tmp");
441 write_bytes(temporary, std::string(id) + "\n");
442 if (std::rename(temporary.c_str(), pointer.c_str()) != 0) {
443 std::error_code ignored;
444 fs::remove(temporary, ignored);
445 fail(
446 "cannot atomically replace ACTIVE pointer: " + pointer.string()
447 );
448 }
449 }
void write_bytes(const fs::path &path, std::string_view bytes)
Definition store.cpp:267

References domain_path(), and arachne::crypto::sha256().

Referenced by finalize_snapshot().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ write_bytes()

void arachne::penelope::anonymous_namespace{store.cpp}::write_bytes ( const fs::path & path,
std::string_view bytes )

Definition at line 267 of file store.cpp.

267 {
268 std::ofstream output(path, std::ios::binary | std::ios::trunc);
269 if (!output) {
270 fail("cannot write file: " + path.string());
271 }
272 output.write(bytes.data(), static_cast<std::streamsize>(bytes.size()));
273 output.flush();
274 if (!output) {
275 fail("failed while writing file: " + path.string());
276 }
277 }

◆ write_validation_report()

json arachne::penelope::anonymous_namespace{store.cpp}::write_validation_report ( const fs::path & staging_path,
graph_domain domain,
std::string_view snapshot_id )

Definition at line 1496 of file store.cpp.

1499 {
1500 const json report {
1501 { "format_version", 1 },
1502 { "domain", domain_name(domain) },
1503 { "snapshot_id", snapshot_id },
1504 { "passed", true },
1505 { "checks",
1506 { { "sqlite_integrity", "ok" },
1507 { "foreign_keys", "ok" },
1508 { "domain_structure", "ok" } } },
1509 };
1510 const fs::path path = staging_path / "validation.json";
1511 write_bytes(path, canonical_json(report) + "\n");
1512 return artifact(
1513 domain_name(domain) + "/snapshots/" + std::string(snapshot_id)
1514 + "/validation.json",
1515 crypto::sha256_file(path), fs::file_size(path), "application/json"
1516 );
1517 }
json artifact(std::string storage_ref, std::string sha256, std::uintmax_t byte_length, std::string media_type)
Definition store.cpp:1484

References domain_name().

Here is the call graph for this function:

Variable Documentation

◆ candidate_contract

std::string_view arachne::penelope::anonymous_namespace{store.cpp}::candidate_contract = "research_candidate_graph_snapshot_v1"
constexpr

Definition at line 39 of file store.cpp.