Skip to content

Commit 06ffa2b

Browse files
authored
Merge pull request #682 from ArnabChatterjee20k/spatial-attribute-support
meters param for distance queries, srid postgres update and fixed structure exception not getting raised for spatial types
2 parents bfc010c + 49bb55a commit 06ffa2b

File tree

9 files changed

+501
-145
lines changed

9 files changed

+501
-145
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+
$useMeters = 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 ($useMeters) {
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: 46 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,51 @@ public function getSizeOfCollectionOnDisk(string $collection): int
7879
return $size;
7980
}
8081

82+
/**
83+
* Handle distance spatial queries
84+
*
85+
* @param Query $query
86+
* @param array<string, mixed> $binds
87+
* @param string $attribute
88+
* @param string $alias
89+
* @param string $placeholder
90+
* @return string
91+
*/
92+
protected function handleDistanceSpatialQueries(Query $query, array &$binds, string $attribute, string $alias, string $placeholder): string
93+
{
94+
$distanceParams = $query->getValues()[0];
95+
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($distanceParams[0]);
96+
$binds[":{$placeholder}_1"] = $distanceParams[1];
97+
98+
$useMeters = isset($distanceParams[2]) && $distanceParams[2] === true;
99+
100+
switch ($query->getMethod()) {
101+
case Query::TYPE_DISTANCE_EQUAL:
102+
$operator = '=';
103+
break;
104+
case Query::TYPE_DISTANCE_NOT_EQUAL:
105+
$operator = '!=';
106+
break;
107+
case Query::TYPE_DISTANCE_GREATER_THAN:
108+
$operator = '>';
109+
break;
110+
case Query::TYPE_DISTANCE_LESS_THAN:
111+
$operator = '<';
112+
break;
113+
default:
114+
throw new DatabaseException('Unknown spatial query method: ' . $query->getMethod());
115+
}
116+
117+
if ($useMeters) {
118+
$attr = "ST_SRID({$alias}.{$attribute}, " . Database::SRID . ")";
119+
$geom = "ST_GeomFromText(:{$placeholder}_0, " . Database::SRID . ")";
120+
return "ST_Distance({$attr}, {$geom}, 'metre') {$operator} :{$placeholder}_1";
121+
}
122+
123+
// Without meters, use default behavior
124+
return "ST_Distance({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0)) {$operator} :{$placeholder}_1";
125+
}
126+
81127
public function getSupportForIndexArray(): bool
82128
{
83129
/**

src/Database/Adapter/Postgres.php

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

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

14761523
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-
14821524
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-
14881525
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-
14941526
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-
1527+
return $this->handleDistanceSpatialQueries($query, $binds, $attribute, $alias, $placeholder);
15001528
case Query::TYPE_EQUAL:
15011529
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
1502-
return "ST_Equals({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1530+
return "ST_Equals({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, " . Database::SRID . "))";
15031531

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

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

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

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

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

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

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

15321560
case Query::TYPE_CONTAINS:
15331561
case Query::TYPE_NOT_CONTAINS:
@@ -1536,8 +1564,8 @@ protected function handleSpatialQueries(Query $query, array &$binds, string $att
15361564
$isNot = $query->getMethod() === Query::TYPE_NOT_CONTAINS;
15371565
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
15381566
return $isNot
1539-
? "NOT ST_Covers({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))"
1540-
: "ST_Covers({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1567+
? "NOT ST_Covers({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, " . Database::SRID . "))"
1568+
: "ST_Covers({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, " . Database::SRID . "))";
15411569

15421570
default:
15431571
throw new DatabaseException('Unknown spatial query method: ' . $query->getMethod());
@@ -1716,15 +1744,15 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool
17161744
case Database::VAR_DATETIME:
17171745
return 'TIMESTAMP(3)';
17181746

1719-
1747+
// in all other DB engines, 4326 is the default SRID
17201748
case Database::VAR_POINT:
1721-
return 'GEOMETRY(POINT)';
1749+
return 'GEOMETRY(POINT,' . Database::SRID . ')';
17221750

17231751
case Database::VAR_LINESTRING:
1724-
return 'GEOMETRY(LINESTRING)';
1752+
return 'GEOMETRY(LINESTRING,' . Database::SRID . ')';
17251753

17261754
case Database::VAR_POLYGON:
1727-
return 'GEOMETRY(POLYGON)';
1755+
return 'GEOMETRY(POLYGON,' . Database::SRID . ')';
17281756

17291757
default:
17301758
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: 6 additions & 1 deletion
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

@@ -7133,7 +7136,9 @@ private function processRelationshipQueries(
71337136
protected function encodeSpatialData(mixed $value, string $type): string
71347137
{
71357138
$validator = new Spatial($type);
7136-
$validator->isValid($value);
7139+
if (!$validator->isValid($value)) {
7140+
throw new StructureException($validator->getDescription());
7141+
}
71377142

71387143
switch ($type) {
71397144
case self::VAR_POINT:

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, bool $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, bool $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)