Skip to content

Commit 21770b5

Browse files
Implement distance spatial queries and validation for spatial attributes
1 parent 6746472 commit 21770b5

File tree

9 files changed

+339
-70
lines changed

9 files changed

+339
-70
lines changed

src/Database/Adapter/MariaDB.php

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1352,6 +1352,47 @@ public function deleteDocument(string $collection, string $id): bool
13521352
return $deleted;
13531353
}
13541354

1355+
/**
1356+
* Handle distance spatial queries
1357+
*
1358+
* @param Query $query
1359+
* @param array<string, mixed> $binds
1360+
* @param string $attribute
1361+
* @param string $alias
1362+
* @param string $placeholder
1363+
* @return string
1364+
*/
1365+
protected function handleDistanceSpatialQueries(Query $query, array &$binds, string $attribute, string $alias, string $placeholder): string
1366+
{
1367+
$distanceParams = $query->getValues()[0];
1368+
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($distanceParams[0]);
1369+
$binds[":{$placeholder}_1"] = $distanceParams[1];
1370+
1371+
$meters = isset($distanceParams[2]) && $distanceParams[2] === true;
1372+
1373+
switch ($query->getMethod()) {
1374+
case Query::TYPE_DISTANCE_EQUAL:
1375+
$operator = '=';
1376+
break;
1377+
case Query::TYPE_DISTANCE_NOT_EQUAL:
1378+
$operator = '!=';
1379+
break;
1380+
case Query::TYPE_DISTANCE_GREATER_THAN:
1381+
$operator = '>';
1382+
break;
1383+
case Query::TYPE_DISTANCE_LESS_THAN:
1384+
$operator = '<';
1385+
break;
1386+
default:
1387+
throw new DatabaseException('Unknown spatial query method: ' . $query->getMethod());
1388+
}
1389+
1390+
if ($meters) {
1391+
return "ST_DISTANCE_SPHERE({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0), 6371000) {$operator} :{$placeholder}_1";
1392+
}
1393+
return "ST_Distance({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0)) {$operator} :{$placeholder}_1";
1394+
}
1395+
13551396
/**
13561397
* Handle spatial queries
13571398
*
@@ -1374,28 +1415,10 @@ protected function handleSpatialQueries(Query $query, array &$binds, string $att
13741415
return "NOT ST_Crosses({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
13751416

13761417
case Query::TYPE_DISTANCE_EQUAL:
1377-
$distanceParams = $query->getValues()[0];
1378-
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($distanceParams[0]);
1379-
$binds[":{$placeholder}_1"] = $distanceParams[1];
1380-
return "ST_Distance({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0)) = :{$placeholder}_1";
1381-
13821418
case Query::TYPE_DISTANCE_NOT_EQUAL:
1383-
$distanceParams = $query->getValues()[0];
1384-
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($distanceParams[0]);
1385-
$binds[":{$placeholder}_1"] = $distanceParams[1];
1386-
return "ST_Distance({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0)) != :{$placeholder}_1";
1387-
13881419
case Query::TYPE_DISTANCE_GREATER_THAN:
1389-
$distanceParams = $query->getValues()[0];
1390-
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($distanceParams[0]);
1391-
$binds[":{$placeholder}_1"] = $distanceParams[1];
1392-
return "ST_Distance({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0)) > :{$placeholder}_1";
1393-
13941420
case Query::TYPE_DISTANCE_LESS_THAN:
1395-
$distanceParams = $query->getValues()[0];
1396-
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($distanceParams[0]);
1397-
$binds[":{$placeholder}_1"] = $distanceParams[1];
1398-
return "ST_Distance({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0)) < :{$placeholder}_1";
1421+
return $this->handleDistanceSpatialQueries($query, $binds, $attribute, $alias, $placeholder);
13991422

14001423
case Query::TYPE_INTERSECTS:
14011424
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);

src/Database/Adapter/MySQL.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Utopia\Database\Exception as DatabaseException;
88
use Utopia\Database\Exception\Dependency as DependencyException;
99
use Utopia\Database\Exception\Timeout as TimeoutException;
10+
use Utopia\Database\Query;
1011

1112
class MySQL extends MariaDB
1213
{
@@ -78,6 +79,36 @@ public function getSizeOfCollectionOnDisk(string $collection): int
7879
return $size;
7980
}
8081

82+
protected function handleDistanceSpatialQueries(Query $query, array &$binds, string $attribute, string $alias, string $placeholder): string
83+
{
84+
$distanceParams = $query->getValues()[0];
85+
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($distanceParams[0]);
86+
$binds[":{$placeholder}_1"] = $distanceParams[1];
87+
88+
$useMeters = isset($distanceParams[2]) && $distanceParams[2] === true;
89+
90+
switch ($query->getMethod()) {
91+
case Query::TYPE_DISTANCE_EQUAL:
92+
$operator = '=';
93+
break;
94+
case Query::TYPE_DISTANCE_NOT_EQUAL:
95+
$operator = '!=';
96+
break;
97+
case Query::TYPE_DISTANCE_GREATER_THAN:
98+
$operator = '>';
99+
break;
100+
case Query::TYPE_DISTANCE_LESS_THAN:
101+
$operator = '<';
102+
break;
103+
default:
104+
throw new DatabaseException('Unknown spatial query method: ' . $query->getMethod());
105+
}
106+
107+
$unit = $useMeters ? ", 'meter'" : '';
108+
109+
return "ST_Distance({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0){$unit}) {$operator} :{$placeholder}_1";
110+
}
111+
81112
public function getSupportForIndexArray(): bool
82113
{
83114
/**

src/Database/Adapter/Postgres.php

Lines changed: 52 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,6 +1452,43 @@ public function getConnectionId(): string
14521452
return $stmt->fetchColumn();
14531453
}
14541454

1455+
protected function handleDistanceSpatialQueries(Query $query, array &$binds, string $attribute, string $alias, string $placeholder): string
1456+
{
1457+
$distanceParams = $query->getValues()[0];
1458+
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($distanceParams[0]);
1459+
$binds[":{$placeholder}_1"] = $distanceParams[1];
1460+
1461+
$meters = isset($distanceParams[2]) && $distanceParams[2] === true;
1462+
1463+
switch ($query->getMethod()) {
1464+
case Query::TYPE_DISTANCE_EQUAL:
1465+
$operator = '=';
1466+
break;
1467+
case Query::TYPE_DISTANCE_NOT_EQUAL:
1468+
$operator = '!=';
1469+
break;
1470+
case Query::TYPE_DISTANCE_GREATER_THAN:
1471+
$operator = '>';
1472+
break;
1473+
case Query::TYPE_DISTANCE_LESS_THAN:
1474+
$operator = '<';
1475+
break;
1476+
default:
1477+
throw new DatabaseException('Unknown spatial query method: ' . $query->getMethod());
1478+
}
1479+
1480+
if ($meters) {
1481+
// Transform both attribute and input geometry to 3857 (meters) for distance calculation
1482+
$attr = "ST_Transform({$alias}.{$attribute}, 3857)";
1483+
$geom = "ST_Transform(ST_GeomFromText(:{$placeholder}_0, " . Database::SRID . "), 3857)";
1484+
return "ST_Distance({$attr}, {$geom}) {$operator} :{$placeholder}_1";
1485+
}
1486+
1487+
// Without meters, use the original SRID (e.g., 4326)
1488+
return "ST_Distance({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, " . Database::SRID . ")) {$operator} :{$placeholder}_1";
1489+
}
1490+
1491+
14551492
/**
14561493
* Handle spatial queries
14571494
*
@@ -1474,60 +1511,41 @@ protected function handleSpatialQueries(Query $query, array &$binds, string $att
14741511
return "NOT ST_Crosses({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
14751512

14761513
case Query::TYPE_DISTANCE_EQUAL:
1477-
$distanceParams = $query->getValues()[0];
1478-
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($distanceParams[0]);
1479-
$binds[":{$placeholder}_1"] = $distanceParams[1];
1480-
return "ST_DWithin({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0), :{$placeholder}_1)";
1481-
14821514
case Query::TYPE_DISTANCE_NOT_EQUAL:
1483-
$distanceParams = $query->getValues()[0];
1484-
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($distanceParams[0]);
1485-
$binds[":{$placeholder}_1"] = $distanceParams[1];
1486-
return "NOT ST_DWithin({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0), :{$placeholder}_1)";
1487-
14881515
case Query::TYPE_DISTANCE_GREATER_THAN:
1489-
$distanceParams = $query->getValues()[0];
1490-
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($distanceParams[0]);
1491-
$binds[":{$placeholder}_1"] = $distanceParams[1];
1492-
return "ST_Distance({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0)) > :{$placeholder}_1";
1493-
14941516
case Query::TYPE_DISTANCE_LESS_THAN:
1495-
$distanceParams = $query->getValues()[0];
1496-
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($distanceParams[0]);
1497-
$binds[":{$placeholder}_1"] = $distanceParams[1];
1498-
return "ST_Distance({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0)) < :{$placeholder}_1";
1499-
1517+
return $this->handleDistanceSpatialQueries($query, $binds, $attribute, $alias, $placeholder);
15001518
case Query::TYPE_EQUAL:
15011519
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
1502-
return "ST_Equals({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1520+
return "ST_Equals({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, " . Database::SRID . "))";
15031521

15041522
case Query::TYPE_NOT_EQUAL:
15051523
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
1506-
return "NOT ST_Equals({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1524+
return "NOT ST_Equals({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, " . Database::SRID . "))";
15071525

15081526
case Query::TYPE_INTERSECTS:
15091527
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
1510-
return "ST_Intersects({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1528+
return "ST_Intersects({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, " . Database::SRID . "))";
15111529

15121530
case Query::TYPE_NOT_INTERSECTS:
15131531
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
1514-
return "NOT ST_Intersects({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1532+
return "NOT ST_Intersects({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, " . Database::SRID . "))";
15151533

15161534
case Query::TYPE_OVERLAPS:
15171535
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
1518-
return "ST_Overlaps({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1536+
return "ST_Overlaps({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, " . Database::SRID . "))";
15191537

15201538
case Query::TYPE_NOT_OVERLAPS:
15211539
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
1522-
return "NOT ST_Overlaps({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1540+
return "NOT ST_Overlaps({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, " . Database::SRID . "))";
15231541

15241542
case Query::TYPE_TOUCHES:
15251543
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
1526-
return "ST_Touches({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1544+
return "ST_Touches({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, " . Database::SRID . "))";
15271545

15281546
case Query::TYPE_NOT_TOUCHES:
15291547
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
1530-
return "NOT ST_Touches({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1548+
return "NOT ST_Touches({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, " . Database::SRID . "))";
15311549

15321550
case Query::TYPE_CONTAINS:
15331551
case Query::TYPE_NOT_CONTAINS:
@@ -1536,8 +1554,8 @@ protected function handleSpatialQueries(Query $query, array &$binds, string $att
15361554
$isNot = $query->getMethod() === Query::TYPE_NOT_CONTAINS;
15371555
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
15381556
return $isNot
1539-
? "NOT ST_Covers({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))"
1540-
: "ST_Covers({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1557+
? "NOT ST_Covers({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, " . Database::SRID . "))"
1558+
: "ST_Covers({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, " . Database::SRID . "))";
15411559

15421560
default:
15431561
throw new DatabaseException('Unknown spatial query method: ' . $query->getMethod());
@@ -1716,15 +1734,15 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool
17161734
case Database::VAR_DATETIME:
17171735
return 'TIMESTAMP(3)';
17181736

1719-
1737+
// in all other DB engines, 4326 is the default SRID
17201738
case Database::VAR_POINT:
1721-
return 'GEOMETRY(POINT)';
1739+
return 'GEOMETRY(POINT,' . Database::SRID . ')';
17221740

17231741
case Database::VAR_LINESTRING:
1724-
return 'GEOMETRY(LINESTRING)';
1742+
return 'GEOMETRY(LINESTRING,' . Database::SRID . ')';
17251743

17261744
case Database::VAR_POLYGON:
1727-
return 'GEOMETRY(POLYGON)';
1745+
return 'GEOMETRY(POLYGON,' . Database::SRID . ')';
17281746

17291747
default:
17301748
throw new DatabaseException('Unknown Type: ' . $type . '. Must be one of ' . Database::VAR_STRING . ', ' . Database::VAR_INTEGER . ', ' . Database::VAR_FLOAT . ', ' . Database::VAR_BOOLEAN . ', ' . Database::VAR_DATETIME . ', ' . Database::VAR_RELATIONSHIP . ', ' . Database::VAR_POINT . ', ' . Database::VAR_LINESTRING . ', ' . Database::VAR_POLYGON);

src/Database/Database.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ class Database
4949
public const BIG_INT_MAX = PHP_INT_MAX;
5050
public const DOUBLE_MAX = PHP_FLOAT_MAX;
5151

52+
// Global SRID for geographic coordinates (WGS84)
53+
public const SRID = 4326;
54+
5255
// Relationship Types
5356
public const VAR_RELATIONSHIP = 'relationship';
5457

src/Database/Query.php

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -903,11 +903,12 @@ public function setOnArray(bool $bool): void
903903
* @param string $attribute
904904
* @param array<mixed> $values
905905
* @param int|float $distance
906+
* @param bool $meters
906907
* @return Query
907908
*/
908-
public static function distanceEqual(string $attribute, array $values, int|float $distance): self
909+
public static function distanceEqual(string $attribute, array $values, int|float $distance, bool $meters = false): self
909910
{
910-
return new self(self::TYPE_DISTANCE_EQUAL, $attribute, [[$values,$distance]]);
911+
return new self(self::TYPE_DISTANCE_EQUAL, $attribute, [[$values,$distance,$meters]]);
911912
}
912913

