Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
67c583a
added regex query support for the mongodb(schema + schemaless)
ArnabChatterjee20k Dec 31, 2025
dbb3c79
added support for regex in mysql/mariadb
ArnabChatterjee20k Dec 31, 2025
2995d34
add regex support methods for database adapters
ArnabChatterjee20k Dec 31, 2025
4c6dbbb
add support for trigram indexes in Postgres adapter and validation
ArnabChatterjee20k Dec 31, 2025
2e13e92
added cleanup of collection for the testFindRegex
ArnabChatterjee20k Dec 31, 2025
cc90ed1
changed the collection name from movies to moviesRegex to remove clas…
ArnabChatterjee20k Dec 31, 2025
e4b7c45
added trigram index in the index vaidator in the databases
ArnabChatterjee20k Dec 31, 2025
e18512d
updated the size of the attr in testTrigramIndexValidation
ArnabChatterjee20k Dec 31, 2025
d50f1f2
fixed typo
ArnabChatterjee20k Dec 31, 2025
e0e2b9d
fixed mariadb support methods
ArnabChatterjee20k Dec 31, 2025
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
57507d6
Add support for regex and enhance index validation in database adapter
ArnabChatterjee20k Jan 7, 2026
ccb69f0
Merge remote-tracking branch 'upstream/3.x' into query-regex
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
8bd5e0e
Refactor index validation and enhance support checks in Database and …
ArnabChatterjee20k Jan 7, 2026
7da7023
Enhance index validation tests by adding support checks for spatial a…
ArnabChatterjee20k Jan 7, 2026
d511a60
refactor: enhance validation for object attribute query values to dis…
ArnabChatterjee20k Jan 7, 2026
82f2bac
removed redundant size from the attribute test
ArnabChatterjee20k Jan 8, 2026
6a98a5d
Merge pull request #775 from utopia-php/query-regex
abnegate Jan 8, 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
43505a1
Merge pull request #777 from utopia-php/mongo-object
abnegate Jan 9, 2026
655c072
Handle bits/sign
abnegate Jan 14, 2026
77b234a
Update lock
abnegate Jan 14, 2026
b06347f
Fix assertions
abnegate Jan 14, 2026
093370b
Fix attribute tests
abnegate Jan 14, 2026
e491ed2
Merge pull request #781 from utopia-php/fix-validator-bits
abnegate Jan 14, 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
320 changes: 183 additions & 137 deletions composer.lock

Large diffs are not rendered by default.

41 changes: 41 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 Expand Up @@ -1442,4 +1449,38 @@ public function enableAlterLocks(bool $enable): self

return $this;
}

/**
* Does the adapter support trigram index?
*
* @return bool
*/
abstract public function getSupportForTrigramIndex(): bool;

/**
* Is PCRE regex supported?
* PCRE (Perl Compatible Regular Expressions) supports \b for word boundaries
*
* @return bool
*/
abstract public function getSupportForPCRERegex(): bool;

/**
* Is POSIX regex supported?
* POSIX regex uses \y for word boundaries instead of \b
*
* @return bool
*/
abstract public function getSupportForPOSIXRegex(): bool;

/**
* Is regex supported at all?
* Returns true if either PCRE or POSIX regex is supported
*
* @return bool
*/
public function getSupportForRegex(): bool
{
return $this->getSupportForPCRERegex() || $this->getSupportForPOSIXRegex();
}
}
25 changes: 25 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 Expand Up @@ -2230,4 +2240,19 @@ public function getSupportForAlterLocks(): bool
{
return true;
}

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

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

public function getSupportForPOSIXRegex(): bool
{
return false;
}
}
177 changes: 171 additions & 6 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 @@ -2476,11 +2604,13 @@ protected function getQueryOperator(string $operator): string
Query::TYPE_STARTS_WITH,
Query::TYPE_NOT_STARTS_WITH,
Query::TYPE_ENDS_WITH,
Query::TYPE_NOT_ENDS_WITH => '$regex',
Query::TYPE_NOT_ENDS_WITH,
Query::TYPE_REGEX => '$regex',
Query::TYPE_OR => '$or',
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 @@ -2749,6 +2879,26 @@ public function getSupportForGetConnectionId(): bool
return false;
}

/**
* Is PCRE regex supported?
*
* @return bool
*/
public function getSupportForPCRERegex(): bool
{
return true;
}

/**
* Is POSIX regex supported?
*
* @return bool
*/
public function getSupportForPOSIXRegex(): bool
{
return false;
}

/**
* Is cache fallback supported?
*
Expand Down Expand Up @@ -2800,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 Expand Up @@ -3230,4 +3390,9 @@ public function getSupportForAlterLocks(): bool
{
return false;
}

public function getSupportForTrigramIndex(): bool
{
return false;
}
}
Loading