Skip to main content

What lands in the target environment

Most automation tools deliver rows. PDQ delivers a described, constrained and classified schema — and the rows.

The tables in your target database are generated from the same metadata that drives the loading. That is why they arrive documented rather than needing documenting: comments, primary keys, foreign keys, unique constraints, category tags and sensitivity tags are emitted by the generator, from declarations made once upstream. Nobody writes a COMMENT ON COLUMN script. Nobody re-tags PII in the catalogue after the fact.

This page describes what actually appears in the target, where each part of it comes from, and how to get it back out again.


Declared once, materialised everywhere​

A field is described in one place — the sourcefile data contract, or the model attribute — and that description travels the whole way down.

You declareWhere you declare itWhat appears in the target
descriptionSourcefile field, model attribute, model objectColumn and table COMMENT in the generated CREATE TABLE
keyFieldIndicator + keyOrderSourcefile fieldPrimary key on the publish table; unique constraint on the core table
Relationship (toObject, toAttribute, relationType)ModelForeign key, child → parent
fieldDomainSourcefile field, via Field CategorisationCategory tag on the column, and on child tables that inherit the key
sensitiveSourcefile fieldSensitivity tag, and a column mask where the platform supports one
validFromSourcefile fieldSCD Type 2 versioning column; the validity period on the historised core
dataTypeSourcefile fieldThe column type, mapped to the target platform's own type

Change the declaration, re-generate, and the target changes with it. The description in the console, the comment on the column and the definition in the repository cannot drift apart, because there is only one of them.


Structure — keys and constraints​

Constraints are generated from metadata that already exists. A business key is already declared to make deduplication and upserts work; a relationship is already declared to make the model hang together. The generator turns both into database objects rather than leaving them as platform-internal knowledge.

ObjectGenerated fromWhere it lands
Unique constraintBusiness keysCore tables — inline where the platform requires it, ALTER … ADD elsewhere
Foreign keyRelationship metadataChild → parent, de-duplicated
Primary keyBusiness keys, with __fileKey includedPublish tables

All three default to on. To keep the pre-3.2 output, set ENABLE_CORE_UNIQUE, ENABLE_FOREIGN_KEY and ENABLE_PUBL_PRIMARY_KEY to 0.

Structural and decorative DDL are separated

The DEVELOPMENT path emits only structural column changes — CREATE TABLE IF NOT EXISTS and ALTER ADD/DROP COLUMN. Comments, unique and foreign-key DDL are definition-only, and duplicate executable statements are de-duplicated in the audit trace. A development deploy therefore does not rewrite decoration it did not change.


Documentation — comments at creation time​

Column and table comments are emitted inside the CREATE TABLE on Snowflake and Databricks, not bolted on afterwards. A table is documented from the moment it exists, so there is no window in which it exists undocumented.

  • The comment text is the description written on the sourcefile field, the model attribute or the model object.
  • Staging landing tables carry fixed technical descriptions on rowId, fileName, the VARIANT column and loadedTs — so even the plumbing explains itself.
  • Apostrophes in descriptions are escaped on every comment path, so a description reading the customer's region does not break the statement.

The same descriptions render in the console's generated documentation and in the definitions produced by the SQL Generator API. One source, three surfaces.


Classification — tags and masking​

Classification is not a separate cataloguing exercise that happens after the warehouse is built. It is a property of the field, and it is applied to the target as part of building the table.

ConceptConfigurationEmitted as
Category tagTAG_NAME, with a default categoryA tag on the column, carrying its fieldDomain
Sensitive dataSENSITIVE_TAG_NAME, SENSITIVE_TAG_VALUEA sensitivity tag on every field flagged sensitive
MaskingPlatform-dependentA masking policy on Snowflake, dynamic data masking on the T-SQL family, SECURITY LABEL on PostgreSQL — and nothing on Databricks, where sensitive produces a tag only

On Databricks classification is emitted as SET TAGS, because that is the facility Unity Catalog actually reads; no mask is generated there. Categories are added to published tables, referencing only the parent table. The full per-platform picture is in Differences between target platforms.

Set your own scheme

TAG_NAME, SENSITIVE_TAG_NAME and SENSITIVE_TAG_VALUE have defaults, and the defaults are easy to live with by accident. Decide the scheme that matches your catalogue's conventions before the first generation run, not after a few hundred tables carry the wrong tag name.

Classification follows the key down the hierarchy​

When a hierarchical source is normalised into a table set, a child table carries the business keys inherited from its ancestor levels. Those key columns arrive with their domain tags attached: a classification declared on a parent level is applied to the child table where the key lands.

The inheritance rules are deliberately conservative:

  • Ancestors are walked nearest-first, and only fill gaps.
  • A domain declared on the table's own level always wins.
  • Each alias is emitted once.
  • Only key fields are inherited.

The practical effect is that a column classified once — say, a national identity number used as a business key — stays classified everywhere it is carried, without anyone tracking where it was copied to.


What each platform actually enforces​

Governance is emitted per platform, using the facility that platform provides. What that means in practice differs:

