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

Classes

class  database_error
class  statement
class  database
class  transaction
struct  file_identity
struct  file_snapshot
class  file_descriptor
struct  parsed_batch
struct  local_id_allocation_state
struct  concept_relation_row
struct  hint_label
struct  hint_candidate_support
struct  hint_components

Typedefs

using json = nlohmann::json
using record_validator
using hint_pair = std::pair<std::string, std::string>
using hint_candidate_map

Enumerations

enum class  value_kind {
  string , integer , number , boolean ,
  object , array , string , integer ,
  number , boolean , object , array
}
enum class  value_kind {
  string , integer , number , boolean ,
  object , array , string , integer ,
  number , boolean , object , array
}

Functions

std::string sqlite_message (sqlite3 *const value, const std::string_view operation)
void require_current_product_structure (sqlite3 *const sql)
bool same_identity (const file_identity &left, const file_identity &right)
file_identity identity_of (const struct stat &state)
file_snapshot read_batch_file (const fs::path &path)
bool snapshot_still_current (const file_snapshot &snapshot)
std::string pointer_escape (const std::string_view value)
void add_issue (parsed_batch &batch, const std::string_view code, const std::string_view path, const std::string_view message, const json *const value=nullptr)
bool valid_batch_id (const std::string_view value)
bool valid_local_id (const std::string_view value)
bool valid_canonical_id (const std::string_view value, const std::string_view family)
bool looks_like_canonical_entity_id (const std::string_view value)
bool valid_slug (const std::string_view value)
void check_keys (parsed_batch &batch, const json &object, const std::string &path, const std::set< std::string, std::less<> > &allowed, const std::set< std::string, std::less<> > &required={})
bool is_kind (const json &value, const value_kind kind)
void require_kind (parsed_batch &batch, const json &object, const std::string &key, const std::string &path, const value_kind kind)
void require_nonempty_string (parsed_batch &batch, const json &object, const std::string &key, const std::string &path)
void require_enum (parsed_batch &batch, const json &object, const std::string &key, const std::string &path, const std::set< std::string, std::less<> > &allowed)
void require_integer_range (parsed_batch &batch, const json &object, const std::string &key, const std::string &path, const std::int64_t minimum, const std::int64_t maximum)
void require_number_range (parsed_batch &batch, const json &object, const std::string &key, const std::string &path, const double minimum, const double maximum)
std::string indexed_path (const std::string_view parent, const std::size_t index)
void set_application_context (parsed_batch &batch, const std::string_view path, const json &value)
void validate_local_id (parsed_batch &batch, const json &record, const std::string &path, std::unordered_map< std::string, std::string > &locals, const std::string_view family)
void validate_entity_reference_shape (parsed_batch &batch, const json &record, const std::string &key, const std::string &path)
void validate_integer_or_local_reference_shape (parsed_batch &batch, const json &record, const std::string &key, const std::string &path)
void validate_create_agent (parsed_batch &batch, const json &value, const std::string &path, std::unordered_map< std::string, std::string > &locals)
void validate_create_work (parsed_batch &batch, const json &value, const std::string &path, std::unordered_map< std::string, std::string > &locals)
void validate_create_concept (parsed_batch &batch, const json &value, const std::string &path, std::unordered_map< std::string, std::string > &locals)
void validate_create_manifestation (parsed_batch &batch, const json &value, const std::string &path, std::unordered_map< std::string, std::string > &locals)
void validate_create_name (parsed_batch &batch, const json &value, const std::string &path)
void validate_create_external_id (parsed_batch &batch, const json &value, const std::string &path)
void validate_create_source (parsed_batch &batch, const json &value, const std::string &path, std::unordered_map< std::string, std::string > &locals)
void validate_create_evidence (parsed_batch &batch, const json &value, const std::string &path, std::unordered_map< std::string, std::string > &locals)
void validate_create_credit (parsed_batch &batch, const json &value, const std::string &path)
void validate_create_measurement (parsed_batch &batch, const json &value, const std::string &path)
void validate_create_financial (parsed_batch &batch, const json &value, const std::string &path)
void validate_evidence_list (parsed_batch &batch, const json &value, const std::string &path)
void validate_create_work_concept (parsed_batch &batch, const json &value, const std::string &path, std::unordered_map< std::string, std::string > &locals)
void validate_create_concept_relation (parsed_batch &batch, const json &value, const std::string &path, std::unordered_map< std::string, std::string > &locals)
void validate_create_parent_guide (parsed_batch &batch, const json &value, const std::string &path, std::unordered_map< std::string, std::string > &locals)
template<typename Validator>
void validate_array (parsed_batch &batch, const json &parent, const std::string &key, const std::string &parent_path, Validator &&validator)
void validate_update_record (parsed_batch &batch, const json &value, const std::string &path, const std::string_view family, const std::map< std::string, value_kind, std::less<> > &mutable_fields, const std::set< std::string, std::less<> > &required_fields, const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > &enum_fields, const bool allow_empty=false, const bool integer_id=false)
void validate_delete_object (parsed_batch &batch, const json &update, const std::string &path)
void validate_merge_record (parsed_batch &batch, const json &value, const std::string &path, const std::string_view family, const std::map< std::string, value_kind, std::less<> > &mutable_fields, const std::set< std::string, std::less<> > &required_fields, const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > &enum_fields, std::map< std::string, std::string, std::less<> > &merged_entities)
void validate_document_shape (parsed_batch &batch)
json parse_strict_json (const std::string &bytes)
bool row_exists (sqlite3 *const database_value, const std::string_view table, const std::string_view column, const json &id)
std::string entity_family (sqlite3 *const database_value, const std::string &id)
std::string ref_string (const json &record, const char *key)
void prevalidate_entity_reference (parsed_batch &batch, sqlite3 *const database_value, const json &record, const std::string &key, const std::string &path, const std::string_view expected_family={})
void prevalidate_integer_or_local_reference (parsed_batch &batch, sqlite3 *const database_value, const json &record, const std::string &key, const std::string &path, const std::string_view local_family, const std::string_view table)
void prevalidate_semantics (parsed_batch &batch, database &product)
std::int64_t maximum_row_id (sqlite3 *const sql, const std::string_view table)
std::int64_t maximum_entity_suffix (sqlite3 *const sql, const std::string_view prefix)
std::string formatted_entity_id (const std::string_view family, const std::int64_t sequence)
local_id_allocation_state initial_allocation_state (database &product)
std::int64_t next_sequence (std::int64_t &sequence, const std::string_view description)
void allocate_local_references (parsed_batch &batch, local_id_allocation_state &sequences)
std::string resolve_entity (const parsed_batch &batch, const json &value)
std::int64_t resolve_row_reference (const std::unordered_map< std::string, std::int64_t > &locals, const json &value)
void bind_optional (statement &insert, const int index, const json &row, const char *key, const bool serialize=false)
void create_entities (parsed_batch &batch, sqlite3 *const sql)
void create_names_and_identifiers (parsed_batch &batch, sqlite3 *const sql)
void create_sources_and_evidence (parsed_batch &batch, sqlite3 *const sql)
void create_facts (parsed_batch &batch, sqlite3 *const sql)
void attach_evidence (const parsed_batch &batch, sqlite3 *const sql, const std::string_view table, const std::int64_t assertion_id, const json &evidence)
void create_assertions (parsed_batch &batch, sqlite3 *const sql)
std::string sql_column (const std::string_view field)
void update_row (sqlite3 *const sql, const std::string_view table, const std::string_view id_column, const json &operation)
void apply_updates (parsed_batch &batch, sqlite3 *const sql)
void apply_deletes (parsed_batch &batch, sqlite3 *const sql)
std::set< std::string, std::less<> > string_set (const json &array)
void resolve_scalar_fields (sqlite3 *const sql, const std::string_view table, const std::string_view id_column, const std::string &target, const std::vector< std::string > &members, const json &operation, const std::vector< std::string > &columns, const std::set< std::string, std::less<> > &required)
void reject_preferred_name_conflict (sqlite3 *const sql, const std::string &target, const std::vector< std::string > &members)
bool query_exists (sqlite3 *const sql, const std::string_view query_text, const std::string &target, const std::string &member)
void merge_entity_scoped_rows (sqlite3 *const sql, const std::string &target, const std::string &member)
void merge_credits (sqlite3 *const sql, const std::string_view id_column, const std::string &target, const std::string &member)
void transfer_assertion_evidence (sqlite3 *const sql, const std::string_view evidence_table, const std::int64_t keeper, const std::int64_t duplicate)
void merge_work_concepts (sqlite3 *const sql, const std::string_view id_column, const std::string &target, const std::string &member)
void merge_parent_guides (sqlite3 *const sql, const std::string_view id_column, const std::string &target, const std::string &member)
void merge_financial_facts (sqlite3 *const sql, const std::string &target, const std::string &member)
void merge_concept_relations (sqlite3 *const sql, const std::string &target, const std::string &member)
void delete_merged_entity (sqlite3 *const sql, const std::string &member)
std::vector< std::string > merge_members (const json &operation)
void mark_merge_neighborhood (parsed_batch &batch, sqlite3 *const sql, const std::string_view family, const std::string &target, const std::vector< std::string > &members)
void merge_agents (parsed_batch &batch, sqlite3 *const sql, const json &operation)
void merge_works (parsed_batch &batch, sqlite3 *const sql, const json &operation)
void merge_concepts (parsed_batch &batch, sqlite3 *const sql, const json &operation)
void apply_merges (parsed_batch &batch, sqlite3 *const sql)
std::string normalized_label (const std::string_view value)
std::vector< hint_labelall_hint_labels (sqlite3 *const sql)
std::optional< hint_labelhint_label_for_id (sqlite3 *const sql, const std::string &id)
std::set< std::string, std::less<> > trigrams (const std::string_view value)
double trigram_score (const std::string_view left, const std::string_view right)
bool is_trigram_block (const std::string_view block_type)
std::size_t maximum_hint_block_size (const std::string_view block_type)
std::string contextual_block_key (const std::string_view context, const std::string_view value)
std::string trigram_support_key (const std::string_view block_key)
void record_hint_candidate (hint_candidate_map &candidates, const std::string &first, const std::string &second, const std::string_view block_type, const std::string_view block_key)
std::set< hint_pairreviewable_blocked_pairs (const hint_candidate_map &candidates)
void insert_hint_block (statement &block, statement &member, const hint_label &label, const std::string_view block_type, const std::string &block_key)
void insert_hint_blocks (sqlite3 *const sql, const hint_label &label)
void refresh_hint_blocks (sqlite3 *const sql, const std::set< std::string, std::less<> > &affected, std::map< std::string, hint_label, std::less<> > &label_cache)
std::set< hint_pairincrementally_blocked_pairs (sqlite3 *const sql, const std::set< std::string, std::less<> > &affected)
std::set< hint_pairall_blocked_pairs (sqlite3 *const sql)
void bootstrap_hint_blocks_if_empty (sqlite3 *const sql)
std::set< std::string, std::less<> > query_value_set (sqlite3 *const sql, const std::string_view query_text, const std::string &id)
double jaccard (const std::set< std::string, std::less<> > &left, const std::set< std::string, std::less<> > &right)
std::size_t intersection_count (const std::set< std::string, std::less<> > &left, const std::set< std::string, std::less<> > &right)
double maximum_text_similarity (const std::vector< std::string > &left, const std::vector< std::string > &right)
double alternate_text_similarity (const hint_label &left, const hint_label &right)
hint_components calculate_hint_components (sqlite3 *const sql, const hint_label &left, const hint_label &right)
void upsert_hint (sqlite3 *const sql, const hint_label &first, const hint_label &second)
void regenerate_incremental_hints (sqlite3 *const sql, const std::set< std::string, std::less<> > &affected)
std::size_t rebuild_hints (database &product)
bool batch_was_applied (sqlite3 *const sql, const std::string &batch_id)
void require_clean_foreign_keys (sqlite3 *const sql)
void record_issues (database &product, const std::string &batch_id, const std::vector< inbox_issue > &issues)
void apply_one_batch (parsed_batch &batch, database &product)
bool real_directory (const fs::path &path)
void require_real_directory (const fs::path &path, const std::string_view description)
void require_real_database_file (const fs::path &path)
std::string read_schema (const fs::path &path)
void ensure_product_database (const fs::path &repository_root, const fs::path &path)
std::vector< file_snapshotsnapshot_inbox (const fs::path &inbox)
parsed_batch parse_batch (file_snapshot snapshot)
void reject_duplicate_pending_ids (std::vector< parsed_batch > &batches, sqlite3 *const sql)
void remove_unchanged (const file_snapshot &snapshot)
void move_rejected_unchanged (const file_snapshot &snapshot, const fs::path &rejected)
void record_and_move_rejected (parsed_batch &batch, database &product, const fs::path &rejected)
inbox_batch_report report_for (const parsed_batch &batch, const inbox_batch_status status)
inbox_result run_inbox (const fs::path &repository_root, const bool apply)

Variables

constexpr std::uintmax_t maximum_batch_bytes = 32U * 1024U * 1024U
constexpr int current_product_schema = 5
const std::set< std::string, std::less<> > agent_types
const std::set< std::string, std::less<> > media
const std::set< std::string, std::less<> > date_precisions
const std::set< std::string, std::less<> > concept_types
const std::set< std::string, std::less<> > manifestation_types
const std::set< std::string, std::less<> > name_types
const std::set< std::string, std::less<> > source_types
const std::set< std::string, std::less<> > stances
const std::set< std::string, std::less<> > credit_roles
const std::set< std::string, std::less<> > importance_values
const std::set< std::string, std::less<> > measurement_types
const std::set< std::string, std::less<> > measurement_units
const std::set< std::string, std::less<> > concept_relation_types
const std::set< std::string, std::less<> > work_concept_types
const std::set< std::string, std::less<> > historical_roles
const std::set< std::string, std::less<> > guide_categories
const std::set< std::string, std::less<> > spoiler_levels
const std::map< std::string, value_kind, std::less<> > agent_mutable
const std::map< std::string, value_kind, std::less<> > agent_merge_mutable
const std::map< std::string, value_kind, std::less<> > work_mutable
const std::map< std::string, value_kind, std::less<> > concept_mutable
const std::map< std::string, value_kind, std::less<> > manifestation_mutable
const std::map< std::string, value_kind, std::less<> > source_mutable
const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > agent_update_enums
const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > agent_merge_enums
const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > work_update_enums
const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > concept_update_enums
const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > manifestation_update_enums
const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > source_update_enums

Typedef Documentation

◆ hint_candidate_map

typedef std::map< hint_pair, hint_candidate_support, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::hint_candidate_map
Initial value:

Definition at line 4697 of file inbox.cpp.

◆ hint_pair

typedef std::pair< std::string, std::string > arachne::penelope::anonymous_namespace{inbox.cpp}::hint_pair = std::pair<std::string, std::string>

Definition at line 4690 of file inbox.cpp.

◆ json

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

Definition at line 46 of file inbox.cpp.

◆ record_validator

typedef void(* arachne::penelope::anonymous_namespace{inbox.cpp}::record_validator)( parsed_batch &, const json &, const std::string &)
Initial value:

Definition at line 1516 of file inbox.cpp.

Enumeration Type Documentation

◆ value_kind [1/2]

enum class arachne::penelope::anonymous_namespace{inbox.cpp}::value_kind
strong

◆ value_kind [2/2]

enum class arachne::penelope::anonymous_namespace{inbox.cpp}::value_kind
strong
Enumerator
string 
integer 
number 
boolean 
object 
array 
string 
integer 
number 
boolean 
object 
array 

Definition at line 709 of file inbox.cpp.

709 {
710 string,
711 integer,
712 number,
713 boolean,
714 object,
715 array,
716};

Function Documentation

◆ add_issue()

void arachne::penelope::anonymous_namespace{inbox.cpp}::add_issue ( parsed_batch & batch,
const std::string_view code,
const std::string_view path,
const std::string_view message,
const json *const value = nullptr )

Definition at line 587 of file inbox.cpp.

591 {
592 batch.issues.push_back(
593 {
594 .batch_id = batch.batch_id,
595 .code = std::string(code),
596 .json_path = path.empty() ? "/" : std::string(path),
597 .message = std::string(message),
598 .value_json = value == nullptr ? std::string() : value->dump(),
599 }
600 );
601}

References arachne::penelope::anonymous_namespace{inbox.cpp}::parsed_batch::batch_id.

◆ all_blocked_pairs()

std::set< hint_pair > arachne::penelope::anonymous_namespace{inbox.cpp}::all_blocked_pairs ( sqlite3 *const sql)
nodiscard

Definition at line 5006 of file inbox.cpp.

5008 {
5009 hint_candidate_map candidates;
5010 statement blocks(
5011 sql,
5012 "SELECT b.entity_type,b.block_type,b.block_key,m.entity_id "
5013 "FROM merge_hint_blocks b "
5014 "JOIN merge_hint_block_members m ON m.block_id=b.id "
5015 "ORDER BY b.entity_type,b.block_type,b.block_key,m.entity_id"
5016 );
5017 std::string current_family;
5018 std::string current_type;
5019 std::string current_key;
5020 std::vector<std::string> members;
5021 auto flush = [&]() {
5022 if (members.size() >= 2U
5023 && members.size() <= maximum_hint_block_size(current_type)) {
5024 for (std::size_t left = 0; left < members.size(); ++left) {
5025 for (std::size_t right = left + 1U;
5026 right < members.size(); ++right) {
5028 candidates, members[left], members[right],
5029 current_type, current_key
5030 );
5031 }
5032 }
5033 }
5034 members.clear();
5035 };
5036 while (blocks.step()) {
5037 const std::string family = blocks.text(0);
5038 const std::string block_type = blocks.text(1);
5039 const std::string block_key = blocks.text(2);
5040 if (!current_type.empty()
5041 && std::tie(family, block_type, block_key)
5042 != std::tie(current_family, current_type, current_key)) {
5043 flush();
5044 }
5045 current_family = family;
5046 current_type = block_type;
5047 current_key = block_key;
5048 members.push_back(blocks.text(3));
5049 }
5050 if (!current_type.empty()) {
5051 flush();
5052 }
5053 return reviewable_blocked_pairs(candidates);
5054}
std::map< hint_pair, hint_candidate_support, std::less<> > hint_candidate_map
Definition inbox.cpp:4697
std::set< hint_pair > reviewable_blocked_pairs(const hint_candidate_map &candidates)
Definition inbox.cpp:4768
std::size_t maximum_hint_block_size(const std::string_view block_type)
Definition inbox.cpp:4705
void record_hint_candidate(hint_candidate_map &candidates, const std::string &first, const std::string &second, const std::string_view block_type, const std::string_view block_key)
Definition inbox.cpp:4747

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::text().

Here is the call graph for this function:

◆ all_hint_labels()

std::vector< hint_label > arachne::penelope::anonymous_namespace{inbox.cpp}::all_hint_labels ( sqlite3 *const sql)
nodiscard

Definition at line 4469 of file inbox.cpp.

4469 {
4470 statement query(
4471 sql,
4472 "SELECT e.id,"
4473 "CASE WHEN e.entity_type IN('person','organization','group') "
4474 "THEN 'agent' ELSE e.entity_type END,"
4475 "CASE WHEN e.entity_type='concept' THEN COALESCE(c.slug,'') "
4476 "ELSE COALESCE(("
4477 " SELECT n.value FROM names n WHERE n.entity_id=e.id "
4478 " ORDER BY n.is_preferred DESC,n.id LIMIT 1"
4479 "),'') END "
4480 "FROM entities e LEFT JOIN concepts c ON c.entity_id=e.id "
4481 "WHERE e.entity_type IN("
4482 "'person','organization','group','work','concept') ORDER BY e.id"
4483 );
4484 std::vector<hint_label> result;
4485 std::map<std::string, std::size_t, std::less<>> index_by_id;
4486 while (query.step()) {
4487 hint_label label {
4488 .id = query.text(0),
4489 .family = query.text(1),
4490 .label = query.text(2),
4491 .fingerprint = {},
4492 .fingerprints = {},
4493 .preferred_fingerprints = {},
4494 .alternate_fingerprints = {},
4495 };
4496 label.fingerprint = normalized_label(label.label);
4497 if (!label.fingerprint.empty()) {
4498 label.fingerprints.push_back(label.fingerprint);
4499 if (label.family == "concept") {
4500 label.preferred_fingerprints.push_back(label.fingerprint);
4501 }
4502 }
4503 index_by_id.emplace(label.id, result.size());
4504 result.push_back(std::move(label));
4505 }
4506 statement names(
4507 sql,
4508 "SELECT n.entity_id,n.value,n.is_preferred FROM names n "
4509 "JOIN entities e ON e.id=n.entity_id "
4510 "WHERE e.entity_type IN("
4511 "'person','organization','group','work','concept') "
4512 "ORDER BY n.entity_id,n.id"
4513 );
4514 while (names.step()) {
4515 const auto found = index_by_id.find(names.text(0));
4516 if (found == index_by_id.end()) {
4517 continue;
4518 }
4519 std::string normalized = normalized_label(names.text(1));
4520 if (!normalized.empty()) {
4521 auto& label = result[found->second];
4522 label.fingerprints.push_back(normalized);
4523 if (label.family != "concept" && names.integer(2) != 0) {
4524 label.preferred_fingerprints.push_back(normalized);
4525 } else {
4526 label.alternate_fingerprints.push_back(std::move(normalized));
4527 }
4528 }
4529 }
4530 for (auto& label : result) {
4531 const auto deduplicate = [](std::vector<std::string>& values) {
4532 std::ranges::sort(values);
4533 values.erase(
4534 std::unique(values.begin(), values.end()), values.end()
4535 );
4536 };
4537 deduplicate(label.fingerprints);
4538 deduplicate(label.preferred_fingerprints);
4539 deduplicate(label.alternate_fingerprints);
4540 if (label.fingerprint.empty() && !label.fingerprints.empty()) {
4541 label.fingerprint = label.fingerprints.front();
4542 }
4543 if (label.preferred_fingerprints.empty()
4544 && !label.fingerprint.empty()) {
4545 label.preferred_fingerprints.push_back(label.fingerprint);
4546 }
4547 std::erase(
4549 );
4550 }
4551 return result;
4552}
std::string normalized_label(const std::string_view value)
Definition inbox.cpp:4396

References arachne::penelope::anonymous_namespace{inbox.cpp}::hint_label::family, arachne::penelope::anonymous_namespace{inbox.cpp}::hint_label::fingerprint, arachne::penelope::anonymous_namespace{inbox.cpp}::hint_label::id, arachne::penelope::anonymous_namespace{inbox.cpp}::statement::integer(), arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::text().

Here is the call graph for this function:

◆ allocate_local_references()

void arachne::penelope::anonymous_namespace{inbox.cpp}::allocate_local_references ( parsed_batch & batch,
local_id_allocation_state & sequences )

Definition at line 2968 of file inbox.cpp.

2970 {
2971 const auto& create = batch.document.at("create");
2972 for (const auto& [collection, family] : {
2973 std::pair { "agents", "agent" },
2974 std::pair { "works", "work" },
2975 std::pair { "concepts", "concept" },
2976 std::pair { "manifestations", "manifestation" } }) {
2977 const auto rows = create.find(collection);
2978 if (rows == create.end() || !rows->is_array()) {
2979 continue;
2980 }
2981 for (const auto& row : *rows) {
2982 auto& sequence = sequences.entity_sequences.at(family);
2983 batch.resolved_entity_ids.emplace(
2984 row.at("local_id").get<std::string>(),
2986 family, next_sequence(sequence, "canonical entity")
2987 )
2988 );
2989 }
2990 }
2991 if (const auto rows = create.find("sources");
2992 rows != create.end() && rows->is_array()) {
2993 for (const auto& row : *rows) {
2994 if (row.is_object() && row.contains("local_id")
2995 && row["local_id"].is_string()) {
2996 batch.source_local_ids.emplace(
2997 row["local_id"].get<std::string>(),
2998 next_sequence(sequences.source_sequence, "source")
2999 );
3000 }
3001 }
3002 }
3003 if (const auto rows = create.find("evidence");
3004 rows != create.end() && rows->is_array()) {
3005 for (const auto& row : *rows) {
3006 if (row.is_object() && row.contains("local_id")
3007 && row["local_id"].is_string()) {
3008 batch.evidence_local_ids.emplace(
3009 row["local_id"].get<std::string>(),
3010 next_sequence(sequences.evidence_sequence, "evidence")
3011 );
3012 }
3013 }
3014 }
3015 for (const auto& [collection, table] : {
3016 std::pair { "work_concepts", "work_concepts" },
3017 std::pair { "concept_relations", "concept_relations" },
3018 std::pair {
3019 "parent_guide_assertions", "parent_guide_assertions" } }) {
3020 const auto rows = create.find(collection);
3021 if (rows == create.end() || !rows->is_array()) {
3022 continue;
3023 }
3024 auto& sequence = sequences.assertion_sequences.at(table);
3025 for (const auto& row : *rows) {
3026 if (row.is_object() && row.contains("local_id")
3027 && row["local_id"].is_string()) {
3028 batch.assertion_local_ids.emplace(
3029 row["local_id"].get<std::string>(),
3030 next_sequence(sequence, "assertion")
3031 );
3032 }
3033 }
3034 }
3035}
std::string formatted_entity_id(const std::string_view family, const std::int64_t sequence)
Definition inbox.cpp:2904
std::int64_t next_sequence(std::int64_t &sequence, const std::string_view description)
Definition inbox.cpp:2957
std::map< std::string, std::int64_t, std::less<> > entity_sequences
Definition inbox.cpp:2928
std::map< std::string, std::int64_t, std::less<> > assertion_sequences
Definition inbox.cpp:2931
std::unordered_map< std::string, std::int64_t > evidence_local_ids
Definition inbox.cpp:580
std::unordered_map< std::string, std::int64_t > source_local_ids
Definition inbox.cpp:579
std::unordered_map< std::string, std::string > resolved_entity_ids
Definition inbox.cpp:578
std::unordered_map< std::string, std::int64_t > assertion_local_ids
Definition inbox.cpp:581

◆ alternate_text_similarity()

double arachne::penelope::anonymous_namespace{inbox.cpp}::alternate_text_similarity ( const hint_label & left,
const hint_label & right )
nodiscard

Definition at line 5143 of file inbox.cpp.

5145 {
5146 /*
5147 * An alternate signal is deliberately not the all-label maximum. It
5148 * compares each side's alternates with the other entity's known labels,
5149 * but excludes the preferred/preferred comparison represented by the
5150 * separate preferred-name/title signal.
5151 */
5152 return std::max(
5155 ),
5158 )
5159 );
5160}
double maximum_text_similarity(const std::vector< std::string > &left, const std::vector< std::string > &right)
Definition inbox.cpp:5117

Referenced by calculate_hint_components().

Here is the caller graph for this function:

◆ apply_deletes()

void arachne::penelope::anonymous_namespace{inbox.cpp}::apply_deletes ( parsed_batch & batch,
sqlite3 *const sql )

Definition at line 3630 of file inbox.cpp.

3630 {
3631 const auto& update = batch.document.at("update");
3632 const auto deletes = update.find("delete");
3633 if (deletes == update.end() || !deletes->is_object()) {
3634 return;
3635 }
3636 // Assertions must be removed before evidence so their cascading evidence-link
3637 // deletes do not look like attempts to orphan an assertion.
3638 for (const std::string_view table_name : {
3639 "names", "external_ids", "credits", "measurements",
3640 "financial_facts", "work_concepts", "concept_relations",
3641 "parent_guide_assertions", "evidence", "ingest_issues" }) {
3642 const auto found = deletes->find(table_name);
3643 if (found == deletes->end()) {
3644 continue;
3645 }
3646 const std::string table(table_name);
3647 const auto& values = *found;
3648 if (!values.is_array()) {
3649 continue;
3650 }
3651 if (table == "ingest_issues") {
3652 statement remove(
3653 sql,
3654 "DELETE FROM ingest_issues "
3655 "WHERE batch_id=? AND code=? AND json_path=? "
3656 "AND status IN('resolved','ignored')"
3657 );
3658 for (std::size_t index = 0; index < values.size(); ++index) {
3659 const auto& value = values[index];
3661 batch,
3663 "/update/delete/" + pointer_escape(table), index
3664 ),
3665 value
3666 );
3667 sqlite3_reset(remove.native());
3668 sqlite3_clear_bindings(remove.native());
3669 remove.bind_json_value(1, value.at("batch_id"));
3670 remove.bind_json_value(2, value.at("code"));
3671 remove.bind_json_value(3, value.at("json_path"));
3672 remove.execute();
3673 if (sqlite3_changes(sql) != 1) {
3674 throw database_error(
3675 "ingest issue disappeared or reopened before deletion"
3676 );
3677 }
3678 }
3679 continue;
3680 }
3681 statement remove(
3682 sql, "DELETE FROM " + table + " WHERE id=?"
3683 );
3684 for (std::size_t value_index = 0;
3685 value_index < values.size(); ++value_index) {
3686 const auto& value = values[value_index];
3688 batch,
3690 "/update/delete/" + pointer_escape(table), value_index
3691 ),
3692 value
3693 );
3694 std::vector<std::string> entity_columns;
3695 if (table == "names" || table == "external_ids"
3696 || table == "measurements") {
3697 entity_columns = { "entity_id" };
3698 } else if (table == "credits") {
3699 entity_columns = { "work_id", "agent_id" };
3700 } else if (table == "financial_facts") {
3701 entity_columns = { "work_id" };
3702 } else if (table == "work_concepts"
3703 || table == "parent_guide_assertions") {
3704 entity_columns = { "work_id", "concept_id" };
3705 } else if (table == "concept_relations") {
3706 entity_columns = {
3707 "subject_concept_id", "object_concept_id"
3708 };
3709 }
3710 if (!entity_columns.empty()) {
3711 std::string query_text = "SELECT ";
3712 for (std::size_t index = 0; index < entity_columns.size();
3713 ++index) {
3714 if (index != 0U) {
3715 query_text += ",";
3716 }
3717 query_text += entity_columns[index];
3718 }
3719 query_text += " FROM " + table + " WHERE id=?";
3720 statement affected(sql, query_text);
3721 affected.bind_json_value(1, value);
3722 if (affected.step()) {
3723 for (std::size_t index = 0;
3724 index < entity_columns.size(); ++index) {
3725 batch.affected_entities.emplace(
3726 affected.text(static_cast<int>(index))
3727 );
3728 }
3729 }
3730 }
3731 sqlite3_reset(remove.native());
3732 sqlite3_clear_bindings(remove.native());
3733 remove.bind_json_value(1, value);
3734 remove.execute();
3735 if (sqlite3_changes(sql) != 1) {
3736 throw database_error(
3737 "relationship row disappeared before deletion"
3738 );
3739 }
3740 }
3741 }
3742}
std::string pointer_escape(const std::string_view value)
Definition inbox.cpp:555
std::string indexed_path(const std::string_view parent, const std::size_t index)
Definition inbox.cpp:896
void set_application_context(parsed_batch &batch, const std::string_view path, const json &value)
Definition inbox.cpp:902
std::set< std::string, std::less<> > affected_entities
Definition inbox.cpp:582

Referenced by apply_one_batch().

Here is the caller graph for this function:

◆ apply_merges()

void arachne::penelope::anonymous_namespace{inbox.cpp}::apply_merges ( parsed_batch & batch,
sqlite3 *const sql )

Definition at line 4362 of file inbox.cpp.

4362 {
4363 const auto& merge = batch.document.at("merge");
4364 if (const auto rows = merge.find("agents");
4365 rows != merge.end() && rows->is_array()) {
4366 for (std::size_t index = 0; index < rows->size(); ++index) {
4367 const auto& row = (*rows)[index];
4369 batch, indexed_path("/merge/agents", index), row
4370 );
4371 merge_agents(batch, sql, row);
4372 }
4373 }
4374 if (const auto rows = merge.find("works");
4375 rows != merge.end() && rows->is_array()) {
4376 for (std::size_t index = 0; index < rows->size(); ++index) {
4377 const auto& row = (*rows)[index];
4379 batch, indexed_path("/merge/works", index), row
4380 );
4381 merge_works(batch, sql, row);
4382 }
4383 }
4384 if (const auto rows = merge.find("concepts");
4385 rows != merge.end() && rows->is_array()) {
4386 for (std::size_t index = 0; index < rows->size(); ++index) {
4387 const auto& row = (*rows)[index];
4389 batch, indexed_path("/merge/concepts", index), row
4390 );
4391 merge_concepts(batch, sql, row);
4392 }
4393 }
4394}
void merge_agents(parsed_batch &batch, sqlite3 *const sql, const json &operation)
Definition inbox.cpp:4242
void merge_works(parsed_batch &batch, sqlite3 *const sql, const json &operation)
Definition inbox.cpp:4268
void merge_concepts(parsed_batch &batch, sqlite3 *const sql, const json &operation)
Definition inbox.cpp:4298

Referenced by apply_one_batch().

Here is the caller graph for this function:

◆ apply_one_batch()

void arachne::penelope::anonymous_namespace{inbox.cpp}::apply_one_batch ( parsed_batch & batch,
database & product )

Definition at line 5629 of file inbox.cpp.

