Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 33 additions & 3 deletions src/Database/Adapter/MariaDB.php
Original file line number Diff line number Diff line change
Expand Up @@ -1696,39 +1696,69 @@ protected function getSQLCondition(Query $query, array &$binds): string

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

case Query::TYPE_NOT_SEARCH:
$binds[":{$placeholder}_0"] = $this->getFulltextValue($query->getValue());

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

case Query::TYPE_BETWEEN:
$binds[":{$placeholder}_0"] = $query->getValues()[0];
$binds[":{$placeholder}_1"] = $query->getValues()[1];

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

case Query::TYPE_NOT_BETWEEN:
$binds[":{$placeholder}_0"] = $query->getValues()[0];
$binds[":{$placeholder}_1"] = $query->getValues()[1];

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

case Query::TYPE_IS_NULL:
case Query::TYPE_IS_NOT_NULL:

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

case Query::TYPE_CONTAINS:
case Query::TYPE_NOT_CONTAINS:
if ($this->getSupportForJSONOverlaps() && $query->onArray()) {
$binds[":{$placeholder}_0"] = json_encode($query->getValues());
return "JSON_OVERLAPS({$alias}.{$attribute}, :{$placeholder}_0)";
$isNot = $query->getMethod() === Query::TYPE_NOT_CONTAINS;
return $isNot
? "NOT (JSON_OVERLAPS({$alias}.{$attribute}, :{$placeholder}_0))"
: "JSON_OVERLAPS({$alias}.{$attribute}, :{$placeholder}_0)";
}

// no break! continue to default case
default:
$conditions = [];
$isNotQuery = in_array($query->getMethod(), [
Query::TYPE_NOT_STARTS_WITH,
Query::TYPE_NOT_ENDS_WITH,
Query::TYPE_NOT_CONTAINS
]);

foreach ($query->getValues() as $key => $value) {
$value = match ($query->getMethod()) {
Query::TYPE_STARTS_WITH => $this->escapeWildcards($value) . '%',
Query::TYPE_NOT_STARTS_WITH => $this->escapeWildcards($value) . '%',
Query::TYPE_ENDS_WITH => '%' . $this->escapeWildcards($value),
Query::TYPE_NOT_ENDS_WITH => '%' . $this->escapeWildcards($value),
Query::TYPE_CONTAINS => $query->onArray() ? \json_encode($value) : '%' . $this->escapeWildcards($value) . '%',
Query::TYPE_NOT_CONTAINS => $query->onArray() ? \json_encode($value) : '%' . $this->escapeWildcards($value) . '%',
default => $value
};

$binds[":{$placeholder}_{$key}"] = $value;
$conditions[] = "{$alias}.{$attribute} {$this->getSQLOperator($query->getMethod())} :{$placeholder}_{$key}";

if ($isNotQuery) {
$conditions[] = "{$alias}.{$attribute} NOT {$this->getSQLOperator($query->getMethod())} :{$placeholder}_{$key}";
} else {
$conditions[] = "{$alias}.{$attribute} {$this->getSQLOperator($query->getMethod())} :{$placeholder}_{$key}";
}
}

return empty($conditions) ? '' : '(' . implode(' OR ', $conditions) . ')';
$separator = $isNotQuery ? ' AND ' : ' OR ';
return empty($conditions) ? '' : '(' . implode($separator, $conditions) . ')';
}
}

Expand Down
37 changes: 34 additions & 3 deletions src/Database/Adapter/Postgres.php
Original file line number Diff line number Diff line change
Expand Up @@ -1794,36 +1794,67 @@ protected function getSQLCondition(Query $query, array &$binds): string
$binds[":{$placeholder}_0"] = $this->getFulltextValue($query->getValue());
return "to_tsvector(regexp_replace({$attribute}, '[^\w]+',' ','g')) @@ websearch_to_tsquery(:{$placeholder}_0)";

case Query::TYPE_NOT_SEARCH:
$binds[":{$placeholder}_0"] = $this->getFulltextValue($query->getValue());
return "NOT (to_tsvector(regexp_replace({$attribute}, '[^\w]+',' ','g')) @@ websearch_to_tsquery(:{$placeholder}_0))";

case Query::TYPE_BETWEEN:
$binds[":{$placeholder}_0"] = $query->getValues()[0];
$binds[":{$placeholder}_1"] = $query->getValues()[1];
return "{$alias}.{$attribute} BETWEEN :{$placeholder}_0 AND :{$placeholder}_1";

case Query::TYPE_NOT_BETWEEN:
$binds[":{$placeholder}_0"] = $query->getValues()[0];
$binds[":{$placeholder}_1"] = $query->getValues()[1];
return "{$alias}.{$attribute} NOT BETWEEN :{$placeholder}_0 AND :{$placeholder}_1";

case Query::TYPE_IS_NULL:
case Query::TYPE_IS_NOT_NULL:
return "{$alias}.{$attribute} {$this->getSQLOperator($query->getMethod())}";

case Query::TYPE_CONTAINS:
$operator = $query->onArray() ? '@>' : null;
case Query::TYPE_NOT_CONTAINS:
if ($query->onArray()) {
$operator = '@>';
} else {
$operator = null;
}

