Skip to content

Commit e223078

Browse files
committed
Added a custom function to make PostgresSQL searches case insensitive
This is required only for postgres as every other database is case invariant by default. But to achieve a portable way, we implement it via a custom DQL function. This fixes issue #784
1 parent b1ba26e commit e223078

File tree

11 files changed

+94
-22
lines changed

11 files changed

+94
-22
lines changed

config/packages/doctrine.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ doctrine:
5757
field2: App\Doctrine\Functions\Field2
5858
natsort: App\Doctrine\Functions\Natsort
5959
array_position: App\Doctrine\Functions\ArrayPosition
60+
ilike: App\Doctrine\Functions\ILike
6061

6162
when@test:
6263
doctrine:

src/ApiPlatform/Filter/LikeFilter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ protected function filterProperty(
5050
}
5151
$parameterName = $queryNameGenerator->generateParameterName($property); // Generate a unique parameter name to avoid collisions with other filters
5252
$queryBuilder
53-
->andWhere(sprintf('o.%s LIKE :%s', $property, $parameterName))
53+
->andWhere(sprintf('ILIKE(o.%s, :%s) = TRUE', $property, $parameterName))
5454
->setParameter($parameterName, $value);
5555
}
5656

src/ApiPlatform/Filter/TagFilter.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,10 @@ protected function filterProperty(
6161
$expr = $queryBuilder->expr();
6262

6363
$tmp = $expr->orX(
64-
$expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_1'),
65-
$expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_2'),
66-
$expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_3'),
67-
$expr->eq('o.'.$property, ':' . $tag_identifier_prefix . '_4'),
64+
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_1) = TRUE',
65+
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_2) = TRUE',
66+
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_3) = TRUE',
67+
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_4) = TRUE',
6868
);
6969

7070
$queryBuilder->andWhere($tmp);

src/DataTables/Filters/Constraints/Part/TagsConstraint.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,10 @@ protected function getExpressionForTag(QueryBuilder $queryBuilder, string $tag):
9393
$expr = $queryBuilder->expr();
9494

9595
$tmp = $expr->orX(
96-
$expr->like($this->property, ':' . $tag_identifier_prefix . '_1'),
97-
$expr->like($this->property, ':' . $tag_identifier_prefix . '_2'),
98-
$expr->like($this->property, ':' . $tag_identifier_prefix . '_3'),
99-
$expr->eq($this->property, ':' . $tag_identifier_prefix . '_4'),
96+
'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_1) = TRUE',
97+
'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_2) = TRUE',
98+
'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_3) = TRUE',
99+
'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_4) = TRUE',
100100
);
101101

102102
//Set the parameters for the LIKE expression, in each variation of the tag (so with a comma, at the end, at the beginning, and on both ends, and equaling the tag)

src/DataTables/Filters/Constraints/TextConstraint.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ public function apply(QueryBuilder $queryBuilder): void
107107
}
108108

109109
if ($like_value !== null) {
110-
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'LIKE', $like_value);
110+
$queryBuilder->andWhere(sprintf('ILIKE(%s, :%s) = TRUE', $this->property, $this->identifier));
111+
$queryBuilder->setParameter($this->identifier, $like_value);
111112
return;
112113
}
113114

src/DataTables/Filters/PartSearchFilter.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
* along with this program. If not, see <https://www.gnu.org/licenses/>.
2222
*/
2323
namespace App\DataTables\Filters;
24-
2524
use Doctrine\ORM\QueryBuilder;
2625

2726
class PartSearchFilter implements FilterInterface
@@ -132,15 +131,15 @@ public function apply(QueryBuilder $queryBuilder): void
132131
return sprintf("REGEXP(%s, :search_query) = TRUE", $field);
133132
}
134133

135-
return sprintf("%s LIKE :search_query", $field);
134+
return sprintf("ILIKE(%s, :search_query) = TRUE", $field);
136135
}, $fields_to_search);
137136

138-
//Add Or concatation of the expressions to our query
137+
//Add Or concatenation of the expressions to our query
139138
$queryBuilder->andWhere(
140139
$queryBuilder->expr()->orX(...$expressions)
141140
);
142141

