Skip to content

Commit a1bb3e2

Browse files
authored
Merge pull request #870 from utopia-php/feat-sqlite-fts
feat(sqlite): back fulltext indexes with FTS5 virtual tables
2 parents e5c9edd + bf5540e commit a1bb3e2

6 files changed

Lines changed: 1557 additions & 80 deletions

File tree

src/Database/Adapter/MariaDB.php

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1565,7 +1565,7 @@ protected function handleSpatialQueries(Query $query, array &$binds, string $att
15651565
* @return string
15661566
* @throws Exception
15671567
*/
1568-
protected function getSQLCondition(Query $query, array &$binds): string
1568+
protected function getSQLCondition(Query $query, array &$binds, ?string $forCollection = null): string
15691569
{
15701570
$query->setAttribute($this->getInternalKeyForAttribute($query->getAttribute()));
15711571

@@ -1585,7 +1585,7 @@ protected function getSQLCondition(Query $query, array &$binds): string
15851585
$conditions = [];
15861586
/* @var $q Query */
15871587
foreach ($query->getValue() as $q) {
1588-
$conditions[] = $this->getSQLCondition($q, $binds);
1588+
$conditions[] = $this->getSQLCondition($q, $binds, $forCollection);
15891589
}
15901590

15911591
$method = strtoupper($query->getMethod());
@@ -1627,12 +1627,30 @@ protected function getSQLCondition(Query $query, array &$binds): string
16271627
case Query::TYPE_CONTAINS:
16281628
case Query::TYPE_CONTAINS_ANY:
16291629
case Query::TYPE_NOT_CONTAINS:
1630-
if ($this->getSupportForJSONOverlaps() && $query->onArray()) {
1631-
$binds[":{$placeholder}_0"] = json_encode($query->getValues());
1630+
if ($query->onArray()) {
16321631
$isNot = $query->getMethod() === Query::TYPE_NOT_CONTAINS;
1633-
return $isNot
1634-
? "NOT (JSON_OVERLAPS({$alias}.{$attribute}, :{$placeholder}_0))"
1635-
: "JSON_OVERLAPS({$alias}.{$attribute}, :{$placeholder}_0)";
1632+
1633+
if ($this->getSupportForJSONOverlaps()) {
1634+
$binds[":{$placeholder}_0"] = json_encode($query->getValues());
1635+
return $isNot
1636+
? "NOT (JSON_OVERLAPS({$alias}.{$attribute}, :{$placeholder}_0))"
1637+
: "JSON_OVERLAPS({$alias}.{$attribute}, :{$placeholder}_0)";
1638+
}
1639+
1640+
// JSON_CONTAINS per element OR'd together — exact
1641+
// element match without LIKE's substring false positives
1642+
// (`%2%` matching `[12, 200]`, `%"apple"%` matching
1643+
// `["pineapple"]`).
1644+
$conditions = [];
1645+
foreach ($query->getValues() as $key => $value) {
1646+
$binds[":{$placeholder}_{$key}"] = json_encode($value);
1647+
$conditions[] = "JSON_CONTAINS({$alias}.{$attribute}, :{$placeholder}_{$key})";
1648+
}
1649+
if (empty($conditions)) {
1650+
return '';
1651+
}
1652+
$expression = '(' . implode(' OR ', $conditions) . ')';
1653+
return $isNot ? "NOT {$expression}" : $expression;
16361654
}
16371655
// no break
16381656
default:
@@ -1649,8 +1667,7 @@ protected function getSQLCondition(Query $query, array &$binds): string
16491667
Query::TYPE_NOT_STARTS_WITH => $this->escapeWildcards($value) . '%',
16501668
Query::TYPE_ENDS_WITH => '%' . $this->escapeWildcards($value),
16511669
Query::TYPE_NOT_ENDS_WITH => '%' . $this->escapeWildcards($value),
1652-
Query::TYPE_CONTAINS, Query::TYPE_CONTAINS_ANY => ($query->onArray()) ? \json_encode($value) : '%' . $this->escapeWildcards($value) . '%',
1653-
Query::TYPE_NOT_CONTAINS => ($query->onArray()) ? \json_encode($value) : '%' . $this->escapeWildcards($value) . '%',
1670+
Query::TYPE_CONTAINS, Query::TYPE_CONTAINS_ANY, Query::TYPE_NOT_CONTAINS => '%' . $this->escapeWildcards($value) . '%',
16541671
default => $value
16551672
};
16561673

src/Database/Adapter/Postgres.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1763,7 +1763,7 @@ protected function handleObjectQueries(Query $query, array &$binds, string $attr
17631763
* @return string
17641764
* @throws Exception
17651765
*/
1766-
protected function getSQLCondition(Query $query, array &$binds): string
1766+
protected function getSQLCondition(Query $query, array &$binds, ?string $forCollection = null): string
17671767
{
17681768
$query->setAttribute($this->getInternalKeyForAttribute($query->getAttribute()));
17691769
$isNestedObjectAttribute = $query->isObjectAttribute() && \str_contains($query->getAttribute(), '.');
@@ -1793,7 +1793,7 @@ protected function getSQLCondition(Query $query, array &$binds): string
17931793
$conditions = [];
17941794
/* @var $q Query */
17951795
foreach ($query->getValue() as $q) {
1796-
$conditions[] = $this->getSQLCondition($q, $binds);
1796+
$conditions[] = $this->getSQLCondition($q, $binds, $forCollection);
17971797
}
17981798

17991799
$method = strtoupper($query->getMethod());

src/Database/Adapter/SQL.php

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ protected function getFloatPrecision(float $value): string
4949
return sprintf('%.'. $this->floatPrecision . 'F', $value);
5050
}
5151

52+
/**
53+
* Build conditions threading `$name` to per-query builders so adapter
54+
* overrides (SQLite FTS5 routing) can resolve auxiliary tables.
55+
*
56+
* @param array<Query> $queries
57+
* @param array<string,mixed> $binds
58+
*/
59+
protected function getSQLConditionsForCollection(string $name, array $queries, array &$binds, string $separator = 'AND'): string
60+
{
61+
return $this->getSQLConditions($queries, $binds, $separator, $name);
62+
}
63+
5264
/**
5365
* Constructor.
5466
*
@@ -2312,19 +2324,21 @@ public function getMaxUIDLength(): int
23122324
/**
23132325
* @param Query $query
23142326
* @param array<string, mixed> $binds
2327+
* @param ?string $forCollection Filtered collection id (for FTS5 routing).
23152328
* @return string
23162329
* @throws Exception
23172330
*/
2318-
abstract protected function getSQLCondition(Query $query, array &$binds): string;
2331+
abstract protected function getSQLCondition(Query $query, array &$binds, ?string $forCollection = null): string;
23192332

23202333
/**
23212334
* @param array<Query> $queries
23222335
* @param array<string, mixed> $binds
23232336
* @param string $separator
2337+
* @param ?string $forCollection See {@see getSQLCondition}.
23242338
* @return string
23252339
* @throws Exception
23262340
*/
2327-
public function getSQLConditions(array $queries, array &$binds, string $separator = 'AND'): string
2341+
public function getSQLConditions(array $queries, array &$binds, string $separator = 'AND', ?string $forCollection = null): string
23282342
{
23292343
$conditions = [];
23302344
foreach ($queries as $query) {
@@ -2333,9 +2347,9 @@ public function getSQLConditions(array $queries, array &$binds, string $separato
23332347
}
23342348

23352349
if ($query->isNested()) {
2336-
$conditions[] = $this->getSQLConditions($query->getValues(), $binds, $query->getMethod());
2350+
$conditions[] = $this->getSQLConditions($query->getValues(), $binds, $query->getMethod(), $forCollection);
23372351
} else {
2338-
$conditions[] = $this->getSQLCondition($query, $binds);
2352+
$conditions[] = $this->getSQLCondition($query, $binds, $forCollection);
23392353
}
23402354
}
23412355

@@ -3155,7 +3169,7 @@ public function find(Document $collection, array $queries = [], ?int $limit = 25
31553169
$where[] = '(' . implode(' OR ', $cursorWhere) . ')';
31563170
}
31573171

3158-
$conditions = $this->getSQLConditions($queries, $binds);
3172+
$conditions = $this->getSQLConditionsForCollection($name, $queries, $binds);
31593173
if (!empty($conditions)) {
31603174
$where[] = $conditions;
31613175
}
@@ -3299,7 +3313,7 @@ public function count(Document $collection, array $queries = [], ?int $max = nul
32993313
}
33003314
}
33013315

3302-
$conditions = $this->getSQLConditions($otherQueries, $binds);
3316+
$conditions = $this->getSQLConditionsForCollection($name, $otherQueries, $binds);
33033317
if (!empty($conditions)) {
33043318
$where[] = $conditions;
33053319
}
@@ -3385,7 +3399,7 @@ public function sum(Document $collection, string $attribute, array $queries = []
33853399
}
33863400
}
33873401

3388-
$conditions = $this->getSQLConditions($otherQueries, $binds);
3402+
$conditions = $this->getSQLConditionsForCollection($name, $otherQueries, $binds);
33893403
if (!empty($conditions)) {
33903404
$where[] = $conditions;
33913405
}

0 commit comments

Comments
 (0)