From ef1cf2cf7d42eb4a84a1593e7cff828f48fa0ba7 Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Tue, 5 Aug 2025 14:10:15 +0200 Subject: [PATCH 1/8] new bolt version 6. wip. --- src/packstream/v1/Packer.php | 5 +- src/packstream/v1/Unpacker.php | 6 +- src/protocol/V6.php | 32 ++++++ src/protocol/v6/AvailableStructures.php | 63 +++++++++++ src/protocol/v6/structures/Vector.php | 144 ++++++++++++++++++++++++ tests/TestLayer.php | 2 + tests/protocol/V6Test.php | 22 ++++ tests/structures/V6/StructuresTest.php | 63 +++++++++++ tests/structures/v5/StructuresTest.php | 6 - 9 files changed, 331 insertions(+), 12 deletions(-) create mode 100644 src/protocol/V6.php create mode 100644 src/protocol/v6/AvailableStructures.php create mode 100644 src/protocol/v6/structures/Vector.php create mode 100644 tests/protocol/V6Test.php create mode 100644 tests/structures/V6/StructuresTest.php diff --git a/src/packstream/v1/Packer.php b/src/packstream/v1/Packer.php index 4318c1a..39a9d95 100644 --- a/src/packstream/v1/Packer.php +++ b/src/packstream/v1/Packer.php @@ -131,8 +131,7 @@ private function packString(string $str): iterable private function packFloat(float $value): iterable { - $packed = pack('d', $value); - yield chr(0xC1) . ($this->littleEndian ? strrev($packed) : $packed); + yield chr(0xC1) . pack('E', $value); } /** @@ -233,7 +232,7 @@ private function packStructure(IStructure $structure): iterable } $packerMethod = 'pack' . ucfirst($packerMethod); - yield from [$this, $packerMethod]($structure->{$parameter->getName()}); + yield from call_user_func([$this, $packerMethod], $structure->{$parameter->getName()}); } } diff --git a/src/packstream/v1/Unpacker.php b/src/packstream/v1/Unpacker.php index 7468cbd..39369b1 100644 --- a/src/packstream/v1/Unpacker.php +++ b/src/packstream/v1/Unpacker.php @@ -103,7 +103,7 @@ private function u(): mixed if ($output !== null) { return $output; } - $output = $this->unpackStruct($marker); + $output = $this->unpackStructure($marker); if ($output !== null) { return $output; } @@ -120,7 +120,7 @@ private function u(): mixed * @return array|IStructure|null * @throws UnpackException */ - private function unpackStruct(int $marker): array|IStructure|null + private function unpackStructure(int $marker): array|IStructure|null { if ($marker >> 4 == 0b1011) { //TINY_STRUCT $size = 0b10110000 ^ $marker; @@ -238,7 +238,7 @@ private function unpackFloat(int $marker): ?float { if ($marker == 0xC1) { $value = $this->next(8); - return (float)unpack('d', $this->littleEndian ? strrev($value) : $value)[1]; + return (float)unpack('E', $value)[1]; } else { return null; } diff --git a/src/protocol/V6.php b/src/protocol/V6.php new file mode 100644 index 0000000..39adbb8 --- /dev/null +++ b/src/protocol/V6.php @@ -0,0 +1,32 @@ + Date::class, + 0x54 => Time::class, + 0x74 => LocalTime::class, + 0x49 => DateTime::class, + 0x69 => DateTimeZoneId::class, + 0x64 => LocalDateTime::class, + 0x45 => Duration::class, + 0x58 => Point2D::class, + 0x59 => Point3D::class, + 0x56 => Vector::class, + ]; + + protected array $unpackStructuresLt = [ + 0x4E => Node::class, + 0x52 => Relationship::class, + 0x72 => UnboundRelationship::class, + 0x50 => Path::class, + 0x44 => Date::class, + 0x54 => Time::class, + 0x74 => LocalTime::class, + 0x49 => DateTime::class, + 0x69 => DateTimeZoneId::class, + 0x64 => LocalDateTime::class, + 0x45 => Duration::class, + 0x58 => Point2D::class, + 0x59 => Point3D::class, + 0x56 => Vector::class, + ]; +} diff --git a/src/protocol/v6/structures/Vector.php b/src/protocol/v6/structures/Vector.php new file mode 100644 index 0000000..58e11ef --- /dev/null +++ b/src/protocol/v6/structures/Vector.php @@ -0,0 +1,144 @@ +type_marker, (string)$this->data]); + } + + private static array $formats = ['s', 'l', 'q']; + + /** + * Encode array as vector structure + * @param int[]|float[] $data + * @return self + * @throws \InvalidArgumentException + */ + public static function encode(array $data): self + { + if (count($data) === 0) { + throw new \InvalidArgumentException('Vector cannot be empty'); + } + if (count($data) > 4096) { + throw new \InvalidArgumentException('Vector cannot have more than 4096 elements'); + } + + $allIntegers = array_reduce($data, fn($carry, $item) => $carry && is_int($item), true); + $allFloats = array_reduce($data, fn($carry, $item) => $carry && is_float($item), true); + + // Check if all values are integer or float + if (!$allIntegers && !$allFloats) { + throw new \InvalidArgumentException('All values in the vector must be integer xor float'); + } + + $minValue = min($data); + $maxValue = max($data); + $marker = 0; + $packed = []; + $packFormat = ''; + + if ($allIntegers) { + if ($minValue >= -128 && $maxValue <= 127) { // INT_8 + $marker = 0xC8; + $packFormat = 'c'; + } elseif ($minValue >= -32768 && $maxValue <= 32767) { // INT_16 + $marker = 0xC9; + $packFormat = 's'; + } elseif ($minValue >= -2147483648 && $maxValue <= 2147483647) { // INT_32 + $marker = 0xCA; + $packFormat = 'l'; + } else { // INT_64 + $marker = 0xCB; + $packFormat = 'q'; + } + } elseif ($allFloats) { + if ($minValue >= 1.18e-38 && $maxValue <= 3.40e+38) { // Single precision float (FLOAT_32) + $marker = 0xC6; + $packFormat = 'G'; + } else { // Double precision float (FLOAT_64) + $marker = 0xC1; + $packFormat = 'E'; + } + } + + if ($marker === 0) { + throw new \InvalidArgumentException('Unsupported data type for vector'); + } + + // Pack the data + $littleEndian = unpack('S', "\x01\x00")[1] === 1; + foreach ($data as $entry) { + $value = pack($packFormat, $entry); + $packed[] = in_array($packFormat, self::$formats) && $littleEndian ? strrev($value) : $value; + } + + return new self(new Bytes([chr($marker)]), new Bytes($packed)); + } + + /** + * Decode vector structure .. returns binary $this->data as array + * @return int[]|float[] + * @throws \InvalidArgumentException + */ + public function decode(): array + { + switch (ord($this->type_marker[0])) { + case 0xC8: // INT_8 + $size = 1; + $unpackFormat = 'c'; + break; + case 0xC9: // INT_16 + $size = 2; + $unpackFormat = 's'; + break; + case 0xCA: // INT_32 + $size = 4; + $unpackFormat = 'l'; + break; + case 0xCB: // INT_64 + $size = 8; + $unpackFormat = 'q'; + break; + case 0xC6: // FLOAT_32 + $size = 4; + $unpackFormat = 'G'; + break; + case 0xC1: // FLOAT_64 + $size = 8; + $unpackFormat = 'E'; + break; + default: + throw new \InvalidArgumentException('Unknown vector type marker: ' . $this->type_marker[0]); + } + + $output = []; + $littleEndian = unpack('S', "\x01\x00")[1] === 1; + foreach(array_chunk($this->data, $size) as $chunk) { + $value = implode('', $chunk); + $output[] = unpack($unpackFormat, in_array($unpackFormat, self::$formats) && $littleEndian ? strrev($value) : $value)[1]; + } + + return $output; + } +} diff --git a/tests/TestLayer.php b/tests/TestLayer.php index 8286a0d..0d35b5f 100644 --- a/tests/TestLayer.php +++ b/tests/TestLayer.php @@ -75,6 +75,8 @@ protected function getCompatibleBoltVersion(string $url = null): float|int $neo4jVersion = $decoded['neo4j_version']; + if (version_compare($neo4jVersion, '2025.08', '>=')) + return 6; if (version_compare($neo4jVersion, '5.26', '>=')) return 5.8; if (version_compare($neo4jVersion, '5.23', '>=')) diff --git a/tests/protocol/V6Test.php b/tests/protocol/V6Test.php new file mode 100644 index 0000000..1a5150d --- /dev/null +++ b/tests/protocol/V6Test.php @@ -0,0 +1,22 @@ +mockConnection()); + $this->assertInstanceOf(V6::class, $cls); + return $cls; + } +} diff --git a/tests/structures/V6/StructuresTest.php b/tests/structures/V6/StructuresTest.php new file mode 100644 index 0000000..f90a3d1 --- /dev/null +++ b/tests/structures/V6/StructuresTest.php @@ -0,0 +1,63 @@ +assertInstanceOf(\Bolt\connection\StreamSocket::class, $conn); + + $bolt = new Bolt($conn); + $this->assertInstanceOf(Bolt::class, $bolt); + + $protocol = $bolt->build(); + $this->assertInstanceOf(AProtocol::class, $protocol); + + if (version_compare($protocol->getVersion(), '6', '<')) { + $this->markTestSkipped('Tests available only for version 6 and higher.'); + } + + $this->sayHello($protocol, $GLOBALS['NEO_USER'], $GLOBALS['NEO_PASS']); + + return $protocol; + } + + // todo ..also test encode and decode in Vector class + public function testVector(AProtocol $protocol) + { + //unpack + // $res = iterator_to_array( + // $protocol + // ->run('RETURN ', [], ['mode' => 'r']) + // ->pull() + // ->getResponses(), + // false + // ); + // $this->assertInstanceOf(Vector::class, $res[1]->content[0]); + + //pack + // $res = iterator_to_array( + // $protocol + // ->run('RETURN toString($p)', [ + // 'p' => $res[1]->content[0] + // ], ['mode' => 'r']) + // ->pull() + // ->getResponses(), + // false + // ); + // $this->assertStringStartsWith('point(', $res[1]->content[0]); + } +} diff --git a/tests/structures/v5/StructuresTest.php b/tests/structures/v5/StructuresTest.php index f6619fa..ca2e6dc 100644 --- a/tests/structures/v5/StructuresTest.php +++ b/tests/structures/v5/StructuresTest.php @@ -44,12 +44,6 @@ public function testInit(): AProtocol return $protocol; } - private string $expectedDateTimeClass = DateTime::class; - use DateTimeTrait; - - private string $expectedDateTimeZoneIdClass = DateTimeZoneId::class; - use DateTimeZoneIdTrait; - /** * @depends testInit */ From 65334e1d714a8ba82778645a3b6bea5fae4a42da Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Tue, 5 Aug 2025 14:20:11 +0200 Subject: [PATCH 2/8] changed float32 range based on neo4j docs --- src/protocol/v6/structures/Vector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/protocol/v6/structures/Vector.php b/src/protocol/v6/structures/Vector.php index 58e11ef..cb405ab 100644 --- a/src/protocol/v6/structures/Vector.php +++ b/src/protocol/v6/structures/Vector.php @@ -73,7 +73,7 @@ public static function encode(array $data): self $packFormat = 'q'; } } elseif ($allFloats) { - if ($minValue >= 1.18e-38 && $maxValue <= 3.40e+38) { // Single precision float (FLOAT_32) + if ($minValue >= 1.4e-45 && $maxValue <= 3.4028235e+38) { // Single precision float (FLOAT_32) $marker = 0xC6; $packFormat = 'G'; } else { // Double precision float (FLOAT_64) From 8120f55370561d11fad74c2a55c4866aec598334 Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Tue, 5 Aug 2025 14:40:29 +0200 Subject: [PATCH 3/8] wip --- .github/workflows/db.50.2204.yml | 2 +- src/protocol/v6/AvailableStructures.php | 2 +- src/protocol/v6/structures/Vector.php | 3 +-- tests/structures/V6/StructuresTest.php | 4 ++++ 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/db.50.2204.yml b/.github/workflows/db.50.2204.yml index d12478d..eabba76 100644 --- a/.github/workflows/db.50.2204.yml +++ b/.github/workflows/db.50.2204.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - neo4j-version: ['5.4', '5.6', '5.8', '5.12', '5.13', '5.23', '5.26', '2025'] + neo4j-version: ['5.4', '5.6', '5.8', '5.12', '5.22', '5.25', '5.26', '2025.07', '2025'] php-version: ['8.1', '8.2', '8.3', '8.4'] services: diff --git a/src/protocol/v6/AvailableStructures.php b/src/protocol/v6/AvailableStructures.php index 64004bf..a7d7976 100644 --- a/src/protocol/v6/AvailableStructures.php +++ b/src/protocol/v6/AvailableStructures.php @@ -1,6 +1,6 @@ data, $size) as $chunk) { - $value = implode('', $chunk); + foreach(mb_str_split((string)$this->data, $size, '8bit') as $value) { $output[] = unpack($unpackFormat, in_array($unpackFormat, self::$formats) && $littleEndian ? strrev($value) : $value)[1]; } diff --git a/tests/structures/V6/StructuresTest.php b/tests/structures/V6/StructuresTest.php index f90a3d1..4709ee3 100644 --- a/tests/structures/V6/StructuresTest.php +++ b/tests/structures/V6/StructuresTest.php @@ -36,8 +36,12 @@ public function testInit(): AProtocol } // todo ..also test encode and decode in Vector class + /** + * @depends testInit + */ public function testVector(AProtocol $protocol) { + $this->markTestIncomplete('This test has not been implemented yet.'); //unpack // $res = iterator_to_array( // $protocol From 830d98958bc6a726f49314ea4ef48f861a959ccd Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Tue, 5 Aug 2025 14:48:45 +0200 Subject: [PATCH 4/8] workflows update --- .github/workflows/neo4j.2025.yml | 48 +++++++++++++++++++ .../{db.44.2204.yml => neo4j.4.4.yml} | 0 .../workflows/{db.50.2204.yml => neo4j.5.yml} | 2 +- .../workflows/{no-db.2204.yml => no-db.yml} | 0 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/neo4j.2025.yml rename .github/workflows/{db.44.2204.yml => neo4j.4.4.yml} (100%) rename .github/workflows/{db.50.2204.yml => neo4j.5.yml} (97%) rename .github/workflows/{no-db.2204.yml => no-db.yml} (100%) diff --git a/.github/workflows/neo4j.2025.yml b/.github/workflows/neo4j.2025.yml new file mode 100644 index 0000000..50bbd70 --- /dev/null +++ b/.github/workflows/neo4j.2025.yml @@ -0,0 +1,48 @@ +name: Tests with Neo4j^2025 on PHP^8 + +on: + pull_request: + branches: [ master ] + +jobs: + db-tests-2025-2204: + runs-on: ubuntu-22.04 + name: "Running Integration tests for PHP ${{ matrix.php-version }} on Neo4j ${{ matrix.neo4j-version }}" + strategy: + fail-fast: false + matrix: + neo4j-version: ['2025.07', '2025'] + php-version: ['8.1', '8.2', '8.3', '8.4'] + + services: + neo4j: + image: neo4j:${{ matrix.neo4j-version }} + env: + NEO4J_AUTH: neo4j/nothing123 + NEO4J_PLUGINS: '["apoc"]' + ports: + - 7687:7687 + - 7474:7474 + options: >- + --health-cmd "wget http://localhost:7474 || exit 1" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: mbstring, sockets + coverage: xdebug + ini-values: max_execution_time=0 + + - name: Install dependencies + run: composer install --no-progress + + - name: Test with phpunit + env: + GDB_USERNAME: neo4j + GDB_PASSWORD: nothing123 + run: vendor/bin/phpunit --configuration phpunit.xml --testsuite "Database" diff --git a/.github/workflows/db.44.2204.yml b/.github/workflows/neo4j.4.4.yml similarity index 100% rename from .github/workflows/db.44.2204.yml rename to .github/workflows/neo4j.4.4.yml diff --git a/.github/workflows/db.50.2204.yml b/.github/workflows/neo4j.5.yml similarity index 97% rename from .github/workflows/db.50.2204.yml rename to .github/workflows/neo4j.5.yml index eabba76..050ff58 100644 --- a/.github/workflows/db.50.2204.yml +++ b/.github/workflows/neo4j.5.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - neo4j-version: ['5.4', '5.6', '5.8', '5.12', '5.22', '5.25', '5.26', '2025.07', '2025'] + neo4j-version: ['5.4', '5.6', '5.8', '5.12', '5.22', '5.25', '5.26'] php-version: ['8.1', '8.2', '8.3', '8.4'] services: diff --git a/.github/workflows/no-db.2204.yml b/.github/workflows/no-db.yml similarity index 100% rename from .github/workflows/no-db.2204.yml rename to .github/workflows/no-db.yml From e86ef048c28f2477fdc91b212002296fc3474b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0tefa=C5=88=C3=A1k?= Date: Mon, 11 Aug 2025 17:37:53 +0200 Subject: [PATCH 5/8] Include new version by default --- src/Bolt.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bolt.php b/src/Bolt.php index c4aff7e..a918144 100644 --- a/src/Bolt.php +++ b/src/Bolt.php @@ -34,7 +34,7 @@ public function __construct(private IConnection $connection) $this->track(); } - $this->setProtocolVersions('5.8.8', '4.4.4'); + $this->setProtocolVersions(6, '5.8.8', '4.4.4'); } private function track(): void From 4067b8c4b84793312f5abff6122f8f8a2f6a99d3 Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Tue, 12 Aug 2025 11:59:04 +0200 Subject: [PATCH 6/8] added new version info --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 09d6384..9944b21 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Website: https://stefanak.serv00.net/ ## :label: Version support -We are trying to keep up and this library supports **Bolt <= 5.8**. +We are trying to keep up and this library supports **Bolt <= 6**. ## :books: Supported ecosystems @@ -198,7 +198,7 @@ foreach ($protocol->getResponses() as $response) { } ``` -:information_source: Default settings for bolt protocol version is 4.3, 4.4 and 5.0 to 5.8. If you are within this list you can ommit calling `$bolt->setProtocolVersions();`. +:information_source: Default settings for bolt protocol version is 4.3, 4.4, 5.0 to 5.8 and 6. If you are within this list you can ommit calling `$bolt->setProtocolVersions();`. ### Autoload From 0a00daede651140045685b4923bb65f23b2ae833 Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Wed, 5 Nov 2025 10:50:50 +0100 Subject: [PATCH 7/8] tested and finalized vector structure implementation --- src/packstream/v1/Packer.php | 10 +-- src/protocol/V6.php | 11 +++- src/protocol/v3/HelloMessage.php | 2 +- src/protocol/v4_1/HelloMessage.php | 2 +- src/protocol/v6/structures/Vector.php | 29 ++++----- tests/structures/V6/StructuresTest.php | 86 ++++++++++++++++++++------ tests/structures/v1/StructuresTest.php | 26 ++++++++ 7 files changed, 115 insertions(+), 51 deletions(-) diff --git a/src/packstream/v1/Packer.php b/src/packstream/v1/Packer.php index 39a9d95..8416248 100644 --- a/src/packstream/v1/Packer.php +++ b/src/packstream/v1/Packer.php @@ -226,13 +226,7 @@ private function packStructure(IStructure $structure): iterable throw new PackException('Undefined parameter type in structure ' . $structure); } - $packerMethod = $type->getName(); - if ($packerMethod === 'int') { - $packerMethod = 'integer'; - } - $packerMethod = 'pack' . ucfirst($packerMethod); - - yield from call_user_func([$this, $packerMethod], $structure->{$parameter->getName()}); + yield from $this->p($structure->{$parameter->getName()}); } } @@ -241,7 +235,7 @@ private function packStructure(IStructure $structure): iterable */ private function packByteArray(Bytes $bytes): iterable { - $size = count($bytes); + $size = mb_strlen($bytes, '8bit'); if ($size < self::MEDIUM) { yield chr(0xCC) . pack('C', $size) . $bytes; } elseif ($size < self::LARGE) { diff --git a/src/protocol/V6.php b/src/protocol/V6.php index 39adbb8..4fc5b42 100644 --- a/src/protocol/V6.php +++ b/src/protocol/V6.php @@ -13,7 +13,7 @@ class V6 extends AProtocol { use \Bolt\protocol\v6\AvailableStructures; - use \Bolt\protocol\v4\ServerStateTransition; + use \Bolt\protocol\v5_1\ServerStateTransition; use \Bolt\protocol\v1\ResetMessage; @@ -26,7 +26,12 @@ class V6 extends AProtocol use \Bolt\protocol\v4\PullMessage; use \Bolt\protocol\v4\DiscardMessage; - use \Bolt\protocol\v4_1\HelloMessage; - use \Bolt\protocol\v4_4\RouteMessage; + + use \Bolt\protocol\v5_1\LogonMessage; + use \Bolt\protocol\v5_1\LogoffMessage; + + use \Bolt\protocol\v5_3\HelloMessage; + + use \Bolt\protocol\v5_4\TelemetryMessage; } diff --git a/src/protocol/v3/HelloMessage.php b/src/protocol/v3/HelloMessage.php index f054ae1..18e1b20 100644 --- a/src/protocol/v3/HelloMessage.php +++ b/src/protocol/v3/HelloMessage.php @@ -12,7 +12,7 @@ trait HelloMessage * The HELLO message request the connection to be authorized for use with the remote database. * * @link https://www.neo4j.com/docs/bolt/current/bolt/message/#messages-hello - * @param array $extra Use \Bolt\helpers\Auth to generate appropriate array + * @param array $extra * @throws BoltException */ public function hello(array $extra): static diff --git a/src/protocol/v4_1/HelloMessage.php b/src/protocol/v4_1/HelloMessage.php index 020d509..742a527 100644 --- a/src/protocol/v4_1/HelloMessage.php +++ b/src/protocol/v4_1/HelloMessage.php @@ -15,7 +15,7 @@ trait HelloMessage * The HELLO message request the connection to be authorized for use with the remote database. * * @link https://www.neo4j.com/docs/bolt/current/bolt/message/#messages-hello - * @param array $extra Use \Bolt\helpers\Auth to generate appropiate array + * @param array $extra * @throws BoltException */ public function hello(array $extra): static diff --git a/src/protocol/v6/structures/Vector.php b/src/protocol/v6/structures/Vector.php index 1a00c25..79196ac 100644 --- a/src/protocol/v6/structures/Vector.php +++ b/src/protocol/v6/structures/Vector.php @@ -44,21 +44,21 @@ public static function encode(array $data): self throw new \InvalidArgumentException('Vector cannot have more than 4096 elements'); } - $allIntegers = array_reduce($data, fn($carry, $item) => $carry && is_int($item), true); - $allFloats = array_reduce($data, fn($carry, $item) => $carry && is_float($item), true); - - // Check if all values are integer or float - if (!$allIntegers && !$allFloats) { - throw new \InvalidArgumentException('All values in the vector must be integer xor float'); - } - + $anyFloat = in_array(true, array_map('is_float', $data)); $minValue = min($data); $maxValue = max($data); $marker = 0; - $packed = []; $packFormat = ''; - if ($allIntegers) { + if ($anyFloat) { + if ($minValue >= 1.4e-45 && $maxValue <= 3.4028235e+38) { // Single precision float (FLOAT_32) + $marker = 0xC6; + $packFormat = 'G'; + } else { // Double precision float (FLOAT_64) + $marker = 0xC1; + $packFormat = 'E'; + } + } else { if ($minValue >= -128 && $maxValue <= 127) { // INT_8 $marker = 0xC8; $packFormat = 'c'; @@ -72,14 +72,6 @@ public static function encode(array $data): self $marker = 0xCB; $packFormat = 'q'; } - } elseif ($allFloats) { - if ($minValue >= 1.4e-45 && $maxValue <= 3.4028235e+38) { // Single precision float (FLOAT_32) - $marker = 0xC6; - $packFormat = 'G'; - } else { // Double precision float (FLOAT_64) - $marker = 0xC1; - $packFormat = 'E'; - } } if ($marker === 0) { @@ -87,6 +79,7 @@ public static function encode(array $data): self } // Pack the data + $packed = []; $littleEndian = unpack('S', "\x01\x00")[1] === 1; foreach ($data as $entry) { $value = pack($packFormat, $entry); diff --git a/tests/structures/V6/StructuresTest.php b/tests/structures/V6/StructuresTest.php index 4709ee3..f09eb83 100644 --- a/tests/structures/V6/StructuresTest.php +++ b/tests/structures/V6/StructuresTest.php @@ -35,33 +35,79 @@ public function testInit(): AProtocol return $protocol; } - // todo ..also test encode and decode in Vector class /** * @depends testInit */ public function testVector(AProtocol $protocol) { - $this->markTestIncomplete('This test has not been implemented yet.'); //unpack - // $res = iterator_to_array( - // $protocol - // ->run('RETURN ', [], ['mode' => 'r']) - // ->pull() - // ->getResponses(), - // false - // ); - // $this->assertInstanceOf(Vector::class, $res[1]->content[0]); + $res = iterator_to_array( + $protocol + ->run('CYPHER 25 RETURN vector([1.05, 0.123, 5], 3, FLOAT), + vector([1.05, 0.123, 5], 3, FLOAT32), + vector([5, 543, 342765], 3, INTEGER), + vector([5, -60, 120], 3, INTEGER8), + vector([5, -20000, 30000], 3, INTEGER16), + vector([5, -2000000000, 2000000000], 3, INTEGER32)', + [], ['mode' => 'r']) + ->pull() + ->getResponses(), + false + ); + + foreach ($res[1]->content as $vector) { + $this->assertInstanceOf(Vector::class, $vector); + } + + // float64 + $values = $res[1]->content[0]->decode(); + $this->assertEqualsWithDelta([1.05, 0.123, 5], $values, 1e-6); + // float32 + $values = $res[1]->content[1]->decode(); + $this->assertEqualsWithDelta([1.05, 0.123, 5], $values, 1e-6); + // int64 + $values = $res[1]->content[2]->decode(); + $this->assertEquals([5, 543, 342765], $values); + // int8 + $values = $res[1]->content[3]->decode(); + $this->assertEquals([5, -60, 120], $values); + // int16 + $values = $res[1]->content[4]->decode(); + $this->assertEquals([5, -20000, 30000], $values); + // int32 + $values = $res[1]->content[5]->decode(); + $this->assertEquals([5, -2000000000, 2000000000], $values); //pack - // $res = iterator_to_array( - // $protocol - // ->run('RETURN toString($p)', [ - // 'p' => $res[1]->content[0] - // ], ['mode' => 'r']) - // ->pull() - // ->getResponses(), - // false - // ); - // $this->assertStringStartsWith('point(', $res[1]->content[0]); + $res = iterator_to_array( + $protocol + ->run('CYPHER 25 RETURN toFloatList($float), toIntegerList($int64), toIntegerList($int8), toIntegerList($int16), toIntegerList($int32)', [ + 'float' => Vector::encode([1.05, 0.123, 5.0]), + 'int64' => Vector::encode([5, -21474836480, 21474836470]), + 'int8' => Vector::encode([5, -60, 120]), + 'int16' => Vector::encode([5, -20000, 30000]), + 'int32' => Vector::encode([5, -2000000000, 2000000000]), + ], ['mode' => 'r']) + ->pull() + ->getResponses(), + false + ); + + $this->assertEqualsWithDelta([1.05, 0.123, 5], $res[1]->content[0], 1e-6); + $this->assertEquals([5, -21474836480, 21474836470], $res[1]->content[1]); + $this->assertEquals([5, -60, 120], $res[1]->content[2]); + $this->assertEquals([5, -20000, 30000], $res[1]->content[3]); + $this->assertEquals([5, -2000000000, 2000000000], $res[1]->content[4]); + } + + /** + * @depends testInit + */ + public function testVectorExceptions() + { + $this->expectException(\InvalidArgumentException::class); + Vector::encode([]); + $this->expectException(\InvalidArgumentException::class); + Vector::encode(range(1, 5000)); } } diff --git a/tests/structures/v1/StructuresTest.php b/tests/structures/v1/StructuresTest.php index 6753521..fba19aa 100644 --- a/tests/structures/v1/StructuresTest.php +++ b/tests/structures/v1/StructuresTest.php @@ -26,6 +26,7 @@ UnboundRelationship }; use Bolt\enum\Signature; +use Bolt\packstream\Bytes; /** * Class StructuresTest @@ -406,4 +407,29 @@ public function testTime(int $timestamp, string $timezone, AProtocol|V4_4|V4_3|V $time = preg_replace(["/\.?0+(.\d{2}:\d{2})$/", "/\+00:00$/"], ['$1', 'Z'], $time); $this->assertEquals($time, $res[1]->content[0], 'pack ' . $time . ' != ' . $res[1]->content[0]); } + + /** + * @depends testInit + */ + public function testBytes(AProtocol|V4_4|V4_3|V4_2|V3 $protocol): void + { + $bytes = new Bytes([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09]); + $res = iterator_to_array( + $protocol + ->begin() + ->run('CREATE (:Test { k: 123, b: $b })', [ + 'b' => $bytes + ], ['mode' => 'w']) + ->run('MATCH (n:Test { k: 123 }) RETURN n.b') + ->pull() + ->rollback() + ->getResponses(), + false + ); + $this->assertInstanceOf(Bytes::class, $res[3]->content[0]); + $this->assertEquals( + (string)$bytes, + (string)($res[3]->content[0]) + ); + } } From 8d17f533fbc19084edd53816158c267c5b775f71 Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Wed, 5 Nov 2025 10:56:50 +0100 Subject: [PATCH 8/8] workflow update --- .github/workflows/neo4j.2025.yml | 2 +- tests/TestLayer.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/neo4j.2025.yml b/.github/workflows/neo4j.2025.yml index 50bbd70..16efd6b 100644 --- a/.github/workflows/neo4j.2025.yml +++ b/.github/workflows/neo4j.2025.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - neo4j-version: ['2025.07', '2025'] + neo4j-version: ['2025.09', '2025'] php-version: ['8.1', '8.2', '8.3', '8.4'] services: diff --git a/tests/TestLayer.php b/tests/TestLayer.php index 0d35b5f..d7f7362 100644 --- a/tests/TestLayer.php +++ b/tests/TestLayer.php @@ -66,7 +66,7 @@ protected function sayHello(AProtocol $protocol, string $name, string $password) * @return float|int * @link https://neo4j.com/docs/http-api/current/endpoints/#discovery-api */ - protected function getCompatibleBoltVersion(string $url = null): float|int + protected function getCompatibleBoltVersion(?string $url = null): float|int { $json = file_get_contents($url ?? $GLOBALS['NEO_BROWSER'] ?? ('http://' . ($GLOBALS['NEO_HOST'] ?? 'localhost') . ':7474/')); $decoded = json_decode($json, true); @@ -75,7 +75,7 @@ protected function getCompatibleBoltVersion(string $url = null): float|int $neo4jVersion = $decoded['neo4j_version']; - if (version_compare($neo4jVersion, '2025.08', '>=')) + if (version_compare($neo4jVersion, '2025.10', '>=')) return 6; if (version_compare($neo4jVersion, '5.26', '>=')) return 5.8;