143-
//For regex we pass the query as is, for like we add % to the start and end as wildcards
142+
//For regex, we pass the query as is, for like we add % to the start and end as wildcards
144143
if ($this->regex) {
145144
$queryBuilder->setParameter('search_query', $this->keyword);
146145
} else {

src/Doctrine/Functions/ILike.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
/*
3+
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4+
*
5+
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU Affero General Public License as published
9+
* by the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU Affero General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
24+
namespace App\Doctrine\Functions;
25+
26+
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
27+
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
28+
use Doctrine\DBAL\Platforms\SQLitePlatform;
29+
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
30+
use Doctrine\ORM\Query\Parser;
31+
use Doctrine\ORM\Query\SqlWalker;
32+
use Doctrine\ORM\Query\TokenType;
33+
34+
/**
35+
* A platform invariant version of the case-insensitive LIKE operation.
36+
* On MySQL and SQLite this is the normal LIKE, but on PostgreSQL it is the ILIKE operator.
37+
*/
38+
class ILike extends FunctionNode
39+
{
40+
41+
public $value = null;
42+
43+
public $expr = null;
44+
45+
public function parse(Parser $parser): void
46+
{
47+
$parser->match(TokenType::T_IDENTIFIER);
48+
$parser->match(TokenType::T_OPEN_PARENTHESIS);
49+
$this->value = $parser->StringPrimary();
50+
$parser->match(TokenType::T_COMMA);
51+
$this->expr = $parser->StringExpression();
52+
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
53+
}
54+
55+
public function getSql(SqlWalker $sqlWalker): string
56+
{
57+
$platform = $sqlWalker->getConnection()->getDatabasePlatform();
58+
59+
//
60+
if ($platform instanceof AbstractMySQLPlatform || $platform instanceof SQLitePlatform) {
61+
$operator = 'LIKE';
62+
} elseif ($platform instanceof PostgreSQLPlatform) {
63+
//Use the case-insensitive operator, to have the same behavior as MySQL
64+
$operator = 'ILIKE';
65+
} else {
66+
throw new \RuntimeException('Platform ' . gettype($platform) . ' does not support case insensitive like expressions.');
67+
}
68+
69+
return '(' . $this->value->dispatch($sqlWalker) . ' ' . $operator . ' ' . $this->expr->dispatch($sqlWalker) . ')';
70+
}
71+
}

src/Repository/AttachmentRepository.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ public function getExternalAttachments(): int
7575
{
7676
$qb = $this->createQueryBuilder('attachment');
7777
$qb->select('COUNT(attachment)')
78-
->where('attachment.path LIKE :http')
79-
->orWhere('attachment.path LIKE :https');
78+
->where('ILIKE(attachment.path, :http) = TRUE')
79+
->orWhere('ILIKE(attachment.path, :https) = TRUE');
8080
$qb->setParameter('http', 'http://%');
8181
$qb->setParameter('https', 'https://%');
8282
$query = $qb->getQuery();

src/Repository/ParameterRepository.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public function autocompleteParamName(string $name, bool $exact = false, int $ma
4444
->select('parameter.name')
4545
->addSelect('parameter.symbol')
4646
->addSelect('parameter.unit')
47-
->where('parameter.name LIKE :name');
47+
->where('ILIKE(parameter.name, :name) = TRUE');
4848
if ($exact) {
4949
$qb->setParameter('name', $name);
5050
} else {

src/Repository/PartRepository.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,10 @@ public function autocompleteSearch(string $query, int $max_limits = 50): array
8181
->leftJoin('part.category', 'category')
8282
->leftJoin('part.footprint', 'footprint')
8383

84-
->where('part.name LIKE :query')
85-
->orWhere('part.description LIKE :query')
86-
->orWhere('category.name LIKE :query')
87-
->orWhere('footprint.name LIKE :query')
84+
->where('ILIKE(part.name, :query) = TRUE')
85+
->orWhere('ILIKE(part.description, :query) = TRUE')
86+
->orWhere('ILIKE(category.name, :query) = TRUE')
87+
->orWhere('ILIKE(footprint.name, :query) = TRUE')
8888
;
8989

9090
$qb->setParameter('query', '%'.$query.'%');

0 commit comments

Comments
 (0)