Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions bolt/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@
<name>ArcadeDB BOLT Protocol</name>
<description>Neo4j BOLT wire protocol implementation for ArcadeDB</description>

<properties>
<neo4j-driver.version>5.27.0</neo4j-driver.version>
</properties>

<dependencies>
<dependency>
<groupId>com.arcadedb</groupId>
Expand Down
35 changes: 30 additions & 5 deletions bolt/src/main/java/com/arcadedb/bolt/BoltNetworkExecutor.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ public class BoltNetworkExecutor extends Thread {
private static final byte[] BOLT_MAGIC = { 0x60, 0x60, (byte) 0xB0, 0x17 };

// Supported protocol versions (in order of preference)
private static final int[] SUPPORTED_VERSIONS = { 0x00000104, 0x00000004, 0x00000003 }; // v4.4, v4.0, v3.0
// Encoding: [unused(8)][range(8)][minor(8)][major(8)] — major = value & 0xFF, minor = (value >> 8) & 0xFF
private static final int[] SUPPORTED_VERSIONS = { 0x00000404, 0x00000004, 0x00000003 }; // v4.4, v4.0, v3.0

// Server states
private enum State {
Expand Down Expand Up @@ -205,13 +206,23 @@ private boolean performHandshake() throws IOException {
Arrays.toString(Arrays.stream(clientVersions).mapToObj(v -> String.format("0x%08X", v)).toArray()));
}

// Select best matching version
// Select best matching version using Bolt version negotiation with range support.
// The range means the client supports minor versions from (minor - range) up to minor
// (inclusive) for the given major version. Zero entries are trailing padding per the Bolt spec.
protocolVersion = 0;
for (final int clientVersion : clientVersions) {
if (clientVersion == 0)
break;

final int clientMajor = getMajorVersion(clientVersion);
final int clientMinor = getMinorVersion(clientVersion);
final int clientRange = getVersionRange(clientVersion);

for (final int supportedVersion : SUPPORTED_VERSIONS) {
// Check major version match (upper 16 bits for BOLT 4.x)
if (clientVersion == supportedVersion ||
(clientVersion >> 8) == (supportedVersion >> 8)) {
final int serverMajor = getMajorVersion(supportedVersion);
final int serverMinor = getMinorVersion(supportedVersion);

if (clientMajor == serverMajor && serverMinor <= clientMinor && serverMinor >= clientMinor - clientRange) {
protocolVersion = supportedVersion;
break;
}
Expand Down Expand Up @@ -1009,4 +1020,18 @@ private void cleanup() {
LogManager.instance().log(this, Level.INFO, "BOLT connection closed");
}
}

// Bolt version encoding: [unused(8)][range(8)][minor(8)][major(8)]

static int getMajorVersion(final int version) {
return version & 0xFF;
}

static int getMinorVersion(final int version) {
return (version >> 8) & 0xFF;
}

static int getVersionRange(final int version) {
return (version >> 16) & 0xFF;
}
}
4 changes: 2 additions & 2 deletions bolt/src/test/java/com/arcadedb/bolt/BoltChunkedIOTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,13 @@ void writeRawInt() throws IOException {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final BoltChunkedOutput output = new BoltChunkedOutput(baos);

output.writeRawInt(0x00000104); // BOLT version 4.4
output.writeRawInt(0x00000404); // BOLT version 4.4

final byte[] result = baos.toByteArray();
assertThat(result).hasSize(4);
assertThat(result[0]).isEqualTo((byte) 0x00);
assertThat(result[1]).isEqualTo((byte) 0x00);
assertThat(result[2]).isEqualTo((byte) 0x01);
assertThat(result[2]).isEqualTo((byte) 0x04);
assertThat(result[3]).isEqualTo((byte) 0x04);
}

Expand Down
68 changes: 35 additions & 33 deletions bolt/src/test/java/com/arcadedb/bolt/BoltProtocolIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
Expand Down Expand Up @@ -580,12 +579,11 @@ void queryWithSkipAndLimit() {
}

// Query with SKIP and LIMIT
Result result = session.run("MATCH (n:SkipTest) RETURN n.idx AS idx ORDER BY n.idx SKIP 5 LIMIT 5");
List<Record> records = session.run("MATCH (n:SkipTest) RETURN n.idx AS idx ORDER BY n.idx SKIP 5 LIMIT 5").list();
assertThat(records).hasSize(5);
List<Long> indices = new ArrayList<>();
while (result.hasNext()) {
indices.add(result.next().get("idx").asLong());
}
assertThat(indices).hasSize(5);
for (final Record r : records)
indices.add(r.get("idx").asLong());
assertThat(indices).containsExactly(5L, 6L, 7L, 8L, 9L);
}
}
Expand All @@ -596,13 +594,14 @@ void nodeWithMultipleProperties() {
try (Driver driver = getDriver()) {
try (Session session = driver.session(SessionConfig.forDatabase(getDatabaseName()))) {
// ArcadeDB auto-creates types
session.run("CREATE (n:MultiProp {" +
"stringProp: 'hello', " +
"intProp: 42, " +
"floatProp: 3.14, " +
"boolProp: true, " +
"listProp: [1, 2, 3]" +
"})");
session.run("""
CREATE (n:MultiProp {
stringProp: 'hello',
intProp: 42,
floatProp: 3.14,
boolProp: true,
listProp: [1, 2, 3]
})""");

Result result = session.run("MATCH (n:MultiProp) RETURN n");
assertThat(result.hasNext()).isTrue();
Expand Down Expand Up @@ -894,31 +893,34 @@ void connectionPoolingWithManyQueries() {

@Test
void transactionIsolation() {
// Test that uncommitted changes in one transaction are not visible to another
// Test that uncommitted (rolled-back) changes never become visible, and committed ones do.
try (Driver driver = getDriver()) {
// Session 1: Start transaction but don't commit
try (Session session1 = driver.session(SessionConfig.forDatabase(getDatabaseName()))) {
try (Transaction tx1 = session1.beginTransaction()) {
tx1.run("CREATE (n:IsolationTest {marker: 'uncommitted'})");

// Session 2: Query while session 1 transaction is open
try (Session session2 = driver.session(SessionConfig.forDatabase(getDatabaseName()))) {
Result result = session2.run("MATCH (n:IsolationTest {marker: 'uncommitted'}) RETURN count(n) AS cnt");
long count = result.next().get("cnt").asLong();
// Should not see uncommitted data
assertThat(count).isEqualTo(0L);
}
try (Session session = driver.session(SessionConfig.forDatabase(getDatabaseName()))) {
// Ensure the type exists so rollback test doesn't depend on schema auto-creation
session.run("CREATE (n:IsolationTest {marker: 'setup'})").consume();

final long countBefore = session.run("MATCH (n:IsolationTest) RETURN count(n) AS cnt")
.next().get("cnt").asLong();

// Now commit
tx1.commit();
// Create data in a transaction, then rollback — it should not be visible
try (Transaction tx = session.beginTransaction()) {
tx.run("CREATE (n:IsolationTest {marker: 'rolled_back'})");
tx.rollback();
}

// Now session 2 should see it
try (Session session2 = driver.session(SessionConfig.forDatabase(getDatabaseName()))) {
Result result = session2.run("MATCH (n:IsolationTest {marker: 'uncommitted'}) RETURN count(n) AS cnt");
long count = result.next().get("cnt").asLong();
assertThat(count).isGreaterThanOrEqualTo(1L);
final long countAfterRollback = session.run("MATCH (n:IsolationTest) RETURN count(n) AS cnt")
.next().get("cnt").asLong();
assertThat(countAfterRollback).isEqualTo(countBefore);

// Create data in a transaction, then commit — it should be visible
try (Transaction tx = session.beginTransaction()) {
tx.run("CREATE (n:IsolationTest {marker: 'committed'})");
tx.commit();
}

final long countAfterCommit = session.run("MATCH (n:IsolationTest) RETURN count(n) AS cnt")
.next().get("cnt").asLong();
assertThat(countAfterCommit).isEqualTo(countBefore + 1);
}
}
}
Expand Down
197 changes: 197 additions & 0 deletions bolt/src/test/java/com/arcadedb/bolt/BoltVersionNegotiationTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
* Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-FileCopyrightText: 2021-present Arcade Data Ltd (info@arcadedata.com)
* SPDX-License-Identifier: Apache-2.0
*/
package com.arcadedb.bolt;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Unit tests for Bolt protocol version encoding helpers and negotiation logic.
*/
class BoltVersionNegotiationTest {

// ============ Version encoding helper tests ============

@Test
void getMajorVersion() {
assertThat(BoltNetworkExecutor.getMajorVersion(0x00000404)).isEqualTo(4); // v4.4
assertThat(BoltNetworkExecutor.getMajorVersion(0x00000004)).isEqualTo(4); // v4.0
assertThat(BoltNetworkExecutor.getMajorVersion(0x00000003)).isEqualTo(3); // v3.0
assertThat(BoltNetworkExecutor.getMajorVersion(0x00000405)).isEqualTo(5); // v5.4
assertThat(BoltNetworkExecutor.getMajorVersion(0x00000000)).isEqualTo(0); // padding
}

@Test
void getMinorVersion() {
assertThat(BoltNetworkExecutor.getMinorVersion(0x00000404)).isEqualTo(4); // v4.4
assertThat(BoltNetworkExecutor.getMinorVersion(0x00000004)).isEqualTo(0); // v4.0
assertThat(BoltNetworkExecutor.getMinorVersion(0x00000003)).isEqualTo(0); // v3.0
assertThat(BoltNetworkExecutor.getMinorVersion(0x00000405)).isEqualTo(4); // v5.4
}

@Test
void getVersionRange() {
assertThat(BoltNetworkExecutor.getVersionRange(0x00020404)).isEqualTo(2); // v4.4 range=2
assertThat(BoltNetworkExecutor.getVersionRange(0x00000404)).isEqualTo(0); // v4.4 no range
assertThat(BoltNetworkExecutor.getVersionRange(0x00030405)).isEqualTo(3); // v5.4 range=3
}

@Test
void versionEncodingRoundTrip() {
// Verify that encoding major=4, minor=4 yields the expected constant
final int version = (4 << 8) | 4;
assertThat(version).isEqualTo(0x00000404);
assertThat(BoltNetworkExecutor.getMajorVersion(version)).isEqualTo(4);
assertThat(BoltNetworkExecutor.getMinorVersion(version)).isEqualTo(4);
assertThat(BoltNetworkExecutor.getVersionRange(version)).isEqualTo(0);
}

// ============ Version negotiation tests ============
// These test the negotiation algorithm by simulating what performHandshake() does
// with various client version proposals against the server's SUPPORTED_VERSIONS.

private static final int[] SUPPORTED_VERSIONS = { 0x00000404, 0x00000004, 0x00000003 }; // v4.4, v4.0, v3.0

/**
* Simulates the version negotiation logic from BoltNetworkExecutor.performHandshake().
*/
private static int negotiate(final int[] clientVersions) {
for (final int clientVersion : clientVersions) {
if (clientVersion == 0)
break;

final int clientMajor = BoltNetworkExecutor.getMajorVersion(clientVersion);
final int clientMinor = BoltNetworkExecutor.getMinorVersion(clientVersion);
final int clientRange = BoltNetworkExecutor.getVersionRange(clientVersion);

for (final int supportedVersion : SUPPORTED_VERSIONS) {
final int serverMajor = BoltNetworkExecutor.getMajorVersion(supportedVersion);
final int serverMinor = BoltNetworkExecutor.getMinorVersion(supportedVersion);

if (clientMajor == serverMajor && serverMinor <= clientMinor && serverMinor >= clientMinor - clientRange) {
return supportedVersion;
}
}
}
return 0;
}

@Test
void exactVersionMatch() {
// Client proposes exactly v4.4
final int result = negotiate(new int[] { 0x00000404, 0, 0, 0 });
assertThat(result).isEqualTo(0x00000404);
}

@Test
void exactMatchV4_0() {
final int result = negotiate(new int[] { 0x00000004, 0, 0, 0 });
assertThat(result).isEqualTo(0x00000004);
}

@Test
void exactMatchV3_0() {
final int result = negotiate(new int[] { 0x00000003, 0, 0, 0 });
assertThat(result).isEqualTo(0x00000003);
}

@Test
void rangeMatchClientSupports4_2through4_4() {
// Client proposes v4.4 with range=2 (supports 4.2, 4.3, 4.4)
final int result = negotiate(new int[] { 0x00020404, 0, 0, 0 });
assertThat(result).isEqualTo(0x00000404); // server's v4.4 falls in range
}

@Test
void rangeMatchClientSupports4_1through4_4() {
// Client proposes v4.4 with range=3 (supports 4.1, 4.2, 4.3, 4.4)
final int result = negotiate(new int[] { 0x00030404, 0, 0, 0 });
assertThat(result).isEqualTo(0x00000404);
}

@Test
void rangeMatchFallsBackToV4_0() {
// Client proposes v4.2 with range=2 (supports 4.0, 4.1, 4.2)
// Server has v4.4 (too high) but also v4.0 (in range)
final int result = negotiate(new int[] { 0x00020204, 0, 0, 0 });
assertThat(result).isEqualTo(0x00000004); // v4.0
}

@Test
void serverVersionOutsideClientRange() {
// Client proposes v4.3 with range=0 (only 4.3 exactly)
// Server supports v4.4 and v4.0, neither is 4.3
final int result = negotiate(new int[] { 0x00000304, 0, 0, 0 });
assertThat(result).isEqualTo(0);
}

@Test
void noMatchUnsupportedMajorVersion() {
// Client only supports Bolt v5.x
final int result = negotiate(new int[] { 0x00020405, 0x00000005, 0, 0 });
assertThat(result).isEqualTo(0);
}

@Test
void neo4j5xDriverTypicalHandshake() {
// Simulate a modern Neo4j 5.x driver proposing:
// v5.4 range=2 (5.2-5.4), v5.1 range=1 (5.0-5.1), v4.4 range=1 (4.3-4.4), v4.2 exact
final int result = negotiate(new int[] { 0x00020405, 0x00010105, 0x00010404, 0x00000204 });
// Server doesn't support 5.x, should negotiate to v4.4
assertThat(result).isEqualTo(0x00000404);
}

@Test
void neo4j4xDriverTypicalHandshake() {
// Simulate an older Neo4j 4.x driver proposing:
// v4.4, v4.0, v3.0, padding
final int result = negotiate(new int[] { 0x00000404, 0x00000004, 0x00000003, 0 });
assertThat(result).isEqualTo(0x00000404);
}

@Test
void clientPrefersHigherVersionFirst() {
// Client proposes v5.0 first then v4.4 — should pick v4.4 (first match)
final int result = negotiate(new int[] { 0x00000005, 0x00000404, 0, 0 });
assertThat(result).isEqualTo(0x00000404);
}

@Test
void allZeroPaddingReturnsNoMatch() {
final int result = negotiate(new int[] { 0, 0, 0, 0 });
assertThat(result).isEqualTo(0);
}

@Test
void zeroAfterValidVersionStopsProcessing() {
// First entry is unsupported v5.0, second is zero (padding), third would match v4.4
// but should stop at zero per Bolt spec
final int result = negotiate(new int[] { 0x00000005, 0, 0x00000404, 0 });
assertThat(result).isEqualTo(0);
}

@Test
void clientRangeCoversMultipleServerVersions() {
// Client proposes v4.4 with range=4 (supports 4.0 through 4.4)
// Server's first supported version is v4.4, which should be picked (highest preference)
final int result = negotiate(new int[] { 0x00040404, 0, 0, 0 });
assertThat(result).isEqualTo(0x00000404);
}
}
1 change: 0 additions & 1 deletion e2e/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
<logback-classic.version>1.5.28</logback-classic.version>
<allowIncompleteProjects>true</allowIncompleteProjects>
<assertj-db.version>3.0.1</assertj-db.version>
<neo4j-driver.version>5.27.0</neo4j-driver.version>
</properties>

<artifactId>arcadedb-e2e</artifactId>
Expand Down
Loading
Loading