A Replayed Import Must Not Create Another Relationship
Stage graph imports by stable identity, distinguish exact replays from conflicts and preserve rejected records instead of silently losing relationships.
13 sections · 4 questions · 1 coding challenge
The arrow keys move between sections too.
Bulk Data Exchange Had to Survive Partial Progress
BOTIE
The operational problem is older than any one loader: a response can be lost after work commits. Stable source identities and a recorded checkpoint make replay a question of recognising prior work rather than assuming every arrival is new.
Sources for this history
The Loader Lost Its Response and Tried Again
Botie imports a dependency file, but the connection drops before the loader receives a success response. Retrying every row with CREATE can add another copy of relationships already committed. Some rows also refer to assets that have not been imported.
We need stable identities for imported relationships and an explicit route for records that cannot be applied. An import is a reconciliation process, not simply a loop that draws every edge it sees.
Invented maintenance records. Python exercises and workshop components expose the mechanism locally. Source-linked Cypher or SPARQL examples require the stated external database setup.
Step 1 of 1
One new edge is resolvable and another has no endpoint
The source batch contains an already known relationship and an unresolved asset reference.
Nodes and properties (4)
- P7 · Known asset
- S1 · Known supply
- P8 · Known asset
- missing · Unresolved source ID
- P7 to S1: a: existing
- P8 to S1: new: ready
- P7 to missing: lost: quarantine
Stage new resolvable relationships
Classification separates new resolvable relationships from replays and rejected rows. The admission list names staged work while the intermediate audit retains reasons for omission.
Stage new resolvable relationships
Retries, changed reused IDs and missing endpoints require different outcomes.
BOTIE
Result for these records
- 0new
Follow the operations
1. Stored graph snapshot
Nodes and properties (3)
- P7 · Asset
- S1 · Asset
- P8 · Asset
- P7 to S1: DEPENDS_ON
Inspect the node and relationship records
{ "nodes": [ { "id": "P7", "kind": "Asset" }, { "id": "S1", "kind": "Asset" }, { "id": "P8", "kind": "Asset" } ], "edges": [ { "id": "a", "source": "P7", "target": "S1", "type": "DEPENDS_ON" } ] }2. Incoming relationship records
[ { "id": "a", "source": "P7", "target": "S1", "type": "DEPENDS_ON" }, { "id": "new", "source": "P8", "target": "S1", "type": "DEPENDS_ON" }, { "id": "lost", "source": "P7", "target": "missing", "type": "DEPENDS_ON" }, { "id": "a", "source": "P7", "target": "P8", "type": "DEPENDS_ON" } ]3. Classify relationship import rows
[ { "id": "a", "source": "P7", "target": "S1", "type": "DEPENDS_ON", "status": "repeat" }, { "id": "new", "source": "P8", "target": "S1", "type": "DEPENDS_ON", "status": "ready" }, { "id": "lost", "source": "P7", "target": "missing", "type": "DEPENDS_ON", "status": "dangling" }, { "id": "a", "source": "P7", "target": "P8", "type": "DEPENDS_ON", "status": "conflict" } ]4. Text comparison value
ready
5. Keep rows passing a comparison
[ { "id": "new", "source": "P8", "target": "S1", "type": "DEPENDS_ON", "status": "ready" } ]6. Collect distinct identifiers
- 0new
7. Result
- 0new
These components operate on the supplied records in your browser. They do not run a graph database or interpret Cypher.
1. Load or resolve entity identities before connecting them
A relationship requires both endpoints. Import node identities first or resolve them against a known registry. An unmatched endpoint should enter a rejection or deferred-resolution queue with its source row identity.
Silently skipping a MATCH failure can make a loader appear successful while losing relationships. Inventing missing nodes with MERGE can be equally misleading when an ID is misspelled.
Choose whether unresolved endpoints are rejected, deferred or explicitly created; do not let query structure choose by accident.
2. Give each imported relationship a replay identity
A stable source relationship ID can identify a logical record across retries. The same ID with the same complete payload is a replay. The same ID with different endpoints, type or properties is a conflict under this lesson’s immutable-import policy.
If the source supports revisions, add a documented version rule rather than treating every changed payload as a harmless repeat. A relationship’s minutes or observation time can matter as much as its endpoints.
Identity equality and payload equality are separate checks.
Notice a property conflict on a reused ID
The endpoints are identical, but the stored payload is different.
old={'id':'r1','source':'A','target':'B','type':'ROUTE','minutes':4}
new={**old,'minutes':9}
print(old==new)FalseA replay check that compares endpoints alone would conceal the changed travel-time fact.
3. Use a parameterised batch to expose the import operation
UNWIND turns a bound list parameter into rows. Matching both endpoint nodes resolves the relationship’s meaning before MERGE uses its stable ID. The example deliberately uses one approved relationship type rather than accepting arbitrary user query structure.
The executor must validate unique IDs and detect changed payload conflicts before this write. It must also compare processed-row counts with the staged batch so unmatched endpoints do not disappear unnoticed.
A successful query response does not prove every source row was imported.
Run in your own environment
Apply a prevalidated dependency batch
Run this only after rejecting unresolved endpoints and reused-ID conflicts.
Use a disposable current Neo4j database supporting relationship property uniqueness constraints. Run CREATE CONSTRAINT import_asset_id IF NOT EXISTS FOR (a:Asset) REQUIRE a.id IS UNIQUE. Then run CREATE CONSTRAINT dependency_import_id IF NOT EXISTS FOR ()-[r:DEPENDS_ON]-() REQUIRE r.import_id IS UNIQUE. These uniqueness constraints do not require property presence; the importer validates it.
Create the complete stored fixture: CREATE (p:Asset {id:'P7'}),(q:Asset {id:'P8'}),(s:Asset {id:'S1'}),(p)-[:DEPENDS_ON {import_id:'a'}]->(s).
Bind rows to [{id:'a',source:'P7',target:'S1'},{id:'new',source:'P8',target:'S1'}] through the driver. This particular write schema has only immutable endpoint identities and the fixed DEPENDS_ON type; reject additional fields rather than silently ignoring payload properties.
Run in a transaction after source and endpoint validation. The first row matches the stored relationship and the second creates one. Running the same batch again should match both. A reused import ID on different endpoints must fail the uniqueness constraint and enter conflict handling. Reconcile returned IDs against the staged input so an unmatched endpoint is not silently lost.
UNWIND $rows AS row
MATCH (a:Asset {id:row.source}), (b:Asset {id:row.target})
MERGE (a)-[r:DEPENDS_ON {import_id:row.id}]->(b)
RETURN row.id AS imported ORDER BY importedThe two import identities are returned on the first and repeated run. Endpoint uniqueness and relationship import-ID uniqueness back this deliberately narrow immutable schema. A richer mutable payload requires its own version/conflict protocol; the workshop demonstrates full-payload classification without committing a database write.
Official documentation. Documentation checked .
Check yourself
Questions on Parts 1 to 3
Question 1 of 2
Under the immutable-import policy, what is this?
Use the information below.
Stored edge r1 has A→B with minutes 4. The batch reuses r1 with A→B and minutes 9.4. Make batch atomicity and checkpoints explicit
A large import may commit in batches to control memory and transaction size. If batch three fails, batches one and two may already be durable. The loader must resume from a checkpoint that corresponds to committed work, not merely rows read from the file.
Record batch IDs, source versions, accepted counts and rejection counts. Keep checkpoints and sink updates consistent through an appropriate transaction or recovery protocol.
Batching reduces the rollback unit; it does not make the whole source file one atomic transaction.
5. Validate properties before they become graph facts
CSV text needs explicit type conversion and missing-value rules. A blank date is not an arbitrary default instant, and an invalid cost must not enter a weighted path algorithm. Parameter binding protects query structure but does not validate the values’ domain meaning.
Reject or quarantine malformed records with enough non-sensitive context to repair them. Keep credentials, personal data and restricted payloads out of casual loader logs.
Import quality directly changes the answers later traversals can produce.
6. Distinguish the admission list from the import audit
The exercise returns IDs of new resolvable edges only. The workshop exposes a richer classification before selecting ready rows: repeat, conflict, dangling and invalid records remain inspectable in the intermediate output.
Both mechanisms preserve the original graph. They stage work for a real importer rather than pretending a browser result has committed anything.
Inspect the classification component when an expected ID does not reach the ready list.
Check yourself
Questions on Parts 1 to 6
Question 1 of 2
What should a retry assume?
Use the information below.
Two batches committed and a third failed.Try it with Botie
Build with Botie
Write and run Python