From 115e341afd778605bdb1a7e8d93ba7b544602525 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 10 Sep 2025 16:41:43 +0300 Subject: [PATCH 01/29] Spatial --- phpunit.xml | 3 +- src/Database/Database.php | 54 +++++++++++++++++++ tests/e2e/Adapter/Base.php | 12 ++--- tests/e2e/Adapter/Scopes/SpatialTests.php | 66 +++++++++++------------ 4 files changed, 94 insertions(+), 41 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 2a0531cfd..7469c5341 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,8 +7,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false" -> + stopOnFailure="true"> ./tests/unit diff --git a/src/Database/Database.php b/src/Database/Database.php index 2fec661a1..62ee486ef 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -477,6 +477,57 @@ function (?string $value) { return DateTime::formatTz($value); } ); + + self::addFilter( + 'point', + function (mixed $value) { + return "POINT({$value[0]} {$value[1]})"; + }, + function (?array $value) { + return $value; + } + ); + + self::addFilter( + 'polygon', + function (mixed $value) { + // Check if this is a single ring (flat array of points) or multiple rings + $isSingleRing = count($value) > 0 && is_array($value[0]) && + count($value[0]) === 2 && is_numeric($value[0][0]) && is_numeric($value[0][1]); + + if ($isSingleRing) { + // Convert single ring format [[x1,y1], [x2,y2], ...] to multi-ring format + $value = [$value]; + } + + $rings = []; + foreach ($value as $ring) { + $points = []; + foreach ($ring as $point) { + $points[] = "{$point[0]} {$point[1]}"; + } + $rings[] = '(' . implode(', ', $points) . ')'; + } + return 'POLYGON(' . implode(', ', $rings) . ')'; + }, + function (?array $value) { + return $value; + } + ); + + self::addFilter( + 'linestring', + function (mixed $value) { + $points = []; + foreach ($value as $point) { + $points[] = "{$point[0]} {$point[1]}"; + } + return 'LINESTRING(' . implode(', ', $points) . ')'; + }, + function (?array $value) { + return $value; + } + ); } /** @@ -1889,6 +1940,9 @@ protected function getRequiredFilters(?string $type): array { return match ($type) { self::VAR_DATETIME => ['datetime'], + self::VAR_POINT => ['point'], + self::VAR_POLYGON => ['polygon'], + self::VAR_LINESTRING => ['linestring'], default => [], }; } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 37ad7cce3..481704ce6 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -18,12 +18,12 @@ abstract class Base extends TestCase { - use CollectionTests; - use DocumentTests; - use AttributeTests; - use IndexTests; - use PermissionTests; - use RelationshipTests; +// use CollectionTests; +// use DocumentTests; +// use AttributeTests; +// use IndexTests; +// use PermissionTests; +// use RelationshipTests; use SpatialTests; use GeneralTests; diff --git a/tests/e2e/Adapter/Scopes/SpatialTests.php b/tests/e2e/Adapter/Scopes/SpatialTests.php index 3d93d233e..2df7981a6 100644 --- a/tests/e2e/Adapter/Scopes/SpatialTests.php +++ b/tests/e2e/Adapter/Scopes/SpatialTests.php @@ -74,7 +74,7 @@ public function testSpatialCollection(): void $this->assertIsArray($col->getAttribute('indexes')); $this->assertCount(2, $col->getAttribute('indexes')); - $database->createAttribute($collectionName, 'attribute3', Database::VAR_POINT, 0, true); + $database->createAttribute($collectionName, 'attribute3', Database::VAR_POINT, 0, true, filters: ['point']); $database->createIndex($collectionName, ID::custom("index3"), Database::INDEX_SPATIAL, ['attribute3']); $col = $database->getCollection($collectionName); @@ -102,9 +102,9 @@ public function testSpatialTypeDocuments(): void $database->createCollection($collectionName); // Create spatial attributes using createAttribute method - $this->assertEquals(true, $database->createAttribute($collectionName, 'pointAttr', Database::VAR_POINT, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true)); - $this->assertEquals(true, $database->createAttribute($collectionName, 'lineAttr', Database::VAR_LINESTRING, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true)); - $this->assertEquals(true, $database->createAttribute($collectionName, 'polyAttr', Database::VAR_POLYGON, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true)); + $this->assertEquals(true, $database->createAttribute($collectionName, 'pointAttr', Database::VAR_POINT, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true, filters: ['point'])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'lineAttr', Database::VAR_LINESTRING, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true, filters: ['linestring'])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'polyAttr', Database::VAR_POLYGON, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true, filters: ['polygon'])); // Create spatial indexes $this->assertEquals(true, $database->createIndex($collectionName, 'point_spatial', Database::INDEX_SPATIAL, ['pointAttr'])); @@ -231,7 +231,7 @@ public function testSpatialRelationshipOneToOne(): void $database->createCollection('building'); $database->createAttribute('location', 'name', Database::VAR_STRING, 255, true); - $database->createAttribute('location', 'coordinates', Database::VAR_POINT, 0, true); + $database->createAttribute('location', 'coordinates', Database::VAR_POINT, 0, true, filters: ['point']); $database->createAttribute('building', 'name', Database::VAR_STRING, 255, true); $database->createAttribute('building', 'area', Database::VAR_STRING, 255, true); @@ -336,9 +336,9 @@ public function testSpatialAttributes(): void $database->createCollection($collectionName); $required = $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true; - $this->assertEquals(true, $database->createAttribute($collectionName, 'pointAttr', Database::VAR_POINT, 0, $required)); - $this->assertEquals(true, $database->createAttribute($collectionName, 'lineAttr', Database::VAR_LINESTRING, 0, $required)); - $this->assertEquals(true, $database->createAttribute($collectionName, 'polyAttr', Database::VAR_POLYGON, 0, $required)); + $this->assertEquals(true, $database->createAttribute($collectionName, 'pointAttr', Database::VAR_POINT, 0, $required, filters: ['point'])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'lineAttr', Database::VAR_LINESTRING, 0, $required, filters: ['linestring'])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'polyAttr', Database::VAR_POLYGON, 0, $required, filters: ['polygon'])); // Create spatial indexes $this->assertEquals(true, $database->createIndex($collectionName, 'idx_point', Database::INDEX_SPATIAL, ['pointAttr'])); @@ -387,7 +387,7 @@ public function testSpatialOneToMany(): void $database->createAttribute($parent, 'name', Database::VAR_STRING, 255, true); $database->createAttribute($child, 'name', Database::VAR_STRING, 255, true); - $database->createAttribute($child, 'coord', Database::VAR_POINT, 0, true); + $database->createAttribute($child, 'coord', Database::VAR_POINT, 0, true, filters: ['point']); $database->createIndex($child, 'coord_spatial', Database::INDEX_SPATIAL, ['coord']); $database->createRelationship( @@ -499,7 +499,7 @@ public function testSpatialManyToOne(): void $database->createAttribute($parent, 'name', Database::VAR_STRING, 255, true); $database->createAttribute($child, 'name', Database::VAR_STRING, 255, true); - $database->createAttribute($child, 'coord', Database::VAR_POINT, 0, true); + $database->createAttribute($child, 'coord', Database::VAR_POINT, 0, true, filters: ['point']); $database->createIndex($child, 'coord_spatial', Database::INDEX_SPATIAL, ['coord']); $database->createRelationship( @@ -603,10 +603,10 @@ public function testSpatialManyToMany(): void $database->createCollection($b); $database->createAttribute($a, 'name', Database::VAR_STRING, 255, true); - $database->createAttribute($a, 'home', Database::VAR_POINT, 0, true); + $database->createAttribute($a, 'home', Database::VAR_POINT, 0, true, filters: ['point']); $database->createIndex($a, 'home_spatial', Database::INDEX_SPATIAL, ['home']); $database->createAttribute($b, 'title', Database::VAR_STRING, 255, true); - $database->createAttribute($b, 'area', Database::VAR_POLYGON, 0, true); + $database->createAttribute($b, 'area', Database::VAR_POLYGON, 0, true, filters: ['polygon']); $database->createIndex($b, 'area_spatial', Database::INDEX_SPATIAL, ['area']); $database->createRelationship( @@ -705,7 +705,7 @@ public function testSpatialIndex(): void $collectionName = 'spatial_index_'; try { $database->createCollection($collectionName); - $database->createAttribute($collectionName, 'loc', Database::VAR_POINT, 0, true); + $database->createAttribute($collectionName, 'loc', Database::VAR_POINT, 0, true, filters: ['point']); $this->assertEquals(true, $database->createIndex($collectionName, 'loc_spatial', Database::INDEX_SPATIAL, ['loc'])); $collection = $database->getCollection($collectionName); @@ -766,7 +766,7 @@ public function testSpatialIndex(): void $collOrderIndex = 'spatial_idx_order_index_' . uniqid(); try { $database->createCollection($collOrderIndex); - $database->createAttribute($collOrderIndex, 'loc', Database::VAR_POINT, 0, true); + $database->createAttribute($collOrderIndex, 'loc', Database::VAR_POINT, 0, true, filters: ['point']); if ($orderSupported) { $this->assertTrue($database->createIndex($collOrderIndex, 'idx_loc', Database::INDEX_SPATIAL, ['loc'], [], [Database::ORDER_DESC])); } else { @@ -826,7 +826,7 @@ public function testSpatialIndex(): void $collNullIndex = 'spatial_idx_null_index_' . uniqid(); try { $database->createCollection($collNullIndex); - $database->createAttribute($collNullIndex, 'loc', Database::VAR_POINT, 0, false); + $database->createAttribute($collNullIndex, 'loc', Database::VAR_POINT, 0, false, filters: ['point']); if ($nullSupported) { $this->assertTrue($database->createIndex($collNullIndex, 'idx_loc', Database::INDEX_SPATIAL, ['loc'])); } else { @@ -855,12 +855,12 @@ public function testComplexGeometricShapes(): void $database->createCollection($collectionName); // Create spatial attributes for different geometric shapes - $this->assertEquals(true, $database->createAttribute($collectionName, 'rectangle', Database::VAR_POLYGON, 0, true)); - $this->assertEquals(true, $database->createAttribute($collectionName, 'square', Database::VAR_POLYGON, 0, true)); - $this->assertEquals(true, $database->createAttribute($collectionName, 'triangle', Database::VAR_POLYGON, 0, true)); - $this->assertEquals(true, $database->createAttribute($collectionName, 'circle_center', Database::VAR_POINT, 0, true)); - $this->assertEquals(true, $database->createAttribute($collectionName, 'complex_polygon', Database::VAR_POLYGON, 0, true)); - $this->assertEquals(true, $database->createAttribute($collectionName, 'multi_linestring', Database::VAR_LINESTRING, 0, true)); + $this->assertEquals(true, $database->createAttribute($collectionName, 'rectangle', Database::VAR_POLYGON, 0, true, filters: ['polygon'])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'square', Database::VAR_POLYGON, 0, true, filters: ['polygon'])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'triangle', Database::VAR_POLYGON, 0, true, filters: ['polygon'])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'circle_center', Database::VAR_POINT, 0, true, filters: ['point'])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'complex_polygon', Database::VAR_POLYGON, 0, true, filters: ['polygon'])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'multi_linestring', Database::VAR_LINESTRING, 0, true, filters: ['linestring'])); // Create spatial indexes $this->assertEquals(true, $database->createIndex($collectionName, 'idx_rectangle', Database::INDEX_SPATIAL, ['rectangle'])); @@ -1285,9 +1285,9 @@ public function testSpatialQueryCombinations(): void $database->createCollection($collectionName); // Create spatial attributes - $this->assertEquals(true, $database->createAttribute($collectionName, 'location', Database::VAR_POINT, 0, true)); - $this->assertEquals(true, $database->createAttribute($collectionName, 'area', Database::VAR_POLYGON, 0, true)); - $this->assertEquals(true, $database->createAttribute($collectionName, 'route', Database::VAR_LINESTRING, 0, true)); + $this->assertEquals(true, $database->createAttribute($collectionName, 'location', Database::VAR_POINT, 0, true, filters: ['point'])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'area', Database::VAR_POLYGON, 0, true, filters: ['polygon'])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'route', Database::VAR_LINESTRING, 0, true, filters: ['linestring'])); $this->assertEquals(true, $database->createAttribute($collectionName, 'name', Database::VAR_STRING, 255, true)); // Create spatial indexes @@ -1719,8 +1719,8 @@ public function testSptialAggregation(): void // Create collection with spatial and numeric attributes $database->createCollection($collectionName); $database->createAttribute($collectionName, 'name', Database::VAR_STRING, 255, true); - $database->createAttribute($collectionName, 'loc', Database::VAR_POINT, 0, true); - $database->createAttribute($collectionName, 'area', Database::VAR_POLYGON, 0, true); + $database->createAttribute($collectionName, 'loc', Database::VAR_POINT, 0, true, filters: ['point']); + $database->createAttribute($collectionName, 'area', Database::VAR_POLYGON, 0, true, filters: ['polygon']); $database->createAttribute($collectionName, 'score', Database::VAR_INTEGER, 0, true); // Spatial indexes @@ -1808,21 +1808,21 @@ public function testUpdateSpatialAttributes(): void // 0) Disallow creation of spatial attributes with size or array try { - $database->createAttribute($collectionName, 'geom_bad_size', Database::VAR_POINT, 10, true); + $database->createAttribute($collectionName, 'geom_bad_size', Database::VAR_POINT, 10, true, filters: ['point']); $this->fail('Expected DatabaseException when creating spatial attribute with non-zero size'); } catch (\Throwable $e) { $this->assertInstanceOf(Exception::class, $e); } try { - $database->createAttribute($collectionName, 'geom_bad_array', Database::VAR_POINT, 0, true, array: true); + $database->createAttribute($collectionName, 'geom_bad_array', Database::VAR_POINT, 0, true, array: true, filters: ['point']); $this->fail('Expected DatabaseException when creating spatial attribute with array=true'); } catch (\Throwable $e) { $this->assertInstanceOf(Exception::class, $e); } // Create a single spatial attribute (required=true) - $this->assertEquals(true, $database->createAttribute($collectionName, 'geom', Database::VAR_POINT, 0, true)); + $this->assertEquals(true, $database->createAttribute($collectionName, 'geom', Database::VAR_POINT, 0, true, filters: ['point'])); $this->assertEquals(true, $database->createIndex($collectionName, 'idx_geom', Database::INDEX_SPATIAL, ['geom'])); // 1) Disallow size and array updates on spatial attributes: expect DatabaseException @@ -1893,9 +1893,9 @@ public function testSpatialAttributeDefaults(): void $database->createCollection($collectionName); // Create spatial attributes with defaults and no indexes to avoid nullability/index constraints - $this->assertEquals(true, $database->createAttribute($collectionName, 'pt', Database::VAR_POINT, 0, false, [1.0, 2.0])); - $this->assertEquals(true, $database->createAttribute($collectionName, 'ln', Database::VAR_LINESTRING, 0, false, [[0.0, 0.0], [1.0, 1.0]])); - $this->assertEquals(true, $database->createAttribute($collectionName, 'pg', Database::VAR_POLYGON, 0, false, [[[0.0, 0.0], [0.0, 2.0], [2.0, 2.0], [0.0, 0.0]]])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'pt', Database::VAR_POINT, 0, false, [1.0, 2.0], filters: ['point'])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'ln', Database::VAR_LINESTRING, 0, false, [[0.0, 0.0], [1.0, 1.0]], filters: ['linestring'])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'pg', Database::VAR_POLYGON, 0, false, [[[0.0, 0.0], [0.0, 2.0], [2.0, 2.0], [0.0, 0.0]]], filters: ['polygon'])); // Create non-spatial attributes (mix of defaults and no defaults) $this->assertEquals(true, $database->createAttribute($collectionName, 'title', Database::VAR_STRING, 255, false, 'Untitled')); @@ -2100,7 +2100,7 @@ public function testSpatialDistanceInMeter(): void $collectionName = 'spatial_distance_meters_'; try { $database->createCollection($collectionName); - $this->assertEquals(true, $database->createAttribute($collectionName, 'loc', Database::VAR_POINT, 0, true)); + $this->assertEquals(true, $database->createAttribute($collectionName, 'loc', Database::VAR_POINT, 0, true, filters: ['point'])); $this->assertEquals(true, $database->createIndex($collectionName, 'idx_loc', Database::INDEX_SPATIAL, ['loc'])); // Two points roughly ~1000 meters apart by latitude delta (~0.009 deg ≈ 1km) From 6f8bdf9d936b7d11db95dd48fd3abac011a15dce Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 14 Sep 2025 10:39:05 +0300 Subject: [PATCH 02/29] Postgres point --- src/Database/Adapter.php | 4 ++ src/Database/Adapter/Postgres.php | 19 ++++- src/Database/Adapter/SQL.php | 10 +++ src/Database/Database.php | 31 ++++----- src/Database/Validator/Spatial.php | 1 + tests/e2e/Adapter/Scopes/SpatialTests.php | 84 +++++++++++++---------- 6 files changed, 94 insertions(+), 55 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 25ed510a5..f7b832143 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -1285,4 +1285,8 @@ abstract public function getTenantQuery(string $collection, string $alias = ''): * @return bool */ abstract protected function execute(mixed $stmt): bool; + + abstract protected function encodePoint(array $point): mixed; + abstract protected function decodePoint(mixed $data): array; + } diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index f666d1184..cacf72610 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -993,7 +993,7 @@ public function createDocument(Document $collection, Document $document): Docume INSERT INTO {$this->getSQLTable($name)} ({$columns} \"_uid\") VALUES ({$columnNames} :_uid) "; - +var_dump($sql); $sql = $this->trigger(Database::EVENT_DOCUMENT_CREATE, $sql); $stmt = $this->getPDO()->prepare($sql); @@ -2002,4 +2002,21 @@ public function getSupportForSpatialAxisOrder(): bool { return false; } + + public function encodePoint(array $point): string + { + return "SRID=4326;POINT({$point[0]} {$point[1]})";// EWKT + } + + public function decodePoint(mixed $data): array + { + $ewkt = str_replace('SRID=4326;', '', $data); + + // Expect format "POINT(x y)" + if (preg_match('/POINT\(([-\d\.]+) ([-\d\.]+)\)/', $ewkt, $matches)) { + return [(float)$matches[1], (float)$matches[2]]; + } + + throw new Exception("Invalid EWKT format: $ewkt"); + } } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 5876858e8..d5757c3ee 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -2710,4 +2710,14 @@ public function getSpatialTypeFromWKT(string $wkt): string } return strtolower(trim(substr($wkt, 0, $pos))); } + + public function encodePoint(array $point): string + { + return "POINT({$point[0]} {$point[1]})"; + } + + public function decodePoint(mixed $data): array + { + return $data; + } } diff --git a/src/Database/Database.php b/src/Database/Database.php index abb0bff2c..36a3031f0 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -481,29 +481,27 @@ function (?string $value) { self::addFilter( Database::VAR_POINT, - /** - * @param mixed $value - * @return mixed - */ function (mixed $value) { - if (!is_array($value)) { - return $value; + if ($value === null) { + return null; } try { - return self::encodeSpatialData($value, Database::VAR_POINT); + //return self::encodeSpatialData($value, Database::VAR_POINT); + return $this->adapter->encodePoint($value); } catch (\Throwable) { - return $value; + throw new StructureException('Invalid point'); } }, - /** - * @param string|null $value - * @return string|null - */ function (?string $value) { - if (!is_string($value)) { - return $value; + if ($value === null) { + return null; + } + try { + //return self::decodeSpatialData($value); + return $this->adapter->decodePoint($value); + } catch (\Throwable) { + throw new StructureException('Invalid point'); } - return self::decodeSpatialData($value); } ); self::addFilter( @@ -1972,9 +1970,6 @@ protected function getRequiredFilters(?string $type): array { return match ($type) { self::VAR_DATETIME => ['datetime'], - self::VAR_POINT => ['point'], - self::VAR_POLYGON => ['polygon'], - self::VAR_LINESTRING => ['linestring'], default => [], }; } diff --git a/src/Database/Validator/Spatial.php b/src/Database/Validator/Spatial.php index 912f05b2b..f08216b80 100644 --- a/src/Database/Validator/Spatial.php +++ b/src/Database/Validator/Spatial.php @@ -135,6 +135,7 @@ protected function validatePolygon(array $value): bool public static function isWKTString(string $value): bool { $value = trim($value); + $value = str_replace('SRID=4326;', '', $value); return (bool) preg_match('/^(POINT|LINESTRING|POLYGON)\s*\(/i', $value); } diff --git a/tests/e2e/Adapter/Scopes/SpatialTests.php b/tests/e2e/Adapter/Scopes/SpatialTests.php index 2f28590f3..3fbf95b92 100644 --- a/tests/e2e/Adapter/Scopes/SpatialTests.php +++ b/tests/e2e/Adapter/Scopes/SpatialTests.php @@ -76,7 +76,7 @@ public function testSpatialCollection(): void $this->assertIsArray($col->getAttribute('indexes')); $this->assertCount(2, $col->getAttribute('indexes')); - $database->createAttribute($collectionName, 'attribute3', Database::VAR_POINT, 0, true, filters: ['point']); + $database->createAttribute($collectionName, 'attribute3', Database::VAR_POINT, 0, true); $database->createIndex($collectionName, ID::custom("index3"), Database::INDEX_SPATIAL, ['attribute3']); $col = $database->getCollection($collectionName); @@ -104,14 +104,26 @@ public function testSpatialTypeDocuments(): void $database->createCollection($collectionName); // Create spatial attributes using createAttribute method - $this->assertEquals(true, $database->createAttribute($collectionName, 'pointAttr', Database::VAR_POINT, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true, filters: ['point'])); - $this->assertEquals(true, $database->createAttribute($collectionName, 'lineAttr', Database::VAR_LINESTRING, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true, filters: ['linestring'])); - $this->assertEquals(true, $database->createAttribute($collectionName, 'polyAttr', Database::VAR_POLYGON, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true, filters: ['polygon'])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'pointAttr', Database::VAR_POINT, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true)); + //$this->assertEquals(true, $database->createAttribute($collectionName, 'lineAttr', Database::VAR_LINESTRING, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true)); + //$this->assertEquals(true, $database->createAttribute($collectionName, 'polyAttr', Database::VAR_POLYGON, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true)); // Create spatial indexes $this->assertEquals(true, $database->createIndex($collectionName, 'point_spatial', Database::INDEX_SPATIAL, ['pointAttr'])); - $this->assertEquals(true, $database->createIndex($collectionName, 'line_spatial', Database::INDEX_SPATIAL, ['lineAttr'])); - $this->assertEquals(true, $database->createIndex($collectionName, 'poly_spatial', Database::INDEX_SPATIAL, ['polyAttr'])); + //$this->assertEquals(true, $database->createIndex($collectionName, 'line_spatial', Database::INDEX_SPATIAL, ['lineAttr'])); + //$this->assertEquals(true, $database->createIndex($collectionName, 'poly_spatial', Database::INDEX_SPATIAL, ['polyAttr'])); + + // Create test document + $doc1 = new Document([ + '$id' => 'doc1', + 'pointAttr' => [5.0, 5.0], + '$permissions' => [Permission::update(Role::any()), Permission::read(Role::any())] + ]); + $createdDoc = $database->createDocument($collectionName, $doc1); + $this->assertInstanceOf(Document::class, $createdDoc); + $this->assertEquals([5.0, 5.0], $createdDoc->getAttribute('pointAttr')); + $this->assertEquals('222','2'); + // Create test document $doc1 = new Document([ @@ -233,7 +245,7 @@ public function testSpatialTypeDocuments(): void $this->assertEquals('doc1', $result[0]->getId(), sprintf('Incorrect document for distance %s on polyAttr', $queryType)); } } finally { - $database->deleteCollection($collectionName); + //$database->deleteCollection($collectionName); } } @@ -251,7 +263,7 @@ public function testSpatialRelationshipOneToOne(): void $database->createCollection('building'); $database->createAttribute('location', 'name', Database::VAR_STRING, 255, true); - $database->createAttribute('location', 'coordinates', Database::VAR_POINT, 0, true, filters: ['point']); + $database->createAttribute('location', 'coordinates', Database::VAR_POINT, 0, true); $database->createAttribute('building', 'name', Database::VAR_STRING, 255, true); $database->createAttribute('building', 'area', Database::VAR_STRING, 255, true); @@ -356,9 +368,9 @@ public function testSpatialAttributes(): void $database->createCollection($collectionName); $required = $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true; - $this->assertEquals(true, $database->createAttribute($collectionName, 'pointAttr', Database::VAR_POINT, 0, $required, filters: ['point'])); - $this->assertEquals(true, $database->createAttribute($collectionName, 'lineAttr', Database::VAR_LINESTRING, 0, $required, filters: ['linestring'])); - $this->assertEquals(true, $database->createAttribute($collectionName, 'polyAttr', Database::VAR_POLYGON, 0, $required, filters: ['polygon'])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'pointAttr', Database::VAR_POINT, 0, $required)); + $this->assertEquals(true, $database->createAttribute($collectionName, 'lineAttr', Database::VAR_LINESTRING, 0, $required)); + $this->assertEquals(true, $database->createAttribute($collectionName, 'polyAttr', Database::VAR_POLYGON, 0, $required)); // Create spatial indexes $this->assertEquals(true, $database->createIndex($collectionName, 'idx_point', Database::INDEX_SPATIAL, ['pointAttr'])); @@ -407,7 +419,7 @@ public function testSpatialOneToMany(): void $database->createAttribute($parent, 'name', Database::VAR_STRING, 255, true); $database->createAttribute($child, 'name', Database::VAR_STRING, 255, true); - $database->createAttribute($child, 'coord', Database::VAR_POINT, 0, true, filters: ['point']); + $database->createAttribute($child, 'coord', Database::VAR_POINT, 0, true); $database->createIndex($child, 'coord_spatial', Database::INDEX_SPATIAL, ['coord']); $database->createRelationship( @@ -519,7 +531,7 @@ public function testSpatialManyToOne(): void $database->createAttribute($parent, 'name', Database::VAR_STRING, 255, true); $database->createAttribute($child, 'name', Database::VAR_STRING, 255, true); - $database->createAttribute($child, 'coord', Database::VAR_POINT, 0, true, filters: ['point']); + $database->createAttribute($child, 'coord', Database::VAR_POINT, 0, true); $database->createIndex($child, 'coord_spatial', Database::INDEX_SPATIAL, ['coord']); $database->createRelationship( @@ -623,10 +635,10 @@ public function testSpatialManyToMany(): void $database->createCollection($b); $database->createAttribute($a, 'name', Database::VAR_STRING, 255, true); - $database->createAttribute($a, 'home', Database::VAR_POINT, 0, true, filters: ['point']); + $database->createAttribute($a, 'home', Database::VAR_POINT, 0, true); $database->createIndex($a, 'home_spatial', Database::INDEX_SPATIAL, ['home']); $database->createAttribute($b, 'title', Database::VAR_STRING, 255, true); - $database->createAttribute($b, 'area', Database::VAR_POLYGON, 0, true, filters: ['polygon']); + $database->createAttribute($b, 'area', Database::VAR_POLYGON, 0, true); $database->createIndex($b, 'area_spatial', Database::INDEX_SPATIAL, ['area']); $database->createRelationship( @@ -725,7 +737,7 @@ public function testSpatialIndex(): void $collectionName = 'spatial_index_'; try { $database->createCollection($collectionName); - $database->createAttribute($collectionName, 'loc', Database::VAR_POINT, 0, true, filters: ['point']); + $database->createAttribute($collectionName, 'loc', Database::VAR_POINT, 0, true); $this->assertEquals(true, $database->createIndex($collectionName, 'loc_spatial', Database::INDEX_SPATIAL, ['loc'])); $collection = $database->getCollection($collectionName); @@ -786,7 +798,7 @@ public function testSpatialIndex(): void $collOrderIndex = 'spatial_idx_order_index_' . uniqid(); try { $database->createCollection($collOrderIndex); - $database->createAttribute($collOrderIndex, 'loc', Database::VAR_POINT, 0, true, filters: ['point']); + $database->createAttribute($collOrderIndex, 'loc', Database::VAR_POINT, 0, true); if ($orderSupported) { $this->assertTrue($database->createIndex($collOrderIndex, 'idx_loc', Database::INDEX_SPATIAL, ['loc'], [], [Database::ORDER_DESC])); } else { @@ -846,7 +858,7 @@ public function testSpatialIndex(): void $collNullIndex = 'spatial_idx_null_index_' . uniqid(); try { $database->createCollection($collNullIndex); - $database->createAttribute($collNullIndex, 'loc', Database::VAR_POINT, 0, false, filters: ['point']); + $database->createAttribute($collNullIndex, 'loc', Database::VAR_POINT, 0, false); if ($nullSupported) { $this->assertTrue($database->createIndex($collNullIndex, 'idx_loc', Database::INDEX_SPATIAL, ['loc'])); } else { @@ -922,12 +934,12 @@ public function testComplexGeometricShapes(): void $database->createCollection($collectionName); // Create spatial attributes for different geometric shapes - $this->assertEquals(true, $database->createAttribute($collectionName, 'rectangle', Database::VAR_POLYGON, 0, true, filters: ['polygon'])); - $this->assertEquals(true, $database->createAttribute($collectionName, 'square', Database::VAR_POLYGON, 0, true, filters: ['polygon'])); - $this->assertEquals(true, $database->createAttribute($collectionName, 'triangle', Database::VAR_POLYGON, 0, true, filters: ['polygon'])); - $this->assertEquals(true, $database->createAttribute($collectionName, 'circle_center', Database::VAR_POINT, 0, true, filters: ['point'])); - $this->assertEquals(true, $database->createAttribute($collectionName, 'complex_polygon', Database::VAR_POLYGON, 0, true, filters: ['polygon'])); - $this->assertEquals(true, $database->createAttribute($collectionName, 'multi_linestring', Database::VAR_LINESTRING, 0, true, filters: ['linestring'])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'rectangle', Database::VAR_POLYGON, 0, true)); + $this->assertEquals(true, $database->createAttribute($collectionName, 'square', Database::VAR_POLYGON, 0, true)); + $this->assertEquals(true, $database->createAttribute($collectionName, 'triangle', Database::VAR_POLYGON, 0, true)); + $this->assertEquals(true, $database->createAttribute($collectionName, 'circle_center', Database::VAR_POINT, 0, true)); + $this->assertEquals(true, $database->createAttribute($collectionName, 'complex_polygon', Database::VAR_POLYGON, 0, true)); + $this->assertEquals(true, $database->createAttribute($collectionName, 'multi_linestring', Database::VAR_LINESTRING, 0, true)); // Create spatial indexes $this->assertEquals(true, $database->createIndex($collectionName, 'idx_rectangle', Database::INDEX_SPATIAL, ['rectangle'])); @@ -1352,9 +1364,9 @@ public function testSpatialQueryCombinations(): void $database->createCollection($collectionName); // Create spatial attributes - $this->assertEquals(true, $database->createAttribute($collectionName, 'location', Database::VAR_POINT, 0, true, filters: ['point'])); - $this->assertEquals(true, $database->createAttribute($collectionName, 'area', Database::VAR_POLYGON, 0, true, filters: ['polygon'])); - $this->assertEquals(true, $database->createAttribute($collectionName, 'route', Database::VAR_LINESTRING, 0, true, filters: ['linestring'])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'location', Database::VAR_POINT, 0, true)); + $this->assertEquals(true, $database->createAttribute($collectionName, 'area', Database::VAR_POLYGON, 0, true)); + $this->assertEquals(true, $database->createAttribute($collectionName, 'route', Database::VAR_LINESTRING, 0, true)); $this->assertEquals(true, $database->createAttribute($collectionName, 'name', Database::VAR_STRING, 255, true)); // Create spatial indexes @@ -1783,8 +1795,8 @@ public function testSptialAggregation(): void // Create collection with spatial and numeric attributes $database->createCollection($collectionName); $database->createAttribute($collectionName, 'name', Database::VAR_STRING, 255, true); - $database->createAttribute($collectionName, 'loc', Database::VAR_POINT, 0, true, filters: ['point']); - $database->createAttribute($collectionName, 'area', Database::VAR_POLYGON, 0, true, filters: ['polygon']); + $database->createAttribute($collectionName, 'loc', Database::VAR_POINT, 0, true); + $database->createAttribute($collectionName, 'area', Database::VAR_POLYGON, 0, true); $database->createAttribute($collectionName, 'score', Database::VAR_INTEGER, 0, true); // Spatial indexes @@ -1872,21 +1884,21 @@ public function testUpdateSpatialAttributes(): void // 0) Disallow creation of spatial attributes with size or array try { - $database->createAttribute($collectionName, 'geom_bad_size', Database::VAR_POINT, 10, true, filters: ['point']); + $database->createAttribute($collectionName, 'geom_bad_size', Database::VAR_POINT, 10, true); $this->fail('Expected DatabaseException when creating spatial attribute with non-zero size'); } catch (\Throwable $e) { $this->assertInstanceOf(Exception::class, $e); } try { - $database->createAttribute($collectionName, 'geom_bad_array', Database::VAR_POINT, 0, true, array: true, filters: ['point']); + $database->createAttribute($collectionName, 'geom_bad_array', Database::VAR_POINT, 0, true, array: true); $this->fail('Expected DatabaseException when creating spatial attribute with array=true'); } catch (\Throwable $e) { $this->assertInstanceOf(Exception::class, $e); } // Create a single spatial attribute (required=true) - $this->assertEquals(true, $database->createAttribute($collectionName, 'geom', Database::VAR_POINT, 0, true, filters: ['point'])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'geom', Database::VAR_POINT, 0, true)); $this->assertEquals(true, $database->createIndex($collectionName, 'idx_geom', Database::INDEX_SPATIAL, ['geom'])); // 1) Disallow size and array updates on spatial attributes: expect DatabaseException @@ -1957,9 +1969,9 @@ public function testSpatialAttributeDefaults(): void $database->createCollection($collectionName); // Create spatial attributes with defaults and no indexes to avoid nullability/index constraints - $this->assertEquals(true, $database->createAttribute($collectionName, 'pt', Database::VAR_POINT, 0, false, [1.0, 2.0], filters: ['point'])); - $this->assertEquals(true, $database->createAttribute($collectionName, 'ln', Database::VAR_LINESTRING, 0, false, [[0.0, 0.0], [1.0, 1.0]], filters: ['linestring'])); - $this->assertEquals(true, $database->createAttribute($collectionName, 'pg', Database::VAR_POLYGON, 0, false, [[[0.0, 0.0], [0.0, 2.0], [2.0, 2.0], [0.0, 0.0]]], filters: ['polygon'])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'pt', Database::VAR_POINT, 0, false, [1.0, 2.0])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'ln', Database::VAR_LINESTRING, 0, false, [[0.0, 0.0], [1.0, 1.0]])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'pg', Database::VAR_POLYGON, 0, false, [[[0.0, 0.0], [0.0, 2.0], [2.0, 2.0], [0.0, 0.0]]])); // Create non-spatial attributes (mix of defaults and no defaults) $this->assertEquals(true, $database->createAttribute($collectionName, 'title', Database::VAR_STRING, 255, false, 'Untitled')); @@ -2164,7 +2176,7 @@ public function testSpatialDistanceInMeter(): void $collectionName = 'spatial_distance_meters_'; try { $database->createCollection($collectionName); - $this->assertEquals(true, $database->createAttribute($collectionName, 'loc', Database::VAR_POINT, 0, true, filters: ['point'])); + $this->assertEquals(true, $database->createAttribute($collectionName, 'loc', Database::VAR_POINT, 0, true)); $this->assertEquals(true, $database->createIndex($collectionName, 'idx_loc', Database::INDEX_SPATIAL, ['loc'])); // Two points roughly ~1000 meters apart by latitude delta (~0.009 deg ≈ 1km) From 4ef5206c193505a412d1b4ed872d6e9c62882ceb Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 14 Sep 2025 14:34:54 +0300 Subject: [PATCH 03/29] decodePoint --- src/Database/Adapter.php | 2 +- src/Database/Adapter/MariaDB.php | 2 +- src/Database/Adapter/Postgres.php | 21 ++++++++++---- src/Database/Adapter/SQL.php | 35 ++++++++++++++++++++--- src/Database/Database.php | 32 ++++++++++++++++++--- src/Database/Validator/Spatial.php | 8 +++++- tests/e2e/Adapter/Scopes/SpatialTests.php | 8 +++++- 7 files changed, 91 insertions(+), 17 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index f7b832143..2d273160e 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -1287,6 +1287,6 @@ abstract public function getTenantQuery(string $collection, string $alias = ''): abstract protected function execute(mixed $stmt): bool; abstract protected function encodePoint(array $point): mixed; - abstract protected function decodePoint(mixed $data): array; + abstract protected function decodePoint(mixed $wkb): array; } diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index c78d6637c..77b3647e5 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -864,7 +864,7 @@ public function createDocument(Document $collection, Document $document): Docume INSERT INTO {$this->getSQLTable($name)} ({$columns} _uid) VALUES ({$columnNames} :_uid) "; - +var_dump($sql); $sql = $this->trigger(Database::EVENT_DOCUMENT_CREATE, $sql); $stmt = $this->getPDO()->prepare($sql); diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index cacf72610..94ad08c42 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -2005,18 +2005,29 @@ public function getSupportForSpatialAxisOrder(): bool public function encodePoint(array $point): string { - return "SRID=4326;POINT({$point[0]} {$point[1]})";// EWKT + return "POINT({$point[0]} {$point[1]})";// EWKT + //return "SRID=4326;POINT({$point[0]} {$point[1]})";// EWKT } - public function decodePoint(mixed $data): array + public function decodePoint(mixed $wkb): array { - $ewkt = str_replace('SRID=4326;', '', $data); + //$wkb = str_replace('SRID=4326;', '', $wkb); // Remove if was added in encode // Expect format "POINT(x y)" - if (preg_match('/POINT\(([-\d\.]+) ([-\d\.]+)\)/', $ewkt, $matches)) { + if (preg_match('/POINT\(([-\d\.]+) ([-\d\.]+)\)/', $wkb, $matches)) { return [(float)$matches[1], (float)$matches[2]]; } - throw new Exception("Invalid EWKT format: $ewkt"); + $bin = hex2bin($wkb); + + $isLE = ord($bin[0]) === 1; + $type = unpack($isLE ? 'V' : 'N', substr($bin, 1, 4))[1]; + $offset = 5 + (($type & 0x20000000) ? 4 : 0); + + $fmt = $isLE ? 'e' : 'E'; // little vs big endian double + $x = unpack($fmt, substr($bin, $offset, 8))[1]; + $y = unpack($fmt, substr($bin, $offset + 8, 8))[1]; + + return [(float)$x, (float)$y]; } } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index d5757c3ee..eb304f471 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -359,14 +359,14 @@ public function getDocument(Document $collection, string $id, array $queries = [ $forUpdate = $forUpdate ? 'FOR UPDATE' : ''; $alias = Query::DEFAULT_ALIAS; - + $spatialAttributes=[]; $sql = " SELECT {$this->getAttributeProjection($selections, $alias, $spatialAttributes)} FROM {$this->getSQLTable($name)} AS {$this->quote($alias)} WHERE {$this->quote($alias)}.{$this->quote('_uid')} = :_uid {$this->getTenantQuery($collection, $alias)} "; - +var_dump($sql); if ($this->getSupportForUpdateLock()) { $sql .= " {$forUpdate}"; } @@ -2716,8 +2716,35 @@ public function encodePoint(array $point): string return "POINT({$point[0]} {$point[1]})"; } - public function decodePoint(mixed $data): array + public function decodePoint(mixed $wkb): array { - return $data; + var_dump($wkb); + + if (str_starts_with(strtoupper($wkb), 'POINT(')) { + $start = strpos($wkb, '(') + 1; + $end = strrpos($wkb, ')'); + $inside = substr($wkb, $start, $end - $start); + + $coords = explode(' ', trim($inside)); + return [(float)$coords[0], (float)$coords[1]]; + } + + // MySQL SRID-aware WKB layout: + // 1 byte = endian (1 = little endian) + // 4 bytes = type + SRID flag + // 4 bytes = SRID + // 16 bytes = X,Y coordinates (double each, little endian) + + $byteOrder = ord($wkb[0]); + $littleEndian = ($byteOrder === 1); + + // Skip 1 + 4 + 4 = 9 bytes to get coordinates + $coordsBin = substr($wkb, 9, 16); + + // Unpack doubles + $format = $littleEndian ? 'd2' : 'd2'; // little-endian doubles + $coords = unpack($format, $coordsBin); + + return [$coords[1], $coords[2]]; } } diff --git a/src/Database/Database.php b/src/Database/Database.php index 36a3031f0..782f93602 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -486,8 +486,8 @@ function (mixed $value) { return null; } try { - //return self::encodeSpatialData($value, Database::VAR_POINT); - return $this->adapter->encodePoint($value); + return self::encodeSpatialData($value, Database::VAR_POINT); + //return $this->adapter->encodePoint($value); } catch (\Throwable) { throw new StructureException('Invalid point'); } @@ -496,11 +496,18 @@ function (?string $value) { if ($value === null) { return null; } + var_dump('shmuel'); + var_dump($value); + + /** + * Validate array point + */ + try { //return self::decodeSpatialData($value); return $this->adapter->decodePoint($value); - } catch (\Throwable) { - throw new StructureException('Invalid point'); + } catch (\Throwable $th) { + throw new StructureException($th->getMessage()); } } ); @@ -1323,6 +1330,19 @@ public function delete(?string $database = null): bool */ public function createCollection(string $id, array $attributes = [], array $indexes = [], ?array $permissions = null, bool $documentSecurity = true): Document { + foreach ($attributes as &$attribute) { + if (in_array($attribute['type'], Database::SPATIAL_TYPES)) { + $existingFilters = $attribute['filters'] ?? []; + if (!is_array($existingFilters)) { + $existingFilters = [$existingFilters]; + } + $attribute['filters'] = array_values( + array_unique(array_merge($existingFilters, [$attribute['type']])) + ); + } + } + unset($attribute); + $permissions ??= [ Permission::create(Role::any()), ]; @@ -1679,6 +1699,10 @@ public function createAttribute(string $collection, string $id, string $type, in if ($collection->isEmpty()) { throw new NotFoundException('Collection not found'); } + if (in_array($type, Database::SPATIAL_TYPES)) { + $filters[] = $type; + $filters = array_unique($filters); + } $attribute = $this->validateAttribute( $collection, diff --git a/src/Database/Validator/Spatial.php b/src/Database/Validator/Spatial.php index f08216b80..cc3ea83d4 100644 --- a/src/Database/Validator/Spatial.php +++ b/src/Database/Validator/Spatial.php @@ -134,8 +134,14 @@ protected function validatePolygon(array $value): bool */ public static function isWKTString(string $value): bool { + + /** + * We need to decode the value first + */ + + // return true; + $value = trim($value); - $value = str_replace('SRID=4326;', '', $value); return (bool) preg_match('/^(POINT|LINESTRING|POLYGON)\s*\(/i', $value); } diff --git a/tests/e2e/Adapter/Scopes/SpatialTests.php b/tests/e2e/Adapter/Scopes/SpatialTests.php index 3fbf95b92..3cdeb9216 100644 --- a/tests/e2e/Adapter/Scopes/SpatialTests.php +++ b/tests/e2e/Adapter/Scopes/SpatialTests.php @@ -109,7 +109,7 @@ public function testSpatialTypeDocuments(): void //$this->assertEquals(true, $database->createAttribute($collectionName, 'polyAttr', Database::VAR_POLYGON, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true)); // Create spatial indexes - $this->assertEquals(true, $database->createIndex($collectionName, 'point_spatial', Database::INDEX_SPATIAL, ['pointAttr'])); + //$this->assertEquals(true, $database->createIndex($collectionName, 'point_spatial', Database::INDEX_SPATIAL, ['pointAttr'])); //$this->assertEquals(true, $database->createIndex($collectionName, 'line_spatial', Database::INDEX_SPATIAL, ['lineAttr'])); //$this->assertEquals(true, $database->createIndex($collectionName, 'poly_spatial', Database::INDEX_SPATIAL, ['polyAttr'])); @@ -120,11 +120,17 @@ public function testSpatialTypeDocuments(): void '$permissions' => [Permission::update(Role::any()), Permission::read(Role::any())] ]); $createdDoc = $database->createDocument($collectionName, $doc1); + $this->assertInstanceOf(Document::class, $createdDoc); + $this->assertEquals([5.0, 5.0], $createdDoc->getAttribute('pointAttr')); + + $createdDoc = $database->getDocument($collectionName, 'doc1'); + $this->assertInstanceOf(Document::class, $createdDoc); $this->assertEquals([5.0, 5.0], $createdDoc->getAttribute('pointAttr')); $this->assertEquals('222','2'); + // Create test document $doc1 = new Document([ '$id' => 'doc1', From 37db0b1984dccfbe9ca76f8523b6650ddddc4a43 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 14 Sep 2025 14:37:55 +0300 Subject: [PATCH 04/29] same decode --- src/Database/Adapter/Postgres.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 94ad08c42..12bdc6ce4 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -2013,9 +2013,13 @@ public function decodePoint(mixed $wkb): array { //$wkb = str_replace('SRID=4326;', '', $wkb); // Remove if was added in encode - // Expect format "POINT(x y)" - if (preg_match('/POINT\(([-\d\.]+) ([-\d\.]+)\)/', $wkb, $matches)) { - return [(float)$matches[1], (float)$matches[2]]; + if (str_starts_with(strtoupper($wkb), 'POINT(')) { + $start = strpos($wkb, '(') + 1; + $end = strrpos($wkb, ')'); + $inside = substr($wkb, $start, $end - $start); + + $coords = explode(' ', trim($inside)); + return [(float)$coords[0], (float)$coords[1]]; } $bin = hex2bin($wkb); From d01fdbceed6588a4cecc4b92fb65ad5d28398705 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 14 Sep 2025 15:49:33 +0300 Subject: [PATCH 05/29] decodeLinestring --- src/Database/Adapter.php | 5 ++- src/Database/Adapter/SQL.php | 46 +++++++++++++++++++++++ src/Database/Database.php | 27 +++++++------ tests/e2e/Adapter/Scopes/SpatialTests.php | 12 +++--- 4 files changed, 69 insertions(+), 21 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 2d273160e..b7f8378b6 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -1286,7 +1286,8 @@ abstract public function getTenantQuery(string $collection, string $alias = ''): */ abstract protected function execute(mixed $stmt): bool; - abstract protected function encodePoint(array $point): mixed; - abstract protected function decodePoint(mixed $wkb): array; + abstract public function encodePoint(array $point): mixed; + abstract public function decodePoint(string $wkb): array; + abstract public function decodeLinestring(string $wkb): array; } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index eb304f471..6c6004539 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -2747,4 +2747,50 @@ public function decodePoint(mixed $wkb): array return [$coords[1], $coords[2]]; } + + public function decodeLinestring(string $wkb): array + { + if (str_starts_with(strtoupper($wkb), 'LINESTRING(')) { + $start = strpos($wkb, '(') + 1; + $end = strrpos($wkb, ')'); + $inside = substr($wkb, $start, $end - $start); + + $points = explode(',', $inside); + return array_map(function ($point) { + $coords = explode(' ', trim($point)); + return [(float)$coords[0], (float)$coords[1]]; + }, $points); + } + + var_dump($wkb); + + $isLE = ord($wkb[0]) === 1; // little-endian? + $type = unpack($isLE ? 'V' : 'N', substr($wkb, 1, 4))[1]; + + // Check for SRID flag (0x20000000) + $hasSRID = ($type & 0x20000000) !== 0; + $geomType = $type & 0xFFFF; // Mask lower 16 bits to get actual type + + if ($geomType !== 2) { // 2 = LINESTRING + throw new \RuntimeException("Not a LINESTRING geometry type, got {$geomType}"); + } + + $offset = 5 + ($hasSRID ? 4 : 0); // skip endian + type + optional SRID + + // Number of points (4 bytes) + $numPoints = unpack($isLE ? 'V' : 'N', substr($wkb, $offset, 4))[1]; + $offset += 4; + + $points = []; + $fmt = $isLE ? 'e' : 'E'; // little/big endian double + + for ($i = 0; $i < $numPoints; $i++) { + $x = unpack($fmt, substr($wkb, $offset, 8))[1]; + $y = unpack($fmt, substr($wkb, $offset + 8, 8))[1]; + $points[] = [(float)$x, (float)$y]; + $offset += 16; + } + + return $points; + } } diff --git a/src/Database/Database.php b/src/Database/Database.php index 782f93602..266aa912b 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -496,27 +496,21 @@ function (?string $value) { if ($value === null) { return null; } - var_dump('shmuel'); - var_dump($value); /** - * Validate array point + * todo:validate array point */ try { - //return self::decodeSpatialData($value); return $this->adapter->decodePoint($value); } catch (\Throwable $th) { throw new StructureException($th->getMessage()); } } ); + self::addFilter( Database::VAR_LINESTRING, - /** - * @param mixed $value - * @return mixed - */ function (mixed $value) { if (!is_array($value)) { return $value; @@ -527,17 +521,22 @@ function (mixed $value) { return $value; } }, - /** - * @param string|null $value - * @return string|null - */ + function (?string $value) { if (is_null($value)) { - return $value; + return null; + } + + try { + //return self::decodeSpatialData($value); + return $this->adapter->decodeLinestring($value); + } catch (\Throwable $th) { + var_dump($th); + throw new StructureException($th->getMessage()); } - return self::decodeSpatialData($value); } ); + self::addFilter( Database::VAR_POLYGON, /** diff --git a/tests/e2e/Adapter/Scopes/SpatialTests.php b/tests/e2e/Adapter/Scopes/SpatialTests.php index 3cdeb9216..fab44ed24 100644 --- a/tests/e2e/Adapter/Scopes/SpatialTests.php +++ b/tests/e2e/Adapter/Scopes/SpatialTests.php @@ -105,32 +105,34 @@ public function testSpatialTypeDocuments(): void // Create spatial attributes using createAttribute method $this->assertEquals(true, $database->createAttribute($collectionName, 'pointAttr', Database::VAR_POINT, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true)); - //$this->assertEquals(true, $database->createAttribute($collectionName, 'lineAttr', Database::VAR_LINESTRING, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true)); + $this->assertEquals(true, $database->createAttribute($collectionName, 'lineAttr', Database::VAR_LINESTRING, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true)); //$this->assertEquals(true, $database->createAttribute($collectionName, 'polyAttr', Database::VAR_POLYGON, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true)); // Create spatial indexes - //$this->assertEquals(true, $database->createIndex($collectionName, 'point_spatial', Database::INDEX_SPATIAL, ['pointAttr'])); - //$this->assertEquals(true, $database->createIndex($collectionName, 'line_spatial', Database::INDEX_SPATIAL, ['lineAttr'])); + $this->assertEquals(true, $database->createIndex($collectionName, 'point_spatial', Database::INDEX_SPATIAL, ['pointAttr'])); + $this->assertEquals(true, $database->createIndex($collectionName, 'line_spatial', Database::INDEX_SPATIAL, ['lineAttr'])); //$this->assertEquals(true, $database->createIndex($collectionName, 'poly_spatial', Database::INDEX_SPATIAL, ['polyAttr'])); // Create test document $doc1 = new Document([ '$id' => 'doc1', 'pointAttr' => [5.0, 5.0], + 'lineAttr' => [[1.0, 2.0], [3.0, 4.0]], '$permissions' => [Permission::update(Role::any()), Permission::read(Role::any())] ]); + $createdDoc = $database->createDocument($collectionName, $doc1); $this->assertInstanceOf(Document::class, $createdDoc); $this->assertEquals([5.0, 5.0], $createdDoc->getAttribute('pointAttr')); + $this->assertEquals([[1.0, 2.0], [3.0, 4.0]], $createdDoc->getAttribute('lineAttr')); $createdDoc = $database->getDocument($collectionName, 'doc1'); - $this->assertInstanceOf(Document::class, $createdDoc); $this->assertEquals([5.0, 5.0], $createdDoc->getAttribute('pointAttr')); + $this->assertEquals([[1.0, 2.0], [3.0, 4.0]], $createdDoc->getAttribute('lineAttr')); $this->assertEquals('222','2'); - // Create test document $doc1 = new Document([ '$id' => 'doc1', From f25ede91e4b148830290d17ec5be8e8f517616bc Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 14 Sep 2025 16:17:29 +0300 Subject: [PATCH 06/29] polygon --- src/Database/Adapter.php | 2 +- src/Database/Adapter/SQL.php | 85 ++++++++++++++++++++++++++++-------- src/Database/Database.php | 19 ++++---- 3 files changed, 77 insertions(+), 29 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index b7f8378b6..6bd2c6d8e 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -1289,5 +1289,5 @@ abstract protected function execute(mixed $stmt): bool; abstract public function encodePoint(array $point): mixed; abstract public function decodePoint(string $wkb): array; abstract public function decodeLinestring(string $wkb): array; - + abstract public function decodePolygon(string $wkb): array; } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 6c6004539..568dcde84 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -2764,33 +2764,82 @@ public function decodeLinestring(string $wkb): array var_dump($wkb); - $isLE = ord($wkb[0]) === 1; // little-endian? - $type = unpack($isLE ? 'V' : 'N', substr($wkb, 1, 4))[1]; + // Skip 1 byte (endianness) + 4 bytes (type) + 4 bytes (SRID) + $offset = 9; - // Check for SRID flag (0x20000000) - $hasSRID = ($type & 0x20000000) !== 0; - $geomType = $type & 0xFFFF; // Mask lower 16 bits to get actual type - - if ($geomType !== 2) { // 2 = LINESTRING - throw new \RuntimeException("Not a LINESTRING geometry type, got {$geomType}"); - } - - $offset = 5 + ($hasSRID ? 4 : 0); // skip endian + type + optional SRID - - // Number of points (4 bytes) - $numPoints = unpack($isLE ? 'V' : 'N', substr($wkb, $offset, 4))[1]; + // Number of points (4 bytes little-endian) + $numPoints = unpack('V', substr($wkb, $offset, 4))[1]; $offset += 4; $points = []; - $fmt = $isLE ? 'e' : 'E'; // little/big endian double - for ($i = 0; $i < $numPoints; $i++) { - $x = unpack($fmt, substr($wkb, $offset, 8))[1]; - $y = unpack($fmt, substr($wkb, $offset + 8, 8))[1]; + $x = unpack('d', substr($wkb, $offset, 8))[1]; + $y = unpack('d', substr($wkb, $offset + 8, 8))[1]; $points[] = [(float)$x, (float)$y]; $offset += 16; } return $points; } + + public function decodePolygon(string $wkb): array + { + // POLYGON((x1,y1),(x2,y2)) + if (str_starts_with($wkb, 'POLYGON((')) { + $start = strpos($wkb, '((') + 2; + $end = strrpos($wkb, '))'); + $inside = substr($wkb, $start, $end - $start); + + $rings = explode('),(', $inside); + return array_map(function ($ring) { + $points = explode(',', $ring); + return array_map(function ($point) { + $coords = explode(' ', trim($point)); + return [(float)$coords[0], (float)$coords[1]]; + }, $points); + }, $rings); + } + + var_dump($wkb); + + if (strlen($wkb) < 9) { + throw new \RuntimeException('WKB too short to be a POLYGON'); + } + + $byteOrder = ord($wkb[0]); + if ($byteOrder !== 1) { + throw new \RuntimeException('Only little-endian WKB supported'); + } + + // Type + SRID flag + $typeInt = unpack('V', substr($wkb, 1, 4))[1]; + $hasSRID = ($typeInt & 0x20000000) === 0x20000000; + $geomType = $typeInt & 0x0FFFFFFF; + + if ($geomType !== 3) { // 3 = POLYGON + throw new \RuntimeException("Not a POLYGON geometry type, got {$geomType}"); + } + + $offset = 5 + ($hasSRID ? 4 : 0); // Skip endian + type + optional SRID + $format = 'd'; // little-endian double + + $numRings = unpack('V', substr($wkb, $offset, 4))[1]; + $offset += 4; + + $polygon = []; + for ($r = 0; $r < $numRings; $r++) { + $numPoints = unpack('V', substr($wkb, $offset, 4))[1]; + $offset += 4; + + $ring = []; + for ($i = 0; $i < $numPoints; $i++) { + $pt = unpack($format . '2', substr($wkb, $offset, 16)); + $ring[] = [(float)$pt[1], (float)$pt[2]]; + $offset += 16; + } + $polygon[] = $ring; + } + + return $polygon; + } } diff --git a/src/Database/Database.php b/src/Database/Database.php index 266aa912b..3540f1646 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -539,10 +539,6 @@ function (?string $value) { self::addFilter( Database::VAR_POLYGON, - /** - * @param mixed $value - * @return mixed - */ function (mixed $value) { if (!is_array($value)) { return $value; @@ -553,15 +549,18 @@ function (mixed $value) { return $value; } }, - /** - * @param string|null $value - * @return string|null - */ function (?string $value) { if (is_null($value)) { - return $value; + return null; + } + + try { + //return self::decodeSpatialData($value); + return $this->adapter->decodePolygon($value); + } catch (\Throwable $th) { + var_dump($th); + throw new StructureException($th->getMessage()); } - return self::decodeSpatialData($value); } ); } From 9e9d50a1dc19d176f64ebf550df9f95baf3356aa Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 14 Sep 2025 16:57:43 +0300 Subject: [PATCH 07/29] Postgres linestring --- src/Database/Adapter/Postgres.php | 59 +++++++++++++++++++++++ src/Database/Adapter/SQL.php | 39 ++++++++++----- src/Database/Database.php | 1 - tests/e2e/Adapter/Scopes/SpatialTests.php | 13 ++++- 4 files changed, 96 insertions(+), 16 deletions(-) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 12bdc6ce4..ed9adb4e1 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -2034,4 +2034,63 @@ public function decodePoint(mixed $wkb): array return [(float)$x, (float)$y]; } + + public function decodeLinestring(mixed $wkb): array + { + if (str_starts_with(strtoupper($wkb), 'LINESTRING(')) { + $start = strpos($wkb, '(') + 1; + $end = strrpos($wkb, ')'); + $inside = substr($wkb, $start, $end - $start); + + $points = explode(',', $inside); + return array_map(function ($point) { + $coords = explode(' ', trim($point)); + return [(float)$coords[0], (float)$coords[1]]; + }, $points); + } + + var_dump($wkb); + + if (ctype_xdigit($wkb)) { + $wkb = hex2bin($wkb); + } + + if (strlen($wkb) < 9) { + throw new DatabaseException("WKB too short to be a valid geometry"); + } + + $byteOrder = ord($wkb[0]); + if ($byteOrder === 0) { + throw new DatabaseException("Big-endian WKB not supported"); + } elseif ($byteOrder !== 1) { + throw new DatabaseException("Invalid byte order in WKB"); + } + + // Type + SRID flag + $typeField = unpack('V', substr($wkb, 1, 4))[1]; + $geomType = $typeField & 0xFF; + $hasSRID = ($typeField & 0x20000000) !== 0; + + if ($geomType !== 2) { // 2 = LINESTRING + throw new DatabaseException("Not a LINESTRING geometry type, got {$geomType}"); + } + + $offset = 5; + if ($hasSRID) { + $offset += 4; + } + + $numPoints = unpack('V', substr($wkb, $offset, 4))[1]; + $offset += 4; + + $points = []; + for ($i = 0; $i < $numPoints; $i++) { + $x = unpack('e', substr($wkb, $offset, 8))[1]; $offset += 8; + $y = unpack('e', substr($wkb, $offset, 8))[1]; $offset += 8; + $points[] = [(float)$x, (float)$y]; + } + + return $points; + } + } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 568dcde84..e959e105a 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -2802,44 +2802,57 @@ public function decodePolygon(string $wkb): array var_dump($wkb); + // Convert HEX to binary if needed + if (ctype_xdigit($wkb) && strlen($wkb) % 2 === 0) { + $wkb = hex2bin($wkb); + if ($wkb === false) { + throw new \RuntimeException("Invalid HEX WKB"); + } + } + if (strlen($wkb) < 9) { - throw new \RuntimeException('WKB too short to be a POLYGON'); + throw new \RuntimeException("WKB too short"); } $byteOrder = ord($wkb[0]); if ($byteOrder !== 1) { - throw new \RuntimeException('Only little-endian WKB supported'); + throw new \RuntimeException("Only little-endian WKB supported"); } // Type + SRID flag $typeInt = unpack('V', substr($wkb, 1, 4))[1]; - $hasSRID = ($typeInt & 0x20000000) === 0x20000000; - $geomType = $typeInt & 0x0FFFFFFF; + $hasSrid = ($typeInt & 0x20000000) !== 0; + $geomType = $typeInt & 0xFF; if ($geomType !== 3) { // 3 = POLYGON throw new \RuntimeException("Not a POLYGON geometry type, got {$geomType}"); } - $offset = 5 + ($hasSRID ? 4 : 0); // Skip endian + type + optional SRID - $format = 'd'; // little-endian double + $offset = 5 + ($hasSrid ? 4 : 0); + // Number of rings $numRings = unpack('V', substr($wkb, $offset, 4))[1]; $offset += 4; - $polygon = []; + $rings = []; + for ($r = 0; $r < $numRings; $r++) { + // Number of points in this ring $numPoints = unpack('V', substr($wkb, $offset, 4))[1]; $offset += 4; - $ring = []; - for ($i = 0; $i < $numPoints; $i++) { - $pt = unpack($format . '2', substr($wkb, $offset, 16)); - $ring[] = [(float)$pt[1], (float)$pt[2]]; + $points = []; + for ($p = 0; $p < $numPoints; $p++) { + $x = unpack('d', substr($wkb, $offset, 8))[1]; + $y = unpack('d', substr($wkb, $offset + 8, 8))[1]; + $points[] = [(float)$x, (float)$y]; $offset += 16; } - $polygon[] = $ring; + + $rings[] = $points; } - return $polygon; + return $rings; + } } diff --git a/src/Database/Database.php b/src/Database/Database.php index 3540f1646..5656b2c3e 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -531,7 +531,6 @@ function (?string $value) { //return self::decodeSpatialData($value); return $this->adapter->decodeLinestring($value); } catch (\Throwable $th) { - var_dump($th); throw new StructureException($th->getMessage()); } } diff --git a/tests/e2e/Adapter/Scopes/SpatialTests.php b/tests/e2e/Adapter/Scopes/SpatialTests.php index fab44ed24..e7a5661de 100644 --- a/tests/e2e/Adapter/Scopes/SpatialTests.php +++ b/tests/e2e/Adapter/Scopes/SpatialTests.php @@ -106,18 +106,27 @@ public function testSpatialTypeDocuments(): void // Create spatial attributes using createAttribute method $this->assertEquals(true, $database->createAttribute($collectionName, 'pointAttr', Database::VAR_POINT, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true)); $this->assertEquals(true, $database->createAttribute($collectionName, 'lineAttr', Database::VAR_LINESTRING, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true)); - //$this->assertEquals(true, $database->createAttribute($collectionName, 'polyAttr', Database::VAR_POLYGON, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true)); + // $this->assertEquals(true, $database->createAttribute($collectionName, 'polyAttr', Database::VAR_POLYGON, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true)); // Create spatial indexes $this->assertEquals(true, $database->createIndex($collectionName, 'point_spatial', Database::INDEX_SPATIAL, ['pointAttr'])); $this->assertEquals(true, $database->createIndex($collectionName, 'line_spatial', Database::INDEX_SPATIAL, ['lineAttr'])); - //$this->assertEquals(true, $database->createIndex($collectionName, 'poly_spatial', Database::INDEX_SPATIAL, ['polyAttr'])); + // $this->assertEquals(true, $database->createIndex($collectionName, 'poly_spatial', Database::INDEX_SPATIAL, ['polyAttr'])); // Create test document $doc1 = new Document([ '$id' => 'doc1', 'pointAttr' => [5.0, 5.0], 'lineAttr' => [[1.0, 2.0], [3.0, 4.0]], +// 'polyAttr' => [ +// [ +// [0.0, 0.0], +// [0.0, 10.0], +// [10.0, 10.0], +// [10.0, 0.0], +// [0.0, 0.0] +// ] +// ], '$permissions' => [Permission::update(Role::any()), Permission::read(Role::any())] ]); From 4e195f82f650c687d3d72e0fdf879c015c946198 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 14 Sep 2025 17:17:38 +0300 Subject: [PATCH 08/29] Postgres polygon --- src/Database/Adapter/Postgres.php | 71 +++++++++++++++++++++++ src/Database/Database.php | 1 - tests/e2e/Adapter/Scopes/SpatialTests.php | 17 ++---- 3 files changed, 77 insertions(+), 12 deletions(-) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index ed9adb4e1..7e12492df 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -2093,4 +2093,75 @@ public function decodeLinestring(mixed $wkb): array return $points; } + public function decodePolygon(string $wkb): array + { + // POLYGON((x1,y1),(x2,y2)) + if (str_starts_with($wkb, 'POLYGON((')) { + $start = strpos($wkb, '((') + 2; + $end = strrpos($wkb, '))'); + $inside = substr($wkb, $start, $end - $start); + + $rings = explode('),(', $inside); + return array_map(function ($ring) { + $points = explode(',', $ring); + return array_map(function ($point) { + $coords = explode(' ', trim($point)); + return [(float)$coords[0], (float)$coords[1]]; + }, $points); + }, $rings); + } + + var_dump($wkb); + + // Convert hex string to binary if needed + if (preg_match('/^[0-9a-fA-F]+$/', $wkb)) { + $wkb = hex2bin($wkb); + if ($wkb === false) { + throw new \RuntimeException("Invalid hex WKB"); + } + } + + if (strlen($wkb) < 9) { + throw new \RuntimeException("WKB too short"); + } + + $byteOrder = ord($wkb[0]); + $isLE = $byteOrder === 1; // assume little-endian + $uInt32 = 'V'; // little-endian 32-bit unsigned + $uDouble = 'd'; // little-endian double + + $typeInt = unpack($uInt32, substr($wkb, 1, 4))[1]; + $hasSrid = ($typeInt & 0x20000000) !== 0; + $geomType = $typeInt & 0xFF; + + if ($geomType !== 3) { // 3 = POLYGON + throw new \RuntimeException("Not a POLYGON geometry type, got {$geomType}"); + } + + $offset = 5; + if ($hasSrid) { + $offset += 4; + } + + // Number of rings + $numRings = unpack($uInt32, substr($wkb, $offset, 4))[1]; + $offset += 4; + + $rings = []; + for ($r = 0; $r < $numRings; $r++) { + $numPoints = unpack($uInt32, substr($wkb, $offset, 4))[1]; + $offset += 4; + $points = []; + for ($i = 0; $i < $numPoints; $i++) { + $x = unpack($uDouble, substr($wkb, $offset, 8))[1]; + $y = unpack($uDouble, substr($wkb, $offset + 8, 8))[1]; + $points[] = [(float)$x, (float)$y]; + $offset += 16; + } + $rings[] = $points; + } + + return $rings; // array of rings, each ring is array of [x,y] + } + } diff --git a/src/Database/Database.php b/src/Database/Database.php index 5656b2c3e..9b6f9d98e 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -557,7 +557,6 @@ function (?string $value) { //return self::decodeSpatialData($value); return $this->adapter->decodePolygon($value); } catch (\Throwable $th) { - var_dump($th); throw new StructureException($th->getMessage()); } } diff --git a/tests/e2e/Adapter/Scopes/SpatialTests.php b/tests/e2e/Adapter/Scopes/SpatialTests.php index e7a5661de..257b57156 100644 --- a/tests/e2e/Adapter/Scopes/SpatialTests.php +++ b/tests/e2e/Adapter/Scopes/SpatialTests.php @@ -106,27 +106,19 @@ public function testSpatialTypeDocuments(): void // Create spatial attributes using createAttribute method $this->assertEquals(true, $database->createAttribute($collectionName, 'pointAttr', Database::VAR_POINT, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true)); $this->assertEquals(true, $database->createAttribute($collectionName, 'lineAttr', Database::VAR_LINESTRING, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true)); - // $this->assertEquals(true, $database->createAttribute($collectionName, 'polyAttr', Database::VAR_POLYGON, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true)); + $this->assertEquals(true, $database->createAttribute($collectionName, 'polyAttr', Database::VAR_POLYGON, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true)); // Create spatial indexes $this->assertEquals(true, $database->createIndex($collectionName, 'point_spatial', Database::INDEX_SPATIAL, ['pointAttr'])); $this->assertEquals(true, $database->createIndex($collectionName, 'line_spatial', Database::INDEX_SPATIAL, ['lineAttr'])); - // $this->assertEquals(true, $database->createIndex($collectionName, 'poly_spatial', Database::INDEX_SPATIAL, ['polyAttr'])); + $this->assertEquals(true, $database->createIndex($collectionName, 'poly_spatial', Database::INDEX_SPATIAL, ['polyAttr'])); // Create test document $doc1 = new Document([ '$id' => 'doc1', 'pointAttr' => [5.0, 5.0], 'lineAttr' => [[1.0, 2.0], [3.0, 4.0]], -// 'polyAttr' => [ -// [ -// [0.0, 0.0], -// [0.0, 10.0], -// [10.0, 10.0], -// [10.0, 0.0], -// [0.0, 0.0] -// ] -// ], + 'polyAttr' => [[[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0], [0.0, 0.0]]], '$permissions' => [Permission::update(Role::any()), Permission::read(Role::any())] ]); @@ -134,11 +126,14 @@ public function testSpatialTypeDocuments(): void $this->assertInstanceOf(Document::class, $createdDoc); $this->assertEquals([5.0, 5.0], $createdDoc->getAttribute('pointAttr')); $this->assertEquals([[1.0, 2.0], [3.0, 4.0]], $createdDoc->getAttribute('lineAttr')); + $this->assertEquals([[[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0], [0.0, 0.0]]], $createdDoc->getAttribute('polyAttr')); $createdDoc = $database->getDocument($collectionName, 'doc1'); $this->assertInstanceOf(Document::class, $createdDoc); $this->assertEquals([5.0, 5.0], $createdDoc->getAttribute('pointAttr')); $this->assertEquals([[1.0, 2.0], [3.0, 4.0]], $createdDoc->getAttribute('lineAttr')); + $this->assertEquals([[[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0], [0.0, 0.0]]], $createdDoc->getAttribute('polyAttr')); + $this->assertEquals('222','2'); From bb4b3e48ed40e32a656c964bbf59e5858f2ce81b Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 14 Sep 2025 17:40:16 +0300 Subject: [PATCH 09/29] Mysql polygon --- src/Database/Adapter/SQL.php | 43 ++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index e959e105a..f73b03d8b 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -2802,54 +2802,59 @@ public function decodePolygon(string $wkb): array var_dump($wkb); - // Convert HEX to binary if needed - if (ctype_xdigit($wkb) && strlen($wkb) % 2 === 0) { - $wkb = hex2bin($wkb); + // Convert HEX string to binary if needed + if (str_starts_with($wkb, '0x') || ctype_xdigit($wkb)) { + $wkb = hex2bin(str_starts_with($wkb, '0x') ? substr($wkb, 2) : $wkb); if ($wkb === false) { - throw new \RuntimeException("Invalid HEX WKB"); + throw new DatabaseException('Invalid hex WKB'); } } - if (strlen($wkb) < 9) { - throw new \RuntimeException("WKB too short"); + if (strlen($wkb) < 21) { + throw new DatabaseException('WKB too short to be a POLYGON'); } - $byteOrder = ord($wkb[0]); + // MySQL SRID-aware WKB layout: 4 bytes SRID prefix + $offset = 4; + + $byteOrder = ord($wkb[$offset]); if ($byteOrder !== 1) { - throw new \RuntimeException("Only little-endian WKB supported"); + throw new DatabaseException('Only little-endian WKB supported'); } + $offset += 1; - // Type + SRID flag - $typeInt = unpack('V', substr($wkb, 1, 4))[1]; - $hasSrid = ($typeInt & 0x20000000) !== 0; - $geomType = $typeInt & 0xFF; + $type = unpack('V', substr($wkb, $offset, 4))[1]; + $hasSRID = ($type & 0x20000000) === 0x20000000; + $geomType = $type & 0xFF; + $offset += 4; if ($geomType !== 3) { // 3 = POLYGON - throw new \RuntimeException("Not a POLYGON geometry type, got {$geomType}"); + throw new DatabaseException("Not a POLYGON geometry type, got {$geomType}"); } - $offset = 5 + ($hasSrid ? 4 : 0); + // Skip SRID in type flag if present + if ($hasSRID) { + $offset += 4; + } - // Number of rings $numRings = unpack('V', substr($wkb, $offset, 4))[1]; $offset += 4; $rings = []; for ($r = 0; $r < $numRings; $r++) { - // Number of points in this ring $numPoints = unpack('V', substr($wkb, $offset, 4))[1]; $offset += 4; + $ring = []; - $points = []; for ($p = 0; $p < $numPoints; $p++) { $x = unpack('d', substr($wkb, $offset, 8))[1]; $y = unpack('d', substr($wkb, $offset + 8, 8))[1]; - $points[] = [(float)$x, (float)$y]; + $ring[] = [(float)$x, (float)$y]; $offset += 16; } - $rings[] = $points; + $rings[] = $ring; } return $rings; From b94dada5fa5f28d4fd1913718b07e8fbe81cf4f9 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 14 Sep 2025 17:58:34 +0300 Subject: [PATCH 10/29] clean var_dump --- src/Database/Adapter.php | 1 - src/Database/Adapter/SQL.php | 10 ----- tests/e2e/Adapter/Scopes/SpatialTests.php | 48 +++++++---------------- 3 files changed, 14 insertions(+), 45 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 6bd2c6d8e..831b87064 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -1286,7 +1286,6 @@ abstract public function getTenantQuery(string $collection, string $alias = ''): */ abstract protected function execute(mixed $stmt): bool; - abstract public function encodePoint(array $point): mixed; abstract public function decodePoint(string $wkb): array; abstract public function decodeLinestring(string $wkb): array; abstract public function decodePolygon(string $wkb): array; diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index f73b03d8b..aabe0bf4d 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -2711,20 +2711,12 @@ public function getSpatialTypeFromWKT(string $wkt): string return strtolower(trim(substr($wkt, 0, $pos))); } - public function encodePoint(array $point): string - { - return "POINT({$point[0]} {$point[1]})"; - } - public function decodePoint(mixed $wkb): array { - var_dump($wkb); - if (str_starts_with(strtoupper($wkb), 'POINT(')) { $start = strpos($wkb, '(') + 1; $end = strrpos($wkb, ')'); $inside = substr($wkb, $start, $end - $start); - $coords = explode(' ', trim($inside)); return [(float)$coords[0], (float)$coords[1]]; } @@ -2800,8 +2792,6 @@ public function decodePolygon(string $wkb): array }, $rings); } - var_dump($wkb); - // Convert HEX string to binary if needed if (str_starts_with($wkb, '0x') || ctype_xdigit($wkb)) { $wkb = hex2bin(str_starts_with($wkb, '0x') ? substr($wkb, 2) : $wkb); diff --git a/tests/e2e/Adapter/Scopes/SpatialTests.php b/tests/e2e/Adapter/Scopes/SpatialTests.php index 257b57156..e9839b7de 100644 --- a/tests/e2e/Adapter/Scopes/SpatialTests.php +++ b/tests/e2e/Adapter/Scopes/SpatialTests.php @@ -113,49 +113,29 @@ public function testSpatialTypeDocuments(): void $this->assertEquals(true, $database->createIndex($collectionName, 'line_spatial', Database::INDEX_SPATIAL, ['lineAttr'])); $this->assertEquals(true, $database->createIndex($collectionName, 'poly_spatial', Database::INDEX_SPATIAL, ['polyAttr'])); + $point = [5.0, 5.0]; + $linestring = [[1.0, 2.0], [3.0, 4.0]]; + $polygon = [[[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0], [0.0, 0.0]]]; + // Create test document $doc1 = new Document([ '$id' => 'doc1', - 'pointAttr' => [5.0, 5.0], - 'lineAttr' => [[1.0, 2.0], [3.0, 4.0]], - 'polyAttr' => [[[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0], [0.0, 0.0]]], + 'pointAttr' => $point, + 'lineAttr' => $linestring, + 'polyAttr' => $polygon, '$permissions' => [Permission::update(Role::any()), Permission::read(Role::any())] ]); - $createdDoc = $database->createDocument($collectionName, $doc1); $this->assertInstanceOf(Document::class, $createdDoc); - $this->assertEquals([5.0, 5.0], $createdDoc->getAttribute('pointAttr')); - $this->assertEquals([[1.0, 2.0], [3.0, 4.0]], $createdDoc->getAttribute('lineAttr')); - $this->assertEquals([[[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0], [0.0, 0.0]]], $createdDoc->getAttribute('polyAttr')); + $this->assertEquals($point, $createdDoc->getAttribute('pointAttr')); + $this->assertEquals($linestring, $createdDoc->getAttribute('lineAttr')); + $this->assertEquals($polygon, $createdDoc->getAttribute('polyAttr')); $createdDoc = $database->getDocument($collectionName, 'doc1'); $this->assertInstanceOf(Document::class, $createdDoc); - $this->assertEquals([5.0, 5.0], $createdDoc->getAttribute('pointAttr')); - $this->assertEquals([[1.0, 2.0], [3.0, 4.0]], $createdDoc->getAttribute('lineAttr')); - $this->assertEquals([[[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [10.0, 0.0], [0.0, 0.0]]], $createdDoc->getAttribute('polyAttr')); - - $this->assertEquals('222','2'); - - - // Create test document - $doc1 = new Document([ - '$id' => 'doc1', - 'pointAttr' => [5.0, 5.0], - 'lineAttr' => [[1.0, 2.0], [3.0, 4.0]], - 'polyAttr' => [ - [ - [0.0, 0.0], - [0.0, 10.0], - [10.0, 10.0], - [10.0, 0.0], - [0.0, 0.0] - ] - ], - '$permissions' => [Permission::update(Role::any()), Permission::read(Role::any())] - ]); - $createdDoc = $database->createDocument($collectionName, $doc1); - $this->assertInstanceOf(Document::class, $createdDoc); - $this->assertEquals([5.0, 5.0], $createdDoc->getAttribute('pointAttr')); + $this->assertEquals($point, $createdDoc->getAttribute('pointAttr')); + $this->assertEquals($linestring, $createdDoc->getAttribute('lineAttr')); + $this->assertEquals($polygon, $createdDoc->getAttribute('polyAttr')); // Update spatial data $doc1->setAttribute('pointAttr', [6.0, 6.0]); @@ -257,7 +237,7 @@ public function testSpatialTypeDocuments(): void $this->assertEquals('doc1', $result[0]->getId(), sprintf('Incorrect document for distance %s on polyAttr', $queryType)); } } finally { - //$database->deleteCollection($collectionName); + $database->deleteCollection($collectionName); } } From 7f7dfbb2eeead078c0b848008112f8cc32a915c5 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 15 Sep 2025 08:28:18 +0300 Subject: [PATCH 11/29] Remove decodeSpatialData method --- src/Database/Database.php | 54 ------------------------------ src/Database/Validator/Spatial.php | 7 ---- 2 files changed, 61 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 9b6f9d98e..23de3f3c5 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -554,7 +554,6 @@ function (?string $value) { } try { - //return self::decodeSpatialData($value); return $this->adapter->decodePolygon($value); } catch (\Throwable $th) { throw new StructureException($th->getMessage()); @@ -7324,57 +7323,4 @@ protected function encodeSpatialData(mixed $value, string $type): string throw new DatabaseException('Unknown spatial type: ' . $type); } } - - /** - * Decode spatial data from WKT (Well-Known Text) format to array format - * - * @param string $wkt - * @return array - * @throws DatabaseException - */ - public function decodeSpatialData(string $wkt): array - { - $upper = strtoupper($wkt); - - // POINT(x y) - if (str_starts_with($upper, 'POINT(')) { - $start = strpos($wkt, '(') + 1; - $end = strrpos($wkt, ')'); - $inside = substr($wkt, $start, $end - $start); - - $coords = explode(' ', trim($inside)); - return [(float)$coords[0], (float)$coords[1]]; - } - - // LINESTRING(x1 y1, x2 y2, ...) - if (str_starts_with($upper, 'LINESTRING(')) { - $start = strpos($wkt, '(') + 1; - $end = strrpos($wkt, ')'); - $inside = substr($wkt, $start, $end - $start); - - $points = explode(',', $inside); - return array_map(function ($point) { - $coords = explode(' ', trim($point)); - return [(float)$coords[0], (float)$coords[1]]; - }, $points); - } - - // POLYGON((x1,y1),(x2,y2)) - if (str_starts_with($upper, 'POLYGON((')) { - $start = strpos($wkt, '((') + 2; - $end = strrpos($wkt, '))'); - $inside = substr($wkt, $start, $end - $start); - - $rings = explode('),(', $inside); - return array_map(function ($ring) { - $points = explode(',', $ring); - return array_map(function ($point) { - $coords = explode(' ', trim($point)); - return [(float)$coords[0], (float)$coords[1]]; - }, $points); - }, $rings); - } - - return [$wkt]; - } } diff --git a/src/Database/Validator/Spatial.php b/src/Database/Validator/Spatial.php index cc3ea83d4..912f05b2b 100644 --- a/src/Database/Validator/Spatial.php +++ b/src/Database/Validator/Spatial.php @@ -134,13 +134,6 @@ protected function validatePolygon(array $value): bool */ public static function isWKTString(string $value): bool { - - /** - * We need to decode the value first - */ - - // return true; - $value = trim($value); return (bool) preg_match('/^(POINT|LINESTRING|POLYGON)\s*\(/i', $value); } From 773df87a340ad921a7e560cc31679246ca6481b8 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 15 Sep 2025 08:31:50 +0300 Subject: [PATCH 12/29] Remove try catch --- src/Database/Adapter/Postgres.php | 6 ------ src/Database/Database.php | 29 ++++------------------------- 2 files changed, 4 insertions(+), 31 deletions(-) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 7e12492df..cb2b71aca 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -2003,12 +2003,6 @@ public function getSupportForSpatialAxisOrder(): bool return false; } - public function encodePoint(array $point): string - { - return "POINT({$point[0]} {$point[1]})";// EWKT - //return "SRID=4326;POINT({$point[0]} {$point[1]})";// EWKT - } - public function decodePoint(mixed $wkb): array { //$wkb = str_replace('SRID=4326;', '', $wkb); // Remove if was added in encode diff --git a/src/Database/Database.php b/src/Database/Database.php index 23de3f3c5..428360046 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -460,7 +460,7 @@ function (mixed $value) { */ function (mixed $value) { if (is_null($value)) { - return; + return null; } try { $value = new \DateTime($value); @@ -487,7 +487,6 @@ function (mixed $value) { } try { return self::encodeSpatialData($value, Database::VAR_POINT); - //return $this->adapter->encodePoint($value); } catch (\Throwable) { throw new StructureException('Invalid point'); } @@ -496,16 +495,7 @@ function (?string $value) { if ($value === null) { return null; } - - /** - * todo:validate array point - */ - - try { - return $this->adapter->decodePoint($value); - } catch (\Throwable $th) { - throw new StructureException($th->getMessage()); - } + return $this->adapter->decodePoint($value); } ); @@ -526,13 +516,7 @@ function (?string $value) { if (is_null($value)) { return null; } - - try { - //return self::decodeSpatialData($value); - return $this->adapter->decodeLinestring($value); - } catch (\Throwable $th) { - throw new StructureException($th->getMessage()); - } + return $this->adapter->decodeLinestring($value); } ); @@ -552,12 +536,7 @@ function (?string $value) { if (is_null($value)) { return null; } - - try { - return $this->adapter->decodePolygon($value); - } catch (\Throwable $th) { - throw new StructureException($th->getMessage()); - } + return $this->adapter->decodePolygon($value); } ); } From f6bd630d6a5e84e6fbd5024a098f45a74669062e Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 15 Sep 2025 08:40:23 +0300 Subject: [PATCH 13/29] Add hints --- src/Database/Database.php | 33 +++++++++++++++++++---- tests/e2e/Adapter/Scopes/SpatialTests.php | 1 + 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 428360046..3e99dfb92 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -481,16 +481,24 @@ function (?string $value) { self::addFilter( Database::VAR_POINT, + /** + * @param mixed $value + * @return mixed + */ function (mixed $value) { - if ($value === null) { - return null; + if (!is_array($value)) { + return $value; } try { - return self::encodeSpatialData($value, Database::VAR_POINT); + return self::encodeSpatialData($value, Database::VAR_POINT); } catch (\Throwable) { - throw new StructureException('Invalid point'); + return $value; } }, + /** + * @param string|null $value + * @return string|null + */ function (?string $value) { if ($value === null) { return null; @@ -501,6 +509,10 @@ function (?string $value) { self::addFilter( Database::VAR_LINESTRING, + /** + * @param mixed $value + * @return mixed + */ function (mixed $value) { if (!is_array($value)) { return $value; @@ -511,7 +523,10 @@ function (mixed $value) { return $value; } }, - + /** + * @param string|null $value + * @return string|null + */ function (?string $value) { if (is_null($value)) { return null; @@ -522,6 +537,10 @@ function (?string $value) { self::addFilter( Database::VAR_POLYGON, + /** + * @param mixed $value + * @return mixed + */ function (mixed $value) { if (!is_array($value)) { return $value; @@ -532,6 +551,10 @@ function (mixed $value) { return $value; } }, + /** + * @param string|null $value + * @return string|null + */ function (?string $value) { if (is_null($value)) { return null; diff --git a/tests/e2e/Adapter/Scopes/SpatialTests.php b/tests/e2e/Adapter/Scopes/SpatialTests.php index e9839b7de..0aaf01928 100644 --- a/tests/e2e/Adapter/Scopes/SpatialTests.php +++ b/tests/e2e/Adapter/Scopes/SpatialTests.php @@ -2101,6 +2101,7 @@ public function testInvalidSpatialTypes(): void ])); $this->fail("Expected StructureException for invalid point"); } catch (\Throwable $th) { + var_dump($th); $this->assertInstanceOf(StructureException::class, $th); } From cc972d2b897f79c75e64876b792a77a7c472cc0e Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 15 Sep 2025 16:20:59 +0300 Subject: [PATCH 14/29] dbg --- src/Database/Adapter/MariaDB.php | 2 +- src/Database/Adapter/Postgres.php | 6 +----- src/Database/Adapter/SQL.php | 4 +--- src/Database/Database.php | 6 +++--- tests/e2e/Adapter/Base.php | 12 ++++++------ tests/e2e/Adapter/Scopes/SpatialTests.php | 1 - 6 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 77b3647e5..c78d6637c 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -864,7 +864,7 @@ public function createDocument(Document $collection, Document $document): Docume INSERT INTO {$this->getSQLTable($name)} ({$columns} _uid) VALUES ({$columnNames} :_uid) "; -var_dump($sql); + $sql = $this->trigger(Database::EVENT_DOCUMENT_CREATE, $sql); $stmt = $this->getPDO()->prepare($sql); diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index cb2b71aca..f7bb3c29a 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -993,7 +993,7 @@ public function createDocument(Document $collection, Document $document): Docume INSERT INTO {$this->getSQLTable($name)} ({$columns} \"_uid\") VALUES ({$columnNames} :_uid) "; -var_dump($sql); + $sql = $this->trigger(Database::EVENT_DOCUMENT_CREATE, $sql); $stmt = $this->getPDO()->prepare($sql); @@ -2043,8 +2043,6 @@ public function decodeLinestring(mixed $wkb): array }, $points); } - var_dump($wkb); - if (ctype_xdigit($wkb)) { $wkb = hex2bin($wkb); } @@ -2105,8 +2103,6 @@ public function decodePolygon(string $wkb): array }, $rings); } - var_dump($wkb); - // Convert hex string to binary if needed if (preg_match('/^[0-9a-fA-F]+$/', $wkb)) { $wkb = hex2bin($wkb); diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index aabe0bf4d..1f734c332 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -366,7 +366,7 @@ public function getDocument(Document $collection, string $id, array $queries = [ WHERE {$this->quote($alias)}.{$this->quote('_uid')} = :_uid {$this->getTenantQuery($collection, $alias)} "; -var_dump($sql); + if ($this->getSupportForUpdateLock()) { $sql .= " {$forUpdate}"; } @@ -2754,8 +2754,6 @@ public function decodeLinestring(string $wkb): array }, $points); } - var_dump($wkb); - // Skip 1 byte (endianness) + 4 bytes (type) + 4 bytes (SRID) $offset = 9; diff --git a/src/Database/Database.php b/src/Database/Database.php index 3e99dfb92..584a4d942 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -497,7 +497,7 @@ function (mixed $value) { }, /** * @param string|null $value - * @return string|null + * @return array|null */ function (?string $value) { if ($value === null) { @@ -525,7 +525,7 @@ function (mixed $value) { }, /** * @param string|null $value - * @return string|null + * @return array|null */ function (?string $value) { if (is_null($value)) { @@ -553,7 +553,7 @@ function (mixed $value) { }, /** * @param string|null $value - * @return string|null + * @return array|null */ function (?string $value) { if (is_null($value)) { diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 481704ce6..37ad7cce3 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -18,12 +18,12 @@ abstract class Base extends TestCase { -// use CollectionTests; -// use DocumentTests; -// use AttributeTests; -// use IndexTests; -// use PermissionTests; -// use RelationshipTests; + use CollectionTests; + use DocumentTests; + use AttributeTests; + use IndexTests; + use PermissionTests; + use RelationshipTests; use SpatialTests; use GeneralTests; diff --git a/tests/e2e/Adapter/Scopes/SpatialTests.php b/tests/e2e/Adapter/Scopes/SpatialTests.php index 0aaf01928..e9839b7de 100644 --- a/tests/e2e/Adapter/Scopes/SpatialTests.php +++ b/tests/e2e/Adapter/Scopes/SpatialTests.php @@ -2101,7 +2101,6 @@ public function testInvalidSpatialTypes(): void ])); $this->fail("Expected StructureException for invalid point"); } catch (\Throwable $th) { - var_dump($th); $this->assertInstanceOf(StructureException::class, $th); } From 1b9b095e534cf6454d9e79eecb024d0fb74d8772 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 15 Sep 2025 16:22:31 +0300 Subject: [PATCH 15/29] formatting --- src/Database/Adapter/Postgres.php | 6 ++++-- src/Database/Adapter/SQL.php | 2 +- src/Database/Database.php | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index f7bb3c29a..4b4c2155a 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -2077,8 +2077,10 @@ public function decodeLinestring(mixed $wkb): array $points = []; for ($i = 0; $i < $numPoints; $i++) { - $x = unpack('e', substr($wkb, $offset, 8))[1]; $offset += 8; - $y = unpack('e', substr($wkb, $offset, 8))[1]; $offset += 8; + $x = unpack('e', substr($wkb, $offset, 8))[1]; + $offset += 8; + $y = unpack('e', substr($wkb, $offset, 8))[1]; + $offset += 8; $points[] = [(float)$x, (float)$y]; } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 1f734c332..0134634a7 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -359,7 +359,7 @@ public function getDocument(Document $collection, string $id, array $queries = [ $forUpdate = $forUpdate ? 'FOR UPDATE' : ''; $alias = Query::DEFAULT_ALIAS; - $spatialAttributes=[]; + $spatialAttributes = []; $sql = " SELECT {$this->getAttributeProjection($selections, $alias, $spatialAttributes)} FROM {$this->getSQLTable($name)} AS {$this->quote($alias)} diff --git a/src/Database/Database.php b/src/Database/Database.php index 584a4d942..90a6d378f 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -460,7 +460,7 @@ function (mixed $value) { */ function (mixed $value) { if (is_null($value)) { - return null; + return; } try { $value = new \DateTime($value); From 9a0e28517a3a34c0142a119146ebfe097894242d Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 15 Sep 2025 16:33:42 +0300 Subject: [PATCH 16/29] signature --- src/Database/Adapter/Postgres.php | 4 +--- src/Database/Adapter/SQL.php | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 4b4c2155a..38807e442 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -2003,10 +2003,8 @@ public function getSupportForSpatialAxisOrder(): bool return false; } - public function decodePoint(mixed $wkb): array + public function decodePoint(string $wkb): array { - //$wkb = str_replace('SRID=4326;', '', $wkb); // Remove if was added in encode - if (str_starts_with(strtoupper($wkb), 'POINT(')) { $start = strpos($wkb, '(') + 1; $end = strrpos($wkb, ')'); diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 0134634a7..b74d468b7 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -2711,7 +2711,7 @@ public function getSpatialTypeFromWKT(string $wkt): string return strtolower(trim(substr($wkt, 0, $pos))); } - public function decodePoint(mixed $wkb): array + public function decodePoint(string $wkb): array { if (str_starts_with(strtoupper($wkb), 'POINT(')) { $start = strpos($wkb, '(') + 1; From 5d2c0c3ba53218ceb0c3a0c11df87efbf7f862c6 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 15 Sep 2025 16:38:47 +0300 Subject: [PATCH 17/29] fix Pool adapter --- src/Database/Adapter/Pool.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Database/Adapter/Pool.php b/src/Database/Adapter/Pool.php index 4d95611e1..3a4faa56a 100644 --- a/src/Database/Adapter/Pool.php +++ b/src/Database/Adapter/Pool.php @@ -549,4 +549,19 @@ public function getSupportForSpatialAxisOrder(): bool { return $this->delegate(__FUNCTION__, \func_get_args()); } + + public function decodePoint(string $wkb): array + { + return $this->delegate(__FUNCTION__, \func_get_args()); + } + + public function decodeLinestring(string $wkb): array + { + return $this->delegate(__FUNCTION__, \func_get_args()); + } + + public function decodePolygon(string $wkb): array + { + return $this->delegate(__FUNCTION__, \func_get_args()); + } } From 44cc3f34be4bcc9d1ea5fec778566c6ded037d2c Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 15 Sep 2025 17:10:39 +0300 Subject: [PATCH 18/29] formatting --- src/Database/Adapter.php | 20 ++++++++++++ src/Database/Adapter/SQL.php | 59 ++++++++++++++++++++++++++++-------- tests/e2e/Adapter/Base.php | 12 ++++---- 3 files changed, 73 insertions(+), 18 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 831b87064..5cd7c8a41 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -1286,7 +1286,27 @@ abstract public function getTenantQuery(string $collection, string $alias = ''): */ abstract protected function execute(mixed $stmt): bool; + /** + * Decode a WKB or textual POINT into [x, y] + * + * @param string $wkb + * @return float[] Array with two elements: [x, y] + */ abstract public function decodePoint(string $wkb): array; + + /** + * Decode a WKB or textual LINESTRING into [[x1, y1], [x2, y2], ...] + * + * @param string $wkb + * @return float[][] Array of points, each as [x, y] + */ abstract public function decodeLinestring(string $wkb): array; + + /** + * Decode a WKB or textual POLYGON into [[[x1, y1], [x2, y2], ...], ...] + * + * @param string $wkb + * @return float[][][] Array of rings, each ring is an array of points [x, y] + */ abstract public function decodePolygon(string $wkb): array; } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index b74d468b7..e2a8f775d 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -2737,7 +2737,11 @@ public function decodePoint(string $wkb): array $format = $littleEndian ? 'd2' : 'd2'; // little-endian doubles $coords = unpack($format, $coordsBin); - return [$coords[1], $coords[2]]; + if ($coords === false || !isset($coords[1], $coords[2])) { + throw new DatabaseException('Invalid WKB for POINT: cannot unpack coordinates'); + } + + return [(float)$coords[1], (float)$coords[2]]; } public function decodeLinestring(string $wkb): array @@ -2758,14 +2762,24 @@ public function decodeLinestring(string $wkb): array $offset = 9; // Number of points (4 bytes little-endian) - $numPoints = unpack('V', substr($wkb, $offset, 4))[1]; + $numPointsArr = unpack('V', substr($wkb, $offset, 4)); + if ($numPointsArr === false || !isset($numPointsArr[1])) { + throw new DatabaseException('Invalid WKB: cannot unpack number of points'); + } + + $numPoints = $numPointsArr[1]; $offset += 4; $points = []; for ($i = 0; $i < $numPoints; $i++) { - $x = unpack('d', substr($wkb, $offset, 8))[1]; - $y = unpack('d', substr($wkb, $offset + 8, 8))[1]; - $points[] = [(float)$x, (float)$y]; + $xArr = unpack('d', substr($wkb, $offset, 8)); + $yArr = unpack('d', substr($wkb, $offset + 8, 8)); + + if ($xArr === false || !isset($xArr[1]) || $yArr === false || !isset($yArr[1])) { + throw new DatabaseException('Invalid WKB: cannot unpack point coordinates'); + } + + $points[] = [(float)$xArr[1], (float)$yArr[1]]; $offset += 16; } @@ -2811,7 +2825,12 @@ public function decodePolygon(string $wkb): array } $offset += 1; - $type = unpack('V', substr($wkb, $offset, 4))[1]; + $typeArr = unpack('V', substr($wkb, $offset, 4)); + if ($typeArr === false || !isset($typeArr[1])) { + throw new DatabaseException('Invalid WKB: cannot unpack geometry type'); + } + + $type = $typeArr[1]; $hasSRID = ($type & 0x20000000) === 0x20000000; $geomType = $type & 0xFF; $offset += 4; @@ -2825,20 +2844,37 @@ public function decodePolygon(string $wkb): array $offset += 4; } - $numRings = unpack('V', substr($wkb, $offset, 4))[1]; + $numRingsArr = unpack('V', substr($wkb, $offset, 4)); + + if ($numRingsArr === false || !isset($numRingsArr[1])) { + throw new DatabaseException('Invalid WKB: cannot unpack number of rings'); + } + + $numRings = $numRingsArr[1]; $offset += 4; $rings = []; for ($r = 0; $r < $numRings; $r++) { - $numPoints = unpack('V', substr($wkb, $offset, 4))[1]; + $numPointsArr = unpack('V', substr($wkb, $offset, 4)); + + if ($numPointsArr === false || !isset($numPointsArr[1])) { + throw new DatabaseException('Invalid WKB: cannot unpack number of points'); + } + + $numPoints = $numPointsArr[1]; $offset += 4; $ring = []; for ($p = 0; $p < $numPoints; $p++) { - $x = unpack('d', substr($wkb, $offset, 8))[1]; - $y = unpack('d', substr($wkb, $offset + 8, 8))[1]; - $ring[] = [(float)$x, (float)$y]; + $xArr = unpack('d', substr($wkb, $offset, 8)); + $yArr = unpack('d', substr($wkb, $offset + 8, 8)); + + if ($xArr === false || $yArr === false || !isset($xArr[1], $yArr[1])) { + throw new DatabaseException('Invalid WKB: cannot unpack point coordinates'); + } + + $ring[] = [(float)$xArr[1], (float)$yArr[1]]; $offset += 16; } @@ -2846,6 +2882,5 @@ public function decodePolygon(string $wkb): array } return $rings; - } } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 37ad7cce3..481704ce6 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -18,12 +18,12 @@ abstract class Base extends TestCase { - use CollectionTests; - use DocumentTests; - use AttributeTests; - use IndexTests; - use PermissionTests; - use RelationshipTests; +// use CollectionTests; +// use DocumentTests; +// use AttributeTests; +// use IndexTests; +// use PermissionTests; +// use RelationshipTests; use SpatialTests; use GeneralTests; From 7849c6c0a393f38cb1cf5644e991c8a4f7700ec1 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 15 Sep 2025 17:56:52 +0300 Subject: [PATCH 19/29] formatting --- src/Database/Adapter/Postgres.php | 135 +++++++++++++++++++++++++----- 1 file changed, 114 insertions(+), 21 deletions(-) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 38807e442..c3524d36b 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -2015,16 +2015,41 @@ public function decodePoint(string $wkb): array } $bin = hex2bin($wkb); + if ($bin === false) { + throw new \RuntimeException('Invalid hex WKB string'); + } $isLE = ord($bin[0]) === 1; - $type = unpack($isLE ? 'V' : 'N', substr($bin, 1, 4))[1]; + $bytes = substr($bin, 1, 4); + if (strlen($bytes) < 4) { + throw new \RuntimeException('WKB too short to read type'); + } + + $unpacked = unpack($isLE ? 'V' : 'N', $bytes); + if ($unpacked === false) { + throw new \RuntimeException('Failed to unpack type from WKB'); + } + + $type = $unpacked[1]; $offset = 5 + (($type & 0x20000000) ? 4 : 0); $fmt = $isLE ? 'e' : 'E'; // little vs big endian double - $x = unpack($fmt, substr($bin, $offset, 8))[1]; - $y = unpack($fmt, substr($bin, $offset + 8, 8))[1]; - return [(float)$x, (float)$y]; + $unpacked = unpack($fmt, substr($bin, $offset, 8)); + if ($unpacked === false) { + throw new \RuntimeException('Failed to unpack double from WKB'); + } + + $x = (float)$unpacked[1]; + + $unpackedY = unpack($fmt, substr($bin, $offset + 8, 8)); + if ($unpackedY === false) { + throw new \RuntimeException('Failed to unpack Y coordinate from WKB'); + } + + $y = (float)$unpackedY[1]; + + return [$x, $y]; } public function decodeLinestring(mixed $wkb): array @@ -2043,26 +2068,36 @@ public function decodeLinestring(mixed $wkb): array if (ctype_xdigit($wkb)) { $wkb = hex2bin($wkb); + if ($wkb === false) { + throw new \RuntimeException("Failed to convert hex WKB to binary."); + } } if (strlen($wkb) < 9) { - throw new DatabaseException("WKB too short to be a valid geometry"); + throw new \RuntimeException("WKB too short to be a valid geometry"); } $byteOrder = ord($wkb[0]); if ($byteOrder === 0) { - throw new DatabaseException("Big-endian WKB not supported"); + throw new \RuntimeException("Big-endian WKB not supported"); } elseif ($byteOrder !== 1) { - throw new DatabaseException("Invalid byte order in WKB"); + throw new \RuntimeException("Invalid byte order in WKB"); } // Type + SRID flag - $typeField = unpack('V', substr($wkb, 1, 4))[1]; + $typeFieldBytes = substr($wkb, 1, 4); + $typeField = unpack('V', $typeFieldBytes); + + if ($typeField === false) { + throw new \RuntimeException('Failed to unpack the type field from WKB.'); + } + + $typeField = $typeField[1]; $geomType = $typeField & 0xFF; $hasSRID = ($typeField & 0x20000000) !== 0; if ($geomType !== 2) { // 2 = LINESTRING - throw new DatabaseException("Not a LINESTRING geometry type, got {$geomType}"); + throw new \RuntimeException("Not a LINESTRING geometry type, got {$geomType}"); } $offset = 5; @@ -2070,16 +2105,39 @@ public function decodeLinestring(mixed $wkb): array $offset += 4; } - $numPoints = unpack('V', substr($wkb, $offset, 4))[1]; + $numPointsBytes = substr($wkb, $offset, 4); + $numPointsUnpacked = unpack('V', $numPointsBytes); + + if ($numPointsUnpacked === false || !isset($numPointsUnpacked[1])) { + throw new \RuntimeException("Failed to unpack number of points at offset {$offset}."); + } + + $numPoints = $numPointsUnpacked[1]; $offset += 4; $points = []; for ($i = 0; $i < $numPoints; $i++) { - $x = unpack('e', substr($wkb, $offset, 8))[1]; + $xBytes = substr($wkb, $offset, 8); + $xUnpacked = unpack('e', $xBytes); + + if ($xUnpacked === false) { + throw new \RuntimeException("Failed to unpack X coordinate at offset {$offset}."); + } + + $x = (float) $xUnpacked[1]; + $offset += 8; - $y = unpack('e', substr($wkb, $offset, 8))[1]; + $yBytes = substr($wkb, $offset, 8); + $yUnpacked = unpack('e', $yBytes); + + if ($yUnpacked === false || !isset($yUnpacked[1])) { + throw new \RuntimeException("Failed to unpack Y coordinate at offset {$offset}."); + } + + $y = (float) $yUnpacked[1]; + $offset += 8; - $points[] = [(float)$x, (float)$y]; + $points[] = [$x, $y]; } return $points; @@ -2115,12 +2173,17 @@ public function decodePolygon(string $wkb): array throw new \RuntimeException("WKB too short"); } - $byteOrder = ord($wkb[0]); - $isLE = $byteOrder === 1; // assume little-endian $uInt32 = 'V'; // little-endian 32-bit unsigned $uDouble = 'd'; // little-endian double - $typeInt = unpack($uInt32, substr($wkb, 1, 4))[1]; + $bytes = substr($wkb, 1, 4); + $unpacked = unpack($uInt32, $bytes); + + if ($unpacked === false || !isset($unpacked[1])) { + throw new \RuntimeException('Failed to unpack type field from WKB.'); + } + + $typeInt = (int) $unpacked[1]; $hasSrid = ($typeInt & 0x20000000) !== 0; $geomType = $typeInt & 0xFF; @@ -2134,18 +2197,48 @@ public function decodePolygon(string $wkb): array } // Number of rings - $numRings = unpack($uInt32, substr($wkb, $offset, 4))[1]; + $bytes = substr($wkb, $offset, 4); + $unpacked = unpack($uInt32, $bytes); + + if ($unpacked === false || !isset($unpacked[1])) { + throw new \RuntimeException('Failed to unpack number of rings from WKB.'); + } + + $numRings = (int) $unpacked[1]; $offset += 4; $rings = []; for ($r = 0; $r < $numRings; $r++) { - $numPoints = unpack($uInt32, substr($wkb, $offset, 4))[1]; + $bytes = substr($wkb, $offset, 4); + $unpacked = unpack($uInt32, $bytes); + + if ($unpacked === false || !isset($unpacked[1])) { + throw new \RuntimeException('Failed to unpack number of points from WKB.'); + } + + $numPoints = (int) $unpacked[1]; $offset += 4; $points = []; for ($i = 0; $i < $numPoints; $i++) { - $x = unpack($uDouble, substr($wkb, $offset, 8))[1]; - $y = unpack($uDouble, substr($wkb, $offset + 8, 8))[1]; - $points[] = [(float)$x, (float)$y]; + $bytes = substr($wkb, $offset, 8); + $unpacked = unpack($uDouble, $bytes); + + if ($unpacked === false) { + throw new \RuntimeException('Failed to unpack X coordinate from WKB.'); + } + + $x = (float) $unpacked[1]; + + $bytes = substr($wkb, $offset + 8, 8); + $unpacked = unpack($uDouble, $bytes); + + if ($unpacked === false || !isset($unpacked[1])) { + throw new \RuntimeException('Failed to unpack Y coordinate from WKB.'); + } + + $y = (float) $unpacked[1]; + + $points[] = [$x, $y]; $offset += 16; } $rings[] = $points; From 2aed9dc6ff1004ec80b79934a1c87b683f714ad9 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 15 Sep 2025 18:05:54 +0300 Subject: [PATCH 20/29] fix getAttributeProjection --- src/Database/Adapter.php | 4 ++-- src/Database/Adapter/Pool.php | 8 +------ src/Database/Adapter/SQL.php | 43 +++++------------------------------ 3 files changed, 9 insertions(+), 46 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 5cd7c8a41..bac76fc73 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -1153,9 +1153,9 @@ abstract public function getKeywords(): array; * * @param array $selections * @param string $prefix - * @return mixed + * @return string */ - abstract protected function getAttributeProjection(array $selections, string $prefix): mixed; + abstract protected function getAttributeProjection(array $selections, string $prefix): string; /** * Get all selected attributes from queries diff --git a/src/Database/Adapter/Pool.php b/src/Database/Adapter/Pool.php index 3a4faa56a..c2cd363ba 100644 --- a/src/Database/Adapter/Pool.php +++ b/src/Database/Adapter/Pool.php @@ -470,13 +470,7 @@ public function getKeywords(): array return $this->delegate(__FUNCTION__, \func_get_args()); } - /** - * @param array $selections - * @param string $prefix - * @param array $spatialAttributes - * @return mixed - */ - protected function getAttributeProjection(array $selections, string $prefix, array $spatialAttributes = []): mixed + protected function getAttributeProjection(array $selections, string $prefix): string { return $this->delegate(__FUNCTION__, \func_get_args()); } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index e2a8f775d..0fc1440e6 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -350,7 +350,6 @@ public function deleteAttribute(string $collection, string $id, bool $array = fa */ public function getDocument(Document $collection, string $id, array $queries = [], bool $forUpdate = false): Document { - $spatialAttributes = $this->getSpatialAttributes($collection); $collection = $collection->getId(); $name = $this->filter($collection); @@ -359,9 +358,9 @@ public function getDocument(Document $collection, string $id, array $queries = [ $forUpdate = $forUpdate ? 'FOR UPDATE' : ''; $alias = Query::DEFAULT_ALIAS; - $spatialAttributes = []; + $sql = " - SELECT {$this->getAttributeProjection($selections, $alias, $spatialAttributes)} + SELECT {$this->getAttributeProjection($selections, $alias)} FROM {$this->getSQLTable($name)} AS {$this->quote($alias)} WHERE {$this->quote($alias)}.{$this->quote('_uid')} = :_uid {$this->getTenantQuery($collection, $alias)} @@ -1877,37 +1876,13 @@ public function getTenantQuery( * * @param array $selections * @param string $prefix - * @param array $spatialAttributes - * @return mixed + * @return string * @throws Exception */ - protected function getAttributeProjection(array $selections, string $prefix, array $spatialAttributes = []): mixed + protected function getAttributeProjection(array $selections, string $prefix): string { if (empty($selections) || \in_array('*', $selections)) { - if (empty($spatialAttributes)) { - return "{$this->quote($prefix)}.*"; - } - - $projections = []; - $projections[] = "{$this->quote($prefix)}.*"; - - $internalColumns = ['_id', '_uid', '_createdAt', '_updatedAt', '_permissions']; - if ($this->sharedTables) { - $internalColumns[] = '_tenant'; - } - foreach ($internalColumns as $col) { - $projections[] = "{$this->quote($prefix)}.{$this->quote($col)}"; - } - - foreach ($spatialAttributes as $spatialAttr) { - $filteredAttr = $this->filter($spatialAttr); - $quotedAttr = $this->quote($filteredAttr); - $axisOrder = $this->getSupportForSpatialAxisOrder() ? ', ' . $this->getSpatialAxisOrderSpec() : ''; - $projections[] = "ST_AsText({$this->quote($prefix)}.{$quotedAttr} {$axisOrder} ) AS {$quotedAttr}"; - } - - - return implode(', ', $projections); + return "{$this->quote($prefix)}.*"; } // Handle specific selections with spatial conversion where needed @@ -1929,13 +1904,7 @@ protected function getAttributeProjection(array $selections, string $prefix, arr foreach ($selections as $selection) { $filteredSelection = $this->filter($selection); $quotedSelection = $this->quote($filteredSelection); - - if (in_array($selection, $spatialAttributes)) { - $axisOrder = $this->getSupportForSpatialAxisOrder() ? ', ' . $this->getSpatialAxisOrderSpec() : ''; - $projections[] = "ST_AsText({$this->quote($prefix)}.{$quotedSelection} {$axisOrder}) AS {$quotedSelection}"; - } else { - $projections[] = "{$this->quote($prefix)}.{$quotedSelection}"; - } + $projections[] = "{$this->quote($prefix)}.{$quotedSelection}"; } return \implode(',', $projections); From 4ed19c6ae9bd2cd2f71349ec8eee3f336d183cf7 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 15 Sep 2025 18:09:09 +0300 Subject: [PATCH 21/29] Runn tests --- tests/e2e/Adapter/Base.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 481704ce6..37ad7cce3 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -18,12 +18,12 @@ abstract class Base extends TestCase { -// use CollectionTests; -// use DocumentTests; -// use AttributeTests; -// use IndexTests; -// use PermissionTests; -// use RelationshipTests; + use CollectionTests; + use DocumentTests; + use AttributeTests; + use IndexTests; + use PermissionTests; + use RelationshipTests; use SpatialTests; use GeneralTests; From ee7fbc1db35bbb285bd87623a09b741cca7bcb95 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 15 Sep 2025 18:12:35 +0300 Subject: [PATCH 22/29] decode polygon --- src/Database/Adapter/SQL.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 0fc1440e6..53e643e64 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -2837,13 +2837,20 @@ public function decodePolygon(string $wkb): array for ($p = 0; $p < $numPoints; $p++) { $xArr = unpack('d', substr($wkb, $offset, 8)); - $yArr = unpack('d', substr($wkb, $offset + 8, 8)); + if ($xArr === false) { + throw new \RuntimeException('Failed to unpack X coordinate from WKB.'); + } + + $x = (float) $xArr[1]; - if ($xArr === false || $yArr === false || !isset($xArr[1], $yArr[1])) { - throw new DatabaseException('Invalid WKB: cannot unpack point coordinates'); + $yArr = unpack('d', substr($wkb, $offset + 8, 8)); + if ($yArr === false) { + throw new \RuntimeException('Failed to unpack Y coordinate from WKB.'); } - $ring[] = [(float)$xArr[1], (float)$yArr[1]]; + $y = (float) $yArr[1]; + + $ring[] = [$x, $y]; $offset += 16; } From 2648ef909bfeb99718a2f8111a6ac84693ba9263 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 15 Sep 2025 18:17:29 +0300 Subject: [PATCH 23/29] remove $spatialAttributes --- src/Database/Adapter/SQL.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 53e643e64..3fceac621 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -2348,7 +2348,6 @@ protected function getAttributeType(string $attributeName, array $attributes): ? */ public function find(Document $collection, array $queries = [], ?int $limit = 25, ?int $offset = null, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER, string $forPermission = Database::PERMISSION_READ): array { - $spatialAttributes = $this->getSpatialAttributes($collection); $attributes = $collection->getAttribute('attributes', []); $collection = $collection->getId(); @@ -2456,7 +2455,7 @@ public function find(Document $collection, array $queries = [], ?int $limit = 25 $sql = " - SELECT {$this->getAttributeProjection($selections, $alias, $spatialAttributes)} + SELECT {$this->getAttributeProjection($selections, $alias)} FROM {$this->getSQLTable($name)} AS {$this->quote($alias)} {$sqlWhere} {$sqlOrder} From a0b963a59d87d11b8b1d20f89109c279204c28c0 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 15 Sep 2025 18:28:33 +0300 Subject: [PATCH 24/29] unpack --- phpunit.xml | 2 +- src/Database/Adapter/Postgres.php | 90 ++++++++++++------------------- 2 files changed, 35 insertions(+), 57 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 7469c5341..8ba994793 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,7 +7,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="true"> + stopOnFailure="false"> ./tests/unit diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index c3524d36b..0a9c76bd0 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -2020,34 +2020,29 @@ public function decodePoint(string $wkb): array } $isLE = ord($bin[0]) === 1; - $bytes = substr($bin, 1, 4); - if (strlen($bytes) < 4) { - throw new \RuntimeException('WKB too short to read type'); - } - - $unpacked = unpack($isLE ? 'V' : 'N', $bytes); - if ($unpacked === false) { + $type = unpack($isLE ? 'V' : 'N', substr($bin, 1, 4)); + if ($type === false) { throw new \RuntimeException('Failed to unpack type from WKB'); } - $type = $unpacked[1]; + $type = $type[1]; $offset = 5 + (($type & 0x20000000) ? 4 : 0); $fmt = $isLE ? 'e' : 'E'; // little vs big endian double - $unpacked = unpack($fmt, substr($bin, $offset, 8)); - if ($unpacked === false) { + $x = unpack($fmt, substr($bin, $offset, 8)); + if ($x === false) { throw new \RuntimeException('Failed to unpack double from WKB'); } - $x = (float)$unpacked[1]; + $x = (float)$x[1]; - $unpackedY = unpack($fmt, substr($bin, $offset + 8, 8)); - if ($unpackedY === false) { + $y = unpack($fmt, substr($bin, $offset + 8, 8)); + if ($y === false) { throw new \RuntimeException('Failed to unpack Y coordinate from WKB'); } - $y = (float)$unpackedY[1]; + $y = (float)$y[1]; return [$x, $y]; } @@ -2085,9 +2080,7 @@ public function decodeLinestring(mixed $wkb): array } // Type + SRID flag - $typeFieldBytes = substr($wkb, 1, 4); - $typeField = unpack('V', $typeFieldBytes); - + $typeField = unpack('V', substr($wkb, 1, 4)); if ($typeField === false) { throw new \RuntimeException('Failed to unpack the type field from WKB.'); } @@ -2105,36 +2098,31 @@ public function decodeLinestring(mixed $wkb): array $offset += 4; } - $numPointsBytes = substr($wkb, $offset, 4); - $numPointsUnpacked = unpack('V', $numPointsBytes); - - if ($numPointsUnpacked === false || !isset($numPointsUnpacked[1])) { + $numPoints = unpack('V', substr($wkb, $offset, 4)); + if ($numPoints === false) { throw new \RuntimeException("Failed to unpack number of points at offset {$offset}."); } - $numPoints = $numPointsUnpacked[1]; + $numPoints = $numPoints[1]; $offset += 4; $points = []; for ($i = 0; $i < $numPoints; $i++) { - $xBytes = substr($wkb, $offset, 8); - $xUnpacked = unpack('e', $xBytes); - - if ($xUnpacked === false) { + $x = unpack('e', substr($wkb, $offset, 8)); + if ($x === false) { throw new \RuntimeException("Failed to unpack X coordinate at offset {$offset}."); } - $x = (float) $xUnpacked[1]; + $x = (float) $x[1]; $offset += 8; - $yBytes = substr($wkb, $offset, 8); - $yUnpacked = unpack('e', $yBytes); - if ($yUnpacked === false || !isset($yUnpacked[1])) { + $y = unpack('e', substr($wkb, $offset, 8)); + if ($y === false) { throw new \RuntimeException("Failed to unpack Y coordinate at offset {$offset}."); } - $y = (float) $yUnpacked[1]; + $y = (float) $y[1]; $offset += 8; $points[] = [$x, $y]; @@ -2176,14 +2164,12 @@ public function decodePolygon(string $wkb): array $uInt32 = 'V'; // little-endian 32-bit unsigned $uDouble = 'd'; // little-endian double - $bytes = substr($wkb, 1, 4); - $unpacked = unpack($uInt32, $bytes); - - if ($unpacked === false || !isset($unpacked[1])) { + $typeInt = unpack($uInt32, substr($wkb, 1, 4)); + if ($typeInt === false) { throw new \RuntimeException('Failed to unpack type field from WKB.'); } - $typeInt = (int) $unpacked[1]; + $typeInt = (int) $typeInt[1]; $hasSrid = ($typeInt & 0x20000000) !== 0; $geomType = $typeInt & 0xFF; @@ -2197,46 +2183,38 @@ public function decodePolygon(string $wkb): array } // Number of rings - $bytes = substr($wkb, $offset, 4); - $unpacked = unpack($uInt32, $bytes); - - if ($unpacked === false || !isset($unpacked[1])) { + $numRings = unpack($uInt32, substr($wkb, $offset, 4)); + if ($numRings === false) { throw new \RuntimeException('Failed to unpack number of rings from WKB.'); } - $numRings = (int) $unpacked[1]; + $numRings = (int) $numRings[1]; $offset += 4; $rings = []; for ($r = 0; $r < $numRings; $r++) { - $bytes = substr($wkb, $offset, 4); - $unpacked = unpack($uInt32, $bytes); - - if ($unpacked === false || !isset($unpacked[1])) { + $numPoints = unpack($uInt32, substr($wkb, $offset, 4)); + if ($numPoints === false) { throw new \RuntimeException('Failed to unpack number of points from WKB.'); } - $numPoints = (int) $unpacked[1]; + $numPoints = (int) $numPoints[1]; $offset += 4; $points = []; for ($i = 0; $i < $numPoints; $i++) { - $bytes = substr($wkb, $offset, 8); - $unpacked = unpack($uDouble, $bytes); - - if ($unpacked === false) { + $x = unpack($uDouble, substr($wkb, $offset, 8)); + if ($x === false) { throw new \RuntimeException('Failed to unpack X coordinate from WKB.'); } - $x = (float) $unpacked[1]; - - $bytes = substr($wkb, $offset + 8, 8); - $unpacked = unpack($uDouble, $bytes); + $x = (float) $x[1]; - if ($unpacked === false || !isset($unpacked[1])) { + $y = unpack($uDouble, substr($wkb, $offset + 8, 8)); + if ($y === false) { throw new \RuntimeException('Failed to unpack Y coordinate from WKB.'); } - $y = (float) $unpacked[1]; + $y = (float) $y[1]; $points[] = [$x, $y]; $offset += 16; From cfe9b848b047575db6f0712b985e69e0bcfdf66d Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 15 Sep 2025 18:29:27 +0300 Subject: [PATCH 25/29] stopOnFailure --- phpunit.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/phpunit.xml b/phpunit.xml index 8ba994793..2a0531cfd 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,7 +7,8 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false"> + stopOnFailure="false" +> ./tests/unit From a1a52f633cda9e061a669cd40289a1be5cb9e8b0 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 16 Sep 2025 12:19:30 +0300 Subject: [PATCH 26/29] fix decode point --- src/Database/Adapter/SQL.php | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 3fceac621..f750627f9 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -2689,24 +2689,35 @@ public function decodePoint(string $wkb): array return [(float)$coords[0], (float)$coords[1]]; } - // MySQL SRID-aware WKB layout: - // 1 byte = endian (1 = little endian) - // 4 bytes = type + SRID flag - // 4 bytes = SRID - // 16 bytes = X,Y coordinates (double each, little endian) + /** + * [0..3] SRID (4 bytes, little-endian) + * [4] Byte order (1 = little-endian, 0 = big-endian) + * [5..8] Geometry type (with SRID flag bit) + * [9..] Geometry payload (coordinates, etc.) + */ - $byteOrder = ord($wkb[0]); + if (strlen($wkb) < 25) { + throw new DatabaseException('Invalid WKB: too short for POINT'); + } + + // 4 bytes SRID first → skip to byteOrder at offset 4 + $byteOrder = ord($wkb[4]); $littleEndian = ($byteOrder === 1); - // Skip 1 + 4 + 4 = 9 bytes to get coordinates - $coordsBin = substr($wkb, 9, 16); + if (!$littleEndian) { + throw new DatabaseException('Only little-endian WKB supported'); + } - // Unpack doubles - $format = $littleEndian ? 'd2' : 'd2'; // little-endian doubles - $coords = unpack($format, $coordsBin); + // After SRID (4) + byteOrder (1) + type (4) = 9 bytes + $coordsBin = substr($wkb, 9, 16); + if (strlen($coordsBin) !== 16) { + throw new DatabaseException('Invalid WKB: missing coordinate bytes'); + } + // Unpack two doubles + $coords = unpack('d2', $coordsBin); if ($coords === false || !isset($coords[1], $coords[2])) { - throw new DatabaseException('Invalid WKB for POINT: cannot unpack coordinates'); + throw new DatabaseException('Invalid WKB: failed to unpack coordinates'); } return [(float)$coords[1], (float)$coords[2]]; From c349c08f8e59c6da7df6ca49c4175dc23cf3b4e8 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 16 Sep 2025 12:34:55 +0300 Subject: [PATCH 27/29] DatabaseException --- src/Database/Adapter/Postgres.php | 42 +++++++++++++++---------------- src/Database/Adapter/SQL.php | 4 +-- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 3caee828f..5e6f99194 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -2014,13 +2014,13 @@ public function decodePoint(string $wkb): array $bin = hex2bin($wkb); if ($bin === false) { - throw new \RuntimeException('Invalid hex WKB string'); + throw new DatabaseException('Invalid hex WKB string'); } $isLE = ord($bin[0]) === 1; $type = unpack($isLE ? 'V' : 'N', substr($bin, 1, 4)); if ($type === false) { - throw new \RuntimeException('Failed to unpack type from WKB'); + throw new DatabaseException('Failed to unpack type from WKB'); } $type = $type[1]; @@ -2030,14 +2030,14 @@ public function decodePoint(string $wkb): array $x = unpack($fmt, substr($bin, $offset, 8)); if ($x === false) { - throw new \RuntimeException('Failed to unpack double from WKB'); + throw new DatabaseException('Failed to unpack double from WKB'); } $x = (float)$x[1]; $y = unpack($fmt, substr($bin, $offset + 8, 8)); if ($y === false) { - throw new \RuntimeException('Failed to unpack Y coordinate from WKB'); + throw new DatabaseException('Failed to unpack Y coordinate from WKB'); } $y = (float)$y[1]; @@ -2062,25 +2062,25 @@ public function decodeLinestring(mixed $wkb): array if (ctype_xdigit($wkb)) { $wkb = hex2bin($wkb); if ($wkb === false) { - throw new \RuntimeException("Failed to convert hex WKB to binary."); + throw new DatabaseException("Failed to convert hex WKB to binary."); } } if (strlen($wkb) < 9) { - throw new \RuntimeException("WKB too short to be a valid geometry"); + throw new DatabaseException("WKB too short to be a valid geometry"); } $byteOrder = ord($wkb[0]); if ($byteOrder === 0) { - throw new \RuntimeException("Big-endian WKB not supported"); + throw new DatabaseException("Big-endian WKB not supported"); } elseif ($byteOrder !== 1) { - throw new \RuntimeException("Invalid byte order in WKB"); + throw new DatabaseException("Invalid byte order in WKB"); } // Type + SRID flag $typeField = unpack('V', substr($wkb, 1, 4)); if ($typeField === false) { - throw new \RuntimeException('Failed to unpack the type field from WKB.'); + throw new DatabaseException('Failed to unpack the type field from WKB.'); } $typeField = $typeField[1]; @@ -2088,7 +2088,7 @@ public function decodeLinestring(mixed $wkb): array $hasSRID = ($typeField & 0x20000000) !== 0; if ($geomType !== 2) { // 2 = LINESTRING - throw new \RuntimeException("Not a LINESTRING geometry type, got {$geomType}"); + throw new DatabaseException("Not a LINESTRING geometry type, got {$geomType}"); } $offset = 5; @@ -2098,7 +2098,7 @@ public function decodeLinestring(mixed $wkb): array $numPoints = unpack('V', substr($wkb, $offset, 4)); if ($numPoints === false) { - throw new \RuntimeException("Failed to unpack number of points at offset {$offset}."); + throw new DatabaseException("Failed to unpack number of points at offset {$offset}."); } $numPoints = $numPoints[1]; @@ -2108,7 +2108,7 @@ public function decodeLinestring(mixed $wkb): array for ($i = 0; $i < $numPoints; $i++) { $x = unpack('e', substr($wkb, $offset, 8)); if ($x === false) { - throw new \RuntimeException("Failed to unpack X coordinate at offset {$offset}."); + throw new DatabaseException("Failed to unpack X coordinate at offset {$offset}."); } $x = (float) $x[1]; @@ -2117,7 +2117,7 @@ public function decodeLinestring(mixed $wkb): array $y = unpack('e', substr($wkb, $offset, 8)); if ($y === false) { - throw new \RuntimeException("Failed to unpack Y coordinate at offset {$offset}."); + throw new DatabaseException("Failed to unpack Y coordinate at offset {$offset}."); } $y = (float) $y[1]; @@ -2151,12 +2151,12 @@ public function decodePolygon(string $wkb): array if (preg_match('/^[0-9a-fA-F]+$/', $wkb)) { $wkb = hex2bin($wkb); if ($wkb === false) { - throw new \RuntimeException("Invalid hex WKB"); + throw new DatabaseException("Invalid hex WKB"); } } if (strlen($wkb) < 9) { - throw new \RuntimeException("WKB too short"); + throw new DatabaseException("WKB too short"); } $uInt32 = 'V'; // little-endian 32-bit unsigned @@ -2164,7 +2164,7 @@ public function decodePolygon(string $wkb): array $typeInt = unpack($uInt32, substr($wkb, 1, 4)); if ($typeInt === false) { - throw new \RuntimeException('Failed to unpack type field from WKB.'); + throw new DatabaseException('Failed to unpack type field from WKB.'); } $typeInt = (int) $typeInt[1]; @@ -2172,7 +2172,7 @@ public function decodePolygon(string $wkb): array $geomType = $typeInt & 0xFF; if ($geomType !== 3) { // 3 = POLYGON - throw new \RuntimeException("Not a POLYGON geometry type, got {$geomType}"); + throw new DatabaseException("Not a POLYGON geometry type, got {$geomType}"); } $offset = 5; @@ -2183,7 +2183,7 @@ public function decodePolygon(string $wkb): array // Number of rings $numRings = unpack($uInt32, substr($wkb, $offset, 4)); if ($numRings === false) { - throw new \RuntimeException('Failed to unpack number of rings from WKB.'); + throw new DatabaseException('Failed to unpack number of rings from WKB.'); } $numRings = (int) $numRings[1]; @@ -2193,7 +2193,7 @@ public function decodePolygon(string $wkb): array for ($r = 0; $r < $numRings; $r++) { $numPoints = unpack($uInt32, substr($wkb, $offset, 4)); if ($numPoints === false) { - throw new \RuntimeException('Failed to unpack number of points from WKB.'); + throw new DatabaseException('Failed to unpack number of points from WKB.'); } $numPoints = (int) $numPoints[1]; @@ -2202,14 +2202,14 @@ public function decodePolygon(string $wkb): array for ($i = 0; $i < $numPoints; $i++) { $x = unpack($uDouble, substr($wkb, $offset, 8)); if ($x === false) { - throw new \RuntimeException('Failed to unpack X coordinate from WKB.'); + throw new DatabaseException('Failed to unpack X coordinate from WKB.'); } $x = (float) $x[1]; $y = unpack($uDouble, substr($wkb, $offset + 8, 8)); if ($y === false) { - throw new \RuntimeException('Failed to unpack Y coordinate from WKB.'); + throw new DatabaseException('Failed to unpack Y coordinate from WKB.'); } $y = (float) $y[1]; diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index f750627f9..600f047f1 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -2848,14 +2848,14 @@ public function decodePolygon(string $wkb): array for ($p = 0; $p < $numPoints; $p++) { $xArr = unpack('d', substr($wkb, $offset, 8)); if ($xArr === false) { - throw new \RuntimeException('Failed to unpack X coordinate from WKB.'); + throw new DatabaseException('Failed to unpack X coordinate from WKB.'); } $x = (float) $xArr[1]; $yArr = unpack('d', substr($wkb, $offset + 8, 8)); if ($yArr === false) { - throw new \RuntimeException('Failed to unpack Y coordinate from WKB.'); + throw new DatabaseException('Failed to unpack Y coordinate from WKB.'); } $y = (float) $yArr[1]; From 96e8a5e648ac5d4f631f73c82ad6ffc384190781 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 16 Sep 2025 12:39:26 +0300 Subject: [PATCH 28/29] Postgres update point --- src/Database/Adapter/Postgres.php | 42 +++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 5e6f99194..abff332d3 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -2017,30 +2017,46 @@ public function decodePoint(string $wkb): array throw new DatabaseException('Invalid hex WKB string'); } + if (strlen($bin) < 13) { // 1 byte endian + 4 bytes type + 8 bytes for X + throw new DatabaseException('WKB too short'); + } + $isLE = ord($bin[0]) === 1; - $type = unpack($isLE ? 'V' : 'N', substr($bin, 1, 4)); - if ($type === false) { + +// Type (4 bytes) + $typeBytes = substr($bin, 1, 4); + if (strlen($typeBytes) !== 4) { + throw new DatabaseException('Failed to extract type bytes from WKB'); + } + + $typeArr = unpack($isLE ? 'V' : 'N', $typeBytes); + if ($typeArr === false || !isset($typeArr[1])) { throw new DatabaseException('Failed to unpack type from WKB'); } + $type = $typeArr[1]; - $type = $type[1]; + // Offset to coordinates (skip SRID if present) $offset = 5 + (($type & 0x20000000) ? 4 : 0); - $fmt = $isLE ? 'e' : 'E'; // little vs big endian double - - $x = unpack($fmt, substr($bin, $offset, 8)); - if ($x === false) { - throw new DatabaseException('Failed to unpack double from WKB'); + if (strlen($bin) < $offset + 16) { // 16 bytes for X,Y + throw new DatabaseException('WKB too short for coordinates'); } - $x = (float)$x[1]; + $fmt = $isLE ? 'e' : 'E'; // little vs big endian double - $y = unpack($fmt, substr($bin, $offset + 8, 8)); - if ($y === false) { - throw new DatabaseException('Failed to unpack Y coordinate from WKB'); + // X coordinate + $xArr = unpack($fmt, substr($bin, $offset, 8)); + if ($xArr === false || !isset($xArr[1])) { + throw new DatabaseException('Failed to unpack X coordinate'); } + $x = (float)$xArr[1]; - $y = (float)$y[1]; + // Y coordinate + $yArr = unpack($fmt, substr($bin, $offset + 8, 8)); + if ($yArr === false || !isset($yArr[1])) { + throw new DatabaseException('Failed to unpack Y coordinate'); + } + $y = (float)$yArr[1]; return [$x, $y]; } From 09be1c0902fe00ca94ad0707f82c2cf606f67018 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 16 Sep 2025 12:41:28 +0300 Subject: [PATCH 29/29] formatting --- src/Database/Adapter/Postgres.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index abff332d3..25f9ffc34 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -2023,7 +2023,7 @@ public function decodePoint(string $wkb): array $isLE = ord($bin[0]) === 1; -// Type (4 bytes) + // Type (4 bytes) $typeBytes = substr($bin, 1, 4); if (strlen($typeBytes) !== 4) { throw new DatabaseException('Failed to extract type bytes from WKB');