Skip to content

Commit 43505a1

Browse files
authored
Merge pull request #777 from utopia-php/mongo-object
2 parents 6a98a5d + 85be9a8 commit 43505a1

File tree

14 files changed

+907
-39
lines changed

14 files changed

+907
-39
lines changed

src/Database/Adapter.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,6 +1079,13 @@ abstract public function getSupportForSpatialAttributes(): bool;
10791079
*/
10801080
abstract public function getSupportForObject(): bool;
10811081

1082+
/**
1083+
* Are object (JSON) indexes supported?
1084+
*
1085+
* @return bool
1086+
*/
1087+
abstract public function getSupportForObjectIndexes(): bool;
1088+
10821089
/**
10831090
* Does the adapter support null values in spatial indexes?
10841091
*

src/Database/Adapter/MariaDB.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2140,6 +2140,16 @@ public function getSupportForObject(): bool
21402140
return false;
21412141
}
21422142

2143+
/**
2144+
* Are object (JSON) indexes supported?
2145+
*
2146+
* @return bool
2147+
*/
2148+
public function getSupportForObjectIndexes(): bool
2149+
{
2150+
return false;
2151+
}
2152+
21432153
/**
21442154
* Get Support for Null Values in Spatial Indexes
21452155
*

src/Database/Adapter/Mongo.php

Lines changed: 144 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Exception;
66
use MongoDB\BSON\Regex;
77
use MongoDB\BSON\UTCDateTime;
8+
use stdClass;
89
use Utopia\Database\Adapter;
910
use Utopia\Database\Change;
1011
use Utopia\Database\Database;
@@ -43,6 +44,8 @@ class Mongo extends Adapter
4344
'$not',
4445
'$nor',
4546
'$exists',
47+
'$elemMatch',
48+
'$exists'
4649
];
4750

4851
protected Client $client;
@@ -415,7 +418,6 @@ public function createCollection(string $name, array $attributes = [], array $in
415418
try {
416419
$options = $this->getTransactionOptions();
417420
$this->getClient()->createCollection($id, $options);
418-
419421
} catch (MongoException $e) {
420422
$e = $this->processException($e);
421423
if ($e instanceof DuplicateException) {
@@ -1232,7 +1234,7 @@ public function castingAfter(Document $collection, Document $document): Document
12321234
case Database::VAR_INTEGER:
12331235
$node = (int)$node;
12341236
break;
1235-
case Database::VAR_DATETIME :
1237+
case Database::VAR_DATETIME:
12361238
if ($node instanceof UTCDateTime) {
12371239
// Handle UTCDateTime objects
12381240
$node = DateTime::format($node->toDateTime());
@@ -1258,6 +1260,12 @@ public function castingAfter(Document $collection, Document $document): Document
12581260
}
12591261
}
12601262
break;
1263+
case Database::VAR_OBJECT:
1264+
// Convert stdClass objects to arrays for object attributes
1265+
if (is_object($node) && get_class($node) === stdClass::class) {
1266+
$node = $this->convertStdClassToArray($node);
1267+
}
1268+
break;
12611269
default:
12621270
break;
12631271
}
@@ -1266,9 +1274,33 @@ public function castingAfter(Document $collection, Document $document): Document
12661274
$document->setAttribute($key, ($array) ? $value : $value[0]);
12671275
}
12681276

1277+
if (!$this->getSupportForAttributes()) {
1278+
foreach ($document->getArrayCopy() as $key => $value) {
1279+
// mongodb results out a stdclass for objects
1280+
if (is_object($value) && get_class($value) === stdClass::class) {
1281+
$document->setAttribute($key, $this->convertStdClassToArray($value));
1282+
}
1283+
}
1284+
}
12691285
return $document;
12701286
}
12711287

1288+
private function convertStdClassToArray(mixed $value): mixed
1289+
{
1290+
if (is_object($value) && get_class($value) === stdClass::class) {
1291+
return array_map($this->convertStdClassToArray(...), get_object_vars($value));
1292+
}
1293+
1294+
if (is_array($value)) {
1295+
return array_map(
1296+
fn ($v) => $this->convertStdClassToArray($v),
1297+
$value
1298+
);
1299+
}
1300+
1301+
return $value;
1302+
}
1303+
12721304
/**
12731305
* Returns the document after casting to
12741306
* @param Document $collection
@@ -1319,6 +1351,9 @@ public function castingBefore(Document $collection, Document $document): Documen
13191351
$node = new UTCDateTime(new \DateTime($node));
13201352
}
13211353
break;
1354+
case Database::VAR_OBJECT:
1355+
$node = json_decode($node);
1356+
break;
13221357
default:
13231358
break;
13241359
}
@@ -1592,7 +1627,6 @@ public function upsertDocuments(Document $collection, string $attribute, array $
15921627
$operations,
15931628
options: $options
15941629
);
1595-
15961630
} catch (MongoException $e) {
15971631
throw $this->processException($e);
15981632
}
@@ -1977,7 +2011,7 @@ public function find(Document $collection, array $queries = [], ?int $limit = 25
19772011
// Process first batch
19782012
foreach ($results as $result) {
19792013
$record = $this->replaceChars('_', '$', (array)$result);
1980-
$found[] = new Document($record);
2014+
$found[] = new Document($this->convertStdClassToArray($record));
19812015
}
19822016

19832017
// Get cursor ID for subsequent batches
@@ -1999,7 +2033,6 @@ public function find(Document $collection, array $queries = [], ?int $limit = 25
19992033

20002034
$cursorId = (int)($moreResponse->cursor->id ?? 0);
20012035
}
2002-
20032036
} catch (MongoException $e) {
20042037
throw $this->processException($e);
20052038
} finally {
@@ -2335,6 +2368,15 @@ protected function buildFilters(array $queries, string $separator = '$and'): arr
23352368
foreach ($queries as $query) {
23362369
/* @var $query Query */
23372370
if ($query->isNested()) {
2371+
if ($query->getMethod() === Query::TYPE_ELEM_MATCH) {
2372+
$filters[$separator][] = [
2373+
$query->getAttribute() => [
2374+
'$elemMatch' => $this->buildFilters($query->getValues(), $separator)
2375+
]
2376+
];
2377+
continue;
2378+
}
2379+
23382380
$operator = $this->getQueryOperator($query->getMethod());
23392381

23402382
$filters[$separator][] = $this->buildFilters($query->getValues(), $operator);
@@ -2385,6 +2427,10 @@ protected function buildFilter(Query $query): array
23852427
};
23862428

