ores.cpp.repository.repository_header
Table of Contents
Repository declarations with custom-member paste points. repository profile. Database access layer through the query builder on the libpq layer, tracking `valid_from`/`valid_to`.
See the Template variable reference for the complete list of available variables and their semantics.
1. Template
The full template source. Edit here and re-tangle with
compass build --direct tangle_codegen_templates to regenerate
library/templates/cpp_domain_type_repository.hpp.mustache.
{{! GENERATED FILE — tangled from projects/ores.codegen/library/templates/ores.cpp.repository.repository_header.org. Edit the org source. }}
{{{cpp_license}}}
{{#domain_entity}}
#ifndef ORES_{{component_core_upper}}_REPOSITORY_{{entity_singular_upper}}_REPOSITORY_HPP
#define ORES_{{component_core_upper}}_REPOSITORY_{{entity_singular_upper}}_REPOSITORY_HPP
#include <chrono>
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "ores.logging/make_logger.hpp"
#include "ores.database/domain/context.hpp"
#include "ores.{{component_include}}/domain/{{entity_singular}}.hpp"
{{#domain_entity.has_filter_record}}
#include "ores.{{component_include}}/messaging/{{entity_singular}}_protocol.hpp"
{{/domain_entity.has_filter_record}}
#include "ores.utility/domain/protocol.hpp"
#include "ores.{{component_core}}/export.hpp"
{{#has_parent_id}}
#include "ores.utility/domain/hierarchy.hpp"
#include <boost/uuid/uuid.hpp>
{{/has_parent_id}}
namespace ores::{{component}}::repository {
<<paste:E4B7A1C3-9D25-4F68-B0A7-3C6E8D1F5A92>>
/**
* @brief Reads and writes {{entity_plural_words}} to data storage.
*/
class ORES_{{component_core_upper}}_EXPORT {{entity_singular}}_repository {
private:
inline static std::string_view logger_name =
"ores.{{component}}.repository.{{entity_singular}}_repository";
[[nodiscard]] static auto& lg() {
using namespace ores::logging;
static auto instance = make_logger(logger_name);
return instance;
}
public:
using context = ores::database::context;
/**
* @brief Returns the insert statement the repository writes.
*/
std::string sql();
/**
* @brief Writes {{entity_plural_words}} to database.
*
* The plain form replaces the row the caller last read: it states the
* version the row carries now, so the store can tell a replace from a
* create. A row that moved on since that read is a conflict, never a silent
* overwrite.
*/
/**@{*/
void write(context ctx, const domain::{{entity_singular}}& v);
void write(context ctx, const std::vector<domain::{{entity_singular}}>& v);
/**@}*/
/**
* @brief Writes a {{entity_singular_words}}, honouring the claim it states.
*
* The claim is the version the caller read (@c must_match_version), that no
* current row exists (@c must_not_exist), or neither (@c any, which
* replaces the row as it stands). The store decides in the write's own
* transaction, so a create that collides with a live row and a write over a
* row that moved on are refused by the store rather than by a check a
* caller might have forgotten.
{{^has_audit_columns}}
*
* This table carries no version column, so the store cannot check a
* version. A @c must_match_version claim is refused here, and a
* @c must_not_exist claim over a live row is refused by the read below
* rather than by the trigger.
{{/has_audit_columns}}
*/
void write(context ctx, const domain::{{entity_singular}}& v,
const ores::utility::domain::precondition& claim);
/**
* @brief Writes a set of {{entity_plural_words}}, each honouring its own
* claim, as one statement.
*/
void write(context ctx, const std::vector<domain::{{entity_singular}}>& v,
const std::vector<ores::utility::domain::precondition>& claims);
{{#append_insert}}
/**
* @brief Appends {{entity_plural_words}} without the claim a write makes.
*
* The table is an append-only series: a row is never edited in place, so
* an insert reads no current row first. A bulk load is one statement, not
* one claim query per row.
*/
/**@{*/
void insert(context ctx, const domain::{{entity_singular}}& v);
void insert(context ctx, const std::vector<domain::{{entity_singular}}>& v);
/**@}*/
{{/append_insert}}
/**
* @brief Reads latest {{entity_plural_words}}, possibly filtered by primary key.
*/
/**@{*/
std::vector<domain::{{entity_singular}}> read_latest(context ctx);
std::vector<domain::{{entity_singular}}>
read_latest(context ctx, {{{primary_key.params}}});
std::vector<domain::{{entity_singular}}>
read_latest(context ctx, {{{primary_key.batch_params}}});
/**@}*/
{{#key_finders}}
/**
* @brief Reads latest {{entity_plural_words}} filtered by {{{key_label}}}.
*/
std::vector<domain::{{entity_singular}}>
read_latest_by_{{suffix}}(context ctx, {{{params}}});
{{/key_finders}}
{{^current_state}}
{{#key_resolvers}}
/**
* @brief Reads the newest {{entity_plural_words}} filtered by {{column}}, current or not.
*
* History is addressed by the key the model declares and must stay readable
* after a delete, which closes the transaction-time window rather than
* removing the row. A latest read cannot resolve a closed row, so this one
* ignores the window and takes the newest match.
{{#domain_entity.has_party_column}}
*
* A key names a row of one party, and another party of the tenant may hold
* the same key, so the match is kept to the session's party. A null party
* column means the row belongs to the whole tenant and is kept for every
* party.
{{/domain_entity.has_party_column}}
*/
std::vector<domain::{{entity_singular}}>
read_any_by_{{suffix}}(context ctx, const std::string& {{column}});
{{/key_resolvers}}
{{/current_state}}
/**
{{#current_state}}
* @brief Reads the {{entity_singular_words}} rows for the given primary key.
*
* A current-state table holds one row per key, so this is the single
* current row, not a version history.
{{/current_state}}
{{^current_state}}
* @brief Reads all {{entity_plural_words}}, possibly filtered by primary key.
{{/current_state}}
*/
std::vector<domain::{{entity_singular}}>
read_all(context ctx, {{{primary_key.params}}});
{{#has_audit_columns}}
/**
* @brief Reads a single {{entity_singular_words}} as it stood at a specific
* version — the version's own [valid_from, valid_to) window is returned
* verbatim, so the caller can compose child entities "as of" the same
* window. See the "Temporal composite entity versioning" architecture
* doc.
* @param ctx Repository context with database connection
* @param version The version to fetch
*/
std::optional<domain::{{entity_singular}}>
read_at_version(context ctx, {{{primary_key.params}}}, std::uint32_t version);
{{/has_audit_columns}}
{{#foreign_keys}}
{{#list_by}}
/**
* @brief Reads latest {{entity_plural_words}} filtered by {{column}}, with pagination.
* @param ctx Repository context with database connection
* @param {{column}} The {{column}} to filter by
* @param offset Number of records to skip
* @param limit Maximum number of records to return
* @param order The stated order; an empty field is the default order
*/
std::vector<domain::{{entity_singular}}>
read_latest_by_{{column}}(context ctx, const std::string& {{column}},
std::uint32_t offset, std::uint32_t limit,
const ores::utility::domain::order& order = {}{{#domain_entity.has_filter_record}},
const std::optional<{{domain_entity.filter_type}}>& filter = std::nullopt{{/domain_entity.has_filter_record}});
/**
* @brief Gets the total count of active {{entity_plural_words}} filtered by {{column}}.
*/
std::uint32_t get_total_{{entity_singular_short}}_count_by_{{column}}(context ctx,
const std::string& {{column}}{{#domain_entity.has_filter_record}},
const std::optional<{{domain_entity.filter_type}}>& filter = std::nullopt{{/domain_entity.has_filter_record}});
{{/list_by}}
{{#list_by_as_of}}
/**
* @brief Reads {{entity_plural_words}} filtered by {{column}} that were live at
* any point during [valid_from_bound, valid_to_bound) — i.e. the set of
* {{entity_plural_words}} that compose a parent entity's state as of one of
* its own historical versions. See the "Temporal composite entity
* versioning" architecture doc.
* @param ctx Repository context with database connection
* @param {{column}} The {{column}} to filter by
* @param valid_from_bound The parent version's own valid_from
* @param valid_to_bound The parent version's own valid_to
*/
std::vector<domain::{{entity_singular}}>
read_by_{{column}}_as_of(context ctx, const std::string& {{column}},
std::chrono::system_clock::time_point valid_from_bound,
std::chrono::system_clock::time_point valid_to_bound);
{{/list_by_as_of}}
{{/foreign_keys}}
/**
* @brief Whether a list of {{entity_plural_words}} can be ordered by a field.
*
* The model's :sortable: columns, and nothing else.
*/
static bool is_sortable(std::string_view field);
/**
* @brief Reads latest {{entity_plural_words}} with pagination support.
* @param ctx Repository context with database connection
* @param offset Number of records to skip
* @param limit Maximum number of records to return
* @param order The stated order; an empty field is the default order
* @param filter The filter record; the members it sets must all hold
* @throws std::invalid_argument if the field is not sortable
*/
std::vector<domain::{{entity_singular}}>
read_latest(context ctx, std::uint32_t offset, std::uint32_t limit,
const ores::utility::domain::order& order = {}{{#domain_entity.has_filter_record}},
const std::optional<{{domain_entity.filter_type}}>& filter = std::nullopt{{/domain_entity.has_filter_record}}{{#domain_entity.has_as_of_lookup}},
const std::optional<std::string>& as_of = std::nullopt{{/domain_entity.has_as_of_lookup}});
{{#domain_entity.has_newest_read}}
/**
* @brief Reads the newest {{entity_singular_words}} for the caller.
*
* A time-series table is read from its newest end. A paged read walks the
* key in order, so it returns the oldest row first and cannot answer "the
* last one" without reading everything before it. The model names the
* column the newest is measured by.
*/
std::optional<domain::{{entity_singular}}> read_newest(context ctx);
{{/domain_entity.has_newest_read}}
/**
* @brief Gets the total count of active {{entity_plural_words}}.
* @param ctx Repository context with database connection
* @return Total number of active {{entity_plural_words}}
*/
std::uint32_t get_total_{{entity_singular_short}}_count(context ctx{{#domain_entity.has_filter_record}},
const std::optional<{{domain_entity.filter_type}}>& filter = std::nullopt{{/domain_entity.has_filter_record}}{{#domain_entity.has_as_of_lookup}},
const std::optional<std::string>& as_of = std::nullopt{{/domain_entity.has_as_of_lookup}});
/**
{{#current_state}}
* @brief Deletes a {{entity_singular_words}} permanently.
*
* A current-state table has no history, so the row is removed, not
* soft-closed.
{{/current_state}}
{{^current_state}}
* @brief Deletes a {{entity_singular_words}} by closing its temporal validity.
{{/current_state}}
*/
void remove(context ctx, {{{primary_key.params}}});
/**
* @brief What a removal did, so a caller reports a conflict as an outcome
* rather than catching an exception.
*
* @c missing means there was no current row to remove, and @c unsupported
* means the store cannot answer the version at all -- a current-state
* table has no version column, so a versioned removal has no meaning
* there.
*/
enum class remove_status { removed, conflicting, missing, unsupported };
/**
* @brief Removes a {{entity_singular_words}}, refusing a row that moved on.
*
* A stated version is the version the caller read. The removal is refused
* with @c conflicting when the current row carries another, so a caller
* that decided on stale state cannot remove a change it never saw. A null
* version removes whatever is current, which is what a caller that stated
* no version asked for.
*/
remove_status remove(context ctx, {{{primary_key.params}}},
std::optional<std::uint32_t> version);
/**
{{#current_state}}
* @brief Deletes {{entity_plural_words}} permanently.
{{/current_state}}
{{^current_state}}
* @brief Deletes {{entity_plural_words}} by closing their temporal validity.
{{/current_state}}
*/
void remove(context ctx, {{{primary_key.batch_params}}});
{{#has_parent_id}}
/**
* @brief Reads the {{entity_singular}} hierarchy as a flat set of {id,
* parent_id, name} rows, via {{sql_name_base}}_hierarchy_fn.
*
* @param ctx Repository context with database connection (tenant is
* derived from ctx.tenant_id()).
* @param root_id The {{entity_singular}} to start from.
* @param from_root If true, first walks up to the ultimate ancestor and
* returns the whole tree the given node belongs to, instead of just its
* subtree.
* @return Flat hierarchy rows, ready for ores::utility::domain::build_tree.
*/
std::vector<ores::utility::domain::hierarchy_flat_row>
get_hierarchy(context ctx, const boost::uuids::uuid& root_id, bool from_root);
{{/has_parent_id}}
<<paste:DCA78C69-E508-48D9-9972-A9B8094D91FB>>
private:
/**
* @brief The claim a replace makes: the version the row carries now, or
* that no row exists yet.
*/
ores::utility::domain::precondition
replace_claim(context ctx, const domain::{{entity_singular}}& v);
/**
* @brief The object with the claim's version stamped onto it.
*
* A claim the store cannot check is refused here rather than ignored.
*/
domain::{{entity_singular}}
apply_claim(context ctx, const domain::{{entity_singular}}& v,
const ores::utility::domain::precondition& claim);
};
}
#endif
{{/domain_entity}}
{{#junction}}
#ifndef ORES_{{component_core_upper}}_REPOSITORY_{{name_singular_upper}}_REPOSITORY_HPP
#define ORES_{{component_core_upper}}_REPOSITORY_{{name_singular_upper}}_REPOSITORY_HPP
#include <cstdint>
{{#history}}
#include <chrono>
{{/history}}
#include <optional>
#include <string>
#include <vector>
{{#has_uuid_left_or_right}}
#include <boost/uuid/uuid.hpp>
{{/has_uuid_left_or_right}}
#include "ores.logging/make_logger.hpp"
#include "ores.database/domain/context.hpp"
#include "ores.{{component_include}}/domain/{{name_singular}}.hpp"
#include "ores.utility/domain/protocol.hpp"
#include "ores.{{component_core}}/export.hpp"
namespace ores::{{component}}::repository {
{{#history}}
/**
* @brief One version of a {{name_singular_words}}, with the end of its validity.
*
* A link is closed by ending its validity, not by deleting the row, so a closed
* link is a row whose end is in the past. @c closed_at is set on a version that
* ended because the link was closed, and empty on a version that a later
* version replaced and on a link that is open. @c closed_by names the account
* that closed it, and is empty on the same versions. The domain type does not carry
* it, and it is final, so the history row holds the link beside it. The actor
* and the time stand flat on the row as well, which is the shape the history
* builder reads.
*/
struct {{name_singular}}_history_row {
domain::{{name_singular}} link;
std::string modified_by;
std::chrono::system_clock::time_point recorded_at;
std::optional<std::chrono::system_clock::time_point> closed_at;
std::string closed_by;
};
{{/history}}
/**
{{#read_only}}
* @brief Reads {{name_words}} from data storage. Read-only: this
* junction's rows are managed via SQL provisioning, not application
* writes.
{{/read_only}}
{{^read_only}}
* @brief Reads and writes {{name_words}} to data storage.
{{/read_only}}
*/
class ORES_{{component_core_upper}}_EXPORT {{name_singular}}_repository {
private:
inline static std::string_view logger_name =
"ores.{{component}}.repository.{{name_singular}}_repository";
[[nodiscard]] static auto& lg() {
using namespace ores::logging;
static auto instance = make_logger(logger_name);
return instance;
}
public:
using context = ores::database::context;
explicit {{name_singular}}_repository(context ctx);
std::string sql();
{{^read_only}}
/**
* @brief Writes {{name_words}} to database.
*
* The plain form replaces the link the caller last read: it states the
* version the row carries now, so the store can tell a replace from a
* create. A row that moved on since that read is a conflict, never a
* silent overwrite.
*/
/**@{*/
void write(const domain::{{name_singular}}& v);
void write(const std::vector<domain::{{name_singular}}>& v);
/**@}*/
/**
* @brief Writes a {{name_singular_words}}, honouring the claim it states.
*
* The claim is the version the caller read (@c must_match_version), that no
* current row exists (@c must_not_exist), or neither (@c any, which
* replaces the row as it stands). The store decides in the write's own
* transaction, so a create that collides with a live row and a write over a
* row that moved on are refused by the store rather than by a check a
* caller might have forgotten.
*/
void write(const domain::{{name_singular}}& v,
const ores::utility::domain::precondition& claim);
/**
* @brief Writes a set of {{name_words}}, each honouring its own claim, as
* one statement.
*/
void write(const std::vector<domain::{{name_singular}}>& v,
const std::vector<ores::utility::domain::precondition>& claims);
{{/read_only}}
std::vector<domain::{{name_singular}}> read_latest();
std::vector<domain::{{name_singular}}>
read_latest(std::uint32_t offset, std::uint32_t limit);
/**
* @brief Reads the {{name_singular_words}} rows for the given pair of keys.
*
* A junction key is the whole pair the link names, so a read that states
* only one half addresses a set and not a row.
*/
std::vector<domain::{{name_singular}}>
read_latest(const {{left.cpp_type}}& {{left.column}},
const {{right.cpp_type}}& {{right.column}});
/**
* @brief Gets the total count of active {{name_words}}.
*/
std::uint32_t get_total_{{name_singular_short}}_count();
std::vector<domain::{{name_singular}}>
read_latest_by_{{left.column_short}}(const {{left.cpp_type}}& {{left.column}});
{{#left.list_by}}
/**
* @brief Reads latest {{name_words}} filtered by {{left.column}}, with pagination.
*/
std::vector<domain::{{name_singular}}>
read_latest_by_{{left.column_short}}(const {{left.cpp_type}}& {{left.column}},
std::uint32_t offset, std::uint32_t limit);
{{/left.list_by}}
/**
* @brief Gets the total count of active {{name_words}} filtered by {{left.column}}.
*/
std::uint32_t get_total_{{name_singular_short}}_count_by_{{left.column_short}}(
const {{left.cpp_type}}& {{left.column}});
std::vector<domain::{{name_singular}}>
read_latest_by_{{right.column_short}}(const {{right.cpp_type}}& {{right.column}});
{{#right.list_by}}
/**
* @brief Reads latest {{name_words}} filtered by {{right.column}}, with pagination.
*/
std::vector<domain::{{name_singular}}>
read_latest_by_{{right.column_short}}(const {{right.cpp_type}}& {{right.column}},
std::uint32_t offset, std::uint32_t limit);
{{/right.list_by}}
/**
* @brief Gets the total count of active {{name_words}} filtered by {{right.column}}.
*/
std::uint32_t get_total_{{name_singular_short}}_count_by_{{right.column_short}}(
const {{right.cpp_type}}& {{right.column}});
{{#history}}
/**
* @brief Reads every version of every {{name_singular_words}} of one {{history_parent.column_title_lower}}.
*
* Closed links are included. The rows are grouped by {{history_child.column_title_lower}}
* and run newest first within a group, so a caller numbers and diffs the
* versions of one link by walking a group.
*/
std::vector<{{name_singular}}_history_row>
read_all_by_{{history_parent.column_short}}(const {{history_parent.cpp_type}}& {{history_parent.column}});
{{/history}}
{{^read_only}}
/**
* @brief Deletes a {{name_singular_words}} by its pair of keys.
*/
void remove(
const {{left.cpp_type}}& {{left.column}},
const {{right.cpp_type}}& {{right.column}});
/**
* @brief What a removal did, so a caller reports a conflict as an outcome
* rather than catching an exception.
*
* @c missing means there was no current row to remove, and @c unsupported
* means the store cannot answer the version at all.
*/
enum class remove_status { removed, conflicting, missing, unsupported };
/**
* @brief Removes a {{name_singular_words}}, refusing a row that moved on.
*
* A stated version is the version the caller read. The removal is refused
* with @c conflicting when the current row carries another, so a caller
* that decided on stale state cannot remove a change it never saw. A null
* version removes whatever is current, which is what a caller that stated
* no version asked for.
*/
remove_status remove(
const {{left.cpp_type}}& {{left.column}},
const {{right.cpp_type}}& {{right.column}},
std::optional<std::uint32_t> version);
/**
* @brief Deletes {{name_words}} by their pairs of keys.
*/
void remove(const std::vector<{{left.cpp_type}}>& {{left.column}}s,
const std::vector<{{right.cpp_type}}>& {{right.column}}s);
void remove_by_{{left.column_short}}(
const {{left.cpp_type}}& {{left.column}});
{{/read_only}}
private:
context ctx_;
{{^read_only}}
/**
* @brief The claim a replace makes: the version the row carries now, or
* that no row exists yet.
*/
ores::utility::domain::precondition
replace_claim(const domain::{{name_singular}}& v);
/**
* @brief The object with the claim's version stamped onto it.
*
* A claim the store cannot check is refused here rather than ignored.
*/
domain::{{name_singular}}
apply_claim(const domain::{{name_singular}}& v,
const ores::utility::domain::precondition& claim);
{{/read_only}}
};
}
#endif
{{/junction}}
2. See also
- Parent facet: ores.cpp.repository
- Template variable reference