Entity naming
Table of Contents
1. Summary
An entity has one name, and the layers spell it in a fixed set of ways. The
table is the plural with a _tbl suffix, the C++ type is the singular, and
every function, trigger, index, policy, message, subject, command and test file
derived from it follows a pattern stated here.
The patterns are not a convention someone chose for this page. They are what the code generator emits: 200 archetype pages and 153 tangled templates produce them, and this page is the index of that output. Where the generator does two things, this page says so rather than choosing.
Read Codegen patterns: choosing a model shape before you pick a shape, and this page before you name anything that comes out of it.
2. The base names
Everything below is built from three strings: {product}, {component} and
{plural}, usually joined as {product}_{component}_{plural}. A model may
override the table name with :tablename:, in which case that string is the
base instead.
2.1. The table
A table is "{{product}}_{{component}}_{{plural}}_tbl".
ores_reporting_report_instances_tbl ores_iam_run_grants_tbl ores_dq_datasets_tbl
_tbl is on every table and says nothing about the shape: a bi-temporal table
and a current-state table both take it.
2.2. The entity and its repository
The C++ type is the singular in snake case. The repository, the collection and the wire entity take the plural.
| Singular | Plural |
|---|---|
file_type |
file_types |
report_instance |
report_instances |
report_definition |
report_definitions |
2.3. Lookups
A lookup names the thing it classifies, and takes _kind or _type.
| Suffix | Example |
|---|---|
_kind |
report_instance_kind |
_type |
analytic_type, configuration_type, file_type |
Undecided. One job, two spellings. Nothing in the generator distinguishes them, so a new lookup may take either until the policy is settled.
2.4. Events
The _event suffix names a fact about an instant: a row written once and never
changed. Such an entity states :current_state: and :immutable:, so it has no
validity window and no audit tail. Because it has no audit tail, the actor and
the time are columns of its own.
ores.iam.auth_event is the worked example: it records who signed in, and
carries event_time, account_id and username for that reason.
3. What an entity derives
Every row below is emitted by the generator. sql_name_base is
{product}_{component}_{plural} and rls_table_base is the same string with
the leading {product}_{component}_ removed.
3.1. SQL
| Artefact | Pattern |
|---|---|
| Table | {sql_name_base}_tbl |
| Primary key | (tenant_id, {pk}, valid_from, valid_to), tenant first only when has_tenant_in_pk |
| Exclusion constraint | Unnamed exclude using gist on (tenant_id, {pk}, tstzrange(valid_from, valid_to)) |
| Check constraints | Unnamed: valid_from < valid_to, one per uuid PK column, one per text PK column |
| Foreign key | {sql_name_base}_{fk_column}_fk, overridable with :constraint_name: |
| Unique index, composite natural key | {index_name_prefix}_{keys}_uniq_idx |
| Unique index, single natural key | {index_name_prefix}_{column}_uniq_idx |
| Version unique index | {index_name_prefix}_version_uniq_idx |
| Id unique index | {index_name_prefix}_id_uniq_idx |
| Tenant index | {index_name_prefix}_tenant_idx |
| Model-declared index | {index_name_prefix}_{index.name}_idx |
| Insert function | {sql_name_base}_insert_fn() |
| Insert trigger | {sql_name_base}_insert_trg |
| Delete function | {sql_name_base}_delete_fn() |
| Delete trigger | {sql_name_base}_delete_trg |
| Delete rule | {sql_name_base}_delete_rule |
| Cascade-delete function | {sql_name_base}_cascade_delete_fn() |
| Immutable function | {sql_name_base}_immutable_fn() |
| Immutable trigger | {sql_name_base}_immutable_trg |
| Immutable truncate trigger | {sql_name_base}_immutable_truncate_trg |
| Touch-version function | {sql_name_base}_touch_version_fn() |
| Hierarchy function | {sql_name_base}_hierarchy_fn() |
| Notify function | {sql_name_base}_notify_fn() |
| Notify trigger | {sql_name_base}_notify_trg |
| pg_notify channel | '{sql_name_base}' |
| RLS tenant-isolation policy | {rls_table_base}_tbl_tenant_isolation_policy |
| RLS party-isolation policy | {rls_table_base}_tbl_party_isolation_policy |
| Validation function | {product}_{component}_validate_{singular}_fn() |
Note the RLS exception: rls_table_base deliberately drops the
{product}_{component}_ prefix, so an RLS policy is the one SQL family named off
the short base.
3.2. C++
| Artefact | Pattern |
|---|---|
| Domain type | struct {singular} final in ores::{component}::domain |
| Domain file | include/ores.{component_include}/domain/{singular}.hpp |
| Entity type | struct {singular}_entity in ores::{component}::repository |
| Entity file | repository/{singular}_entity.hpp |
| Table renderer | convert_to_table(const std::vector<{singular}>&) |
| Table IO | operator<<(std::ostream&, const std::vector<{singular}>&) |
| JSON IO | operator<<(std::ostream&, const {singular}&) |
| Mapper | class ORES_{COMPONENT_CORE_UPPER}_EXPORT {singular}_mapper |
| History field mapper | render_{singular}_fields(const domain::{singular}&) |
| Repository | class ORES_{COMPONENT_CORE_UPPER}_EXPORT {singular}_repository |
| Generator | generate_synthetic_{singular}() and generate_synthetic_{plural}(n, ...) |
| Service | class ORES_{COMPONENT_CORE_UPPER}_EXPORT {singular}_service |
| Protocol header | include/ores.{component_include}/messaging/{singular}_protocol.hpp |
| Event type | struct entity_event_traits<...::{singular}_event>, subject_prefix = "{component}.v1.{plural}_events" |
| Event registrar | register_{singular}_event_mapping(...) |
| Sub registrar | register_{singular}_handlers(...) |
| History provider registrar | register_{singular}_history_provider(...) |
| Event cache | class {singular}_cache |
| Event cache registrar | warm_and_subscribe_{singular}_cache(...) |
| HTTP route class | class {singular}_routes in ores::http::routes::{component} |
| Shell command class | class {singular}_commands |
| Operations command class | class {singular}_operations_commands |
A grouped entity also puts {group_prefix} and {audit_prefix} in front of the
member names. A grouped entity that omits :entity_singular_short: or
:entity_plural_short: renders an empty segment rather than falling back, so the
identifier silently collapses; declare both.
3.3. Protocol messages and subjects
| Artefact | Pattern |
|---|---|
| Key, write, change, removal, lookup | {singular}_key, {singular}_write, {singular}_change, {singular}_removal, {singular}_lookup |
| Filter | {plural}_filter |
| Event | {singular}_event |
| Version key, versions filter | {facet_stem}_version_key, {facet_stem}_versions_filter |
| List | list_{plural}_request / _response on {component}.v1.{plural}.list |
| Get | get_{singular}_request / _response on {component}.v1.{plural}.get |
| Get many | get_many_{plural}_request / _response on {component}.v1.{plural}.get_many |
| Put | put_{singular}_request / _response on {component}.v1.{plural}.put |
| Put many | put_many_{plural}_request / _response on {component}.v1.{plural}.put_many |
| Delete | delete_{singular}_request / _response on {component}.v1.{plural}.delete |
| Delete many | delete_many_{plural}_request / _response on {component}.v1.{plural}.delete_many |
| Scoped list | list_by_{relation}_{plural}_request / _response on {component}.v1.{plural}.list_by_{relation} |
| Versions | list_{facet_stem}_versions_request / _response on {component}.v1.{plural}_versions.list |
| Single version | get_{facet_stem}_version_request / _response on {component}.v1.{plural}_versions.get |
| Event subjects | namespace {singular}_event_subjects with created, updated, deleted |
The subject grammar is four segments for a request,
{component}.v1.{plural}.{verb}, and five for an event,
{component}.v1.events.{plural}.{created,updated,deleted}. See
NATS entity protocol specification
for the grammar itself.
3.4. Shell, HTTP, TypeScript, docs and tests
| Artefact | Pattern |
|---|---|
| Shell verbs | list, get, get-many, add, set, put-many, delete, delete-many, versions, version, by-{relation} |
| Shell menu | {plural} |
| Shell recipe script | projects/ores.shell/scripts/library/{menu}/{menu}-{command}.ores |
| Shell recipe doc | doc/recipes/shell/{menu}/{singular}.org |
| HTTP route paths | /api/v1/{component}/{plural} with /{{{key}}}, /versions, /versions/{version}, /get-many, /put-many, /delete-many, /by-{relation}/{relation} |
| HTTP recipe doc | doc/recipes/http/{plural}/{singular}.org |
| TypeScript domain interface | export interface {Pascal} in .../generated/{component}/domain/{singular}.ts |
| TypeScript protocol interfaces | export interface {Pascal(message_name)} in .../protocol/{singular}_protocol.ts |
| TypeScript maps | subjects, requiresSession, eventSubjects |
| Entity model file | projects/ores.{component}/modeling/ores.{component}.{entity}.org |
| CMake source list | projects/{component_dir}/src/component_files.cmake |
| CMake test list | projects/{component_dir}/tests/component_files.cmake |
| Eventing integration test | tests/{singular}_eventing_integration_tests.cpp |
| Shell command test | projects/ores.shell/{component}/tests/{singular}_commands_tests.cpp |
| HTTP route test | projects/ores.http/{component}/tests/{singular}_routes_tests.cpp |
| ER diagram | projects/ores.sql/modeling/ores_schema.{component}.puml |
Two dead or misleading names are worth knowing. The archetype
ores.plantuml.er.diagram emits output/{entity}_er.puml, and no code
references it; the live diagrams are the group .puml files above. And there is
no generated domain_{entity}_tests.cpp: the file exists for report_instance
but is hand-written.
3.5. Junctions
A junction replaces {plural} with the junction's {name}.
| Artefact | Pattern |
|---|---|
| Table | {product}_{component}_{name}_tbl |
| Functions and triggers | {product}_{component}_{name}_insert_fn, _insert_trg, _delete_rule |
| Indexes | {name}_{column}_idx, {name}_uniq_idx, {name}_tenant_idx |
| RLS policies | {name}_tbl_tenant_isolation_policy, {name}_tbl_party_isolation_policy |
| Notify | {events_channel}_notify_fn, _notify_trg, _notify_close_trg |
| Messages | get_{name_singular}_request and the rest, on {component}.v1.{name}.{verb} |
| History row | struct {name_singular}_history_row |
4. Where an entity's name is decided
The generator is the authority, not this page. Each name above comes from one archetype page and its tangled template.
- Codegen input org-file schema reference — the syntax of a model, and the flags that change which names are emitted at all.
- Codegen patterns: choosing a model shape — which shape to pick, and the neighbouring shape it is mistaken for.
- Adding a flagged combo field to a codegen entity — a worked change to a model, and the names it moves.
- ores.codegen — the generator itself, its facets and its gates.
5. Where the code does not agree
Two append-only logs record a fact about an instant and do not take _event.
| Entity | What it records | Name under this page |
|---|---|---|
ores.iam.run_token_issue |
one run grant exchange, issued or refused | run_token_issue_event |
ores.dq.publication |
one dataset publication | publication_event |
ores.dq.publication also states its table name rather than letting the
generator derive it, because the entity is a publication and the table is
ores_dq_dataset_publications_tbl. See the capture
Ensure all entities follow the entity
naming convention for the review this opens.
6. See also
- Entities — the hub this page belongs to.
- Entity Lifecycle — the layer order and the per-transport patterns of a full-stack entity.
- PostgreSQL Architecture — the bi-temporal design the SQL names assume.
- Zettelkasten — why every page carries an id and links by it rather than by path.