Skip to content

Enable declarative custom metadata schemas for repo-backed namespaces - #2468

Merged
shangyian merged 2 commits into
mainfrom
dj/cm-phase-7-repo-backed
Aug 28, 2026
Merged

Enable declarative custom metadata schemas for repo-backed namespaces#2468
shangyian merged 2 commits into
mainfrom
dj/cm-phase-7-repo-backed

Conversation

@shangyian

@shangyian shangyian commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

A namespace's custom_metadata vocabulary can now be declared in the deployment manifest, next to the nodes it governs, instead of only through admin API calls:

namespace: analytics.sales
custom_metadata_schemas:
  - key: sla
    description: Freshness commitment for scheduled outputs.
    owner: 'foobar'
    json_schema:
      type: object
      properties:
        max_staleness_hours:
          type: integer
          minimum: 1
        pager_rotation:
          type: string
      required: [max_staleness_hours]

  - key: review_status
    node_type: metric
    description: Where a metric definition sits in review.
    json_schema:
      type: string
      enum: [draft, in_review, approved]

  - key: pii_reviewed
    namespace: analytics.sales.customer
    description: Whether a privacy review has been recorded.
    json_schema:
      type: boolean

A deployment manages the complete set of schemas for the namespace it's scoped to. Any declared keys here are created, updated, or revived if previously retired. Omitting the section leaves existing schemas alone, but declaring an empty list [] will make DJ match the (empty-list) manifest and retire any existing schemas.

The schema defaults to the deploying namespace, but you can also name one to narrow it to a sub-namespace. Anything outside of the deploying namespace is rejected at spec validation, so one repo can't register schemas that govern another's nodes. Globally reserved keys remain untouchable from a deployment.

Test Plan

Last of the custom metadata schema registry stack, following #2456 and #2461.

Deployment Plan

A namespace's manifest can now carry `custom_metadata_schemas`, so the keys a
namespace expects -- and the JSON Schema each value must satisfy -- are declared
alongside the nodes that carry them, and reviewed in the same PR. Registration
goes through the same helpers as the REST API, so two writers to one table
cannot enforce different rules.

The manifest is the whole truth for its own namespace: declared keys are
upserted, and rows it no longer declares are retired. The `namespace` column is
what makes that safe -- it already records who owns a row, and a global row has
no owning namespace, so no deployment may retire one.

`custom_metadata_schemas` is nullable because None and `[]` are different
manifests. None does not manage schemas at all; `[]` manages them and declares
none, which retires the namespace's rows. With a list default, every deployment
predating this field would have read as the latter.

Registration revives a soft-deleted row rather than inserting beside it. The
unique index spans deactivated rows while every read filters them out, so an
insert next to a tombstone violates the constraint -- which made a retired key
permanently unregisterable, by either writer. Reviving also preserves the row's
id and created_at, so a key that comes back is the same registration rather
than a new one.

The transaction belongs to the caller. The orchestrator opens a SAVEPOINT so a
dry run can be rolled back, and committing here releases it -- so `POST
/deployments/impact`, which exists only to report what a deployment would do,
would have permanently registered the schemas it was asked to analyse. Index
DDL is skipped for a dry run, since a rolled-back CREATE INDEX is work done for
nothing.
A schema took the deploying namespace and nothing else, so a repo could only
govern its whole graph at once. That is the wrong granularity for a staged
rollout: a vocabulary usually starts on the part of the graph that matters most
-- conformed dimensions, say -- and widens once it holds.

A spec may now name its own namespace. Omitted, it still defaults to the
deployment's, so nothing existing changes. Named, it must be that namespace or
one beneath it: narrower is a rollout choice, while wider or sideways would let
one repo govern another repo's nodes. The check lives in `set_namespaces`
alongside the defaulting it already does for nodes, hierarchies and pre-aggs,
and rejects a prefix that only looks like a descendant -- `shared_other` is not
under `shared`.

Reconciliation now covers the deploying namespace plus whatever sub-namespaces
the specs name, rather than the deploying namespace alone. Both halves matter:
declaring a schema for `shared.conformed` must not retire one for
`shared.finance`, which another deployment owns, and an empty list must still
retire the deploying namespace's own rows, which is the only thing that
distinguishes "declares none" from "does not manage these".
@netlify

netlify Bot commented Aug 28, 2026

Copy link
Copy Markdown

Deploy Preview for thriving-cassata-78ae72 canceled.

Name Link
🔨 Latest commit c438c1f
🔍 Latest deploy log https://app.netlify.com/projects/thriving-cassata-78ae72/deploys/6a91883bae16b90008f59282

@shangyian shangyian changed the title Custom Metadata Declarative JSON Schema Enable declarative custom metadata schemas for repo-backed namespaces Aug 28, 2026
@shangyian
shangyian marked this pull request as ready for review August 28, 2026 15:49
@shangyian
shangyian merged commit efbb81f into main Aug 28, 2026
21 checks passed
@shangyian
shangyian deleted the dj/cm-phase-7-repo-backed branch August 28, 2026 15:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant