Skip to content

Bolt: integer property values silently coerced to strings after CREATE INDEX, and index-backed property lookups return empty #4222

Description

@mpreusse

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:

  1. 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.

  2. 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.

Activity

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions