Skip to content

Commit 828acd3

Browse files
authored
Merge pull request #652 from utopia-php/not-query-types-implementation
Not query types implementation
2 parents 669f38d + 760b579 commit 828acd3

9 files changed

Lines changed: 608 additions & 9 deletions

File tree

src/Database/Adapter/MariaDB.php

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1696,39 +1696,69 @@ protected function getSQLCondition(Query $query, array &$binds): string
16961696

16971697
return "MATCH({$alias}.{$attribute}) AGAINST (:{$placeholder}_0 IN BOOLEAN MODE)";
16981698

1699+
case Query::TYPE_NOT_SEARCH:
1700+
$binds[":{$placeholder}_0"] = $this->getFulltextValue($query->getValue());
1701+
1702+
return "NOT (MATCH({$alias}.{$attribute}) AGAINST (:{$placeholder}_0 IN BOOLEAN MODE))";
1703+
16991704
case Query::TYPE_BETWEEN:
17001705
$binds[":{$placeholder}_0"] = $query->getValues()[0];
17011706
$binds[":{$placeholder}_1"] = $query->getValues()[1];
17021707

17031708
return "{$alias}.{$attribute} BETWEEN :{$placeholder}_0 AND :{$placeholder}_1";
17041709

1710+
case Query::TYPE_NOT_BETWEEN:
1711+
$binds[":{$placeholder}_0"] = $query->getValues()[0];
1712+
$binds[":{$placeholder}_1"] = $query->getValues()[1];
1713+
1714+
return "{$alias}.{$attribute} NOT BETWEEN :{$placeholder}_0 AND :{$placeholder}_1";
1715+
17051716
case Query::TYPE_IS_NULL:
17061717
case Query::TYPE_IS_NOT_NULL:
17071718

17081719
return "{$alias}.{$attribute} {$this->getSQLOperator($query->getMethod())}";
17091720

17101721
case Query::TYPE_CONTAINS:
1722+
case Query::TYPE_NOT_CONTAINS:
17111723
if ($this->getSupportForJSONOverlaps() && $query->onArray()) {
17121724
$binds[":{$placeholder}_0"] = json_encode($query->getValues());
1713-
return "JSON_OVERLAPS({$alias}.{$attribute}, :{$placeholder}_0)";
1725+
$isNot = $query->getMethod() === Query::TYPE_NOT_CONTAINS;
1726+
return $isNot
1727+
? "NOT (JSON_OVERLAPS({$alias}.{$attribute}, :{$placeholder}_0))"
1728+
: "JSON_OVERLAPS({$alias}.{$attribute}, :{$placeholder}_0)";
17141729
}
17151730

17161731
// no break! continue to default case
17171732
default:
17181733
$conditions = [];
1734+
$isNotQuery = in_array($query->getMethod(), [
1735+
Query::TYPE_NOT_STARTS_WITH,
1736+
Query::TYPE_NOT_ENDS_WITH,
1737+
Query::TYPE_NOT_CONTAINS
1738+
]);
1739+
17191740
foreach ($query->getValues() as $key => $value) {
17201741
$value = match ($query->getMethod()) {
17211742
Query::TYPE_STARTS_WITH => $this->escapeWildcards($value) . '%',
1743+
Query::TYPE_NOT_STARTS_WITH => $this->escapeWildcards($value) . '%',
17221744
Query::TYPE_ENDS_WITH => '%' . $this->escapeWildcards($value),
1745+
Query::TYPE_NOT_ENDS_WITH => '%' . $this->escapeWildcards($value),
17231746
Query::TYPE_CONTAINS => $query->onArray() ? \json_encode($value) : '%' . $this->escapeWildcards($value) . '%',
1747+
Query::TYPE_NOT_CONTAINS => $query->onArray() ? \json_encode($value) : '%' . $this->escapeWildcards($value) . '%',
17241748
default => $value
17251749
};
17261750

17271751
$binds[":{$placeholder}_{$key}"] = $value;
1728-
$conditions[] = "{$alias}.{$attribute} {$this->getSQLOperator($query->getMethod())} :{$placeholder}_{$key}";
1752+
1753+
if ($isNotQuery) {
1754+
$conditions[] = "{$alias}.{$attribute} NOT {$this->getSQLOperator($query->getMethod())} :{$placeholder}_{$key}";
1755+
} else {
1756+
$conditions[] = "{$alias}.{$attribute} {$this->getSQLOperator($query->getMethod())} :{$placeholder}_{$key}";
1757+
}
17291758
}
17301759

