ores.cpp.repository.repository_header
Table of Contents
Repository declarations with custom-member paste points. repository profile. Database access layer using sqlgen parameterised queries, 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 <vector>
#include <sqlgen/postgres.hpp>
#include "ores.logging/make_logger.hpp"
#include "ores.database/domain/context.hpp"
#include "ores.{{component_include}}/domain/{{entity_singular}}.hpp"
#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 {
/**
* @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 SQL created by sqlgen to construct the table.
*/
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);
/**
* @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}}
{{#parent_column}}
/**
* @brief Reads latest {{entity_plural_words}} filtered by {{parent_column}} and {{column}}.
*/
std::vector<domain::{{entity_singular}}>
read_latest_by_{{suffix}}(context ctx, const std::string& {{parent_column}}, const std::string& {{column}});
{{/parent_column}}
{{^parent_column}}
/**
* @brief Reads latest {{entity_plural_words}} filtered by {{column}}.
*/
std::vector<domain::{{entity_singular}}>
read_latest_by_{{suffix}}(context ctx, const std::string& {{column}});
{{/parent_column}}
{{/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.
*/
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
*/
std::vector<domain::{{entity_singular}}>
read_latest_by_{{column}}(context ctx, const std::string& {{column}},
std::uint32_t offset, std::uint32_t limit);
/**
* @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}});
{{/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 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
*/
std::vector<domain::{{entity_singular}}>
read_latest(context ctx, std::uint32_t offset, std::uint32_t limit);
{{#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);
/**
{{#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>
#include <optional>
#include <string>
#include <vector>
#include <sqlgen/postgres.hpp>
{{#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 {
/**
{{#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}}>
{{#left.is_uuid}}
read_latest(const boost::uuids::uuid& {{left.column}},
{{/left.is_uuid}}
{{^left.is_uuid}}
read_latest(const std::string& {{left.column}},
{{/left.is_uuid}}
{{#right.is_uuid}}
const boost::uuids::uuid& {{right.column}});
{{/right.is_uuid}}
{{^right.is_uuid}}
const std::string& {{right.column}});
{{/right.is_uuid}}
/**
* @brief Gets the total count of active {{name_words}}.
*/
std::uint32_t get_total_{{name_singular_short}}_count();
std::vector<domain::{{name_singular}}>
{{#left.is_uuid}}
read_latest_by_{{left.column_short}}(const boost::uuids::uuid& {{left.column}});
{{/left.is_uuid}}
{{^left.is_uuid}}
read_latest_by_{{left.column_short}}(const std::string& {{left.column}});
{{/left.is_uuid}}
{{#left.list_by}}
/**
* @brief Reads latest {{name_words}} filtered by {{left.column}}, with pagination.
*/
std::vector<domain::{{name_singular}}>
{{#left.is_uuid}}
read_latest_by_{{left.column_short}}(const boost::uuids::uuid& {{left.column}},
std::uint32_t offset, std::uint32_t limit);
{{/left.is_uuid}}
{{^left.is_uuid}}
read_latest_by_{{left.column_short}}(const std::string& {{left.column}},
std::uint32_t offset, std::uint32_t limit);
{{/left.is_uuid}}
{{/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}}(
{{#left.is_uuid}}
const boost::uuids::uuid& {{left.column}});
{{/left.is_uuid}}
{{^left.is_uuid}}
const std::string& {{left.column}});
{{/left.is_uuid}}
std::vector<domain::{{name_singular}}>
{{#right.is_uuid}}
read_latest_by_{{right.column_short}}(const boost::uuids::uuid& {{right.column}});
{{/right.is_uuid}}
{{^right.is_uuid}}
read_latest_by_{{right.column_short}}(const std::string& {{right.column}});
{{/right.is_uuid}}
{{#right.list_by}}
/**
* @brief Reads latest {{name_words}} filtered by {{right.column}}, with pagination.
*/
std::vector<domain::{{name_singular}}>
{{#right.is_uuid}}
read_latest_by_{{right.column_short}}(const boost::uuids::uuid& {{right.column}},
std::uint32_t offset, std::uint32_t limit);
{{/right.is_uuid}}
{{^right.is_uuid}}
read_latest_by_{{right.column_short}}(const std::string& {{right.column}},
std::uint32_t offset, std::uint32_t limit);
{{/right.is_uuid}}
{{/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}}(
{{#right.is_uuid}}
const boost::uuids::uuid& {{right.column}});
{{/right.is_uuid}}
{{^right.is_uuid}}
const std::string& {{right.column}});
{{/right.is_uuid}}
{{^read_only}}
/**
* @brief Deletes a {{name_singular_words}} by its pair of keys.
*/
void remove(
{{#left.is_uuid}}
const boost::uuids::uuid& {{left.column}},
{{/left.is_uuid}}
{{^left.is_uuid}}
const std::string& {{left.column}},
{{/left.is_uuid}}
{{#right.is_uuid}}
const boost::uuids::uuid& {{right.column}});
{{/right.is_uuid}}
{{^right.is_uuid}}
const std::string& {{right.column}});
{{/right.is_uuid}}
/**
* @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(
{{#left.is_uuid}}
const boost::uuids::uuid& {{left.column}},
{{/left.is_uuid}}
{{^left.is_uuid}}
const std::string& {{left.column}},
{{/left.is_uuid}}
{{#right.is_uuid}}
const boost::uuids::uuid& {{right.column}},
{{/right.is_uuid}}
{{^right.is_uuid}}
const std::string& {{right.column}},
{{/right.is_uuid}}
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}}(
{{#left.is_uuid}}
const boost::uuids::uuid& {{left.column}});
{{/left.is_uuid}}
{{^left.is_uuid}}
const std::string& {{left.column}});
{{/left.is_uuid}}
{{/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