ores.sql.schema.domain_entity_create

Table of Contents

The original bi-temporal create, driven by the domain_entity model ({{#domain_entity.columns}}, natural keys, primary key). Carries the richest validation machinery: book/cross-check error fragments, {{#declare_vars}} function-local declarations, {{#coalesce}} / {{#copy_empty}} column handling. sql profile; same output path as the unified variant — the two coexist while models migrate formats. PostgreSQL DDL for the bi-temporal table: natural keys, temporal columns (`valid_from` / `valid_to`), audit trail (`modified_by`, `performed_by`, `recorded_at`), optimistic-lock version, and the matching history table.

See the Template variable reference for the complete list of available variables and their semantics. See SQL naming conventions for the official object-name patterns all SQL templates follow.

Template

The full template source. Edit here and re-tangle with compass build --direct tangle_codegen_templates to regenerate library/templates/sql_schema_domain_entity_create.mustache.

{{! GENERATED FILE — tangled from projects/ores.codegen/library/templates/sql_schema.org. Edit the org source. }}
{{! Template to generate SQL schema for domain entity table with UUID primary key }}
{{{sql_license}}}
/**
 * AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
 * Template: sql_schema_domain_entity_create.mustache
 * To modify, update the template and regenerate.
 *
 * {{domain_entity.entity_title}} Table
 *
 * {{{domain_entity.description_formatted}}}
 */

create table if not exists "{{domain_entity.sql_name_base}}_tbl" (
{{#domain_entity.primary_key.columns}}
    "{{column}}" {{type}} not null,
{{/domain_entity.primary_key.columns}}
{{#domain_entity.has_tenant_id}}
{{#domain_entity.sql.system_scope}}
    "tenant_id" uuid not null default ores_utility_system_tenant_id_fn(),
{{/domain_entity.sql.system_scope}}
{{#domain_entity.sql.nullable_tenant_id}}
    "tenant_id" uuid,
{{/domain_entity.sql.nullable_tenant_id}}
{{^domain_entity.sql.system_scope}}
{{^domain_entity.sql.nullable_tenant_id}}
    "tenant_id" uuid not null,
{{/domain_entity.sql.nullable_tenant_id}}
{{/domain_entity.sql.system_scope}}
{{/domain_entity.has_tenant_id}}
{{#domain_entity.has_audit_columns}}
    "version" integer not null,
{{/domain_entity.has_audit_columns}}
{{#domain_entity.natural_keys}}
{{#nullable}}
    "{{column}}" {{type}} null,
{{/nullable}}
{{^nullable}}
    "{{column}}" {{type}} not null,
{{/nullable}}
{{/domain_entity.natural_keys}}
{{#domain_entity.columns}}
{{^is_image_id}}
    "{{name}}" {{type}}{{#nullable}} null{{/nullable}}{{^nullable}} not null{{/nullable}}{{#default}} default {{{default}}}{{/default}},
{{/is_image_id}}
{{/domain_entity.columns}}
{{#domain_entity.has_coding_scheme}}
    "coding_scheme_code" text not null,
{{/domain_entity.has_coding_scheme}}
{{#domain_entity.has_nullable_coding_scheme}}
    "coding_scheme_code" text,
{{/domain_entity.has_nullable_coding_scheme}}
{{#domain_entity.has_image_id}}
    "image_id" uuid,
{{/domain_entity.has_image_id}}
{{#domain_entity.has_workspace_id}}
    "workspace_id" uuid not null default ores_utility_live_workspace_id_fn(), -- soft FK to ores_workspaces_tbl(id)
{{/domain_entity.has_workspace_id}}
{{#domain_entity.has_audit_columns}}
    "modified_by" text not null,
    "performed_by" text not null,
    "change_reason_code" text not null,
    "change_commentary" text not null,
{{/domain_entity.has_audit_columns}}
    "valid_from" timestamp with time zone not null,
    "valid_to" timestamp with time zone not null,
{{#domain_entity.has_tenant_in_pk}}
    primary key (tenant_id, {{domain_entity.primary_key.column_list}}, valid_from, valid_to),
{{#domain_entity.has_gist_exclusion}}
    exclude using gist (
        tenant_id WITH =,
        {{{domain_entity.primary_key.gist_column_clause}}}
        tstzrange(valid_from, valid_to) WITH &&
    ),
{{/domain_entity.has_gist_exclusion}}
{{/domain_entity.has_tenant_in_pk}}
{{^domain_entity.has_tenant_in_pk}}
{{#domain_entity.sql.hypertable}}
    primary key ({{domain_entity.primary_key.column_list}}, {{domain_entity.sql.hypertable_partition_column}}),
{{/domain_entity.sql.hypertable}}
{{^domain_entity.sql.hypertable}}
    primary key ({{domain_entity.primary_key.column_list}}, valid_from, valid_to),
{{#domain_entity.has_gist_exclusion}}
    exclude using gist (
        {{{domain_entity.primary_key.gist_column_clause}}}
        tstzrange(valid_from, valid_to) WITH &&
    ),
{{/domain_entity.has_gist_exclusion}}
{{/domain_entity.sql.hypertable}}
{{/domain_entity.has_tenant_in_pk}}
    check ("valid_from" < "valid_to"){{#domain_entity.primary_key.columns}}{{#is_uuid}}{{^skip_uuid_check}},
    check ("{{column}}" <> {{uuid_check_fn}}){{/skip_uuid_check}}{{/is_uuid}}{{#is_text}},
    check ("{{column}}" <> ''){{/is_text}}{{/domain_entity.primary_key.columns}}{{#domain_entity.sql.extra_checks}},
    check ({{{.}}}){{/domain_entity.sql.extra_checks}}
);
{{#domain_entity.has_multiple_natural_keys}}
{{^domain_entity.sql.suppress_nk_index}}

-- Composite natural key: unique combination for active records
{{#domain_entity.has_tenant_id}}
create unique index if not exists {{domain_entity.index_name_prefix}}_{{domain_entity.natural_keys_composite_name}}_uniq_idx
on "{{domain_entity.sql_name_base}}_tbl" (tenant_id, {{domain_entity.natural_keys_composite_columns}})
where valid_to = ores_utility_infinity_timestamp_fn();
{{/domain_entity.has_tenant_id}}
{{^domain_entity.has_tenant_id}}
create unique index if not exists {{domain_entity.index_name_prefix}}_{{domain_entity.natural_keys_composite_name}}_uniq_idx
on "{{domain_entity.sql_name_base}}_tbl" ({{domain_entity.natural_keys_composite_columns}})
where valid_to = ores_utility_infinity_timestamp_fn();
{{/domain_entity.has_tenant_id}}
{{/domain_entity.sql.suppress_nk_index}}
{{/domain_entity.has_multiple_natural_keys}}
{{^domain_entity.has_multiple_natural_keys}}
{{#domain_entity.natural_keys}}

-- Unique {{column}} for active records
{{#domain_entity.has_tenant_in_pk}}
create unique index if not exists {{domain_entity.index_name_prefix}}_{{column}}_uniq_idx
on "{{domain_entity.sql_name_base}}_tbl" (tenant_id{{#prefix_columns}}, {{prefix_columns}}{{/prefix_columns}}, {{column}})
where valid_to = ores_utility_infinity_timestamp_fn();
{{/domain_entity.has_tenant_in_pk}}
{{^domain_entity.has_tenant_in_pk}}
create unique index if not exists {{domain_entity.index_name_prefix}}_{{column}}_uniq_idx
on "{{domain_entity.sql_name_base}}_tbl" ({{column}})
where valid_to = ores_utility_infinity_timestamp_fn();
{{/domain_entity.has_tenant_in_pk}}
{{/domain_entity.natural_keys}}
{{/domain_entity.has_multiple_natural_keys}}

{{#domain_entity.has_audit_columns}}
-- Version uniqueness for optimistic concurrency
{{/domain_entity.has_audit_columns}}
{{#domain_entity.has_tenant_in_pk}}
{{#domain_entity.has_audit_columns}}
create unique index if not exists {{domain_entity.index_name_prefix}}_version_uniq_idx
on "{{domain_entity.sql_name_base}}_tbl" (tenant_id, {{domain_entity.primary_key.column_list}}, version)
where valid_to = ores_utility_infinity_timestamp_fn();
{{/domain_entity.has_audit_columns}}

{{#domain_entity.primary_key.is_uuid}}
create unique index if not exists {{domain_entity.index_name_prefix}}_id_uniq_idx
on "{{domain_entity.sql_name_base}}_tbl" (tenant_id, {{domain_entity.primary_key.column_list}})
where valid_to = ores_utility_infinity_timestamp_fn();
{{/domain_entity.primary_key.is_uuid}}
{{^domain_entity.primary_key.is_uuid}}
create unique index if not exists {{domain_entity.index_name_prefix}}_{{domain_entity.primary_key.index_suffix}}_uniq_idx
on "{{domain_entity.sql_name_base}}_tbl" (tenant_id, {{domain_entity.primary_key.column_list}})
where valid_to = ores_utility_infinity_timestamp_fn();
{{/domain_entity.primary_key.is_uuid}}

create index if not exists {{domain_entity.index_name_prefix}}_tenant_idx
on "{{domain_entity.sql_name_base}}_tbl" (tenant_id)
where valid_to = ores_utility_infinity_timestamp_fn();
{{/domain_entity.has_tenant_in_pk}}
{{^domain_entity.has_tenant_in_pk}}
{{#domain_entity.has_audit_columns}}
create unique index if not exists {{domain_entity.index_name_prefix}}_version_uniq_idx
on "{{domain_entity.sql_name_base}}_tbl" ({{domain_entity.primary_key.column_list}}, version)
where valid_to = ores_utility_infinity_timestamp_fn();
{{/domain_entity.has_audit_columns}}

{{#domain_entity.sql.nullable_tenant_id}}
create unique index if not exists {{domain_entity.index_name_prefix}}_id_uniq_idx
on "{{domain_entity.sql_name_base}}_tbl" ({{domain_entity.primary_key.column_list}})
where valid_to = ores_utility_infinity_timestamp_fn();

create index if not exists {{domain_entity.index_name_prefix}}_tenant_idx
on "{{domain_entity.sql_name_base}}_tbl" (tenant_id)
where valid_to = ores_utility_infinity_timestamp_fn();
{{/domain_entity.sql.nullable_tenant_id}}
{{/domain_entity.has_tenant_in_pk}}
{{#domain_entity.indexes}}

create{{#unique}} unique{{/unique}} index if not exists {{domain_entity.index_name_prefix}}_{{name}}_idx
on "{{domain_entity.sql_name_base}}_tbl" ({{{columns}}}){{#current_only}}
where valid_to = ores_utility_infinity_timestamp_fn(){{#where_extra}}
  and {{{where_extra}}}{{/where_extra}}{{/current_only}}{{^current_only}}{{#where_extra}}
where {{{where_extra}}}{{/where_extra}}{{/current_only}};
{{/domain_entity.indexes}}
{{#domain_entity.has_workspace_id}}

create index if not exists {{domain_entity.index_name_prefix}}_workspace_idx
on "{{domain_entity.sql_name_base}}_tbl" (workspace_id)
where valid_to = ores_utility_infinity_timestamp_fn();
{{/domain_entity.has_workspace_id}}

{{#domain_entity.has_tenant_id}}
{{#domain_entity.generate_touch_function}}

-- =============================================================================
-- Touch-version function for {{domain_entity.entity_singular}}
-- Bumps this entity's version without changing any of its own columns,
-- called by a child entity's insert trigger / delete rule when that
-- child is flagged :bump_parent_version: true against this entity. See
-- the "Temporal composite entity versioning" architecture doc.
--
-- Deliberately does NOT duplicate the version/valid_from/valid_to
-- management here: it fetches the current row, resets version to the
-- 0 sentinel, and re-inserts — the table's own insert trigger (below)
-- then performs the exact same close-current/bump-version dance a
-- normal application save does. This also sidesteps a real footgun:
-- current_timestamp is frozen for the whole transaction, so a
-- same-transaction bulk import (parent + child created together, as
-- GLEIF provisioning does) would otherwise make the just-inserted
-- parent row's own valid_from collide with this function's close
-- timestamp.
--
-- Defined before the insert trigger/delete rule below (not after):
-- for a self-referencing entity (this entity is its own composite
-- parent, e.g. a portfolio tree), the delete rule calling this
-- function lives in the *same* file. PostgreSQL parses CREATE RULE
-- eagerly, so the function must already exist by then -- unlike a
-- PL/pgSQL function body (e.g. the insert trigger function), which is
-- opaque at CREATE time and only resolves calls at execution.
--
-- p_reason_code is passed through as-is (already a validated code from
-- the child's own row); p_child_entity distinguishes which child
-- triggered the bump in the free-text commentary.
-- =============================================================================
create or replace function {{domain_entity.sql_name_base}}_touch_version_fn(
    p_tenant_id uuid,
    p_id {{domain_entity.primary_key.type}},
    p_reason_code text,
    p_commentary text,
    p_modified_by text,
    p_performed_by text,
    p_child_entity text
) returns void as $$
declare
    rec {{domain_entity.sql_name_base}}_tbl%rowtype;
begin
    -- for update: takes the same row lock the parent's own insert
    -- trigger takes, so the snapshot in rec can't be based on a
    -- business-column value a concurrent direct edit is about to
    -- change — without this, that concurrent edit could be silently
    -- reverted once this function's later insert proceeds. See the
    -- "Temporal composite entity versioning" architecture doc,
    -- Concurrency section.
    select * into rec
    from "{{domain_entity.sql_name_base}}_tbl"
    where tenant_id = p_tenant_id
      and {{domain_entity.primary_key.column}} = p_id
      and valid_to = ores_utility_infinity_timestamp_fn()
    for update;

    if not found then
        return;
    end if;

    rec.version := 0;
    rec.modified_by := p_modified_by;
    rec.performed_by := p_performed_by;
    rec.change_reason_code := p_reason_code;
    rec.change_commentary := format('Bumped by child %s: %s', p_child_entity, coalesce(p_commentary, ''));

    insert into "{{domain_entity.sql_name_base}}_tbl"
    select (rec).*;
end;
$$ language plpgsql security definer set search_path = public, pg_temp;

{{/domain_entity.generate_touch_function}}
{{/domain_entity.has_tenant_id}}
{{#domain_entity.sql.bitemporal_soft_update}}
create or replace function {{domain_entity.sql_name_base}}_insert_fn()
returns trigger as $$
begin
    new.tenant_id := ores_iam_validate_tenant_fn(new.tenant_id);

    update "{{domain_entity.sql_name_base}}_tbl"
    set valid_to = current_timestamp
    where tenant_id = new.tenant_id
{{#domain_entity.sql.bitemporal_natural_keys}}
{{#is_nullable}}
      and coalesce({{column}}, '') = coalesce(new.{{column}}, '')
{{/is_nullable}}
{{^is_nullable}}
      and {{column}} = new.{{column}}
{{/is_nullable}}
{{/domain_entity.sql.bitemporal_natural_keys}}
      and valid_to = ores_utility_infinity_timestamp_fn()
      and valid_from < current_timestamp;

    new.valid_from := current_timestamp;
    new.valid_to   := ores_utility_infinity_timestamp_fn();
    return new;
end;
$$ language plpgsql security definer set search_path = public, pg_temp;

create or replace trigger {{domain_entity.sql_name_base}}_insert_trg
before insert on "{{domain_entity.sql_name_base}}_tbl"
for each row execute function {{domain_entity.sql_name_base}}_insert_fn();

create or replace function {{domain_entity.sql_name_base}}_delete_fn()
returns trigger as $$
begin
    update "{{domain_entity.sql_name_base}}_tbl"
    set valid_to = current_timestamp
    where tenant_id = old.tenant_id
{{#domain_entity.sql.bitemporal_natural_keys}}
{{#is_nullable}}
      and coalesce({{column}}, '') = coalesce(old.{{column}}, '')
{{/is_nullable}}
{{^is_nullable}}
      and {{column}} = old.{{column}}
{{/is_nullable}}
{{/domain_entity.sql.bitemporal_natural_keys}}
      and valid_to = ores_utility_infinity_timestamp_fn();
    return null;
end;
$$ language plpgsql security definer set search_path = public, pg_temp;

create or replace trigger {{domain_entity.sql_name_base}}_delete_trg
before delete on "{{domain_entity.sql_name_base}}_tbl"
for each row execute function {{domain_entity.sql_name_base}}_delete_fn();
{{/domain_entity.sql.bitemporal_soft_update}}
{{^domain_entity.sql.bitemporal_soft_update}}
create or replace function {{domain_entity.sql_name_base}}_insert_fn()
returns trigger as $$
declare
{{#domain_entity.has_audit_columns}}
    current_version integer;
{{/domain_entity.has_audit_columns}}
{{#domain_entity.sql.party_id_from_book_id}}
    v_book_portfolio_id uuid;
{{/domain_entity.sql.party_id_from_book_id}}
{{#domain_entity.sql.fk_copy_validations}}
{{#declare_vars}}
    {{name}} {{type}};
{{/declare_vars}}
{{/domain_entity.sql.fk_copy_validations}}
{{#domain_entity.sql.has_cardinality_limit_validations}}
    v_max_cardinality integer;
    v_current_count integer;
{{/domain_entity.sql.has_cardinality_limit_validations}}
begin
{{#domain_entity.has_tenant_in_pk}}
    -- Validate tenant_id
    NEW.tenant_id := ores_iam_validate_tenant_fn(NEW.tenant_id);

{{/domain_entity.has_tenant_in_pk}}
{{#domain_entity.has_workspace_id}}
    -- Validate workspace_id
    NEW.workspace_id := ores_workspace_validate_fn(NEW.workspace_id);

{{/domain_entity.has_workspace_id}}
{{#domain_entity.sql.nullable_tenant_id}}
    -- Validate tenant_id only when set (system records have NULL tenant_id)
    if NEW.tenant_id is not null then
        NEW.tenant_id := ores_iam_validate_tenant_fn(NEW.tenant_id);
    end if;

{{/domain_entity.sql.nullable_tenant_id}}
{{#domain_entity.sql.system_scope}}
    -- All {{domain_entity.entity_plural}} belong to the system tenant
    NEW.tenant_id := ores_utility_system_tenant_id_fn();

{{/domain_entity.sql.system_scope}}
{{#domain_entity.has_any_coding_scheme}}
    -- Validate foreign key references
    if NEW.coding_scheme_code is not null and not exists (
        select 1 from ores_dq_coding_schemes_tbl
        where code = NEW.coding_scheme_code
        and valid_to = ores_utility_infinity_timestamp_fn()
    ) then
        raise exception 'Invalid coding_scheme_code: %. Coding scheme must exist.', NEW.coding_scheme_code
        using errcode = '23503';
    end if;

{{/domain_entity.has_any_coding_scheme}}
{{#domain_entity.sql.party_id_from_session}}
    -- Set party_id from session context
    NEW.party_id := current_setting('app.current_party_id')::uuid;

{{/domain_entity.sql.party_id_from_session}}
{{#domain_entity.sql.party_id_from_book_id}}
    -- Validate book_id (soft FK to {{book_table}})
    if not exists (
        select 1 from {{book_table}}
        where tenant_id = NEW.tenant_id
          and id = NEW.book_id
          and valid_to = ores_utility_infinity_timestamp_fn()
    ) then
        raise exception '{{book_error_message}}', NEW.book_id
            using errcode = '23503';
    end if;

    -- Denormalise party_id and capture parent_portfolio_id from book
    select party_id, parent_portfolio_id
      into NEW.party_id, v_book_portfolio_id
    from {{book_table}}
    where id = NEW.book_id
      and tenant_id = NEW.tenant_id
      and valid_to = ores_utility_infinity_timestamp_fn();

    -- Validate portfolio_id (soft FK to {{portfolio_table}})
    if not exists (
        select 1 from {{portfolio_table}}
        where tenant_id = NEW.tenant_id
          and id = NEW.portfolio_id
          and valid_to = ores_utility_infinity_timestamp_fn()
    ) then
        raise exception '{{portfolio_error_message}}', NEW.portfolio_id
            using errcode = '23503';
    end if;

    -- Validate that portfolio_id matches the book's parent portfolio
    if NEW.portfolio_id != v_book_portfolio_id then
        raise exception '{{cross_check_error_message}}',
            NEW.portfolio_id, v_book_portfolio_id
            using errcode = '23514';
    end if;

{{/domain_entity.sql.party_id_from_book_id}}
{{#domain_entity.foreign_keys}}
{{^skip_check}}
{{^nullable}}
    -- Validate {{column}} (soft FK to {{table}})
    if not exists (
        select 1 from {{table}}
        where {{^use_no_tenant}}{{^use_system_tenant}}tenant_id = NEW.tenant_id
          and {{/use_system_tenant}}{{#use_system_tenant}}tenant_id = ores_utility_system_tenant_id_fn()
          and {{/use_system_tenant}}{{/use_no_tenant}}{{target_column}} = NEW.{{column}}
          and valid_to = ores_utility_infinity_timestamp_fn()
    ) then
        raise exception '{{error_message}}', NEW.{{column}}
            using errcode = '23503';
    end if;

{{/nullable}}
{{#nullable}}
    -- Validate {{column}} (optional soft FK to {{table}})
    if NEW.{{column}} is not null then
        if not exists (
            select 1 from {{table}}
            where {{^use_no_tenant}}{{^use_system_tenant}}tenant_id = NEW.tenant_id
              and {{/use_system_tenant}}{{#use_system_tenant}}tenant_id = ores_utility_system_tenant_id_fn()
              and {{/use_system_tenant}}{{/use_no_tenant}}{{target_column}} = NEW.{{column}}
              and valid_to = ores_utility_infinity_timestamp_fn()
        ) then
            raise exception '{{error_message}}', NEW.{{column}}
                using errcode = '23503';
        end if;
{{#prevent_cycle}}

        -- Reject a {{column}} pointing back at NEW's own row, directly or
        -- transitively, since the touch-version mechanism re-fires this
        -- same trigger walking up {{column}} -- an undetected cycle would
        -- recurse without bound instead of failing cleanly.
        if exists (
            with recursive ancestor_chain as (
                select id, {{column}} as parent_id
                from {{table}}
                where id = NEW.{{column}}
                  and valid_to = ores_utility_infinity_timestamp_fn()
                union all
                select t.id, t.{{column}} as parent_id
                from {{table}} t
                join ancestor_chain a on t.id = a.parent_id
                where t.valid_to = ores_utility_infinity_timestamp_fn()
            )
            select 1 from ancestor_chain where id = NEW.{{domain_entity.primary_key.column}}
        ) then
            raise exception 'Invalid {{column}}: % would create a cycle in the {{table}} hierarchy.', NEW.{{column}}
                using errcode = '23514';
        end if;
{{/prevent_cycle}}
    end if;

{{/nullable}}
{{/skip_check}}
{{/domain_entity.foreign_keys}}
{{#domain_entity.sql.fk_copy_validations}}
    -- Validate {{column}}; copy {{select_fields}} from {{table}} if not provided
    select {{select_fields}}
      into {{into_vars}}
    from {{table}}
    where tenant_id = NEW.tenant_id
      and id = NEW.{{column}}
      and valid_to = ores_utility_infinity_timestamp_fn();

    if not found then
        raise exception '{{error_message}}',
            NEW.{{column}}
            using errcode = '23503';
    end if;

{{#copy_empty}}
    if NEW.{{target}} = '' then
{{#coalesce}}
        NEW.{{target}} := coalesce({{source}}, '');
{{/coalesce}}
{{^coalesce}}
        NEW.{{target}} := {{source}};
{{/coalesce}}
    end if;
{{/copy_empty}}
{{/domain_entity.sql.fk_copy_validations}}
{{#domain_entity.sql.text_code_validations}}
    -- Validate {{column}} FK
    if not exists (
        select 1 from {{table}}
        where {{lookup_column}} = NEW.{{column}}
          and valid_to = ores_utility_infinity_timestamp_fn()
    ) then
        raise exception '{{error_message}}',
            NEW.{{column}} using errcode = '23503';
    end if;

{{/domain_entity.sql.text_code_validations}}
{{#domain_entity.insert_trigger.validations}}
{{#nullable}}
    -- Validate {{column}} (optional field -- skip validation when null)
    if NEW.{{column}} is not null then
        NEW.{{column}} := {{validation_function}}({{#domain_entity.has_tenant_id}}NEW.tenant_id, {{/domain_entity.has_tenant_id}}NEW.{{column}});
    end if;

{{/nullable}}
{{^nullable}}
    -- Validate {{column}}
    NEW.{{column}} := {{validation_function}}({{#domain_entity.has_tenant_id}}NEW.tenant_id, {{/domain_entity.has_tenant_id}}NEW.{{column}});

{{/nullable}}
{{#cardinality_limit_table}}
    -- Validate cardinality limit for this {{column}}
    select {{cardinality_limit_column}} into v_max_cardinality
    from {{cardinality_limit_table}}
    where {{#domain_entity.has_tenant_id}}tenant_id = NEW.tenant_id
      and {{/domain_entity.has_tenant_id}}code = NEW.{{column}}
      and valid_to = ores_utility_infinity_timestamp_fn();

    if v_max_cardinality is not null then
        select count(*) into v_current_count
        from "{{domain_entity.sql_name_base}}_tbl"
        where {{#domain_entity.has_tenant_id}}tenant_id = NEW.tenant_id
          and {{/domain_entity.has_tenant_id}}{{cardinality_scope_column}} = NEW.{{cardinality_scope_column}}
          and {{column}} = NEW.{{column}}
          and {{domain_entity.primary_key.column}} != NEW.{{domain_entity.primary_key.column}}
          and valid_to = ores_utility_infinity_timestamp_fn();

        if v_current_count >= v_max_cardinality then
            raise exception 'Cardinality violation for {{column}} %: % already has % {{cardinality_noun}}(s) (max %).',
                NEW.{{column}}, NEW.{{cardinality_scope_column}}, v_current_count, v_max_cardinality
                using errcode = '23514';
        end if;
    end if;

{{/cardinality_limit_table}}
{{/domain_entity.insert_trigger.validations}}
<<paste:d4e8e136-828d-440a-ba59-82ba2b9d3e2e>>
{{#domain_entity.has_audit_columns}}
{{^domain_entity.change_reason_code_declared}}
{{#domain_entity.has_tenant_in_pk}}
    -- Validate change_reason_code
    NEW.change_reason_code := ores_dq_validate_change_reason_fn(NEW.tenant_id, NEW.change_reason_code);

{{/domain_entity.has_tenant_in_pk}}
{{#domain_entity.sql.nullable_tenant_id}}
    -- Validate change_reason_code
    NEW.change_reason_code := ores_dq_validate_change_reason_fn(NEW.tenant_id, NEW.change_reason_code);

{{/domain_entity.sql.nullable_tenant_id}}
{{#domain_entity.sql.system_scope}}
    -- Validate change_reason_code (use system tenant for {{domain_entity.entity_plural}} records)
    NEW.change_reason_code := ores_dq_validate_change_reason_fn(ores_utility_system_tenant_id_fn(), NEW.change_reason_code);

{{/domain_entity.sql.system_scope}}
{{^domain_entity.has_tenant_id}}
    -- Validate change_reason_code
    NEW.change_reason_code := ores_dq_validate_change_reason_fn(NEW.change_reason_code);

{{/domain_entity.has_tenant_id}}
{{/domain_entity.change_reason_code_declared}}
{{/domain_entity.has_audit_columns}}
{{#domain_entity.foreign_keys}}
{{#bump_parent_version}}
    -- Bump {{table}}'s version alongside this write (composite entity
    -- versioning — see the "Temporal composite entity versioning"
    -- architecture doc). The touch function re-validates modified_by
    -- itself; change_reason_code is passed through as-is since it was
    -- already validated above.
    perform {{touch_function}}(
        NEW.tenant_id,
        NEW.{{column}},
        NEW.change_reason_code,
        NEW.change_commentary,
        NEW.modified_by,
        NEW.performed_by,
        '{{domain_entity.entity_singular}}'
    );

{{/bump_parent_version}}
{{/domain_entity.foreign_keys}}
{{#domain_entity.has_audit_columns}}
    -- Version management
    select version into current_version
    from "{{domain_entity.sql_name_base}}_tbl"
    where {{#domain_entity.has_tenant_in_pk}}tenant_id = NEW.tenant_id
      and {{/domain_entity.has_tenant_in_pk}}{{#domain_entity.sql.nullable_tenant_id}}(tenant_id = NEW.tenant_id or (tenant_id is null and NEW.tenant_id is null))
      and {{/domain_entity.sql.nullable_tenant_id}}{{{domain_entity.primary_key.where_new}}}
      and valid_to = ores_utility_infinity_timestamp_fn()
    for update;

    if found then
        if NEW.version != 0 and NEW.version != current_version then
            raise exception 'Version conflict: expected version %, but current version is %',
                NEW.version, current_version
                using errcode = 'P0002';
        end if;
        NEW.version = current_version + 1;
<<paste:59F4313F-B119-44D8-8E4E-05AE8B18DE95>>
        -- clock_timestamp(), not current_timestamp: current_timestamp is
        -- frozen for the whole transaction, so a same-transaction
        -- multi-write to this row (e.g. a composite entity's parent
        -- touched twice by two different children in one transaction)
        -- would collide with itself. clock_timestamp() always advances.
        update "{{domain_entity.sql_name_base}}_tbl"
        set valid_to = clock_timestamp()
        where {{#domain_entity.has_tenant_in_pk}}tenant_id = NEW.tenant_id
          and {{/domain_entity.has_tenant_in_pk}}{{#domain_entity.sql.nullable_tenant_id}}(tenant_id = NEW.tenant_id or (tenant_id is null and NEW.tenant_id is null))
          and {{/domain_entity.sql.nullable_tenant_id}}{{{domain_entity.primary_key.where_new}}}
          and valid_to = ores_utility_infinity_timestamp_fn()
          and valid_from < clock_timestamp();
    else
        NEW.version = 1;
<<paste:8697C751-ECD5-4501-AF06-77AD9A8985AB>>
    end if;
{{/domain_entity.has_audit_columns}}
{{^domain_entity.has_audit_columns}}
    -- Close the current active row before inserting the new one
    update "{{domain_entity.sql_name_base}}_tbl"
    set valid_to = clock_timestamp()
    where {{{domain_entity.primary_key.where_new}}}
      and valid_to = ores_utility_infinity_timestamp_fn()
      and valid_from < clock_timestamp();
{{/domain_entity.has_audit_columns}}

    NEW.valid_from = clock_timestamp();
    NEW.valid_to = ores_utility_infinity_timestamp_fn();
{{#domain_entity.has_audit_columns}}
    NEW.modified_by := ores_iam_validate_account_username_fn(NEW.modified_by);
    NEW.performed_by = coalesce(ores_iam_current_service_fn(), current_user);
{{/domain_entity.has_audit_columns}}

    return NEW;
end;
$$ language plpgsql security definer set search_path = public, pg_temp;

create or replace trigger {{domain_entity.sql_name_base}}_insert_trg
before insert on "{{domain_entity.sql_name_base}}_tbl"
for each row execute function {{domain_entity.sql_name_base}}_insert_fn();

create or replace rule {{domain_entity.sql_name_base}}_delete_rule as
on delete to "{{domain_entity.sql_name_base}}_tbl" do instead (
    update "{{domain_entity.sql_name_base}}_tbl"
    set valid_to = clock_timestamp(){{#domain_entity.sql.extra_delete_sets}},
        {{{.}}}{{/domain_entity.sql.extra_delete_sets}}
    where {{#domain_entity.has_tenant_in_pk}}tenant_id = OLD.tenant_id
      and {{/domain_entity.has_tenant_in_pk}}{{#domain_entity.sql.nullable_tenant_id}}(tenant_id = OLD.tenant_id or (tenant_id is null and OLD.tenant_id is null))
      and {{/domain_entity.sql.nullable_tenant_id}}{{{domain_entity.primary_key.where_old}}}
      and valid_to = ores_utility_infinity_timestamp_fn();
{{#domain_entity.foreign_keys}}
{{#bump_parent_version}}
    -- Bump {{table}}'s version on delete too (composite entity
    -- versioning), symmetric with the insert-side call above.
    select {{touch_function}}(
        OLD.tenant_id,
        OLD.{{column}},
        OLD.change_reason_code,
        OLD.change_commentary,
        OLD.modified_by,
        OLD.performed_by,
        '{{domain_entity.entity_singular}}'
    );
{{/bump_parent_version}}
{{/domain_entity.foreign_keys}}
);
{{/domain_entity.sql.bitemporal_soft_update}}
{{#domain_entity.has_tenant_id}}
{{#domain_entity.validation_fn}}
{{^domain_entity.primary_key.is_compound}}

-- =============================================================================
-- Validation function for {{domain_entity.entity_singular}}
-- Validates that a {{domain_entity.primary_key.column}} exists in the {{domain_entity.entity_plural}} table.
-- Returns the validated value, or default if null/empty.
{{#domain_entity.validation_fn.scope_system}}
-- Uses system tenant data (shared reference data).
{{/domain_entity.validation_fn.scope_system}}
{{#domain_entity.validation_fn.scope_both}}
-- Validates against both the tenant's own data and the system tenant's canonical set.
{{/domain_entity.validation_fn.scope_both}}
{{#domain_entity.validation_fn.scope_tenant}}
-- Validates against the tenant's own data only.
{{/domain_entity.validation_fn.scope_tenant}}
-- =============================================================================
create or replace function {{domain_entity.product}}_{{domain_entity.component}}_validate_{{domain_entity.entity_singular}}_fn(
    p_tenant_id uuid,
    p_value {{domain_entity.primary_key.type}}
) returns {{domain_entity.primary_key.type}} as $$
begin
    -- Return default if null or empty
    if p_value is null{{#domain_entity.primary_key.is_text}} or p_value = ''{{/domain_entity.primary_key.is_text}} then
{{#domain_entity.validation_fn.default}}
{{#domain_entity.primary_key.is_text}}
        return '{{domain_entity.validation_fn.default}}';
{{/domain_entity.primary_key.is_text}}
{{^domain_entity.primary_key.is_text}}
        return {{domain_entity.validation_fn.default}};
{{/domain_entity.primary_key.is_text}}
{{/domain_entity.validation_fn.default}}
{{^domain_entity.validation_fn.default}}
        raise exception 'Invalid {{domain_entity.entity_singular}}: value cannot be null{{#domain_entity.primary_key.is_text}} or empty{{/domain_entity.primary_key.is_text}}'
            using errcode = '23502';
{{/domain_entity.validation_fn.default}}
    end if;

{{#domain_entity.validation_fn.scope_system}}
    -- Allow pass-through during bootstrap (no active rows for system tenant).
    if not exists (
        select 1 from {{domain_entity.sql_name_base}}_tbl
        where tenant_id = ores_utility_system_tenant_id_fn()
          and valid_to = ores_utility_infinity_timestamp_fn()
    ) then
        return p_value;
    end if;

    -- Validate against reference data
    if not exists (
        select 1 from {{domain_entity.sql_name_base}}_tbl
        where tenant_id = ores_utility_system_tenant_id_fn()
          and {{domain_entity.primary_key.column}} = p_value
          and valid_to = ores_utility_infinity_timestamp_fn()
    ) then
        raise exception 'Invalid {{domain_entity.entity_singular}}: %. Must be one of: %', p_value, (
            select string_agg({{domain_entity.primary_key.column}}::text, ', ' order by {{domain_entity.validation_fn.order_by}})
            from {{domain_entity.sql_name_base}}_tbl
            where tenant_id = ores_utility_system_tenant_id_fn()
              and valid_to = ores_utility_infinity_timestamp_fn()
        ) using errcode = '23503';
    end if;
{{/domain_entity.validation_fn.scope_system}}
{{#domain_entity.validation_fn.scope_both}}
    -- Allow pass-through if neither this tenant nor the system tenant has
    -- seeded active {{domain_entity.entity_plural}} yet (freshly provisioned tenant).
    if not exists (
        select 1 from {{domain_entity.sql_name_base}}_tbl
        where tenant_id in (p_tenant_id, ores_utility_system_tenant_id_fn())
          and valid_to = ores_utility_infinity_timestamp_fn()
    ) then
        return p_value;
    end if;

    -- Validate against this tenant's values and the system tenant's canonical set.
    if not exists (
        select 1 from {{domain_entity.sql_name_base}}_tbl
        where tenant_id in (p_tenant_id, ores_utility_system_tenant_id_fn())
          and {{domain_entity.primary_key.column}} = p_value
          and valid_to = ores_utility_infinity_timestamp_fn()
    ) then
        raise exception 'Invalid {{domain_entity.entity_singular}}: %. Must be one of: %', p_value, (
            select string_agg({{domain_entity.primary_key.column}}::text, ', ' order by {{domain_entity.validation_fn.order_by}})
            from {{domain_entity.sql_name_base}}_tbl
            where tenant_id in (p_tenant_id, ores_utility_system_tenant_id_fn())
              and valid_to = ores_utility_infinity_timestamp_fn()
        ) using errcode = '23503';
    end if;
{{/domain_entity.validation_fn.scope_both}}
{{#domain_entity.validation_fn.scope_tenant}}
    -- Allow pass-through if this tenant has no active {{domain_entity.entity_plural}} yet.
    if not exists (
        select 1 from {{domain_entity.sql_name_base}}_tbl
        where tenant_id = p_tenant_id
          and valid_to = ores_utility_infinity_timestamp_fn()
    ) then
        return p_value;
    end if;

    -- Validate against this tenant's values.
    if not exists (
        select 1 from {{domain_entity.sql_name_base}}_tbl
        where tenant_id = p_tenant_id
          and {{domain_entity.primary_key.column}} = p_value
          and valid_to = ores_utility_infinity_timestamp_fn()
    ) then
        raise exception 'Invalid {{domain_entity.entity_singular}}: %. Must be one of: %', p_value, (
            select string_agg({{domain_entity.primary_key.column}}::text, ', ' order by {{domain_entity.validation_fn.order_by}})
            from {{domain_entity.sql_name_base}}_tbl
            where tenant_id = p_tenant_id
              and valid_to = ores_utility_infinity_timestamp_fn()
        ) using errcode = '23503';
    end if;
{{/domain_entity.validation_fn.scope_tenant}}

    return p_value;
end;
$$ language plpgsql security definer set search_path = public, pg_temp;
{{/domain_entity.primary_key.is_compound}}
{{/domain_entity.validation_fn}}
{{/domain_entity.has_tenant_id}}
{{#domain_entity.has_parent_id}}

-- =============================================================================
-- Hierarchy traversal for {{domain_entity.entity_plural}}.
-- Returns a flat set of {id, parent_id, name} nodes for the subtree rooted
-- at p_root_id. When p_from_root is true, first walks up parent_id to the
-- ultimate ancestor (parent_id is null) and recurses down from there instead,
-- returning the whole tenant tree the given node belongs to.
-- =============================================================================
create or replace function {{domain_entity.sql_name_base}}_hierarchy_fn(
    p_tenant_id uuid,
    p_root_id uuid,
    p_from_root boolean default false
) returns table(id uuid, parent_id uuid, name text) as $$
declare
    v_root_id uuid;
begin
    v_root_id := p_root_id;

    if p_from_root then
        with recursive ancestors as (
            select t.id, t.{{domain_entity.parent_id_column}} as parent_id
            from "{{domain_entity.sql_name_base}}_tbl" t
            where t.tenant_id = p_tenant_id
              and t.id = p_root_id
              and t.valid_to = ores_utility_infinity_timestamp_fn()
            union all
            select t.id, t.{{domain_entity.parent_id_column}} as parent_id
            from "{{domain_entity.sql_name_base}}_tbl" t
            join ancestors a on t.id = a.parent_id
            where t.tenant_id = p_tenant_id
              and t.valid_to = ores_utility_infinity_timestamp_fn()
        )
        select a.id into v_root_id
        from ancestors a
        where a.parent_id is null
        limit 1;

        if v_root_id is null then
            v_root_id := p_root_id;
        end if;
    end if;

    return query
    with recursive descendants as (
        select t.id, t.{{domain_entity.parent_id_column}} as parent_id,
               t.{{domain_entity.hierarchy_name_field}}::text as name
        from "{{domain_entity.sql_name_base}}_tbl" t
        where t.tenant_id = p_tenant_id
          and t.id = v_root_id
          and t.valid_to = ores_utility_infinity_timestamp_fn()
        union all
        select t.id, t.{{domain_entity.parent_id_column}} as parent_id,
               t.{{domain_entity.hierarchy_name_field}}::text as name
        from "{{domain_entity.sql_name_base}}_tbl" t
        join descendants d on t.{{domain_entity.parent_id_column}} = d.id
        where t.tenant_id = p_tenant_id
          and t.valid_to = ores_utility_infinity_timestamp_fn()
    )
    select d.id, d.parent_id, d.name from descendants d;
end;
$$ language plpgsql stable security definer set search_path = public, pg_temp;
{{/domain_entity.has_parent_id}}
{{#domain_entity.sql.hypertable}}
do $$
declare
    tsdb_installed boolean;
begin
    select exists (
        select 1 from pg_extension where extname = 'timescaledb'
    ) into tsdb_installed;

    if tsdb_installed then
        raise notice 'TimescaleDB detected - creating hypertable ({{domain_entity.sql.hypertable_chunk_interval}} chunks)';

        perform public.create_hypertable(
            '"{{domain_entity.sql_name_base}}_tbl"',
            '{{domain_entity.sql.hypertable_partition_column}}',
            chunk_time_interval => interval '{{domain_entity.sql.hypertable_chunk_interval}}',
            if_not_exists => true
        );
    else
        raise notice 'TimescaleDB not available - using regular table (manual cleanup required)';
    end if;
end $$;
{{/domain_entity.sql.hypertable}}
<<paste:92432102-d405-46fc-911a-111dfed27358>>

See also

Emacs 29.3 (Org mode 9.6.15)