5629 {
5630 transaction change(product);
5631 if (batch_was_applied(product.native(), batch.batch_id)) {
5632 batch.already_applied = true;
5633 change.commit();
5634 return;
5635 }
5636 /*
5637 * References were resolved before BEGIN. Recheck every canonical reference
5638 * against the writer snapshot so a concurrent deletion cannot invalidate
5639 * the preflight.
5640 */
5641 parsed_batch recheck = batch;
5642 recheck.issues.clear();
5643 prevalidate_semantics(recheck, product);
5644 if (!recheck.issues.empty()) {
5645 batch.issues = std::move(recheck.issues);
5646 throw database_error("reference changed after batch prevalidation");
5647 }
5648
5649 /*
5650 * Delete explicitly replaced internal/relationship rows before inserting
5651 * their successors. Otherwise the old row can occupy the same natural
5652 * UNIQUE key and make an atomic replacement depend on impossible ordering.
5653 */
5654 apply_deletes(batch, product.native());
5655 create_entities(batch, product.native());
5656 create_names_and_identifiers(batch, product.native());
5657 create_sources_and_evidence(batch, product.native());
5658 create_facts(batch, product.native());
5659 create_assertions(batch, product.native());
5660 apply_updates(batch, product.native());
5661 apply_merges(batch, product.native());
5662 batch.application_path = "/merge_hints";
5663 batch.application_value_json.clear();
5665 product.native(), batch.affected_entities
5666 );
5667 {
5668 statement resolve(
5669 product.native(),
5670 "UPDATE ingest_issues SET status='resolved' "
5671 "WHERE batch_id=? AND status='open'"
5672 );
5673 resolve.bind(1, batch.batch_id);
5674 resolve.execute();
5675 }
5676 batch.application_path = "/";
5677 batch.application_value_json.clear();
5679 {
5680 statement applied(
5681 product.native(),
5682 "INSERT INTO applied_batches(batch_id) VALUES(?)"
5683 );
5684 applied.bind(1, batch.batch_id);
5685 applied.execute();
5686 }
5687 change.commit();
5688}
void require_clean_foreign_keys(sqlite3 *const sql)
Definition inbox.cpp:5587
void regenerate_incremental_hints(sqlite3 *const sql, const std::set< std::string, std::less<> > &affected)
Definition inbox.cpp:5499
bool batch_was_applied(sqlite3 *const sql, const std::string &batch_id)
Definition inbox.cpp:5577
void create_names_and_identifiers(parsed_batch &batch, sqlite3 *const sql)
Definition inbox.cpp:3184
void apply_updates(parsed_batch &batch, sqlite3 *const sql)
Definition inbox.cpp:3584
void apply_deletes(parsed_batch &batch, sqlite3 *const sql)
Definition inbox.cpp:3630
void create_facts(parsed_batch &batch, sqlite3 *const sql)
Definition inbox.cpp:3310
void create_sources_and_evidence(parsed_batch &batch, sqlite3 *const sql)
Definition inbox.cpp:3242
void create_assertions(parsed_batch &batch, sqlite3 *const sql)
Definition inbox.cpp:3418
void prevalidate_semantics(parsed_batch &batch, database &product)
Definition inbox.cpp:2434
void apply_merges(parsed_batch &batch, sqlite3 *const sql)
Definition inbox.cpp:4362
void create_entities(parsed_batch &batch, sqlite3 *const sql)
Definition inbox.cpp:3073

References arachne::penelope::anonymous_namespace{inbox.cpp}::parsed_batch::already_applied, arachne::penelope::anonymous_namespace{inbox.cpp}::parsed_batch::application_value_json, apply_deletes(), apply_merges(), apply_updates(), arachne::penelope::anonymous_namespace{inbox.cpp}::parsed_batch::batch_id, batch_was_applied(), arachne::penelope::anonymous_namespace{inbox.cpp}::transaction::commit(), create_assertions(), create_entities(), create_facts(), create_names_and_identifiers(), create_sources_and_evidence(), arachne::penelope::anonymous_namespace{inbox.cpp}::statement::execute(), arachne::penelope::anonymous_namespace{inbox.cpp}::database::native(), prevalidate_semantics(), require_clean_foreign_keys(), and arachne::penelope::anonymous_namespace{inbox.cpp}::transaction::transaction().

Here is the call graph for this function:

◆ apply_updates()

void arachne::penelope::anonymous_namespace{inbox.cpp}::apply_updates ( parsed_batch & batch,
sqlite3 *const sql )

Definition at line 3584 of file inbox.cpp.

3584 {
3585 const auto& update = batch.document.at("update");
3586 auto apply_entities = [&](const char* collection, const char* table) {
3587 const auto rows = update.find(collection);
3588 if (rows == update.end() || !rows->is_array()) {
3589 return;
3590 }
3591 for (std::size_t index = 0; index < rows->size(); ++index) {
3592 const auto& row = (*rows)[index];
3594 batch,
3596 "/update/" + std::string(collection), index
3597 ),
3598 row
3599 );
3600 update_row(sql, table, "entity_id", row);
3601 const std::string id = row.at("id").get<std::string>();
3602 batch.affected_entities.emplace(id);
3603 if (std::string_view(table) == "agents"
3604 && row.at("set").contains("agent_type")) {
3605 statement entity(
3606 sql, "UPDATE entities SET entity_type=? WHERE id=?"
3607 );
3608 entity.bind_json_value(1, row.at("set").at("agent_type"));
3609 entity.bind(2, id);
3610 entity.execute();
3611 }
3612 }
3613 };
3614 apply_entities("agents", "agents");
3615 apply_entities("works", "works");
3616 apply_entities("concepts", "concepts");
3617 apply_entities("manifestations", "manifestations");
3618 if (const auto rows = update.find("sources");
3619 rows != update.end() && rows->is_array()) {
3620 for (std::size_t index = 0; index < rows->size(); ++index) {
3621 const auto& row = (*rows)[index];
3623 batch, indexed_path("/update/sources", index), row
3624 );
3625 update_row(sql, "sources", "id", row);
3626 }
3627 }
3628}
void update_row(sqlite3 *const sql, const std::string_view table, const std::string_view id_column, const json &operation)
Definition inbox.cpp:3545

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::execute().

Referenced by apply_one_batch().

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

◆ attach_evidence()

void arachne::penelope::anonymous_namespace{inbox.cpp}::attach_evidence ( const parsed_batch & batch,
sqlite3 *const sql,
const std::string_view table,
const std::int64_t assertion_id,
const json & evidence )

Definition at line 3398 of file inbox.cpp.

3402 {
3403 for (const auto& reference : evidence) {
3404 statement insert(
3405 sql,
3406 "INSERT INTO " + std::string(table)
3407 + "(assertion_id,evidence_id) VALUES(?,?)"
3408 );
3409 insert.bind(1, assertion_id);
3410 insert.bind(
3411 2,
3413 );
3414 insert.execute();
3415 }
3416}
std::int64_t resolve_row_reference(const std::unordered_map< std::string, std::int64_t > &locals, const json &value)
Definition inbox.cpp:3045

◆ batch_was_applied()

bool arachne::penelope::anonymous_namespace{inbox.cpp}::batch_was_applied ( sqlite3 *const sql,
const std::string & batch_id )
nodiscard

Definition at line 5577 of file inbox.cpp.

5579 {
5580 statement query(
5581 sql, "SELECT 1 FROM applied_batches WHERE batch_id=?"
5582 );
5583 query.bind(1, batch_id);
5584 return query.step();
5585}

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step().

Referenced by apply_one_batch().

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

◆ bind_optional()

void arachne::penelope::anonymous_namespace{inbox.cpp}::bind_optional ( statement & insert,
const int index,
const json & row,
const char * key,
const bool serialize = false )

Definition at line 3059 of file inbox.cpp.

3062 {
3063 const auto found = row.find(key);
3064 if (found == row.end()) {
3065 insert.bind_null(index);
3066 } else if (serialize) {
3067 insert.bind(index, found->dump());
3068 } else {
3069 insert.bind_json_value(index, *found);
3070 }
3071}
void bind_json_value(const int index, const json &value)
Definition inbox.cpp:115
void bind(const int index, const std::string_view value)
Definition inbox.cpp:87

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::bind_null().

Here is the call graph for this function:

◆ bootstrap_hint_blocks_if_empty()

void arachne::penelope::anonymous_namespace{inbox.cpp}::bootstrap_hint_blocks_if_empty ( sqlite3 *const sql)

Definition at line 5056 of file inbox.cpp.

5056 {
5057 statement present(
5058 sql, "SELECT 1 FROM merge_hint_block_members LIMIT 1"
5059 );
5060 if (present.step()) {
5061 return;
5062 }
5063 /*
5064 * The one-way v4-to-v5 migration creates derived tables empty. Bootstrap
5065 * them with the same C++ normalization used by routine and full rebuilds
5066 * before the first affected-only refresh, so pre-existing entities can be
5067 * peers of the first new or changed entity.
5068 */
5069 statement clear(sql, "DELETE FROM merge_hint_blocks");
5070 clear.execute();
5071 for (const auto& label : all_hint_labels(sql)) {
5072 insert_hint_blocks(sql, label);
5073 }
5074}
void insert_hint_blocks(sqlite3 *const sql, const hint_label &label)
Definition inbox.cpp:4810
std::vector< hint_label > all_hint_labels(sqlite3 *const sql)
Definition inbox.cpp:4469

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::execute(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step().

Referenced by regenerate_incremental_hints().

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

◆ calculate_hint_components()

hint_components arachne::penelope::anonymous_namespace{inbox.cpp}::calculate_hint_components ( sqlite3 *const sql,
const hint_label & left,
const hint_label & right )
nodiscard

Definition at line 5168 of file inbox.cpp.

5170 {
5171 hint_components result;
5172 if (left.family == "agent") {
5173 const double preferred_name_similarity = maximum_text_similarity(
5175 );
5176 const double alias_similarity
5177 = alternate_text_similarity(left, right);
5178 const auto left_works = query_value_set(
5179 sql, "SELECT work_id FROM credits WHERE agent_id=?", left.id
5180 );
5181 const auto right_works = query_value_set(
5182 sql, "SELECT work_id FROM credits WHERE agent_id=?", right.id
5183 );
5184 const auto left_roles = query_value_set(
5185 sql, "SELECT role FROM credits WHERE agent_id=?", left.id
5186 );
5187 const auto right_roles = query_value_set(
5188 sql, "SELECT role FROM credits WHERE agent_id=?", right.id
5189 );
5190 const double work_score = jaccard(left_works, right_works);
5191 const double role_score = jaccard(left_roles, right_roles);
5192 result.graph = 0.7 * work_score + 0.3 * role_score;
5193 result.signals["preferred_name_similarity"]
5194 = preferred_name_similarity;
5195 result.signals["alias_similarity"] = alias_similarity;
5196 result.signals["shared_work_count"]
5197 = intersection_count(left_works, right_works);
5198 result.signals["shared_work_jaccard"] = work_score;
5199 result.signals["shared_role_count"]
5200 = intersection_count(left_roles, right_roles);
5201 result.signals["shared_role_jaccard"] = role_score;
5202 statement years(
5203 sql,
5204 "SELECT l.birth_year,r.birth_year,l.death_year,r.death_year "
5205 "FROM agents l,agents r WHERE l.entity_id=? AND r.entity_id=?"
5206 );
5207 years.bind(1, left.id);
5208 years.bind(2, right.id);
5209 if (years.step()) {
5210 double compatibility_sum = 0.0;
5211 int comparable = 0;
5212 for (const auto [left_column, right_column, signal_prefix] :
5213 {
5214 std::tuple { 0, 1, "birth_year" },
5215 std::tuple { 2, 3, "death_year" },
5216 }) {
5217 if (!years.is_null(left_column)
5218 && !years.is_null(right_column)) {
5219 ++comparable;
5220 const auto difference = std::abs(
5221 years.integer(left_column)
5222 - years.integer(right_column)
5223 );
5224 const double compatibility = std::max(
5225 0.0,
5226 1.0 - static_cast<double>(difference) / 5.0
5227 );
5228 compatibility_sum += compatibility;
5229 result.signals[
5230 std::string(signal_prefix) + "_difference"
5231 ] = difference;
5232 result.signals[
5233 std::string(signal_prefix) + "_compatible"
5234 ] = difference == 0;
5235 }
5236 }
5237 result.context = comparable == 0
5238 ? 0.0
5239 : compatibility_sum / static_cast<double>(comparable);
5240 result.signals["life_date_compatibility"] = result.context;
5241 }
5242 const auto left_external_ids = query_value_set(
5243 sql,
5244 "SELECT json_array(lower(trim(scheme)),lower(trim(value))) "
5245 "FROM external_ids WHERE entity_id=?",
5246 left.id
5247 );
5248 const auto right_external_ids = query_value_set(
5249 sql,
5250 "SELECT json_array(lower(trim(scheme)),lower(trim(value))) "
5251 "FROM external_ids WHERE entity_id=?",
5252 right.id
5253 );
5254 const auto identifier_collisions
5255 = intersection_count(left_external_ids, right_external_ids);
5256 result.signals["external_identifier_collision_count"]
5257 = identifier_collisions;
5258 result.signals["external_identifier_collision"]
5259 = identifier_collisions != 0U;
5260 } else if (left.family == "work") {
5261 const double preferred_title_similarity = maximum_text_similarity(
5263 );
5264 const double alternate_title_similarity
5265 = alternate_text_similarity(left, right);
5266 const auto left_agents = query_value_set(
5267 sql,
5268 "SELECT agent_id FROM credits WHERE work_id=? "
5269 "AND importance IN('primary','key')",
5270 left.id
5271 );
5272 const auto right_agents = query_value_set(
5273 sql,
5274 "SELECT agent_id FROM credits WHERE work_id=? "
5275 "AND importance IN('primary','key')",
5276 right.id
5277 );
5278 const auto left_concepts = query_value_set(
5279 sql, "SELECT concept_id FROM work_concepts WHERE work_id=?", left.id
5280 );
5281 const auto right_concepts = query_value_set(
5282 sql, "SELECT concept_id FROM work_concepts WHERE work_id=?", right.id
5283 );
5284 const double agent_score = jaccard(left_agents, right_agents);
5285 const double concept_score = jaccard(left_concepts, right_concepts);
5286 result.graph = 0.6 * agent_score + 0.4 * concept_score;
5287 result.signals["preferred_title_similarity"]
5288 = preferred_title_similarity;
5289 result.signals["alternate_title_similarity"]
5290 = alternate_title_similarity;
5291 result.signals["shared_primary_agent_count"]
5292 = intersection_count(left_agents, right_agents);
5293 result.signals["primary_agent_jaccard"] = agent_score;
5294 result.signals["shared_concept_count"]
5295 = intersection_count(left_concepts, right_concepts);
5296 result.signals["concept_set_jaccard"] = concept_score;
5297 statement context(
5298 sql,
5299 "SELECT l.medium=r.medium,"
5300 "CASE WHEN l.year_start IS NULL OR r.year_start IS NULL THEN NULL "
5301 "ELSE abs(l.year_start-r.year_start) END,"
5302 "CASE WHEN l.year_end IS NULL OR r.year_end IS NULL THEN NULL "
5303 "ELSE abs(l.year_end-r.year_end) END "
5304 "FROM works l,works r WHERE l.entity_id=? AND r.entity_id=?"
5305 );
5306 context.bind(1, left.id);
5307 context.bind(2, right.id);
5308 if (context.step()) {
5309 const double medium_score
5310 = context.integer(0) == 0 ? 0.0 : 1.0;
5311 double date_score = 0.0;
5312 int comparable_dates = 0;
5313 for (const auto [column, signal] :
5314 {
5315 std::pair { 1, "start_year_difference" },
5316 std::pair { 2, "end_year_difference" },
5317 }) {
5318 if (context.is_null(column)) {
5319 continue;
5320 }
5321 ++comparable_dates;
5322 const auto difference = context.integer(column);
5323 date_score += std::max(
5324 0.0, 1.0 - static_cast<double>(difference) / 10.0
5325 );
5326 result.signals[signal] = difference;
5327 }
5328 if (comparable_dates != 0) {
5329 date_score /= static_cast<double>(comparable_dates);
5330 }
5331 result.context = 0.6 * medium_score + 0.4 * date_score;
5332 result.signals["medium_equal"] = context.integer(0) != 0;
5333 result.signals["date_compatibility"] = date_score;
5334 }
5335 } else if (left.family == "concept") {
5336 result.signals["slug_similarity"] = maximum_text_similarity(
5337 { left.fingerprint }, { right.fingerprint }
5338 );
5339 result.signals["token_fingerprint_equal"]
5340 = left.fingerprint == right.fingerprint;
5341 const auto left_works = query_value_set(
5342 sql, "SELECT work_id FROM work_concepts WHERE concept_id=?", left.id
5343 );
5344 const auto right_works = query_value_set(
5345 sql, "SELECT work_id FROM work_concepts WHERE concept_id=?", right.id
5346 );
5347 const auto left_neighbors = query_value_set(
5348 sql,
5349 "SELECT object_concept_id||':'||relation_type "
5350 "FROM concept_relations WHERE subject_concept_id=?1 "
5351 "UNION SELECT subject_concept_id||':'||relation_type "
5352 "FROM concept_relations WHERE object_concept_id=?1",
5353 left.id
5354 );
5355 statement right_neighbors_query(
5356 sql,
5357 "SELECT object_concept_id||':'||relation_type "
5358 "FROM concept_relations WHERE subject_concept_id=?1 "
5359 "UNION SELECT subject_concept_id||':'||relation_type "
5360 "FROM concept_relations WHERE object_concept_id=?1"
5361 );
5362 right_neighbors_query.bind(1, right.id);
5363 std::set<std::string, std::less<>> right_neighbors;
5364 while (right_neighbors_query.step()) {
5365 right_neighbors.emplace(right_neighbors_query.text(0));
5366 }
5367 const double work_score = jaccard(left_works, right_works);
5368 const double neighbor_score
5369 = jaccard(left_neighbors, right_neighbors);
5370 result.graph = 0.7 * work_score + 0.3 * neighbor_score;
5371 result.signals["shared_work_count"]
5372 = intersection_count(left_works, right_works);
5373 result.signals["shared_work_jaccard"] = work_score;
5374 result.signals["shared_relation_neighbor_count"]
5375 = intersection_count(left_neighbors, right_neighbors);
5376 result.signals["relation_neighborhood_jaccard"]
5377 = neighbor_score;
5378 statement type(
5379 sql,
5380 "SELECT l.concept_type=r.concept_type FROM concepts l,concepts r "
5381 "WHERE l.entity_id=? AND r.entity_id=?"
5382 );
5383 type.bind(1, left.id);
5384 type.bind(2, right.id);
5385 if (type.step()) {
5386 result.context = type.integer(0) == 0 ? 0.0 : 1.0;
5387 result.signals["concept_type_equal"] = type.integer(0) != 0;
5388 }
5389 }
5390 return result;
5391}
double jaccard(const std::set< std::string, std::less<> > &left, const std::set< std::string, std::less<> > &right)
Definition inbox.cpp:5089
double alternate_text_similarity(const hint_label &left, const hint_label &right)
Definition inbox.cpp:5143
std::set< std::string, std::less<> > query_value_set(sqlite3 *const sql, const std::string_view query_text, const std::string &id)
Definition inbox.cpp:5076
std::size_t intersection_count(const std::set< std::string, std::less<> > &left, const std::set< std::string, std::less<> > &right)
Definition inbox.cpp:5105

References alternate_text_similarity(), arachne::penelope::anonymous_namespace{inbox.cpp}::hint_components::context, arachne::penelope::anonymous_namespace{inbox.cpp}::hint_label::family, arachne::penelope::anonymous_namespace{inbox.cpp}::hint_label::fingerprint, arachne::penelope::anonymous_namespace{inbox.cpp}::hint_components::graph, arachne::penelope::anonymous_namespace{inbox.cpp}::hint_label::id, arachne::penelope::anonymous_namespace{inbox.cpp}::statement::integer(), arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::text().

Referenced by upsert_hint().

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

◆ check_keys()

void arachne::penelope::anonymous_namespace{inbox.cpp}::check_keys ( parsed_batch & batch,
const json & object,
const std::string & path,
const std::set< std::string, std::less<> > & allowed,
const std::set< std::string, std::less<> > & required = {} )

Definition at line 680 of file inbox.cpp.

683 {}
684) {
685 if (!object.is_object()) {
686 add_issue(
687 batch, "type_mismatch", path, "value must be an object", &object
688 );
689 return;
690 }
691 for (const auto& [key, value] : object.items()) {
692 if (!allowed.contains(key)) {
693 add_issue(
694 batch, "unknown_field",
695 path + "/" + pointer_escape(key), "field is not allowed", &value
696 );
697 }
698 }
699 for (const auto& key : required) {
700 if (!object.contains(key)) {
701 add_issue(
702 batch, "required_field", path + "/" + pointer_escape(key),
703 "required field is missing"
704 );
705 }
706 }
707}
void add_issue(parsed_batch &batch, const std::string_view code, const std::string_view path, const std::string_view message, const json *const value=nullptr)
Definition inbox.cpp:587

◆ contextual_block_key()

std::string arachne::penelope::anonymous_namespace{inbox.cpp}::contextual_block_key ( const std::string_view context,
const std::string_view value )
nodiscard

Definition at line 4732 of file inbox.cpp.

4734 {
4735 return std::string(context) + "\n" + std::string(value);
4736}

◆ create_assertions()

void arachne::penelope::anonymous_namespace{inbox.cpp}::create_assertions ( parsed_batch & batch,
sqlite3 *const sql )

Definition at line 3418 of file inbox.cpp.

3418 {
3419 const auto& create = batch.document.at("create");
3420 if (const auto rows = create.find("work_concepts");
3421 rows != create.end() && rows->is_array()) {
3422 for (std::size_t index = 0; index < rows->size(); ++index) {
3423 const auto& row = (*rows)[index];
3425 batch, indexed_path("/create/work_concepts", index), row
3426 );
3427 const std::int64_t id = batch.assertion_local_ids.at(
3428 row.at("local_id").get<std::string>()
3429 );
3430 statement insert(
3431 sql,
3432 "INSERT INTO work_concepts("
3433 "id,work_id,concept_id,relation_type,centrality,"
3434 "historical_role,confidence) VALUES(?,?,?,?,?,?,?)"
3435 );
3436 insert.bind(1, id);
3437 insert.bind(2, resolve_entity(batch, row.at("work_id")));
3438 insert.bind(3, resolve_entity(batch, row.at("concept_id")));
3439 insert.bind_json_value(4, row.at("relation_type"));
3440 insert.bind_json_value(5, row.at("centrality"));
3441 bind_optional(insert, 6, row, "historical_role");
3442 bind_optional(insert, 7, row, "confidence");
3443 insert.execute();
3445 batch, sql, "work_concept_evidence", id, row.at("evidence")
3446 );
3447 batch.affected_entities.emplace(
3448 resolve_entity(batch, row.at("work_id"))
3449 );
3450 batch.affected_entities.emplace(
3451 resolve_entity(batch, row.at("concept_id"))
3452 );
3453 }
3454 }
3455 if (const auto rows = create.find("concept_relations");
3456 rows != create.end() && rows->is_array()) {
3457 for (std::size_t index = 0; index < rows->size(); ++index) {
3458 const auto& row = (*rows)[index];
3460 batch, indexed_path("/create/concept_relations", index), row
3461 );
3462 const std::int64_t id = batch.assertion_local_ids.at(
3463 row.at("local_id").get<std::string>()
3464 );
3465 statement insert(
3466 sql,
3467 "INSERT INTO concept_relations("
3468 "id,subject_concept_id,relation_type,object_concept_id,"
3469 "strength,from_year,to_year,region_code,confidence)"
3470 " VALUES(?,?,?,?,?,?,?,?,?)"
3471 );
3472 insert.bind(1, id);
3473 insert.bind(
3474 2, resolve_entity(batch, row.at("subject_concept_id"))
3475 );
3476 insert.bind_json_value(3, row.at("relation_type"));
3477 insert.bind(
3478 4, resolve_entity(batch, row.at("object_concept_id"))
3479 );
3480 bind_optional(insert, 5, row, "strength");
3481 bind_optional(insert, 6, row, "from_year");
3482 bind_optional(insert, 7, row, "to_year");
3483 bind_optional(insert, 8, row, "region_code");
3484 bind_optional(insert, 9, row, "confidence");
3485 insert.execute();
3487 batch, sql, "concept_relation_evidence", id,
3488 row.at("evidence")
3489 );
3490 batch.affected_entities.emplace(
3491 resolve_entity(batch, row.at("subject_concept_id"))
3492 );
3493 batch.affected_entities.emplace(
3494 resolve_entity(batch, row.at("object_concept_id"))
3495 );
3496 }
3497 }
3498 if (const auto rows = create.find("parent_guide_assertions");
3499 rows != create.end() && rows->is_array()) {
3500 for (std::size_t index = 0; index < rows->size(); ++index) {
3501 const auto& row = (*rows)[index];
3503 batch,
3504 indexed_path("/create/parent_guide_assertions", index), row
3505 );
3506 const std::int64_t id = batch.assertion_local_ids.at(
3507 row.at("local_id").get<std::string>()
3508 );
3509 statement insert(
3510 sql,
3511 "INSERT INTO parent_guide_assertions("
3512 "id,work_id,concept_id,category,intensity,explicitness,"
3513 "frequency,centrality,realism,spoiler_level,confidence)"
3514 " VALUES(?,?,?,?,?,?,?,?,?,?,?)"
3515 );
3516 insert.bind(1, id);
3517 insert.bind(2, resolve_entity(batch, row.at("work_id")));
3518 insert.bind(3, resolve_entity(batch, row.at("concept_id")));
3519 insert.bind_json_value(4, row.at("category"));
3520 insert.bind_json_value(5, row.at("intensity"));
3521 insert.bind_json_value(6, row.at("explicitness"));
3522 insert.bind_json_value(7, row.at("frequency"));
3523 insert.bind_json_value(8, row.at("centrality"));
3524 insert.bind_json_value(9, row.at("realism"));
3525 insert.bind_json_value(10, row.at("spoiler_level"));
3526 bind_optional(insert, 11, row, "confidence");
3527 insert.execute();
3529 batch, sql, "parent_guide_evidence", id, row.at("evidence")
3530 );
3531 batch.affected_entities.emplace(
3532 resolve_entity(batch, row.at("work_id"))
3533 );
3534 batch.affected_entities.emplace(
3535 resolve_entity(batch, row.at("concept_id"))
3536 );
3537 }
3538 }
3539}
std::string resolve_entity(const parsed_batch &batch, const json &value)
Definition inbox.cpp:3037
void bind_optional(statement &insert, const int index, const json &row, const char *key, const bool serialize=false)
Definition inbox.cpp:3059
void attach_evidence(const parsed_batch &batch, sqlite3 *const sql, const std::string_view table, const std::int64_t assertion_id, const json &evidence)
Definition inbox.cpp:3398

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::bind(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::execute().

Referenced by apply_one_batch().

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

◆ create_entities()

void arachne::penelope::anonymous_namespace{inbox.cpp}::create_entities ( parsed_batch & batch,
sqlite3 *const sql )

Definition at line 3073 of file inbox.cpp.

3073 {
3074 const auto& create = batch.document.at("create");
3075 auto insert_entity = [&](const json& row, const std::string_view type) {
3076 const std::string local = row.at("local_id").get<std::string>();
3077 const std::string id = batch.resolved_entity_ids.at(local);
3078 statement entity(
3079 sql, "INSERT INTO entities(id, entity_type) VALUES (?, ?)"
3080 );
3081 entity.bind(1, id);
3082 entity.bind(2, type);
3083 entity.execute();
3084 batch.affected_entities.emplace(id);
3085 return id;
3086 };
3087
3088 if (const auto rows = create.find("agents");
3089 rows != create.end() && rows->is_array()) {
3090 for (std::size_t index = 0; index < rows->size(); ++index) {
3091 const auto& row = (*rows)[index];
3093 batch, indexed_path("/create/agents", index), row
3094 );
3095 const std::string agent_type = row.at("agent_type");
3096 const std::string id = insert_entity(row, agent_type);
3097 statement insert(
3098 sql,
3099 "INSERT INTO agents(entity_id,agent_type,birth_year,death_year)"
3100 " VALUES(?,?,?,?)"
3101 );
3102 insert.bind(1, id);
3103 insert.bind(2, agent_type);
3104 bind_optional(insert, 3, row, "birth_year");
3105 bind_optional(insert, 4, row, "death_year");
3106 insert.execute();
3107 }
3108 }
3109 if (const auto rows = create.find("works");
3110 rows != create.end() && rows->is_array()) {
3111 for (std::size_t index = 0; index < rows->size(); ++index) {
3112 const auto& row = (*rows)[index];
3114 batch, indexed_path("/create/works", index), row
3115 );
3116 const std::string id = insert_entity(row, "work");
3117 statement insert(
3118 sql,
3119 "INSERT INTO works("
3120 "entity_id,medium,year_start,year_end,date_precision,"
3121 "date_start_text,date_end_text,date_qualifier,language_code,"
3122 "country_code,production_info_json)"
3123 " VALUES(?,?,?,?,?,?,?,?,?,?,?)"
3124 );
3125 insert.bind(1, id);
3126 insert.bind_json_value(2, row.at("medium"));
3127 bind_optional(insert, 3, row, "year_start");
3128 bind_optional(insert, 4, row, "year_end");
3129 bind_optional(insert, 5, row, "date_precision");
3130 bind_optional(insert, 6, row, "date_start_text");
3131 bind_optional(insert, 7, row, "date_end_text");
3132 bind_optional(insert, 8, row, "date_qualifier");
3133 bind_optional(insert, 9, row, "language_code");
3134 bind_optional(insert, 10, row, "country_code");
3135 bind_optional(insert, 11, row, "production_info_json");
3136 insert.execute();
3137 }
3138 }
3139 if (const auto rows = create.find("concepts");
3140 rows != create.end() && rows->is_array()) {
3141 for (std::size_t index = 0; index < rows->size(); ++index) {
3142 const auto& row = (*rows)[index];
3144 batch, indexed_path("/create/concepts", index), row
3145 );
3146 const std::string id = insert_entity(row, "concept");
3147 statement insert(
3148 sql,
3149 "INSERT INTO concepts(entity_id,concept_type,slug)"
3150 " VALUES(?,?,?)"
3151 );
3152 insert.bind(1, id);
3153 insert.bind_json_value(2, row.at("concept_type"));
3154 insert.bind_json_value(3, row.at("slug"));
3155 insert.execute();
3156 }
3157 }
3158 if (const auto rows = create.find("manifestations");
3159 rows != create.end() && rows->is_array()) {
3160 for (std::size_t index = 0; index < rows->size(); ++index) {
3161 const auto& row = (*rows)[index];
3163 batch, indexed_path("/create/manifestations", index), row
3164 );
3165 const std::string id = insert_entity(row, "manifestation");
3166 statement insert(
3167 sql,
3168 "INSERT INTO manifestations("
3169 "entity_id,work_id,manifestation_type,release_year,"
3170 "region_code,language_code,label) VALUES(?,?,?,?,?,?,?)"
3171 );
3172 insert.bind(1, id);
3173 insert.bind(2, resolve_entity(batch, row.at("work_id")));
3174 insert.bind_json_value(3, row.at("manifestation_type"));
3175 bind_optional(insert, 4, row, "release_year");
3176 bind_optional(insert, 5, row, "region_code");
3177 bind_optional(insert, 6, row, "language_code");
3178 insert.bind_json_value(7, row.at("label"));
3179 insert.execute();
3180 }
3181 }
3182}

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::execute().

Referenced by apply_one_batch().

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

◆ create_facts()

void arachne::penelope::anonymous_namespace{inbox.cpp}::create_facts ( parsed_batch & batch,
sqlite3 *const sql )

Definition at line 3310 of file inbox.cpp.

