Skip to content

feat(core): content references database schema#1367

Open
MA2153 wants to merge 8 commits into
emdash-cms:mainfrom
MA2153:content-reference-schema
Open

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

Conversation

@MA2153
Copy link
Copy Markdown
Contributor

@MA2153 MA2153 commented Jun 5, 2026

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
Copy link
Copy Markdown

changeset-bot Bot commented Jun 5, 2026

🦋 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
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 5, 2026

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

Copy link
Copy Markdown
Contributor

@emdashbot emdashbot Bot left a comment

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
Copy link
Copy Markdown
Contributor Author

MA2153 commented Jun 7, 2026

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core cla: signed review/needs-rereview Author pushed changes since the last review size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants