Skip to content

Commit dbecdf8

Browse files
authored
Merge pull request #700 from utopia-php/spatial-filter
get document fix
2 parents 4500995 + 753800f commit dbecdf8

File tree

4 files changed

+182
-7
lines changed

4 files changed

+182
-7
lines changed

src/Database/Adapter/SQL.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1902,7 +1902,8 @@ protected function getAttributeProjection(array $selections, string $prefix, arr
19021902
foreach ($spatialAttributes as $spatialAttr) {
19031903
$filteredAttr = $this->filter($spatialAttr);
19041904
$quotedAttr = $this->quote($filteredAttr);
1905-
$projections[] = "ST_AsText({$this->quote($prefix)}.{$quotedAttr}) AS {$quotedAttr}";
1905+
$axisOrder = $this->getSupportForSpatialAxisOrder() ? ', ' . $this->getSpatialAxisOrderSpec() : '';
1906+
$projections[] = "ST_AsText({$this->quote($prefix)}.{$quotedAttr} {$axisOrder} ) AS {$quotedAttr}";
19061907
}
19071908

19081909

@@ -1930,7 +1931,8 @@ protected function getAttributeProjection(array $selections, string $prefix, arr
19301931
$quotedSelection = $this->quote($filteredSelection);
19311932

19321933
if (in_array($selection, $spatialAttributes)) {
1933-
$projections[] = "ST_AsText({$this->quote($prefix)}.{$quotedSelection}) AS {$quotedSelection}";
1934+
$axisOrder = $this->getSupportForSpatialAxisOrder() ? ', ' . $this->getSpatialAxisOrderSpec() : '';
1935+
$projections[] = "ST_AsText({$this->quote($prefix)}.{$quotedSelection} {$axisOrder}) AS {$quotedSelection}";
19341936
} else {
19351937
$projections[] = "{$this->quote($prefix)}.{$quotedSelection}";
19361938
}

src/Database/Validator/Spatial.php

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ protected function validatePoint(array $value): bool
3333
return false;
3434
}
3535

36-
return true;
36+
return $this->isValidCoordinate((float)$value[0], (float) $value[1]);
3737
}
3838