913914
/**
@@ -916,11 +917,12 @@ public static function distanceEqual(string $attribute, array $values, int|float
916917
* @param string $attribute
917918
* @param array<mixed> $values
918919
* @param int|float $distance
920+
* @param bool $meters
919921
* @return Query
920922
*/
921-
public static function distanceNotEqual(string $attribute, array $values, int|float $distance): self
923+
public static function distanceNotEqual(string $attribute, array $values, int|float $distance, $meters = false): self
922924
{
923-
return new self(self::TYPE_DISTANCE_NOT_EQUAL, $attribute, [[$values,$distance]]);
925+
return new self(self::TYPE_DISTANCE_NOT_EQUAL, $attribute, [[$values,$distance,$meters]]);
924926
}
925927

926928
/**
@@ -929,11 +931,12 @@ public static function distanceNotEqual(string $attribute, array $values, int|fl
929931
* @param string $attribute
930932
* @param array<mixed> $values
931933
* @param int|float $distance
934+
* @param bool $meters
932935
* @return Query
933936
*/
934-
public static function distanceGreaterThan(string $attribute, array $values, int|float $distance): self
937+
public static function distanceGreaterThan(string $attribute, array $values, int|float $distance, $meters = false): self
935938
{
936-
return new self(self::TYPE_DISTANCE_GREATER_THAN, $attribute, [[$values,$distance]]);
939+
return new self(self::TYPE_DISTANCE_GREATER_THAN, $attribute, [[$values,$distance, $meters]]);
937940
}
938941

