Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
94c7012
* added object attribute support in mongodb
ArnabChatterjee20k Jan 5, 2026
3b5360b
added elemMatch
ArnabChatterjee20k Jan 5, 2026
7a26698
refactor: improve elemMatch handling and clean up code style
ArnabChatterjee20k Jan 6, 2026
c2e1670
refactor: streamline elemMatch validation and update schemaless tests
ArnabChatterjee20k Jan 6, 2026
e4f00f3
refactor: update ObjectAttributeTests to check for attribute support …
ArnabChatterjee20k Jan 6, 2026
b397af6
feat: add support for object (JSON) indexes across database adapters
ArnabChatterjee20k Jan 7, 2026
3f85b56
updated tests for elemMatch
ArnabChatterjee20k Jan 7, 2026
c579ec5
refactor: enhance object query validation and add corresponding tests
ArnabChatterjee20k Jan 7, 2026
3c04a4b
linting
ArnabChatterjee20k Jan 7, 2026
1214e56
refactor: improve validation logic for object query values to handle …
ArnabChatterjee20k Jan 7, 2026
9534938
Merge remote-tracking branch 'upstream/3.x' into mongo-object
ArnabChatterjee20k Jan 7, 2026
d511a60
refactor: enhance validation for object attribute query values to dis…
ArnabChatterjee20k Jan 7, 2026
473f3c4
Update src/Database/Adapter/Mongo.php
ArnabChatterjee20k Jan 8, 2026
6d6b975
Merge remote-tracking branch 'upstream/3.x' into mongo-object
ArnabChatterjee20k Jan 8, 2026
d52f10e
refactor: rename getSupportForIndexObject to getSupportForObjectIndex…
ArnabChatterjee20k Jan 8, 2026
93ea96f
linting
ArnabChatterjee20k Jan 8, 2026
6cfac6f
linting
ArnabChatterjee20k Jan 8, 2026
838068b
updated index validator
ArnabChatterjee20k Jan 8, 2026
2aadc20
updated database index validator
ArnabChatterjee20k Jan 8, 2026
1556e6e
test: add schemaless nested object attribute queries
ArnabChatterjee20k Jan 9, 2026
887775d
fix: adjust condition for schemaless object attribute support in tests
ArnabChatterjee20k Jan 9, 2026
6de1a2d
test: enhance schemaless nested object attribute queries with additio…
ArnabChatterjee20k Jan 9, 2026
85be9a8
updated tests
ArnabChatterjee20k Jan 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/Database/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,13 @@ abstract public function getSupportForSpatialAttributes(): bool;
*/
abstract public function getSupportForObject(): bool;

/**
* Are object (JSON) indexes supported?
*
* @return bool
*/
abstract public function getSupportForObjectIndexes(): bool;

/**
* Does the adapter support null values in spatial indexes?
*
Expand Down
10 changes: 10 additions & 0 deletions src/Database/Adapter/MariaDB.php
Original file line number Diff line number Diff line change
Expand Up @@ -2140,6 +2140,16 @@ public function getSupportForObject(): bool
return false;
}

/**
* Are object (JSON) indexes supported?
*
* @return bool
*/
public function getSupportForObjectIndexes(): bool
{
return false;
}

/**
* Get Support for Null Values in Spatial Indexes
*
Expand Down
149 changes: 144 additions & 5 deletions src/Database/Adapter/Mongo.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -43,6 +44,8 @@ class Mongo extends Adapter
'$not',
'$nor',
'$exists',
'$elemMatch',
'$exists'
];

protected Client $client;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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());
Expand All @@ -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;
}
Expand All @@ -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($this->convertStdClassToArray(...), get_object_vars($value));
}

if (is_array($value)) {
return array_map(
fn ($v) => $this->convertStdClassToArray($v),
$value
);
}

return $value;
}

/**
* Returns the document after casting to
* @param Document $collection
Expand Down Expand Up @@ -1319,6 +1351,9 @@ public function castingBefore(Document $collection, Document $document): Documen
$node = new UTCDateTime(new \DateTime($node));
}
break;
case Database::VAR_OBJECT:
$node = json_decode($node);
break;
default:
break;
}
Expand Down Expand Up @@ -1592,7 +1627,6 @@ public function upsertDocuments(Document $collection, string $attribute, array $
$operations,
options: $options
);

} catch (MongoException $e) {
throw $this->processException($e);
}
Expand Down Expand Up @@ -1977,7 +2011,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
Expand All @@ -1999,7 +2033,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 {
Expand Down Expand Up @@ -2335,6 +2368,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);
Expand Down Expand Up @@ -2385,6 +2427,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;
Expand Down Expand Up @@ -2448,6 +2494,88 @@ 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 = [];

$stack = [];

$initialKey = $prefix === '' ? $key : $prefix . '.' . $key;
$stack[] = [$initialKey, $value];
while (!empty($stack)) {
[$currentPath, $currentValue] = array_pop($stack);
if (is_array($currentValue) && !array_is_list($currentValue)) {
foreach ($currentValue as $nextKey => $nextValue) {
$nextKey = (string)$nextKey;
$nextPath = $currentPath === '' ? $nextKey : $currentPath . '.' . $nextKey;
$stack[] = [$nextPath, $nextValue];
}
} else {
// leaf node
$result[$currentPath] = $currentValue;
}
}

return $result;
}

/**
* Get Query Operator
*
Expand Down Expand Up @@ -2482,6 +2610,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),
};
}
Expand Down Expand Up @@ -2821,6 +2950,16 @@ public function getSupportForBatchCreateAttributes(): bool
}

public function getSupportForObject(): bool
{
return true;
}

/**
* Are object (JSON) indexes supported?
*
* @return bool
*/
public function getSupportForObjectIndexes(): bool
{
return false;
}
Expand Down
5 changes: 5 additions & 0 deletions src/Database/Adapter/MySQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,11 @@ public function getSupportForSpatialAxisOrder(): bool
return true;
}

public function getSupportForObjectIndexes(): bool
{
return false;
}

/**
* Get the spatial axis order specification string for MySQL
* MySQL with SRID 4326 expects lat-long by default, but our data is in long-lat format
Expand Down
5 changes: 5 additions & 0 deletions src/Database/Adapter/Pool.php
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,11 @@ public function getSupportForObject(): bool
return $this->delegate(__FUNCTION__, \func_get_args());
}

public function getSupportForObjectIndexes(): bool
{
return $this->delegate(__FUNCTION__, \func_get_args());
}

public function castingBefore(Document $collection, Document $document): Document
{
return $this->delegate(__FUNCTION__, \func_get_args());
Expand Down
10 changes: 10 additions & 0 deletions src/Database/Adapter/Postgres.php
Original file line number Diff line number Diff line change
Expand Up @@ -2224,6 +2224,16 @@ public function getSupportForObject(): bool
return true;
}

/**
* Are object (JSONB) indexes supported?
*
* @return bool
*/
public function getSupportForObjectIndexes(): bool
{
return true;
}

/**
* Does the adapter support null values in spatial indexes?
*
Expand Down
10 changes: 10 additions & 0 deletions src/Database/Adapter/SQLite.php
Original file line number Diff line number Diff line change
Expand Up @@ -1013,6 +1013,16 @@ public function getSupportForObject(): bool
return false;
}

/**
* Are object (JSON) indexes supported?
*
* @return bool
*/
public function getSupportForObjectIndexes(): bool
{
return false;
}

public function getSupportForSpatialIndexNull(): bool
{
return false; // SQLite doesn't have native spatial support
Expand Down
Loading