3310 {
3311 const auto& create = batch.document.at("create");
3312 if (const auto rows = create.find("credits");
3313 rows != create.end() && rows->is_array()) {
3314 for (std::size_t index = 0; index < rows->size(); ++index) {
3315 const auto& row = (*rows)[index];
3317 batch, indexed_path("/create/credits", index), row
3318 );
3319 statement insert(
3320 sql,
3321 "INSERT INTO credits("
3322 "work_id,agent_id,role,credit_order,importance,credited_as)"
3323 " VALUES(?,?,?,?,?,?)"
3324 );
3325 insert.bind(1, resolve_entity(batch, row.at("work_id")));
3326 insert.bind(2, resolve_entity(batch, row.at("agent_id")));
3327 insert.bind_json_value(3, row.at("role"));
3328 bind_optional(insert, 4, row, "credit_order");
3329 insert.bind_json_value(5, row.at("importance"));
3330 bind_optional(insert, 6, row, "credited_as");
3331 insert.execute();
3332 batch.affected_entities.emplace(
3333 resolve_entity(batch, row.at("work_id"))
3334 );
3335 batch.affected_entities.emplace(
3336 resolve_entity(batch, row.at("agent_id"))
3337 );
3338 }
3339 }
3340 if (const auto rows = create.find("measurements");
3341 rows != create.end() && rows->is_array()) {
3342 for (std::size_t index = 0; index < rows->size(); ++index) {
3343 const auto& row = (*rows)[index];
3345 batch, indexed_path("/create/measurements", index), row
3346 );
3347 statement insert(
3348 sql,
3349 "INSERT INTO measurements("
3350 "entity_id,measurement_type,value,unit,qualifier)"
3351 " VALUES(?,?,?,?,?)"
3352 );
3353 insert.bind(1, resolve_entity(batch, row.at("entity_id")));
3354 insert.bind_json_value(2, row.at("measurement_type"));
3355 insert.bind_json_value(3, row.at("value"));
3356 insert.bind_json_value(4, row.at("unit"));
3357 bind_optional(insert, 5, row, "qualifier");
3358 insert.execute();
3359 batch.affected_entities.emplace(
3360 resolve_entity(batch, row.at("entity_id"))
3361 );
3362 }
3363 }
3364 if (const auto rows = create.find("financial_facts");
3365 rows != create.end() && rows->is_array()) {
3366 for (std::size_t index = 0; index < rows->size(); ++index) {
3367 const auto& row = (*rows)[index];
3369 batch, indexed_path("/create/financial_facts", index), row
3370 );
3371 statement insert(
3372 sql,
3373 "INSERT INTO financial_facts("
3374 "work_id,fact_type,amount_min,amount_max,currency_code,"
3375 "value_year,is_estimate,confidence) VALUES(?,?,?,?,?,?,?,?)"
3376 );
3377 insert.bind(1, resolve_entity(batch, row.at("work_id")));
3378 insert.bind_json_value(2, row.at("fact_type"));
3379 insert.bind_json_value(3, row.at("amount_min"));
3380 bind_optional(insert, 4, row, "amount_max");
3381 insert.bind_json_value(5, row.at("currency_code"));
3382 bind_optional(insert, 6, row, "value_year");
3383 insert.bind(
3384 7,
3385 static_cast<std::int64_t>(
3386 row.at("is_estimate").get<bool>() ? 1 : 0
3387 )
3388 );
3389 bind_optional(insert, 8, row, "confidence");
3390 insert.execute();
3391 batch.affected_entities.emplace(
3392 resolve_entity(batch, row.at("work_id"))
3393 );
3394 }
3395 }
3396}

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::bind(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::execute().

Referenced by apply_one_batch().

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

◆ create_names_and_identifiers()

void arachne::penelope::anonymous_namespace{inbox.cpp}::create_names_and_identifiers ( parsed_batch & batch,
sqlite3 *const sql )

Definition at line 3184 of file inbox.cpp.

3186 {
3187 const auto& create = batch.document.at("create");
3188 if (const auto rows = create.find("names");
3189 rows != create.end() && rows->is_array()) {
3190 for (std::size_t index = 0; index < rows->size(); ++index) {
3191 const auto& row = (*rows)[index];
3193 batch, indexed_path("/create/names", index), row
3194 );
3195 statement insert(
3196 sql,
3197 "INSERT INTO names("
3198 "entity_id,name_type,language_code,script_code,value,"
3199 "is_preferred) VALUES(?,?,?,?,?,?)"
3200 );
3201 insert.bind(1, resolve_entity(batch, row.at("entity_id")));
3202 insert.bind_json_value(2, row.at("name_type"));
3203 bind_optional(insert, 3, row, "language_code");
3204 bind_optional(insert, 4, row, "script_code");
3205 insert.bind_json_value(5, row.at("value"));
3206 insert.bind(
3207 6,
3208 static_cast<std::int64_t>(
3209 row.at("is_preferred").get<bool>() ? 1 : 0
3210 )
3211 );
3212 insert.execute();
3213 batch.affected_entities.emplace(
3214 resolve_entity(batch, row.at("entity_id"))
3215 );
3216 }
3217 }
3218 if (const auto rows = create.find("external_ids");
3219 rows != create.end() && rows->is_array()) {
3220 for (std::size_t index = 0; index < rows->size(); ++index) {
3221 const auto& row = (*rows)[index];
3223 batch, indexed_path("/create/external_ids", index), row
3224 );
3225 statement insert(
3226 sql,
3227 "INSERT INTO external_ids("
3228 "entity_id,scheme,value,canonical_url) VALUES(?,?,?,?)"
3229 );
3230 insert.bind(1, resolve_entity(batch, row.at("entity_id")));
3231 insert.bind_json_value(2, row.at("scheme"));
3232 insert.bind_json_value(3, row.at("value"));
3233 bind_optional(insert, 4, row, "canonical_url");
3234 insert.execute();
3235 batch.affected_entities.emplace(
3236 resolve_entity(batch, row.at("entity_id"))
3237 );
3238 }
3239 }
3240}

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::bind(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::execute().

Referenced by apply_one_batch().

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

◆ create_sources_and_evidence()

void arachne::penelope::anonymous_namespace{inbox.cpp}::create_sources_and_evidence ( parsed_batch & batch,
sqlite3 *const sql )

Definition at line 3242 of file inbox.cpp.

3244 {
3245 const auto& create = batch.document.at("create");
3246 if (const auto rows = create.find("sources");
3247 rows != create.end() && rows->is_array()) {
3248 for (std::size_t index = 0; index < rows->size(); ++index) {
3249 const auto& row = (*rows)[index];
3251 batch, indexed_path("/create/sources", index), row
3252 );
3253 const std::int64_t id = batch.source_local_ids.at(
3254 row.at("local_id").get<std::string>()
3255 );
3256 statement insert(
3257 sql,
3258 "INSERT INTO sources("
3259 "id,source_type,title,bibliography_text,author_text,publisher,"
3260 "publication_date,url,doi,isbn,language_code)"
3261 " VALUES(?,?,?,?,?,?,?,?,?,?,?)"
3262 );
3263 insert.bind(1, id);
3264 insert.bind_json_value(2, row.at("source_type"));
3265 bind_optional(insert, 3, row, "title");
3266 bind_optional(insert, 4, row, "bibliography_text");
3267 bind_optional(insert, 5, row, "author_text");
3268 bind_optional(insert, 6, row, "publisher");
3269 bind_optional(insert, 7, row, "publication_date");
3270 bind_optional(insert, 8, row, "url");
3271 bind_optional(insert, 9, row, "doi");
3272 bind_optional(insert, 10, row, "isbn");
3273 bind_optional(insert, 11, row, "language_code");
3274 insert.execute();
3275 }
3276 }
3277 if (const auto rows = create.find("evidence");
3278 rows != create.end() && rows->is_array()) {
3279 for (std::size_t index = 0; index < rows->size(); ++index) {
3280 const auto& row = (*rows)[index];
3282 batch, indexed_path("/create/evidence", index), row
3283 );
3284 const std::int64_t id = batch.evidence_local_ids.at(
3285 row.at("local_id").get<std::string>()
3286 );
3287 statement insert(
3288 sql,
3289 "INSERT INTO evidence("
3290 "id,source_id,exact_quote,quote_language,quote_translation,"
3291 "locator_json,stance) VALUES(?,?,?,?,?,?,?)"
3292 );
3293 insert.bind(1, id);
3294 insert.bind(
3295 2,
3297 batch.source_local_ids, row.at("source_id")
3298 )
3299 );
3300 insert.bind_json_value(3, row.at("exact_quote"));
3301 bind_optional(insert, 4, row, "quote_language");
3302 bind_optional(insert, 5, row, "quote_translation");
3303 bind_optional(insert, 6, row, "locator_json");
3304 insert.bind_json_value(7, row.at("stance"));
3305 insert.execute();
3306 }
3307 }
3308}

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::bind(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::execute().

Referenced by apply_one_batch().

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

◆ delete_merged_entity()

void arachne::penelope::anonymous_namespace{inbox.cpp}::delete_merged_entity ( sqlite3 *const sql,
const std::string & member )

Definition at line 4182 of file inbox.cpp.

4184 {
4185 statement remove(sql, "DELETE FROM entities WHERE id=?");
4186 remove.bind(1, member);
4187 remove.execute();
4188 if (sqlite3_changes(sql) != 1) {
4189 throw database_error("merged entity disappeared before deletion");
4190 }
4191}

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::execute().

Here is the call graph for this function:

◆ ensure_product_database()

void arachne::penelope::anonymous_namespace{inbox.cpp}::ensure_product_database ( const fs::path & repository_root,
const fs::path & path )

Definition at line 5734 of file inbox.cpp.

5736 {
5737 struct stat state {};
5738 if (::lstat(path.c_str(), &state) == 0) {
5740 return;
5741 }
5742 if (errno != ENOENT) {
5743 throw inbox_error(
5744 "cannot inspect product database: " + path.string()
5745 );
5746 }
5747 require_real_directory(path.parent_path(), "database directory");
5748 sqlite3* raw = nullptr;
5749 const std::string native = path.string();
5750 if (sqlite3_open_v2(
5751 native.c_str(), &raw,
5752 SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_EXCLUSIVE,
5753 nullptr
5754 )
5755 != SQLITE_OK) {
5756 const std::string message = sqlite_message(raw, "create product database");
5757 if (raw != nullptr) {
5758 sqlite3_close(raw);
5759 }
5760 throw inbox_error(message);
5761 }
5762 try {
5763 char* error = nullptr;
5764 const std::string schema = read_schema(
5765 repository_root / "schema" / "product_v5.sql"
5766 );
5767 if (sqlite3_exec(
5768 raw, schema.c_str(), nullptr, nullptr, &error
5769 )
5770 != SQLITE_OK) {
5771 const std::string message
5772 = error == nullptr ? sqlite3_errmsg(raw) : error;
5773 sqlite3_free(error);
5774 throw inbox_error("cannot initialize product database: " + message);
5775 }
5776 sqlite3_close(raw);
5777 } catch (...) {
5778 sqlite3_close(raw);
5779 std::error_code ignored;
5780 fs::remove(path, ignored);
5781 throw;
5782 }
5784}
void require_real_database_file(const fs::path &path)
Definition inbox.cpp:5707
std::string read_schema(const fs::path &path)
Definition inbox.cpp:5721
void require_real_directory(const fs::path &path, const std::string_view description)
Definition inbox.cpp:5696
std::string sqlite_message(sqlite3 *const value, const std::string_view operation)
Definition inbox.cpp:56

References require_real_database_file().

Referenced by run_inbox().

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

◆ entity_family()

std::string arachne::penelope::anonymous_namespace{inbox.cpp}::entity_family ( sqlite3 *const database_value,
const std::string & id )
nodiscard

Definition at line 2334 of file inbox.cpp.

2336 {
2337 statement query(
2338 database_value,
2339 "SELECT entity_type FROM entities WHERE id = ?"
2340 );
2341 query.bind(1, id);
2342 if (!query.step()) {
2343 return {};
2344 }
2345 const std::string type = query.text(0);
2346 if (type == "person" || type == "organization" || type == "group") {
2347 return "agent";
2348 }
2349 return type;
2350}

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::text().

Here is the call graph for this function:

◆ formatted_entity_id()

std::string arachne::penelope::anonymous_namespace{inbox.cpp}::formatted_entity_id ( const std::string_view family,
const std::int64_t sequence )
nodiscard

Definition at line 2904 of file inbox.cpp.

2906 {
2907 std::string prefix;
2908 std::size_t width = 6U;
2909 if (family == "agent") {
2910 prefix = "agent-";
2911 } else if (family == "work") {
2912 prefix = "work-";
2913 } else if (family == "concept") {
2914 prefix = "concept-";
2915 } else if (family == "manifestation") {
2916 prefix = "manifestation-";
2917 } else {
2918 throw database_error("cannot allocate ID for non-entity family");
2919 }
2920 std::string digits = std::to_string(sequence);
2921 if (digits.size() < width) {
2922 digits.insert(0U, width - digits.size(), '0');
2923 }
2924 return prefix + digits;
2925}

◆ hint_label_for_id()

std::optional< hint_label > arachne::penelope::anonymous_namespace{inbox.cpp}::hint_label_for_id ( sqlite3 *const sql,
const std::string & id )
nodiscard

Definition at line 4554 of file inbox.cpp.

4556 {
4557 statement query(
4558 sql,
4559 "SELECT e.id,"
4560 "CASE WHEN e.entity_type IN('person','organization','group') "
4561 "THEN 'agent' ELSE e.entity_type END,"
4562 "CASE WHEN e.entity_type='concept' THEN COALESCE(c.slug,'') "
4563 "ELSE COALESCE(("
4564 " SELECT n.value FROM names n WHERE n.entity_id=e.id "
4565 " ORDER BY n.is_preferred DESC,n.id LIMIT 1"
4566 "),'') END "
4567 "FROM entities e LEFT JOIN concepts c ON c.entity_id=e.id "
4568 "WHERE e.id=? AND e.entity_type IN("
4569 "'person','organization','group','work','concept')"
4570 );
4571 query.bind(1, id);
4572 if (!query.step()) {
4573 return std::nullopt;
4574 }
4575 hint_label result {
4576 .id = query.text(0),
4577 .family = query.text(1),
4578 .label = query.text(2),
4579 .fingerprint = {},
4580 .fingerprints = {},
4581 .preferred_fingerprints = {},
4582 .alternate_fingerprints = {},
4583 };
4584 result.fingerprint = normalized_label(result.label);
4585 if (!result.fingerprint.empty()) {
4586 result.fingerprints.push_back(result.fingerprint);
4587 if (result.family == "concept") {
4588 result.preferred_fingerprints.push_back(result.fingerprint);
4589 }
4590 }
4591
4592 statement names(
4593 sql,
4594 "SELECT value,is_preferred FROM names WHERE entity_id=? ORDER BY id"
4595 );
4596 names.bind(1, id);
4597 while (names.step()) {
4598 std::string normalized = normalized_label(names.text(0));
4599 if (normalized.empty()) {
4600 continue;
4601 }
4602 result.fingerprints.push_back(normalized);
4603 if (result.family != "concept" && names.integer(1) != 0) {
4604 result.preferred_fingerprints.push_back(normalized);
4605 } else {
4606 result.alternate_fingerprints.push_back(std::move(normalized));
4607 }
4608 }
4609 const auto deduplicate = [](std::vector<std::string>& values) {
4610 std::ranges::sort(values);
4611 values.erase(std::unique(values.begin(), values.end()), values.end());
4612 };
4613 deduplicate(result.fingerprints);
4614 deduplicate(result.preferred_fingerprints);
4615 deduplicate(result.alternate_fingerprints);
4616 if (result.fingerprint.empty() && !result.fingerprints.empty()) {
4617 result.fingerprint = result.fingerprints.front();
4618 }
4619 if (result.preferred_fingerprints.empty()
4620 && !result.fingerprint.empty()) {
4621 result.preferred_fingerprints.push_back(result.fingerprint);
4622 }
4623 std::erase(result.alternate_fingerprints, result.fingerprint);
4624 return result;
4625}

References arachne::penelope::anonymous_namespace{inbox.cpp}::hint_label::family, arachne::penelope::anonymous_namespace{inbox.cpp}::hint_label::fingerprint, arachne::penelope::anonymous_namespace{inbox.cpp}::statement::integer(), arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::text().

Here is the call graph for this function:

◆ identity_of()

file_identity arachne::penelope::anonymous_namespace{inbox.cpp}::identity_of ( const struct stat & state)
nodiscard

Definition at line 458 of file inbox.cpp.

458 {
459 return {
460 .device = state.st_dev,
461 .inode = state.st_ino,
462 .size = state.st_size,
463#if defined(__APPLE__)
464 .modified = state.st_mtimespec,
465#else
466 .modified = state.st_mtim,
467#endif
468 };
469}

References arachne::penelope::anonymous_namespace{inbox.cpp}::file_identity::device, arachne::penelope::anonymous_namespace{inbox.cpp}::file_identity::inode, arachne::penelope::anonymous_namespace{inbox.cpp}::file_identity::modified, and arachne::penelope::anonymous_namespace{inbox.cpp}::file_identity::size.

Referenced by read_batch_file(), and snapshot_still_current().

Here is the caller graph for this function:

◆ incrementally_blocked_pairs()

std::set< hint_pair > arachne::penelope::anonymous_namespace{inbox.cpp}::incrementally_blocked_pairs ( sqlite3 *const sql,
const std::set< std::string, std::less<> > & affected )
nodiscard

Definition at line 4958 of file inbox.cpp.

4961 {
4962 hint_candidate_map candidates;
4963 statement blocks(
4964 sql,
4965 "SELECT b.id,b.block_type,b.block_key "
4966 "FROM merge_hint_block_members m "
4967 "JOIN merge_hint_blocks b ON b.id=m.block_id "
4968 "WHERE m.entity_id=? ORDER BY b.id"
4969 );
4970 statement peers(
4971 sql,
4972 "SELECT entity_id FROM merge_hint_block_members "
4973 "WHERE block_id=? ORDER BY entity_id LIMIT ?"
4974 );
4975 for (const auto& id : affected) {
4976 sqlite3_reset(blocks.native());
4977 sqlite3_clear_bindings(blocks.native());
4978 blocks.bind(1, id);
4979 while (blocks.step()) {
4980 const std::int64_t block_id = blocks.integer(0);
4981 const std::string block_type = blocks.text(1);
4982 const std::string block_key = blocks.text(2);
4983 const std::size_t maximum
4984 = maximum_hint_block_size(block_type);
4985 sqlite3_reset(peers.native());
4986 sqlite3_clear_bindings(peers.native());
4987 peers.bind(1, block_id);
4988 peers.bind(2, static_cast<std::int64_t>(maximum + 1U));
4989 std::vector<std::string> members;
4990 while (peers.step()) {
4991 members.push_back(peers.text(0));
4992 }
4993 if (members.size() > maximum) {
4994 continue;
4995 }
4996 for (const auto& peer : members) {
4998 candidates, id, peer, block_type, block_key
4999 );
5000 }
5001 }
5002 }
5003 return reviewable_blocked_pairs(candidates);
5004}

◆ indexed_path()

std::string arachne::penelope::anonymous_namespace{inbox.cpp}::indexed_path ( const std::string_view parent,
const std::size_t index )
nodiscard

Definition at line 896 of file inbox.cpp.

898 {
899 return std::string(parent) + "/" + std::to_string(index);
900}

◆ initial_allocation_state()

local_id_allocation_state arachne::penelope::anonymous_namespace{inbox.cpp}::initial_allocation_state ( database & product)
nodiscard

Definition at line 2935 of file inbox.cpp.

2935 {
2936 return {
2937 .entity_sequences = {
2938 { "agent", maximum_entity_suffix(product.native(), "agent-") },
2939 { "work", maximum_entity_suffix(product.native(), "work-") },
2940 { "concept", maximum_entity_suffix(product.native(), "concept-") },
2941 { "manifestation",
2942 maximum_entity_suffix(product.native(), "manifestation-") },
2943 },
2944 .source_sequence = maximum_row_id(product.native(), "sources"),
2945 .evidence_sequence = maximum_row_id(product.native(), "evidence"),
2946 .assertion_sequences = {
2947 { "work_concepts",
2948 maximum_row_id(product.native(), "work_concepts") },
2949 { "concept_relations",
2950 maximum_row_id(product.native(), "concept_relations") },
2951 { "parent_guide_assertions",
2952 maximum_row_id(product.native(), "parent_guide_assertions") },
2953 },
2954 };
2955}
std::int64_t maximum_row_id(sqlite3 *const sql, const std::string_view table)
Definition inbox.cpp:2874
std::int64_t maximum_entity_suffix(sqlite3 *const sql, const std::string_view prefix)
Definition inbox.cpp:2886

Referenced by run_inbox().

Here is the caller graph for this function:

◆ insert_hint_block()

void arachne::penelope::anonymous_namespace{inbox.cpp}::insert_hint_block ( statement & block,
statement & member,
const hint_label & label,
const std::string_view block_type,
const std::string & block_key )

Definition at line 4786 of file inbox.cpp.

4789 {
4790 if (block_key.empty()) {
4791 return;
4792 }
4793 sqlite3_reset(block.native());
4794 sqlite3_clear_bindings(block.native());
4795 block.bind(1, label.family);
4796 block.bind(2, block_type);
4797 block.bind(3, block_key);
4798 if (!block.step()) {
4799 throw database_error("cannot resolve merge-hint block key");
4800 }
4801 const std::int64_t block_id = block.integer(0);
4802
4803 sqlite3_reset(member.native());
4804 sqlite3_clear_bindings(member.native());
4805 member.bind(1, block_id);
4806 member.bind(2, label.id);
4807 member.execute();
4808}
std::int64_t integer(const int column) const
Definition inbox.cpp:157

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::bind(), arachne::penelope::anonymous_namespace{inbox.cpp}::statement::execute(), arachne::penelope::anonymous_namespace{inbox.cpp}::hint_label::family, arachne::penelope::anonymous_namespace{inbox.cpp}::hint_label::id, arachne::penelope::anonymous_namespace{inbox.cpp}::statement::integer(), arachne::penelope::anonymous_namespace{inbox.cpp}::statement::native(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step().

Here is the call graph for this function:

◆ insert_hint_blocks()

void arachne::penelope::anonymous_namespace{inbox.cpp}::insert_hint_blocks ( sqlite3 *const sql,
const hint_label & label )

Definition at line 4810 of file inbox.cpp.

4810 {
4811 statement block(
4812 sql,
4813 "INSERT INTO merge_hint_blocks("
4814 "entity_type,block_type,block_key) VALUES(?,?,?) "
4815 "ON CONFLICT(entity_type,block_type,block_key) DO UPDATE SET "
4816 "block_key=excluded.block_key RETURNING id"
4817 );
4818 statement member(
4819 sql,
4820 "INSERT INTO merge_hint_block_members(block_id,entity_id) "
4821 "VALUES(?,?) ON CONFLICT(entity_id,block_id) DO NOTHING"
4822 );
4823 for (const auto& fingerprint : label.fingerprints) {
4825 block, member, label, "label_fingerprint", fingerprint
4826 );
4827 for (const auto& trigram : trigrams(fingerprint)) {
4829 block, member, label, "label_trigram", trigram
4830 );
4831 }
4832 }
4833
4834 if (label.family == "work") {
4835 statement work(
4836 sql,
4837 "SELECT medium,year_start FROM works WHERE entity_id=?"
4838 );
4839 work.bind(1, label.id);
4840 if (work.step()) {
4841 const std::string medium = work.text(0);
4842 const std::optional<std::string> year = work.is_null(1)
4843 ? std::nullopt
4844 : std::optional<std::string>(
4845 std::to_string(work.integer(1))
4846 );
4847 for (const auto& fingerprint : label.fingerprints) {
4849 block, member, label, "work_medium_title_fingerprint",
4850 contextual_block_key(medium, fingerprint)
4851 );
4852 if (year) {
4854 block, member, label, "work_year_title_fingerprint",
4855 contextual_block_key(*year, fingerprint)
4856 );
4857 }
4858 for (const auto& trigram : trigrams(fingerprint)) {
4860 block, member, label, "work_medium_title_trigram",
4861 contextual_block_key(medium, trigram)
4862 );
4863 if (year) {
4865 block, member, label, "work_year_title_trigram",
4866 contextual_block_key(*year, trigram)
4867 );
4868 }
4869 }
4870 }
4871 }
4872 statement agents(
4873 sql,
4874 "SELECT DISTINCT agent_id FROM credits WHERE work_id=? "
4875 "AND importance IN('primary','key')"
4876 );
4877 agents.bind(1, label.id);
4878 while (agents.step()) {
4880 block, member, label, "work_primary_agent", agents.text(0)
4881 );
4882 }
4883 } else if (label.family == "agent") {
4884 statement work_roles(
4885 sql,
4886 "SELECT DISTINCT work_id,role FROM credits WHERE agent_id=?"
4887 );
4888 work_roles.bind(1, label.id);
4889 while (work_roles.step()) {
4891 block, member, label, "agent_work_role",
4892 json::array({ work_roles.text(0), work_roles.text(1) }).dump()
4893 );
4894 }
4895 } else if (label.family == "concept") {
4896 statement works(
4897 sql,
4898 "SELECT DISTINCT work_id FROM work_concepts WHERE concept_id=?"
4899 );
4900 works.bind(1, label.id);
4901 while (works.step()) {
4903 block, member, label, "concept_work", works.text(0)
4904 );
4905 }
4906 statement neighbors(
4907 sql,
4908 "SELECT object_concept_id,relation_type "
4909 "FROM concept_relations WHERE subject_concept_id=?1 "
4910 "UNION SELECT subject_concept_id,relation_type "
4911 "FROM concept_relations WHERE object_concept_id=?1"
4912 );
4913 neighbors.bind(1, label.id);
4914 while (neighbors.step()) {
4916 block, member, label, "concept_neighbor",
4917 json::array({ neighbors.text(0), neighbors.text(1) }).dump()
4918 );
4919 }
4920 }
4921
4922 statement external_ids(
4923 sql,
4924 "SELECT DISTINCT lower(trim(scheme)),lower(trim(value)) "
4925 "FROM external_ids WHERE entity_id=?"
4926 );
4927 external_ids.bind(1, label.id);
4928 while (external_ids.step()) {
4930 block, member, label, "external_identifier",
4931 json::array({ external_ids.text(0), external_ids.text(1) }).dump()
4932 );
4933 }
4934}
std::set< std::string, std::less<> > trigrams(const std::string_view value)
Definition inbox.cpp:4628
void insert_hint_block(statement &block, statement &member, const hint_label &label, const std::string_view block_type, const std::string &block_key)
Definition inbox.cpp:4786
std::string contextual_block_key(const std::string_view context, const std::string_view value)
Definition inbox.cpp:4732

References arachne::penelope::anonymous_namespace{inbox.cpp}::hint_label::family, arachne::penelope::anonymous_namespace{inbox.cpp}::hint_label::id, arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::text().

Here is the call graph for this function:

◆ intersection_count()

std::size_t arachne::penelope::anonymous_namespace{inbox.cpp}::intersection_count ( const std::set< std::string, std::less<> > & left,
const std::set< std::string, std::less<> > & right )
nodiscard

Definition at line 5105 of file inbox.cpp.

5108 {
5109 return static_cast<std::size_t>(std::ranges::count_if(
5110 left,
5111 [&right](const std::string& value) {
5112 return right.contains(value);
5113 }
5114 ));
5115}

◆ is_kind()

bool arachne::penelope::anonymous_namespace{inbox.cpp}::is_kind ( const json & value,
const value_kind kind )
nodiscard

Definition at line 718 of file inbox.cpp.

718 {
719 switch (kind) {
721 return value.is_string();
723 return value.is_number_integer() || value.is_number_unsigned();
725 return value.is_number();
727 return value.is_boolean();
729 return value.is_object();
731 return value.is_array();
732 }
733 return false;
734}

References array, boolean, integer, number, object, and string.

◆ is_trigram_block()

bool arachne::penelope::anonymous_namespace{inbox.cpp}::is_trigram_block ( const std::string_view block_type)
nodiscard

Definition at line 4701 of file inbox.cpp.

4701 {
4702 return block_type.ends_with("_trigram");
4703}

◆ jaccard()

double arachne::penelope::anonymous_namespace{inbox.cpp}::jaccard ( const std::set< std::string, std::less<> > & left,
const std::set< std::string, std::less<> > & right )
nodiscard

Definition at line 5089 of file inbox.cpp.

5092 {
5093 std::size_t intersection = 0;
5094 for (const auto& value : left) {
5095 intersection += static_cast<std::size_t>(right.contains(value));
5096 }
5097 const std::size_t union_size
5098 = left.size() + right.size() - intersection;
5099 return union_size == 0U
5100 ? 0.0
5101 : static_cast<double>(intersection)
5102 / static_cast<double>(union_size);
5103}

◆ looks_like_canonical_entity_id()

bool arachne::penelope::anonymous_namespace{inbox.cpp}::looks_like_canonical_entity_id ( const std::string_view value)
nodiscard

Definition at line 648 of file inbox.cpp.

650 {
651 return valid_canonical_id(value, "agent")
652 || valid_canonical_id(value, "work")
653 || valid_canonical_id(value, "concept")
654 || valid_canonical_id(value, "manifestation");
655}
bool valid_canonical_id(const std::string_view value, const std::string_view family)
Definition inbox.cpp:624

Referenced by validate_local_id().

Here is the caller graph for this function:

◆ mark_merge_neighborhood()

void arachne::penelope::anonymous_namespace{inbox.cpp}::mark_merge_neighborhood ( parsed_batch & batch,
sqlite3 *const sql,
const std::string_view family,
const std::string & target,
const std::vector< std::string > & members )

Definition at line 4202 of file inbox.cpp.

4205 {
4206 batch.affected_entities.emplace(target);
4207 std::vector<std::string> merged { target };
4208 merged.insert(merged.end(), members.begin(), members.end());
4209 auto add_values = [&](const std::string_view query_text) {
4210 statement query(sql, query_text);
4211 for (const auto& id : merged) {
4212 sqlite3_reset(query.native());
4213 sqlite3_clear_bindings(query.native());
4214 query.bind(1, id);
4215 while (query.step()) {
4216 batch.affected_entities.emplace(query.text(0));
4217 }
4218 }
4219 };
4220 if (family == "agent") {
4221 add_values("SELECT work_id FROM credits WHERE agent_id=?");
4222 } else if (family == "work") {
4223 add_values("SELECT agent_id FROM credits WHERE work_id=?");
4224 add_values(
4225 "SELECT concept_id FROM work_concepts WHERE work_id=?"
4226 );
4227 } else if (family == "concept") {
4228 add_values(
4229 "SELECT work_id FROM work_concepts WHERE concept_id=?"
4230 );
4231 add_values(
4232 "SELECT object_concept_id FROM concept_relations "
4233 "WHERE subject_concept_id=?"
4234 );
4235 add_values(
4236 "SELECT subject_concept_id FROM concept_relations "
4237 "WHERE object_concept_id=?"
4238 );
4239 }
4240}

◆ maximum_entity_suffix()

std::int64_t arachne::penelope::anonymous_namespace{inbox.cpp}::maximum_entity_suffix ( sqlite3 *const sql,
const std::string_view prefix )
nodiscard

Definition at line 2886 of file inbox.cpp.