939942
/**
@@ -942,11 +945,12 @@ public static function distanceGreaterThan(string $attribute, array $values, int
942945
* @param string $attribute
943946
* @param array<mixed> $values
944947
* @param int|float $distance
948+
* @param bool $meters
945949
* @return Query
946950
*/
947-
public static function distanceLessThan(string $attribute, array $values, int|float $distance): self
951+
public static function distanceLessThan(string $attribute, array $values, int|float $distance, bool $meters = false): self
948952
{
949-
return new self(self::TYPE_DISTANCE_LESS_THAN, $attribute, [[$values,$distance]]);
953+
return new self(self::TYPE_DISTANCE_LESS_THAN, $attribute, [[$values,$distance,$meters]]);
950954
}
951955

952956
/**

src/Database/Validator/Query/Filter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ public function isValid($value): bool
263263
case Query::TYPE_DISTANCE_NOT_EQUAL:
264264
case Query::TYPE_DISTANCE_GREATER_THAN:
265265
case Query::TYPE_DISTANCE_LESS_THAN:
266-
if (count($value->getValues()) !== 1 || !is_array($value->getValues()[0]) || count($value->getValues()[0]) !== 2) {
266+
if (count($value->getValues()) !== 1 || !is_array($value->getValues()[0]) || count($value->getValues()[0]) !== 3) {
267267
$this->message = 'Distance query requires [[geometry, distance]] parameters';
268268
return false;
269269
}

0 commit comments

Comments
 (0)