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

Emacs 29.3 (Org mode 9.6.15)