2888 {
2889 statement query(
2890 sql,
2891 "SELECT COALESCE(MAX(CAST(substr(id, ?) AS INTEGER)), 0) "
2892 "FROM entities WHERE id GLOB ?"
2893 );
2894 query.bind(
2895 1, static_cast<std::int64_t>(prefix.size() + 1U)
2896 );
2897 query.bind(2, std::string(prefix) + "[0-9]*");
2898 if (!query.step()) {
2899 throw database_error("cannot read maximum canonical ID suffix");
2900 }
2901 return query.integer(0);
2902}

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::bind(), arachne::penelope::anonymous_namespace{inbox.cpp}::statement::integer(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step().

Here is the call graph for this function:

◆ maximum_hint_block_size()

std::size_t arachne::penelope::anonymous_namespace{inbox.cpp}::maximum_hint_block_size ( const std::string_view block_type)
nodiscard

Definition at line 4705 of file inbox.cpp.

4707 {
4708 if (block_type == "label_fingerprint") {
4709 return 100U;
4710 }
4711 if (block_type == "label_trigram"
4712 || block_type == "work_year_title_trigram"
4713 || block_type == "work_medium_title_trigram") {
4714 return 20U;
4715 }
4716 if (block_type == "work_year_title_fingerprint"
4717 || block_type == "work_medium_title_fingerprint"
4718 || block_type == "work_primary_agent") {
4719 return 40U;
4720 }
4721 if (block_type == "agent_work_role"
4722 || block_type == "concept_work"
4723 || block_type == "concept_neighbor") {
4724 return 30U;
4725 }
4726 if (block_type == "external_identifier") {
4727 return 10U;
4728 }
4729 throw database_error("unknown merge-hint block type");
4730}

◆ maximum_row_id()

std::int64_t arachne::penelope::anonymous_namespace{inbox.cpp}::maximum_row_id ( sqlite3 *const sql,
const std::string_view table )
nodiscard

Definition at line 2874 of file inbox.cpp.

2876 {
2877 statement query(
2878 sql, "SELECT COALESCE(MAX(id), 0) FROM " + std::string(table)
2879 );
2880 if (!query.step()) {
2881 throw database_error("cannot read maximum row ID");
2882 }
2883 return query.integer(0);
2884}

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::integer(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step().

Here is the call graph for this function:

◆ maximum_text_similarity()

double arachne::penelope::anonymous_namespace{inbox.cpp}::maximum_text_similarity ( const std::vector< std::string > & left,
const std::vector< std::string > & right )
nodiscard

Definition at line 5117 of file inbox.cpp.

5120 {
5121 double result = 0.0;
5122 for (const auto& left_value : left) {
5123 for (const auto& right_value : right) {
5124 result = std::max(
5125 result,
5126 std::max({
5127 rapidfuzz::fuzz::token_sort_ratio(
5128 left_value, right_value
5129 ),
5130 rapidfuzz::fuzz::token_set_ratio(
5131 left_value, right_value
5132 ),
5133 rapidfuzz::fuzz::ratio(
5134 left_value, right_value, 35.0
5135 ),
5136 }) / 100.0
5137 );
5138 }
5139 }
5140 return result;
5141}

◆ merge_agents()

void arachne::penelope::anonymous_namespace{inbox.cpp}::merge_agents ( parsed_batch & batch,
sqlite3 *const sql,
const json & operation )

Definition at line 4242 of file inbox.cpp.

4244 {
4245 const std::string target = operation.at("target");
4246 const auto members = merge_members(operation);
4247 mark_merge_neighborhood(batch, sql, "agent", target, members);
4248 reject_preferred_name_conflict(sql, target, members);
4250 sql, "agents", "entity_id", target, members, operation,
4251 { "agent_type", "birth_year", "death_year" }, { "agent_type" }
4252 );
4253 if (operation.at("set").contains("agent_type")) {
4254 statement sync(
4255 sql, "UPDATE entities SET entity_type=? WHERE id=?"
4256 );
4257 sync.bind_json_value(1, operation.at("set").at("agent_type"));
4258 sync.bind(2, target);
4259 sync.execute();
4260 }
4261 for (const auto& member : members) {
4262 merge_entity_scoped_rows(sql, target, member);
4263 merge_credits(sql, "agent_id", target, member);
4264 delete_merged_entity(sql, member);
4265 }
4266}
void merge_entity_scoped_rows(sqlite3 *const sql, const std::string &target, const std::string &member)
Definition inbox.cpp:3861
void resolve_scalar_fields(sqlite3 *const sql, const std::string_view table, const std::string_view id_column, const std::string &target, const std::vector< std::string > &members, const json &operation, const std::vector< std::string > &columns, const std::set< std::string, std::less<> > &required)
Definition inbox.cpp:3754
void mark_merge_neighborhood(parsed_batch &batch, sqlite3 *const sql, const std::string_view family, const std::string &target, const std::vector< std::string > &members)
Definition inbox.cpp:4202
void merge_credits(sqlite3 *const sql, const std::string_view id_column, const std::string &target, const std::string &member)
Definition inbox.cpp:3923
void reject_preferred_name_conflict(sqlite3 *const sql, const std::string &target, const std::vector< std::string > &members)
Definition inbox.cpp:3827
std::vector< std::string > merge_members(const json &operation)
Definition inbox.cpp:4193
void delete_merged_entity(sqlite3 *const sql, const std::string &member)
Definition inbox.cpp:4182

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::execute().

Here is the call graph for this function:

◆ merge_concept_relations()

void arachne::penelope::anonymous_namespace{inbox.cpp}::merge_concept_relations ( sqlite3 *const sql,
const std::string & target,
const std::string & member )

Definition at line 4107 of file inbox.cpp.

4109 {
4110 statement query(
4111 sql,
4112 "SELECT id,subject_concept_id,relation_type,object_concept_id "
4113 "FROM concept_relations "
4114 "WHERE subject_concept_id=? OR object_concept_id=? ORDER BY id"
4115 );
4116 query.bind(1, member);
4117 query.bind(2, member);
4118 std::vector<concept_relation_row> rows;
4119 while (query.step()) {
4120 rows.push_back(
4121 {
4122 .id = query.integer(0),
4123 .subject = query.text(1),
4124 .relation_type = query.text(2),
4125 .object = query.text(3),
4126 }
4127 );
4128 }
4129 for (const auto& row : rows) {
4130 const std::string subject
4131 = row.subject == member ? target : row.subject;
4132 const std::string object = row.object == member ? target : row.object;
4133 if (subject == object) {
4134 statement remove(sql, "DELETE FROM concept_relations WHERE id=?");
4135 remove.bind(1, row.id);
4136 remove.execute();
4137 continue;
4138 }
4139 statement collision(
4140 sql,
4141 "SELECT id,"
4142 "(strength IS (SELECT strength FROM concept_relations WHERE id=?) "
4143 "AND from_year IS (SELECT from_year FROM concept_relations WHERE id=?) "
4144 "AND to_year IS (SELECT to_year FROM concept_relations WHERE id=?) "
4145 "AND region_code IS (SELECT region_code FROM concept_relations WHERE id=?) "
4146 "AND confidence IS (SELECT confidence FROM concept_relations WHERE id=?)) "
4147 "FROM concept_relations WHERE subject_concept_id=? "
4148 "AND relation_type=? AND object_concept_id=? AND id<>? LIMIT 1"
4149 );
4150 for (int binding = 1; binding <= 5; ++binding) {
4151 collision.bind(binding, row.id);
4152 }
4153 collision.bind(6, subject);
4154 collision.bind(7, row.relation_type);
4155 collision.bind(8, object);
4156 collision.bind(9, row.id);
4157 if (collision.step()) {
4158 if (collision.integer(1) == 0) {
4159 throw database_error("conflicting concept relation");
4160 }
4161 const std::int64_t keeper = collision.integer(0);
4163 sql, "concept_relation_evidence", keeper, row.id
4164 );
4165 statement remove(sql, "DELETE FROM concept_relations WHERE id=?");
4166 remove.bind(1, row.id);
4167 remove.execute();
4168 } else {
4169 statement rewrite(
4170 sql,
4171 "UPDATE concept_relations SET subject_concept_id=?,"
4172 "object_concept_id=? WHERE id=?"
4173 );
4174 rewrite.bind(1, subject);
4175 rewrite.bind(2, object);
4176 rewrite.bind(3, row.id);
4177 rewrite.execute();
4178 }
4179 }
4180}
void transfer_assertion_evidence(sqlite3 *const sql, const std::string_view evidence_table, const std::int64_t keeper, const std::int64_t duplicate)
Definition inbox.cpp:3960

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::integer(), arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::text().

Here is the call graph for this function:

◆ merge_concepts()

void arachne::penelope::anonymous_namespace{inbox.cpp}::merge_concepts ( parsed_batch & batch,
sqlite3 *const sql,
const json & operation )

Definition at line 4298 of file inbox.cpp.

4300 {
4301 const std::string target = operation.at("target");
4302 const auto members = merge_members(operation);
4303 mark_merge_neighborhood(batch, sql, "concept", target, members);
4304 reject_preferred_name_conflict(sql, target, members);
4305 if (operation.at("set").contains("slug")) {
4306 const std::string requested = operation.at("set").at("slug");
4307 for (const auto& member : members) {
4308 statement inspect(
4309 sql, "SELECT slug FROM concepts WHERE entity_id=?"
4310 );
4311 inspect.bind(1, member);
4312 if (!inspect.step()) {
4313 throw database_error("merge concept disappeared");
4314 }
4315 if (inspect.text(0) == requested) {
4316 std::string temporary = "merge-temporary-";
4317 for (const char raw_character : member) {
4318 const auto character
4319 = static_cast<unsigned char>(raw_character);
4320 if (std::isdigit(character) != 0) {
4321 temporary.push_back(static_cast<char>(character));
4322 }
4323 }
4324 const std::string base = temporary;
4325 std::size_t attempt = 0;
4326 statement collision(
4327 sql,
4328 "SELECT 1 FROM concepts WHERE slug=? LIMIT 1"
4329 );
4330 for (;;) {
4331 sqlite3_reset(collision.native());
4332 sqlite3_clear_bindings(collision.native());
4333 collision.bind(1, temporary);
4334 if (!collision.step()) {
4335 break;
4336 }
4337 ++attempt;
4338 temporary = base + "-" + std::to_string(attempt);
4339 }
4340 statement release(
4341 sql, "UPDATE concepts SET slug=? WHERE entity_id=?"
4342 );
4343 release.bind(1, temporary);
4344 release.bind(2, member);
4345 release.execute();
4346 }
4347 }
4348 }
4350 sql, "concepts", "entity_id", target, members, operation,
4351 { "concept_type", "slug" }, { "concept_type", "slug" }
4352 );
4353 for (const auto& member : members) {
4354 merge_entity_scoped_rows(sql, target, member);
4355 merge_work_concepts(sql, "concept_id", target, member);
4356 merge_parent_guides(sql, "concept_id", target, member);
4357 merge_concept_relations(sql, target, member);
4358 delete_merged_entity(sql, member);
4359 }
4360}
void merge_parent_guides(sqlite3 *const sql, const std::string_view id_column, const std::string &target, const std::string &member)
Definition inbox.cpp:4018
void merge_work_concepts(sqlite3 *const sql, const std::string_view id_column, const std::string &target, const std::string &member)
Definition inbox.cpp:3975
void merge_concept_relations(sqlite3 *const sql, const std::string &target, const std::string &member)
Definition inbox.cpp:4107

◆ merge_credits()

void arachne::penelope::anonymous_namespace{inbox.cpp}::merge_credits ( sqlite3 *const sql,
const std::string_view id_column,
const std::string & target,
const std::string & member )

Definition at line 3923 of file inbox.cpp.

3926 {
3927 const std::string other_column
3928 = id_column == "work_id" ? "agent_id" : "work_id";
3929 const std::string conflict
3930 = "SELECT 1 FROM credits t JOIN credits m ON t." + std::string(id_column)
3931 + "=? AND m." + std::string(id_column) + "=? AND t." + other_column
3932 + "=m." + other_column
3933 + " AND t.role=m.role AND t.credit_order IS m.credit_order "
3934 "AND t.credited_as IS m.credited_as "
3935 "WHERE t.importance<>m.importance LIMIT 1";
3936 if (query_exists(sql, conflict, target, member)) {
3937 throw database_error("conflicting duplicate credit");
3938 }
3939 statement remove(
3940 sql,
3941 "DELETE FROM credits AS m WHERE m." + std::string(id_column)
3942 + "=? AND EXISTS(SELECT 1 FROM credits AS t WHERE t."
3943 + std::string(id_column) + "=? AND t." + other_column + "=m."
3944 + other_column
3945 + " AND t.role=m.role AND t.credit_order IS m.credit_order "
3946 "AND t.credited_as IS m.credited_as)"
3947 );
3948 remove.bind(1, member);
3949 remove.bind(2, target);
3950 remove.execute();
3951 statement rewrite(
3952 sql, "UPDATE credits SET " + std::string(id_column) + "=? WHERE "
3953 + std::string(id_column) + "=?"
3954 );
3955 rewrite.bind(1, target);
3956 rewrite.bind(2, member);
3957 rewrite.execute();
3958}
bool query_exists(sqlite3 *const sql, const std::string_view query_text, const std::string &target, const std::string &member)
Definition inbox.cpp:3851

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::execute(), and query_exists().

Here is the call graph for this function:

◆ merge_entity_scoped_rows()

void arachne::penelope::anonymous_namespace{inbox.cpp}::merge_entity_scoped_rows ( sqlite3 *const sql,
const std::string & target,
const std::string & member )

Definition at line 3861 of file inbox.cpp.

3863 {
3864 if (query_exists(
3865 sql,
3866 "SELECT 1 FROM names t JOIN names m "
3867 "ON t.entity_id=? AND m.entity_id=? "
3868 "AND t.name_type=m.name_type "
3869 "AND t.language_code IS m.language_code "
3870 "AND t.script_code IS m.script_code AND t.value=m.value "
3871 "WHERE t.is_preferred<>m.is_preferred LIMIT 1",
3872 target, member
3873 )) {
3874 throw database_error("conflicting duplicate name preference");
3875 }
3876 {
3877 statement remove(
3878 sql,
3879 "DELETE FROM names AS m WHERE m.entity_id=? AND EXISTS("
3880 "SELECT 1 FROM names AS t WHERE t.entity_id=? "
3881 "AND t.name_type=m.name_type "
3882 "AND t.language_code IS m.language_code "
3883 "AND t.script_code IS m.script_code AND t.value=m.value)"
3884 );
3885 remove.bind(1, member);
3886 remove.bind(2, target);
3887 remove.execute();
3888 statement rewrite(
3889 sql, "UPDATE names SET entity_id=? WHERE entity_id=?"
3890 );
3891 rewrite.bind(1, target);
3892 rewrite.bind(2, member);
3893 rewrite.execute();
3894 }
3895 {
3896 statement rewrite(
3897 sql, "UPDATE external_ids SET entity_id=? WHERE entity_id=?"
3898 );
3899 rewrite.bind(1, target);
3900 rewrite.bind(2, member);
3901 rewrite.execute();
3902 }
3903 {
3904 statement remove(
3905 sql,
3906 "DELETE FROM measurements AS m WHERE m.entity_id=? AND EXISTS("
3907 "SELECT 1 FROM measurements AS t WHERE t.entity_id=? "
3908 "AND t.measurement_type=m.measurement_type AND t.value=m.value "
3909 "AND t.unit=m.unit AND t.qualifier IS m.qualifier)"
3910 );
3911 remove.bind(1, member);
3912 remove.bind(2, target);
3913 remove.execute();
3914 statement rewrite(
3915 sql, "UPDATE measurements SET entity_id=? WHERE entity_id=?"
3916 );
3917 rewrite.bind(1, target);
3918 rewrite.bind(2, member);
3919 rewrite.execute();
3920 }
3921}

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::execute().

Here is the call graph for this function:

◆ merge_financial_facts()

void arachne::penelope::anonymous_namespace{inbox.cpp}::merge_financial_facts ( sqlite3 *const sql,
const std::string & target,
const std::string & member )

Definition at line 4064 of file inbox.cpp.

4066 {
4067 if (query_exists(
4068 sql,
4069 "SELECT 1 FROM financial_facts t JOIN financial_facts m "
4070 "ON t.work_id=? AND m.work_id=? AND t.fact_type=m.fact_type "
4071 "AND t.amount_min=m.amount_min AND t.amount_max IS m.amount_max "
4072 "AND t.currency_code=m.currency_code "
4073 "AND t.value_year IS m.value_year "
4074 "WHERE t.is_estimate<>m.is_estimate "
4075 "OR t.confidence IS NOT m.confidence LIMIT 1",
4076 target, member
4077 )) {
4078 throw database_error("conflicting duplicate financial fact");
4079 }
4080 statement remove(
4081 sql,
4082 "DELETE FROM financial_facts AS m WHERE m.work_id=? AND EXISTS("
4083 "SELECT 1 FROM financial_facts AS t WHERE t.work_id=? "
4084 "AND t.fact_type=m.fact_type AND t.amount_min=m.amount_min "
4085 "AND t.amount_max IS m.amount_max "
4086 "AND t.currency_code=m.currency_code "
4087 "AND t.value_year IS m.value_year)"
4088 );
4089 remove.bind(1, member);
4090 remove.bind(2, target);
4091 remove.execute();
4092 statement rewrite(
4093 sql, "UPDATE financial_facts SET work_id=? WHERE work_id=?"
4094 );
4095 rewrite.bind(1, target);
4096 rewrite.bind(2, member);
4097 rewrite.execute();
4098}

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::execute().

Here is the call graph for this function:

◆ merge_members()

std::vector< std::string > arachne::penelope::anonymous_namespace{inbox.cpp}::merge_members ( const json & operation)
nodiscard

Definition at line 4193 of file inbox.cpp.

4193 {
4194 std::vector<std::string> result;
4195 result.reserve(operation.at("members").size());
4196 for (const auto& value : operation.at("members")) {
4197 result.push_back(value.get<std::string>());
4198 }
4199 return result;
4200}

◆ merge_parent_guides()

void arachne::penelope::anonymous_namespace{inbox.cpp}::merge_parent_guides ( sqlite3 *const sql,
const std::string_view id_column,
const std::string & target,
const std::string & member )

Definition at line 4018 of file inbox.cpp.

4021 {
4022 const std::string other_column
4023 = id_column == "work_id" ? "concept_id" : "work_id";
4024 statement rows(
4025 sql,
4026 "SELECT m.id,t.id,"
4027 "(m.intensity=t.intensity AND m.explicitness=t.explicitness "
4028 "AND m.frequency=t.frequency AND m.centrality=t.centrality "
4029 "AND m.realism=t.realism AND m.spoiler_level=t.spoiler_level "
4030 "AND m.confidence IS t.confidence) "
4031 "FROM parent_guide_assertions m JOIN parent_guide_assertions t "
4032 "ON m." + std::string(id_column) + "=? AND t."
4033 + std::string(id_column) + "=? AND m." + other_column + "=t."
4034 + other_column + " AND m.category=t.category"
4035 );
4036 rows.bind(1, member);
4037 rows.bind(2, target);
4038 std::vector<std::pair<std::int64_t, std::int64_t>> duplicates;
4039 while (rows.step()) {
4040 if (rows.integer(2) == 0) {
4041 throw database_error("conflicting parent-guide assertion");
4042 }
4043 duplicates.emplace_back(rows.integer(0), rows.integer(1));
4044 }
4045 for (const auto& [duplicate, keeper] : duplicates) {
4047 sql, "parent_guide_evidence", keeper, duplicate
4048 );
4049 statement remove(
4050 sql, "DELETE FROM parent_guide_assertions WHERE id=?"
4051 );
4052 remove.bind(1, duplicate);
4053 remove.execute();
4054 }
4055 statement rewrite(
4056 sql, "UPDATE parent_guide_assertions SET " + std::string(id_column)
4057 + "=? WHERE " + std::string(id_column) + "=?"
4058 );
4059 rewrite.bind(1, target);
4060 rewrite.bind(2, member);
4061 rewrite.execute();
4062}

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::execute(), arachne::penelope::anonymous_namespace{inbox.cpp}::statement::integer(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step().

Here is the call graph for this function:

◆ merge_work_concepts()

void arachne::penelope::anonymous_namespace{inbox.cpp}::merge_work_concepts ( sqlite3 *const sql,
const std::string_view id_column,
const std::string & target,
const std::string & member )

Definition at line 3975 of file inbox.cpp.

3978 {
3979 const std::string other_column
3980 = id_column == "work_id" ? "concept_id" : "work_id";
3981 statement rows(
3982 sql,
3983 "SELECT m.id,t.id,"
3984 "(m.centrality=t.centrality "
3985 "AND m.historical_role IS t.historical_role "
3986 "AND m.confidence IS t.confidence) "
3987 "FROM work_concepts m JOIN work_concepts t "
3988 "ON m." + std::string(id_column) + "=? AND t."
3989 + std::string(id_column) + "=? AND m." + other_column + "=t."
3990 + other_column + " AND m.relation_type=t.relation_type"
3991 );
3992 rows.bind(1, member);
3993 rows.bind(2, target);
3994 std::vector<std::pair<std::int64_t, std::int64_t>> duplicates;
3995 while (rows.step()) {
3996 if (rows.integer(2) == 0) {
3997 throw database_error("conflicting work-concept assertion");
3998 }
3999 duplicates.emplace_back(rows.integer(0), rows.integer(1));
4000 }
4001 for (const auto& [duplicate, keeper] : duplicates) {
4003 sql, "work_concept_evidence", keeper, duplicate
4004 );
4005 statement remove(sql, "DELETE FROM work_concepts WHERE id=?");
4006 remove.bind(1, duplicate);
4007 remove.execute();
4008 }
4009 statement rewrite(
4010 sql, "UPDATE work_concepts SET " + std::string(id_column)
4011 + "=? WHERE " + std::string(id_column) + "=?"
4012 );
4013 rewrite.bind(1, target);
4014 rewrite.bind(2, member);
4015 rewrite.execute();
4016}

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::execute(), arachne::penelope::anonymous_namespace{inbox.cpp}::statement::integer(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step().

Here is the call graph for this function:

◆ merge_works()

void arachne::penelope::anonymous_namespace{inbox.cpp}::merge_works ( parsed_batch & batch,
sqlite3 *const sql,
const json & operation )

Definition at line 4268 of file inbox.cpp.

4270 {
4271 const std::string target = operation.at("target");
4272 const auto members = merge_members(operation);
4273 mark_merge_neighborhood(batch, sql, "work", target, members);
4274 reject_preferred_name_conflict(sql, target, members);
4276 sql, "works", "entity_id", target, members, operation,
4277 { "medium", "year_start", "year_end", "date_precision",
4278 "date_start_text", "date_end_text", "date_qualifier",
4279 "language_code", "country_code", "production_info_json" },
4280 { "medium" }
4281 );
4282 for (const auto& member : members) {
4283 merge_entity_scoped_rows(sql, target, member);
4284 merge_credits(sql, "work_id", target, member);
4285 merge_financial_facts(sql, target, member);
4286 merge_work_concepts(sql, "work_id", target, member);
4287 merge_parent_guides(sql, "work_id", target, member);
4288 statement manifestations(
4289 sql, "UPDATE manifestations SET work_id=? WHERE work_id=?"
4290 );
4291 manifestations.bind(1, target);
4292 manifestations.bind(2, member);
4293 manifestations.execute();
4294 delete_merged_entity(sql, member);
4295 }
4296}
void merge_financial_facts(sqlite3 *const sql, const std::string &target, const std::string &member)
Definition inbox.cpp:4064

◆ move_rejected_unchanged()

void arachne::penelope::anonymous_namespace{inbox.cpp}::move_rejected_unchanged ( const file_snapshot & snapshot,
const fs::path & rejected )

Definition at line 5910 of file inbox.cpp.

5912 {
5913 if (!snapshot_still_current(snapshot)) {
5914 throw inbox_error(
5915 "rejected inbox file changed and remains in place: "
5916 + snapshot.path.string()
5917 );
5918 }
5919 if (!fs::exists(rejected)) {
5920 std::error_code error;
5921 static_cast<void>(fs::create_directory(rejected, error));
5922 if (error) {
5923 throw inbox_error(
5924 "cannot create rejected inbox directory "
5925 + rejected.string() + ": " + error.message()
5926 );
5927 }
5928 }
5929 if (!real_directory(rejected)) {
5930 throw inbox_error(
5931 "rejected inbox path is not a real directory: "
5932 + rejected.string()
5933 );
5934 }
5935 fs::path destination = rejected / snapshot.path.filename();
5936 for (std::size_t suffix = 1U;; ++suffix) {
5937 struct stat existing {};
5938 if (::lstat(destination.c_str(), &existing) != 0) {
5939 if (errno == ENOENT) {
5940 break;
5941 }
5942 throw inbox_error(
5943 "cannot inspect rejected destination "
5944 + destination.string()
5945 );
5946 }
5947 if (suffix == 100000U) {
5948 throw inbox_error(
5949 "cannot allocate a rejected filename for "
5950 + snapshot.path.filename().string()
5951 );
5952 }
5953 destination = rejected
5954 / (snapshot.path.stem().string() + "-"
5955 + std::to_string(suffix) + snapshot.path.extension().string());
5956 }
5957 std::error_code error;
5958 fs::rename(snapshot.path, destination, error);
5959 if (error) {
5960 throw inbox_error(
5961 "cannot move rejected inbox file " + snapshot.path.string()
5962 + " to " + destination.string() + ": " + error.message()
5963 );
5964 }
5965}
bool snapshot_still_current(const file_snapshot &snapshot)
Definition inbox.cpp:548

References real_directory(), and snapshot_still_current().

Here is the call graph for this function:

◆ next_sequence()

std::int64_t arachne::penelope::anonymous_namespace{inbox.cpp}::next_sequence ( std::int64_t & sequence,
const std::string_view description )
nodiscard

Definition at line 2957 of file inbox.cpp.

2959 {
2960 if (sequence == std::numeric_limits<std::int64_t>::max()) {
2961 throw database_error(
2962 std::string(description) + " ID sequence is exhausted"
2963 );
2964 }
2965 return ++sequence;
2966}

◆ normalized_label()

std::string arachne::penelope::anonymous_namespace{inbox.cpp}::normalized_label ( const std::string_view value)
nodiscard

Definition at line 4396 of file inbox.cpp.

4396 {
4397 utf8proc_uint8_t* mapped = nullptr;
4398 const auto options = static_cast<utf8proc_option_t>(
4399 UTF8PROC_STABLE | UTF8PROC_COMPOSE | UTF8PROC_COMPAT
4400 | UTF8PROC_CASEFOLD
4401 );
4402 const utf8proc_ssize_t mapped_size = utf8proc_map(
4403 reinterpret_cast<const utf8proc_uint8_t*>(value.data()),
4404 static_cast<utf8proc_ssize_t>(value.size()), &mapped, options
4405 );
4406 if (mapped_size < 0 || mapped == nullptr) {
4407 std::free(mapped);
4408 return {};
4409 }
4410 std::vector<std::string> tokens;
4411 std::string token;
4412 utf8proc_ssize_t offset = 0;
4413 while (offset < mapped_size) {
4414 utf8proc_int32_t codepoint = 0;
4415 const utf8proc_ssize_t consumed = utf8proc_iterate(
4416 mapped + offset, mapped_size - offset, &codepoint
4417 );
4418 if (consumed <= 0) {
4419 std::free(mapped);
4420 return {};
4421 }
4422 const utf8proc_category_t category = utf8proc_category(codepoint);
4423 const bool word = (category >= UTF8PROC_CATEGORY_LU
4424 && category <= UTF8PROC_CATEGORY_LO)
4425 || (category >= UTF8PROC_CATEGORY_MN
4426 && category <= UTF8PROC_CATEGORY_ME)
4427 || (category >= UTF8PROC_CATEGORY_ND
4428 && category <= UTF8PROC_CATEGORY_NO);
4429 if (word) {
4430 std::array<utf8proc_uint8_t, 4> encoded {};
4431 const utf8proc_ssize_t encoded_size
4432 = utf8proc_encode_char(codepoint, encoded.data());
4433 token.append(
4434 reinterpret_cast<const char*>(encoded.data()),
4435 static_cast<std::size_t>(encoded_size)
4436 );
4437 } else if (!token.empty()) {
4438 tokens.push_back(std::move(token));
4439 token.clear();
4440 }
4441 offset += consumed;
4442 }
4443 std::free(mapped);
4444 if (!token.empty()) {
4445 tokens.push_back(std::move(token));
4446 }
4447 std::ranges::sort(tokens);
4448 std::string result;
4449 for (const auto& current : tokens) {
4450 if (!result.empty()) {
4451 result.push_back(' ');
4452 }
4453 result += current;
4454 }
4455 return result;
4456}

◆ parse_batch()

parsed_batch arachne::penelope::anonymous_namespace{inbox.cpp}::parse_batch ( file_snapshot snapshot)
nodiscard

Definition at line 5835 of file inbox.cpp.

5835 {
5836 parsed_batch result;
5837 result.file = std::move(snapshot);
5838 try {
5839 result.document = parse_strict_json(result.file.bytes);
5841 const auto contract = arachnespace::contracts::validate(
5843 result.document
5844 );
5845 for (const auto& diagnostic : contract.diagnostics) {
5846 const std::string path = diagnostic.instance_path.empty()
5847 ? "/"
5848 : diagnostic.instance_path;
5849 const bool present = std::ranges::any_of(
5850 result.issues, [&](const inbox_issue& issue) {
5851 // The table-specific validator owns diagnostics for paths it
5852 // understands. Contract validation is a backstop, not a
5853 // second issue for the same rejected field/value.
5854 return issue.json_path == path;
5855 }
5856 );
5857 if (!present) {
5858 add_issue(
5859 result, "contract_" + diagnostic.code, path,
5860 diagnostic.message
5861 );
5862 }
5863 }
5864 result.structurally_valid = result.issues.empty();
5865 } catch (const std::exception& error) {
5866 add_issue(result, "invalid_json", "/", error.what());
5867 }
5868 return result;
5869}
json parse_strict_json(const std::string &bytes)
Definition inbox.cpp:2283
void validate_document_shape(parsed_batch &batch)
Definition inbox.cpp:1995
validation_result validate(const nlohmann::json &document)

References arachnespace::contracts::arachne_batch, arachne::penelope::anonymous_namespace{inbox.cpp}::parsed_batch::structurally_valid, and validate_document_shape().

Here is the call graph for this function:

◆ parse_strict_json()

json arachne::penelope::anonymous_namespace{inbox.cpp}::parse_strict_json ( const std::string & bytes)
nodiscard

Definition at line 2283 of file inbox.cpp.

2283 {
2284 if (bytes.size() >= 3U
2285 && static_cast<unsigned char>(bytes[0]) == 0xEFU
2286 && static_cast<unsigned char>(bytes[1]) == 0xBBU
2287 && static_cast<unsigned char>(bytes[2]) == 0xBFU) {
2288 throw inbox_error("UTF-8 BOM is not allowed");
2289 }
2290 std::vector<std::set<std::string, std::less<>>> object_keys;
2291 json::parser_callback_t callback
2292 = [&object_keys](
2293 const int depth, const json::parse_event_t event, json& parsed
2294 ) {
2295 static_cast<void>(depth);
2296 if (event == json::parse_event_t::object_start) {
2297 object_keys.emplace_back();
2298 } else if (event == json::parse_event_t::key) {
2299 if (object_keys.empty()) {
2300 throw inbox_error("JSON key outside an object");
2301 }
2302 const std::string key = parsed.get<std::string>();
2303 if (!object_keys.back().emplace(key).second) {
2304 throw inbox_error("duplicate JSON object key: " + key);
2305 }
2306 } else if (event == json::parse_event_t::object_end) {
2307 if (object_keys.empty()) {
2308 throw inbox_error("unbalanced JSON object");
2309 }
2310 object_keys.pop_back();
2311 }
2312 return true;
2313 };
2314 try {
2315 return json::parse(bytes, callback, true, false);
2316 } catch (const inbox_error&) {
2317 throw;
2318 } catch (const json::exception& error) {
2319 throw inbox_error(std::string("invalid UTF-8 JSON: ") + error.what());
2320 }
2321}

◆ pointer_escape()

std::string arachne::penelope::anonymous_namespace{inbox.cpp}::pointer_escape ( const std::string_view value)
nodiscard

Definition at line 555 of file inbox.cpp.

555 {
556 std::string result;
557 result.reserve(value.size());
558 for (const char character : value) {
559 if (character == '~') {
560 result += "~0";
561 } else if (character == '/') {
562 result += "~1";
563 } else {
564 result.push_back(character);
565 }
566 }
567 return result;
568}

◆ prevalidate_entity_reference()

void arachne::penelope::anonymous_namespace{inbox.cpp}::prevalidate_entity_reference ( parsed_batch & batch,
sqlite3 *const database_value,
const json & record,
const std::string & key,
const std::string & path,
const std::string_view expected_family = {} )

Definition at line 2356 of file inbox.cpp.

2359 {}
2360) {
2361 const auto found = record.find(key);
2362 if (found == record.end() || !found->is_string()) {
2363 return;
2364 }
2365 const std::string& reference = found->get_ref<const std::string&>();
2366 const auto local = batch.entity_local_ids.find(reference);
2367 std::string actual_family;
2368 if (local != batch.entity_local_ids.end()) {
2369 actual_family = local->second;
2370 if (actual_family == "source" || actual_family == "evidence"
2371 || actual_family == "work_concept"
2372 || actual_family == "concept_relation"
2373 || actual_family == "parent_guide") {
2374 add_issue(
2375 batch, "wrong_reference_family",
2376 path + "/" + pointer_escape(key),
2377 "reference is not a canonical entity", &*found
2378 );
2379 return;
2380 }
2381 } else {
2382 actual_family = entity_family(database_value, reference);
2383 if (actual_family.empty()) {
2384 add_issue(
2385 batch, "unknown_reference", path + "/" + pointer_escape(key),
2386 "canonical entity or local_id does not exist", &*found
2387 );
2388 return;
2389 }
2390 }
2391 if (!expected_family.empty() && actual_family != expected_family) {
2392 add_issue(
2393 batch, "wrong_reference_family",
2394 path + "/" + pointer_escape(key),
2395 "reference must identify a " + std::string(expected_family),
2396 &*found
2397 );
2398 }
2399}
std::string entity_family(sqlite3 *const database_value, const std::string &id)
Definition inbox.cpp:2334
std::unordered_map< std::string, std::string > entity_local_ids
Definition inbox.cpp:577

◆ prevalidate_integer_or_local_reference()

void arachne::penelope::anonymous_namespace{inbox.cpp}::prevalidate_integer_or_local_reference ( parsed_batch & batch,
sqlite3 *const database_value,
const json & record,
const std::string & key,
const std::string & path,
const std::string_view local_family,
const std::string_view table )

Definition at line 2401 of file inbox.cpp.

2405 {
2406 const auto found = record.find(key);
2407 if (found == record.end()) {
2408 return;
2409 }
2410 if (found->is_string()) {
2411 const auto local = batch.entity_local_ids.find(found->get<std::string>());
2412 if (local == batch.entity_local_ids.end()) {
2413 add_issue(
2414 batch, "unknown_local_reference",
2415 path + "/" + pointer_escape(key), "local_id does not exist",
2416 &*found
2417 );
2418 } else if (local->second != local_family) {
2419 add_issue(
2420 batch, "wrong_reference_family",
2421 path + "/" + pointer_escape(key),
2422 "local_id has the wrong record family", &*found
2423 );
2424 }
2425 } else if ((found->is_number_integer() || found->is_number_unsigned())
2426 && !row_exists(database_value, table, "id", *found)) {
2427 add_issue(
2428 batch, "unknown_reference", path + "/" + pointer_escape(key),
2429 "canonical database row does not exist", &*found
2430 );
2431 }
2432}
bool row_exists(sqlite3 *const database_value, const std::string_view table, const std::string_view column, const json &id)
Definition inbox.cpp:2323

◆ prevalidate_semantics()

void arachne::penelope::anonymous_namespace{inbox.cpp}::prevalidate_semantics ( parsed_batch & batch,
database & product )

Definition at line 2434 of file inbox.cpp.

