Codegen entity meta-model — keys and columns

Table of Contents

This page is a segment of the Codegen org-entity meta-model hub. It is the one segment that does not map to a single facet — a column declaration is relevant to both SQL (to generate the table's DDL) and C++ Domain/C++ Repository (to generate the domain struct field and the sqlgen-mapped entity field), so it was abstracted into a segment of its own rather than duplicated in each. Author a column once here; it becomes a SQL column, a domain struct field, and a repository entity field, all three kept in sync by construction.

This page splits its fields across two diagrams, per Applied MASD § "The entity model file as a unified literate document":

1. Physical model mapping

This segment projects into three facets in physical space:

Facet Description
ores.sql.schema Bi-temporal schema: table create/drop, insert triggers, notify triggers.
ores.cpp.domain Domain types: class, JSON I/O, table I/O.
ores.cpp.repository Repository layer: entity, mapper, CRUD.

2. Flags

Cross-cutting properties consulted by both the SQL and C++ pipelines, set once at the top of the file:

Property Required Meaning
:schema: yes Postgres schema (almost always public).
:product: yes Top-level C++ namespace (ores).
:component: yes Component name (refdata, trading, …) — drives both the C++ namespace and the <product>_<component>_... SQL naming convention.
:no_primary_key: no true for a record that carries no key. See Keyless records below.
:has_tenant_id: no See Multi-tenancy below.
:has_workspace_id: no See Multi-tenancy below.

:subcomponent: is required, and :no_subcomponent: is its negation. Both belong in the * C++ ** Flags drawer, because that is where codegen reads the sub-component from. See Records with no sub-component below.

Structural view — the identity properties above, plus the Primary key, Natural key, and Column property sets documented further down this page:

entity_meta_model_keys_and_columns.png

(Source: entity_meta_model_keys_and_columns.puml, rendered with plantuml.)

* Flags
,:PROPERTIES:
,:schema: public
,:product: ores
,:component: refdata
,:has_tenant_id: true
,:END:

3. Keyless records

A key is required of a record that is stored, not of one that is only carried. :no_primary_key: true states which of the two this model is, so the loader asks a stored record for its key and lets a carried record have none.

State Result
No key flagged, no :no_primary_key: Refused — the loader reports a missing primary key.
:no_primary_key: true, no key flagged Accepted.
:no_primary_key: true, a key flagged Refused — the two contradict each other.

The flag is a statement, not a waiver. A model that says it has no key and then declares one fails, so the contradiction is caught at load time rather than producing a record whose identity nothing agrees on.

A keyless record is never fetched by identity on its own. It is a member of a larger payload, such as a byte span inside a history response.

4. Records with no sub-component

:subcomponent: names the directory between the component and the entity in a component that separates its headers by concern (api, domain, messages, …). Codegen reads it from the * C++ ** Flags drawer, and every domain-entity model states it there.

:no_subcomponent: true states that the component keeps its headers at its own include root instead, so a model in it has no sub-component to name. State the flag in the same * C++ ** Flags drawer, in place of :subcomponent:, because it says the same thing by negation. The loader then requires no :subcomponent: property.

A component whose headers sit one level up has no other use for the value: it feeds the include path and the class's qualified name, and both follow from the component alone.

5. Multi-tenancy: has_tenant_id=/=has_workspace_id

Setting :has_tenant_id: true tells the generator to emit a tenant_id uuid not null column on the table and a matching field on the domain struct, isolating the entity's rows by tenant; omit it only for genuinely global entities (currencies, countries). :has_workspace_id: does the same for workspace-scoped isolation.

Non-structural view — neither property is itself a field of the entity: authoring :has_tenant_id: true produces no field named has_tenant_id anywhere in the generated code. Rather, the generator reads it to decide whether to branch into the template logic that emits the tenant_id column and field. That the resulting column exists is a structural fact about the entity once the decision is made; the decision itself is a genuine, authored VMM feature, no different in kind from has_parent_id:

entity_meta_model_keys_and_columns.svg

:has_parent_id: is the third feature in this bundle. It gates the entity's hierarchy block: the recursive-CTE function, the repository and service get_hierarchy members, and the protocol and handler entries that reach them. Its narrative home is SQL § "Hierarchy: has_parent_id".

6. Primary key

The entity's surrogate key. In SQL this becomes part of the composite primary key (paired with tenant_id and the temporal range — see SQL); in C++ it becomes an sqlgen::PrimaryKey<T>-wrapped field on the repository entity (not on the domain struct — the domain struct exposes it as a plain field, the wrapping is a repository-layer concern).

Property Required Meaning
:column: yes SQL column name.
:type: yes SQL type (uuid, text, etc.).
:cpp_type: yes C++ type (boost::uuids::uuid, std::string, …).

Example of book's primary key:

* Primary key
,:PROPERTIES:
,:column:   id
,:type:     uuid
,:cpp_type: boost::uuids::uuid
,:END:

UUID uniquely identifying this book.

Generates, in the repository entity (book_entity.hpp):

struct book_entity {
    constexpr static const char* schema = "public";
    constexpr static const char* tablename = "ores_refdata_books_tbl";

    sqlgen::PrimaryKey<std::string> id;
    // ...
};

(book's primary key is actually text, not uuid — see Presentation's :has_uuid_primary_key: knob, which governs how the UI layer treats whichever type is declared here; the SQL/repository/domain layers just use whatever :type:=/:cpp_type:= says, uuid or not.)

7. Natural keys

A container for natural-key sub-headings — the human-meaningful column(s) that uniquely identify a record within a tenant (as opposed to the surrogate Primary key above, which exists purely for foreign-key stability). Each ** sub-heading is one natural key, using the same property shape as a column (see below), and may carry a generator babel block for synthetic test data.

8. Columns

A container for the entity's remaining column sub-headings. Each ** sub-heading is one column, its heading title the column name.

Property Required Meaning
:type: yes SQL type.
:cpp_type: yes C++ type.
:nullable: no true for nullable columns; default false.
:base64: no true for a column whose text database column holds the base64 spelling of a byte vector, while the domain member is the raw std::vector<std::uint8_t>.

Each column's body is split into a description (first paragraph — becomes the field's doc-comment) and an optional detail (second paragraph onward — extra context for a human reader, not emitted into generated code). A column may also carry a generator babel block: a C++ expression producing a synthetic value for that field, used by the C++ Generator facet.