1731-
return empty($conditions) ? '' : '(' . implode(' OR ', $conditions) . ')';
1760+
$separator = $isNotQuery ? ' AND ' : ' OR ';
1761+
return empty($conditions) ? '' : '(' . implode($separator, $conditions) . ')';
17321762
}
17331763
}
17341764

src/Database/Adapter/Postgres.php

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1794,36 +1794,67 @@ protected function getSQLCondition(Query $query, array &$binds): string
17941794
$binds[":{$placeholder}_0"] = $this->getFulltextValue($query->getValue());
17951795
return "to_tsvector(regexp_replace({$attribute}, '[^\w]+',' ','g')) @@ websearch_to_tsquery(:{$placeholder}_0)";
17961796

1797+
case Query::TYPE_NOT_SEARCH:
1798+
$binds[":{$placeholder}_0"] = $this->getFulltextValue($query->getValue());
1799+
return "NOT (to_tsvector(regexp_replace({$attribute}, '[^\w]+',' ','g')) @@ websearch_to_tsquery(:{$placeholder}_0))";
1800+
17971801
case Query::TYPE_BETWEEN:
17981802
$binds[":{$placeholder}_0"] = $query->getValues()[0];
17991803
$binds[":{$placeholder}_1"] = $query->getValues()[1];
18001804
return "{$alias}.{$attribute} BETWEEN :{$placeholder}_0 AND :{$placeholder}_1";
18011805

1806+
case Query::TYPE_NOT_BETWEEN:
1807+
$binds[":{$placeholder}_0"] = $query->getValues()[0];
1808+
$binds[":{$placeholder}_1"] = $query->getValues()[1];
1809+
return "{$alias}.{$attribute} NOT BETWEEN :{$placeholder}_0 AND :{$placeholder}_1";
1810+
18021811
case Query::TYPE_IS_NULL:
18031812
case Query::TYPE_IS_NOT_NULL:
18041813
return "{$alias}.{$attribute} {$this->getSQLOperator($query->getMethod())}";
18051814

18061815
case Query::TYPE_CONTAINS:
1807-
$operator = $query->onArray() ? '@>' : null;
1816+
case Query::TYPE_NOT_CONTAINS:
1817+
if ($query->onArray()) {
1818+
$operator = '@>';
1819+
} else {
1820+
$operator = null;
1821+
}
18081822

18091823
// no break
18101824
default:
18111825
$conditions = [];
18121826
$operator = $operator ?? $this->getSQLOperator($query->getMethod());
1827+
$isNotQuery = in_array($query->getMethod(), [
1828+
Query::TYPE_NOT_STARTS_WITH,
1829+
Query::TYPE_NOT_ENDS_WITH,
1830+
Query::TYPE_NOT_CONTAINS
1831+
]);
18131832