23872429
$filter = [];
2430+
if ($query->isObjectAttribute() && in_array($query->getMethod(), [Query::TYPE_EQUAL, Query::TYPE_CONTAINS, Query::TYPE_NOT_CONTAINS, Query::TYPE_NOT_EQUAL])) {
2431+
$this->handleObjectFilters($query, $filter);
2432+
return $filter;
2433+
}
23882434

23892435
if ($operator == '$eq' && \is_array($value)) {
23902436
$filter[$attribute]['$in'] = $value;
@@ -2448,6 +2494,88 @@ protected function buildFilter(Query $query): array
24482494
return $filter;
24492495
}
24502496

2497+
/**
2498+
* @param Query $query
2499+
* @param array<string, mixed> $filter
2500+
* @return void
2501+
*/
2502+
private function handleObjectFilters(Query $query, array &$filter): void
2503+
{
2504+
$conditions = [];
2505+
$isNot = in_array($query->getMethod(), [Query::TYPE_NOT_CONTAINS,Query::TYPE_NOT_EQUAL]);
2506+
$values = $query->getValues();
2507+
foreach ($values as $attribute => $value) {
2508+
$flattendQuery = $this->flattenWithDotNotation(is_string($attribute) ? $attribute : '', $value);
2509+
$flattenedObjectKey = array_key_first($flattendQuery);
2510+
$queryValue = $flattendQuery[$flattenedObjectKey];
2511+
$flattenedObjectKey = $query->getAttribute() . '.' . array_key_first($flattendQuery);
2512+
switch ($query->getMethod()) {
2513+
2514+
case Query::TYPE_CONTAINS:
2515+
case Query::TYPE_NOT_CONTAINS: {
2516+
$arrayValue = \is_array($queryValue) ? $queryValue : [$queryValue];
2517+
$operator = $isNot ? '$nin' : '$in';
2518+
$conditions[] = [ $flattenedObjectKey => [ $operator => $arrayValue] ];
2519+
break;
2520+
}
2521+
2522+
case Query::TYPE_EQUAL:
2523+
case Query::TYPE_NOT_EQUAL: {
2524+
if (\is_array($queryValue)) {
2525+
$operator = $isNot ? '$nin' : '$in';
2526+
$conditions[] = [ $flattenedObjectKey => [ $operator => $queryValue] ];
2527+
} else {
2528+
$operator = $isNot ? '$ne' : '$eq';
2529+
$conditions[] = [ $flattenedObjectKey => [ $operator => $queryValue] ];
2530+
}
2531+
2532+
break;
2533+
}
2534+
}
2535+
}
2536+
2537+
$logicalOperator = $isNot ? '$and' : '$or';
2538+
if (count($conditions) && isset($filter[$logicalOperator])) {
2539+
$filter[$logicalOperator] = array_merge($filter[$logicalOperator], $conditions);
2540+
} else {
2541+
$filter[$logicalOperator] = $conditions;
2542+
}
2543+
}
2544+
2545+
/**
2546+
* Flatten a nested associative array into Mongo-style dot notation.
2547+
*
2548+
* @param string $key
2549+
* @param mixed $value
2550+
* @param string $prefix
2551+
* @return array<string, mixed>
2552+
*/
2553+
private function flattenWithDotNotation(string $key, mixed $value, string $prefix = ''): array
2554+
{
2555+
/** @var array<string, mixed> $result */
2556+
$result = [];
2557+
2558+
$stack = [];
2559+
2560+
$initialKey = $prefix === '' ? $key : $prefix . '.' . $key;
2561+
$stack[] = [$initialKey, $value];
2562+
while (!empty($stack)) {
2563+
[$currentPath, $currentValue] = array_pop($stack);
2564+
if (is_array($currentValue) && !array_is_list($currentValue)) {
2565+
foreach ($currentValue as $nextKey => $nextValue) {
2566+
$nextKey = (string)$nextKey;
2567+
$nextPath = $currentPath === '' ? $nextKey : $currentPath . '.' . $nextKey;
2568+
$stack[] = [$nextPath, $nextValue];
2569+
}
2570+
} else {
2571+
// leaf node
2572+
$result[$currentPath] = $currentValue;
2573+
}
2574+
}
2575+
2576+
return $result;
2577+
}
2578+
24512579
/**
24522580
* Get Query Operator
24532581
*
@@ -2482,6 +2610,7 @@ protected function getQueryOperator(string $operator): string
24822610
Query::TYPE_AND => '$and',
24832611
Query::TYPE_EXISTS,
24842612
Query::TYPE_NOT_EXISTS => '$exists',
2613+
Query::TYPE_ELEM_MATCH => '$elemMatch',
24852614
default => throw new DatabaseException('Unknown operator:' . $operator . '. Must be one of ' . Query::TYPE_EQUAL . ', ' . Query::TYPE_NOT_EQUAL . ', ' . Query::TYPE_LESSER . ', ' . Query::TYPE_LESSER_EQUAL . ', ' . Query::TYPE_GREATER . ', ' . Query::TYPE_GREATER_EQUAL . ', ' . Query::TYPE_IS_NULL . ', ' . Query::TYPE_IS_NOT_NULL . ', ' . Query::TYPE_BETWEEN . ', ' . Query::TYPE_NOT_BETWEEN . ', ' . Query::TYPE_STARTS_WITH . ', ' . Query::TYPE_NOT_STARTS_WITH . ', ' . Query::TYPE_ENDS_WITH . ', ' . Query::TYPE_NOT_ENDS_WITH . ', ' . Query::TYPE_CONTAINS . ', ' . Query::TYPE_NOT_CONTAINS . ', ' . Query::TYPE_SEARCH . ', ' . Query::TYPE_NOT_SEARCH . ', ' . Query::TYPE_SELECT),
24862615
};
24872616
}
@@ -2821,6 +2950,16 @@ public function getSupportForBatchCreateAttributes(): bool
28212950
}
28222951

28232952
public function getSupportForObject(): bool
2953+
{
2954+
return true;
2955+
}
2956+
2957+
/**
2958+
* Are object (JSON) indexes supported?
2959+
*
2960+
* @return bool
2961+
*/
2962+
public function getSupportForObjectIndexes(): bool
28242963
{
28252964
return false;
28262965
}

