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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/Database/Adapter/SQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -1902,7 +1902,8 @@ protected function getAttributeProjection(array $selections, string $prefix, arr
foreach ($spatialAttributes as $spatialAttr) {
$filteredAttr = $this->filter($spatialAttr);
$quotedAttr = $this->quote($filteredAttr);
$projections[] = "ST_AsText({$this->quote($prefix)}.{$quotedAttr}) AS {$quotedAttr}";
$axisOrder = $this->getSupportForSpatialAxisOrder() ? ', ' . $this->getSpatialAxisOrderSpec() : '';
$projections[] = "ST_AsText({$this->quote($prefix)}.{$quotedAttr} {$axisOrder} ) AS {$quotedAttr}";
}


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

if (in_array($selection, $spatialAttributes)) {
$projections[] = "ST_AsText({$this->quote($prefix)}.{$quotedSelection}) AS {$quotedSelection}";
$axisOrder = $this->getSupportForSpatialAxisOrder() ? ', ' . $this->getSpatialAxisOrderSpec() : '';
$projections[] = "ST_AsText({$this->quote($prefix)}.{$quotedSelection} {$axisOrder}) AS {$quotedSelection}";
} else {
$projections[] = "{$this->quote($prefix)}.{$quotedSelection}";
}
Expand Down
32 changes: 28 additions & 4 deletions src/Database/Validator/Spatial.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ protected function validatePoint(array $value): bool
return false;
}

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

/**
Expand All @@ -49,7 +49,7 @@ protected function validateLineString(array $value): bool
return false;
}

foreach ($value as $point) {
foreach ($value as $pointIndex => $point) {
if (!is_array($point) || count($point) !== 2) {
$this->message = 'Each point in LineString must be an array of two values [x, y]';
return false;
Expand All @@ -59,6 +59,11 @@ protected function validateLineString(array $value): bool
$this->message = 'Each point in LineString must have numeric coordinates';
return false;
}

if (!$this->isValidCoordinate((float)$point[0], (float)$point[1])) {
$this->message = "Invalid coordinates at point #{$pointIndex}: {$this->message}";
return false;
}
}

return true;
Expand All @@ -77,14 +82,13 @@ protected function validatePolygon(array $value): bool
return false;
}

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

if ($isSingleRing) {
$value = [$value]; // wrap single ring
$value = [$value];
}

foreach ($value as $ringIndex => $ring) {
Expand All @@ -108,6 +112,11 @@ protected function validatePolygon(array $value): bool
$this->message = "Coordinates of point #{$pointIndex} in ring #{$ringIndex} must be numeric";
return false;
}

if (!$this->isValidCoordinate((float)$point[0], (float)$point[1])) {
$this->message = "Invalid coordinates at point #{$pointIndex} in ring #{$ringIndex}: {$this->message}";
return false;
}
}

// Check that the ring is closed (first point == last point)
Expand Down Expand Up @@ -182,4 +191,19 @@ public function isValid($value): bool
$this->message = 'Spatial value must be array or WKT string';
return false;
}

private function isValidCoordinate(int|float $x, int|float $y): bool
{
if ($x < -180 || $x > 180) {
$this->message = "Longitude (x) must be between -180 and 180, got {$x}";
return false;
}

if ($y < -90 || $y > 90) {
$this->message = "Latitude (y) must be between -90 and 90, got {$y}";
return false;
}

return true;
}
}
123 changes: 122 additions & 1 deletion tests/e2e/Adapter/Scopes/SpatialTests.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Index;

trait SpatialTests
{
Expand Down Expand Up @@ -2626,4 +2625,126 @@ public function testSpatialIndexOnNonSpatial(): void
$database->deleteCollection($collUpdateNull);
}
}

public function testSpatialDocOrder(): void
{
/** @var Database $database */
$database = static::getDatabase();
if (!$database->getAdapter()->getSupportForSpatialAttributes()) {
$this->markTestSkipped('Adapter does not support spatial attributes');
}

$collectionName = 'test_spatial_order_axis';
// Create collection first
$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));