TargetConstraintsCommentsClassification
SnowflakeUnique, primary and foreign keys generated, all NOT ENFORCEDCOMMENT inside CREATE TABLETags; QUERY_TAG carries the run
DatabricksNone emitted — the UNIQUE, PK and FK templates are deliberately omittedCOMMENT inside CREATE TABLESET TAGS in Unity Catalog
SQL Server / FabricEnforced unique constraints; foreign keys generated but not enforcedExtended properties on SQL Server; not carried on FabricExtended properties
SynapseUnique NOT ENFORCED; no foreign keys — dedicated pools do not support themNot carriedExtended properties
PostgreSQLEnforced unique and foreign keys, sequencesBase layer onlySide table
Only PostgreSQL enforces referential integrity

Databricks, Snowflake and Synapse emit no enforced UNIQUE, and only PostgreSQL enforces a foreign key. Duplicates still cannot reach the core — deduplication on the business key is part of the generated loading pattern — so what the missing constraint costs you is protection against writes that bypass that load path. See Differences between target platforms for the full breakdown.


Every statement carries the job that issued it​

Metadata is not only captured about the pipeline — it is pushed into the target's own observability surfaces. Every statement the worker sends carries the unit of work it belongs to: app, worker version, kind (dwa / dm / qpi), job, step, run, queue, retry, plus the step's own object, attribute and relationship identifiers. Nothing needs configuring, and a static QUERY_TAG or APP value you already set is preserved rather than discarded.

TargetNative facility the tag lands in
SnowflakeQUERY_TAG, as JSON
Databricksuser_agent_entry
PostgreSQLapplication_name (63-byte limit)
SQL Server / Synapse / FabricODBC APP → program_name
All targetsLeading sqlcommenter comment

The comment is emitted in sqlcommenter form — sorted, percent-encoded key='value' pairs that database observability tooling already recognises.

Why this matters beyond debugging: a query in the target's own history can be joined back to the pipeline run that issued it. That is the join key cost attribution has been missing — consumption recorded by the warehouse on one side, the declaration that caused it on the other.


Metadata captured everywhere, and actionable​

Every stage writes its own record, and every record is addressable — in the console, and through the API.

What is capturedWhere it is capturedWhat you can do with it
Delivery audit logLanding zone, per fileTrace a delivery through Landing → Raw → Trusted → Published
Profiling statisticsProfile zone, per sourcefileSee type conformance, null ratios, distributions and schema drift
Loading statisticsPer sourcefile and per objectRows staged and inserted, per run
Publisher auditPer publishConfirm each publish landed in its destination table
Quality-check resultsPer check, keyed (qpi, runDttm)Distinguish a rule that caught something from a check that was itself broken
Action audit logPer console actionWho did what, in which role, on which page, and when
LineageDerived from live metadataSystems → source files → models → exports

Two design decisions make this actionable rather than merely present:

  • A failed load is visible, not absent. Audit logging in the landing zone retries five times, then pushes the message to a <zone>-parked queue and reports failure. Parked messages are not retried automatically — monitor those queues.
  • A quality run reads as data, not as a single pass/fail. Each check records its own state and result. A check whose SQL ran but whose test failed is Completed with resultState=fail; a check whose SQL raised is Failed with the real error and traceback.

Getting it out — APIs and OpenLineage​

None of this is locked inside the platform. It is metadata, and metadata is exportable.

REST API​

The AME REST API is the same interface the console and the generators read — there is no privileged internal channel. The v3 family serves configuration, structure, schedules, logs and lineage, with /api/v3.2 for object-level generation.

/api/v4 is where the data product contract lives. A data product specification is the /api/v4 data product contract — the two are the same thing under two names. It is a separate family from v3 rather than a successor to it: v3 describes how the platform is configured and what it did, v4 describes what is being delivered.

  • Field Categorisation endpoints fetch, create and update field domain categorisations, with full version tracking.
  • Log-by-date endpoints and publisher audit endpoints expose the operational record.
  • Source file endpoints return the full data contract, including listLevels, targetNormalization and Published schemas.
  • Definition generation through the SQL Generator API returns the generated DDL itself, so a definition can be diffed in CI before it is deployed.

The sourcefile data contract is negotiated rather than assumed — tried at v3.2, then v3.1, then v3.

OpenLineage​

PDQ emits OpenLineage events, so lineage lands in whatever catalogue or observability platform you already run rather than only in the PDQ console.

  • Start and end events for all executions, from the SQL execution engine.
  • Parent-job events from QPI quality checks, so a check attaches to the run it belongs to.
  • Attribute-level lineage available as a detailed option, alongside dataset-level events.
  • Published patterns, trusted datasets and PDQ views are all represented, with namespaces resolved per zone.

The practical consequence: the catalogue you already run does not need a pipeline of its own to know where a column came from.


What this replaces​

The usual arrangementWith PDQ
A cataloguing project that tags PII after the warehouse is builtThe tag is a property of the field, applied when the table is created
A data dictionary that drifts from the databaseThe comment in the database is the description in the repository
Constraints documented in a modelling tool and never deployedKeys and relationships generated as database objects
Lineage maintained by hand, or inferred by parsing SQLLineage emitted as OpenLineage events by the run itself
Warehouse spend attributed by guessworkEvery query in the target's history carries the job that issued it

Governance stops being a parallel project running beside the pipeline, and becomes a property of the pipeline.


Next steps​