Skip to content

Commit 6a98a5d

Browse files
authored
Merge pull request #775 from utopia-php/query-regex
2 parents 60e1d32 + 82f2bac commit 6a98a5d

16 files changed

Lines changed: 1491 additions & 55 deletions

File tree

src/Database/Adapter.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1442,4 +1442,38 @@ public function enableAlterLocks(bool $enable): self
14421442

14431443
return $this;
14441444
}
1445+
1446+
/**
1447+
* Does the adapter support trigram index?
1448+
*
1449+
* @return bool
1450+
*/
1451+
abstract public function getSupportForTrigramIndex(): bool;
1452+
1453+
/**
1454+
* Is PCRE regex supported?
1455+
* PCRE (Perl Compatible Regular Expressions) supports \b for word boundaries
1456+
*
1457+
* @return bool
1458+
*/
1459+
abstract public function getSupportForPCRERegex(): bool;
1460+
1461+
/**
1462+
* Is POSIX regex supported?
1463+
* POSIX regex uses \y for word boundaries instead of \b
1464+
*
1465+
* @return bool
1466+
*/
1467+
abstract public function getSupportForPOSIXRegex(): bool;
1468+
1469+
/**
1470+
* Is regex supported at all?
1471+
* Returns true if either PCRE or POSIX regex is supported
1472+
*
1473+
* @return bool
1474+
*/
1475+
public function getSupportForRegex(): bool
1476+
{
1477+
return $this->getSupportForPCRERegex() || $this->getSupportForPOSIXRegex();
1478+
}
14451479
}

src/Database/Adapter/MariaDB.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2230,4 +2230,19 @@ public function getSupportForAlterLocks(): bool
22302230
{
22312231
return true;
22322232
}
2233+
2234+
public function getSupportForTrigramIndex(): bool
2235+
{
2236+
return false;
2237+
}
2238+
2239+
public function getSupportForPCRERegex(): bool
2240+
{
2241+
return true;
2242+
}
2243+
2244+
public function getSupportForPOSIXRegex(): bool
2245+
{
2246+
return false;
2247+
}
22332248
}

src/Database/Adapter/Mongo.php

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2476,7 +2476,8 @@ protected function getQueryOperator(string $operator): string
24762476
Query::TYPE_STARTS_WITH,
24772477
Query::TYPE_NOT_STARTS_WITH,
24782478
Query::TYPE_ENDS_WITH,
2479-
Query::TYPE_NOT_ENDS_WITH => '$regex',
2479+
Query::TYPE_NOT_ENDS_WITH,
2480+
Query::TYPE_REGEX => '$regex',
24802481
Query::TYPE_OR => '$or',
24812482
Query::TYPE_AND => '$and',
24822483
Query::TYPE_EXISTS,
@@ -2749,6 +2750,26 @@ public function getSupportForGetConnectionId(): bool
27492750
return false;
27502751
}
27512752