3939
/**
@@ -49,7 +49,7 @@ protected function validateLineString(array $value): bool
4949
return false;
5050
}
5151

52-
foreach ($value as $point) {
52+
foreach ($value as $pointIndex => $point) {
5353
if (!is_array($point) || count($point) !== 2) {
5454
$this->message = 'Each point in LineString must be an array of two values [x, y]';
5555
return false;
@@ -59,6 +59,11 @@ protected function validateLineString(array $value): bool
5959
$this->message = 'Each point in LineString must have numeric coordinates';
6060
return false;
6161
}
62+
63+
if (!$this->isValidCoordinate((float)$point[0], (float)$point[1])) {
64+
$this->message = "Invalid coordinates at point #{$pointIndex}: {$this->message}";
65+
return false;
66+
}
6267
}
6368

6469
return true;
@@ -77,14 +82,13 @@ protected function validatePolygon(array $value): bool
7782
return false;
7883
}
7984

80-
// Detect single-ring polygon: [[x, y], [x, y], ...]
8185
$isSingleRing = isset($value[0]) && is_array($value[0]) &&
8286
count($value[0]) === 2 &&
8387
is_numeric($value[0][0]) &&
8488
is_numeric($value[0][1]);
8589

8690
if ($isSingleRing) {
87-
$value = [$value]; // wrap single ring
91+
$value = [$value];
8892
}
8993

9094
foreach ($value as $ringIndex => $ring) {
@@ -108,6 +112,11 @@ protected function validatePolygon(array $value): bool
108112
$this->message = "Coordinates of point #{$pointIndex} in ring #{$ringIndex} must be numeric";
109113
return false;
110114
}
115+
116+
if (!$this->isValidCoordinate((float)$point[0], (float)$point[1])) {
117+
$this->message = "Invalid coordinates at point #{$pointIndex} in ring #{$ringIndex}: {$this->message}";
118+
return false;
119+
}
111120
}
112121

113122
// Check that the ring is closed (first point == last point)
@@ -182,4 +191,19 @@ public function isValid($value): bool
182191
$this->message = 'Spatial value must be array or WKT string';
183192
return false;
184193
}
194+
195+
private function isValidCoordinate(int|float $x, int|float $y): bool
196+
{
197+
if ($x < -180 || $x > 180) {
198+
$this->message = "Longitude (x) must be between -180 and 180, got {$x}";
199+
return false;
200+
}
201+
202+
if ($y < -90 || $y > 90) {
203+
$this->message = "Latitude (y) must be between -90 and 90, got {$y}";
204+
return false;
205+
}
206+
207+
return true;
208+
}
185209
}

tests/e2e/Adapter/Scopes/SpatialTests.php

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
use Utopia\Database\Helpers\Permission;
1313
use Utopia\Database\Helpers\Role;
1414
use Utopia\Database\Query;
15-
use Utopia\Database\Validator\Index;
1615

1716
trait SpatialTests
1817
{
@@ -2626,4 +2625,126 @@ public function testSpatialIndexOnNonSpatial(): void
26262625
$database->deleteCollection($collUpdateNull);
26272626
}
26282627
}
2628+
2629+
public function testSpatialDocOrder(): void
2630+
{
2631+
/** @var Database $database */
2632+
$database = static::getDatabase();
2633+
if (!$database->getAdapter()->getSupportForSpatialAttributes()) {
2634+
$this->markTestSkipped('Adapter does not support spatial attributes');
2635+
}
2636+
2637+
$collectionName = 'test_spatial_order_axis';
2638+
// Create collection first
2639+
$database->createCollection($collectionName);
2640+
2641+
// Create spatial attributes using createAttribute method
2642+
$this->assertEquals(true, $database->createAttribute($collectionName, 'pointAttr', Database::VAR_POINT, 0, $database->getAdapter()->getSupportForSpatialIndexNull() ? false : true));
2643+
2644+
// Create test document
2645+
$doc1 = new Document(
2646+
[
2647+
'$id' => 'doc1',
2648+
'pointAttr' => [5.0, 5.5],
2649+
'$permissions' => [Permission::update(Role::any()), Permission::read(Role::any())]
2650+
]
2651+
);
2652+
$database->createDocument($collectionName, $doc1);
2653+
2654+
$result = $database->getDocument($collectionName, 'doc1');
2655+
$this->assertEquals($result->getAttribute('pointAttr')[0], 5.0);
2656+
$this->assertEquals($result->getAttribute('pointAttr')[1], 5.5);
2657+
$database->deleteCollection($collectionName);
2658+
}
2659+
2660+
public function testInvalidCoordinateDocuments(): void
2661+
{
2662+
/** @var Database $database */
2663+
$database = static::getDatabase();
2664+
if (!$database->getAdapter()->getSupportForSpatialAttributes()) {
2665+
$this->markTestSkipped('Adapter does not support spatial attributes');
2666+
}
2667+
2668+
$collectionName = 'test_invalid_coord_';
2669+
try {
2670+
$database->createCollection($collectionName);
2671+
2672+
$database->createAttribute($collectionName, 'pointAttr', Database::VAR_POINT, 0, true);
2673+
$database->createAttribute($collectionName, 'lineAttr', Database::VAR_LINESTRING, 0, true);
2674+
$database->createAttribute($collectionName, 'polyAttr', Database::VAR_POLYGON, 0, true);
2675+
2676+
$invalidDocs = [
2677+
// Invalid POINT (longitude > 180)
2678+
[
2679+
'$id' => 'invalidDoc1',
2680+
'pointAttr' => [200.0, 20.0],
2681+
'lineAttr' => [[1.0, 2.0], [3.0, 4.0]],
2682+
'polyAttr' => [
2683+
[
2684+
[0.0, 0.0],
2685+
[0.0, 10.0],
2686+
[10.0, 10.0],
2687+
[10.0, 0.0],
2688+
[0.0, 0.0]
2689+
]
2690+
]
2691+
],
2692+
// Invalid POINT (latitude < -90)
2693+
[
2694+
'$id' => 'invalidDoc2',
2695+
'pointAttr' => [50.0, -100.0],
2696+
'lineAttr' => [[1.0, 2.0], [3.0, 4.0]],
2697+
'polyAttr' => [
2698+
[
2699+
[0.0, 0.0],
2700+
[0.0, 10.0],
2701+
[10.0, 10.0],
2702+
[10.0, 0.0],
2703+
[0.0, 0.0]
2704+
]
2705+
]
2706+
],
2707+
// Invalid LINESTRING (point outside valid range)
2708+
[
2709+
'$id' => 'invalidDoc3',
2710+
'pointAttr' => [50.0, 20.0],
2711+
'lineAttr' => [[1.0, 2.0], [300.0, 4.0]], // invalid longitude in line
2712+
'polyAttr' => [
2713+
[
2714+
[0.0, 0.0],
2715+
[0.0, 10.0],
2716+
[10.0, 10.0],
2717+
[10.0, 0.0],
2718+
[0.0, 0.0]
2719+
]
2720+
]
2721+
],
2722+
// Invalid POLYGON (point outside valid range)
2723+
[
2724+
'$id' => 'invalidDoc4',
2725+
'pointAttr' => [50.0, 20.0],
2726+
'lineAttr' => [[1.0, 2.0], [3.0, 4.0]],
2727+
'polyAttr' => [
2728+
[
2729+
[0.0, 0.0],
2730+
[0.0, 10.0],
2731+
[190.0, 10.0], // invalid longitude
2732+
[10.0, 0.0],
2733+
[0.0, 0.0]
2734+
]
2735+
]
2736+
],
2737+
];
2738+
foreach ($invalidDocs as $docData) {
2739+
$this->expectException(StructureException::class);
2740+
$docData['$permissions'] = [Permission::update(Role::any()), Permission::read(Role::any())];
2741+
$doc = new Document($docData);
2742+
$database->createDocument($collectionName, $doc);
2743+
}
2744+
2745+
2746+
} finally {
2747+
$database->deleteCollection($collectionName);
2748+
}
2749+
}
26292750
}

