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
This commit is contained in:
Jan Böhmer 2024-12-02 00:17:54 +01:00
parent b1ba26e0b9
commit e223078af9
11 changed files with 94 additions and 22 deletions

View file

@ -57,6 +57,7 @@ doctrine:
field2: App\Doctrine\Functions\Field2
natsort: App\Doctrine\Functions\Natsort
array_position: App\Doctrine\Functions\ArrayPosition
ilike: App\Doctrine\Functions\ILike
when@test:
doctrine:

View file

@ -50,7 +50,7 @@ final class LikeFilter extends AbstractFilter
}
$parameterName = $queryNameGenerator->generateParameterName($property); // Generate a unique parameter name to avoid collisions with other filters
$queryBuilder
->andWhere(sprintf('o.%s LIKE :%s', $property, $parameterName))
->andWhere(sprintf('ILIKE(o.%s, :%s) = TRUE', $property, $parameterName))
->setParameter($parameterName, $value);
}

View file

@ -61,10 +61,10 @@ final class TagFilter extends AbstractFilter
$expr = $queryBuilder->expr();
$tmp = $expr->orX(
$expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_1'),
$expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_2'),
$expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_3'),
$expr->eq('o.'.$property, ':' . $tag_identifier_prefix . '_4'),
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_1) = TRUE',
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_2) = TRUE',
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_3) = TRUE',
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_4) = TRUE',
);
$queryBuilder->andWhere($tmp);

View file

@ -93,10 +93,10 @@ class TagsConstraint extends AbstractConstraint
$expr = $queryBuilder->expr();
$tmp = $expr->orX(
$expr->like($this->property, ':' . $tag_identifier_prefix . '_1'),
$expr->like($this->property, ':' . $tag_identifier_prefix . '_2'),
$expr->like($this->property, ':' . $tag_identifier_prefix . '_3'),
$expr->eq($this->property, ':' . $tag_identifier_prefix . '_4'),
'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_1) = TRUE',
'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_2) = TRUE',
'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_3) = TRUE',
'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_4) = TRUE',
);
//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)

View file

@ -107,7 +107,8 @@ class TextConstraint extends AbstractConstraint
}
if ($like_value !== null) {
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'LIKE', $like_value);
$queryBuilder->andWhere(sprintf('ILIKE(%s, :%s) = TRUE', $this->property, $this->identifier));
$queryBuilder->setParameter($this->identifier, $like_value);
return;
}

View file

@ -21,7 +21,6 @@ declare(strict_types=1);
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\DataTables\Filters;
use Doctrine\ORM\QueryBuilder;
class PartSearchFilter implements FilterInterface
@ -132,15 +131,15 @@ class PartSearchFilter implements FilterInterface
return sprintf("REGEXP(%s, :search_query) = TRUE", $field);
}
return sprintf("%s LIKE :search_query", $field);
return sprintf("ILIKE(%s, :search_query) = TRUE", $field);
}, $fields_to_search);
//Add Or concatation of the expressions to our query
//Add Or concatenation of the expressions to our query
$queryBuilder->andWhere(
$queryBuilder->expr()->orX(...$expressions)
);
//For regex we pass the query as is, for like we add % to the start and end as wildcards
//For regex, we pass the query as is, for like we add % to the start and end as wildcards
if ($this->regex) {
$queryBuilder->setParameter('search_query', $this->keyword);
} else {

View file

@ -0,0 +1,71 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Doctrine\Functions;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Platforms\SQLitePlatform;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\TokenType;
/**
* A platform invariant version of the case-insensitive LIKE operation.
* On MySQL and SQLite this is the normal LIKE, but on PostgreSQL it is the ILIKE operator.
*/
class ILike extends FunctionNode
{
public $value = null;
public $expr = null;
public function parse(Parser $parser): void
{
$parser->match(TokenType::T_IDENTIFIER);
$parser->match(TokenType::T_OPEN_PARENTHESIS);
$this->value = $parser->StringPrimary();
$parser->match(TokenType::T_COMMA);
$this->expr = $parser->StringExpression();
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
}
public function getSql(SqlWalker $sqlWalker): string
{
$platform = $sqlWalker->getConnection()->getDatabasePlatform();
//
if ($platform instanceof AbstractMySQLPlatform || $platform instanceof SQLitePlatform) {
$operator = 'LIKE';
} elseif ($platform instanceof PostgreSQLPlatform) {
//Use the case-insensitive operator, to have the same behavior as MySQL
$operator = 'ILIKE';
} else {
throw new \RuntimeException('Platform ' . gettype($platform) . ' does not support case insensitive like expressions.');
}
return '(' . $this->value->dispatch($sqlWalker) . ' ' . $operator . ' ' . $this->expr->dispatch($sqlWalker) . ')';
}
}

View file

@ -75,8 +75,8 @@ class AttachmentRepository extends DBElementRepository
{
$qb = $this->createQueryBuilder('attachment');
$qb->select('COUNT(attachment)')
->where('attachment.path LIKE :http')
->orWhere('attachment.path LIKE :https');
->where('ILIKE(attachment.path, :http) = TRUE')
->orWhere('ILIKE(attachment.path, :https) = TRUE');
$qb->setParameter('http', 'http://%');
$qb->setParameter('https', 'https://%');
$query = $qb->getQuery();

View file

@ -44,7 +44,7 @@ class ParameterRepository extends DBElementRepository
->select('parameter.name')
->addSelect('parameter.symbol')
->addSelect('parameter.unit')
->where('parameter.name LIKE :name');
->where('ILIKE(parameter.name, :name) = TRUE');
if ($exact) {
$qb->setParameter('name', $name);
} else {

View file

@ -81,10 +81,10 @@ class PartRepository extends NamedDBElementRepository
->leftJoin('part.category', 'category')
->leftJoin('part.footprint', 'footprint')
->where('part.name LIKE :query')
->orWhere('part.description LIKE :query')
->orWhere('category.name LIKE :query')
->orWhere('footprint.name LIKE :query')
->where('ILIKE(part.name, :query) = TRUE')
->orWhere('ILIKE(part.description, :query) = TRUE')
->orWhere('ILIKE(category.name, :query) = TRUE')
->orWhere('ILIKE(footprint.name, :query) = TRUE')
;
$qb->setParameter('query', '%'.$query.'%');

View file

@ -66,7 +66,7 @@ class TagFinder
$qb->select('p.tags')
->from(Part::class, 'p')
->where('p.tags LIKE ?1')
->where('ILIKE(p.tags, ?1) = TRUE')
->setMaxResults($options['query_limit'])
//->orderBy('RAND()')
->setParameter(1, '%'.$keyword.'%');