2753+
/**
2754+
* Is PCRE regex supported?
2755+
*
2756+
* @return bool
2757+
*/
2758+
public function getSupportForPCRERegex(): bool
2759+
{
2760+
return true;
2761+
}
2762+
2763+
/**
2764+
* Is POSIX regex supported?
2765+
*
2766+
* @return bool
2767+
*/
2768+
public function getSupportForPOSIXRegex(): bool
2769+
{
2770+
return false;
2771+
}
2772+
27522773
/**
27532774
* Is cache fallback supported?
27542775
*
@@ -3230,4 +3251,9 @@ public function getSupportForAlterLocks(): bool
32303251
{
32313252
return false;
32323253
}
3254+
3255+
public function getSupportForTrigramIndex(): bool
3256+
{
3257+
return false;
3258+
}
32333259
}

src/Database/Adapter/MySQL.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ public function setTimeout(int $milliseconds, string $event = Database::EVENT_AL
3131

3232
$this->timeout = $milliseconds;
3333

34+
$pdo = $this->getPDO();
35+
$pdo->exec("SET GLOBAL regexp_time_limit = {$milliseconds}");
36+
3437
$this->before($event, 'timeout', function ($sql) use ($milliseconds) {
3538
return \preg_replace(
3639
pattern: '/SELECT/',
@@ -152,6 +155,11 @@ protected function processException(PDOException $e): \Exception
152155
return new TimeoutException('Query timed out', $e->getCode(), $e);
153156
}
154157

158+
// Regex timeout
159+
if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 3699) {
160+
return new TimeoutException('Query timed out', $e->getCode(), $e);
161+
}
162+
155163
// Functional index dependency
156164
if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 3837) {
157165
return new DependencyException('Attribute cannot be deleted because it is used in an index', $e->getCode(), $e);

src/Database/Adapter/Pool.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,21 @@ public function getSupportForFulltextWildcardIndex(): bool
365365
return $this->delegate(__FUNCTION__, \func_get_args());
366366
}
367367

368+
public function getSupportForPCRERegex(): bool
369+
{
370+
return $this->delegate(__FUNCTION__, \func_get_args());
371+
}
372+
373+
public function getSupportForPOSIXRegex(): bool
374+
{
375+
return $this->delegate(__FUNCTION__, \func_get_args());
376+
}
377+
378+
public function getSupportForTrigramIndex(): bool
379+
{
380+
return $this->delegate(__FUNCTION__, \func_get_args());
381+
}
382+
368383
public function getSupportForCasting(): bool
369384
{
370385
return $this->delegate(__FUNCTION__, \func_get_args());

src/Database/Adapter/Postgres.php

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ public function create(string $name): bool
154154
// Enable extensions
155155
$this->getPDO()->prepare('CREATE EXTENSION IF NOT EXISTS postgis')->execute();
156156
$this->getPDO()->prepare('CREATE EXTENSION IF NOT EXISTS vector')->execute();
157+
$this->getPDO()->prepare('CREATE EXTENSION IF NOT EXISTS pg_trgm')->execute();
157158

158159
$collation = "
159160
CREATE COLLATION IF NOT EXISTS utf8_ci_ai (
@@ -899,9 +900,10 @@ public function createIndex(string $collection, string $id, string $type, array
899900
Database::INDEX_SPATIAL,
900901
Database::INDEX_HNSW_EUCLIDEAN,
901902
Database::INDEX_HNSW_COSINE,
902-
Database::INDEX_HNSW_DOT => 'INDEX',
903+
Database::INDEX_HNSW_DOT,
904+
Database::INDEX_OBJECT,
905+
Database::INDEX_TRIGRAM => 'INDEX',
903906
Database::INDEX_UNIQUE => 'UNIQUE INDEX',
904-
Database::INDEX_OBJECT => 'INDEX',
905907
default => throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_FULLTEXT . ', ' . Database::INDEX_SPATIAL . ', ' . Database::INDEX_OBJECT . ', ' . Database::INDEX_HNSW_EUCLIDEAN . ', ' . Database::INDEX_HNSW_COSINE . ', ' . Database::INDEX_HNSW_DOT),
906908
};
907909

@@ -922,6 +924,11 @@ public function createIndex(string $collection, string $id, string $type, array
922924
Database::INDEX_HNSW_COSINE => " USING HNSW ({$attributes} vector_cosine_ops)",
923925
Database::INDEX_HNSW_DOT => " USING HNSW ({$attributes} vector_ip_ops)",
924926
Database::INDEX_OBJECT => " USING GIN ({$attributes})",
927+
Database::INDEX_TRIGRAM =>
928+
" USING GIN (" . implode(', ', array_map(
929+
fn ($attr) => "$attr gin_trgm_ops",
930+
array_map(fn ($attr) => trim($attr), explode(',', $attributes))
931+
)) . ")",
925932
default => " ({$attributes})",
926933
};
927934

@@ -2112,6 +2119,21 @@ public function getSupportForVectors(): bool
21122119
return true;
21132120
}
21142121

2122+
public function getSupportForPCRERegex(): bool
2123+
{
2124+
return false;
2125+
}
2126+
2127+
public function getSupportForPOSIXRegex(): bool
2128+
{
2129+
return true;
2130+
}
2131+
2132+
public function getSupportForTrigramIndex(): bool
2133+
{
2134+
return true;
2135+
}
2136+
21152137
/**
21162138
* @return string
21172139
*/
@@ -2120,6 +2142,14 @@ public function getLikeOperator(): string
21202142
return 'ILIKE';
21212143
}
21222144

