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":
- Structural fields — always part of the entity's shape — drawn as
a UML class diagram under Flags below: the identity properties
(
:schema:,:product:,:component:) and the Primary key, Natural key, and Column property sets. - Non-structural — two authored, optional VMM features — drawn as a
UVL feature diagram under Multi-tenancy below:
:has_tenant_id:and:has_workspace_id:.
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:
(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:
: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
- Codegen org-entity meta-model — the hub.
- SQL — how these columns become the table's DDL, temporal constraints, and triggers.
- Variability — the structural/non-structural distinction referenced above.
- C++ Domain — how they become the plain domain struct.
- C++ Repository — how they become the sqlgen-mapped entity and mapper.