DQ artefacts
Table of Contents
1. Summary
A DQ artefact is a staging table, ores_dq_<plural>_artefact_tbl, that holds
an entity's rows by dataset until a publication writes them into the service
that owns the entity. There is one way to make one: the entity's own model, in
the component that owns the data, opts in with
:ores.sql.schema.domain_entity_artefact_create.enabled: true, and codegen
generates the table from that model. The model adds an * Artefact columns
section only when the staging shape differs from the entity. A staging table
for another component's entity is not an entity of DQ: it gets no DQ model, no
DQ C++ code, no shell commands and no TypeScript. A staging table that is
terminal – that publishes nowhere and is therefore the entity's only store –
is DQ's own, model and all. The rest of the pipeline is a dataset, an artefact type, a bundle
member, any dataset dependencies, and the owning service's publish subject and
SQL function. Older tables were made other ways; they are listed at the end,
with the task that retires each.
2. Detail
2.1. Words
| Word | Meaning |
|---|---|
| Artefact table | The staging table of one entity in the dq schema, keyed by dataset_id, written by seed scripts in the system tenant |
| Owning model | The model of the entity the artefact stages, in the component that owns the published rows, such as ores.refdata.book |
| Dataset | One named, versioned set of artefact rows, registered in ores_dq_datasets_tbl with its artefact type |
| Artefact type | The row in ores_dq_artefact_types_tbl that names the artefact table, the target table and the publish subject |
| Bundle | A named list of datasets published together, such as base or system_core |
| Publication | A workflow that publishes a bundle's datasets in dependency order, one step per dataset |
2.2. The one way
The artefact belongs to the model of the data it stages. A book's artefact table
comes from ores.refdata.book, a currency's from ores.refdata.currency, an
image's from ores.assets.image: one model states the entity and its staging
shape, and the two cannot drift.
- Opt in on the owning model. Add
:ores.sql.schema.domain_entity_artefact_create.enabled: trueto the model's properties. The archetype is ores.sql.schema.domain_entity_artefact_create; it is off by default because most entities have no import. - Declare the staging shape when it differs. By default the artefact table
projects the entity's columns. Add an
* Artefact columnssection, one** <column>per staging column in order, when the import needs something the store does not: a natural key where the store holds a uuid, a field the publisher supplies left out, or a denormalised row that publishes into several tables. The first column is the staging key unless:key:names another. Anexpressiontable in the section's own body, above the first column, states the staging table's check constraints, as the** Checkstable of the* SQLsection does for the entity's. - Regenerate the SQL schema for the model:
./compass.sh codegen regenerate --component <component> --address ores.sql.schema --entity <entity>. The table lands inprojects/ores.sql/create/dq/dq_<plural>_artefact_create.sql. - Include it by hand. Add the
\irline todq_create.sql, and write the drop scriptprojects/ores.sql/drop/dq/dq_<plural>_artefact_drop.sqlwith its line indq_drop.sql, as every artefact does today. - Seed the rows. A populate script in the owning component's populate folder
registers the dataset with
ores_dq_datasets_upsert_fn, naming its catalogue and its artefact type, and inserts the artefact rows for that dataset id. It is idempotent: it leaves a dataset that already has rows, or replaces them. - Name the artefact type. Add a row to
projects/ores.sql/populate/dq/dq_artefact_types_populate.sql: the code, the artefact table, the target table and the publish subject,<component>.v1.ops.publish_<plural>_from_dq. A new kind of data gets a new artefact type; data of an existing kind reuses its type. - Put the dataset in a bundle with
ores_dq_dataset_bundle_members_upsert_fn.system_coreholds the installation's own data and is published into the system tenant once; every other bundle is published into each tenant a profile names. - Declare what it depends on with
ores_dq_dataset_dependencies_upsert_fn. The publication orders datasets by their dependencies and nothing else: the display order of a bundle member does not decide it. A dataset whose rows validate against another dataset's rows, such as a feed naming its provider, declares that dataset. - Publish in the owning service. The service subscribes to the subject in its
registrar and lets its
publish_from_dq_handlercallores_<component>_publish_<plural>_from_dq_fn. Follow the component's existing handler: synthetic checks each function against an allow-list, and refdata lists its subjects in the registrar. - Write the publish function in
projects/ores.sql/create/<component>/<component>_publish_from_dq_create.sql. - Test the publish in pgTAP.
2.3. The publish function
Every publish function has the same signature and contract:
create or replace function ores_<component>_publish_<plural>_from_dq_fn( p_dataset_id uuid, p_target_tenant_id uuid, p_mode text default 'upsert', p_params jsonb default '{}'::jsonb ) returns table (action text, record_count bigint)
- It is
security definer, withset search_path = public, pg_temp, because it reads the DQ artefact table and writes the owning service's table. - It accepts the modes
upsert,insert_onlyandreplace_all, and refuses any other.replace_allcloses current rows before it writes the dataset's rows, and the function states the scope it closes: the rows this dataset wrote, or every row of the table in the target tenant or party. The two differ when several datasets write one table, so a function that closes the wider scope says so in its comment. It returns one row per action it took:inserted,updated,skippedordeleted, each with its count. - It is idempotent. A repeat publish of the same dataset changes nothing it already wrote. A function that upserts compares a row it holds with the staged row and skips the row when the values match, so a repeat publish adds no version. The older functions rewrite every row; do not copy that.
- It closes a row with
clock_timestamp(), as the tables' triggers and delete rules do, never withcurrent_timestamp: a row written earlier in the same transaction would otherwise get avalid_tobefore itsvalid_from. - Installation data refuses any target but the system tenant. Reference data
that every tenant reads, such as the ORE parameters, is a member of both
system_coreandbase, so its function writes to the system tenant and to each tenant. That doubles the data on purpose until readers use one source; see One source for reference data that the system and each tenant read. - It reads
p_paramsonly for what the bundle publication passes, such as the party a party-scoped publish writes for. - It writes the owning tables directly, so it is listed in the entity-writes
baseline,
projects/ores.codegen/scripts/entity_table_writes_baseline.json, beside the other publish functions.
2.4. Checklist
[ ]The owning model carries the artefact flag, and an* Artefact columnssection only when the staging shape differs.[ ]Noores.dqmodel, DQ C++ code, shell command or TypeScript was added for the artefact.[ ]The generated create script is included indq_create.sql, with its drop script indq_drop.sql.[ ]The dataset is registered with its catalogue and artefact type, and its rows are seeded idempotently.[ ]The artefact type names the artefact table, the target table and the subject.[ ]The dataset is in the right bundle:system_corefor installation data, a tenant bundle otherwise.[ ]Every dataset it validates against is declared as a dependency.[ ]The owning service subscribes to the subject, and its handler reaches the function.[ ]The publish function follows the contract above and is in the entity-writes baseline.[ ]A pgTAP test publishes the dataset, publishes it again inupsert,insert_onlyandreplace_allin the same transaction, and checks the rows.
2.5. Legacy patterns
The hand-written pattern still exists in the tree and is not to be copied. The
last group is undecided rather than legacy: where DQ owns the data, as with the
badges and coding schemes, the ores.dq model is the owning model and follows
the one way. Each group has a task in
Bring every DQ artefact table onto the canonical model.
Two patterns are retired. A separate ores.dq model that duplicated a domain
model of another component, with a DQ C++, shell and TypeScript stack for the
staging table, went first: the CSAs, netting agreements, netting sets, report
definitions and market data providers now come from their owning models, by
Move the duplicated DQ models onto their owners.
Five hand-written tables also moved onto their owning models: the CRM
topology, the market data observations and the three synthetic feed tables,
by Model the hand-written artefact tables.
The older lookup template, sql_schema_artefact_create, is gone. Its fifteen
tables – trading_account_types, asset_classes, asset_measures,
benchmark_rates, business_centres, business_processes, cashflow_types,
entity_classifications, local_jurisdictions, party_relationships,
party_roles, person_roles, regulatory_corporate_sectors,
reporting_regimes and supervisory_bodies – are generated from their own
models' artefact hook, and the template and its literate source are deleted, by
Move the lookup artefact tables onto domain models.
| Pattern | Tables | Why it is legacy | Retired by |
|---|---|---|---|
| Hand-written SQL with no model | ip2country |
Nothing generates or checks the table against its entity | Model the geo ip2country store |
An ores.dq model whose owner is unsettled is settled by one rule: the owning
model is the model of the entity the artefact stages, in the component that owns
the published rows. Where the artefact's own type row is terminal – its
target_table and target_subject both null – the artefact table is the
entity's only store, and DQ owns the model. The four DQ registries
(badge_definition, badge_severity, code_domain, coding_scheme) and the
three GLEIF staging models (lei_entity, lei_relationship, lei_bic) are
terminal and stay in ores.dq. Where the type row publishes onward, the rows
are the publishing component's data in transit, and the staging shape moves onto
that component's model as an * Artefact columns section:
counterparty_alias onto ores.refdata.counterparty_identifier,
netting_set_alias onto ores.refdata.netting_set_identifier, and
synthetic_fx_spot_config onto ores.synthetic.fx_spot_generation_config. The
report configuration binding staged in dq_report_configurations_artefact_tbl
moved the same way, onto ores.reporting.report_configuration, by
Settle the owner of the DQ-only models.
Junction artefact tables come from their own template,
sql_schema_junction_artefact_create, and are out of scope of this page.
The eight publish_from_dq_handler files, one per service, repeat one pattern
by hand and validate the subject in different ways. A new artefact follows its
component's handler; making them one is not yet a task.
2.6. The fact the code shape does not show
The artefact table lives in the dq schema, but it is the owning component's
data in transit. That is why the owning model generates it, why the owning
service publishes it, and why DQ holds no code for it: DQ owns the datasets,
bundles, artefact types and publications, not the rows they carry.
3. See also
- Data storage — the structure note this page belongs to
- ores.sql.schema.domain_entity_artefact_create — the archetype that generates an artefact table from its owning model
- ores.dq — the component that owns datasets, bundles and publications
- Data Quality — the DQ dimensions and the provenance every dataset carries
- Provisioning operations and the plan — how a profile publishes bundles into a tenant
- Bring every DQ artefact table onto the canonical model — the story that retires the legacy patterns