2434 {
2435 sqlite3* const sql = product.native();
2436 const auto& create = batch.document.at("create");
2437 auto walk = [&batch, &create](
2438 const std::string& key, const auto& callback
2439 ) {
2440 const auto found = create.find(key);
2441 if (found == create.end() || !found->is_array()) {
2442 return;
2443 }
2444 for (std::size_t index = 0; index < found->size(); ++index) {
2445 callback((*found)[index], indexed_path("/create/" + key, index));
2446 }
2447 };
2448 walk("manifestations", [&](const json& row, const std::string& path) {
2450 batch, sql, row, "work_id", path, "work"
2451 );
2452 });
2453 walk("names", [&](const json& row, const std::string& path) {
2454 prevalidate_entity_reference(batch, sql, row, "entity_id", path);
2455 });
2456 walk("external_ids", [&](const json& row, const std::string& path) {
2457 prevalidate_entity_reference(batch, sql, row, "entity_id", path);
2458 });
2459 walk("evidence", [&](const json& row, const std::string& path) {
2461 batch, sql, row, "source_id", path, "source", "sources"
2462 );
2463 });
2464 walk("credits", [&](const json& row, const std::string& path) {
2466 batch, sql, row, "work_id", path, "work"
2467 );
2469 batch, sql, row, "agent_id", path, "agent"
2470 );
2471 });
2472 walk("measurements", [&](const json& row, const std::string& path) {
2473 prevalidate_entity_reference(batch, sql, row, "entity_id", path);
2474 });
2475 walk("financial_facts", [&](const json& row, const std::string& path) {
2477 batch, sql, row, "work_id", path, "work"
2478 );
2479 });
2480 auto assertion_evidence = [&](const json& row, const std::string& path) {
2481 const auto values = row.find("evidence");
2482 if (values == row.end() || !values->is_array()) {
2483 return;
2484 }
2485 for (std::size_t index = 0; index < values->size(); ++index) {
2486 const auto& reference = (*values)[index];
2487 json wrapper { { "evidence_id", reference } };
2489 batch, sql, wrapper, "evidence_id",
2490 indexed_path(path + "/evidence", index), "evidence", "evidence"
2491 );
2492 }
2493 };
2494 walk("work_concepts", [&](const json& row, const std::string& path) {
2496 batch, sql, row, "work_id", path, "work"
2497 );
2499 batch, sql, row, "concept_id", path, "concept"
2500 );
2501 assertion_evidence(row, path);
2502 });
2503 walk("concept_relations", [&](const json& row, const std::string& path) {
2505 batch, sql, row, "subject_concept_id", path, "concept"
2506 );
2508 batch, sql, row, "object_concept_id", path, "concept"
2509 );
2510 assertion_evidence(row, path);
2511 });
2512 walk(
2513 "parent_guide_assertions",
2514 [&](const json& row, const std::string& path) {
2516 batch, sql, row, "work_id", path, "work"
2517 );
2519 batch, sql, row, "concept_id", path, "concept"
2520 );
2521 assertion_evidence(row, path);
2522 }
2523 );
2524
2525 const auto& update = batch.document.at("update");
2526 for (const auto& [collection, family] : {
2527 std::pair { "agents", "agent" },
2528 std::pair { "works", "work" },
2529 std::pair { "concepts", "concept" },
2530 std::pair { "manifestations", "manifestation" } }) {
2531 const auto rows = update.find(collection);
2532 if (rows == update.end() || !rows->is_array()) {
2533 continue;
2534 }
2535 for (std::size_t index = 0; index < rows->size(); ++index) {
2536 const auto& row = (*rows)[index];
2537 if (!row.is_object() || !row.contains("id")
2538 || !row["id"].is_string()) {
2539 continue;
2540 }
2541 const std::string id = row["id"].get<std::string>();
2542 if (entity_family(sql, id) != family) {
2543 add_issue(
2544 batch, "unknown_canonical_id",
2545 indexed_path("/update/" + std::string(collection), index)
2546 + "/id",
2547 "canonical entity does not exist in the required family",
2548 &row["id"]
2549 );
2550 }
2551 if (family == std::string_view("manifestation")
2552 && row.contains("set") && row["set"].is_object()
2553 && row["set"].contains("work_id")) {
2555 batch, sql, row["set"], "work_id",
2557 "/update/" + std::string(collection), index
2558 )
2559 + "/set",
2560 "work"
2561 );
2562 }
2563 }
2564 }
2565 const auto source_updates = update.find("sources");
2566 if (source_updates != update.end() && source_updates->is_array()) {
2567 for (std::size_t index = 0; index < source_updates->size(); ++index) {
2568 const auto& row = (*source_updates)[index];
2569 if (row.is_object() && row.contains("id")
2570 && (row["id"].is_number_integer()
2571 || row["id"].is_number_unsigned())
2572 && !row_exists(sql, "sources", "id", row["id"])) {
2573 add_issue(
2574 batch, "unknown_canonical_id",
2575 indexed_path("/update/sources", index) + "/id",
2576 "source does not exist", &row["id"]
2577 );
2578 }
2579 if (!row.is_object() || !row.contains("id")
2580 || !(row["id"].is_number_integer()
2581 || row["id"].is_number_unsigned())) {
2582 continue;
2583 }
2584 statement identity(
2585 sql,
2586 "SELECT bibliography_text,url,doi,isbn FROM sources WHERE id=?"
2587 );
2588 identity.bind_json_value(1, row["id"]);
2589 if (!identity.step()) {
2590 continue;
2591 }
2592 std::map<std::string, bool, std::less<>> present {
2593 { "bibliography_text", !identity.is_null(0) },
2594 { "url", !identity.is_null(1) },
2595 { "doi", !identity.is_null(2) },
2596 { "isbn", !identity.is_null(3) },
2597 };
2598 if (row.contains("set") && row["set"].is_object()) {
2599 for (const auto& [field, value] : row["set"].items()) {
2600 if (present.contains(field)) {
2601 present[field] = !value.is_null();
2602 }
2603 }
2604 }
2605 if (row.contains("unset") && row["unset"].is_array()) {
2606 for (const auto& field : row["unset"]) {
2607 if (field.is_string()
2608 && present.contains(field.get<std::string>())) {
2609 present[field.get<std::string>()] = false;
2610 }
2611 }
2612 }
2613 if (std::ranges::none_of(
2614 present,
2615 [](const auto& item) { return item.second; }
2616 )) {
2617 add_issue(
2618 batch, "source_identity_required",
2619 indexed_path("/update/sources", index),
2620 "source update would remove every strong or fallback identity",
2621 &row
2622 );
2623 }
2624 }
2625 }
2626 const auto deletes = update.find("delete");
2627 if (deletes != update.end() && deletes->is_object()) {
2628 for (const auto& [table, values] : deletes->items()) {
2629 if (!values.is_array()) {
2630 continue;
2631 }
2632 if (table == "ingest_issues") {
2633 statement issue(
2634 sql,
2635 "SELECT status FROM ingest_issues "
2636 "WHERE batch_id=? AND code=? AND json_path=?"
2637 );
2638 for (std::size_t index = 0; index < values.size(); ++index) {
2639 const auto& value = values[index];
2640 if (!value.is_object() || !value.contains("batch_id")
2641 || !value.contains("code")
2642 || !value.contains("json_path")) {
2643 continue;
2644 }
2645 sqlite3_reset(issue.native());
2646 sqlite3_clear_bindings(issue.native());
2647 issue.bind_json_value(1, value.at("batch_id"));
2648 issue.bind_json_value(2, value.at("code"));
2649 issue.bind_json_value(3, value.at("json_path"));
2650 const std::string value_path = indexed_path(
2651 "/update/delete/ingest_issues", index
2652 );
2653 if (!issue.step()) {
2654 add_issue(
2655 batch, "unknown_delete_row", value_path,
2656 "ingest issue does not exist", &value
2657 );
2658 } else if (issue.text(0) == "open") {
2659 add_issue(
2660 batch, "open_issue_delete_forbidden", value_path,
2661 "only resolved or ignored ingest issues may be "
2662 "deleted",
2663 &value
2664 );
2665 }
2666 }
2667 continue;
2668 }
2669 for (std::size_t index = 0; index < values.size(); ++index) {
2670 if ((values[index].is_number_integer()
2671 || values[index].is_number_unsigned())
2672 && !row_exists(sql, table, "id", values[index])) {
2673 add_issue(
2674 batch, "unknown_delete_row",
2675 indexed_path("/update/delete/" + table, index),
2676 "relationship row does not exist", &values[index]
2677 );
2678 }
2679 }
2680 }
2681 const auto evidence_rows = deletes->find("evidence");
2682 if (evidence_rows != deletes->end() && evidence_rows->is_array()
2683 && !evidence_rows->empty()) {
2684 std::set<std::int64_t> deleted_evidence;
2685 for (const auto& value : *evidence_rows) {
2686 if (value.is_number_integer() || value.is_number_unsigned()) {
2687 deleted_evidence.emplace(value.get<std::int64_t>());
2688 }
2689 }
2690 for (const auto collection : {
2691 "work_concepts", "concept_relations",
2692 "parent_guide_assertions" }) {
2693 const auto created = create.find(collection);
2694 if (created == create.end() || !created->is_array()) {
2695 continue;
2696 }
2697 for (std::size_t index = 0; index < created->size(); ++index) {
2698 const auto& row = (*created)[index];
2699 const auto evidence_values = row.find("evidence");
2700 if (evidence_values == row.end()
2701 || !evidence_values->is_array()) {
2702 continue;
2703 }
2704 for (std::size_t evidence_index = 0;
2705 evidence_index < evidence_values->size();
2706 ++evidence_index) {
2707 const auto& reference
2708 = (*evidence_values)[evidence_index];
2709 if ((reference.is_number_integer()
2710 || reference.is_number_unsigned())
2711 && deleted_evidence.contains(
2712 reference.get<std::int64_t>()
2713 )) {
2714 add_issue(
2715 batch, "deleted_evidence_reference",
2718 "/create/"
2719 + std::string(collection),
2720 index
2721 )
2722 + "/evidence",
2723 evidence_index
2724 ),
2725 "new assertion references evidence scheduled "
2726 "for deletion in the same batch",
2727 &reference
2728 );
2729 }
2730 }
2731 const bool has_remaining = std::ranges::any_of(
2732 *evidence_values,
2733 [&deleted_evidence](const json& reference) {
2734 return reference.is_string()
2735 || ((reference.is_number_integer()
2736 || reference.is_number_unsigned())
2737 && !deleted_evidence.contains(
2738 reference.get<std::int64_t>()
2739 ));
2740 }
2741 );
2742 if (!has_remaining) {
2743 add_issue(
2744 batch, "assertion_evidence_required",
2746 "/create/" + std::string(collection), index
2747 )
2748 + "/evidence",
2749 "evidence deletion would leave the newly created "
2750 "assertion without evidence",
2751 &*evidence_values
2752 );
2753 }
2754 }
2755 }
2756 for (const auto& [assertion_table, link_table] : {
2757 std::pair {
2758 "work_concepts", "work_concept_evidence" },
2759 std::pair {
2760 "concept_relations", "concept_relation_evidence" },
2761 std::pair {
2762 "parent_guide_assertions", "parent_guide_evidence" } }) {
2763 std::set<std::int64_t> deleted_assertions;
2764 if (const auto values = deletes->find(assertion_table);
2765 values != deletes->end() && values->is_array()) {
2766 for (const auto& value : *values) {
2767 if (value.is_number_integer()
2768 || value.is_number_unsigned()) {
2769 deleted_assertions.emplace(
2770 value.get<std::int64_t>()
2771 );
2772 }
2773 }
2774 }
2775 statement affected(
2776 sql,
2777 "SELECT DISTINCT assertion_id FROM "
2778 + std::string(link_table) + " WHERE evidence_id=?"
2779 );
2780 std::set<std::int64_t> assertions;
2781 for (const auto evidence_id : deleted_evidence) {
2782 sqlite3_reset(affected.native());
2783 sqlite3_clear_bindings(affected.native());
2784 affected.bind(1, evidence_id);
2785 while (affected.step()) {
2786 assertions.emplace(affected.integer(0));
2787 }
2788 }
2789 for (const auto assertion_id : assertions) {
2790 if (deleted_assertions.contains(assertion_id)) {
2791 continue;
2792 }
2793 statement remaining(
2794 sql,
2795 "SELECT evidence_id FROM " + std::string(link_table)
2796 + " WHERE assertion_id=?"
2797 );
2798 remaining.bind(1, assertion_id);
2799 bool has_remaining = false;
2800 while (remaining.step()) {
2801 if (!deleted_evidence.contains(remaining.integer(0))) {
2802 has_remaining = true;
2803 break;
2804 }
2805 }
2806 if (!has_remaining) {
2807 add_issue(
2808 batch, "assertion_evidence_required",
2809 "/update/delete/evidence/assertion_impacts/"
2810 + std::string(assertion_table) + "/"
2811 + std::to_string(assertion_id),
2812 "evidence deletion would leave "
2813 + std::string(assertion_table)
2814 + " row " + std::to_string(assertion_id)
2815 + " without evidence",
2816 &*evidence_rows
2817 );
2818 }
2819 }
2820 }
2821 }
2822 }
2823
2824 const auto& merge = batch.document.at("merge");
2825 for (const auto& [collection, family] : {
2826 std::pair { "agents", "agent" },
2827 std::pair { "works", "work" },
2828 std::pair { "concepts", "concept" } }) {
2829 const auto rows = merge.find(collection);
2830 if (rows == merge.end() || !rows->is_array()) {
2831 continue;
2832 }
2833 for (std::size_t index = 0; index < rows->size(); ++index) {
2834 const auto& row = (*rows)[index];
2835 if (!row.is_object()) {
2836 continue;
2837 }
2838 auto inspect = [&](const json& id, const std::string& path) {
2839 if (id.is_string()
2840 && entity_family(sql, id.get<std::string>()) != family) {
2841 add_issue(
2842 batch, "unknown_canonical_id", path,
2843 "merge entity does not exist in the required family",
2844 &id
2845 );
2846 }
2847 };
2848 if (row.contains("target")) {
2849 inspect(
2850 row["target"],
2851 indexed_path("/merge/" + std::string(collection), index)
2852 + "/target"
2853 );
2854 }
2855 if (row.contains("members") && row["members"].is_array()) {
2856 for (std::size_t member = 0; member < row["members"].size();
2857 ++member) {
2858 inspect(
2859 row["members"][member],
2862 "/merge/" + std::string(collection), index
2863 )
2864 + "/members",
2865 member
2866 )
2867 );
2868 }
2869 }
2870 }
2871 }
2872}
void prevalidate_integer_or_local_reference(parsed_batch &batch, sqlite3 *const database_value, const json &record, const std::string &key, const std::string &path, const std::string_view local_family, const std::string_view table)
Definition inbox.cpp:2401
void prevalidate_entity_reference(parsed_batch &batch, sqlite3 *const database_value, const json &record, const std::string &key, const std::string &path, const std::string_view expected_family={})
Definition inbox.cpp:2356
const json * field(const json &object, const std::string_view key, const std::string_view path, validation_result &result)
Definition contracts.cpp:57
@ item
IDs prefixed with 'Q'.
Definition utils.hpp:48

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::is_null(), arachne::penelope::anonymous_namespace{inbox.cpp}::database::native(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step().

Referenced by apply_one_batch().

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

◆ query_exists()

bool arachne::penelope::anonymous_namespace{inbox.cpp}::query_exists ( sqlite3 *const sql,
const std::string_view query_text,
const std::string & target,
const std::string & member )
nodiscard

Definition at line 3851 of file inbox.cpp.

3854 {
3855 statement query(sql, query_text);
3856 query.bind(1, target);
3857 query.bind(2, member);
3858 return query.step();
3859}

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step().

Referenced by merge_credits().

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

◆ query_value_set()

std::set< std::string, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::query_value_set ( sqlite3 *const sql,
const std::string_view query_text,
const std::string & id )
nodiscard

Definition at line 5076 of file inbox.cpp.

5079 {
5080 statement query(sql, query_text);
5081 query.bind(1, id);
5082 std::set<std::string, std::less<>> result;
5083 while (query.step()) {
5084 result.emplace(query.text(0));
5085 }
5086 return result;
5087}

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::text().

Here is the call graph for this function:

◆ read_batch_file()

file_snapshot arachne::penelope::anonymous_namespace{inbox.cpp}::read_batch_file ( const fs::path & path)
nodiscard

Definition at line 488 of file inbox.cpp.

488 {
489 struct stat link_state {};
490 if (::lstat(path.c_str(), &link_state) != 0) {
491 throw inbox_error(
492 "cannot inspect inbox file " + path.string() + ": "
493 + std::strerror(errno)
494 );
495 }
496 if (S_ISLNK(link_state.st_mode) || !S_ISREG(link_state.st_mode)) {
497 throw inbox_error(
498 "inbox entry is not a real regular file: " + path.string()
499 );
500 }
501 const int raw = ::open(path.c_str(), O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
502 if (raw < 0) {
503 throw inbox_error(
504 "cannot open inbox file " + path.string() + ": "
505 + std::strerror(errno)
506 );
507 }
508 file_descriptor descriptor(raw);
509 struct stat before {};
510 if (::fstat(descriptor.get(), &before) != 0 || !S_ISREG(before.st_mode)) {
511 throw inbox_error("cannot snapshot inbox file " + path.string());
512 }
513 if (before.st_size < 0
514 || static_cast<std::uintmax_t>(before.st_size) > maximum_batch_bytes) {
515 throw inbox_error("inbox batch exceeds its byte limit: " + path.string());
516 }
517 std::string bytes(static_cast<std::size_t>(before.st_size), '\0');
518 std::size_t offset = 0;
519 while (offset < bytes.size()) {
520 const ssize_t count = ::read(
521 descriptor.get(), bytes.data() + offset, bytes.size() - offset
522 );
523 if (count < 0 && errno == EINTR) {
524 continue;
525 }
526 if (count <= 0) {
527 throw inbox_error("cannot read complete inbox file " + path.string());
528 }
529 offset += static_cast<std::size_t>(count);
530 }
531 char trailing {};
532 ssize_t trailing_count;
533 do {
534 trailing_count = ::read(descriptor.get(), &trailing, 1U);
535 } while (trailing_count < 0 && errno == EINTR);
536 struct stat after {};
537 if (trailing_count != 0 || ::fstat(descriptor.get(), &after) != 0
538 || !same_identity(identity_of(before), identity_of(after))) {
539 throw inbox_error("inbox file changed while it was read: " + path.string());
540 }
541 return {
542 .path = path,
543 .bytes = std::move(bytes),
544 .identity = identity_of(after),
545 };
546}
constexpr std::uintmax_t maximum_batch_bytes
Definition inbox.cpp:48
file_identity identity_of(const struct stat &state)
Definition inbox.cpp:458
bool same_identity(const file_identity &left, const file_identity &right)
Definition inbox.cpp:449

References arachne::penelope::anonymous_namespace{inbox.cpp}::file_descriptor::file_descriptor(), arachne::penelope::anonymous_namespace{inbox.cpp}::file_descriptor::get(), identity_of(), maximum_batch_bytes, and same_identity().

Here is the call graph for this function:

◆ read_schema()

std::string arachne::penelope::anonymous_namespace{inbox.cpp}::read_schema ( const fs::path & path)
nodiscard

Definition at line 5721 of file inbox.cpp.

5721 {
5722 std::ifstream input(path, std::ios::binary);
5723 if (!input) {
5724 throw inbox_error("cannot open current product schema: " + path.string());
5725 }
5726 std::ostringstream buffer;
5727 buffer << input.rdbuf();
5728 if (!input.eof() && input.fail()) {
5729 throw inbox_error("cannot read current product schema: " + path.string());
5730 }
5731 return buffer.str();
5732}

◆ real_directory()

bool arachne::penelope::anonymous_namespace{inbox.cpp}::real_directory ( const fs::path & path)
nodiscard

Definition at line 5690 of file inbox.cpp.

5690 {
5691 struct stat state {};
5692 return ::lstat(path.c_str(), &state) == 0 && S_ISDIR(state.st_mode)
5693 && !S_ISLNK(state.st_mode);
5694}

Referenced by move_rejected_unchanged(), and require_real_directory().

Here is the caller graph for this function:

◆ rebuild_hints()

std::size_t arachne::penelope::anonymous_namespace{inbox.cpp}::rebuild_hints ( database & product)
nodiscard

Definition at line 5543 of file inbox.cpp.

5543 {
5544 transaction change(product);
5545 product.execute("DELETE FROM merge_hints WHERE status='open'");
5546 product.execute("DELETE FROM merge_hint_blocks");
5547 const auto labels = all_hint_labels(product.native());
5548 std::map<std::string, const hint_label*, std::less<>> by_id;
5549 for (const auto& label : labels) {
5550 by_id.emplace(label.id, &label);
5551 insert_hint_blocks(product.native(), label);
5552 }
5553 for (const auto& [left, right] :
5554 all_blocked_pairs(product.native())) {
5555 const auto left_label = by_id.find(left);
5556 const auto right_label = by_id.find(right);
5557 if (left_label != by_id.end() && right_label != by_id.end()
5558 && left_label->second->family == right_label->second->family) {
5560 product.native(), *left_label->second, *right_label->second
5561 );
5562 }
5563 }
5564 statement count(
5565 product.native(),
5566 "SELECT count(*) FROM merge_hints WHERE status='open'"
5567 );
5568 if (!count.step()) {
5569 throw database_error("cannot count rebuilt merge hints");
5570 }
5571 const std::size_t inserted
5572 = static_cast<std::size_t>(count.integer(0));
5573 change.commit();
5574 return inserted;
5575}
void upsert_hint(sqlite3 *const sql, const hint_label &first, const hint_label &second)
Definition inbox.cpp:5393
std::set< hint_pair > all_blocked_pairs(sqlite3 *const sql)
Definition inbox.cpp:5006

References arachne::penelope::anonymous_namespace{inbox.cpp}::transaction::commit(), arachne::penelope::anonymous_namespace{inbox.cpp}::statement::integer(), arachne::penelope::anonymous_namespace{inbox.cpp}::database::native(), arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step(), and arachne::penelope::anonymous_namespace{inbox.cpp}::transaction::transaction().

Referenced by arachne::penelope::rebuild_product_merge_hints().

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

◆ record_and_move_rejected()

void arachne::penelope::anonymous_namespace{inbox.cpp}::record_and_move_rejected ( parsed_batch & batch,
database & product,
const fs::path & rejected )

Definition at line 5967 of file inbox.cpp.

5969 {
5970 try {
5971 record_issues(product, batch.batch_id, batch.issues);
5972 } catch (const std::exception& error) {
5973 add_issue(
5974 batch, "issue_storage_error", "/",
5975 "structured issues could not be stored; rejected file remains in "
5976 "place: "
5977 + std::string(error.what())
5978 );
5979 return;
5980 }
5981 try {
5983 } catch (const std::exception& error) {
5984 add_issue(
5985 batch, "rejected_file_not_moved", "/",
5986 std::string(error.what())
5987 );
5988 }
5989}
void record_issues(database &product, const std::string &batch_id, const std::vector< inbox_issue > &issues)
Definition inbox.cpp:5596
void move_rejected_unchanged(const file_snapshot &snapshot, const fs::path &rejected)
Definition inbox.cpp:5910

References arachne::penelope::anonymous_namespace{inbox.cpp}::parsed_batch::batch_id.

◆ record_hint_candidate()

void arachne::penelope::anonymous_namespace{inbox.cpp}::record_hint_candidate ( hint_candidate_map & candidates,
const std::string & first,
const std::string & second,
const std::string_view block_type,
const std::string_view block_key )

Definition at line 4747 of file inbox.cpp.

4751 {
4752 if (first == second) {
4753 return;
4754 }
4755 auto& support = candidates[
4756 {
4757 std::min(first, second),
4758 std::max(first, second),
4759 }
4760 ];
4761 if (is_trigram_block(block_type)) {
4762 support.shared_trigrams.emplace(trigram_support_key(block_key));
4763 } else {
4764 support.direct = true;
4765 }
4766}
bool is_trigram_block(const std::string_view block_type)
Definition inbox.cpp:4701
std::string trigram_support_key(const std::string_view block_key)
Definition inbox.cpp:4738

◆ record_issues()

void arachne::penelope::anonymous_namespace{inbox.cpp}::record_issues ( database & product,
const std::string & batch_id,
const std::vector< inbox_issue > & issues )

Definition at line 5596 of file inbox.cpp.

5599 {
5600 if (!valid_batch_id(batch_id) || issues.empty()) {
5601 return;
5602 }
5603 transaction change(product);
5604 statement insert(
5605 product.native(),
5606 "INSERT INTO ingest_issues("
5607 "batch_id,code,json_path,message,value_json,status)"
5608 " VALUES(?,?,?,?,?,'open') "
5609 "ON CONFLICT(batch_id,code,json_path) DO UPDATE SET "
5610 "message=excluded.message,value_json=excluded.value_json,status='open'"
5611 );
5612 for (const auto& issue : issues) {
5613 sqlite3_reset(insert.native());
5614 sqlite3_clear_bindings(insert.native());
5615 insert.bind(1, batch_id);
5616 insert.bind(2, issue.code);
5617 insert.bind(3, issue.json_path);
5618 insert.bind(4, issue.message);
5619 if (issue.value_json.empty()) {
5620 insert.bind_null(5);
5621 } else {
5622 insert.bind(5, issue.value_json);
5623 }
5624 insert.execute();
5625 }
5626 change.commit();
5627}
bool valid_batch_id(const std::string_view value)
Definition inbox.cpp:603

References arachne::penelope::anonymous_namespace{inbox.cpp}::transaction::commit(), arachne::penelope::anonymous_namespace{inbox.cpp}::database::native(), and arachne::penelope::anonymous_namespace{inbox.cpp}::transaction::transaction().

Here is the call graph for this function:

◆ ref_string()

std::string arachne::penelope::anonymous_namespace{inbox.cpp}::ref_string ( const json & record,
const char * key )
nodiscard

Definition at line 2352 of file inbox.cpp.

2352 {
2353 return record.at(key).get<std::string>();
2354}

◆ refresh_hint_blocks()

void arachne::penelope::anonymous_namespace{inbox.cpp}::refresh_hint_blocks ( sqlite3 *const sql,
const std::set< std::string, std::less<> > & affected,
std::map< std::string, hint_label, std::less<> > & label_cache )

Definition at line 4936 of file inbox.cpp.

4940 {
4941 statement remove(
4942 sql, "DELETE FROM merge_hint_block_members WHERE entity_id=?"
4943 );
4944 for (const auto& id : affected) {
4945 sqlite3_reset(remove.native());
4946 sqlite3_clear_bindings(remove.native());
4947 remove.bind(1, id);
4948 remove.execute();
4949 auto label = hint_label_for_id(sql, id);
4950 if (!label) {
4951 continue;
4952 }
4953 insert_hint_blocks(sql, *label);
4954 label_cache.insert_or_assign(id, std::move(*label));
4955 }
4956}
std::optional< hint_label > hint_label_for_id(sqlite3 *const sql, const std::string &id)
Definition inbox.cpp:4554

◆ regenerate_incremental_hints()

void arachne::penelope::anonymous_namespace{inbox.cpp}::regenerate_incremental_hints ( sqlite3 *const sql,
const std::set< std::string, std::less<> > & affected )

Definition at line 5499 of file inbox.cpp.

5502 {
5503 if (affected.empty()) {
5504 return;
5505 }
5506 statement remove(
5507 sql,
5508 "DELETE FROM merge_hints WHERE status='open' "
5509 "AND (left_id=? OR right_id=?)"
5510 );
5511 for (const auto& id : affected) {
5512 sqlite3_reset(remove.native());
5513 sqlite3_clear_bindings(remove.native());
5514 remove.bind(1, id);
5515 remove.bind(2, id);
5516 remove.execute();
5517 }
5519 std::map<std::string, hint_label, std::less<>> labels;
5520 refresh_hint_blocks(sql, affected, labels);
5521 for (const auto& [left, right] :
5522 incrementally_blocked_pairs(sql, affected)) {
5523 auto load = [&](const std::string& id) -> const hint_label* {
5524 const auto cached = labels.find(id);
5525 if (cached != labels.end()) {
5526 return &cached->second;
5527 }
5528 auto label = hint_label_for_id(sql, id);
5529 if (!label) {
5530 return nullptr;
5531 }
5532 return &labels.emplace(id, std::move(*label)).first->second;
5533 };
5534 const hint_label* const left_label = load(left);
5535 const hint_label* const right_label = load(right);
5536 if (left_label != nullptr && right_label != nullptr
5537 && left_label->family == right_label->family) {
5538 upsert_hint(sql, *left_label, *right_label);
5539 }
5540 }
5541}
void refresh_hint_blocks(sqlite3 *const sql, const std::set< std::string, std::less<> > &affected, std::map< std::string, hint_label, std::less<> > &label_cache)
Definition inbox.cpp:4936
void bootstrap_hint_blocks_if_empty(sqlite3 *const sql)
Definition inbox.cpp:5056
std::set< hint_pair > incrementally_blocked_pairs(sqlite3 *const sql, const std::set< std::string, std::less<> > &affected)
Definition inbox.cpp:4958

References bootstrap_hint_blocks_if_empty().

Here is the call graph for this function:

◆ reject_duplicate_pending_ids()

void arachne::penelope::anonymous_namespace{inbox.cpp}::reject_duplicate_pending_ids ( std::vector< parsed_batch > & batches,
sqlite3 *const sql )

Definition at line 5871 of file inbox.cpp.

5873 {
5874 std::map<std::string, std::vector<std::size_t>, std::less<>> occurrences;
5875 for (std::size_t index = 0; index < batches.size(); ++index) {
5876 if (batches[index].structurally_valid) {
5877 occurrences[batches[index].batch_id].push_back(index);
5878 }
5879 }
5880 for (const auto& [batch_id, indexes] : occurrences) {
5881 if (indexes.size() < 2U || batch_was_applied(sql, batch_id)) {
5882 continue;
5883 }
5884 for (const std::size_t index : indexes) {
5885 add_issue(
5886 batches[index], "duplicate_pending_batch_id", "/batch_id",
5887 "more than one pending file uses batch_id " + batch_id,
5888 &batches[index].document["batch_id"]
5889 );
5890 }
5891 }
5892}

◆ reject_preferred_name_conflict()

void arachne::penelope::anonymous_namespace{inbox.cpp}::reject_preferred_name_conflict ( sqlite3 *const sql,
const std::string & target,
const std::vector< std::string > & members )

Definition at line 3827 of file inbox.cpp.

3830 {
3831 std::set<std::string, std::less<>> preferred;
3832 auto inspect = [&](const std::string& id) {
3833 statement query(
3834 sql,
3835 "SELECT value FROM names WHERE entity_id=? AND is_preferred=1"
3836 );
3837 query.bind(1, id);
3838 while (query.step()) {
3839 preferred.emplace(query.text(0));
3840 }
3841 };
3842 inspect(target);
3843 for (const auto& member : members) {
3844 inspect(member);
3845 }
3846 if (preferred.size() > 1U) {
3847 throw database_error("unresolved preferred name or title conflict");
3848 }
3849}

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::text().

Here is the call graph for this function:

◆ remove_unchanged()

void arachne::penelope::anonymous_namespace{inbox.cpp}::remove_unchanged ( const file_snapshot & snapshot)

Definition at line 5894 of file inbox.cpp.

5894 {
5895 if (!snapshot_still_current(snapshot)) {
5896 throw inbox_error(
5897 "inbox file changed after commit and was not deleted: "
5898 + snapshot.path.string()
5899 );
5900 }
5901 std::error_code error;
5902 if (!fs::remove(snapshot.path, error) || error) {
5903 throw inbox_error(
5904 "cannot delete committed inbox file " + snapshot.path.string()
5905 + ": " + error.message()
5906 );
5907 }
5908}

References snapshot_still_current().

Here is the call graph for this function:

◆ report_for()

inbox_batch_report arachne::penelope::anonymous_namespace{inbox.cpp}::report_for ( const parsed_batch & batch,
const inbox_batch_status status )
nodiscard

Definition at line 5991 of file inbox.cpp.

5993 {
5994 return {
5995 .path = batch.file.path,
5996 .batch_id = batch.batch_id,
5997 .status = status,
5998 .issues = batch.issues,
5999 };
6000}

References arachne::penelope::anonymous_namespace{inbox.cpp}::parsed_batch::batch_id.

◆ require_clean_foreign_keys()

void arachne::penelope::anonymous_namespace{inbox.cpp}::require_clean_foreign_keys ( sqlite3 *const sql)

Definition at line 5587 of file inbox.cpp.

5587 {
5588 statement check(sql, "PRAGMA foreign_key_check");
5589 if (check.step()) {
5590 throw database_error(
5591 "foreign-key validation failed for table " + check.text(0)
5592 );
5593 }
5594}

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::text().

Referenced by apply_one_batch().

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

◆ require_current_product_structure()

void arachne::penelope::anonymous_namespace{inbox.cpp}::require_current_product_structure ( sqlite3 *const sql)

Definition at line 184 of file inbox.cpp.

184 {
185 const std::set<std::string, std::less<>> expected_tables {
186 "agents",
187 "applied_batches",
188 "concept_relation_evidence",
189 "concept_relations",
190 "concepts",
191 "credits",
192 "entities",
193 "evidence",
194 "external_ids",
195 "financial_facts",
196 "ingest_issues",
197 "manifestations",
198 "measurements",
199 "merge_hint_block_members",
200 "merge_hint_blocks",
201 "merge_hints",
202 "names",
203 "parent_guide_assertions",
204 "parent_guide_evidence",
205 "sources",
206 "work_concept_evidence",
207 "work_concepts",
208 "works",
209 };
210 std::set<std::string, std::less<>> actual_tables;
211 statement tables(
212 sql,
213 "SELECT name FROM sqlite_schema WHERE type='table' "
214 "AND name NOT LIKE 'sqlite_%'"
215 );
216 while (tables.step()) {
217 actual_tables.emplace(tables.text(0));
218 }
219 if (actual_tables != expected_tables) {
220 throw database_error(
221 "product database table set does not match schema version 5"
222 );
223 }
224
225 const std::set<std::string, std::less<>> expected_indexes {
226 "concept_relations_object_idx",
227 "credits_agent_idx",
228 "credits_logical_unique",
229 "credits_work_idx",
230 "evidence_logical_unique",
231 "external_ids_entity_idx",
232 "financial_facts_logical_unique",
233 "ingest_issues_status_idx",
234 "measurements_logical_unique",
235 "merge_hint_block_members_peer_idx",
236 "merge_hints_left_idx",
237 "merge_hints_right_idx",
238 "merge_hints_status_score_idx",
239 "names_entity_idx",
240 "names_logical_unique",
241 "sources_bibliography_fallback_unique",
242 "sources_doi_unique",
243 "sources_isbn_unique",
244 "sources_url_unique",
245 "work_concepts_concept_idx",
246 };
247 std::set<std::string, std::less<>> actual_indexes;
248 statement indexes(
249 sql,
250 "SELECT name FROM sqlite_schema WHERE type='index' "
251 "AND name NOT LIKE 'sqlite_autoindex_%'"
252 );
253 while (indexes.step()) {
254 actual_indexes.emplace(indexes.text(0));
255 }
256 if (actual_indexes != expected_indexes) {
257 throw database_error(
258 "product database index set does not match schema version 5"
259 );
260 }
261
262 const std::set<std::string, std::less<>> expected_triggers {
263 "agents_entity_type",
264 "agents_entity_type_update",
265 "concept_relation_last_evidence_delete",
266 "concepts_entity_type",
267 "entities_agent_type_update",
268 "entities_subtype_update_guard",
269 "manifestations_entity_type",
270 "merge_hint_block_members_entity_family_insert",
271 "merge_hint_block_members_entity_family_update",
272 "merge_hint_block_members_remove_orphan",
273 "merge_hint_blocks_identity_update_guard",
274 "merge_hints_entity_family_insert",
275 "merge_hints_entity_family_update",
276 "parent_guide_last_evidence_delete",
277 "work_concept_last_evidence_delete",
278 "works_entity_type",
279 };
280 std::set<std::string, std::less<>> actual_triggers;
281 statement triggers(
282 sql, "SELECT name FROM sqlite_schema WHERE type='trigger'"
283 );
284 while (triggers.step()) {
285 actual_triggers.emplace(triggers.text(0));
286 }
287 if (actual_triggers != expected_triggers) {
288 throw database_error(
289 "product database trigger set does not match schema version 5"
290 );
291 }
292 const std::vector<std::string> expected_block_columns {
293 "id", "entity_type", "block_type", "block_key"
294 };
295 std::vector<std::string> actual_block_columns;
296 statement columns(sql, "PRAGMA table_info(merge_hint_blocks)");
297 while (columns.step()) {
298 actual_block_columns.push_back(columns.text(1));
299 }
300 if (actual_block_columns != expected_block_columns) {
301 throw database_error(
302 "merge_hint_blocks columns do not match schema version 5"
303 );
304 }
305 const std::vector<std::string> expected_member_columns {
306 "id", "block_id", "entity_id"
307 };
308 std::vector<std::string> actual_member_columns;
309 statement member_columns(
310 sql, "PRAGMA table_info(merge_hint_block_members)"
311 );
312 while (member_columns.step()) {
313 actual_member_columns.push_back(member_columns.text(1));
314 }
315 if (actual_member_columns != expected_member_columns) {
316 throw database_error(
317 "merge_hint_block_members columns do not match schema version 5"
318 );
319 }
320 statement foreign_keys(
321 sql, "PRAGMA foreign_key_list(merge_hint_block_members)"
322 );
323 bool entity_cascade = false;
324 bool block_cascade = false;
325 while (foreign_keys.step()) {
326 entity_cascade = entity_cascade
327 || (
328 foreign_keys.text(2) == "entities"
329 && foreign_keys.text(3) == "entity_id"
330 && foreign_keys.text(4) == "id"
331 && foreign_keys.text(6) == "CASCADE"
332 );
333 block_cascade = block_cascade
334 || (
335 foreign_keys.text(2) == "merge_hint_blocks"
336 && foreign_keys.text(3) == "block_id"
337 && foreign_keys.text(4) == "id"
338 && foreign_keys.text(6) == "CASCADE"
339 );
340 }
341 if (!entity_cascade || !block_cascade) {
342 throw database_error(
343 "merge-hint block memberships must cascade with blocks and entities"
344 );
345 }
346}

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::text().

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

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

◆ require_enum()

void arachne::penelope::anonymous_namespace{inbox.cpp}::require_enum ( parsed_batch & batch,
const json & object,
const std::string & key,
const std::string & path,
const std::set< std::string, std::less<> > & allowed )

Definition at line 767 of file inbox.cpp.

771 {
772 require_kind(batch, object, key, path, value_kind::string);
773 const auto found = object.find(key);
774 if (found != object.end() && found->is_string()
775 && !allowed.contains(found->get<std::string>())) {
776 add_issue(
777 batch, "unknown_enum", path + "/" + pointer_escape(key),
778 "field contains an unknown enum value", &*found
779 );
780 }
781}
void require_kind(parsed_batch &batch, const json &object, const std::string &key, const std::string &path, const value_kind kind)
Definition inbox.cpp:736

References string.

◆ require_integer_range()

void arachne::penelope::anonymous_namespace{inbox.cpp}::require_integer_range ( parsed_batch & batch,
const json & object,
const std::string & key,
const std::string & path,
const std::int64_t minimum,
const std::int64_t maximum )

Definition at line 783 of file inbox.cpp.

787 {
788 require_kind(batch, object, key, path, value_kind::integer);
789 const auto found = object.find(key);
790 if (found == object.end()
791 || !(found->is_number_integer() || found->is_number_unsigned())) {
792 return;
793 }
794 try {
795 const auto value = found->get<std::int64_t>();
796 if (value < minimum || value > maximum) {
797 add_issue(
798 batch, "number_out_of_range",
799 path + "/" + pointer_escape(key), "integer is outside its range",
800 &*found
801 );
802 }
803 } catch (const json::exception&) {
804 add_issue(
805 batch, "number_out_of_range", path + "/" + pointer_escape(key),
806 "integer is outside SQLite's range", &*found
807 );
808 }
809}

References integer.

◆ require_kind()

void arachne::penelope::anonymous_namespace{inbox.cpp}::require_kind ( parsed_batch & batch,
const json & object,
const std::string & key,
const std::string & path,
const value_kind kind )

Definition at line 736 of file inbox.cpp.

739 {
740 const auto found = object.find(key);
741 if (found == object.end()) {
742 return;
743 }
744 if (!is_kind(*found, kind)) {
745 add_issue(
746 batch, "type_mismatch", path + "/" + pointer_escape(key),
747 "field has the wrong JSON type", &*found
748 );
749 }
750}
bool is_kind(const json &value, const value_kind kind)
Definition inbox.cpp:718

◆ require_nonempty_string()

void arachne::penelope::anonymous_namespace{inbox.cpp}::require_nonempty_string ( parsed_batch & batch,
const json & object,
const std::string & key,
const std::string & path )

Definition at line 752 of file inbox.cpp.

755 {
756 require_kind(batch, object, key, path, value_kind::string);
757 const auto found = object.find(key);
758 if (found != object.end() && found->is_string()
759 && found->get_ref<const std::string&>().empty()) {
760 add_issue(
761 batch, "empty_string", path + "/" + pointer_escape(key),
762 "field must not be empty", &*found
763 );
764 }
765}

References string.

◆ require_number_range()

void arachne::penelope::anonymous_namespace{inbox.cpp}::require_number_range ( parsed_batch & batch,
const json & object,
const std::string & key,
const std::string & path,
const double minimum,
const double maximum )

Definition at line 811 of file inbox.cpp.

814 {
815 require_kind(batch, object, key, path, value_kind::number);
816 const auto found = object.find(key);
817 if (found == object.end() || !found->is_number()) {
818 return;
819 }
820 const double value = found->get<double>();
821 if (!std::isfinite(value) || value < minimum || value > maximum) {
822 add_issue(
823 batch, "number_out_of_range", path + "/" + pointer_escape(key),
824 "number is outside its range", &*found
825 );
826 }
827}

References number.

◆ require_real_database_file()

void arachne::penelope::anonymous_namespace{inbox.cpp}::require_real_database_file ( const fs::path & path)

Definition at line 5707 of file inbox.cpp.

5707 {
5708 struct stat state {};
5709 if (::lstat(path.c_str(), &state) != 0) {
5710 throw inbox_error(
5711 "product database does not exist: " + path.string()
5712 );
5713 }
5714 if (!S_ISREG(state.st_mode) || S_ISLNK(state.st_mode)) {
5715 throw inbox_error(
5716 "product database must be a real regular file: " + path.string()
5717 );
5718 }
5719}

Referenced by ensure_product_database(), arachne::penelope::rebuild_product_merge_hints(), and run_inbox().

Here is the caller graph for this function:

◆ require_real_directory()

void arachne::penelope::anonymous_namespace{inbox.cpp}::require_real_directory ( const fs::path & path,
const std::string_view description )

Definition at line 5696 of file inbox.cpp.

5698 {
5699 if (!real_directory(path)) {
5700 throw inbox_error(
5701 std::string(description) + " must be a real directory: "
5702 + path.string()
5703 );
5704 }
5705}

References real_directory().

Here is the call graph for this function:

◆ resolve_entity()

std::string arachne::penelope::anonymous_namespace{inbox.cpp}::resolve_entity ( const parsed_batch & batch,
const json & value )
nodiscard

Definition at line 3037 of file inbox.cpp.

3039 {
3040 const std::string reference = value.get<std::string>();
3041 const auto local = batch.resolved_entity_ids.find(reference);
3042 return local == batch.resolved_entity_ids.end() ? reference : local->second;
3043}

◆ resolve_row_reference()

std::int64_t arachne::penelope::anonymous_namespace{inbox.cpp}::resolve_row_reference ( const std::unordered_map< std::string, std::int64_t > & locals,
const json & value )
nodiscard

Definition at line 3045 of file inbox.cpp.

3048 {
3049 if (value.is_string()) {
3050 const auto found = locals.find(value.get<std::string>());
3051 if (found == locals.end()) {
3052 throw database_error("unresolved local row reference");
3053 }
3054 return found->second;
3055 }
3056 return value.get<std::int64_t>();
3057}

◆ resolve_scalar_fields()

void arachne::penelope::anonymous_namespace{inbox.cpp}::resolve_scalar_fields ( sqlite3 *const sql,
const std::string_view table,
const std::string_view id_column,
const std::string & target,
const std::vector< std::string > & members,
const json & operation,
const std::vector< std::string > & columns,
const std::set< std::string, std::less<> > & required )

Definition at line 3754 of file inbox.cpp.

3760 {
3761 const auto explicitly_set = [&operation](const std::string& field) {
3762 return operation.at("set").contains(field);
3763 };
3764 const auto unset = string_set(operation.at("unset"));
3765 json implicit_set = json::object();
3766 for (const auto& field : columns) {
3767 if (explicitly_set(field) || unset.contains(field)) {
3768 continue;
3769 }
3770 std::set<std::string, std::less<>> values;
3771 std::optional<json> target_value;
3772 auto inspect = [&](const std::string& id, const bool is_target) {
3773 statement query(
3774 sql,
3775 "SELECT " + sql_column(field) + " FROM " + std::string(table)
3776 + " WHERE " + std::string(id_column) + "=?"
3777 );
3778 query.bind(1, id);
3779 if (!query.step()) {
3780 throw database_error("merge entity disappeared");
3781 }
3782 if (!query.is_null(0)) {
3783 const int type = sqlite3_column_type(query.native(), 0);
3784 json value;
3785 if (type == SQLITE_INTEGER) {
3786 value = query.integer(0);
3787 } else if (type == SQLITE_FLOAT) {
3788 value = query.real(0);
3789 } else {
3790 value = query.text(0);
3791 }
3792 values.emplace(value.dump());
3793 if (is_target) {
3794 target_value = value;
3795 }
3796 }
3797 };
3798 inspect(target, true);
3799 for (const auto& member : members) {
3800 inspect(member, false);
3801 }
3802 if (values.size() > 1U) {
3803 throw database_error(
3804 "unresolved merge scalar conflict for field " + field
3805 );
3806 }
3807 if (required.contains(field) && values.empty()) {
3808 throw database_error(
3809 "merge would leave required field empty: " + field
3810 );
3811 }
3812 if (!target_value && !values.empty()) {
3813 implicit_set[field] = json::parse(*values.begin());
3814 }
3815 }
3816 json target_update {
3817 { "id", target },
3818 { "set", implicit_set },
3819 { "unset", operation.at("unset") },
3820 };
3821 for (const auto& [field, value] : operation.at("set").items()) {
3822 target_update["set"][field] = value;
3823 }
3824 update_row(sql, table, id_column, target_update);
3825}
std::set< std::string, std::less<> > string_set(const json &array)
Definition inbox.cpp:3744
std::string sql_column(const std::string_view field)
Definition inbox.cpp:3541

◆ reviewable_blocked_pairs()

std::set< hint_pair > arachne::penelope::anonymous_namespace{inbox.cpp}::reviewable_blocked_pairs ( const hint_candidate_map & candidates)
nodiscard

Definition at line 4768 of file inbox.cpp.

4770 {
4771 std::set<hint_pair> result;
4772 for (const auto& [pair, support] : candidates) {
4773 /*
4774 * A non-text block supplies explicit context (for example a shared key
4775 * agent or exact fingerprint). Text-only discovery requires at least
4776 * two distinct rare trigrams; this bounds candidate volume before
4777 * scoring without turning the combined score into a persistence gate.
4778 */
4779 if (support.direct || support.shared_trigrams.size() >= 2U) {
4780 result.emplace(pair);
4781 }
4782 }
4783 return result;
4784}

◆ row_exists()

bool arachne::penelope::anonymous_namespace{inbox.cpp}::row_exists ( sqlite3 *const database_value,
const std::string_view table,
const std::string_view column,
const json & id )
nodiscard

Definition at line 2323 of file inbox.cpp.

2326 {
2327 const std::string sql = "SELECT 1 FROM " + std::string(table) + " WHERE "
2328 + std::string(column) + " = ? LIMIT 1";
2329 statement query(database_value, sql);
2330 query.bind_json_value(1, id);
2331 return query.step();
2332}

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::step().

Here is the call graph for this function:

◆ run_inbox()

inbox_result arachne::penelope::anonymous_namespace{inbox.cpp}::run_inbox ( const fs::path & repository_root,
const bool apply )
nodiscard

Definition at line 6002 of file inbox.cpp.

6004 {
6005 const fs::path inbox = repository_root / "inbox";
6006 const fs::path database_path
6007 = repository_root / "database" / "art-islands.sqlite";
6008 if (apply) {
6009 ensure_product_database(repository_root, database_path);
6010 } else {
6011 require_real_database_file(database_path);
6012 }
6013 auto snapshots = snapshot_inbox(inbox);
6014 std::vector<parsed_batch> batches;
6015 batches.reserve(snapshots.size());
6016 for (auto& snapshot : snapshots) {
6017 batches.push_back(parse_batch(std::move(snapshot)));
6018 }
6019 database product(database_path, apply);
6020 reject_duplicate_pending_ids(batches, product.native());
6021 auto allocation = initial_allocation_state(product);
6022 for (auto& batch : batches) {
6023 if (!batch.structurally_valid || !batch.issues.empty()) {
6024 continue;
6025 }
6026 if (batch_was_applied(product.native(), batch.batch_id)) {
6027 batch.already_applied = true;
6028 continue;
6029 }
6030 prevalidate_semantics(batch, product);
6031 if (batch.issues.empty()) {
6032 allocate_local_references(batch, allocation);
6033 }
6034 }
6035
6036 inbox_result result;
6037 result.applied = apply;
6038 for (auto& batch : batches) {
6039 if (!batch.issues.empty()) {
6040 ++result.rejected_count;
6041 if (apply && valid_batch_id(batch.batch_id)) {
6043 batch, product, inbox / "rejected"
6044 );
6045 }
6046 result.batches.push_back(
6048 );
6049 continue;
6050 }
6051 if (batch.already_applied) {
6052 ++result.already_applied_count;
6053 if (apply) {
6054 remove_unchanged(batch.file);
6055 }
6056 result.batches.push_back(
6058 );
6059 continue;
6060 }
6061 if (!apply) {
6062 ++result.valid_count;
6063 result.batches.push_back(
6065 );
6066 continue;
6067 }
6068 try {
6069 apply_one_batch(batch, product);
6070 } catch (const std::exception& error) {
6071 if (batch.issues.empty()) {
6072 const std::string message = error.what();
6073 std::string code = "application_error";
6074 if (batch.application_path.starts_with("/merge/")) {
6075 code = "merge_conflict";
6076 } else if (
6077 message.find("constraint") != std::string::npos
6078 || message.find("UNIQUE") != std::string::npos
6079 || message.find("CHECK") != std::string::npos
6080 || message.find("FOREIGN KEY") != std::string::npos
6081 ) {
6082 code = "constraint_violation";
6083 } else if (message.find("disappeared") != std::string::npos
6084 || message.find("changed")
6085 != std::string::npos) {
6086 code = "concurrent_change";
6087 }
6088 std::optional<json> rejected_value;
6089 if (!batch.application_value_json.empty()) {
6090 rejected_value = json::parse(
6091 batch.application_value_json
6092 );
6093 }
6094 add_issue(
6095 batch, code, batch.application_path, message,
6096 rejected_value ? &*rejected_value : nullptr
6097 );
6098 }
6099 ++result.rejected_count;
6101 batch, product, inbox / "rejected"
6102 );
6103 result.batches.push_back(
6105 );
6106 continue;
6107 }
6108 if (batch.already_applied) {
6109 ++result.already_applied_count;
6110 result.batches.push_back(
6112 );
6113 } else {
6114 ++result.applied_count;
6115 result.batches.push_back(
6117 );
6118 }
6119 /*
6120 * Retirement is deliberately outside the database-application catch.
6121 * A post-commit filesystem failure must never relabel committed data as
6122 * rejected or create a false application issue. The still-present file
6123 * is safely retired as an already-applied batch on the next run.
6124 */
6125 remove_unchanged(batch.file);
6126 }
6127 result.ok = result.rejected_count == 0U;
6128 return result;
6129}
local_id_allocation_state initial_allocation_state(database &product)
Definition inbox.cpp:2935
void reject_duplicate_pending_ids(std::vector< parsed_batch > &batches, sqlite3 *const sql)
Definition inbox.cpp:5871
void apply_one_batch(parsed_batch &batch, database &product)
Definition inbox.cpp:5629
parsed_batch parse_batch(file_snapshot snapshot)
Definition inbox.cpp:5835
inbox_batch_report report_for(const parsed_batch &batch, const inbox_batch_status status)
Definition inbox.cpp:5991
void ensure_product_database(const fs::path &repository_root, const fs::path &path)
Definition inbox.cpp:5734
void allocate_local_references(parsed_batch &batch, local_id_allocation_state &sequences)
Definition inbox.cpp:2968
void remove_unchanged(const file_snapshot &snapshot)
Definition inbox.cpp:5894
void record_and_move_rejected(parsed_batch &batch, database &product, const fs::path &rejected)
Definition inbox.cpp:5967
std::vector< file_snapshot > snapshot_inbox(const fs::path &inbox)
Definition inbox.cpp:5787