src/Database/Adapter/MySQL.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,11 @@ public function getSupportForSpatialAxisOrder(): bool
253253
return true;
254254
}
255255

256+
public function getSupportForObjectIndexes(): bool
257+
{
258+
return false;
259+
}
260+
256261
/**
257262
* Get the spatial axis order specification string for MySQL
258263
* MySQL with SRID 4326 expects lat-long by default, but our data is in long-lat format

src/Database/Adapter/Pool.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,11 @@ public function getSupportForObject(): bool
605605
return $this->delegate(__FUNCTION__, \func_get_args());
606606
}
607607

608+
public function getSupportForObjectIndexes(): bool
609+
{
610+
return $this->delegate(__FUNCTION__, \func_get_args());
611+
}
612+
608613
public function castingBefore(Document $collection, Document $document): Document
609614
{
610615
return $this->delegate(__FUNCTION__, \func_get_args());

src/Database/Adapter/Postgres.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2224,6 +2224,16 @@ public function getSupportForObject(): bool
22242224
return true;
22252225
}
22262226

2227+
/**
2228+
* Are object (JSONB) indexes supported?
2229+
*
2230+
* @return bool
2231+
*/
2232+
public function getSupportForObjectIndexes(): bool
2233+
{
2234+
return true;
2235+
}
2236+
22272237
/**
22282238
* Does the adapter support null values in spatial indexes?
22292239
*

src/Database/Adapter/SQLite.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,16 @@ public function getSupportForObject(): bool
10131013
return false;
10141014
}
10151015

1016+
/**
1017+
* Are object (JSON) indexes supported?
1018+
*
1019+
* @return bool
1020+
*/
1021+
public function getSupportForObjectIndexes(): bool
1022+
{
1023+
return false;
1024+
}
1025+
10161026
public function getSupportForSpatialIndexNull(): bool
10171027
{
10181028
return false; // SQLite doesn't have native spatial support

0 commit comments

Comments
 (0)