18141833
foreach ($query->getValues() as $key => $value) {
18151834
$value = match ($query->getMethod()) {
18161835
Query::TYPE_STARTS_WITH => $this->escapeWildcards($value) . '%',
1836+
Query::TYPE_NOT_STARTS_WITH => $this->escapeWildcards($value) . '%',
18171837
Query::TYPE_ENDS_WITH => '%' . $this->escapeWildcards($value),
1838+
Query::TYPE_NOT_ENDS_WITH => '%' . $this->escapeWildcards($value),
18181839
Query::TYPE_CONTAINS => $query->onArray() ? \json_encode($value) : '%' . $this->escapeWildcards($value) . '%',
1840+
Query::TYPE_NOT_CONTAINS => $query->onArray() ? \json_encode($value) : '%' . $this->escapeWildcards($value) . '%',
18191841
default => $value
18201842
};
18211843

18221844
$binds[":{$placeholder}_{$key}"] = $value;
1823-
$conditions[] = "{$alias}.{$attribute} {$operator} :{$placeholder}_{$key}";
1845+
1846+
if ($isNotQuery && $query->onArray()) {
1847+
// For array NOT queries, wrap the entire condition in NOT()
1848+
$conditions[] = "NOT ({$alias}.{$attribute} {$operator} :{$placeholder}_{$key})";
1849+
} elseif ($isNotQuery && !$query->onArray()) {
1850+
$conditions[] = "{$alias}.{$attribute} NOT {$operator} :{$placeholder}_{$key}";
1851+
} else {
1852+
$conditions[] = "{$alias}.{$attribute} {$operator} :{$placeholder}_{$key}";
1853+
}
18241854
}
18251855

1826-
return empty($conditions) ? '' : '(' . implode(' OR ', $conditions) . ')';
1856+
$separator = $isNotQuery ? ' AND ' : ' OR ';
1857+
return empty($conditions) ? '' : '(' . implode($separator, $conditions) . ')';
18271858
}
18281859
}
18291860

src/Database/Adapter/SQL.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1501,6 +1501,9 @@ protected function getSQLOperator(string $method): string
15011501
case Query::TYPE_STARTS_WITH:
15021502
case Query::TYPE_ENDS_WITH:
15031503
case Query::TYPE_CONTAINS:
1504+
case Query::TYPE_NOT_STARTS_WITH:
1505+
case Query::TYPE_NOT_ENDS_WITH:
1506+
case Query::TYPE_NOT_CONTAINS:
15041507
return $this->getLikeOperator();
15051508
default:
15061509
throw new DatabaseException('Unknown method: ' . $method);

src/Database/Query.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,17 @@ class Query
1515
public const TYPE_GREATER = 'greaterThan';
1616
public const TYPE_GREATER_EQUAL = 'greaterThanEqual';
1717
public const TYPE_CONTAINS = 'contains';
18+
public const TYPE_NOT_CONTAINS = 'notContains';
1819
public const TYPE_SEARCH = 'search';
20+
public const TYPE_NOT_SEARCH = 'notSearch';
1921
public const TYPE_IS_NULL = 'isNull';
2022
public const TYPE_IS_NOT_NULL = 'isNotNull';
2123
public const TYPE_BETWEEN = 'between';
24+
public const TYPE_NOT_BETWEEN = 'notBetween';
2225
public const TYPE_STARTS_WITH = 'startsWith';
26+
public const TYPE_NOT_STARTS_WITH = 'notStartsWith';
2327
public const TYPE_ENDS_WITH = 'endsWith';
28+
public const TYPE_NOT_ENDS_WITH = 'notEndsWith';
2429

2530
public const TYPE_SELECT = 'select';
2631