// no break
default:
$conditions = [];
$operator = $operator ?? $this->getSQLOperator($query->getMethod());
$isNotQuery = in_array($query->getMethod(), [
Query::TYPE_NOT_STARTS_WITH,
Query::TYPE_NOT_ENDS_WITH,
Query::TYPE_NOT_CONTAINS
]);

foreach ($query->getValues() as $key => $value) {
$value = match ($query->getMethod()) {
Query::TYPE_STARTS_WITH => $this->escapeWildcards($value) . '%',
Query::TYPE_NOT_STARTS_WITH => $this->escapeWildcards($value) . '%',
Query::TYPE_ENDS_WITH => '%' . $this->escapeWildcards($value),
Query::TYPE_NOT_ENDS_WITH => '%' . $this->escapeWildcards($value),
Query::TYPE_CONTAINS => $query->onArray() ? \json_encode($value) : '%' . $this->escapeWildcards($value) . '%',
Query::TYPE_NOT_CONTAINS => $query->onArray() ? \json_encode($value) : '%' . $this->escapeWildcards($value) . '%',
default => $value
};

$binds[":{$placeholder}_{$key}"] = $value;
$conditions[] = "{$alias}.{$attribute} {$operator} :{$placeholder}_{$key}";

if ($isNotQuery && $query->onArray()) {
// For array NOT queries, wrap the entire condition in NOT()
$conditions[] = "NOT ({$alias}.{$attribute} {$operator} :{$placeholder}_{$key})";
} elseif ($isNotQuery && !$query->onArray()) {
$conditions[] = "{$alias}.{$attribute} NOT {$operator} :{$placeholder}_{$key}";
} else {
$conditions[] = "{$alias}.{$attribute} {$operator} :{$placeholder}_{$key}";
}
}

return empty($conditions) ? '' : '(' . implode(' OR ', $conditions) . ')';
$separator = $isNotQuery ? ' AND ' : ' OR ';
return empty($conditions) ? '' : '(' . implode($separator, $conditions) . ')';
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/Database/Adapter/SQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -1501,6 +1501,9 @@ protected function getSQLOperator(string $method): string
case Query::TYPE_STARTS_WITH:
case Query::TYPE_ENDS_WITH:
case Query::TYPE_CONTAINS:
case Query::TYPE_NOT_STARTS_WITH:
case Query::TYPE_NOT_ENDS_WITH:
case Query::TYPE_NOT_CONTAINS:
return $this->getLikeOperator();
default:
throw new DatabaseException('Unknown method: ' . $method);
Expand Down
62 changes: 62 additions & 0 deletions src/Database/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ class Query
public const TYPE_GREATER = 'greaterThan';
public const TYPE_GREATER_EQUAL = 'greaterThanEqual';
public const TYPE_CONTAINS = 'contains';
public const TYPE_NOT_CONTAINS = 'notContains';
public const TYPE_SEARCH = 'search';
public const TYPE_NOT_SEARCH = 'notSearch';
public const TYPE_IS_NULL = 'isNull';
public const TYPE_IS_NOT_NULL = 'isNotNull';
public const TYPE_BETWEEN = 'between';
public const TYPE_NOT_BETWEEN = 'notBetween';
public const TYPE_STARTS_WITH = 'startsWith';
public const TYPE_NOT_STARTS_WITH = 'notStartsWith';
public const TYPE_ENDS_WITH = 'endsWith';
public const TYPE_NOT_ENDS_WITH = 'notEndsWith';

public const TYPE_SELECT = 'select';

Expand Down Expand Up @@ -48,12 +53,17 @@ class Query
self::TYPE_GREATER,
self::TYPE_GREATER_EQUAL,
self::TYPE_CONTAINS,
self::TYPE_NOT_CONTAINS,
self::TYPE_SEARCH,
self::TYPE_NOT_SEARCH,
self::TYPE_IS_NULL,
self::TYPE_IS_NOT_NULL,
self::TYPE_BETWEEN,
self::TYPE_NOT_BETWEEN,
self::TYPE_STARTS_WITH,
self::TYPE_NOT_STARTS_WITH,
self::TYPE_ENDS_WITH,
self::TYPE_NOT_ENDS_WITH,
self::TYPE_SELECT,
self::TYPE_ORDER_DESC,
self::TYPE_ORDER_ASC,
Expand Down Expand Up @@ -206,7 +216,9 @@ public static function isMethod(string $value): bool
self::TYPE_GREATER,
self::TYPE_GREATER_EQUAL,
self::TYPE_CONTAINS,
self::TYPE_NOT_CONTAINS,
self::TYPE_SEARCH,
self::TYPE_NOT_SEARCH,
self::TYPE_ORDER_ASC,
self::TYPE_ORDER_DESC,
self::TYPE_LIMIT,
Expand All @@ -216,8 +228,11 @@ public static function isMethod(string $value): bool
self::TYPE_IS_NULL,
self::TYPE_IS_NOT_NULL,
self::TYPE_BETWEEN,
self::TYPE_NOT_BETWEEN,
self::TYPE_STARTS_WITH,
self::TYPE_NOT_STARTS_WITH,
self::TYPE_ENDS_WITH,
self::TYPE_NOT_ENDS_WITH,
self::TYPE_OR,
self::TYPE_AND,
self::TYPE_SELECT => true,
Expand Down Expand Up @@ -429,6 +444,18 @@ public static function contains(string $attribute, array $values): self
return new self(self::TYPE_CONTAINS, $attribute, $values);
}

