Commit a365910
Create Neo4j uniqueness constraints during graph rebuild (#1083)
* Create Neo4j uniqueness constraints during graph rebuild
Fixes #874
Neo4jConstraintUpdater::createDefaultConstraints() could create the Page (wiki_id, id)
and Subject.id uniqueness constraints but was only ever called from tests, so a real
install ran Neo4j without them: duplicate-id nodes were possible and id lookups were
unindexed.
Wire constraint creation into RebuildGraphDatabases.php -- the production path that
(re)builds the graph from the MediaWiki source of truth -- via a new
NeoWikiExtension::createGraphDatabaseConstraints() that no-ops when no Neo4j backend is
configured (e.g. a SPARQL-only install) and is idempotent (CREATE CONSTRAINT ... IF NOT
EXISTS). Creating the constraints before re-projecting means a rebuilt graph always
carries them, and an unchanged re-save still succeeds because the projection MERGEs
nodes by id. The graph-model.md claim that the constraints are "not created
automatically yet" is corrected.
The rebuild is the simplest reliable point: it is already the canonical way to establish
the graph, it runs with every backend resolved, and its idempotency makes repeated runs
safe. A LoadExtensionSchemaUpdates hook was considered but rejected -- NeoWiki registers
no SQL schema updater today, and coupling external-Neo4j reachability to update.php is
fragile.
Relation (edge) ID uniqueness (#351) is not implemented here. Neo4j relationship
uniqueness constraints are per relationship type (on this stack, Neo4j 2026.05.0
Enterprise, a type-less "FOR ()-[r]->() REQUIRE r.id IS UNIQUE" is a syntax error), and
Relations use an open, user-defined set of relationship types, so no single constraint
can enforce global Relation-id uniqueness. A hard pre-save rejection would need a graph
read in the deliberately graph-free validation path, which is out of scope here; analysis
is posted on issue #351.
Tests:
- RebuildGraphDatabasesTest::testRebuildCreatesGraphUniquenessConstraints -- after the
rebuild runs, SHOW CONSTRAINTS reports both node constraints; a subject page is seeded
so the rebuild re-projects real data under the freshly-created constraints.
- RebuildGraphDatabasesTest::testEnsuringConstraintsIsSkippedWhenNeo4jIsNotConfigured --
the guard keeps the rebuild safe on a wiki with no Neo4j backend.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Correct graph-model.md constraint-creation claim
The Constraints section implied the uniqueness constraints are always present and that the rebuild is the path that builds the graph. They are in fact created only by running RebuildGraphDatabases.php; the incremental projection on page edits does not create them, so an existing graph gains them only after a rebuild. State this honestly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Trim RebuildGraphDatabasesTest to the constraint-wiring fact
The test re-asserted Neo4jConstraintUpdaterTest's full constraint shape byte-for-byte, so a constraint-shape change would break two tests. Assert only that the rebuild created both named constraints (the name embeds the properties); their shape and enforcement stay owned by Neo4jConstraintUpdaterTest. Also soften the comment (a single subject cannot collide, so the test does not prove non-violation) and add the missing @Covers for the extension method the rebuild exercises.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Route graph store initialization through the GraphDatabasePlugin system
Constraint creation on rebuild ran through a Neo4j-specific
NeoWikiExtension::createGraphDatabaseConstraints() facade method that null-checked
the Neo4j plugin and built a Neo4jConstraintUpdater itself. That special-cased one
backend on the facade, outside the GraphDatabasePlugin seam every other rebuild step
already goes through.
Add initialize() to the GraphDatabasePlugin interface: a store prepares its backing
store for projection there. Neo4jProjectionStore creates its uniqueness constraints
(via a Neo4jConstraintUpdater the Neo4j composition root injects from the write engine
it already owns); SparqlProjectionStore is a no-op (no store-level structures to
create). RebuildGraphDatabases now calls getGraphDatabasePlugin()->initialize() -- the
propagating composite over every configured backend -- before re-projecting, so each
backend initializes through the same seam it uses for savePage/deletePage, and a
no-backend install is just an empty fan-out.
initialize() propagates on the rebuild path like savePage/deletePage;
FailureIsolatingGraphDatabasePlugin (hook path only, which never initializes) passes it
straight through. The facade method and its Neo4jConstraintUpdater import are removed.
Behavior is unchanged: the rebuild creates the same two Neo4j constraints, idempotently,
and the incremental per-edit path still does not. extending.md documents the new
interface method for extension authors; graph-model.md is unaffected.
Tests:
- testRebuildCreatesGraphUniquenessConstraints stays as the end-to-end proof the rebuild
creates both Neo4j constraints, now through the plugin system (verified failing when
Neo4jProjectionStore::initialize is stubbed to a no-op).
- testEnsuringConstraintsIsSkippedWhenNeo4jIsNotConfigured becomes
testInitializingGraphDatabasesDoesNotThrowWithoutABackend: with no backend configured,
the rebuild's initialization step (the composite resolved via the facade) is a no-op.
- CompositeGraphDatabasePlugin and FailureIsolatingGraphDatabasePlugin gain initialize()
fan-out / delegation and failure-propagation coverage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>1 parent 58650e0 commit a365910
16 files changed
Lines changed: 173 additions & 5 deletions
File tree
- docs
- api
- extending
- maintenance
- src
- Domain/GraphDatabase
- GraphDatabasePlugins
- Neo4j
- Persistence
- Sparql/Persistence
- tests
- RedHerb/src
- phpunit
- Domain/GraphDatabase
- GraphDatabasePlugins/Neo4j/Persistence
- Maintenance
- TestDoubles
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
107 | 107 | | |
108 | 108 | | |
109 | 109 | | |
110 | | - | |
111 | | - | |
112 | | - | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
113 | 120 | | |
114 | 121 | | |
115 | 122 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
151 | 151 | | |
152 | 152 | | |
153 | 153 | | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
154 | 158 | | |
155 | 159 | | |
156 | 160 | | |
| |||
168 | 172 | | |
169 | 173 | | |
170 | 174 | | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
171 | 179 | | |
172 | 180 | | |
173 | 181 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
| 36 | + | |
| 37 | + | |
36 | 38 | | |
37 | 39 | | |
38 | 40 | | |
| |||
52 | 54 | | |
53 | 55 | | |
54 | 56 | | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
55 | 68 | | |
56 | 69 | | |
57 | 70 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
30 | 36 | | |
31 | 37 | | |
32 | 38 | | |
| |||
Lines changed: 9 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
45 | 54 | | |
46 | 55 | | |
47 | 56 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
21 | 23 | | |
22 | 24 | | |
23 | 25 | | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
24 | 34 | | |
25 | 35 | | |
26 | 36 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
| |||
42 | 43 | | |
43 | 44 | | |
44 | 45 | | |
| 46 | + | |
45 | 47 | | |
46 | 48 | | |
47 | 49 | | |
| |||
50 | 52 | | |
51 | 53 | | |
52 | 54 | | |
| 55 | + | |
| 56 | + | |
53 | 57 | | |
54 | 58 | | |
55 | 59 | | |
56 | | - | |
57 | 60 | | |
58 | 61 | | |
59 | 62 | | |
| |||
Lines changed: 5 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
24 | 25 | | |
25 | 26 | | |
26 | 27 | | |
27 | 28 | | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
28 | 33 | | |
29 | 34 | | |
30 | 35 | | |
| |||
Lines changed: 5 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
48 | 53 | | |
49 | 54 | | |
50 | 55 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
24 | 29 | | |
25 | 30 | | |
26 | 31 | | |
| |||
0 commit comments