References arachne::penelope::inbox_result::applied, arachne::penelope::anonymous_namespace{inbox.cpp}::database::database(), ensure_product_database(), initial_allocation_state(), arachne::penelope::anonymous_namespace{inbox.cpp}::database::native(), arachne::penelope::inbox_result::ok, arachne::penelope::inbox_result::rejected_count, and require_real_database_file().

Referenced by arachne::penelope::apply_product_inbox(), and arachne::penelope::check_product_inbox().

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

◆ same_identity()

◆ set_application_context()

void arachne::penelope::anonymous_namespace{inbox.cpp}::set_application_context ( parsed_batch & batch,
const std::string_view path,
const json & value )

Definition at line 902 of file inbox.cpp.

904 {
905 batch.application_path = path.empty() ? "/" : std::string(path);
906 batch.application_value_json = value.dump();
907}

References arachne::penelope::anonymous_namespace{inbox.cpp}::parsed_batch::application_value_json.

◆ snapshot_inbox()

std::vector< file_snapshot > arachne::penelope::anonymous_namespace{inbox.cpp}::snapshot_inbox ( const fs::path & inbox)
nodiscard

Definition at line 5787 of file inbox.cpp.

5787 {
5788 require_real_directory(inbox, "inbox");
5789 std::vector<fs::path> paths;
5790 std::error_code error;
5791 for (fs::directory_iterator iterator(inbox, error), end;
5792 !error && iterator != end; iterator.increment(error)) {
5793 const fs::path path = iterator->path();
5794 const std::string filename = path.filename().string();
5795 struct stat state {};
5796 if (::lstat(path.c_str(), &state) != 0) {
5797 throw inbox_error("cannot inspect inbox entry: " + path.string());
5798 }
5799 if (filename == "rejected" && S_ISDIR(state.st_mode)
5800 && !S_ISLNK(state.st_mode)) {
5801 continue;
5802 }
5803 if (S_ISLNK(state.st_mode)) {
5804 throw inbox_error("symbolic link in inbox is rejected: " + path.string());
5805 }
5806 if (S_ISDIR(state.st_mode)) {
5807 throw inbox_error(
5808 "unexpected directory in inbox: " + path.string()
5809 );
5810 }
5811 if (!S_ISREG(state.st_mode)) {
5812 throw inbox_error(
5813 "non-regular inbox entry is rejected: " + path.string()
5814 );
5815 }
5816 if (path.extension() != ".json") {
5817 throw inbox_error(
5818 "only plain .json batch files are accepted: " + path.string()
5819 );
5820 }
5821 paths.push_back(path);
5822 }
5823 if (error) {
5824 throw inbox_error("cannot scan inbox: " + error.message());
5825 }
5826 std::ranges::sort(paths);
5827 std::vector<file_snapshot> result;
5828 result.reserve(paths.size());
5829 for (const auto& path : paths) {
5830 result.push_back(read_batch_file(path));
5831 }
5832 return result;
5833}
file_snapshot read_batch_file(const fs::path &path)
Definition inbox.cpp:488

◆ snapshot_still_current()

bool arachne::penelope::anonymous_namespace{inbox.cpp}::snapshot_still_current ( const file_snapshot & snapshot)
nodiscard

Definition at line 548 of file inbox.cpp.

548 {
549 struct stat state {};
550 return ::lstat(snapshot.path.c_str(), &state) == 0
551 && S_ISREG(state.st_mode) && !S_ISLNK(state.st_mode)
552 && same_identity(snapshot.identity, identity_of(state));
553}

References arachne::penelope::anonymous_namespace{inbox.cpp}::file_snapshot::identity, identity_of(), and same_identity().

Referenced by move_rejected_unchanged(), and remove_unchanged().

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

◆ sql_column()

std::string arachne::penelope::anonymous_namespace{inbox.cpp}::sql_column ( const std::string_view field)
nodiscard

Definition at line 3541 of file inbox.cpp.

3541 {
3542 return std::string(field);
3543}

◆ sqlite_message()

std::string arachne::penelope::anonymous_namespace{inbox.cpp}::sqlite_message ( sqlite3 *const value,
const std::string_view operation )
nodiscard

Definition at line 56 of file inbox.cpp.

58 {
59 return std::string(operation) + ": "
60 + (value == nullptr ? "SQLite error" : sqlite3_errmsg(value));
61}

◆ string_set()

std::set< std::string, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::string_set ( const json & array)
nodiscard

Definition at line 3744 of file inbox.cpp.

3746 {
3747 std::set<std::string, std::less<>> result;
3748 for (const auto& value : array) {
3749 result.emplace(value.get<std::string>());
3750 }
3751 return result;
3752}

◆ transfer_assertion_evidence()

void arachne::penelope::anonymous_namespace{inbox.cpp}::transfer_assertion_evidence ( sqlite3 *const sql,
const std::string_view evidence_table,
const std::int64_t keeper,
const std::int64_t duplicate )

Definition at line 3960 of file inbox.cpp.

3963 {
3964 statement transfer(
3965 sql,
3966 "INSERT OR IGNORE INTO " + std::string(evidence_table)
3967 + "(assertion_id,evidence_id) SELECT ?,evidence_id FROM "
3968 + std::string(evidence_table) + " WHERE assertion_id=?"
3969 );
3970 transfer.bind(1, keeper);
3971 transfer.bind(2, duplicate);
3972 transfer.execute();
3973}

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::bind(), and arachne::penelope::anonymous_namespace{inbox.cpp}::statement::execute().

Here is the call graph for this function:

◆ trigram_score()

double arachne::penelope::anonymous_namespace{inbox.cpp}::trigram_score ( const std::string_view left,
const std::string_view right )
nodiscard

Definition at line 4671 of file inbox.cpp.

4673 {
4674 const auto left_values = trigrams(left);
4675 const auto right_values = trigrams(right);
4676 std::size_t intersection = 0;
4677 for (const auto& value : left_values) {
4678 intersection += static_cast<std::size_t>(
4679 right_values.contains(value)
4680 );
4681 }
4682 const std::size_t combined
4683 = left_values.size() + right_values.size() - intersection;
4684 return combined == 0U
4685 ? 1.0
4686 : static_cast<double>(intersection)
4687 / static_cast<double>(combined);
4688}

◆ trigram_support_key()

std::string arachne::penelope::anonymous_namespace{inbox.cpp}::trigram_support_key ( const std::string_view block_key)
nodiscard

Definition at line 4738 of file inbox.cpp.

4740 {
4741 const std::size_t separator = block_key.rfind('\n');
4742 return separator == std::string_view::npos
4743 ? std::string(block_key)
4744 : std::string(block_key.substr(separator + 1U));
4745}

◆ trigrams()

std::set< std::string, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::trigrams ( const std::string_view value)
nodiscard

Definition at line 4628 of file inbox.cpp.

4628 {
4629 std::set<std::string, std::less<>> result;
4630 std::vector<std::string_view> characters;
4631 std::size_t offset = 0;
4632 while (offset < value.size()) {
4633 utf8proc_int32_t code_point = 0;
4634 const utf8proc_ssize_t width = utf8proc_iterate(
4635 reinterpret_cast<const utf8proc_uint8_t*>(
4636 value.data() + offset
4637 ),
4638 static_cast<utf8proc_ssize_t>(value.size() - offset),
4639 &code_point
4640 );
4641 if (width <= 0) {
4642 throw inbox_error(
4643 "normalized label contains invalid UTF-8"
4644 );
4645 }
4646 characters.emplace_back(
4647 value.data() + offset, static_cast<std::size_t>(width)
4648 );
4649 offset += static_cast<std::size_t>(width);
4650 }
4651 if (characters.size() < 3U) {
4652 if (!value.empty()) {
4653 result.emplace(value);
4654 }
4655 return result;
4656 }
4657 for (std::size_t index = 0; index + 3U <= characters.size(); ++index) {
4658 std::string trigram;
4659 trigram.reserve(
4660 characters[index].size() + characters[index + 1U].size()
4661 + characters[index + 2U].size()
4662 );
4663 trigram += characters[index];
4664 trigram += characters[index + 1U];
4665 trigram += characters[index + 2U];
4666 result.emplace(std::move(trigram));
4667 }
4668 return result;
4669}

◆ update_row()

void arachne::penelope::anonymous_namespace{inbox.cpp}::update_row ( sqlite3 *const sql,
const std::string_view table,
const std::string_view id_column,
const json & operation )

Definition at line 3545 of file inbox.cpp.

3548 {
3549 std::vector<std::pair<std::string, const json*>> assignments;
3550 for (const auto& [field, value] : operation.at("set").items()) {
3551 assignments.emplace_back(field, &value);
3552 }
3553 for (const auto& field : operation.at("unset")) {
3554 assignments.emplace_back(field.get<std::string>(), nullptr);
3555 }
3556 if (assignments.empty()) {
3557 return;
3558 }
3559 std::string sql_text = "UPDATE " + std::string(table) + " SET ";
3560 for (std::size_t index = 0; index < assignments.size(); ++index) {
3561 if (index != 0U) {
3562 sql_text += ",";
3563 }
3564 sql_text += sql_column(assignments[index].first) + "=?";
3565 }
3566 sql_text += " WHERE " + std::string(id_column) + "=?";
3567 statement update(sql, sql_text);
3568 int binding = 1;
3569 for (const auto& [field, value] : assignments) {
3570 if (value == nullptr) {
3571 update.bind_null(binding);
3572 } else {
3573 update.bind_json_value(binding, *value);
3574 }
3575 ++binding;
3576 }
3577 update.bind_json_value(binding, operation.at("id"));
3578 update.execute();
3579 if (sqlite3_changes(sql) != 1) {
3580 throw database_error("update target disappeared before mutation");
3581 }
3582}

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::execute().

Here is the call graph for this function:

◆ upsert_hint()

void arachne::penelope::anonymous_namespace{inbox.cpp}::upsert_hint ( sqlite3 *const sql,
const hint_label & first,
const hint_label & second )

Definition at line 5393 of file inbox.cpp.

5395 {
5396 const hint_label& left = first.id < second.id ? first : second;
5397 const hint_label& right = first.id < second.id ? second : first;
5398 bool fingerprint_equal = false;
5399 double token_sort = 0.0;
5400 double token_set = 0.0;
5401 double edit = 0.0;
5402 double trigram_context = 0.0;
5403 double text_score = 0.0;
5404 std::string best_left = left.fingerprint;
5405 std::string best_right = right.fingerprint;
5406 for (const auto& left_value : left.fingerprints) {
5407 for (const auto& right_value : right.fingerprints) {
5408 fingerprint_equal
5409 = fingerprint_equal || left_value == right_value;
5410 const double current_sort
5411 = rapidfuzz::fuzz::token_sort_ratio(
5412 left_value, right_value
5413 )
5414 / 100.0;
5415 const double current_set
5416 = rapidfuzz::fuzz::token_set_ratio(
5417 left_value, right_value
5418 )
5419 / 100.0;
5420 const double current_edit
5421 = rapidfuzz::fuzz::ratio(left_value, right_value, 35.0)
5422 / 100.0;
5423 const double current_trigram
5424 = trigram_score(left_value, right_value);
5425 const double current_text
5426 = std::max({ current_sort, current_set, current_edit });
5427 token_sort = std::max(token_sort, current_sort);
5428 token_set = std::max(token_set, current_set);
5429 edit = std::max(edit, current_edit);
5430 trigram_context
5431 = std::max(trigram_context, current_trigram);
5432 if (current_text > text_score) {
5433 text_score = current_text;
5434 best_left = left_value;
5435 best_right = right_value;
5436 }
5437 }
5438 }
5439 hint_components components
5440 = calculate_hint_components(sql, left, right);
5441 components.context = std::max(components.context, trigram_context);
5442 const double score = std::clamp(
5443 0.65 * text_score + 0.25 * components.graph
5444 + 0.10 * components.context,
5445 0.0, 1.0
5446 );
5447 json signals {
5448 { "token_fingerprint_equal", fingerprint_equal },
5449 { "token_sort_ratio", token_sort },
5450 { "token_set_ratio", token_set },
5451 { "normalized_edit_similarity", edit },
5452 { "trigram_jaccard", trigram_context },
5453 { "normalized_left", best_left },
5454 { "normalized_right", best_right },
5455 { "alternate_label_match",
5456 best_left != left.fingerprint || best_right != right.fingerprint },
5457 };
5458 if (left.family == "concept") {
5459 signals["slug_similarity"] = std::max(
5460 {
5461 rapidfuzz::fuzz::token_sort_ratio(
5462 left.fingerprint, right.fingerprint
5463 ),
5464 rapidfuzz::fuzz::token_set_ratio(
5465 left.fingerprint, right.fingerprint
5466 ),
5467 rapidfuzz::fuzz::ratio(
5468 left.fingerprint, right.fingerprint, 35.0
5469 ),
5470 }
5471 ) / 100.0;
5472 } else {
5473 signals[left.family == "agent"
5474 ? "alias_similarity"
5475 : "alternate_title_similarity"] = text_score;
5476 }
5477 signals.update(components.signals);
5478 statement insert(
5479 sql,
5480 "INSERT INTO merge_hints("
5481 "entity_type,left_id,right_id,score,text_score,graph_score,"
5482 "context_score,signals_json,status) VALUES(?,?,?,?,?,?,?,?,'open') "
5483 "ON CONFLICT(entity_type,left_id,right_id) DO UPDATE SET "
5484 "score=excluded.score,text_score=excluded.text_score,"
5485 "graph_score=excluded.graph_score,context_score=excluded.context_score,"
5486 "signals_json=excluded.signals_json WHERE merge_hints.status='open'"
5487 );
5488 insert.bind(1, left.family);
5489 insert.bind(2, left.id);
5490 insert.bind(3, right.id);
5491 insert.bind(4, score);
5492 insert.bind(5, text_score);
5493 insert.bind(6, components.graph);
5494 insert.bind(7, components.context);
5495 insert.bind(8, signals.dump());
5496 insert.execute();
5497}
hint_components calculate_hint_components(sqlite3 *const sql, const hint_label &left, const hint_label &right)
Definition inbox.cpp:5168
double trigram_score(const std::string_view left, const std::string_view right)
Definition inbox.cpp:4671