// Create test document
$doc1 = new Document(
[
'$id' => 'doc1',
'pointAttr' => [5.0, 5.5],
'$permissions' => [Permission::update(Role::any()), Permission::read(Role::any())]
]
);
$database->createDocument($collectionName, $doc1);

$result = $database->getDocument($collectionName, 'doc1');
$this->assertEquals($result->getAttribute('pointAttr')[0], 5.0);
$this->assertEquals($result->getAttribute('pointAttr')[1], 5.5);
$database->deleteCollection($collectionName);
}

public function testInvalidCoordinateDocuments(): void
{
/** @var Database $database */
$database = static::getDatabase();
if (!$database->getAdapter()->getSupportForSpatialAttributes()) {
$this->markTestSkipped('Adapter does not support spatial attributes');
}

$collectionName = 'test_invalid_coord_';
try {
$database->createCollection($collectionName);

$database->createAttribute($collectionName, 'pointAttr', Database::VAR_POINT, 0, true);
$database->createAttribute($collectionName, 'lineAttr', Database::VAR_LINESTRING, 0, true);
$database->createAttribute($collectionName, 'polyAttr', Database::VAR_POLYGON, 0, true);

$invalidDocs = [
// Invalid POINT (longitude > 180)
[
'$id' => 'invalidDoc1',
'pointAttr' => [200.0, 20.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]
]
]
],
// Invalid POINT (latitude < -90)
[
'$id' => 'invalidDoc2',
'pointAttr' => [50.0, -100.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]
]
]
],
// Invalid LINESTRING (point outside valid range)
[
'$id' => 'invalidDoc3',
'pointAttr' => [50.0, 20.0],
'lineAttr' => [[1.0, 2.0], [300.0, 4.0]], // invalid longitude in line
'polyAttr' => [
[
[0.0, 0.0],
[0.0, 10.0],
[10.0, 10.0],
[10.0, 0.0],
[0.0, 0.0]
]
]
],
// Invalid POLYGON (point outside valid range)
[
'$id' => 'invalidDoc4',
'pointAttr' => [50.0, 20.0],
'lineAttr' => [[1.0, 2.0], [3.0, 4.0]],
'polyAttr' => [
[
[0.0, 0.0],
[0.0, 10.0],
[190.0, 10.0], // invalid longitude
[10.0, 0.0],
[0.0, 0.0]
]
]
],
];
foreach ($invalidDocs as $docData) {
$this->expectException(StructureException::class);
$docData['$permissions'] = [Permission::update(Role::any()), Permission::read(Role::any())];
$doc = new Document($docData);
$database->createDocument($collectionName, $doc);
}


} finally {
$database->deleteCollection($collectionName);
}
}
}
28 changes: 28 additions & 0 deletions tests/unit/Validator/SpatialTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,32 @@ public function testWKTStrings(): void
$this->assertFalse(Spatial::isWKTString('CIRCLE(0 0,1)'));
$this->assertFalse(Spatial::isWKTString('POINT1(1 2)'));
}

public function testInvalidCoordinate(): void
{
// Point with invalid longitude
$validator = new Spatial(Database::VAR_POINT);
$this->assertFalse($validator->isValid([200, 10])); // longitude > 180
$this->assertStringContainsString('Longitude', $validator->getDescription());

// Point with invalid latitude
$validator = new Spatial(Database::VAR_POINT);
$this->assertFalse($validator->isValid([10, -100])); // latitude < -90
$this->assertStringContainsString('Latitude', $validator->getDescription());

// LineString with invalid coordinates
$validator = new Spatial(Database::VAR_LINESTRING);
$this->assertFalse($validator->isValid([
[0, 0],
[181, 45] // invalid longitude
]));
$this->assertStringContainsString('Invalid coordinates', $validator->getDescription());

// Polygon with invalid coordinates
$validator = new Spatial(Database::VAR_POLYGON);
$this->assertFalse($validator->isValid([
[[0, 0], [1, 1], [190, 5], [0, 0]] // invalid longitude in ring
]));
$this->assertStringContainsString('Invalid coordinates', $validator->getDescription());
}
}