2145+
/**
2146+
* @return string
2147+
*/
2148+
public function getRegexOperator(): string
2149+
{
2150+
return '~';
2151+
}
2152+
21232153
protected function processException(PDOException $e): \Exception
21242154
{
21252155
// Timeout

src/Database/Adapter/SQL.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1794,6 +1794,8 @@ protected function getSQLOperator(string $method): string
17941794
case Query::TYPE_NOT_ENDS_WITH:
17951795
case Query::TYPE_NOT_CONTAINS:
17961796
return $this->getLikeOperator();
1797+
case Query::TYPE_REGEX:
1798+
return $this->getRegexOperator();
17971799
case Query::TYPE_VECTOR_DOT:
17981800
case Query::TYPE_VECTOR_COSINE:
17991801
case Query::TYPE_VECTOR_EUCLIDEAN:
@@ -2287,6 +2289,14 @@ public function getLikeOperator(): string
22872289
return 'LIKE';
22882290
}
22892291

2292+
/**
2293+
* @return string
2294+
*/
2295+
public function getRegexOperator(): string
2296+
{
2297+
return 'REGEXP';
2298+
}
2299+
22902300
public function getInternalIndexesKeys(): array
22912301
{
22922302
return [];

src/Database/Adapter/SQLite.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1876,4 +1876,26 @@ public function getSupportForAlterLocks(): bool
18761876
{
18771877
return false;
18781878
}
1879+
1880+
/**
1881+
* Is PCRE regex supported?
1882+
* SQLite does not have native REGEXP support - it requires compile-time option or user-defined function
1883+
*
1884+
* @return bool
1885+
*/
1886+
public function getSupportForPCRERegex(): bool
1887+
{
1888+
return false;
1889+
}
1890+
1891+
/**
1892+
* Is POSIX regex supported?
1893+
* SQLite does not have native REGEXP support - it requires compile-time option or user-defined function
1894+
*
1895+
* @return bool
1896+
*/
1897+
public function getSupportForPOSIXRegex(): bool
1898+
{
1899+
return false;
1900+
}
18791901
}

src/Database/Database.php

Lines changed: 17 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ class Database
8585
public const INDEX_HNSW_EUCLIDEAN = 'hnsw_euclidean';
8686
public const INDEX_HNSW_COSINE = 'hnsw_cosine';
8787
public const INDEX_HNSW_DOT = 'hnsw_dot';
88+
public const INDEX_TRIGRAM = 'trigram';
8889

