Skip to content

Commit 3a4edc0

Browse files
fixed structure exception not getting raised for spatial types
1 parent bfc010c commit 3a4edc0

File tree

3 files changed

+145
-87
lines changed

3 files changed

+145
-87
lines changed

src/Database/Database.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7133,7 +7133,9 @@ private function processRelationshipQueries(
71337133
protected function encodeSpatialData(mixed $value, string $type): string
71347134
{
71357135
$validator = new Spatial($type);
7136-
$validator->isValid($value);
7136+
if (!$validator->isValid($value)) {
7137+
throw new StructureException($validator->getDescription());
7138+
}
71377139

71387140
switch ($type) {
71397141
case self::VAR_POINT:

src/Database/Validator/Spatial.php

Lines changed: 43 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -3,87 +3,55 @@
33
namespace Utopia\Database\Validator;
44

55
use Utopia\Database\Database;
6-
use Utopia\Database\Exception;
76
use Utopia\Validator;
87

98
class Spatial extends Validator
109
{
1110
private string $spatialType;
11+
protected string $message = '';
1212

1313
public function __construct(string $spatialType)
1414
{
1515
$this->spatialType = $spatialType;
1616
}
1717

18-
/**
19-
* Validate spatial data according to its type
20-
*
21-
* @param mixed $value
22-
* @param string $type
23-
* @return bool
24-
* @throws Exception
25-
*/
26-
public static function validate(mixed $value, string $type): bool
27-
{
28-
if (!is_array($value)) {
29-
throw new Exception('Spatial data must be provided as an array');
30-
}
31-
32-
switch ($type) {
33-
case Database::VAR_POINT:
34-
return self::validatePoint($value);
35-
36-
case Database::VAR_LINESTRING:
37-
return self::validateLineString($value);
38-
39-
case Database::VAR_POLYGON:
40-
return self::validatePolygon($value);
41-
42-
default:
43-
throw new Exception('Unknown spatial type: ' . $type);
44-
}
45-
}
46-
4718
/**
4819
* Validate POINT data
49-
*
50-
* @param array<mixed,mixed> $value
51-
* @return bool
52-
* @throws Exception
5320
*/
54-
protected static function validatePoint(array $value): bool
21+
protected function validatePoint(array $value): bool
5522
{
5623
if (count($value) !== 2) {
57-
throw new Exception('Point must be an array of two numeric values [x, y]');
24+
$this->message = 'Point must be an array of two numeric values [x, y]';
25+
return false;
5826
}
5927

6028
if (!is_numeric($value[0]) || !is_numeric($value[1])) {
61-
throw new Exception('Point coordinates must be numeric values');
29+
$this->message = 'Point coordinates must be numeric values';
30+
return false;
6231
}
6332

6433
return true;
6534
}
6635

6736
/**
6837
* Validate LINESTRING data
69-
*
70-
* @param array<mixed,mixed> $value
71-
* @return bool
72-
* @throws Exception
7338
*/
74-
protected static function validateLineString(array $value): bool
39+
protected function validateLineString(array $value): bool
7540
{
7641
if (count($value) < 2) {
77-
throw new Exception('LineString must contain at least one point');
42+
$this->message = 'LineString must contain at least two points';
43+
return false;
7844
}
7945

8046
foreach ($value as $point) {
8147
if (!is_array($point) || count($point) !== 2) {
82-
throw new Exception('Each point in LineString must be an array of two values [x, y]');
48+
$this->message = 'Each point in LineString must be an array of two values [x, y]';
49+
return false;
8350
}
8451

8552
if (!is_numeric($point[0]) || !is_numeric($point[1])) {
86-
throw new Exception('Each point in LineString must have numeric coordinates');
53+
$this->message = 'Each point in LineString must have numeric coordinates';
54+
return false;
8755
}
8856
}
8957

@@ -92,36 +60,39 @@ protected static function validateLineString(array $value): bool
9260

9361
/**
9462
* Validate POLYGON data
95-
*
96-
* @param array<mixed,mixed> $value
97-
* @return bool
98-
* @throws Exception
9963
*/
100-
protected static function validatePolygon(array $value): bool
64+
protected function validatePolygon(array $value): bool
10165
{
10266
if (empty($value)) {
103-
throw new Exception('Polygon must contain at least one ring');
67+
$this->message = 'Polygon must contain at least one ring';
68+
return false;
10469
}
10570

10671
// Detect single-ring polygon: [[x, y], [x, y], ...]
10772
$isSingleRing = isset($value[0]) && is_array($value[0]) &&
108-
count($value[0]) === 2 && is_numeric($value[0][0]) && is_numeric($value[0][1]);
73+
count($value[0]) === 2 &&
74+
is_numeric($value[0][0]) &&
75+
is_numeric($value[0][1]);
10976

11077
if ($isSingleRing) {
111-
$value = [$value]; // Wrap single ring into multi-ring format
78+
$value = [$value]; // wrap single ring
11279
}
11380

11481
foreach ($value as $ring) {
11582
if (!is_array($ring) || empty($ring)) {
116-
throw new Exception('Each ring in Polygon must be an array of points');
83+
$this->message = 'Each ring in Polygon must be an array of points';
84+
return false;
11785
}
11886

11987
foreach ($ring as $point) {
12088
if (!is_array($point) || count($point) !== 2) {
121-
throw new Exception('Each point in Polygon ring must be an array of two values [x, y]');
89+
$this->message = 'Each point in Polygon ring must be an array of two values [x, y]';
90+
return false;
12291
}
92+
12393
if (!is_numeric($point[0]) || !is_numeric($point[1])) {
124-
throw new Exception('Each point in Polygon ring must have numeric coordinates');
94+
$this->message = 'Each point in Polygon ring must have numeric coordinates';
95+
return false;
12596
}
12697
}
12798
}
@@ -131,51 +102,30 @@ protected static function validatePolygon(array $value): bool
131102

132103
/**
133104
* Check if a value is valid WKT string
134-
*
135-
* @param string $value
136-
* @return bool
137105
*/
138106
public static function isWKTString(string $value): bool
139107
{
140108
$value = trim($value);
141109
return (bool) preg_match('/^(POINT|LINESTRING|POLYGON)\s*\(/i', $value);
142110
}
143111

144-
/**
145-
* Get validator description
146-
*
147-
* @return string
148-
*/
149112
public function getDescription(): string
150113
{
151-
return 'Value must be a valid ' . $this->spatialType . ' format (array or WKT string)';
114+
return 'Value must be a valid ' . $this->spatialType . ": {$this->message}";
152115
}
153116

154-
/**
155-
* Is array
156-
*
157-
* @return bool
158-
*/
159117
public function isArray(): bool
160118
{
161119
return false;
162120
}
163121

164-
/**
165-
* Get Type
166-
*
167-
* @return string
168-
*/
169122
public function getType(): string
170123
{
171124
return 'spatial';
172125
}
173126

174127
/**
175-
* Is valid
176-
*
177-
* @param mixed $value
178-
* @return bool
128+
* Main validation entrypoint
179129
*/
180130
public function isValid($value): bool
181131
{
@@ -184,20 +134,27 @@ public function isValid($value): bool
184134
}
185135

186136
if (is_string($value)) {
187-
// Check if it's a valid WKT string
188137
return self::isWKTString($value);
189138
}
190139

191140
if (is_array($value)) {
192-
// Validate the array format according to the specific spatial type
193-
try {
194-
self::validate($value, $this->spatialType);
195-
return true;
196-
} catch (\Exception $e) {
197-
return false;
141+
switch ($this->spatialType) {
142+
case Database::VAR_POINT:
143+
return $this->validatePoint($value);
144+
145+
case Database::VAR_LINESTRING:
146+
return $this->validateLineString($value);
147+
148+
case Database::VAR_POLYGON:
149+
return $this->validatePolygon($value);
150+
151+
default:
152+
$this->message = 'Unknown spatial type: ' . $this->spatialType;
153+
return false;
198154
}
199155
}
200156

157+
$this->message = 'Spatial value must be array or WKT string';
201158
return false;
202159
}
203160
}

tests/e2e/Adapter/Scopes/SpatialTests.php

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Utopia\Database\Database;
66
use Utopia\Database\Document;
77
use Utopia\Database\Exception;
8+
use Utopia\Database\Exception\Structure as StructureException;
89
use Utopia\Database\Helpers\ID;
910
use Utopia\Database\Helpers\Permission;
1011
use Utopia\Database\Helpers\Role;
@@ -1552,6 +1553,19 @@ public function testSpatialBulkOperation(): void
15521553
$updateResults[] = $doc;
15531554
});
15541555

1556+
// should fail due to invalid structure
1557+
try {
1558+
$database->updateDocuments($collectionName, new Document([
1559+
'name' => 'Updated Location',
1560+
'location' => [15.0, 25.0],
1561+
'area' => [15.0, 25.0] // invalid polygon
1562+
]));
1563+
$this->fail("fail to throw structure exception for the invalid spatial type");
1564+
} catch (\Throwable $th) {
1565+
$this->assertInstanceOf(StructureException::class, $th);
1566+
1567+
}
1568+
15551569
$this->assertGreaterThan(0, $updateCount);
15561570

15571571
// Verify updated documents
@@ -1969,4 +1983,89 @@ public function testSpatialAttributeDefaults(): void
19691983
$database->deleteCollection($collectionName);
19701984
}
19711985
}
1986+
1987+
public function testInvalidSpatialTypes(): void
1988+
{
1989+
/** @var Database $database */
1990+
$database = static::getDatabase();
1991+
if (!$database->getAdapter()->getSupportForSpatialAttributes()) {
1992+
$this->markTestSkipped('Adapter does not support spatial attributes');
1993+
}
1994+
1995+
$collectionName = 'test_invalid_spatial_types';
1996+
1997+
$attributes = [
1998+
new Document([
1999+
'$id' => ID::custom('pointAttr'),
2000+
'type' => Database::VAR_POINT,
2001+
'size' => 0,
2002+
'required' => false,
2003+
'signed' => true,
2004+
'array' => false,
2005+
'filters' => [],
2006+
]),
2007+
new Document([
2008+
'$id' => ID::custom('lineAttr'),
2009+
'type' => Database::VAR_LINESTRING,
2010+
'size' => 0,
2011+
'required' => false,
2012+
'signed' => true,
2013+
'array' => false,
2014+
'filters' => [],
2015+
]),
2016+
new Document([
2017+
'$id' => ID::custom('polyAttr'),
2018+
'type' => Database::VAR_POLYGON,
2019+
'size' => 0,
2020+
'required' => false,
2021+
'signed' => true,
2022+
'array' => false,
2023+
'filters' => [],
2024+
])
2025+
];
2026+
2027+
$database->createCollection($collectionName, $attributes);
2028+
2029+
// ❌ Invalid Point (must be [x, y])
2030+
try {
2031+
$database->createDocument($collectionName, new Document([
2032+
'pointAttr' => [10.0], // only 1 coordinate
2033+
]));
2034+
$this->fail("Expected StructureException for invalid point");
2035+
} catch (\Throwable $th) {
2036+
$this->assertInstanceOf(StructureException::class, $th);
2037+
}
2038+
2039+
// ❌ Invalid LineString (must be [[x,y],[x,y],...], at least 2 points)
2040+
try {
2041+
$database->createDocument($collectionName, new Document([
2042+
'lineAttr' => [[10.0, 20.0]], // only one point
2043+
]));
2044+
$this->fail("Expected StructureException for invalid line");
2045+
} catch (\Throwable $th) {
2046+
$this->assertInstanceOf(StructureException::class, $th);
2047+
}
2048+
2049+
try {
2050+
$database->createDocument($collectionName, new Document([
2051+
'lineAttr' => [10.0, 20.0], // not an array of arrays
2052+
]));
2053+
$this->fail("Expected StructureException for invalid line structure");
2054+
} catch (\Throwable $th) {
2055+
$this->assertInstanceOf(StructureException::class, $th);
2056+
}
2057+
2058+
try {
2059+
$database->createDocument($collectionName, new Document([
2060+
'polyAttr' => [10.0, 20.0] // not an array of arrays
2061+
]));
2062+
$this->fail("Expected StructureException for invalid polygon structure");
2063+
} catch (\Throwable $th) {
2064+
$this->assertInstanceOf(StructureException::class, $th);
2065+
}
2066+
2067+
// Cleanup
2068+
$database->deleteCollection($collectionName);
2069+
}
2070+
19722071
}

0 commit comments

Comments
 (0)