Skip to content
Closed
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
16 changes: 15 additions & 1 deletion src/Database/Adapter/Mongo.php
Original file line number Diff line number Diff line change
Expand Up @@ -1283,6 +1283,11 @@ protected function buildFilters(array $queries): array
),
};

$negated = match ($query->getMethod()) {
Query::TYPE_NOT_LIKE => true,
default => false,
};

if ($operator == '$eq' && \is_array($value)) {
$filters[$attribute]['$in'] = $value;
} elseif ($operator == '$ne' && \is_array($value)) {
Expand All @@ -1294,6 +1299,8 @@ protected function buildFilters(array $queries): array
} elseif ($operator === Query::TYPE_BETWEEN) {
$filters[$attribute]['$lte'] = $value[1];
$filters[$attribute]['$gte'] = $value[0];
} elseif ($negated) {
$filters[$attribute]['$not'][$operator] = $value;
} else {
$filters[$attribute][$operator] = $value;
}
Expand Down Expand Up @@ -1325,7 +1332,10 @@ protected function getQueryOperator(string $operator): string
Query::TYPE_SEARCH => '$search',
Query::TYPE_BETWEEN => 'between',
Query::TYPE_STARTS_WITH,
Query::TYPE_ENDS_WITH => '$regex',
Query::TYPE_ENDS_WITH,
Query::TYPE_LIKE,
Query::TYPE_NOT_LIKE,
Query::TYPE_REGEX => '$regex',
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_CONTAINS . ', ' . Query::TYPE_SEARCH . ', ' . Query::TYPE_SELECT),
};
}
Expand All @@ -1339,6 +1349,10 @@ protected function getQueryValue(string $method, mixed $value): mixed
case Query::TYPE_ENDS_WITH:
$value = $this->escapeWildcards($value);
return '.*'.$value;
case Query::TYPE_LIKE:
case Query::TYPE_NOT_LIKE:
$value = $this->escapeWildcards($value);
return '.*'.$value.'.*';
default:
return $value;
}
Expand Down
50 changes: 22 additions & 28 deletions src/Database/Adapter/SQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,7 @@ protected function bindConditionValue(mixed $stmt, Query $query): void
$value = match ($query->getMethod()) {
Query::TYPE_STARTS_WITH => $this->escapeWildcards($value) . '%',
Query::TYPE_ENDS_WITH => '%' . $this->escapeWildcards($value),
Query::TYPE_LIKE => '%' . $this->escapeWildcards($value) . '%',
Query::TYPE_SEARCH => $this->getFulltextValue($value),
default => $value
};
Expand Down Expand Up @@ -748,29 +749,22 @@ protected function getFulltextValue(string $value): string
*/
protected function getSQLOperator(string $method): string
{
switch ($method) {
case Query::TYPE_EQUAL:
return '=';
case Query::TYPE_NOT_EQUAL:
return '!=';
case Query::TYPE_LESSER:
return '<';
case Query::TYPE_LESSER_EQUAL:
return '<=';
case Query::TYPE_GREATER:
return '>';
case Query::TYPE_GREATER_EQUAL:
return '>=';
case Query::TYPE_IS_NULL:
return 'IS NULL';
case Query::TYPE_IS_NOT_NULL:
return 'IS NOT NULL';
case Query::TYPE_STARTS_WITH:
case Query::TYPE_ENDS_WITH:
return 'LIKE';
default:
throw new DatabaseException('Unknown method: ' . $method);
}
return match ($method) {
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 => 'IS NULL',
Query::TYPE_IS_NOT_NULL => 'IS NOT NULL',
Query::TYPE_STARTS_WITH,
Query::TYPE_ENDS_WITH,
Query::TYPE_LIKE => 'LIKE',
Query::TYPE_NOT_LIKE => 'NOT LIKE',
Query::TYPE_REGEX => 'REGEXP',
default => throw new DatabaseException('Unknown method: ' . $method),
};
}

/**
Expand Down Expand Up @@ -837,11 +831,11 @@ protected function getSQLPermissionsCondition(string $collection, array $roles):
{
$roles = array_map(fn (string $role) => $this->getPDO()->quote($role), $roles);
return "table_main._uid IN (
SELECT distinct(_document)
FROM {$this->getSQLTable($collection . '_perms')}
WHERE _permission IN (" . implode(', ', $roles) . ")
AND _type = 'read'
)";
SELECT distinct(_document)
FROM {$this->getSQLTable($collection . '_perms')}
WHERE _permission IN (" . implode(', ', $roles) . ")
AND _type = 'read'
)";
}

/**
Expand Down
36 changes: 34 additions & 2 deletions src/Database/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ class Query
public const TYPE_BETWEEN = 'between';
public const TYPE_STARTS_WITH = 'startsWith';
public const TYPE_ENDS_WITH = 'endsWith';
public const TYPE_LIKE = 'like';
public const TYPE_NOT_LIKE = 'notLike';
public const TYPE_REGEX = 'regex';

public const TYPE_SELECT = 'select';

Expand Down Expand Up @@ -51,7 +54,6 @@ class Query
*/
protected array $values = [];