tests/unit/Validator/SpatialTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,32 @@ public function testWKTStrings(): void
8181
$this->assertFalse(Spatial::isWKTString('CIRCLE(0 0,1)'));
8282
$this->assertFalse(Spatial::isWKTString('POINT1(1 2)'));
8383
}
84+
85+
public function testInvalidCoordinate(): void
86+
{
87+
// Point with invalid longitude
88+
$validator = new Spatial(Database::VAR_POINT);
89+
$this->assertFalse($validator->isValid([200, 10])); // longitude > 180
90+
$this->assertStringContainsString('Longitude', $validator->getDescription());
91+
92+
// Point with invalid latitude
93+
$validator = new Spatial(Database::VAR_POINT);
94+
$this->assertFalse($validator->isValid([10, -100])); // latitude < -90
95+
$this->assertStringContainsString('Latitude', $validator->getDescription());
96+
97+
// LineString with invalid coordinates
98+
$validator = new Spatial(Database::VAR_LINESTRING);
99+
$this->assertFalse($validator->isValid([
100+
[0, 0],
101+
[181, 45] // invalid longitude
102+
]));
103+
$this->assertStringContainsString('Invalid coordinates', $validator->getDescription());
104+
105+
// Polygon with invalid coordinates
106+
$validator = new Spatial(Database::VAR_POLYGON);
107+
$this->assertFalse($validator->isValid([
108+
[[0, 0], [1, 1], [190, 5], [0, 0]] // invalid longitude in ring
109+
]));
110+
$this->assertStringContainsString('Invalid coordinates', $validator->getDescription());
111+
}
84112
}

0 commit comments

Comments
 (0)