8.1. Base64 columns: base64

A column that holds binary data stores it base64-encoded in a text column, because the schema keeps no binary column type. :base64: true states both halves of that: the entity member is the base64 std::string the column stores, and the domain member is the raw std::vector<std::uint8_t> the caller works with. The mapper performs the base64 hop in both directions.

The property is explicit rather than derived from :type:, because a text column that already holds text is indistinguishable from this one by type alone — only the model knows which spelling the bytes are in. It follows the C++ Repository conversion pattern a required timestamp already uses, with base64 in place of the datetime conversion.

The synthetic generator writes a non-empty byte sequence for such a column, since no faker expression applies to a byte vector.

** payload
,:PROPERTIES:
,:type:     text
,:cpp_type: std::vector<std::uint8_t>
,:base64:   true
,:nullable: false
,:END:

Raw image bytes, stored base64-encoded.

Generates, in the domain struct:

struct image {
    // ...
    std::vector<std::uint8_t> payload;
    // ...
};

And, in the sqlgen entity and mapper:

struct image_entity {
    // ...
    std::string payload;
    // ...
};

// map(const image_entity&): decode the column into the bytes.
r.payload = utility::convert::base64_converter::convert(v.payload);

// map(const domain::image&): encode the bytes into the column.
r.payload = utility::convert::base64_converter::convert(v.payload);

Example of book's name column:

** name
,:PROPERTIES:
,:type:     text
,:cpp_type: std::string
,:nullable: false
,:END:

Human-readable identifier for this book.

Unique per tenant; used as the natural key in the list window.

#+begin_src cpp :name generator
,std::string(faker::word::adjective()) + "_" + std::string(faker::word::noun())
#+end_src

Generates, in the domain struct (book.hpp):

namespace ores::refdata::domain {

struct book {
    // ...
    std::string name;
    // ...
};

}

And, in the SQL table DDL (refdata_books_create.sql):

create table if not exists "ores_refdata_books_tbl" (
    "id" uuid not null,
    "tenant_id" uuid not null,
    "version" integer not null,
    "party_id" uuid not null,
    "name" text not null,
    "description" text null,
    -- ...
);

9. See also

Emacs 29.3 (Org mode 9.6.15)