From 6f6aa8e4870ce8a9e15e4476e6a53070fff077b1 Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Wed, 5 Nov 2025 14:50:39 +0100 Subject: [PATCH 1/6] Refactor vector structure implementation --- src/protocol/v6/structures/Vector.php | 71 ++++++++++++++++----------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/src/protocol/v6/structures/Vector.php b/src/protocol/v6/structures/Vector.php index 79196ac..25eca31 100644 --- a/src/protocol/v6/structures/Vector.php +++ b/src/protocol/v6/structures/Vector.php @@ -27,54 +27,57 @@ public function __toString(): string return json_encode([(string)$this->type_marker, (string)$this->data]); } - private static array $formats = ['s', 'l', 'q']; + private static array $endianesFormats = ['s', 'l', 'q']; /** * Encode array as vector structure + * This is a helper method to create Vector structure from array of numbers * @param int[]|float[] $data + * @param TypeMarker|null $type Optional type to force specific data type .. null = auto decide * @return self * @throws \InvalidArgumentException */ - public static function encode(array $data): self + public static function encode(array $data, ?TypeMarker $type = null): 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'); + $anyFloat = false; + foreach ($data as $entry) { + if (!is_int($entry) && !is_float($entry)) { + throw new \InvalidArgumentException('Vector can only contain numeric values'); + } + if (!$anyFloat && is_float($entry)) { + $anyFloat = true; + } } - $anyFloat = in_array(true, array_map('is_float', $data)); - $minValue = min($data); - $maxValue = max($data); - $marker = 0; + $minValue = count($data) ? min($data) : 0; + $maxValue = count($data) ? max($data) : 0; $packFormat = ''; if ($anyFloat) { if ($minValue >= 1.4e-45 && $maxValue <= 3.4028235e+38) { // Single precision float (FLOAT_32) - $marker = 0xC6; + if ($type === null) $type = TypeMarker::FLOAT_32; $packFormat = 'G'; } else { // Double precision float (FLOAT_64) - $marker = 0xC1; + if ($type === null) $type = TypeMarker::FLOAT_64; $packFormat = 'E'; } } else { if ($minValue >= -128 && $maxValue <= 127) { // INT_8 - $marker = 0xC8; + if ($type === null) $type = TypeMarker::INT_8; $packFormat = 'c'; } elseif ($minValue >= -32768 && $maxValue <= 32767) { // INT_16 - $marker = 0xC9; + if ($type === null) $type = TypeMarker::INT_16; $packFormat = 's'; } elseif ($minValue >= -2147483648 && $maxValue <= 2147483647) { // INT_32 - $marker = 0xCA; + if ($type === null) $type = TypeMarker::INT_32; $packFormat = 'l'; } else { // INT_64 - $marker = 0xCB; + if ($type === null) $type = TypeMarker::INT_64; $packFormat = 'q'; } } - if ($marker === 0) { + if ($type === null) { throw new \InvalidArgumentException('Unsupported data type for vector'); } @@ -82,42 +85,42 @@ public static function encode(array $data): self $packed = []; $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; + $value = pack($packFormat, $anyFloat ? (float)$entry : (int)$entry); + $packed[] = in_array($packFormat, self::$endianesFormats) && $littleEndian ? strrev($value) : $value; } - return new self(new Bytes([chr($marker)]), new Bytes($packed)); + return new self(new Bytes([chr($type->value)]), new Bytes($packed)); } /** - * Decode vector structure .. returns binary $this->data as array + * Decode vector structure .. returns binary $this->data as array of numbers * @return int[]|float[] * @throws \InvalidArgumentException */ public function decode(): array { switch (ord($this->type_marker[0])) { - case 0xC8: // INT_8 + case TypeMarker::INT_8->value: // INT_8 $size = 1; $unpackFormat = 'c'; break; - case 0xC9: // INT_16 + case TypeMarker::INT_16->value: // INT_16 $size = 2; $unpackFormat = 's'; break; - case 0xCA: // INT_32 + case TypeMarker::INT_32->value: // INT_32 $size = 4; $unpackFormat = 'l'; break; - case 0xCB: // INT_64 + case TypeMarker::INT_64->value: // INT_64 $size = 8; $unpackFormat = 'q'; break; - case 0xC6: // FLOAT_32 + case TypeMarker::FLOAT_32->value: // FLOAT_32 $size = 4; $unpackFormat = 'G'; break; - case 0xC1: // FLOAT_64 + case TypeMarker::FLOAT_64->value: // FLOAT_64 $size = 8; $unpackFormat = 'E'; break; @@ -128,9 +131,19 @@ public function decode(): array $output = []; $littleEndian = unpack('S', "\x01\x00")[1] === 1; foreach(mb_str_split((string)$this->data, $size, '8bit') as $value) { - $output[] = unpack($unpackFormat, in_array($unpackFormat, self::$formats) && $littleEndian ? strrev($value) : $value)[1]; + $output[] = unpack($unpackFormat, in_array($unpackFormat, self::$endianesFormats) && $littleEndian ? strrev($value) : $value)[1]; } return $output; } } + +enum TypeMarker: int +{ + case INT_8 = 0xC8; + case INT_16 = 0xC9; + case INT_32 = 0xCA; + case INT_64 = 0xCB; + case FLOAT_32 = 0xC6; + case FLOAT_64 = 0xC1; +} From 895ede77a2ff555f1e7689a570d5072dc73c1d5d Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Wed, 5 Nov 2025 14:52:13 +0100 Subject: [PATCH 2/6] typo --- src/protocol/v6/structures/Vector.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/protocol/v6/structures/Vector.php b/src/protocol/v6/structures/Vector.php index 25eca31..22beef1 100644 --- a/src/protocol/v6/structures/Vector.php +++ b/src/protocol/v6/structures/Vector.php @@ -27,7 +27,7 @@ public function __toString(): string return json_encode([(string)$this->type_marker, (string)$this->data]); } - private static array $endianesFormats = ['s', 'l', 'q']; + private static array $endiannessFormats = ['s', 'l', 'q']; /** * Encode array as vector structure @@ -86,7 +86,7 @@ public static function encode(array $data, ?TypeMarker $type = null): self $littleEndian = unpack('S', "\x01\x00")[1] === 1; foreach ($data as $entry) { $value = pack($packFormat, $anyFloat ? (float)$entry : (int)$entry); - $packed[] = in_array($packFormat, self::$endianesFormats) && $littleEndian ? strrev($value) : $value; + $packed[] = in_array($packFormat, self::$endiannessFormats) && $littleEndian ? strrev($value) : $value; } return new self(new Bytes([chr($type->value)]), new Bytes($packed)); @@ -131,7 +131,7 @@ public function decode(): array $output = []; $littleEndian = unpack('S', "\x01\x00")[1] === 1; foreach(mb_str_split((string)$this->data, $size, '8bit') as $value) { - $output[] = unpack($unpackFormat, in_array($unpackFormat, self::$endianesFormats) && $littleEndian ? strrev($value) : $value)[1]; + $output[] = unpack($unpackFormat, in_array($unpackFormat, self::$endiannessFormats) && $littleEndian ? strrev($value) : $value)[1]; } return $output; From c1a90cd4c11c01f9476dddf3f035401119d1717d Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Wed, 5 Nov 2025 14:58:16 +0100 Subject: [PATCH 3/6] separated enum --- src/protocol/v6/structures/TypeMarker.php | 20 ++++++++++++++++++++ src/protocol/v6/structures/Vector.php | 11 +---------- 2 files changed, 21 insertions(+), 10 deletions(-) create mode 100644 src/protocol/v6/structures/TypeMarker.php diff --git a/src/protocol/v6/structures/TypeMarker.php b/src/protocol/v6/structures/TypeMarker.php new file mode 100644 index 0000000..1d22b5b --- /dev/null +++ b/src/protocol/v6/structures/TypeMarker.php @@ -0,0 +1,20 @@ + Date: Wed, 5 Nov 2025 15:10:00 +0100 Subject: [PATCH 4/6] fixed test --- src/protocol/v6/structures/Vector.php | 11 +++-------- tests/structures/V6/StructuresTest.php | 11 ++++++----- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/protocol/v6/structures/Vector.php b/src/protocol/v6/structures/Vector.php index 62f878e..7e0ea06 100644 --- a/src/protocol/v6/structures/Vector.php +++ b/src/protocol/v6/structures/Vector.php @@ -20,8 +20,7 @@ class Vector implements IStructure public function __construct( public readonly Bytes $type_marker, public readonly Bytes $data - ) { - } + ) {} public function __toString(): string { @@ -78,10 +77,6 @@ public static function encode(array $data, ?TypeMarker $type = null): self } } - if ($type === null) { - throw new \InvalidArgumentException('Unsupported data type for vector'); - } - // Pack the data $packed = []; $littleEndian = unpack('S', "\x01\x00")[1] === 1; @@ -128,10 +123,10 @@ public function decode(): array default: throw new \InvalidArgumentException('Unknown vector type marker: ' . $this->type_marker[0]); } - + $output = []; $littleEndian = unpack('S', "\x01\x00")[1] === 1; - foreach(mb_str_split((string)$this->data, $size, '8bit') as $value) { + foreach (mb_str_split((string)$this->data, $size, '8bit') as $value) { $output[] = unpack($unpackFormat, in_array($unpackFormat, self::$endiannessFormats) && $littleEndian ? strrev($value) : $value)[1]; } diff --git a/tests/structures/V6/StructuresTest.php b/tests/structures/V6/StructuresTest.php index f09eb83..0da9557 100644 --- a/tests/structures/V6/StructuresTest.php +++ b/tests/structures/V6/StructuresTest.php @@ -43,13 +43,16 @@ public function testVector(AProtocol $protocol) //unpack $res = iterator_to_array( $protocol - ->run('CYPHER 25 RETURN vector([1.05, 0.123, 5], 3, FLOAT), + ->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']) + [], + ['mode' => 'r'] + ) ->pull() ->getResponses(), false @@ -106,8 +109,6 @@ public function testVector(AProtocol $protocol) public function testVectorExceptions() { $this->expectException(\InvalidArgumentException::class); - Vector::encode([]); - $this->expectException(\InvalidArgumentException::class); - Vector::encode(range(1, 5000)); + Vector::encode(['abc', 'def']); } } From bf3920204c59777d14722740875592dac996cbd7 Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Wed, 5 Nov 2025 16:22:15 +0100 Subject: [PATCH 5/6] refactoring --- src/protocol/v6/structures/Vector.php | 56 +++++++++++++++++++-------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/src/protocol/v6/structures/Vector.php b/src/protocol/v6/structures/Vector.php index 7e0ea06..ddd267a 100644 --- a/src/protocol/v6/structures/Vector.php +++ b/src/protocol/v6/structures/Vector.php @@ -51,30 +51,31 @@ public static function encode(array $data, ?TypeMarker $type = null): self $minValue = count($data) ? min($data) : 0; $maxValue = count($data) ? max($data) : 0; - $packFormat = ''; - if ($anyFloat) { - if ($minValue >= 1.4e-45 && $maxValue <= 3.4028235e+38) { // Single precision float (FLOAT_32) - if ($type === null) $type = TypeMarker::FLOAT_32; + if ($type === null) { + $type = self::detectTypeMarker($anyFloat, $minValue, $maxValue); + } + + $packFormat = ''; + switch ($type) { + case TypeMarker::FLOAT_32: $packFormat = 'G'; - } else { // Double precision float (FLOAT_64) - if ($type === null) $type = TypeMarker::FLOAT_64; + break; + case TypeMarker::FLOAT_64: $packFormat = 'E'; - } - } else { - if ($minValue >= -128 && $maxValue <= 127) { // INT_8 - if ($type === null) $type = TypeMarker::INT_8; + break; + case TypeMarker::INT_8: $packFormat = 'c'; - } elseif ($minValue >= -32768 && $maxValue <= 32767) { // INT_16 - if ($type === null) $type = TypeMarker::INT_16; + break; + case TypeMarker::INT_16: $packFormat = 's'; - } elseif ($minValue >= -2147483648 && $maxValue <= 2147483647) { // INT_32 - if ($type === null) $type = TypeMarker::INT_32; + break; + case TypeMarker::INT_32: $packFormat = 'l'; - } else { // INT_64 - if ($type === null) $type = TypeMarker::INT_64; + break; + case TypeMarker::INT_64: $packFormat = 'q'; - } + break; } // Pack the data @@ -88,6 +89,27 @@ public static function encode(array $data, ?TypeMarker $type = null): self return new self(new Bytes([chr($type->value)]), new Bytes($packed)); } + private static function detectTypeMarker(bool $anyFloat, int|float $minValue, int|float $maxValue): TypeMarker + { + if ($anyFloat) { + if ($minValue >= -3.4028235e+38 && $maxValue <= 3.4028235e+38) { // Single precision float (FLOAT_32) + return TypeMarker::FLOAT_32; + } else { // Double precision float (FLOAT_64) + return TypeMarker::FLOAT_64; + } + } else { + if ($minValue >= -128 && $maxValue <= 127) { // INT_8 + return TypeMarker::INT_8; + } elseif ($minValue >= -32768 && $maxValue <= 32767) { // INT_16 + return TypeMarker::INT_16; + } elseif ($minValue >= -2147483648 && $maxValue <= 2147483647) { // INT_32 + return TypeMarker::INT_32; + } else { // INT_64 + return TypeMarker::INT_64; + } + } + } + /** * Decode vector structure .. returns binary $this->data as array of numbers * @return int[]|float[] From f326b1cf8d81caebe0fab03ad913e82b0eb18f6d Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Wed, 5 Nov 2025 16:32:32 +0100 Subject: [PATCH 6/6] removed unused variables --- src/protocol/v6/structures/Vector.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/protocol/v6/structures/Vector.php b/src/protocol/v6/structures/Vector.php index ddd267a..295855e 100644 --- a/src/protocol/v6/structures/Vector.php +++ b/src/protocol/v6/structures/Vector.php @@ -49,11 +49,8 @@ public static function encode(array $data, ?TypeMarker $type = null): self } } - $minValue = count($data) ? min($data) : 0; - $maxValue = count($data) ? max($data) : 0; - if ($type === null) { - $type = self::detectTypeMarker($anyFloat, $minValue, $maxValue); + $type = self::detectTypeMarker($anyFloat, count($data) ? min($data) : 0, count($data) ? max($data) : 0); } $packFormat = '';