/**
* Construct a new query object
*
Expand Down Expand Up @@ -158,7 +160,7 @@ public function setValue(mixed $value): self
*/
public static function isMethod(string $value): bool
{
return match (static::getMethodFromAlias($value)) {
return match ($value) {
self::TYPE_EQUAL,
self::TYPE_NOT_EQUAL,
self::TYPE_LESSER,
Expand All @@ -178,6 +180,8 @@ public static function isMethod(string $value): bool
self::TYPE_BETWEEN,
self::TYPE_STARTS_WITH,
self::TYPE_ENDS_WITH,
self::TYPE_LIKE,
self::TYPE_NOT_LIKE,
self::TYPE_SELECT => true,
default => false,
};
Expand Down Expand Up @@ -340,6 +344,9 @@ public static function parse(string $filter): self
case self::TYPE_IS_NOT_NULL:
case self::TYPE_STARTS_WITH:
case self::TYPE_ENDS_WITH:
case self::TYPE_LIKE:
case self::TYPE_NOT_LIKE:
case self::TYPE_REGEX:
$attribute = $parsedParams[0] ?? '';
if (count($parsedParams) < 2) {
return new self($method, $attribute);
Expand Down Expand Up @@ -694,6 +701,31 @@ public static function endsWith(string $attribute, string $value): self
return new self(self::TYPE_ENDS_WITH, $attribute, [$value]);
}

/**
* @param string $attribute
* @param array<mixed> $values
* @return self
*/
public static function like(string $attribute, array $values): self
{
return new self(self::TYPE_LIKE, $attribute, $values);
}

/**
* @param string $attribute
* @param array<mixed> $values
* @return self
*/
public static function notLike(string $attribute, array $values): self
{
return new self(self::TYPE_NOT_LIKE, $attribute, $values);
}

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

/**
* Filters $queries for $types
*
Expand Down
5 changes: 4 additions & 1 deletion src/Database/Validator/Queries.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ public function isValid($value): bool
Query::TYPE_BETWEEN,
Query::TYPE_STARTS_WITH,
Query::TYPE_CONTAINS,
Query::TYPE_ENDS_WITH => Base::METHOD_TYPE_FILTER,
Query::TYPE_ENDS_WITH,
Query::TYPE_LIKE,
Query::TYPE_NOT_LIKE,
Query::TYPE_REGEX => Base::METHOD_TYPE_FILTER,
default => '',
};

Expand Down
3 changes: 3 additions & 0 deletions src/Database/Validator/Query/Filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ public function isValid($value): bool
switch ($method) {
case Query::TYPE_EQUAL:
case Query::TYPE_CONTAINS:
case Query::TYPE_LIKE:
case Query::TYPE_NOT_LIKE:
if ($this->isEmpty($value->getValues())) {
$this->message = \ucfirst($method) . ' queries require at least one value.';
return false;
Expand All @@ -153,6 +155,7 @@ public function isValid($value): bool
case Query::TYPE_SEARCH:
case Query::TYPE_STARTS_WITH:
case Query::TYPE_ENDS_WITH:
case Query::TYPE_REGEX:
if (count($value->getValues()) != 1) {
$this->message = \ucfirst($method) . ' queries require exactly one value.';
return false;
Expand Down
27 changes: 27 additions & 0 deletions tests/Database/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -2950,6 +2950,33 @@ public function testFindSelect(): void
}
}

public function testFindLike(): void
{
$documents = static::getDatabase()->find('movies', [
Query::like('name', ['Frozen']),
]);

$this->assertEquals(2, count($documents));
}

public function testFindNotLike(): void
{
$documents = static::getDatabase()->find('movies', [
Query::notLike('name', ['Frozen']),
]);

$this->assertEquals(5, count($documents));
}

public function testFindRegex(): void
{
$documents = static::getDatabase()->find('movies', [
Query::regex('name', 'Fr..en\sII'),
]);

$this->assertEquals(1, count($documents));
}

/**
* @depends testFind
*/
Expand Down
12 changes: 12 additions & 0 deletions tests/Database/QueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,18 @@ public function testParse(): void
$this->assertEquals('lastUpdate', $query->getAttribute());
$this->assertEquals('DATE1', $query->getValues()[0]);
$this->assertEquals('DATE2', $query->getValues()[1]);

$query = Query::parse('like("director", "Tester")');

$this->assertEquals('like', $query->getMethod());
$this->assertEquals('director', $query->getAttribute());
$this->assertEquals(["Tester"], $query->getValues());

$query = Query::parse('notLike("director", "Tester")');

$this->assertEquals('notLike', $query->getMethod());
$this->assertEquals('director', $query->getAttribute());
$this->assertEquals(["Tester"], $query->getValues());
}

public function testParseV2(): void
Expand Down
2 changes: 2 additions & 0 deletions tests/Database/Validator/QueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ public function testQuery(): void
$this->assertEquals(true, $validator->isValid([Query::parse('endsWith("title", "Zen")')]));
$this->assertEquals(true, $validator->isValid([Query::parse('select(["title", "description"])')]));
$this->assertEquals(true, $validator->isValid([Query::parse('notEqual("title", [""])')]));
$this->assertEquals(true, $validator->isValid(Query::parse('like("title", "Iron Man")')));
$this->assertEquals(true, $validator->isValid(Query::parse('notLike("title", "Iron Man")')));
}

/**
Expand Down