References arachne::penelope::anonymous_namespace{inbox.cpp}::statement::bind(), calculate_hint_components(), arachne::penelope::anonymous_namespace{inbox.cpp}::hint_components::context, arachne::penelope::anonymous_namespace{inbox.cpp}::statement::execute(), arachne::penelope::anonymous_namespace{inbox.cpp}::hint_label::family, arachne::penelope::anonymous_namespace{inbox.cpp}::hint_label::fingerprint, arachne::penelope::anonymous_namespace{inbox.cpp}::hint_components::graph, and arachne::penelope::anonymous_namespace{inbox.cpp}::hint_label::id.

Here is the call graph for this function:

◆ valid_batch_id()

bool arachne::penelope::anonymous_namespace{inbox.cpp}::valid_batch_id ( const std::string_view value)
nodiscard

Definition at line 603 of file inbox.cpp.

603 {
604 if (value.empty() || value.size() > 128U) {
605 return false;
606 }
607 return std::ranges::all_of(value, [](const unsigned char character) {
608 return std::isalnum(character) != 0 || character == '-'
609 || character == '_' || character == '.' || character == ':';
610 });
611}

◆ valid_canonical_id()

bool arachne::penelope::anonymous_namespace{inbox.cpp}::valid_canonical_id ( const std::string_view value,
const std::string_view family )
nodiscard

Definition at line 624 of file inbox.cpp.

626 {
627 std::string_view prefix;
628 if (family == "agent") {
629 prefix = "agent-";
630 } else if (family == "work") {
631 prefix = "work-";
632 } else if (family == "concept") {
633 prefix = "concept-";
634 } else if (family == "manifestation") {
635 prefix = "manifestation-";
636 } else {
637 return false;
638 }
639 if (!value.starts_with(prefix) || value.size() < prefix.size() + 6U) {
640 return false;
641 }
642 return std::ranges::all_of(
643 value.substr(prefix.size()),
644 [](const unsigned char character) { return std::isdigit(character) != 0; }
645 );
646}

◆ valid_local_id()

bool arachne::penelope::anonymous_namespace{inbox.cpp}::valid_local_id ( const std::string_view value)
nodiscard

Definition at line 613 of file inbox.cpp.

613 {
614 if (value.empty() || value.size() > 128U
615 || std::isalnum(static_cast<unsigned char>(value.front())) == 0) {
616 return false;
617 }
618 return std::ranges::all_of(value, [](const unsigned char character) {
619 return std::isalnum(character) != 0 || character == '.'
620 || character == '_' || character == ':' || character == '-';
621 });
622}

◆ valid_slug()

bool arachne::penelope::anonymous_namespace{inbox.cpp}::valid_slug ( const std::string_view value)
nodiscard

Definition at line 657 of file inbox.cpp.

657 {
658 if (value.empty() || value.front() == '-' || value.back() == '-') {
659 return false;
660 }
661 bool previous_dash = false;
662 for (const char raw_character : value) {
663 const auto character = static_cast<unsigned char>(raw_character);
664 if (character == '-') {
665 if (previous_dash) {
666 return false;
667 }
668 previous_dash = true;
669 } else {
670 if (!(std::isdigit(character) != 0
671 || (character >= 'a' && character <= 'z'))) {
672 return false;
673 }
674 previous_dash = false;
675 }
676 }
677 return true;
678}

◆ validate_array()

template<typename Validator>
void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_array ( parsed_batch & batch,
const json & parent,
const std::string & key,
const std::string & parent_path,
Validator && validator )

Definition at line 1521 of file inbox.cpp.

1524 {
1525 const auto found = parent.find(key);
1526 if (found == parent.end()) {
1527 return;
1528 }
1529 const std::string path = parent_path + "/" + pointer_escape(key);
1530 if (!found->is_array()) {
1531 add_issue(
1532 batch, "type_mismatch", path, "operation collection must be an array",
1533 &*found
1534 );
1535 return;
1536 }
1537 for (std::size_t index = 0; index < found->size(); ++index) {
1538 validator(batch, (*found)[index], indexed_path(path, index));
1539 }
1540}

◆ validate_create_agent()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_create_agent ( parsed_batch & batch,
const json & value,
const std::string & path,
std::unordered_map< std::string, std::string > & locals )

Definition at line 987 of file inbox.cpp.

990 {
992 batch, value, path,
993 { "local_id", "agent_type", "birth_year", "death_year" },
994 { "local_id", "agent_type" }
995 );
996 if (!value.is_object()) {
997 return;
998 }
999 validate_local_id(batch, value, path, locals, "agent");
1000 require_enum(batch, value, "agent_type", path, agent_types);
1001 require_integer_range(batch, value, "birth_year", path, -9999, 9999);
1002 require_integer_range(batch, value, "death_year", path, -9999, 9999);
1003}
void require_integer_range(parsed_batch &batch, const json &object, const std::string &key, const std::string &path, const std::int64_t minimum, const std::int64_t maximum)
Definition inbox.cpp:783
void check_keys(parsed_batch &batch, const json &object, const std::string &path, const std::set< std::string, std::less<> > &allowed, const std::set< std::string, std::less<> > &required={})
Definition inbox.cpp:680
void require_enum(parsed_batch &batch, const json &object, const std::string &key, const std::string &path, const std::set< std::string, std::less<> > &allowed)
Definition inbox.cpp:767
const std::set< std::string, std::less<> > agent_types
Definition inbox.cpp:829
void validate_local_id(parsed_batch &batch, const json &record, const std::string &path, std::unordered_map< std::string, std::string > &locals, const std::string_view family)
Definition inbox.cpp:909

◆ validate_create_concept()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_create_concept ( parsed_batch & batch,
const json & value,
const std::string & path,
std::unordered_map< std::string, std::string > & locals )

Definition at line 1064 of file inbox.cpp.

1067 {
1068 check_keys(
1069 batch, value, path, { "local_id", "concept_type", "slug" },
1070 { "local_id", "concept_type", "slug" }
1071 );
1072 if (!value.is_object()) {
1073 return;
1074 }
1075 validate_local_id(batch, value, path, locals, "concept");
1076 require_enum(batch, value, "concept_type", path, concept_types);
1077 require_nonempty_string(batch, value, "slug", path);
1078 if (value.contains("slug") && value["slug"].is_string()
1079 && !valid_slug(value["slug"].get_ref<const std::string&>())) {
1080 add_issue(
1081 batch, "invalid_slug", path + "/slug",
1082 "slug must contain lowercase alphanumeric dash-separated tokens",
1083 &value["slug"]
1084 );
1085 }
1086}
bool valid_slug(const std::string_view value)
Definition inbox.cpp:657
const std::set< std::string, std::less<> > concept_types
Definition inbox.cpp:841
void require_nonempty_string(parsed_batch &batch, const json &object, const std::string &key, const std::string &path)
Definition inbox.cpp:752

◆ validate_create_concept_relation()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_create_concept_relation ( parsed_batch & batch,
const json & value,
const std::string & path,
std::unordered_map< std::string, std::string > & locals )

Definition at line 1442 of file inbox.cpp.

1445 {
1446 check_keys(
1447 batch, value, path,
1448 { "local_id", "subject_concept_id", "relation_type",
1449 "object_concept_id", "strength",
1450 "from_year", "to_year", "region_code", "confidence", "evidence" },
1451 { "local_id", "subject_concept_id", "relation_type",
1452 "object_concept_id", "evidence" }
1453 );
1454 if (!value.is_object()) {
1455 return;
1456 }
1457 validate_local_id(batch, value, path, locals, "concept_relation");
1459 batch, value, "subject_concept_id", path
1460 );
1462 batch, value, "object_concept_id", path
1463 );
1465 batch, value, "relation_type", path, concept_relation_types
1466 );
1467 require_integer_range(batch, value, "strength", path, 1, 100);
1468 require_integer_range(batch, value, "from_year", path, -9999, 9999);
1469 require_integer_range(batch, value, "to_year", path, -9999, 9999);
1470 if (value.contains("from_year") && value["from_year"].is_number_integer()
1471 && value.contains("to_year") && value["to_year"].is_number_integer()
1472 && value["to_year"].get<std::int64_t>()
1473 < value["from_year"].get<std::int64_t>()) {
1474 add_issue(
1475 batch, "invalid_range", path + "/to_year",
1476 "to_year must not be earlier than from_year", &value["to_year"]
1477 );
1478 }
1479 if (value.contains("region_code")) {
1480 require_nonempty_string(batch, value, "region_code", path);
1481 }
1482 require_number_range(batch, value, "confidence", path, 0.0, 1.0);
1483 validate_evidence_list(batch, value, path);
1484}
const std::set< std::string, std::less<> > concept_relation_types
Definition inbox.cpp:875
void validate_entity_reference_shape(parsed_batch &batch, const json &record, const std::string &key, const std::string &path)
Definition inbox.cpp:941
void validate_evidence_list(parsed_batch &batch, const json &value, const std::string &path)
Definition inbox.cpp:1344
void require_number_range(parsed_batch &batch, const json &object, const std::string &key, const std::string &path, const double minimum, const double maximum)
Definition inbox.cpp:811

◆ validate_create_credit()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_create_credit ( parsed_batch & batch,
const json & value,
const std::string & path )

Definition at line 1235 of file inbox.cpp.

1237 {
1238 check_keys(
1239 batch, value, path,
1240 { "work_id", "agent_id", "role", "credit_order", "importance",
1241 "credited_as" },
1242 { "work_id", "agent_id", "role", "importance" }
1243 );
1244 if (!value.is_object()) {
1245 return;
1246 }
1247 validate_entity_reference_shape(batch, value, "work_id", path);
1248 validate_entity_reference_shape(batch, value, "agent_id", path);
1249 require_enum(batch, value, "role", path, credit_roles);
1250 require_enum(batch, value, "importance", path, importance_values);
1252 batch, value, "credit_order", path, 0,
1253 std::numeric_limits<std::int64_t>::max()
1254 );
1255 if (value.contains("credited_as")) {
1256 require_nonempty_string(batch, value, "credited_as", path);
1257 }
1258}
const std::set< std::string, std::less<> > credit_roles
Definition inbox.cpp:860
const std::set< std::string, std::less<> > importance_values
Definition inbox.cpp:866

◆ validate_create_evidence()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_create_evidence ( parsed_batch & batch,
const json & value,
const std::string & path,
std::unordered_map< std::string, std::string > & locals )

Definition at line 1192 of file inbox.cpp.

1195 {
1196 check_keys(
1197 batch, value, path,
1198 { "local_id", "source_id", "exact_quote", "quote_language",
1199 "quote_translation", "locator_json", "stance" },
1200 { "local_id", "source_id", "exact_quote", "stance" }
1201 );
1202 if (!value.is_object()) {
1203 return;
1204 }
1205 validate_local_id(batch, value, path, locals, "evidence");
1207 batch, value, "source_id", path
1208 );
1209 require_nonempty_string(batch, value, "exact_quote", path);
1210 require_enum(batch, value, "stance", path, stances);
1211 for (const auto& key : { "quote_language", "quote_translation" }) {
1212 if (value.contains(key)) {
1213 require_nonempty_string(batch, value, key, path);
1214 }
1215 }
1216 if (value.contains("locator_json")) {
1217 require_nonempty_string(batch, value, "locator_json", path);
1218 if (value["locator_json"].is_string()) {
1219 try {
1220 const json parsed = json::parse(
1221 value["locator_json"].get<std::string>()
1222 );
1223 static_cast<void>(parsed);
1224 } catch (const json::exception&) {
1225 add_issue(
1226 batch, "invalid_json_text", path + "/locator_json",
1227 "locator_json must contain valid JSON",
1228 &value["locator_json"]
1229 );
1230 }
1231 }
1232 }
1233}
void validate_integer_or_local_reference_shape(parsed_batch &batch, const json &record, const std::string &key, const std::string &path)
Definition inbox.cpp:948
const std::set< std::string, std::less<> > stances
Definition inbox.cpp:857

◆ validate_create_external_id()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_create_external_id ( parsed_batch & batch,
const json & value,
const std::string & path )

Definition at line 1138 of file inbox.cpp.

1140 {
1141 check_keys(
1142 batch, value, path,
1143 { "entity_id", "scheme", "value", "canonical_url" },
1144 { "entity_id", "scheme", "value" }
1145 );
1146 if (!value.is_object()) {
1147 return;
1148 }
1149 validate_entity_reference_shape(batch, value, "entity_id", path);
1150 require_nonempty_string(batch, value, "scheme", path);
1151 require_nonempty_string(batch, value, "value", path);
1152 if (value.contains("canonical_url")) {
1153 require_nonempty_string(batch, value, "canonical_url", path);
1154 }
1155}

◆ validate_create_financial()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_create_financial ( parsed_batch & batch,
const json & value,
const std::string & path )

Definition at line 1285 of file inbox.cpp.

1287 {
1288 check_keys(
1289 batch, value, path,
1290 { "work_id", "fact_type", "amount_min", "amount_max",
1291 "currency_code", "value_year", "is_estimate", "confidence" },
1292 { "work_id", "fact_type", "amount_min", "currency_code",
1293 "is_estimate" }
1294 );
1295 if (!value.is_object()) {
1296 return;
1297 }
1298 validate_entity_reference_shape(batch, value, "work_id", path);
1300 batch, value, "fact_type", path,
1301 std::set<std::string, std::less<>> { "budget" }
1302 );
1304 batch, value, "amount_min", path, 0,
1305 std::numeric_limits<std::int64_t>::max()
1306 );
1308 batch, value, "amount_max", path, 0,
1309 std::numeric_limits<std::int64_t>::max()
1310 );
1311 if (value.contains("amount_min") && value["amount_min"].is_number_integer()
1312 && value.contains("amount_max")
1313 && value["amount_max"].is_number_integer()
1314 && value["amount_max"].get<std::int64_t>()
1315 < value["amount_min"].get<std::int64_t>()) {
1316 add_issue(
1317 batch, "invalid_range", path + "/amount_max",
1318 "amount_max must not be less than amount_min",
1319 &value["amount_max"]
1320 );
1321 }
1322 require_nonempty_string(batch, value, "currency_code", path);
1323 if (value.contains("currency_code") && value["currency_code"].is_string()) {
1324 const std::string& currency
1325 = value["currency_code"].get_ref<const std::string&>();
1326 if (currency.size() != 3U
1327 || !std::ranges::all_of(currency, [](const char character) {
1328 return character >= 'A' && character <= 'Z';
1329 })) {
1330 add_issue(
1331 batch, "invalid_currency_code", path + "/currency_code",
1332 "currency_code must contain exactly three uppercase letters",
1333 &value["currency_code"]
1334 );
1335 }
1336 }
1338 batch, value, "value_year", path, -9999, 9999
1339 );
1340 require_kind(batch, value, "is_estimate", path, value_kind::boolean);
1341 require_number_range(batch, value, "confidence", path, 0.0, 1.0);
1342}

References boolean.

◆ validate_create_manifestation()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_create_manifestation ( parsed_batch & batch,
const json & value,
const std::string & path,
std::unordered_map< std::string, std::string > & locals )

Definition at line 1088 of file inbox.cpp.

1091 {
1092 check_keys(
1093 batch, value, path,
1094 { "local_id", "work_id", "manifestation_type", "release_year",
1095 "region_code", "language_code", "label" },
1096 { "local_id", "work_id", "manifestation_type", "label" }
1097 );
1098 if (!value.is_object()) {
1099 return;
1100 }
1101 validate_local_id(batch, value, path, locals, "manifestation");
1102 validate_entity_reference_shape(batch, value, "work_id", path);
1104 batch, value, "manifestation_type", path, manifestation_types
1105 );
1106 require_nonempty_string(batch, value, "label", path);
1107 require_integer_range(batch, value, "release_year", path, -9999, 9999);
1108 for (const auto& key : { "region_code", "language_code" }) {
1109 if (value.contains(key)) {
1110 require_nonempty_string(batch, value, key, path);
1111 }
1112 }
1113}
const std::set< std::string, std::less<> > manifestation_types
Definition inbox.cpp:845

◆ validate_create_measurement()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_create_measurement ( parsed_batch & batch,
const json & value,
const std::string & path )

Definition at line 1260 of file inbox.cpp.

1262 {
1263 check_keys(
1264 batch, value, path,
1265 { "entity_id", "measurement_type", "value", "unit", "qualifier" },
1266 { "entity_id", "measurement_type", "value", "unit" }
1267 );
1268 if (!value.is_object()) {
1269 return;
1270 }
1271 validate_entity_reference_shape(batch, value, "entity_id", path);
1273 batch, value, "measurement_type", path, measurement_types
1274 );
1276 batch, value, "value", path, 0.0,
1277 std::numeric_limits<double>::max()
1278 );
1279 require_enum(batch, value, "unit", path, measurement_units);
1280 if (value.contains("qualifier")) {
1281 require_nonempty_string(batch, value, "qualifier", path);
1282 }
1283}
const std::set< std::string, std::less<> > measurement_units
Definition inbox.cpp:872
const std::set< std::string, std::less<> > measurement_types
Definition inbox.cpp:869

◆ validate_create_name()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_create_name ( parsed_batch & batch,
const json & value,
const std::string & path )

Definition at line 1115 of file inbox.cpp.

1117 {
1118 check_keys(
1119 batch, value, path,
1120 { "entity_id", "name_type", "language_code", "script_code", "value",
1121 "is_preferred" },
1122 { "entity_id", "name_type", "value", "is_preferred" }
1123 );
1124 if (!value.is_object()) {
1125 return;
1126 }
1127 validate_entity_reference_shape(batch, value, "entity_id", path);
1128 require_enum(batch, value, "name_type", path, name_types);
1129 require_nonempty_string(batch, value, "value", path);
1130 require_kind(batch, value, "is_preferred", path, value_kind::boolean);
1131 for (const auto& key : { "language_code", "script_code" }) {
1132 if (value.contains(key)) {
1133 require_nonempty_string(batch, value, key, path);
1134 }
1135 }
1136}
const std::set< std::string, std::less<> > name_types
Definition inbox.cpp:849

References boolean.

◆ validate_create_parent_guide()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_create_parent_guide ( parsed_batch & batch,
const json & value,
const std::string & path,
std::unordered_map< std::string, std::string > & locals )

Definition at line 1486 of file inbox.cpp.

1489 {
1490 check_keys(
1491 batch, value, path,
1492 { "local_id", "work_id", "concept_id", "category", "intensity",
1493 "explicitness", "frequency", "centrality", "realism",
1494 "spoiler_level", "confidence", "evidence" },
1495 { "local_id", "work_id", "concept_id", "category", "intensity",
1496 "explicitness", "frequency", "centrality", "realism",
1497 "spoiler_level", "evidence" }
1498 );
1499 if (!value.is_object()) {
1500 return;
1501 }
1502 validate_local_id(batch, value, path, locals, "parent_guide");
1503 validate_entity_reference_shape(batch, value, "work_id", path);
1504 validate_entity_reference_shape(batch, value, "concept_id", path);
1505 require_enum(batch, value, "category", path, guide_categories);
1506 for (const auto& key : {
1507 "intensity", "explicitness", "frequency", "centrality",
1508 "realism" }) {
1509 require_integer_range(batch, value, key, path, 1, 5);
1510 }
1511 require_enum(batch, value, "spoiler_level", path, spoiler_levels);
1512 require_number_range(batch, value, "confidence", path, 0.0, 1.0);
1513 validate_evidence_list(batch, value, path);
1514}
const std::set< std::string, std::less<> > guide_categories
Definition inbox.cpp:888
const std::set< std::string, std::less<> > spoiler_levels
Definition inbox.cpp:892

◆ validate_create_source()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_create_source ( parsed_batch & batch,
const json & value,
const std::string & path,
std::unordered_map< std::string, std::string > & locals )

Definition at line 1157 of file inbox.cpp.

1160 {
1161 check_keys(
1162 batch, value, path,
1163 { "local_id", "source_type", "title", "bibliography_text",
1164 "author_text", "publisher", "publication_date", "url", "doi",
1165 "isbn", "language_code" },
1166 { "local_id", "source_type" }
1167 );
1168 if (!value.is_object()) {
1169 return;
1170 }
1171 validate_local_id(batch, value, path, locals, "source");
1172 require_enum(batch, value, "source_type", path, source_types);
1173 for (const auto& key : {
1174 "title", "bibliography_text", "author_text", "publisher",
1175 "publication_date", "url", "doi", "isbn", "language_code" }) {
1176 if (value.contains(key)) {
1177 require_nonempty_string(batch, value, key, path);
1178 }
1179 }
1180 bool identified = false;
1181 for (const auto& key : { "doi", "isbn", "url", "bibliography_text" }) {
1182 identified = identified || value.contains(key);
1183 }
1184 if (!identified) {
1185 add_issue(
1186 batch, "source_identity_required", path,
1187 "source needs doi, isbn, url, or bibliography_text", &value
1188 );
1189 }
1190}
const std::set< std::string, std::less<> > source_types
Definition inbox.cpp:853

◆ validate_create_work()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_create_work ( parsed_batch & batch,
const json & value,
const std::string & path,
std::unordered_map< std::string, std::string > & locals )

Definition at line 1005 of file inbox.cpp.

1008 {
1009 check_keys(
1010 batch, value, path,
1011 { "local_id", "medium", "year_start", "year_end", "date_precision",
1012 "date_start_text", "date_end_text", "date_qualifier",
1013 "language_code", "country_code", "production_info_json" },
1014 { "local_id", "medium" }
1015 );
1016 if (!value.is_object()) {
1017 return;
1018 }
1019 validate_local_id(batch, value, path, locals, "work");
1020 require_enum(batch, value, "medium", path, media);
1021 require_integer_range(batch, value, "year_start", path, -9999, 9999);
1022 require_integer_range(batch, value, "year_end", path, -9999, 9999);
1023 if (value.contains("year_start") && value["year_start"].is_number_integer()
1024 && value.contains("year_end") && value["year_end"].is_number_integer()
1025 && value["year_end"].get<std::int64_t>()
1026 < value["year_start"].get<std::int64_t>()) {
1027 add_issue(
1028 batch, "invalid_range", path + "/year_end",
1029 "year_end must not be earlier than year_start", &value["year_end"]
1030 );
1031 }
1032 if (value.contains("date_precision")) {
1033 require_enum(batch, value, "date_precision", path, date_precisions);
1034 }
1035 for (const auto& key : {
1036 "date_start_text", "date_end_text", "date_qualifier",
1037 "language_code", "country_code" }) {
1038 if (value.contains(key)) {
1039 require_nonempty_string(batch, value, key, path);
1040 }
1041 }
1042 if (value.contains("production_info_json")) {
1044 batch, value, "production_info_json", path
1045 );
1046 if (value["production_info_json"].is_string()) {
1047 try {
1048 const json parsed = json::parse(
1049 value["production_info_json"].get<std::string>()
1050 );
1051 static_cast<void>(parsed);
1052 } catch (const json::exception&) {
1053 add_issue(
1054 batch, "invalid_json_text",
1055 path + "/production_info_json",
1056 "production_info_json must contain valid JSON",
1057 &value["production_info_json"]
1058 );
1059 }
1060 }
1061 }
1062}
const std::set< std::string, std::less<> > media
Definition inbox.cpp:832
const std::set< std::string, std::less<> > date_precisions
Definition inbox.cpp:838

◆ validate_create_work_concept()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_create_work_concept ( parsed_batch & batch,
const json & value,
const std::string & path,
std::unordered_map< std::string, std::string > & locals )

Definition at line 1412 of file inbox.cpp.

1415 {
1416 check_keys(
1417 batch, value, path,
1418 { "local_id", "work_id", "concept_id", "relation_type", "centrality",
1419 "historical_role", "confidence", "evidence" },
1420 { "local_id", "work_id", "concept_id", "relation_type", "centrality",
1421 "evidence" }
1422 );
1423 if (!value.is_object()) {
1424 return;
1425 }
1426 validate_local_id(batch, value, path, locals, "work_concept");
1427 validate_entity_reference_shape(batch, value, "work_id", path);
1428 validate_entity_reference_shape(batch, value, "concept_id", path);
1430 batch, value, "relation_type", path, work_concept_types
1431 );
1432 require_integer_range(batch, value, "centrality", path, 1, 100);
1433 if (value.contains("historical_role")) {
1435 batch, value, "historical_role", path, historical_roles
1436 );
1437 }
1438 require_number_range(batch, value, "confidence", path, 0.0, 1.0);
1439 validate_evidence_list(batch, value, path);
1440}
const std::set< std::string, std::less<> > historical_roles
Definition inbox.cpp:884
const std::set< std::string, std::less<> > work_concept_types
Definition inbox.cpp:880

◆ validate_delete_object()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_delete_object ( parsed_batch & batch,
const json & update,
const std::string & path )

Definition at line 1779 of file inbox.cpp.

1781 {
1782 const auto found = update.find("delete");
1783 if (found == update.end()) {
1784 return;
1785 }
1786 const std::set<std::string, std::less<>> tables {
1787 "names", "external_ids", "credits", "measurements",
1788 "financial_facts", "evidence", "work_concepts", "concept_relations",
1789 "parent_guide_assertions", "ingest_issues"
1790 };
1791 check_keys(batch, *found, path + "/delete", tables);
1792 if (!found->is_object()) {
1793 return;
1794 }
1795 for (const auto& table : tables) {
1796 const auto rows = found->find(table);
1797 if (rows == found->end()) {
1798 continue;
1799 }
1800 const std::string rows_path
1801 = path + "/delete/" + pointer_escape(table);
1802 if (!rows->is_array()) {
1803 add_issue(
1804 batch, "type_mismatch", rows_path,
1805 "delete collection must be an array", &*rows
1806 );
1807 continue;
1808 }
1809 if (table == "ingest_issues") {
1810 std::set<std::string, std::less<>> seen;
1811 for (std::size_t index = 0; index < rows->size(); ++index) {
1812 const auto& value = (*rows)[index];
1813 const std::string value_path = indexed_path(rows_path, index);
1814 check_keys(
1815 batch, value, value_path,
1816 { "batch_id", "code", "json_path" },
1817 { "batch_id", "code", "json_path" }
1818 );
1819 if (!value.is_object()) {
1820 continue;
1821 }
1823 batch, value, "batch_id", value_path
1824 );
1825 require_nonempty_string(batch, value, "code", value_path);
1827 batch, value, "json_path", value_path
1828 );
1829 if (const auto batch_id = value.find("batch_id");
1830 batch_id != value.end() && batch_id->is_string()
1831 && !valid_batch_id(batch_id->get_ref<const std::string&>())) {
1832 add_issue(
1833 batch, "invalid_batch_id",
1834 value_path + "/batch_id",
1835 "issue batch_id must be a stable identifier", &*batch_id
1836 );
1837 }
1838 if (const auto json_path = value.find("json_path");
1839 json_path != value.end() && json_path->is_string()
1840 && !json_path->get_ref<const std::string&>().starts_with(
1841 '/'
1842 )) {
1843 add_issue(
1844 batch, "invalid_json_pointer",
1845 value_path + "/json_path",
1846 "JSON Pointer must start with '/'", &*json_path
1847 );
1848 }
1849 if (!seen.emplace(value.dump()).second) {
1850 add_issue(
1851 batch, "duplicate_delete", value_path,
1852 "ingest issue key occurs more than once", &value
1853 );
1854 }
1855 }
1856 continue;
1857 }
1858 std::set<std::int64_t> seen;
1859 for (std::size_t index = 0; index < rows->size(); ++index) {
1860 const auto& value = (*rows)[index];
1861 const std::string value_path = indexed_path(rows_path, index);
1862 if (!(value.is_number_integer() || value.is_number_unsigned())) {
1863 add_issue(
1864 batch, "type_mismatch", value_path,
1865 "row ID must be a positive integer", &value
1866 );
1867 continue;
1868 }
1869 try {
1870 const std::int64_t id = value.get<std::int64_t>();
1871 if (id <= 0) {
1872 add_issue(
1873 batch, "number_out_of_range", value_path,
1874 "row ID must be positive", &value
1875 );
1876 } else if (!seen.emplace(id).second) {
1877 add_issue(
1878 batch, "duplicate_delete", value_path,
1879 "row ID occurs more than once", &value
1880 );
1881 }
1882 } catch (const json::exception&) {
1883 add_issue(
1884 batch, "number_out_of_range", value_path,
1885 "row ID is outside SQLite's range", &value
1886 );
1887 }
1888 }
1889 }
1890}

◆ validate_document_shape()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_document_shape ( parsed_batch & batch)

Definition at line 1995 of file inbox.cpp.

