Skip to content

Resolve Subject ids through a MediaWiki table - #1310

Open
JeroenDeDauw wants to merge 1 commit into
masterfrom
subject-page-index
Open

Resolve Subject ids through a MediaWiki table#1310
JeroenDeDauw wants to merge 1 commit into
masterfrom
subject-page-index

Conversation

@JeroenDeDauw

@JeroenDeDauw JeroenDeDauw commented Aug 20, 2026

Copy link
Copy Markdown
Member

Fixes #1040

Subject ids are not derived from page titles, so finding the page holding a Subject takes a query, and the only index
answering it was the Neo4j projection. That put identity resolution in storage that is allowed to lag — graph failures
during a write are swallowed by design — so a Subject written during an outage stayed unfindable, and therefore
uneditable, until an administrator rebuilt. It also made Neo4j a requirement for Subject CRUD, {{#view}},
{{#neowiki_value}} and the mw.neowiki.* getters.

The mapping is now authoritative in neowiki_subject_page. A write that changes what a page holds replaces that
page's rows; the edit, delete, undelete and import paths all reach it, and a save that changes no Subjects leaves the
index untouched. An edit indexes from RevisionFromEditComplete, inside PageUpdater's atomic section, so the index
commits with the revision or not at all, outside the projection's failure isolation. Rows are deleted by primary key
rather than by page id, so concurrent saves do not contend on gap locks in the page-id index — the same reason core
deletes page_restrictions that way (T214035).

Ids come from the slot's raw JSON rather than the deserializer, so a Subject too broken to deserialize stays findable.
Reads join page, so moves need no index maintenance and the rows a deleted page leaves behind resolve to nothing.
Duplicate ids from cross-wiki transfer resolve to the lowest page id.

A wiki with no graph backend configured is now a supported mode. Query surfaces stay registered per backend;
relation-target suggestions still come from Neo4j and degrade to none without it.

Because identity now resolves authoritatively, an unresolvable Subject is refused rather than measured against the
wiki-global edit right. Creating a Subject authorizes against the page the request names, so it is unaffected;
deleting one the index cannot resolve answers 404, like the other endpoints keyed by Subject id.

RebuildSubjectPageIndex.php backfills the table and repairs it, registered to run from update.php. The decision is
recorded as ADR 32, added here.

Worth a close look

Considered, omitted

  • ArticleMergeComplete coverage for a history merge that leaves the source page as a redirect — the one write
    neither the index nor the graph projection is told about. The rebuild script repairs both.
  • Subject Sources (Implement the Subject Sources foundation (ADR 23) #993) beyond keying on the bare local nanoid.

AI-authored — Claude Code, Fable 5 (max); detailed spec from @JeroenDeDauw resolving the issue's open questions, no redirections; diff not yet human-reviewed; TDD throughout, 29 targeted mutations confirmed caught, an independent review pass whose findings are applied, full PHPUnit suite plus phpcs and phpstan green locally on the rebased branch.

Production notes

Implementation and the review pass ran as Opus 5 subagents; design, triage of the review findings and this description are Fable 5. The independent review measured the index's locking behaviour on a live MariaDB, which is what produced the primary-key delete.

Fixes #1040

Subject ids are not derived from page titles, so finding the page holding a Subject takes a query, and the only index
answering it was the Neo4j projection. That put identity resolution in storage that is allowed to lag — graph failures
during a write are swallowed by design — so a Subject written during an outage stayed unfindable, and therefore
uneditable, until an administrator rebuilt. It also made Neo4j a requirement for Subject CRUD, `{{#view}}`,
`{{#neowiki_value}}` and the `mw.neowiki.*` getters.

The mapping is now authoritative in `neowiki_subject_page`. A write that changes what a page holds replaces that
page's rows; the edit, delete, undelete and import paths all reach it, and a save that changes no Subjects leaves the
index untouched. An edit indexes from `RevisionFromEditComplete`, inside PageUpdater's atomic section, so the index
commits with the revision or not at all, outside the projection's failure isolation. Rows are deleted by primary key
rather than by page id, so concurrent saves do not contend on gap locks in the page-id index — the same reason core
deletes `page_restrictions` that way (T214035).

Ids come from the slot's raw JSON rather than the deserializer, so a Subject too broken to deserialize stays findable.
Reads join `page`, so moves need no index maintenance and the rows a deleted page leaves behind resolve to nothing.
Duplicate ids from cross-wiki transfer resolve to the lowest page id.

A wiki with no graph backend configured is now a supported mode. Query surfaces stay registered per backend;
relation-target suggestions still come from Neo4j and degrade to none without it.

Because identity now resolves authoritatively, an unresolvable Subject is refused rather than measured against the
wiki-global `edit` right. Creating a Subject authorizes against the page the request names, so it is unaffected;
deleting one the index cannot resolve answers 404, like the other endpoints keyed by Subject id.

`RebuildSubjectPageIndex.php` backfills the table and repairs it, registered to run from `update.php`. The decision is
recorded as ADR 32, added here.

## Worth a close look

- The rebuild sweeps with one unbatched `DELETE` and loads revisions a page at a time. Free on the initial backfill,
  not on a later repair run.
- `DELETE /subject/{id}` still has no per-page read gate, unlike its siblings. Pre-existing, and this PR changes the
  same method, so it is filed as #1312

## Considered, omitted

- **`ArticleMergeComplete` coverage** for a history merge that leaves the source page as a redirect — the one write
  neither the index nor the graph projection is told about. The rebuild script repairs both.
- **Subject Sources** (#993) beyond keying on the bare local nanoid.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JeroenDeDauw
JeroenDeDauw marked this pull request as ready for review August 20, 2026 22:12
@JeroenDeDauw

Copy link
Copy Markdown
Member Author

LGTM

Relation value autocomplete will not work (graceful degradation) on wikis without Neo4j. This is tracked by #1313. Considered solving it in this PR by adding a label and schema field to the table (cc suggested approach) and rejected due to outstanding design work around multilinguality.

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.

Make the subject-to-page lookup MediaWiki-native so Subjects work without a graph backend

1 participant