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 repository 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.
:sql_only: no true for a model whose store is the whole of it: only the SQL facets render. See Models the store is the whole of below.
:has_tenant_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. A model that states :sql_only: renders no C++ unit, so it is not asked for a sub-component at all.

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.

A keyless record that is stored is a whole table's shape, and the SQL facet reads the flag as exactly that: the table is append-only. The generated create emits the columns and the indexes the model declares, and nothing else — no primary key, no valid_from=/=valid_to, no key-derived checks, no insert trigger, no delete rule and no validation function, so a correction is a new row and the reader takes the latest. The indexes the facet would otherwise add (the id, version, tenant and natural-key indexes) do not render either, because none of them names a row that a key identifies. The drop script follows: it drops the table and its policies alone.

The flag removes the key-derived part of the table body, so a keyless table states no ** Checks section, no enforced foreign key and no pinned key today. A table that needs one has a key.

4. Models the store is the whole of

Most entities are modelled so that C++, TypeScript, the shell and the screens project from one model. A store that an ingest writes and a hand-written reader reads has none of those: it is a table, its indexes, its policies and the grant that lets the ingest write. :sql_only: true states that, and the resolver then renders the facets under ores.sql alone.

* Flags
,:PROPERTIES:
,:schema:         public
,:product:        ores
,:component:      marketdata
,:has_tenant_id:  true
,:no_primary_key: true
,:sql_only:       true
,:END:

The flag is a declaration of what the entity is, not a per-run filter. An :ores.*.enabled: override cannot re-admit a unit for it, for the same reason the other hard gates in resolve_targets do not: a domain type generated for a table no service mentions does not compile against anything, and a screen for it reads a store that has no read path. ores.marketdata.tick is the model that states it.

5. 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.

6. Multi-tenancy: has_tenant_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).

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".

7. 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 a plain field on both the domain struct and the repository entity. The repository entity's column list marks it as the key, and the generated statements match rows on it; the key is a repository-layer concern, so the domain struct does not carry the marker.

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), a member of the type the column has:

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

    boost::uuids::uuid 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.)

8. 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.

9. 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.

9.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 repository 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,
    -- ...
);

10. See also

Emacs 29.3 (Org mode 9.6.15)