/**
* Helper method to create Query with notContains method
*
* @param string $attribute
* @param array<mixed> $values
* @return Query
*/
public static function notContains(string $attribute, array $values): self
{
return new self(self::TYPE_NOT_CONTAINS, $attribute, $values);
}

/**
* Helper method to create Query with between method
*
Expand All @@ -442,6 +469,19 @@ public static function between(string $attribute, string|int|float|bool $start,
return new self(self::TYPE_BETWEEN, $attribute, [$start, $end]);
}

/**
* Helper method to create Query with notBetween method
*
* @param string $attribute
* @param string|int|float|bool $start
* @param string|int|float|bool $end
* @return Query
*/
public static function notBetween(string $attribute, string|int|float|bool $start, string|int|float|bool $end): self
{
return new self(self::TYPE_NOT_BETWEEN, $attribute, [$start, $end]);
}

/**
* Helper method to create Query with search method
*
Expand All @@ -454,6 +494,18 @@ public static function search(string $attribute, string $value): self
return new self(self::TYPE_SEARCH, $attribute, [$value]);
}

/**
* Helper method to create Query with notSearch method
*
* @param string $attribute
* @param string $value
* @return Query
*/
public static function notSearch(string $attribute, string $value): self
{
return new self(self::TYPE_NOT_SEARCH, $attribute, [$value]);
}

/**
* Helper method to create Query with select method
*
Expand Down Expand Up @@ -558,11 +610,21 @@ public static function startsWith(string $attribute, string $value): self
return new self(self::TYPE_STARTS_WITH, $attribute, [$value]);
}

public static function notStartsWith(string $attribute, string $value): self
{
return new self(self::TYPE_NOT_STARTS_WITH, $attribute, [$value]);
}

public static function endsWith(string $attribute, string $value): self
{
return new self(self::TYPE_ENDS_WITH, $attribute, [$value]);
}

public static function notEndsWith(string $attribute, string $value): self
{
return new self(self::TYPE_NOT_ENDS_WITH, $attribute, [$value]);
}

/**
* @param array<Query> $queries
* @return Query
Expand Down
5 changes: 5 additions & 0 deletions src/Database/Validator/Queries.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,17 @@ public function isValid($value): bool
Query::TYPE_GREATER,
Query::TYPE_GREATER_EQUAL,
Query::TYPE_SEARCH,
Query::TYPE_NOT_SEARCH,
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_CONTAINS,
Query::TYPE_NOT_CONTAINS,
Query::TYPE_ENDS_WITH,
Query::TYPE_NOT_ENDS_WITH,
Query::TYPE_AND,
Query::TYPE_OR => Base::METHOD_TYPE_FILTER,
default => '',
Expand Down
12 changes: 9 additions & 3 deletions src/Database/Validator/Query/Filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,16 +181,17 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s

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

if (
$array &&
!in_array($method, [Query::TYPE_CONTAINS, Query::TYPE_IS_NULL, Query::TYPE_IS_NOT_NULL])
!in_array($method, [Query::TYPE_CONTAINS, Query::TYPE_NOT_CONTAINS, Query::TYPE_IS_NULL, Query::TYPE_IS_NOT_NULL])
) {
$this->message = 'Cannot query '. $method .' on attribute "' . $attribute . '" because it is an array.';
return false;
Expand Down Expand Up @@ -233,6 +234,7 @@ public function isValid($value): bool
switch ($method) {
case Query::TYPE_EQUAL:
case Query::TYPE_CONTAINS:
case Query::TYPE_NOT_CONTAINS:
if ($this->isEmpty($value->getValues())) {
$this->message = \ucfirst($method) . ' queries require at least one value.';
return false;
Expand All @@ -246,8 +248,11 @@ public function isValid($value): bool
case Query::TYPE_GREATER:
case Query::TYPE_GREATER_EQUAL:
case Query::TYPE_SEARCH:
case Query::TYPE_NOT_SEARCH:
case Query::TYPE_STARTS_WITH:
case Query::TYPE_NOT_STARTS_WITH:
case Query::TYPE_ENDS_WITH:
case Query::TYPE_NOT_ENDS_WITH:
if (count($value->getValues()) != 1) {
$this->message = \ucfirst($method) . ' queries require exactly one value.';
return false;
Expand All @@ -256,6 +261,7 @@ public function isValid($value): bool
return $this->isValidAttributeAndValues($attribute, $value->getValues(), $method);

case Query::TYPE_BETWEEN:
case Query::TYPE_NOT_BETWEEN:
if (count($value->getValues()) != 2) {
$this->message = \ucfirst($method) . ' queries require exactly two values.';
return false;
Expand Down
Loading