-
Notifications
You must be signed in to change notification settings - Fork 55
Mongo object support and relevant queries #777
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
94c7012
* added object attribute support in mongodb
ArnabChatterjee20k 3b5360b
added elemMatch
ArnabChatterjee20k 7a26698
refactor: improve elemMatch handling and clean up code style
ArnabChatterjee20k c2e1670
refactor: streamline elemMatch validation and update schemaless tests
ArnabChatterjee20k e4f00f3
refactor: update ObjectAttributeTests to check for attribute support …
ArnabChatterjee20k b397af6
feat: add support for object (JSON) indexes across database adapters
ArnabChatterjee20k 3f85b56
updated tests for elemMatch
ArnabChatterjee20k c579ec5
refactor: enhance object query validation and add corresponding tests
ArnabChatterjee20k 3c04a4b
linting
ArnabChatterjee20k 1214e56
refactor: improve validation logic for object query values to handle …
ArnabChatterjee20k 9534938
Merge remote-tracking branch 'upstream/3.x' into mongo-object
ArnabChatterjee20k d511a60
refactor: enhance validation for object attribute query values to dis…
ArnabChatterjee20k 473f3c4
Update src/Database/Adapter/Mongo.php
ArnabChatterjee20k 6d6b975
Merge remote-tracking branch 'upstream/3.x' into mongo-object
ArnabChatterjee20k d52f10e
refactor: rename getSupportForIndexObject to getSupportForObjectIndex…
ArnabChatterjee20k 93ea96f
linting
ArnabChatterjee20k 6cfac6f
linting
ArnabChatterjee20k 838068b
updated index validator
ArnabChatterjee20k 2aadc20
updated database index validator
ArnabChatterjee20k 1556e6e
test: add schemaless nested object attribute queries
ArnabChatterjee20k 887775d
fix: adjust condition for schemaless object attribute support in tests
ArnabChatterjee20k 6de1a2d
test: enhance schemaless nested object attribute queries with additio…
ArnabChatterjee20k 85be9a8
updated tests
ArnabChatterjee20k File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |
| use Exception; | ||
| use MongoDB\BSON\Regex; | ||
| use MongoDB\BSON\UTCDateTime; | ||
| use stdClass; | ||
| use Utopia\Database\Adapter; | ||
| use Utopia\Database\Change; | ||
| use Utopia\Database\Database; | ||
|
|
@@ -43,6 +44,8 @@ class Mongo extends Adapter | |
| '$not', | ||
| '$nor', | ||
| '$exists', | ||
| '$elemMatch', | ||
| '$exists' | ||
| ]; | ||
|
|
||
| protected Client $client; | ||
|
|
@@ -415,7 +418,6 @@ public function createCollection(string $name, array $attributes = [], array $in | |
| try { | ||
| $options = $this->getTransactionOptions(); | ||
| $this->getClient()->createCollection($id, $options); | ||
|
|
||
| } catch (MongoException $e) { | ||
| $e = $this->processException($e); | ||
| if ($e instanceof DuplicateException) { | ||
|
|
@@ -1232,7 +1234,7 @@ public function castingAfter(Document $collection, Document $document): Document | |
| case Database::VAR_INTEGER: | ||
| $node = (int)$node; | ||
| break; | ||
| case Database::VAR_DATETIME : | ||
| case Database::VAR_DATETIME: | ||
| if ($node instanceof UTCDateTime) { | ||
| // Handle UTCDateTime objects | ||
| $node = DateTime::format($node->toDateTime()); | ||
|
|
@@ -1258,6 +1260,12 @@ public function castingAfter(Document $collection, Document $document): Document | |
| } | ||
| } | ||
| break; | ||
| case Database::VAR_OBJECT: | ||
| // Convert stdClass objects to arrays for object attributes | ||
| if (is_object($node) && get_class($node) === stdClass::class) { | ||
| $node = $this->convertStdClassToArray($node); | ||
| } | ||
| break; | ||
| default: | ||
| break; | ||
| } | ||
|
|
@@ -1266,9 +1274,33 @@ public function castingAfter(Document $collection, Document $document): Document | |
| $document->setAttribute($key, ($array) ? $value : $value[0]); | ||
| } | ||
|
|
||
| if (!$this->getSupportForAttributes()) { | ||
| foreach ($document->getArrayCopy() as $key => $value) { | ||
| // mongodb results out a stdclass for objects | ||
| if (is_object($value) && get_class($value) === stdClass::class) { | ||
| $document->setAttribute($key, $this->convertStdClassToArray($value)); | ||
| } | ||
| } | ||
| } | ||
| return $document; | ||
| } | ||
|
|
||
| private function convertStdClassToArray(mixed $value): mixed | ||
| { | ||
| if (is_object($value) && get_class($value) === stdClass::class) { | ||
| return array_map(fn ($v) => $this->convertStdClassToArray($v), get_object_vars($value)); | ||
|
ArnabChatterjee20k marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| if (is_array($value)) { | ||
| return array_map( | ||
| fn ($v) => $this->convertStdClassToArray($v), | ||
| $value | ||
| ); | ||
| } | ||
|
|
||
| return $value; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the document after casting to | ||
| * @param Document $collection | ||
|
|
@@ -1319,6 +1351,10 @@ public function castingBefore(Document $collection, Document $document): Documen | |
| $node = new UTCDateTime(new \DateTime($node)); | ||
| } | ||
| break; | ||
| case Database::VAR_OBJECT: | ||
| $node = json_decode($node); | ||
| $node = $this->convertStdClassToArray($node); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it redundant? Since we already have an array from json_decode |
||
| break; | ||
| default: | ||
| break; | ||
| } | ||
|
|
@@ -1592,7 +1628,6 @@ public function upsertDocuments(Document $collection, string $attribute, array $ | |
| $operations, | ||
| options: $options | ||
| ); | ||
|
|
||
| } catch (MongoException $e) { | ||
| throw $this->processException($e); | ||
| } | ||
|
|
@@ -1977,7 +2012,7 @@ public function find(Document $collection, array $queries = [], ?int $limit = 25 | |
| // Process first batch | ||
| foreach ($results as $result) { | ||
| $record = $this->replaceChars('_', '$', (array)$result); | ||
| $found[] = new Document($record); | ||
| $found[] = new Document($this->convertStdClassToArray($record)); | ||
| } | ||
|
|
||
| // Get cursor ID for subsequent batches | ||
|
|
@@ -1999,7 +2034,6 @@ public function find(Document $collection, array $queries = [], ?int $limit = 25 | |
|
|
||
| $cursorId = (int)($moreResponse->cursor->id ?? 0); | ||
| } | ||
|
|
||
| } catch (MongoException $e) { | ||
| throw $this->processException($e); | ||
| } finally { | ||
|
|
@@ -2335,6 +2369,15 @@ protected function buildFilters(array $queries, string $separator = '$and'): arr | |
| foreach ($queries as $query) { | ||
| /* @var $query Query */ | ||
| if ($query->isNested()) { | ||
| if ($query->getMethod() === Query::TYPE_ELEM_MATCH) { | ||
| $filters[$separator][] = [ | ||
| $query->getAttribute() => [ | ||
| '$elemMatch' => $this->buildFilters($query->getValues(), $separator) | ||
| ] | ||
| ]; | ||
| continue; | ||
| } | ||
|
|
||
| $operator = $this->getQueryOperator($query->getMethod()); | ||
|
|
||
| $filters[$separator][] = $this->buildFilters($query->getValues(), $operator); | ||
|
|
@@ -2385,6 +2428,10 @@ protected function buildFilter(Query $query): array | |
| }; | ||
|
|
||
| $filter = []; | ||
| if ($query->isObjectAttribute() && in_array($query->getMethod(), [Query::TYPE_EQUAL, Query::TYPE_CONTAINS, Query::TYPE_NOT_CONTAINS, Query::TYPE_NOT_EQUAL])) { | ||
| $this->handleObjectFilters($query, $filter); | ||
| return $filter; | ||
| } | ||
|
|
||
| if ($operator == '$eq' && \is_array($value)) { | ||
| $filter[$attribute]['$in'] = $value; | ||
|
|
@@ -2448,6 +2495,84 @@ protected function buildFilter(Query $query): array | |
| return $filter; | ||
| } | ||
|
|
||
| /** | ||
| * @param Query $query | ||
| * @param array<string, mixed> $filter | ||
| * @return void | ||
| */ | ||
| private function handleObjectFilters(Query $query, array &$filter): void | ||
| { | ||
| $conditions = []; | ||
| $isNot = in_array($query->getMethod(), [Query::TYPE_NOT_CONTAINS,Query::TYPE_NOT_EQUAL]); | ||
| $values = $query->getValues(); | ||
| foreach ($values as $attribute => $value) { | ||
| $flattendQuery = $this->flattenWithDotNotation(is_string($attribute) ? $attribute : '', $value); | ||
| $flattenedObjectKey = array_key_first($flattendQuery); | ||
| $queryValue = $flattendQuery[$flattenedObjectKey]; | ||
| $flattenedObjectKey = $query->getAttribute() . '.' . array_key_first($flattendQuery); | ||
| switch ($query->getMethod()) { | ||
|
|
||
| case Query::TYPE_CONTAINS: | ||
| case Query::TYPE_NOT_CONTAINS: { | ||
| $arrayValue = \is_array($queryValue) ? $queryValue : [$queryValue]; | ||
| $operator = $isNot ? '$nin' : '$in'; | ||
| $conditions[] = [ $flattenedObjectKey => [ $operator => $arrayValue] ]; | ||
| break; | ||
| } | ||
|
|
||
| case Query::TYPE_EQUAL: | ||
| case Query::TYPE_NOT_EQUAL: { | ||
| if (\is_array($queryValue)) { | ||
| $operator = $isNot ? '$nin' : '$in'; | ||
| $conditions[] = [ $flattenedObjectKey => [ $operator => $queryValue] ]; | ||
| } else { | ||
| $operator = $isNot ? '$ne' : '$eq'; | ||
| $conditions[] = [ $flattenedObjectKey => [ $operator => $queryValue] ]; | ||
| } | ||
|
|
||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| $logicalOperator = $isNot ? '$and' : '$or'; | ||
| if (count($conditions) && isset($filter[$logicalOperator])) { | ||
| $filter[$logicalOperator] = array_merge($filter[$logicalOperator], $conditions); | ||
| } else { | ||
| $filter[$logicalOperator] = $conditions; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Flatten a nested associative array into Mongo-style dot notation. | ||
| * | ||
| * @param string $key | ||
| * @param mixed $value | ||
| * @param string $prefix | ||
| * @return array<string, mixed> | ||
| */ | ||
| private function flattenWithDotNotation(string $key, mixed $value, string $prefix = ''): array | ||
| { | ||
| /** @var array<string, mixed> $result */ | ||
| $result = []; | ||
| $currentPref = $prefix === '' ? $key : $prefix . '.' . $key; | ||
|
|
||
| if (\is_array($value) && !\array_is_list($value)) { | ||
| $nextKey = \array_key_first($value); | ||
| if ($nextKey === null) { | ||
| return $result; | ||
| } | ||
|
|
||
| $nextKeyString = (string) $nextKey; | ||
| $result += $this->flattenWithDotNotation($nextKeyString, $value[$nextKey], $currentPref); | ||
| } else { | ||
| // at the leaf node | ||
| $result[$currentPref] = $value; | ||
| } | ||
|
|
||
| return $result; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's use a loop instead of recursion here |
||
| } | ||
|
|
||
| /** | ||
| * Get Query Operator | ||
| * | ||
|
|
@@ -2481,6 +2606,7 @@ protected function getQueryOperator(string $operator): string | |
| Query::TYPE_AND => '$and', | ||
| Query::TYPE_EXISTS, | ||
| Query::TYPE_NOT_EXISTS => '$exists', | ||
| Query::TYPE_ELEM_MATCH => '$elemMatch', | ||
| 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), | ||
| }; | ||
| } | ||
|
|
@@ -2800,6 +2926,16 @@ public function getSupportForBatchCreateAttributes(): bool | |
| } | ||
|
|
||
| public function getSupportForObject(): bool | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Are object (JSON) indexes supported? | ||
| * | ||
| * @return bool | ||
| */ | ||
| public function getSupportForIndexObject(): bool | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For consistency, I think it should be
getSupportForObjectIndexes, double check the other methods