Skip to content

feat(core): content references database schema - #1367

Merged
ascorbic merged 8 commits into
emdash-cms:mainfrom
MA2153:content-reference-schema
Jun 12, 2026
Merged

feat(core): content references database schema#1367
ascorbic merged 8 commits into
emdash-cms:mainfrom
MA2153:content-reference-schema

Conversation

@MA2153

@MA2153 MA2153 commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds the content-references database schema — the narrow first step toward reference fields. Schema only: no field type, API, handlers, repositories, or admin UI yet (deliberately out of scope; those come in follow-ups).

Two new system tables via forward-only migration 043_content_references.ts:

  • _emdash_relations — relationship-type definitions. Row-per-locale, mirroring _emdash_taxonomy_defs: declares which collection is the parent and which is the child (the multipliable side), plus localized parent_label / child_label for each role. UNIQUE(name, locale).
  • _emdash_content_references — directed parent → child edges between content entries. Both endpoints and the relation are referenced by translation_group, so edges are locale-agnostic. Mirrors content_taxonomies: group-linking means no foreign keys (a translation_group is unique nowhere), and dangling-edge cleanup is an application-layer obligation for the future write path. UNIQUE(relation_group, parent_group, child_group).

Design notes: relations are directed; the child is the side that multiplies; many-to-many within a single relation is intentionally not modeled (swap parent/child to express the inverse). Same-collection and self-references are permitted. The migration is idempotent (.ifNotExists() / .ifExists()) so it survives the runner's partial-run recovery path.

Related Discussion: #386

Closes #