8990
// Max limits
9091
public const MAX_INT = 2147483647;
@@ -1641,6 +1642,11 @@ public function createCollection(string $id, array $attributes = [], array $inde
16411642
$this->adapter->getSupportForMultipleFulltextIndexes(),
16421643
$this->adapter->getSupportForIdenticalIndexes(),
16431644
$this->adapter->getSupportForObject(),
1645+
$this->adapter->getSupportForTrigramIndex(),
1646+
$this->adapter->getSupportForSpatialAttributes(),
1647+
$this->adapter->getSupportForIndex(),
1648+
$this->adapter->getSupportForUniqueIndex(),
1649+
$this->adapter->getSupportForFulltextIndex(),
16441650
);
16451651
foreach ($indexes as $index) {
16461652
if (!$validator->isValid($index)) {
@@ -2785,7 +2791,12 @@ public function updateAttribute(string $collection, string $id, ?string $type =
27852791
$this->adapter->getSupportForAttributes(),
27862792
$this->adapter->getSupportForMultipleFulltextIndexes(),
27872793
$this->adapter->getSupportForIdenticalIndexes(),
2788-
$this->adapter->getSupportForObject()
2794+
$this->adapter->getSupportForObject(),
2795+
$this->adapter->getSupportForTrigramIndex(),
2796+
$this->adapter->getSupportForSpatialAttributes(),
2797+
$this->adapter->getSupportForIndex(),
2798+
$this->adapter->getSupportForUniqueIndex(),
2799+
$this->adapter->getSupportForFulltextIndex(),
27892800
);
27902801

27912802
foreach ($indexes as $index) {
@@ -3623,52 +3634,6 @@ public function createIndex(string $collection, string $id, string $type, array
36233634
throw new LimitException('Index limit reached. Cannot create new index.');
36243635
}
36253636

3626-
switch ($type) {
3627-
case self::INDEX_KEY:
3628-
if (!$this->adapter->getSupportForIndex()) {
3629-
throw new DatabaseException('Key index is not supported');
3630-
}
3631-
break;
3632-
3633-
case self::INDEX_UNIQUE:
3634-
if (!$this->adapter->getSupportForUniqueIndex()) {
3635-
throw new DatabaseException('Unique index is not supported');
3636-
}
3637-
break;
3638-
3639-
case self::INDEX_FULLTEXT:
3640-
if (!$this->adapter->getSupportForFulltextIndex()) {
3641-
throw new DatabaseException('Fulltext index is not supported');
3642-
}
3643-
break;
3644-
3645-
case self::INDEX_SPATIAL:
3646-
if (!$this->adapter->getSupportForSpatialAttributes()) {
3647-
throw new DatabaseException('Spatial indexes are not supported');
3648-
}
3649-
if (!empty($orders) && !$this->adapter->getSupportForSpatialIndexOrder()) {
3650-
throw new DatabaseException('Spatial indexes with explicit orders are not supported. Remove the orders to create this index.');
3651-
}
3652-
break;
3653-
3654-
case Database::INDEX_HNSW_EUCLIDEAN:
3655-
case Database::INDEX_HNSW_COSINE:
3656-
case Database::INDEX_HNSW_DOT:
3657-
if (!$this->adapter->getSupportForVectors()) {
3658-
throw new DatabaseException('Vector indexes are not supported');
3659-
}
3660-
break;
3661-
3662-
case self::INDEX_OBJECT:
3663-
if (!$this->adapter->getSupportForObject()) {
3664-
throw new DatabaseException('Object indexes are not supported');
3665-
}
3666-
break;
3667-
3668-
default:
3669-
throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_FULLTEXT . ', ' . Database::INDEX_SPATIAL . ', ' . Database::INDEX_OBJECT . ', ' . Database::INDEX_HNSW_EUCLIDEAN . ', ' . Database::INDEX_HNSW_COSINE . ', ' . Database::INDEX_HNSW_DOT);
3670-
}
3671-
36723637
/** @var array<Document> $collectionAttributes */
36733638
$collectionAttributes = $collection->getAttribute('attributes', []);
36743639
$indexAttributesWithTypes = [];
@@ -3722,6 +3687,11 @@ public function createIndex(string $collection, string $id, string $type, array
37223687
$this->adapter->getSupportForMultipleFulltextIndexes(),
37233688
$this->adapter->getSupportForIdenticalIndexes(),
37243689
$this->adapter->getSupportForObject(),
3690+
$this->adapter->getSupportForTrigramIndex(),
3691+
$this->adapter->getSupportForSpatialAttributes(),
3692+
$this->adapter->getSupportForIndex(),
3693+
$this->adapter->getSupportForUniqueIndex(),
3694+
$this->adapter->getSupportForFulltextIndex(),
37253695
);
37263696
if (!$validator->isValid($index)) {
37273697
throw new IndexException($validator->getDescription());

0 commit comments

Comments
 (0)