@@ -48,12 +53,17 @@ class Query
4853
self::TYPE_GREATER,
4954
self::TYPE_GREATER_EQUAL,
5055
self::TYPE_CONTAINS,
56+
self::TYPE_NOT_CONTAINS,
5157
self::TYPE_SEARCH,
58+
self::TYPE_NOT_SEARCH,
5259
self::TYPE_IS_NULL,
5360
self::TYPE_IS_NOT_NULL,
5461
self::TYPE_BETWEEN,
62+
self::TYPE_NOT_BETWEEN,
5563
self::TYPE_STARTS_WITH,
64+
self::TYPE_NOT_STARTS_WITH,
5665
self::TYPE_ENDS_WITH,
66+
self::TYPE_NOT_ENDS_WITH,
5767
self::TYPE_SELECT,
5868
self::TYPE_ORDER_DESC,
5969
self::TYPE_ORDER_ASC,
@@ -206,7 +216,9 @@ public static function isMethod(string $value): bool
206216
self::TYPE_GREATER,
207217
self::TYPE_GREATER_EQUAL,
208218
self::TYPE_CONTAINS,
219+
self::TYPE_NOT_CONTAINS,
209220
self::TYPE_SEARCH,
221+
self::TYPE_NOT_SEARCH,
210222
self::TYPE_ORDER_ASC,
211223
self::TYPE_ORDER_DESC,
212224
self::TYPE_LIMIT,
@@ -216,8 +228,11 @@ public static function isMethod(string $value): bool
216228
self::TYPE_IS_NULL,
217229
self::TYPE_IS_NOT_NULL,
218230
self::TYPE_BETWEEN,
231+
self::TYPE_NOT_BETWEEN,
219232
self::TYPE_STARTS_WITH,
233+
self::TYPE_NOT_STARTS_WITH,
220234
self::TYPE_ENDS_WITH,
235+
self::TYPE_NOT_ENDS_WITH,
221236
self::TYPE_OR,
222237
self::TYPE_AND,
223238
self::TYPE_SELECT => true,
@@ -429,6 +444,18 @@ public static function contains(string $attribute, array $values): self
429444
return new self(self::TYPE_CONTAINS, $attribute, $values);
430445
}
431446

