Found while pointing the test suite of graphio (a Python OGM / bulk loader originally written for Neo4j) at ArcadeDB. graphio creates indexes on merge-key properties and wipes the graph between tests — exactly the pattern that triggers this. Some tests fail silently against ArcadeDB, all tracing back to the issue below.
Summary
After a CREATE INDEX on a node property, the same DELETE + re-CREATE cycle that worked moments earlier starts:
- storing integer property values as strings (the literal
1 is read back as '1')
- returning empty results for property-equality lookups (
MATCH (n:Label {prop: 1}) and WHERE n.prop = 1 both return []), even though a label-only MATCH finds the node
The bug is not triggered by DELETE + re-CREATE alone — only after at least one CREATE INDEX has been run in the same database session/lifetime. The control sequence (no index) round-trips integer values correctly.
This affects any client that combines secondary indexes with cyclical data churn — for example, test suites that wipe the graph between tests (MATCH (n) DETACH DELETE n) and rely on property-equality MERGE/MATCH against indexed merge keys.
Reproduction
Self-contained Python script using the official Neo4j driver (Bolt). The only difference between Part 1 and Part 2 is a single CREATE INDEX line.
1. Start a fresh ArcadeDB container
The --rm + no-volume combo ensures a clean state. Studio UI at http://localhost:2480 (login root / testtest).
docker rm -f arcadedb-repro 2>/dev/null; \
docker run -d --rm --name arcadedb-repro \
-p 7687:7687 \
-p 2480:2480 \
-e JAVA_OPTS="-Darcadedb.server.rootPassword=testtest \
-Darcadedb.server.plugins=Bolt:com.arcadedb.bolt.BoltProtocolPlugin \
-Darcadedb.server.defaultDatabases=somedb[] \
-Darcadedb.bolt.defaultDatabase=somedb" \
arcadedata/arcadedb:latest && sleep 5
2. Install the driver and run the script
pip install neo4j
python arcadedb_repro.py
3. The script
from neo4j import GraphDatabase
driver = GraphDatabase.driver('bolt://localhost:7687', auth=('root', 'testtest'))
def run(cypher):
with driver.session() as session:
return [dict(r) for r in session.run(cypher)]
# =============================================================================
# PART 1 — Without CREATE INDEX (works correctly)
# =============================================================================
print('=== PART 1: WITHOUT CREATE INDEX ===\n')
run('MATCH (n) DETACH DELETE n')
run("CREATE (a:Person {uuid: 1})")
run('MATCH (n) DETACH DELETE n')
run("CREATE (a:Person {uuid: 1})")
print(' MATCH (n:Person) RETURN n.uuid ->',
run('MATCH (n:Person) RETURN n.uuid AS u'))
print(' MATCH (n:Person {uuid: 1}) RETURN n.uuid ->',
run('MATCH (n:Person {uuid: 1}) RETURN n.uuid AS u'))
print(' MATCH (n:Person) WHERE n.uuid = 1 RETURN n.uuid ->',
run('MATCH (n:Person) WHERE n.uuid = 1 RETURN n.uuid AS u'))
# =============================================================================
# PART 2 — Same sequence + a single CREATE INDEX (broken)
# =============================================================================
print('\n=== PART 2: SAME — but with one CREATE INDEX added ===\n')
run('MATCH (n) DETACH DELETE n')
run("CREATE (a:Person {uuid: 1})")
run('CREATE INDEX IF NOT EXISTS FOR (n:Person) ON (n.uuid)') # <-- only change
run('MATCH (n) DETACH DELETE n')
run("CREATE (a:Person {uuid: 1})")
print(' MATCH (n:Person) RETURN n.uuid ->',
run('MATCH (n:Person) RETURN n.uuid AS u'))
print(' MATCH (n:Person {uuid: 1}) RETURN n.uuid ->',
run('MATCH (n:Person {uuid: 1}) RETURN n.uuid AS u'))
print(' MATCH (n:Person) WHERE n.uuid = 1 RETURN n.uuid ->',
run('MATCH (n:Person) WHERE n.uuid = 1 RETURN n.uuid AS u'))
driver.close()
Expected vs. actual output
=== PART 1: WITHOUT CREATE INDEX ===
MATCH (n:Person) RETURN n.uuid -> [{'u': 1}]
MATCH (n:Person {uuid: 1}) RETURN n.uuid -> [{'u': 1}]
MATCH (n:Person) WHERE n.uuid = 1 RETURN n.uuid -> [{'u': 1}]
=== PART 2: SAME — but with one CREATE INDEX added ===
MATCH (n:Person) RETURN n.uuid -> [{'u': '1'}]
MATCH (n:Person {uuid: 1}) RETURN n.uuid -> []
MATCH (n:Person) WHERE n.uuid = 1 RETURN n.uuid -> []
Expected on every line: [{'u': 1}] (integer).
Actual in Part 2:
| Query |
Returned |
What's wrong |
MATCH (n:Person) RETURN n.uuid |
[{'u': '1'}] |
Integer literal 1 stored as the string '1'. |
MATCH (n:Person {uuid: 1}) RETURN ... |
[] |
Lookup fails — the node exists (count > 0), but the index doesn't find it. |
MATCH (n:Person) WHERE n.uuid = 1 RETURN ... |
[] |
Same — index-backed WHERE filter returns empty. |
Environment
|
|
| ArcadeDB |
26.6.1-SNAPSHOT (image arcadedata/arcadedb:latest) |
| Bolt plugin |
Neo4j-Bolt v4.0, TLS disabled |
| Client |
neo4j Python driver 5.x |
| Python |
3.12 |
| Host |
macOS arm64, Docker Desktop |
Related observations
These may be the same underlying issue, or two separate things — worth mentioning together because they compound in real applications:
-
Bolt write counters are always empty. result.consume().counters returns {} even for queries that clearly created/deleted records. This makes the bug above invisible to client code: rs.create() returns successfully, the script believes it wrote the data, and only a follow-up MATCH reveals that no data is reachable by property filter.
-
Inconsistent error vs. silent failure. In some repeated runs, the same UNWIND $rels AS rel MATCH (a:X), (b:Y) WHERE ... CREATE ... pattern raises Neo.DatabaseError.General.UnknownError; in others it returns success with no rows changed. Possibly related to the same index-state corruption.
Impact in the wild
I hit this while trying to point graphio — a Python OGM / bulk loader originally written for Neo4j — at ArcadeDB. graphio creates indexes on merge-key properties (standard practice), and its test suite calls MATCH (n) DETACH DELETE n between tests. With those two ingredients, some of graphio's tests fail silently against ArcadeDB — rs.create() reports success, but MATCH ... RETURN count(r) returns 0. The test failure pattern is what led me to the reproducer above.
Happy to test patches or provide additional debug output.
Found while pointing the test suite of graphio (a Python OGM / bulk loader originally written for Neo4j) at ArcadeDB. graphio creates indexes on merge-key properties and wipes the graph between tests — exactly the pattern that triggers this. Some tests fail silently against ArcadeDB, all tracing back to the issue below.
Summary
After a
CREATE INDEXon a node property, the sameDELETE+ re-CREATEcycle that worked moments earlier starts:1is read back as'1')MATCH (n:Label {prop: 1})andWHERE n.prop = 1both return[]), even though a label-onlyMATCHfinds the nodeThe bug is not triggered by
DELETE+ re-CREATEalone — only after at least oneCREATE INDEXhas been run in the same database session/lifetime. The control sequence (no index) round-trips integer values correctly.This affects any client that combines secondary indexes with cyclical data churn — for example, test suites that wipe the graph between tests (
MATCH (n) DETACH DELETE n) and rely on property-equalityMERGE/MATCHagainst indexed merge keys.Reproduction
Self-contained Python script using the official Neo4j driver (Bolt). The only difference between Part 1 and Part 2 is a single
CREATE INDEXline.1. Start a fresh ArcadeDB container
The
--rm+ no-volume combo ensures a clean state. Studio UI at http://localhost:2480 (loginroot/testtest).2. Install the driver and run the script
3. The script
Expected vs. actual output
Expected on every line:
[{'u': 1}](integer).Actual in Part 2:
MATCH (n:Person) RETURN n.uuid[{'u': '1'}]1stored as the string'1'.MATCH (n:Person {uuid: 1}) RETURN ...[]MATCH (n:Person) WHERE n.uuid = 1 RETURN ...[]WHEREfilter returns empty.Environment
26.6.1-SNAPSHOT(imagearcadedata/arcadedb:latest)neo4jPython driver 5.xRelated observations
These may be the same underlying issue, or two separate things — worth mentioning together because they compound in real applications:
Bolt write counters are always empty.
result.consume().countersreturns{}even for queries that clearly created/deleted records. This makes the bug above invisible to client code:rs.create()returns successfully, the script believes it wrote the data, and only a follow-upMATCHreveals that no data is reachable by property filter.Inconsistent error vs. silent failure. In some repeated runs, the same
UNWIND $rels AS rel MATCH (a:X), (b:Y) WHERE ... CREATE ...pattern raisesNeo.DatabaseError.General.UnknownError; in others it returns success with no rows changed. Possibly related to the same index-state corruption.Impact in the wild
I hit this while trying to point graphio — a Python OGM / bulk loader originally written for Neo4j — at ArcadeDB. graphio creates indexes on merge-key properties (standard practice), and its test suite calls
MATCH (n) DETACH DELETE nbetween tests. With those two ingredients, some of graphio's tests fail silently against ArcadeDB —rs.create()reports success, butMATCH ... RETURN count(r)returns 0. The test failure pattern is what led me to the reproducer above.Happy to test patches or provide additional debug output.