Skip to content

Commit 1bac5c6

Browse files
authored
Merge pull request #694 from utopia-php/spatial-filter
Refactor spatial attribute handling(order, index , filters)
2 parents 16f96e5 + 7123d28 commit 1bac5c6

File tree

5 files changed

+383
-47
lines changed

5 files changed

+383
-47
lines changed

src/Database/Adapter/MariaDB.php

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,11 +1417,11 @@ protected function handleSpatialQueries(Query $query, array &$binds, string $att
14171417
switch ($query->getMethod()) {
14181418
case Query::TYPE_CROSSES:
14191419
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
1420-
return "ST_Crosses({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1420+
return "ST_Crosses({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))";
14211421

14221422
case Query::TYPE_NOT_CROSSES:
14231423
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
1424-
return "NOT ST_Crosses({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1424+
return "NOT ST_Crosses({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))";
14251425

14261426
case Query::TYPE_DISTANCE_EQUAL:
14271427
case Query::TYPE_DISTANCE_NOT_EQUAL:
@@ -1431,43 +1431,43 @@ protected function handleSpatialQueries(Query $query, array &$binds, string $att
14311431

14321432
case Query::TYPE_INTERSECTS:
14331433
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
1434-
return "ST_Intersects({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1434+
return "ST_Intersects({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))";
14351435

14361436
case Query::TYPE_NOT_INTERSECTS:
14371437
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
1438-
return "NOT ST_Intersects({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1438+
return "NOT ST_Intersects({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))";
14391439

14401440
case Query::TYPE_OVERLAPS:
14411441
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
1442-
return "ST_Overlaps({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1442+
return "ST_Overlaps({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))";
14431443

14441444
case Query::TYPE_NOT_OVERLAPS:
14451445
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
1446-
return "NOT ST_Overlaps({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1446+
return "NOT ST_Overlaps({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))";
14471447

14481448
case Query::TYPE_TOUCHES:
14491449
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
1450-
return "ST_Touches({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1450+
return "ST_Touches({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))";
14511451

14521452
case Query::TYPE_NOT_TOUCHES:
14531453
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
1454-
return "NOT ST_Touches({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1454+
return "NOT ST_Touches({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))";
14551455

14561456
case Query::TYPE_EQUAL:
14571457
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
1458-
return "ST_Equals({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1458+
return "ST_Equals({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))";
14591459

14601460
case Query::TYPE_NOT_EQUAL:
14611461
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
1462-
return "NOT ST_Equals({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1462+
return "NOT ST_Equals({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))";
14631463

14641464
case Query::TYPE_CONTAINS:
14651465
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
1466-
return "ST_Contains({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1466+
return "ST_Contains({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))";
14671467

14681468
case Query::TYPE_NOT_CONTAINS:
14691469
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
1470-
return "NOT ST_Contains({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))";
1470+
return "NOT ST_Contains({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))";
14711471

14721472
default:
14731473
throw new DatabaseException('Unknown spatial query method: ' . $query->getMethod());
@@ -1641,13 +1641,39 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool
16411641

16421642

16431643
case Database::VAR_POINT:
1644-
return 'POINT' . ($required && !$this->getSupportForSpatialIndexNull() ? ' NOT NULL' : '');
1644+
$type = 'POINT';
1645+
if (!$this->getSupportForSpatialIndexNull()) {
1646+
if ($required) {
1647+
$type .= ' NOT NULL';
1648+
} else {
1649+
$type .= ' NULL';
1650+
}
1651+
}
1652+
return $type;
16451653

16461654
case Database::VAR_LINESTRING:
1647-
return 'LINESTRING' . ($required && !$this->getSupportForSpatialIndexNull() ? ' NOT NULL' : '');
1655+
$type = 'LINESTRING';
1656+
if (!$this->getSupportForSpatialIndexNull()) {
1657+
if ($required) {
1658+
$type .= ' NOT NULL';
1659+
} else {
1660+
$type .= ' NULL';
1661+
}
1662+
}
1663+
return $type;
1664+
16481665

16491666
case Database::VAR_POLYGON:
1650-
return 'POLYGON' . ($required && !$this->getSupportForSpatialIndexNull() ? ' NOT NULL' : '');
1667+
$type = 'POLYGON';
1668+
if (!$this->getSupportForSpatialIndexNull()) {
1669+
if ($required) {
1670+
$type .= ' NOT NULL';
1671+
} else {
1672+
$type .= ' NULL';
1673+
}
1674+
}
1675+
return $type;
1676+
16511677

16521678
default:
16531679
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/Adapter/MySQL.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ protected function handleDistanceSpatialQueries(Query $query, array &$binds, str
117117

118118
if ($useMeters) {
119119
$attr = "ST_SRID({$alias}.{$attribute}, " . Database::SRID . ")";
120-
$geom = "ST_GeomFromText(:{$placeholder}_0, " . Database::SRID . ")";
120+
$geom = "ST_GeomFromText(:{$placeholder}_0, " . Database::SRID . ",'axis-order=long-lat')";
121121
return "ST_Distance({$attr}, {$geom}, 'metre') {$operator} :{$placeholder}_1";
122122
}
123123

src/Database/Database.php

Lines changed: 103 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,88 @@ function (?string $value) {
477477
return DateTime::formatTz($value);
478478
}
479479
);
480+
481+
self::addFilter(
482+
Database::VAR_POINT,
483+
/**
484+
* @param mixed $value
485+
* @return mixed
486+
*/
487+
function (mixed $value) {
488+
if (!is_array($value)) {
489+
return $value;
490+
}
491+
try {
492+
return self::encodeSpatialData($value, Database::VAR_POINT);
493+
} catch (\Throwable) {
494+
return $value;
495+
}
496+
},
497+
/**
498+
* @param string|null $value
499+
* @return string|null
500+
*/
501+
function (?string $value) {
502+
if (!is_string($value)) {
503+
return $value;
504+
}
505+
return self::decodeSpatialData($value);
506+
}
507+
);
508+
self::addFilter(
509+
Database::VAR_LINESTRING,
510+
/**
511+
* @param mixed $value
512+
* @return mixed
513+
*/
514+
function (mixed $value) {
515+
if (is_null($value)) {
516+
return;
517+
}
518+
try {
519+
return self::encodeSpatialData($value, Database::VAR_LINESTRING);
520+
} catch (\Throwable) {
521+
return $value;
522+
}
523+
},
524+
/**
525+
* @param string|null $value
526+
* @return string|null
527+
*/
528+
function (?string $value) {
529+
if (is_null($value)) {
530+
return $value;
531+
}
532+
return self::decodeSpatialData($value);
533+
}
534+
);
535+
self::addFilter(
536+
Database::VAR_POLYGON,
537+
/**
538+
* @param mixed $value
539+
* @return mixed
540+
*/
541+
function (mixed $value) {
542+
if (is_null($value)) {
543+
return;
544+
}
545+
try {
546+
return self::encodeSpatialData($value, Database::VAR_POLYGON);
547+
} catch (\Throwable) {
548+
return $value;
549+
}
550+
},
551+
/**
552+
* @param string|null $value
553+
* @return string|null
554+
*/
555+
function (?string $value) {
556+
if (is_null($value)) {
557+
return $value;
558+
}
559+
return self::decodeSpatialData($value);
560+
}
561+
);
480562
}
481563

482564
/**
@@ -1242,6 +1324,19 @@ public function delete(?string $database = null): bool
12421324
*/
12431325
public function createCollection(string $id, array $attributes = [], array $indexes = [], ?array $permissions = null, bool $documentSecurity = true): Document
12441326
{
1327+
foreach ($attributes as &$attribute) {
1328+
if (in_array($attribute['type'], Database::SPATIAL_TYPES)) {
1329+
$existingFilters = $attribute['filters'] ?? [];
1330+
if (!is_array($existingFilters)) {
1331+
$existingFilters = [$existingFilters];
1332+
}
1333+
$attribute['filters'] = array_values(
1334+
array_unique(array_merge($existingFilters, [$attribute['type']]))
1335+
);
1336+
}
1337+
}
1338+
unset($attribute);
1339+
12451340
$permissions ??= [
12461341
Permission::create(Role::any()),
12471342
];
@@ -1598,6 +1693,10 @@ public function createAttribute(string $collection, string $id, string $type, in
15981693
if ($collection->isEmpty()) {
15991694
throw new NotFoundException('Collection not found');
16001695
}
1696+
if (in_array($type, Database::SPATIAL_TYPES)) {
1697+
$filters[] = $type;
1698+
$filters = array_unique($filters);
1699+
}
16011700

16021701
$attribute = $this->validateAttribute(
16031702
$collection,
@@ -2161,7 +2260,8 @@ public function updateAttribute(string $collection, string $id, ?string $type =
21612260
$default = null;
21622261
}
21632262

2164-
if ($required === true && in_array($type, Database::SPATIAL_TYPES)) {
2263+
// we need to alter table attribute type to NOT NULL/NULL for change in required
2264+
if (!$this->adapter->getSupportForSpatialIndexNull() && in_array($type, Database::SPATIAL_TYPES)) {
21652265
$altering = true;
21662266
}
21672267

@@ -3221,12 +3321,12 @@ public function createIndex(string $collection, string $id, string $type, array
32213321
if ($type === self::INDEX_SPATIAL) {
32223322
foreach ($attributes as $attr) {
32233323
if (!isset($indexAttributesWithTypes[$attr])) {
3224-
throw new DatabaseException('Attribute "' . $attr . '" not found in collection');
3324+
throw new IndexException('Attribute "' . $attr . '" not found in collection');
32253325
}
32263326

32273327
$attributeType = $indexAttributesWithTypes[$attr];
32283328
if (!in_array($attributeType, [self::VAR_POINT, self::VAR_LINESTRING, self::VAR_POLYGON])) {
3229-
throw new DatabaseException('Spatial index can only be created on spatial attributes (point, linestring, polygon). Attribute "' . $attr . '" is of type "' . $attributeType . '"');
3329+
throw new IndexException('Spatial index can only be created on spatial attributes (point, linestring, polygon). Attribute "' . $attr . '" is of type "' . $attributeType . '"');
32303330
}
32313331
}
32323332

@@ -4640,7 +4740,6 @@ public function updateDocuments(
46404740
if (!is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) {
46414741
throw new ConflictException('Document was updated after the request timestamp');
46424742
}
4643-
46444743
$batch[$index] = $this->encode($collection, $document);
46454744
}
46464745

@@ -6561,14 +6660,6 @@ public function encode(Document $collection, Document $document): Document
65616660

65626661
foreach ($value as $index => $node) {
65636662
if ($node !== null) {
6564-
// Handle spatial data encoding
6565-
$attributeType = $attribute['type'] ?? '';
6566-
if (in_array($attributeType, Database::SPATIAL_TYPES)) {
6567-
if (is_array($node)) {
6568-
$node = $this->encodeSpatialData($node, $attributeType);
6569-
}
6570-
}
6571-
65726663
foreach ($filters as $filter) {
65736664
$node = $this->encodeAttribute($filter, $node, $document);
65746665
}
@@ -6647,9 +6738,6 @@ public function decode(Document $collection, Document $document, array $selectio
66476738
$value = (is_null($value)) ? [] : $value;
66486739

66496740
foreach ($value as $index => $node) {
6650-
if (is_string($node) && in_array($type, Database::SPATIAL_TYPES)) {
6651-
$node = $this->decodeSpatialData($node);
6652-
}
66536741

66546742
foreach (array_reverse($filters) as $filter) {
66556743
$node = $this->decodeAttribute($filter, $node, $document, $key);

src/Database/Validator/Index.php

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -339,14 +339,6 @@ public function getType(): string
339339
public function checkSpatialIndex(Document $index): bool
340340
{
341341
$type = $index->getAttribute('type');
342-
if ($type !== Database::INDEX_SPATIAL) {
343-
return true;
344-
}
345-
346-
if (!$this->spatialIndexSupport) {
347-
$this->message = 'Spatial indexes are not supported';
348-
return false;
349-
}
350342

351343
$attributes = $index->getAttribute('attributes', []);
352344
$orders = $index->getAttribute('orders', []);
@@ -356,22 +348,36 @@ public function checkSpatialIndex(Document $index): bool
356348
$attributeType = $attribute->getAttribute('type', '');
357349

358350
if (!\in_array($attributeType, Database::SPATIAL_TYPES, true)) {
359-
$this->message = 'Spatial index can only be created on spatial attributes (point, linestring, polygon). Attribute "' . $attributeName . '" is of type "' . $attributeType . '"';
351+
continue;
352+
}
353+
354+
if (!$this->spatialIndexSupport) {
355+
$this->message = 'Spatial indexes are not supported';
360356
return false;
361357
}
362358

359+
if (count($attributes) !== 1) {
360+
$this->message = 'Spatial index can be created on a single spatial attribute';
361+
return false;
362+
}
363+
364+
if ($type !== Database::INDEX_SPATIAL) {
365+
$this->message = 'Spatial index can only be created on spatial attributes (point, linestring, polygon). Attribute "' . $attributeName . '" is of type "' . $attributeType . '"';
366+
return false;
367+
}
363368
$required = (bool) $attribute->getAttribute('required', false);
364369
if (!$required && !$this->spatialIndexNullSupport) {
365370
$this->message = 'Spatial indexes do not allow null values. Mark the attribute "' . $attributeName . '" as required or create the index on a column with no null values.';
366371
return false;
367372
}
368-
}
369373

370-
if (!empty($orders) && !$this->spatialIndexOrderSupport) {
371-
$this->message = 'Spatial indexes with explicit orders are not supported. Remove the orders to create this index.';
372-
return false;
374+
if (!empty($orders) && !$this->spatialIndexOrderSupport) {
375+
$this->message = 'Spatial indexes with explicit orders are not supported. Remove the orders to create this index.';
376+
return false;
377+
}
373378
}
374379

380+
375381
return true;
376382
}
377383
}

0 commit comments

Comments
 (0)