447+
/**
448+
* Helper method to create Query with notContains method
449+
*
450+
* @param string $attribute
451+
* @param array<mixed> $values
452+
* @return Query
453+
*/
454+
public static function notContains(string $attribute, array $values): self
455+
{
456+
return new self(self::TYPE_NOT_CONTAINS, $attribute, $values);
457+
}
458+
432459
/**
433460
* Helper method to create Query with between method
434461
*
@@ -442,6 +469,19 @@ public static function between(string $attribute, string|int|float|bool $start,
442469
return new self(self::TYPE_BETWEEN, $attribute, [$start, $end]);
443470
}
444471

472+
/**
473+
* Helper method to create Query with notBetween method
474+
*
475+
* @param string $attribute
476+
* @param string|int|float|bool $start
477+
* @param string|int|float|bool $end
478+
* @return Query
479+
*/
480+
public static function notBetween(string $attribute, string|int|float|bool $start, string|int|float|bool $end): self
481+
{
482+
return new self(self::TYPE_NOT_BETWEEN, $attribute, [$start, $end]);
483+
}
484+
445485
/**
446486
* Helper method to create Query with search method
447487
*
@@ -454,6 +494,18 @@ public static function search(string $attribute, string $value): self
454494
return new self(self::TYPE_SEARCH, $attribute, [$value]);
455495
}
456496

497+
/**
498+
* Helper method to create Query with notSearch method
499+
*
500+
* @param string $attribute
501+
* @param string $value
502+
* @return Query
503+
*/
504+
public static function notSearch(string $attribute, string $value): self
505+
{
506+
return new self(self::TYPE_NOT_SEARCH, $attribute, [$value]);
507+
}
508+
457509
/**
458510
* Helper method to create Query with select method
459511
*
@@ -558,11 +610,21 @@ public static function startsWith(string $attribute, string $value): self
558610
return new self(self::TYPE_STARTS_WITH, $attribute, [$value]);
559611
}
560612

613+
public static function notStartsWith(string $attribute, string $value): self
614+
{
615+
return new self(self::TYPE_NOT_STARTS_WITH, $attribute, [$value]);
616+
}
617+
561618
public static function endsWith(string $attribute, string $value): self
562619
{
563620
return new self(self::TYPE_ENDS_WITH, $attribute, [$value]);
564621
}
565622

623+
public static function notEndsWith(string $attribute, string $value): self
624+
{
625+
return new self(self::TYPE_NOT_ENDS_WITH, $attribute, [$value]);
626+
}
627+
566628
/**
567629
* @param array<Query> $queries
568630
* @return Query

src/Database/Validator/Queries.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,17 @@ public function isValid($value): bool
9393
Query::TYPE_GREATER,
9494
Query::TYPE_GREATER_EQUAL,
9595
Query::TYPE_SEARCH,
96+
Query::TYPE_NOT_SEARCH,
9697
Query::TYPE_IS_NULL,
9798
Query::TYPE_IS_NOT_NULL,
9899
Query::TYPE_BETWEEN,
100+
Query::TYPE_NOT_BETWEEN,
99101
Query::TYPE_STARTS_WITH,
102+
Query::TYPE_NOT_STARTS_WITH,
100103
Query::TYPE_CONTAINS,
104+
Query::TYPE_NOT_CONTAINS,
101105
Query::TYPE_ENDS_WITH,
106+
Query::TYPE_NOT_ENDS_WITH,
102107
Query::TYPE_AND,
103108
Query::TYPE_OR => Base::METHOD_TYPE_FILTER,
104109
default => '',

src/Database/Validator/Query/Filter.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,16 +181,17 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s
181181

182182
if (
183183
!$array &&
184-
$method === Query::TYPE_CONTAINS &&
184+
in_array($method, [Query::TYPE_CONTAINS, Query::TYPE_NOT_CONTAINS]) &&
185185
$attributeSchema['type'] !== Database::VAR_STRING
186186
) {
187-
$this->message = 'Cannot query contains on attribute "' . $attribute . '" because it is not an array or string.';
187+
$queryType = $method === Query::TYPE_NOT_CONTAINS ? 'notContains' : 'contains';
188+
$this->message = 'Cannot query ' . $queryType . ' on attribute "' . $attribute . '" because it is not an array or string.';
188189
return false;
189190
}
190191

191192
if (
192193
$array &&
193-
!in_array($method, [Query::TYPE_CONTAINS, Query::TYPE_IS_NULL, Query::TYPE_IS_NOT_NULL])
194+
!in_array($method, [Query::TYPE_CONTAINS, Query::TYPE_NOT_CONTAINS, Query::TYPE_IS_NULL, Query::TYPE_IS_NOT_NULL])
194195
) {
195196
$this->message = 'Cannot query '. $method .' on attribute "' . $attribute . '" because it is an array.';
196197
return false;
@@ -233,6 +234,7 @@ public function isValid($value): bool
233234
switch ($method) {
234235
case Query::TYPE_EQUAL:
235236
case Query::TYPE_CONTAINS:
237+
case Query::TYPE_NOT_CONTAINS:
236238
if ($this->isEmpty($value->getValues())) {
237239
$this->message = \ucfirst($method) . ' queries require at least one value.';
238240
return false;
@@ -246,8 +248,11 @@ public function isValid($value): bool
246248
case Query::TYPE_GREATER:
247249
case Query::TYPE_GREATER_EQUAL:
248250
case Query::TYPE_SEARCH:
251+
case Query::TYPE_NOT_SEARCH:
249252
case Query::TYPE_STARTS_WITH:
253+
case Query::TYPE_NOT_STARTS_WITH:
250254
case Query::TYPE_ENDS_WITH:
255+
case Query::TYPE_NOT_ENDS_WITH:
251256
if (count($value->getValues()) != 1) {
252257
$this->message = \ucfirst($method) . ' queries require exactly one value.';
253258
return false;
@@ -256,6 +261,7 @@ public function isValid($value): bool
256261
return $this->isValidAttributeAndValues($attribute, $value->getValues(), $method);
257262

258263
case Query::TYPE_BETWEEN:
264+
case Query::TYPE_NOT_BETWEEN:
259265
if (count($value->getValues()) != 2) {
260266
$this->message = \ucfirst($method) . ' queries require exactly two values.';
261267
return false;

0 commit comments

Comments
 (0)