From 0d4f2ec6a365d97f4e6f5e9ea6917fc5ea6c60d7 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 10 Sep 2025 15:45:33 +0530 Subject: [PATCH 1/8] added long-lat order to constant in the mariadb/mysql adapter --- src/Database/Adapter/MariaDB.php | 24 ++++++++++++------------ src/Database/Adapter/MySQL.php | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index cb4b27fed..0c2f339bb 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1417,11 +1417,11 @@ protected function handleSpatialQueries(Query $query, array &$binds, string $att switch ($query->getMethod()) { case Query::TYPE_CROSSES: $binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]); - return "ST_Crosses({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))"; + return "ST_Crosses({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))"; case Query::TYPE_NOT_CROSSES: $binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]); - return "NOT ST_Crosses({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))"; + return "NOT ST_Crosses({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))"; case Query::TYPE_DISTANCE_EQUAL: case Query::TYPE_DISTANCE_NOT_EQUAL: @@ -1431,43 +1431,43 @@ protected function handleSpatialQueries(Query $query, array &$binds, string $att case Query::TYPE_INTERSECTS: $binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]); - return "ST_Intersects({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))"; + return "ST_Intersects({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))"; case Query::TYPE_NOT_INTERSECTS: $binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]); - return "NOT ST_Intersects({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))"; + return "NOT ST_Intersects({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))"; case Query::TYPE_OVERLAPS: $binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]); - return "ST_Overlaps({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))"; + return "ST_Overlaps({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))"; case Query::TYPE_NOT_OVERLAPS: $binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]); - return "NOT ST_Overlaps({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))"; + return "NOT ST_Overlaps({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))"; case Query::TYPE_TOUCHES: $binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]); - return "ST_Touches({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))"; + return "ST_Touches({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))"; case Query::TYPE_NOT_TOUCHES: $binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]); - return "NOT ST_Touches({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))"; + return "NOT ST_Touches({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))"; case Query::TYPE_EQUAL: $binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]); - return "ST_Equals({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))"; + return "ST_Equals({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))"; case Query::TYPE_NOT_EQUAL: $binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]); - return "NOT ST_Equals({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))"; + return "NOT ST_Equals({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))"; case Query::TYPE_CONTAINS: $binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]); - return "ST_Contains({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))"; + return "ST_Contains({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))"; case Query::TYPE_NOT_CONTAINS: $binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]); - return "NOT ST_Contains({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0))"; + return "NOT ST_Contains({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0, 'axis-order=long-lat'))"; default: throw new DatabaseException('Unknown spatial query method: ' . $query->getMethod()); diff --git a/src/Database/Adapter/MySQL.php b/src/Database/Adapter/MySQL.php index 27a4028f3..f15e5d76c 100644 --- a/src/Database/Adapter/MySQL.php +++ b/src/Database/Adapter/MySQL.php @@ -117,7 +117,7 @@ protected function handleDistanceSpatialQueries(Query $query, array &$binds, str if ($useMeters) { $attr = "ST_SRID({$alias}.{$attribute}, " . Database::SRID . ")"; - $geom = "ST_GeomFromText(:{$placeholder}_0, " . Database::SRID . ")"; + $geom = "ST_GeomFromText(:{$placeholder}_0, " . Database::SRID . ",'axis-order=long-lat')"; return "ST_Distance({$attr}, {$geom}, 'metre') {$operator} :{$placeholder}_1"; } From 796310b430b59b09b4b179136d7598336af0b597 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 10 Sep 2025 17:01:49 +0530 Subject: [PATCH 2/8] spatial types filter --- src/Database/Database.php | 113 +++++++++++++++++++--- tests/e2e/Adapter/Scopes/SpatialTests.php | 68 +++++++++++++ 2 files changed, 170 insertions(+), 11 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 3f74ef10b..85bce93b8 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -477,6 +477,91 @@ function (?string $value) { return DateTime::formatTz($value); } ); + + self::addFilter( + Database::VAR_POINT, + /** + * @param mixed $value + * @return mixed + */ + function (mixed $value) { + if (is_null($value)) { + return; + } + try { + return self::encodeSpatialData($value, Database::VAR_POINT); + } catch (\Throwable) { + return $value; + } + }, + /** + * @param string|null $value + * @return string|null + */ + function (?string $value) { + if (is_null($value)) { + return $value; + } + return self::decodeSpatialData($value); + } + ); + self::addFilter( + Database::VAR_LINESTRING, + /** + * @param mixed $value + * @return mixed + */ + function (mixed $value) { + if (is_null($value)) { + return; + } + try { + return self::encodeSpatialData($value, Database::VAR_LINESTRING); + } catch (\Throwable) { + if (is_null($value)) { + return $value; + } + return $value; + } + }, + /** + * @param string|null $value + * @return string|null + */ + function (?string $value) { + if (is_null($value)) { + return $value; + } + return self::decodeSpatialData($value); + } + ); + self::addFilter( + Database::VAR_POLYGON, + /** + * @param mixed $value + * @return mixed + */ + function (mixed $value) { + if (is_null($value)) { + return; + } + try { + return self::encodeSpatialData($value, Database::VAR_POLYGON); + } catch (\Throwable) { + return $value; + } + }, + /** + * @param string|null $value + * @return string|null + */ + function (?string $value) { + if (is_null($value)) { + return $value; + } + return self::decodeSpatialData($value); + } + ); } /** @@ -1242,6 +1327,19 @@ public function delete(?string $database = null): bool */ public function createCollection(string $id, array $attributes = [], array $indexes = [], ?array $permissions = null, bool $documentSecurity = true): Document { + foreach ($attributes as &$attribute) { + if (in_array($attribute['type'], Database::SPATIAL_TYPES)) { + $existingFilters = $attribute['filters'] ?? []; + if (!is_array($existingFilters)) { + $existingFilters = [$existingFilters]; + } + $attribute['filters'] = array_values( + array_unique(array_merge($existingFilters, [$attribute['type']])) + ); + } + } + unset($attribute); + $permissions ??= [ Permission::create(Role::any()), ]; @@ -1598,6 +1696,10 @@ public function createAttribute(string $collection, string $id, string $type, in if ($collection->isEmpty()) { throw new NotFoundException('Collection not found'); } + if (in_array($type, Database::SPATIAL_TYPES)) { + $filters[] = $type; + $filters = array_unique($filters); + } $attribute = $this->validateAttribute( $collection, @@ -6561,14 +6663,6 @@ public function encode(Document $collection, Document $document): Document foreach ($value as $index => $node) { if ($node !== null) { - // Handle spatial data encoding - $attributeType = $attribute['type'] ?? ''; - if (in_array($attributeType, Database::SPATIAL_TYPES)) { - if (is_array($node)) { - $node = $this->encodeSpatialData($node, $attributeType); - } - } - foreach ($filters as $filter) { $node = $this->encodeAttribute($filter, $node, $document); } @@ -6647,9 +6741,6 @@ public function decode(Document $collection, Document $document, array $selectio $value = (is_null($value)) ? [] : $value; foreach ($value as $index => $node) { - if (is_string($node) && in_array($type, Database::SPATIAL_TYPES)) { - $node = $this->decodeSpatialData($node); - } foreach (array_reverse($filters) as $filter) { $node = $this->decodeAttribute($filter, $node, $document, $key); diff --git a/tests/e2e/Adapter/Scopes/SpatialTests.php b/tests/e2e/Adapter/Scopes/SpatialTests.php index 281edf070..983851a71 100644 --- a/tests/e2e/Adapter/Scopes/SpatialTests.php +++ b/tests/e2e/Adapter/Scopes/SpatialTests.php @@ -2392,4 +2392,72 @@ public function testSpatialDistanceInMeterError(): void } } } + public function testSpatialEncodeDecode(): void + { + $collection = new Document([ + '$collection' => ID::custom(Database::METADATA), + '$id' => ID::custom('users'), + 'name' => 'Users', + 'attributes' => [ + [ + '$id' => ID::custom('point'), + 'type' => Database::VAR_POINT, + 'required' => false, + 'filters' => [Database::VAR_POINT], + ], + [ + '$id' => ID::custom('line'), + 'type' => Database::VAR_LINESTRING, + 'format' => '', + 'required' => false, + 'filters' => [Database::VAR_LINESTRING], + ], + [ + '$id' => ID::custom('poly'), + 'type' => Database::VAR_POLYGON, + 'format' => '', + 'required' => false, + 'filters' => [Database::VAR_POLYGON], + ] + ] + ]); + + /** @var Database $database */ + $database = static::getDatabase(); + if (!$database->getAdapter()->getSupportForSpatialAttributes()) { + $this->markTestSkipped('Adapter does not support spatial attributes'); + } + $point = "POINT(1 2)"; + $line = "LINESTRING(1 2, 1 2)"; + $poly = "POLYGON((0 0, 0 10, 10 10, 0 0))"; + + $pointArr = [1,2]; + $lineArr = [[1,2],[1,2]]; + $polyArr = [[[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [0.0, 0.0]]]; + $doc = new Document(['point' => $pointArr ,'line' => $lineArr, 'poly' => $polyArr]); + + $result = $database->encode($collection, $doc); + + $this->assertEquals($result->getAttribute('point'), $point); + $this->assertEquals($result->getAttribute('line'), $line); + $this->assertEquals($result->getAttribute('poly'), $poly); + + + $result = $database->decode($collection, $doc); + $this->assertEquals($result->getAttribute('point'), $pointArr); + $this->assertEquals($result->getAttribute('line'), $lineArr); + $this->assertEquals($result->getAttribute('poly'), $polyArr); + + $stringDoc = new Document(['point' => $point,'line' => $line, 'poly' => $poly]); + $result = $database->decode($collection, $stringDoc); + $this->assertEquals($result->getAttribute('point'), $pointArr); + $this->assertEquals($result->getAttribute('line'), $lineArr); + $this->assertEquals($result->getAttribute('poly'), $polyArr); + + $nullDoc = new Document(['point' => null,'line' => null, 'poly' => null]); + $result = $database->decode($collection, $nullDoc); + $this->assertEquals($result->getAttribute('point'), null); + $this->assertEquals($result->getAttribute('line'), null); + $this->assertEquals($result->getAttribute('poly'), null); + } } From 7f08617ce0c838e7e37825c8aa350664363fb1d8 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 10 Sep 2025 17:25:31 +0530 Subject: [PATCH 3/8] updated condition for returning value in the filters encode/decode --- src/Database/Database.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 85bce93b8..b1d9053e2 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -485,8 +485,8 @@ function (?string $value) { * @return mixed */ function (mixed $value) { - if (is_null($value)) { - return; + if (!is_array($value)) { + return $value; } try { return self::encodeSpatialData($value, Database::VAR_POINT); @@ -499,7 +499,7 @@ function (mixed $value) { * @return string|null */ function (?string $value) { - if (is_null($value)) { + if (!is_string($value)) { return $value; } return self::decodeSpatialData($value); @@ -4742,7 +4742,6 @@ public function updateDocuments( if (!is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) { throw new ConflictException('Document was updated after the request timestamp'); } - $batch[$index] = $this->encode($collection, $document); } From adb82de6663419e882ea1f6bbe47ebf385d4a497 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 10 Sep 2025 18:30:11 +0530 Subject: [PATCH 4/8] refactor spatial attribute handling to support NULL values and improve index validation * fixed long-lat order for geo function * added spatial types as filters * index validation updates * test update for index createion edge cases and filter encoding and decoding --- src/Database/Adapter/MariaDB.php | 32 +++++++- src/Database/Database.php | 7 +- src/Database/Validator/Index.php | 14 ++-- tests/e2e/Adapter/Scopes/SpatialTests.php | 92 ++++++++++++++++++++++- 4 files changed, 131 insertions(+), 14 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 0c2f339bb..08afe6452 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1641,13 +1641,39 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool case Database::VAR_POINT: - return 'POINT' . ($required && !$this->getSupportForSpatialIndexNull() ? ' NOT NULL' : ''); + $type = 'POINT'; + if (!$this->getSupportForSpatialIndexNull()) { + if ($required) { + $type .= ' NOT NULL'; + } else { + $type .= ' NULL'; + } + } + return $type; case Database::VAR_LINESTRING: - return 'LINESTRING' . ($required && !$this->getSupportForSpatialIndexNull() ? ' NOT NULL' : ''); + $type = 'LINESTRING'; + if (!$this->getSupportForSpatialIndexNull()) { + if ($required) { + $type .= ' NOT NULL'; + } else { + $type .= ' NULL'; + } + } + return $type; + case Database::VAR_POLYGON: - return 'POLYGON' . ($required && !$this->getSupportForSpatialIndexNull() ? ' NOT NULL' : ''); + $type = 'POLYGON'; + if (!$this->getSupportForSpatialIndexNull()) { + if ($required) { + $type .= ' NOT NULL'; + } else { + $type .= ' NULL'; + } + } + return $type; + default: 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); diff --git a/src/Database/Database.php b/src/Database/Database.php index b1d9053e2..471c7277b 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2263,7 +2263,8 @@ public function updateAttribute(string $collection, string $id, ?string $type = $default = null; } - if ($required === true && in_array($type, Database::SPATIAL_TYPES)) { + // we need to alter table attribute type to NOT NULL/NULL for change in required + if (!$this->adapter->getSupportForSpatialIndexNull() && in_array($type, Database::SPATIAL_TYPES)) { $altering = true; } @@ -3323,12 +3324,12 @@ public function createIndex(string $collection, string $id, string $type, array if ($type === self::INDEX_SPATIAL) { foreach ($attributes as $attr) { if (!isset($indexAttributesWithTypes[$attr])) { - throw new DatabaseException('Attribute "' . $attr . '" not found in collection'); + throw new IndexException('Attribute "' . $attr . '" not found in collection'); } $attributeType = $indexAttributesWithTypes[$attr]; if (!in_array($attributeType, [self::VAR_POINT, self::VAR_LINESTRING, self::VAR_POLYGON])) { - throw new DatabaseException('Spatial index can only be created on spatial attributes (point, linestring, polygon). Attribute "' . $attr . '" is of type "' . $attributeType . '"'); + throw new IndexException('Spatial index can only be created on spatial attributes (point, linestring, polygon). Attribute "' . $attr . '" is of type "' . $attributeType . '"'); } } diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index 87fa51e78..2385888aa 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -339,9 +339,6 @@ public function getType(): string public function checkSpatialIndex(Document $index): bool { $type = $index->getAttribute('type'); - if ($type !== Database::INDEX_SPATIAL) { - return true; - } if (!$this->spatialIndexSupport) { $this->message = 'Spatial indexes are not supported'; @@ -351,15 +348,22 @@ public function checkSpatialIndex(Document $index): bool $attributes = $index->getAttribute('attributes', []); $orders = $index->getAttribute('orders', []); + if (count($attributes) !== 1) { + $this->message = 'Spatial index can be created on a single spatial attribute'; + return false; + } + foreach ($attributes as $attributeName) { $attribute = $this->attributes[\strtolower($attributeName)] ?? new Document(); $attributeType = $attribute->getAttribute('type', ''); - if (!\in_array($attributeType, Database::SPATIAL_TYPES, true)) { + continue; + } + + if ($type !== Database::INDEX_SPATIAL) { $this->message = 'Spatial index can only be created on spatial attributes (point, linestring, polygon). Attribute "' . $attributeName . '" is of type "' . $attributeType . '"'; return false; } - $required = (bool) $attribute->getAttribute('required', false); if (!$required && !$this->spatialIndexNullSupport) { $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.'; diff --git a/tests/e2e/Adapter/Scopes/SpatialTests.php b/tests/e2e/Adapter/Scopes/SpatialTests.php index 983851a71..7b8512e2e 100644 --- a/tests/e2e/Adapter/Scopes/SpatialTests.php +++ b/tests/e2e/Adapter/Scopes/SpatialTests.php @@ -5,6 +5,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception; +use Utopia\Database\Exception\Index as IndexException; use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Exception\Structure as StructureException; use Utopia\Database\Helpers\ID; @@ -1469,7 +1470,6 @@ public function testSpatialBulkOperation(): void 'required' => true, 'signed' => true, 'array' => false, - 'filters' => [], ]), new Document([ '$id' => ID::custom('location'), @@ -1478,7 +1478,6 @@ public function testSpatialBulkOperation(): void 'required' => true, 'signed' => true, 'array' => false, - 'filters' => [], ]), new Document([ '$id' => ID::custom('area'), @@ -1487,7 +1486,6 @@ public function testSpatialBulkOperation(): void 'required' => false, 'signed' => true, 'array' => false, - 'filters' => [], ]) ]; @@ -2460,4 +2458,92 @@ public function testSpatialEncodeDecode(): void $this->assertEquals($result->getAttribute('line'), null); $this->assertEquals($result->getAttribute('poly'), null); } + + public function testSpatialIndexSingleAttributeOnly(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + if (!$database->getAdapter()->getSupportForSpatialAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + $collectionName = 'spatial_idx_single_attr_' . uniqid(); + try { + $database->createCollection($collectionName); + + // Create a spatial attribute + $database->createAttribute($collectionName, 'loc', Database::VAR_POINT, 0, true); + $database->createAttribute($collectionName, 'loc2', Database::VAR_POINT, 0, true); + $database->createAttribute($collectionName, 'title', Database::VAR_STRING, 255, true); + + // Case 1: Valid spatial index on a single spatial attribute + $this->assertTrue( + $database->createIndex($collectionName, 'idx_loc', Database::INDEX_SPATIAL, ['loc']) + ); + + // Case 2: Fail when trying to create spatial index with multiple attributes + try { + $database->createIndex($collectionName, 'idx_multi', Database::INDEX_SPATIAL, ['loc', 'loc2']); + $this->fail('Expected exception when creating spatial index on multiple attributes'); + } catch (\Throwable $e) { + $this->assertInstanceOf(IndexException::class, $e); + } + + // Case 3: Fail when trying to create non-spatial index on a spatial attribute + try { + $database->createIndex($collectionName, 'idx_wrong_type', Database::INDEX_KEY, ['loc']); + $this->fail('Expected exception when creating non-spatial index on spatial attribute'); + } catch (\Throwable $e) { + $this->assertInstanceOf(IndexException::class, $e); + } + + // Case 4: Fail when trying to mix spatial + non-spatial attributes in a spatial index + try { + $database->createIndex($collectionName, 'idx_mix', Database::INDEX_SPATIAL, ['loc', 'title']); + $this->fail('Expected exception when creating spatial index with mixed attribute types'); + } catch (\Throwable $e) { + $this->assertInstanceOf(IndexException::class, $e); + } + + } finally { + $database->deleteCollection($collectionName); + } + } + + public function testSpatialIndexRequiredToggling(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + if (!$database->getAdapter()->getSupportForSpatialAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + if ($database->getAdapter()->getSupportForSpatialIndexNull()) { + $this->expectNotToPerformAssertions(); + return; + } + + try { + $collUpdateNull = 'spatial_idx_toggle'; + $database->createCollection($collUpdateNull); + + $database->createAttribute($collUpdateNull, 'loc', Database::VAR_POINT, 0, false); + try { + $database->createIndex($collUpdateNull, 'idx_loc', Database::INDEX_SPATIAL, ['loc']); + $this->fail('Expected exception when creating spatial index on NULL-able attribute'); + } catch (\Throwable $e) { + $this->assertInstanceOf(Exception::class, $e); + } + $database->updateAttribute($collUpdateNull, 'loc', required: true); + $this->assertTrue($database->createIndex($collUpdateNull, 'new index', Database::INDEX_SPATIAL, ['loc'])); + $this->assertTrue($database->deleteIndex($collUpdateNull, 'new index')); + $database->updateAttribute($collUpdateNull, 'loc', required: false); + + $database->createDocument($collUpdateNull, new Document(['loc' => null])); + } finally { + $database->deleteCollection($collUpdateNull); + } + } + } From d291d6ae1e4b3988090d0229f8873d0131e76cfb Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 10 Sep 2025 18:34:57 +0530 Subject: [PATCH 5/8] linting --- src/Database/Database.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 471c7277b..91ca5c1ca 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -518,9 +518,6 @@ function (mixed $value) { try { return self::encodeSpatialData($value, Database::VAR_LINESTRING); } catch (\Throwable) { - if (is_null($value)) { - return $value; - } return $value; } }, From 7ed30f442ba3957ddf982d679e6e3e92d7cf8004 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 10 Sep 2025 19:30:24 +0530 Subject: [PATCH 6/8] updated index validator for spatial types --- src/Database/Validator/Index.php | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index 2385888aa..bab80c173 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -340,26 +340,27 @@ public function checkSpatialIndex(Document $index): bool { $type = $index->getAttribute('type'); - if (!$this->spatialIndexSupport) { - $this->message = 'Spatial indexes are not supported'; - return false; - } - $attributes = $index->getAttribute('attributes', []); $orders = $index->getAttribute('orders', []); - if (count($attributes) !== 1) { - $this->message = 'Spatial index can be created on a single spatial attribute'; - return false; - } - foreach ($attributes as $attributeName) { $attribute = $this->attributes[\strtolower($attributeName)] ?? new Document(); $attributeType = $attribute->getAttribute('type', ''); + if (!\in_array($attributeType, Database::SPATIAL_TYPES, true)) { continue; } + if (!$this->spatialIndexSupport) { + $this->message = 'Spatial indexes are not supported'; + return false; + } + + if (count($attributes) !== 1) { + $this->message = 'Spatial index can be created on a single spatial attribute'; + return false; + } + if ($type !== Database::INDEX_SPATIAL) { $this->message = 'Spatial index can only be created on spatial attributes (point, linestring, polygon). Attribute "' . $attributeName . '" is of type "' . $attributeType . '"'; return false; @@ -369,13 +370,14 @@ public function checkSpatialIndex(Document $index): bool $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.'; return false; } - } - if (!empty($orders) && !$this->spatialIndexOrderSupport) { - $this->message = 'Spatial indexes with explicit orders are not supported. Remove the orders to create this index.'; - return false; + if (!empty($orders) && !$this->spatialIndexOrderSupport) { + $this->message = 'Spatial indexes with explicit orders are not supported. Remove the orders to create this index.'; + return false; + } } + return true; } } From 4f7bae74abf3968ea24609378b34a2675778e999 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 10 Sep 2025 19:38:34 +0530 Subject: [PATCH 7/8] updated spatial type tests with spatial index and non spatial combiation --- tests/e2e/Adapter/Scopes/SpatialTests.php | 62 +++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/e2e/Adapter/Scopes/SpatialTests.php b/tests/e2e/Adapter/Scopes/SpatialTests.php index 7b8512e2e..1aadeeed3 100644 --- a/tests/e2e/Adapter/Scopes/SpatialTests.php +++ b/tests/e2e/Adapter/Scopes/SpatialTests.php @@ -12,6 +12,7 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; +use Utopia\Database\Validator\Index; trait SpatialTests { @@ -2546,4 +2547,65 @@ public function testSpatialIndexRequiredToggling(): void } } + public function testSpatialIndexOnNonSpatial(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + if (!$database->getAdapter()->getSupportForSpatialAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + try { + $collUpdateNull = 'spatial_idx_toggle'; + $database->createCollection($collUpdateNull); + + $database->createAttribute($collUpdateNull, 'loc', Database::VAR_POINT, 0, true); + $database->createAttribute($collUpdateNull, 'name', Database::VAR_STRING, 0, true); + try { + $database->createIndex($collUpdateNull, 'idx_loc', Database::INDEX_SPATIAL, ['name']); + $this->fail('Expected exception when creating spatial index on NULL-able attribute'); + } catch (\Throwable $e) { + $this->assertInstanceOf(IndexException::class, $e); + } + + try { + $database->createIndex($collUpdateNull, 'idx_loc', Database::INDEX_KEY, ['loc']); + $this->fail('Expected exception when creating non spatial index on spatial attribute'); + } catch (\Throwable $e) { + $this->assertInstanceOf(IndexException::class, $e); + } + + try { + $database->createIndex($collUpdateNull, 'idx_loc', Database::INDEX_KEY, ['loc,name']); + $this->fail('Expected exception when creating index'); + } catch (\Throwable $e) { + $this->assertInstanceOf(IndexException::class, $e); + } + + try { + $database->createIndex($collUpdateNull, 'idx_loc', Database::INDEX_KEY, ['name,loc']); + $this->fail('Expected exception when creating index'); + } catch (\Throwable $e) { + $this->assertInstanceOf(IndexException::class, $e); + } + + try { + $database->createIndex($collUpdateNull, 'idx_loc', Database::INDEX_SPATIAL, ['name,loc']); + $this->fail('Expected exception when creating index'); + } catch (\Throwable $e) { + $this->assertInstanceOf(IndexException::class, $e); + } + + try { + $database->createIndex($collUpdateNull, 'idx_loc', Database::INDEX_SPATIAL, ['loc,name']); + $this->fail('Expected exception when creating index'); + } catch (\Throwable $e) { + $this->assertInstanceOf(IndexException::class, $e); + } + + } finally { + $database->deleteCollection($collUpdateNull); + } + } } From 7123d2870bb15571b18c95631f0daf1c762f76f3 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 10 Sep 2025 19:51:28 +0530 Subject: [PATCH 8/8] fixed postgres test failing due to 0 length varchar --- tests/e2e/Adapter/Scopes/SpatialTests.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Adapter/Scopes/SpatialTests.php b/tests/e2e/Adapter/Scopes/SpatialTests.php index 1aadeeed3..5e1a4f4e4 100644 --- a/tests/e2e/Adapter/Scopes/SpatialTests.php +++ b/tests/e2e/Adapter/Scopes/SpatialTests.php @@ -2561,7 +2561,7 @@ public function testSpatialIndexOnNonSpatial(): void $database->createCollection($collUpdateNull); $database->createAttribute($collUpdateNull, 'loc', Database::VAR_POINT, 0, true); - $database->createAttribute($collUpdateNull, 'name', Database::VAR_STRING, 0, true); + $database->createAttribute($collUpdateNull, 'name', Database::VAR_STRING, 4, true); try { $database->createIndex($collUpdateNull, 'idx_loc', Database::INDEX_SPATIAL, ['name']); $this->fail('Expected exception when creating spatial index on NULL-able attribute');