1995 {
1996 if (!batch.document.is_object()) {
1997 add_issue(
1998 batch, "root_type", "/", "batch root must be an object",
1999 &batch.document
2000 );
2001 return;
2002 }
2003 check_keys(
2004 batch, batch.document, "",
2005 { "format", "batch_id", "create", "update", "merge" },
2006 { "format", "batch_id", "create", "update", "merge" }
2007 );
2009 batch, batch.document, "format", "", value_kind::string
2010 );
2011 if (batch.document.contains("format")
2012 && batch.document["format"].is_string()
2013 && batch.document["format"] != "arachne_batch_v2") {
2014 add_issue(
2015 batch, "unsupported_format", "/format",
2016 "format must be arachne_batch_v2", &batch.document["format"]
2017 );
2018 }
2020 batch, batch.document, "batch_id", "", value_kind::string
2021 );
2022 if (batch.document.contains("batch_id")
2023 && batch.document["batch_id"].is_string()) {
2024 batch.batch_id = batch.document["batch_id"].get<std::string>();
2025 for (auto& issue : batch.issues) {
2026 issue.batch_id = batch.batch_id;
2027 }
2028 if (!valid_batch_id(batch.batch_id)) {
2029 add_issue(
2030 batch, "invalid_batch_id", "/batch_id",
2031 "batch_id must contain 1-128 letters, digits, dot, dash, or underscore",
2032 &batch.document["batch_id"]
2033 );
2034 }
2035 }
2036 for (const auto& section : { "create", "update", "merge" }) {
2038 batch, batch.document, section, "", value_kind::object
2039 );
2040 }
2041 if (!(batch.document.contains("create")
2042 && batch.document["create"].is_object()
2043 && batch.document.contains("update")
2044 && batch.document["update"].is_object()
2045 && batch.document.contains("merge")
2046 && batch.document["merge"].is_object())) {
2047 return;
2048 }
2049
2050 auto& create = batch.document["create"];
2051 const std::set<std::string, std::less<>> create_keys {
2052 "agents", "works", "concepts", "manifestations", "names",
2053 "external_ids", "sources", "evidence", "credits", "measurements",
2054 "financial_facts", "work_concepts", "concept_relations",
2055 "parent_guide_assertions"
2056 };
2057 check_keys(batch, create, "/create", create_keys);
2058 auto& locals = batch.entity_local_ids;
2060 batch, create, "agents", "/create",
2061 [&locals](
2062 parsed_batch& target, const json& value, const std::string& path
2063 ) { validate_create_agent(target, value, path, locals); }
2064 );
2066 batch, create, "works", "/create",
2067 [&locals](
2068 parsed_batch& target, const json& value, const std::string& path
2069 ) { validate_create_work(target, value, path, locals); }
2070 );
2072 batch, create, "concepts", "/create",
2073 [&locals](
2074 parsed_batch& target, const json& value, const std::string& path
2075 ) { validate_create_concept(target, value, path, locals); }
2076 );
2078 batch, create, "manifestations", "/create",
2079 [&locals](
2080 parsed_batch& target, const json& value, const std::string& path
2081 ) { validate_create_manifestation(target, value, path, locals); }
2082 );
2083 validate_array(batch, create, "names", "/create", validate_create_name);
2085 batch, create, "external_ids", "/create",
2087 );
2089 batch, create, "sources", "/create",
2090 [&locals](
2091 parsed_batch& target, const json& value, const std::string& path
2092 ) { validate_create_source(target, value, path, locals); }
2093 );
2095 batch, create, "evidence", "/create",
2096 [&locals](
2097 parsed_batch& target, const json& value, const std::string& path
2098 ) { validate_create_evidence(target, value, path, locals); }
2099 );
2101 batch, create, "credits", "/create", validate_create_credit
2102 );
2104 batch, create, "measurements", "/create",
2106 );
2108 batch, create, "financial_facts", "/create",
2110 );
2112 batch, create, "work_concepts", "/create",
2113 [&locals](
2114 parsed_batch& target, const json& value, const std::string& path
2115 ) { validate_create_work_concept(target, value, path, locals); }
2116 );
2118 batch, create, "concept_relations", "/create",
2119 [&locals](
2120 parsed_batch& target, const json& value, const std::string& path
2121 ) { validate_create_concept_relation(target, value, path, locals); }
2122 );
2124 batch, create, "parent_guide_assertions", "/create",
2125 [&locals](
2126 parsed_batch& target, const json& value, const std::string& path
2127 ) { validate_create_parent_guide(target, value, path, locals); }
2128 );
2129
2130 auto& update = batch.document["update"];
2131 const std::set<std::string, std::less<>> update_keys {
2132 "agents", "works", "concepts", "manifestations", "sources", "delete"
2133 };
2134 check_keys(batch, update, "/update", update_keys);
2136 batch, update, "agents", "/update",
2137 [](parsed_batch& target, const json& value, const std::string& path) {
2139 target, value, path, "agent", agent_mutable, {},
2141 );
2142 }
2143 );
2145 batch, update, "works", "/update",
2146 [](parsed_batch& target, const json& value, const std::string& path) {
2148 target, value, path, "work", work_mutable, { "medium" },
2150 );
2151 }
2152 );
2154 batch, update, "concepts", "/update",
2155 [](parsed_batch& target, const json& value, const std::string& path) {
2157 target, value, path, "concept", concept_mutable,
2158 { "concept_type", "slug" }, concept_update_enums
2159 );
2160 }
2161 );
2163 batch, update, "manifestations", "/update",
2164 [](parsed_batch& target, const json& value, const std::string& path) {
2166 target, value, path, "manifestation",
2168 { "work_id", "manifestation_type", "label" },
2170 );
2171 }
2172 );
2174 batch, update, "sources", "/update",
2175 [](parsed_batch& target, const json& value, const std::string& path) {
2177 target, value, path, "source", source_mutable,
2178 { "source_type" }, source_update_enums, false, true
2179 );
2180 }
2181 );
2182 validate_delete_object(batch, update, "/update");
2183 for (const auto collection : {
2184 "agents", "works", "concepts", "manifestations", "sources" }) {
2185 const auto rows = update.find(collection);
2186 if (rows == update.end() || !rows->is_array()) {
2187 continue;
2188 }
2189 std::set<std::string, std::less<>> targets;
2190 for (std::size_t index = 0; index < rows->size(); ++index) {
2191 const auto& row = (*rows)[index];
2192 const auto id = row.is_object() ? row.find("id") : row.end();
2193 if (id == row.end()
2194 || (!id->is_string() && !id->is_number_integer()
2195 && !id->is_number_unsigned())) {
2196 continue;
2197 }
2198 const std::string identity = id->dump();
2199 if (!targets.emplace(identity).second) {
2200 add_issue(
2201 batch, "duplicate_update_target",
2203 "/update/" + std::string(collection), index
2204 )
2205 + "/id",
2206 "a canonical record may be updated at most once per batch",
2207 &*id
2208 );
2209 }
2210 }
2211 }
2212
2213 auto& merge = batch.document["merge"];
2214 check_keys(batch, merge, "/merge", { "agents", "works", "concepts" });
2215 std::map<std::string, std::string, std::less<>> merged_entities;
2217 batch, merge, "agents", "/merge",
2218 [&merged_entities](
2219 parsed_batch& target, const json& value, const std::string& path
2220 ) {
2222 target, value, path, "agent", agent_merge_mutable,
2223 { "agent_type" }, agent_merge_enums, merged_entities
2224 );
2225 }
2226 );
2228 batch, merge, "works", "/merge",
2229 [&merged_entities](
2230 parsed_batch& target, const json& value, const std::string& path
2231 ) {
2233 target, value, path, "work", work_mutable, { "medium" },
2234 work_update_enums, merged_entities
2235 );
2236 }
2237 );
2239 batch, merge, "concepts", "/merge",
2240 [&merged_entities](
2241 parsed_batch& target, const json& value, const std::string& path
2242 ) {
2244 target, value, path, "concept", concept_mutable,
2245 { "concept_type", "slug" }, concept_update_enums,
2246 merged_entities
2247 );
2248 }
2249 );
2250
2251 for (const auto& family : {
2252 std::pair { "agents", std::string_view("agent") },
2253 std::pair { "works", std::string_view("work") },
2254 std::pair { "concepts", std::string_view("concept") },
2255 std::pair { "manifestations", std::string_view("manifestation") } }) {
2256 const auto updates = update.find(family.first);
2257 if (updates == update.end() || !updates->is_array()) {
2258 continue;
2259 }
2260 for (std::size_t index = 0; index < updates->size(); ++index) {
2261 const auto& operation = (*updates)[index];
2262 if (!operation.is_object() || !operation.contains("id")
2263 || !operation["id"].is_string()) {
2264 continue;
2265 }
2266 const std::string id = operation["id"].get<std::string>();
2267 const auto conflict = merged_entities.find(id);
2268 if (conflict != merged_entities.end()) {
2269 add_issue(
2270 batch, "conflicting_merge_update",
2272 "/update/" + std::string(family.first), index
2273 )
2274 + "/id",
2275 "entity is also modified by " + conflict->second,
2276 &operation["id"]
2277 );
2278 }
2279 }
2280 }
2281}
void validate_create_manifestation(parsed_batch &batch, const json &value, const std::string &path, std::unordered_map< std::string, std::string > &locals)
Definition inbox.cpp:1088
const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > agent_merge_enums
Definition inbox.cpp:1754
void validate_create_financial(parsed_batch &batch, const json &value, const std::string &path)
Definition inbox.cpp:1285
void validate_delete_object(parsed_batch &batch, const json &update, const std::string &path)
Definition inbox.cpp:1779
const std::map< std::string, value_kind, std::less<> > source_mutable
Definition inbox.cpp:1735
const std::map< std::string, value_kind, std::less<> > agent_merge_mutable
Definition inbox.cpp:1705
void validate_array(parsed_batch &batch, const json &parent, const std::string &key, const std::string &parent_path, Validator &&validator)
Definition inbox.cpp:1521
void validate_create_name(parsed_batch &batch, const json &value, const std::string &path)
Definition inbox.cpp:1115
void validate_create_parent_guide(parsed_batch &batch, const json &value, const std::string &path, std::unordered_map< std::string, std::string > &locals)
Definition inbox.cpp:1486
void validate_create_work(parsed_batch &batch, const json &value, const std::string &path, std::unordered_map< std::string, std::string > &locals)
Definition inbox.cpp:1005
void validate_create_external_id(parsed_batch &batch, const json &value, const std::string &path)
Definition inbox.cpp:1138
const std::map< std::string, value_kind, std::less<> > work_mutable
Definition inbox.cpp:1710
void validate_create_agent(parsed_batch &batch, const json &value, const std::string &path, std::unordered_map< std::string, std::string > &locals)
Definition inbox.cpp:987
void validate_create_concept(parsed_batch &batch, const json &value, const std::string &path, std::unordered_map< std::string, std::string > &locals)
Definition inbox.cpp:1064
const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > manifestation_update_enums
Definition inbox.cpp:1770
void validate_create_measurement(parsed_batch &batch, const json &value, const std::string &path)
Definition inbox.cpp:1260
const std::map< std::string, value_kind, std::less<> > agent_mutable
Definition inbox.cpp:1701
void validate_create_evidence(parsed_batch &batch, const json &value, const std::string &path, std::unordered_map< std::string, std::string > &locals)
Definition inbox.cpp:1192
void validate_update_record(parsed_batch &batch, const json &value, const std::string &path, const std::string_view family, const std::map< std::string, value_kind, std::less<> > &mutable_fields, const std::set< std::string, std::less<> > &required_fields, const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > &enum_fields, const bool allow_empty=false, const bool integer_id=false)
Definition inbox.cpp:1542
void validate_create_source(parsed_batch &batch, const json &value, const std::string &path, std::unordered_map< std::string, std::string > &locals)
Definition inbox.cpp:1157
void validate_merge_record(parsed_batch &batch, const json &value, const std::string &path, const std::string_view family, const std::map< std::string, value_kind, std::less<> > &mutable_fields, const std::set< std::string, std::less<> > &required_fields, const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > &enum_fields, std::map< std::string, std::string, std::less<> > &merged_entities)
Definition inbox.cpp:1892
void validate_create_concept_relation(parsed_batch &batch, const json &value, const std::string &path, std::unordered_map< std::string, std::string > &locals)
Definition inbox.cpp:1442
void validate_create_credit(parsed_batch &batch, const json &value, const std::string &path)
Definition inbox.cpp:1235
const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > agent_update_enums
Definition inbox.cpp:1750
const std::map< std::string, value_kind, std::less<> > manifestation_mutable
Definition inbox.cpp:1727
void validate_create_work_concept(parsed_batch &batch, const json &value, const std::string &path, std::unordered_map< std::string, std::string > &locals)
Definition inbox.cpp:1412
const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > work_update_enums
Definition inbox.cpp:1759
const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > source_update_enums
Definition inbox.cpp:1775
const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > concept_update_enums
Definition inbox.cpp:1765
const std::map< std::string, value_kind, std::less<> > concept_mutable
Definition inbox.cpp:1722

References arachne::penelope::anonymous_namespace{inbox.cpp}::parsed_batch::batch_id, and string.

Referenced by parse_batch().

Here is the caller graph for this function:

◆ validate_entity_reference_shape()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_entity_reference_shape ( parsed_batch & batch,
const json & record,
const std::string & key,
const std::string & path )

Definition at line 941 of file inbox.cpp.

944 {
945 require_nonempty_string(batch, record, key, path);
946}

◆ validate_evidence_list()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_evidence_list ( parsed_batch & batch,
const json & value,
const std::string & path )

Definition at line 1344 of file inbox.cpp.

1346 {
1347 const auto found = value.find("evidence");
1348 if (found == value.end()) {
1349 return;
1350 }
1351 if (!found->is_array()) {
1352 add_issue(
1353 batch, "type_mismatch", path + "/evidence",
1354 "evidence must be an array", &*found
1355 );
1356 return;
1357 }
1358 if (found->empty()) {
1359 add_issue(
1360 batch, "evidence_required", path + "/evidence",
1361 "semantic assertion needs at least one evidence reference", &*found
1362 );
1363 }
1364 std::set<std::string, std::less<>> seen;
1365 for (std::size_t index = 0; index < found->size(); ++index) {
1366 const auto& reference = (*found)[index];
1367 const std::string identity = reference.dump();
1368 if (!seen.emplace(identity).second) {
1369 add_issue(
1370 batch, "duplicate_evidence_reference",
1371 indexed_path(path + "/evidence", index),
1372 "evidence reference occurs more than once", &reference
1373 );
1374 }
1375 if (reference.is_string()) {
1376 if (reference.get_ref<const std::string&>().empty()) {
1377 add_issue(
1378 batch, "empty_string",
1379 indexed_path(path + "/evidence", index),
1380 "local evidence reference must not be empty", &reference
1381 );
1382 }
1383 } else if (!(reference.is_number_integer()
1384 || reference.is_number_unsigned())) {
1385 add_issue(
1386 batch, "type_mismatch",
1387 indexed_path(path + "/evidence", index),
1388 "evidence reference must be a positive integer or local_id",
1389 &reference
1390 );
1391 } else {
1392 try {
1393 if (reference.get<std::int64_t>() <= 0) {
1394 add_issue(
1395 batch, "number_out_of_range",
1396 indexed_path(path + "/evidence", index),
1397 "evidence row reference must be positive", &reference
1398 );
1399 }
1400 } catch (const json::exception&) {
1401 add_issue(
1402 batch, "number_out_of_range",
1403 indexed_path(path + "/evidence", index),
1404 "evidence row reference is outside SQLite's range",
1405 &reference
1406 );
1407 }
1408 }
1409 }
1410}

◆ validate_integer_or_local_reference_shape()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_integer_or_local_reference_shape ( parsed_batch & batch,
const json & record,
const std::string & key,
const std::string & path )

Definition at line 948 of file inbox.cpp.

951 {
952 const auto found = record.find(key);
953 if (found == record.end()) {
954 return;
955 }
956 if (found->is_string()) {
957 if (found->get_ref<const std::string&>().empty()) {
958 add_issue(
959 batch, "empty_string", path + "/" + pointer_escape(key),
960 "local reference must not be empty", &*found
961 );
962 }
963 } else if (!(found->is_number_integer() || found->is_number_unsigned())) {
964 add_issue(
965 batch, "type_mismatch", path + "/" + pointer_escape(key),
966 "reference must be a positive database integer or local_id", &*found
967 );
968 } else {
969 try {
970 if (found->get<std::int64_t>() <= 0) {
971 add_issue(
972 batch, "number_out_of_range",
973 path + "/" + pointer_escape(key),
974 "database row reference must be positive", &*found
975 );
976 }
977 } catch (const json::exception&) {
978 add_issue(
979 batch, "number_out_of_range",
980 path + "/" + pointer_escape(key),
981 "database row reference is outside SQLite's range", &*found
982 );
983 }
984 }
985}

◆ validate_local_id()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_local_id ( parsed_batch & batch,
const json & record,
const std::string & path,
std::unordered_map< std::string, std::string > & locals,
const std::string_view family )

Definition at line 909 of file inbox.cpp.

913 {
914 require_nonempty_string(batch, record, "local_id", path);
915 const auto found = record.find("local_id");
916 if (found == record.end() || !found->is_string()
917 || found->get_ref<const std::string&>().empty()) {
918 return;
919 }
920 const std::string& value = found->get_ref<const std::string&>();
921 if (!valid_local_id(value)) {
922 add_issue(
923 batch, "invalid_local_id", path + "/local_id",
924 "local_id must be 1-128 identifier characters", &*found
925 );
926 } else if (looks_like_canonical_entity_id(value)) {
927 add_issue(
928 batch, "reserved_local_id", path + "/local_id",
929 "local_id must not use a canonical entity ID pattern", &*found
930 );
931 }
932 const auto [position, inserted] = locals.emplace(value, family);
933 if (!inserted) {
934 add_issue(
935 batch, "duplicate_local_id", path + "/local_id",
936 "local_id is already used by " + position->second, &*found
937 );
938 }
939}
bool valid_local_id(const std::string_view value)
Definition inbox.cpp:613
bool looks_like_canonical_entity_id(const std::string_view value)
Definition inbox.cpp:648

References looks_like_canonical_entity_id().

Here is the call graph for this function:

◆ validate_merge_record()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_merge_record ( parsed_batch & batch,
const json & value,
const std::string & path,
const std::string_view family,
const std::map< std::string, value_kind, std::less<> > & mutable_fields,
const std::set< std::string, std::less<> > & required_fields,
const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > & enum_fields,
std::map< std::string, std::string, std::less<> > & merged_entities )

Definition at line 1892 of file inbox.cpp.

1900 {
1901 check_keys(
1902 batch, value, path, { "target", "members", "set", "unset" },
1903 { "target", "members", "set", "unset" }
1904 );
1905 if (!value.is_object()) {
1906 return;
1907 }
1908 require_nonempty_string(batch, value, "target", path);
1909 require_kind(batch, value, "members", path, value_kind::array);
1910 require_kind(batch, value, "set", path, value_kind::object);
1911 require_kind(batch, value, "unset", path, value_kind::array);
1912 const auto target = value.find("target");
1913 std::string target_id;
1914 if (target != value.end() && target->is_string()) {
1915 target_id = target->get<std::string>();
1916 if (!valid_canonical_id(target_id, family)) {
1917 add_issue(
1918 batch, "invalid_canonical_id", path + "/target",
1919 "target does not identify the required entity family", &*target
1920 );
1921 }
1922 const auto [position, inserted] = merged_entities.emplace(
1923 target_id, path
1924 );
1925 if (!inserted) {
1926 add_issue(
1927 batch, "conflicting_merge", path + "/target",
1928 "entity already participates in merge " + position->second,
1929 &*target
1930 );
1931 }
1932 }
1933 const auto members = value.find("members");
1934 std::set<std::string, std::less<>> local_members;
1935 if (members != value.end() && members->is_array()) {
1936 if (members->empty()) {
1937 add_issue(
1938 batch, "members_required", path + "/members",
1939 "merge needs at least one member", &*members
1940 );
1941 }
1942 for (std::size_t index = 0; index < members->size(); ++index) {
1943 const auto& member = (*members)[index];
1944 const std::string member_path
1945 = indexed_path(path + "/members", index);
1946 if (!member.is_string()
1948 member.is_string() ? member.get_ref<const std::string&>()
1949 : std::string_view(),
1950 family
1951 )) {
1952 add_issue(
1953 batch, "invalid_canonical_id", member_path,
1954 "member does not identify the required entity family",
1955 &member
1956 );
1957 continue;
1958 }
1959 const std::string member_id = member.get<std::string>();
1960 if (member_id == target_id) {
1961 add_issue(
1962 batch, "target_is_member", member_path,
1963 "merge target cannot also be a member", &member
1964 );
1965 }
1966 if (!local_members.emplace(member_id).second) {
1967 add_issue(
1968 batch, "duplicate_merge_member", member_path,
1969 "merge member occurs more than once", &member
1970 );
1971 }
1972 const auto [position, inserted] = merged_entities.emplace(
1973 member_id, path
1974 );
1975 if (!inserted) {
1976 add_issue(
1977 batch, "conflicting_merge", member_path,
1978 "entity already participates in merge " + position->second,
1979 &member
1980 );
1981 }
1982 }
1983 }
1984 json update {
1985 { "id", target_id },
1986 { "set", value.value("set", json::object()) },
1987 { "unset", value.value("unset", json::array()) },
1988 };
1990 batch, update, path, family, mutable_fields, required_fields,
1991 enum_fields, true
1992 );
1993}

References array, and object.

◆ validate_update_record()

void arachne::penelope::anonymous_namespace{inbox.cpp}::validate_update_record ( parsed_batch & batch,
const json & value,
const std::string & path,
const std::string_view family,
const std::map< std::string, value_kind, std::less<> > & mutable_fields,
const std::set< std::string, std::less<> > & required_fields,
const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > & enum_fields,
const bool allow_empty = false,
const bool integer_id = false )

Definition at line 1542 of file inbox.cpp.

1550 {
1551 check_keys(
1552 batch, value, path, { "id", "set", "unset" },
1553 { "id", "set", "unset" }
1554 );
1555 if (!value.is_object()) {
1556 return;
1557 }
1558 if (integer_id) {
1560 batch, value, "id", path, 1,
1561 std::numeric_limits<std::int64_t>::max()
1562 );
1563 } else {
1564 require_nonempty_string(batch, value, "id", path);
1565 const auto id = value.find("id");
1566 if (id != value.end() && id->is_string()
1567 && !valid_canonical_id(id->get_ref<const std::string&>(), family)) {
1568 add_issue(
1569 batch, "invalid_canonical_id", path + "/id",
1570 "id does not identify the required entity family", &*id
1571 );
1572 }
1573 }
1574 require_kind(batch, value, "set", path, value_kind::object);
1575 require_kind(batch, value, "unset", path, value_kind::array);
1576 std::set<std::string, std::less<>> set_names;
1577 const auto set = value.find("set");
1578 if (set != value.end() && set->is_object()) {
1579 for (const auto& [field, field_value] : set->items()) {
1580 const auto expected = mutable_fields.find(field);
1581 const std::string field_path
1582 = path + "/set/" + pointer_escape(field);
1583 if (expected == mutable_fields.end()) {
1584 add_issue(
1585 batch, "unknown_field", field_path,
1586 "field is not mutable for this entity family", &field_value
1587 );
1588 continue;
1589 }
1590 set_names.emplace(field);
1591 if (field_value.is_null()) {
1592 add_issue(
1593 batch, "ambiguous_null", field_path,
1594 "use unset to remove a field", &field_value
1595 );
1596 } else if (!is_kind(field_value, expected->second)) {
1597 add_issue(
1598 batch, "type_mismatch", field_path,
1599 "field has the wrong JSON type", &field_value
1600 );
1601 }
1602 if (field_value.is_string()
1603 && field_value.get_ref<const std::string&>().empty()) {
1604 add_issue(
1605 batch, "empty_string", field_path,
1606 "set value must not be empty", &field_value
1607 );
1608 }
1609 if (field == "slug" && field_value.is_string()
1610 && !valid_slug(
1611 field_value.get_ref<const std::string&>()
1612 )) {
1613 add_issue(
1614 batch, "invalid_slug", field_path,
1615 "slug must contain lowercase alphanumeric dash-separated tokens",
1616 &field_value
1617 );
1618 }
1619 if (field == "work_id" && field_value.is_string()
1621 field_value.get_ref<const std::string&>(), "work"
1622 )) {
1623 add_issue(
1624 batch, "invalid_canonical_id", field_path,
1625 "updated work_id must be an existing canonical work ID",
1626 &field_value
1627 );
1628 }
1629 if (field == "production_info_json" && field_value.is_string()) {
1630 try {
1631 const json parsed = json::parse(
1632 field_value.get<std::string>()
1633 );
1634 static_cast<void>(parsed);
1635 } catch (const json::exception&) {
1636 add_issue(
1637 batch, "invalid_json_text", field_path,
1638 "production_info_json must contain valid JSON",
1639 &field_value
1640 );
1641 }
1642 }
1643 const auto enum_rule = enum_fields.find(field);
1644 if (enum_rule != enum_fields.end() && field_value.is_string()
1645 && !enum_rule->second.contains(field_value.get<std::string>())) {
1646 add_issue(
1647 batch, "unknown_enum", field_path,
1648 "field contains an unknown enum value", &field_value
1649 );
1650 }
1651 }
1652 }
1653 const auto unset = value.find("unset");
1654 std::set<std::string, std::less<>> unset_names;
1655 if (unset != value.end() && unset->is_array()) {
1656 for (std::size_t index = 0; index < unset->size(); ++index) {
1657 const auto& field = (*unset)[index];
1658 const std::string field_path
1659 = indexed_path(path + "/unset", index);
1660 if (!field.is_string()) {
1661 add_issue(
1662 batch, "type_mismatch", field_path,
1663 "unset entry must be a mutable field name", &field
1664 );
1665 continue;
1666 }
1667 const std::string name = field.get<std::string>();
1668 if (!mutable_fields.contains(name)) {
1669 add_issue(
1670 batch, "unknown_field", field_path,
1671 "field is not mutable for this entity family", &field
1672 );
1673 } else if (required_fields.contains(name)) {
1674 add_issue(
1675 batch, "required_field_unset", field_path,
1676 "required field cannot be unset", &field
1677 );
1678 }
1679 if (!unset_names.emplace(name).second) {
1680 add_issue(
1681 batch, "duplicate_unset", field_path,
1682 "field occurs more than once in unset", &field
1683 );
1684 }
1685 if (set_names.contains(name)) {
1686 add_issue(
1687 batch, "set_unset_conflict", field_path,
1688 "field cannot be set and unset in the same operation", &field
1689 );
1690 }
1691 }
1692 }
1693 if (!allow_empty && set_names.empty() && unset_names.empty()) {
1694 add_issue(
1695 batch, "empty_update", path,
1696 "update must set or unset at least one field", &value
1697 );
1698 }
1699}

References array, and object.

Variable Documentation

◆ agent_merge_enums

const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::agent_merge_enums
Initial value:
{
{ "agent_type", agent_types },
}

Definition at line 1754 of file inbox.cpp.

1754 {
1755 { "agent_type", agent_types },
1756 };

◆ agent_merge_mutable

const std::map< std::string, value_kind, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::agent_merge_mutable
Initial value:
{
{ "agent_type", value_kind::string },
{ "birth_year", value_kind::integer },
{ "death_year", value_kind::integer },
}

Definition at line 1705 of file inbox.cpp.

1705 {
1706 { "agent_type", value_kind::string },
1707 { "birth_year", value_kind::integer },
1708 { "death_year", value_kind::integer },
1709};

◆ agent_mutable

const std::map< std::string, value_kind, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::agent_mutable
Initial value:
{
{ "birth_year", value_kind::integer },
{ "death_year", value_kind::integer },
}

Definition at line 1701 of file inbox.cpp.

1701 {
1702 { "birth_year", value_kind::integer },
1703 { "death_year", value_kind::integer },
1704};

◆ agent_types

const std::set< std::string, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::agent_types
Initial value:
{
"person", "organization", "group"
}

Definition at line 829 of file inbox.cpp.

829 {
830 "person", "organization", "group"
831};

◆ agent_update_enums

const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::agent_update_enums
Initial value:
{
}

Definition at line 1750 of file inbox.cpp.

1750 {
1751 };

◆ concept_mutable

const std::map< std::string, value_kind, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::concept_mutable
Initial value:
{
{ "concept_type", value_kind::string },
{ "slug", value_kind::string },
}

Definition at line 1722 of file inbox.cpp.

1722 {
1723 { "concept_type", value_kind::string },
1724 { "slug", value_kind::string },
1725};

◆ concept_relation_types

const std::set< std::string, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::concept_relation_types
Initial value:
{
"broader_than", "narrower_than", "derived_from", "precursor_of",
"hybrid_of", "revival_of", "regional_variant_of", "influenced_by",
"opposes", "alias_of"
}

Definition at line 875 of file inbox.cpp.

875 {
876 "broader_than", "narrower_than", "derived_from", "precursor_of",
877 "hybrid_of", "revival_of", "regional_variant_of", "influenced_by",
878 "opposes", "alias_of"
879};

◆ concept_types

const std::set< std::string, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::concept_types
Initial value:
{
"genre", "style", "theme", "keyword", "motif", "trope", "phobia",
"taboo", "technique", "movement", "setting", "mood", "content_warning"
}

Definition at line 841 of file inbox.cpp.

841 {
842 "genre", "style", "theme", "keyword", "motif", "trope", "phobia",
843 "taboo", "technique", "movement", "setting", "mood", "content_warning"
844};

◆ concept_update_enums

const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::concept_update_enums
Initial value:
{
{ "concept_type", concept_types },
}

Definition at line 1765 of file inbox.cpp.

1765 {
1766 { "concept_type", concept_types },
1767 };

◆ credit_roles

const std::set< std::string, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::credit_roles
Initial value:
{
"author", "director", "screenwriter", "producer", "actor", "composer",
"performer", "artist", "engraver", "sculptor", "photographer", "editor",
"cinematographer", "production_company", "publisher", "record_label",
"band"
}

Definition at line 860 of file inbox.cpp.

860 {
861 "author", "director", "screenwriter", "producer", "actor", "composer",
862 "performer", "artist", "engraver", "sculptor", "photographer", "editor",
863 "cinematographer", "production_company", "publisher", "record_label",
864 "band"
865};

◆ current_product_schema

int arachne::penelope::anonymous_namespace{inbox.cpp}::current_product_schema = 5
constexpr

◆ date_precisions

const std::set< std::string, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::date_precisions
Initial value:
{
"year", "decade", "approximate", "range", "exact"
}

Definition at line 838 of file inbox.cpp.

838 {
839 "year", "decade", "approximate", "range", "exact"
840};

◆ guide_categories

const std::set< std::string, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::guide_categories
Initial value:
{
"violence", "sex_nudity", "language", "drugs", "frightening", "self_harm",
"discrimination", "abuse", "taboo"
}

Definition at line 888 of file inbox.cpp.

888 {
889 "violence", "sex_nudity", "language", "drugs", "frightening", "self_harm",
890 "discrimination", "abuse", "taboo"
891};

◆ historical_roles

const std::set< std::string, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::historical_roles
Initial value:
{
"formative", "canonical", "transitional", "hybrid", "revival",
"late_derivative", "peripheral", "precursor"
}

Definition at line 884 of file inbox.cpp.

884 {
885 "formative", "canonical", "transitional", "hybrid", "revival",
886 "late_derivative", "peripheral", "precursor"
887};

◆ importance_values

const std::set< std::string, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::importance_values
Initial value:
{
"primary", "key", "supporting"
}

Definition at line 866 of file inbox.cpp.

866 {
867 "primary", "key", "supporting"
868};

◆ manifestation_mutable

const std::map< std::string, value_kind, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::manifestation_mutable
Initial value:
{
{ "work_id", value_kind::string },
{ "manifestation_type", value_kind::string },
{ "release_year", value_kind::integer },
{ "region_code", value_kind::string },
{ "language_code", value_kind::string },
{ "label", value_kind::string },
}

Definition at line 1727 of file inbox.cpp.

1727 {
1728 { "work_id", value_kind::string },
1729 { "manifestation_type", value_kind::string },
1730 { "release_year", value_kind::integer },
1731 { "region_code", value_kind::string },
1732 { "language_code", value_kind::string },
1733 { "label", value_kind::string },
1734 };

◆ manifestation_types

const std::set< std::string, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::manifestation_types
Initial value:
{
"edition", "translation", "release", "pressing", "cut", "restoration",
"reissue"
}

Definition at line 845 of file inbox.cpp.

845 {
846 "edition", "translation", "release", "pressing", "cut", "restoration",
847 "reissue"
848};

◆ manifestation_update_enums

const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::manifestation_update_enums
Initial value:
{
{ "manifestation_type", manifestation_types },
}

Definition at line 1770 of file inbox.cpp.

1770 {
1771 { "manifestation_type", manifestation_types },
1772 };

◆ maximum_batch_bytes

std::uintmax_t arachne::penelope::anonymous_namespace{inbox.cpp}::maximum_batch_bytes = 32U * 1024U * 1024U
constexpr

Definition at line 48 of file inbox.cpp.

Referenced by read_batch_file().

◆ measurement_types

const std::set< std::string, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::measurement_types
Initial value:
{
"duration", "height", "width", "depth", "pages"
}

Definition at line 869 of file inbox.cpp.

869 {
870 "duration", "height", "width", "depth", "pages"
871};

◆ measurement_units

const std::set< std::string, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::measurement_units
Initial value:
{
"seconds", "millimetres", "pages"
}

Definition at line 872 of file inbox.cpp.

872 {
873 "seconds", "millimetres", "pages"
874};

◆ media

const std::set< std::string, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::media
Initial value:
{
"film", "short_film", "television", "novel", "novella", "short_story",
"poetry", "play", "essay", "album", "single", "composition", "painting",
"print", "engraving", "drawing", "sculpture", "installation",
"photography", "mixed_media"
}

Definition at line 832 of file inbox.cpp.

832 {
833 "film", "short_film", "television", "novel", "novella", "short_story",
834 "poetry", "play", "essay", "album", "single", "composition", "painting",
835 "print", "engraving", "drawing", "sculpture", "installation",
836 "photography", "mixed_media"
837};

◆ name_types

const std::set< std::string, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::name_types
Initial value:
{
"original", "english", "transliteration", "translation", "alias",
"credited"
}

Definition at line 849 of file inbox.cpp.

849 {
850 "original", "english", "transliteration", "translation", "alias",
851 "credited"
852};

◆ source_mutable

const std::map< std::string, value_kind, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::source_mutable
Initial value:
{
{ "source_type", value_kind::string },
{ "title", value_kind::string },
{ "bibliography_text", value_kind::string },
{ "author_text", value_kind::string },
{ "publisher", value_kind::string },
{ "publication_date", value_kind::string },
{ "url", value_kind::string },
{ "doi", value_kind::string },
{ "isbn", value_kind::string },
{ "language_code", value_kind::string },
}

Definition at line 1735 of file inbox.cpp.

1735 {
1736 { "source_type", value_kind::string },
1737 { "title", value_kind::string },
1738 { "bibliography_text", value_kind::string },
1739 { "author_text", value_kind::string },
1740 { "publisher", value_kind::string },
1741 { "publication_date", value_kind::string },
1742 { "url", value_kind::string },
1743 { "doi", value_kind::string },
1744 { "isbn", value_kind::string },
1745 { "language_code", value_kind::string },
1746};

◆ source_types

const std::set< std::string, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::source_types
Initial value:
{
"book", "article", "catalogue", "web_page", "interview", "database",
"video", "audio", "PDF"
}

Definition at line 853 of file inbox.cpp.

853 {
854 "book", "article", "catalogue", "web_page", "interview", "database",
855 "video", "audio", "PDF"
856};

◆ source_update_enums

const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::source_update_enums
Initial value:
{
{ "source_type", source_types },
}

Definition at line 1775 of file inbox.cpp.

1775 {
1776 { "source_type", source_types },
1777 };

◆ spoiler_levels

const std::set< std::string, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::spoiler_levels
Initial value:
{
"none", "mild", "major"
}

Definition at line 892 of file inbox.cpp.

892 {
893 "none", "mild", "major"
894};

◆ stances

const std::set< std::string, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::stances
Initial value:
{
"supports", "contradicts", "contextualizes"
}

Definition at line 857 of file inbox.cpp.

857 {
858 "supports", "contradicts", "contextualizes"
859};

◆ work_concept_types

const std::set< std::string, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::work_concept_types
Initial value:
{
"exemplifies", "contains", "anticipates", "influenced_by", "influences",
"revives", "parodies", "deconstructs", "associated_with"
}

Definition at line 880 of file inbox.cpp.

880 {
881 "exemplifies", "contains", "anticipates", "influenced_by", "influences",
882 "revives", "parodies", "deconstructs", "associated_with"
883};

◆ work_mutable

const std::map< std::string, value_kind, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::work_mutable
Initial value:
{
{ "medium", value_kind::string },
{ "year_start", value_kind::integer },
{ "year_end", value_kind::integer },
{ "date_precision", value_kind::string },
{ "date_start_text", value_kind::string },
{ "date_end_text", value_kind::string },
{ "date_qualifier", value_kind::string },
{ "language_code", value_kind::string },
{ "country_code", value_kind::string },
{ "production_info_json", value_kind::string },
}

Definition at line 1710 of file inbox.cpp.

1710 {
1711 { "medium", value_kind::string },
1712 { "year_start", value_kind::integer },
1713 { "year_end", value_kind::integer },
1714 { "date_precision", value_kind::string },
1715 { "date_start_text", value_kind::string },
1716 { "date_end_text", value_kind::string },
1717 { "date_qualifier", value_kind::string },
1718 { "language_code", value_kind::string },
1719 { "country_code", value_kind::string },
1720 { "production_info_json", value_kind::string },
1721};

◆ work_update_enums

const std::map< std::string, std::set< std::string, std::less<> >, std::less<> > arachne::penelope::anonymous_namespace{inbox.cpp}::work_update_enums
Initial value:
{
{ "medium", media },
{ "date_precision", date_precisions },
}

Definition at line 1759 of file inbox.cpp.

1759 {
1760 { "medium", media },
1761 { "date_precision", date_precisions },
1762 };