Type of change

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes — packages/core typechecks clean. (A pre-existing, unrelated failure in packages/plugin-cli re: registry-lexicons exports is present on main and not touched here.)
  • pnpm lint passes — no diagnostics in changed files. (8 pre-existing diagnostics in unrelated templates//demos/ astro configs predate this PR.)
  • pnpm test passes (targeted) — 23/23 across content-references.test.ts (8) and migrations.test.ts (15), SQLite. Postgres parity runs via describeEachDialect when PG_CONNECTION_STRING is set.
  • pnpm format has been run
  • I have added/updated tests for my changes
  • User-visible admin strings wrapped for translation — n/a (no admin UI in this PR; schema only)
  • I have added a changeset
  • New features link to an approved Discussion: Complete the reference field: collection picker in schema editor + content picker in content editor #386

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.8 (Claude Code)

Screenshots / test output

Test Files  2 passed (2)
     Tests  23 passed (23)

New schema tests cover: table existence; insert + default backfill (locale='en', sort_order=0); both unique constraints (edge triple + name/locale, incl. cross-locale allow); forward + backlink traversal with ordering; same-collection / self-references; index existence (SQLite); and down()up() rollback. Dialect-agnostic (SQLite + Postgres).

🤖 Generated with Claude Code

MA2153 and others added 7 commits June 5, 2026 22:14
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Guard every CREATE TABLE/INDEX in 043 with .ifNotExists() so a crash
mid-migration (or the runner's race-recovery re-run) re-applies cleanly,
and add 043 to the trailing-migration re-run test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Match the .ifNotExists() hardening on up() so dropTable is safe to
re-run after a partial rollback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jun 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: dbbf5dc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
emdash Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/admin Minor
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added review/needs-review No maintainer or bot review yet area/core size/L labels Jun 5, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jun 5, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@1367

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@1367

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@1367

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@1367

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@1367

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@1367

emdash

npm i https://pkg.pr.new/emdash@1367

create-emdash

npm i https://pkg.pr.new/create-emdash@1367

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@1367

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@1367

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@1367

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@1367

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@1367

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@1367

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@1367

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@1367

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@1367

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@1367

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@1367

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@1367

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@1367

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@1367

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@1367

commit: dbbf5dc

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approach assessment

This is well-scoped, schema-only groundwork for content references. The design sensibly mirrors the existing taxonomy/byline row-per-locale + translation-group model (_emdash_taxonomy_defs / content_taxonomies). Locale-agnostic edges linked by translation_group are the right fit for EmDash's content-localization architecture, and keeping foreign keys out of the edge table is consistent with how content_taxonomies already works.

The migration is additive, forward-only, and idempotent (.ifNotExists() / .ifExists()), which is exactly what the runner's partial-run recovery path needs. The down() ordering drops the edge table before the definition table, which is correct.

What I checked

  • Migration correctness (idempotency, SQLite/Postgres compatibility, column types, defaults, unique constraints, index shapes)
  • Type definitions in types.ts against the migration schema
  • Runner registration and MIGRATION_COUNT
  • Test coverage (table existence, defaults, unique constraints, cross-locale duplicates, forward/backlink traversal, self-references, index existence, down/up round-trip)
  • Cross-cutting concerns: no orphaned-table impact, no handler/repository side effects (intentionally out of scope), no SQL interpolation issues

Headline conclusion

The implementation is clean and the tests are thorough. There are no logic bugs or regressions. I have two non-blocking suggestions:

  1. Index naming: align with the literal idx_{table}_{column} convention used in recent migrations (double-underscore for _emdash_* tables).
  2. Changeset level: consider minor instead of patch for new feature groundwork.
  3. Optional: consider a partial unique index on _emdash_relations(translation_group, locale) to enforce the row-per-locale invariant at the DB level, matching what migration 040 does for bylines.

Good to merge once the maintainer weighs the naming suggestion.

Comment thread packages/core/src/database/migrations/043_content_references.ts
@@ -0,0 +1,5 @@
---
"emdash": patch
---

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] This is net-new feature groundwork. While the package is pre-1.0, a minor bump is more idiomatic in Changesets for additive capabilities than patch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumped to minor.

Comment thread packages/core/src/database/migrations/043_content_references.ts
@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Jun 6, 2026
@masonjames

Copy link
Copy Markdown
Contributor

Nice work @MA2153

I pulled this down locally and tested it against nearby open work.

Overall, this looks like solid schema groundwork. The main thing I’d recommend before merge is the relation group invariant. _emdash_content_references.relation_group points at _emdash_relations.translation_group, but _emdash_relations.translation_group is currently nullable and not unique per locale. That leaves room for future lookups by (relation_group, locale) to return zero rows or multiple rows. Since this is a new table, I’d either make translation_group required or add the partial unique index emdashbot suggested:

UNIQUE (translation_group, locale) WHERE translation_group IS NOT NULL
…names

Address PR emdash-cms#1367 review:
- Make _emdash_relations.translation_group NOT NULL. Edges reference
  relations by translation_group (relation_group is NOT NULL), so a null
  group is an unreferenceable dead row. New table, no backfill window.
- Add a unique index on (translation_group, locale) — one relation variant
  per locale. Plain (not partial like bylines' 040) since the column is now
  NOT NULL.
- Align index names with the idx_{full_table}_{column} convention used by
  036/040/042 (idx__emdash_*).
- Bump changeset to minor (additive feature groundwork).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Jun 7, 2026
@MA2153

MA2153 commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for pulling it down @masonjames. On the relation-group invariant: I took the stronger of your two options — _emdash_relations.translation_group is now NOT NULL (rather than just adding a partial unique). Reasoning: edges reference relations by translation_group and _emdash_content_references.relation_group is NOT NULL, so a null group is a row no edge can ever point at — structurally dead. Since this is a new table there is no backfill window (the reason bylines/040 keep theirs nullable + partial), so NOT NULL + a plain UNIQUE(translation_group, locale) index fully enforces "at most one variant per locale." Pushed in dbbf5dc, with tests for the dup and null cases.

@ascorbic ascorbic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview Author pushed changes since the last review labels Jun 12, 2026
@ascorbic
ascorbic merged commit f41092b into emdash-cms:main Jun 12, 2026
55 of 56 checks passed
@emdashbot emdashbot Bot mentioned this pull request Jun 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3 participants