2022-08-20 02:43:15 +02:00
|
|
|
<?php
|
2022-11-29 21:21:26 +01:00
|
|
|
/*
|
|
|
|
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
|
|
|
|
*
|
2023-12-05 23:50:07 +01:00
|
|
|
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
2022-11-29 21:21:26 +01:00
|
|
|
*
|
|
|
|
|
* 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/>.
|
|
|
|
|
*/
|
2023-12-05 23:50:07 +01:00
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
namespace App\Doctrine\Middleware;
|
2022-08-20 02:43:15 +02:00
|
|
|
|
Add SI-prefix-aware sorting column for parts tableFeature/si value sort (#1344)
* Add SI-prefix-aware sorting column for the parts table
Adds an optional "Name (SI)" column that parses numeric values with SI
prefixes (p, n, u/µ, m, k/K, M, G, T) from part names and sorts by the
resulting physical value. This is useful for electronic components where
alphabetical sorting produces wrong results — e.g. 100nF, 10pF, 1uF
should sort as 10pF < 100nF < 1uF.
Implementation:
- New SiValueSort DQL function with platform-specific SQL generation
for PostgreSQL (POSIX regex), MySQL/MariaDB (REGEXP_SUBSTR), and
SQLite (PHP callback registered via the existing middleware).
- The regex is start-anchored: only names beginning with a number are
matched. Part numbers like "MCP2515" or "Crystal 20MHz" are ignored.
- When SI sort is active, NATSORT is appended as a secondary sort so
that non-matching parts fall back to natural string ordering instead
of appearing in arbitrary order.
- The column is opt-in (not in default columns) and displays the parsed
float value, or an empty cell for non-matching names.
* Rename SI column from "Name (SI)" to "SI Value"
The column now shows the parsed numeric value rather than the part name,
so the label should reflect that.
* Support comma as decimal separator in SI value parsing
Part names using European decimal notation (e.g. "4,7 kΩ", "2,2uF")
were parsed incorrectly because the regex only recognized dots. Now
commas are normalized to dots before parsing, matching the existing
pattern used elsewhere in the codebase (PartNormalizer, price providers).
2026-04-15 22:56:34 +02:00
|
|
|
use App\Doctrine\Functions\SiValueSort;
|
2023-05-09 00:26:40 +02:00
|
|
|
use App\Exceptions\InvalidRegexException;
|
2023-12-05 23:50:07 +01:00
|
|
|
use Doctrine\DBAL\Driver\Connection;
|
|
|
|
|
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
|
2026-06-25 11:59:37 +02:00
|
|
|
use Pdo\Sqlite;
|
2022-08-20 02:43:15 +02:00
|
|
|
|
|
|
|
|
/**
|
2023-12-05 23:50:07 +01:00
|
|
|
* This middleware is used to add the regexp operator to the SQLite platform.
|
2022-08-20 02:43:15 +02:00
|
|
|
* As a PHP callback is called for every entry to compare it is most likely much slower than using regex on MySQL.
|
|
|
|
|
* But as regex is not often used, this should be fine for most use cases, also it is almost impossible to implement a better solution.
|
|
|
|
|
*/
|
2023-12-05 23:50:07 +01:00
|
|
|
class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware
|
2022-08-20 02:43:15 +02:00
|
|
|
{
|
2023-12-05 23:50:07 +01:00
|
|
|
public function connect(#[\SensitiveParameter] array $params): Connection
|
2022-08-20 02:43:15 +02:00
|
|
|
{
|
2023-12-05 23:50:07 +01:00
|
|
|
//Do connect process first
|
|
|
|
|
$connection = parent::connect($params); // TODO: Change the autogenerated stub
|
2022-08-20 02:43:15 +02:00
|
|
|
|
2023-12-05 23:50:07 +01:00
|
|
|
//Then add the functions if we are on SQLite
|
2024-06-09 23:28:46 +02:00
|
|
|
if ($params['driver'] === 'pdo_sqlite') {
|
2022-08-20 02:43:15 +02:00
|
|
|
$native_connection = $connection->getNativeConnection();
|
|
|
|
|
|
2024-12-28 22:31:04 +01:00
|
|
|
if($native_connection instanceof \PDO) {
|
2024-06-18 00:09:44 +02:00
|
|
|
|
2026-06-25 11:59:37 +02:00
|
|
|
//Use the new PDO::createFunction and PDO::createCollation methods if available (PHP 8.4+)
|
2026-06-25 12:09:06 +02:00
|
|
|
if (is_a($native_connection, Sqlite::class)) { #TODO: Remove this check when PHP 8.4 is the minimum requirement
|
2026-06-25 11:59:37 +02:00
|
|
|
$native_connection->createFunction('REGEXP', self::regexp(...), 2, Sqlite::DETERMINISTIC);
|
|
|
|
|
$native_connection->createFunction('FIELD', self::field(...), -1, Sqlite::DETERMINISTIC);
|
|
|
|
|
$native_connection->createFunction('FIELD2', self::field2(...), 2, Sqlite::DETERMINISTIC);
|
Add SI-prefix-aware sorting column for parts tableFeature/si value sort (#1344)
* Add SI-prefix-aware sorting column for the parts table
Adds an optional "Name (SI)" column that parses numeric values with SI
prefixes (p, n, u/µ, m, k/K, M, G, T) from part names and sorts by the
resulting physical value. This is useful for electronic components where
alphabetical sorting produces wrong results — e.g. 100nF, 10pF, 1uF
should sort as 10pF < 100nF < 1uF.
Implementation:
- New SiValueSort DQL function with platform-specific SQL generation
for PostgreSQL (POSIX regex), MySQL/MariaDB (REGEXP_SUBSTR), and
SQLite (PHP callback registered via the existing middleware).
- The regex is start-anchored: only names beginning with a number are
matched. Part numbers like "MCP2515" or "Crystal 20MHz" are ignored.
- When SI sort is active, NATSORT is appended as a secondary sort so
that non-matching parts fall back to natural string ordering instead
of appearing in arbitrary order.
- The column is opt-in (not in default columns) and displays the parsed
float value, or an empty cell for non-matching names.
* Rename SI column from "Name (SI)" to "SI Value"
The column now shows the parsed numeric value rather than the part name,
so the label should reflect that.
* Support comma as decimal separator in SI value parsing
Part names using European decimal notation (e.g. "4,7 kΩ", "2,2uF")
were parsed incorrectly because the regex only recognized dots. Now
commas are normalized to dots before parsing, matching the existing
pattern used elsewhere in the codebase (PartNormalizer, price providers).
2026-04-15 22:56:34 +02:00
|
|
|
|
2026-06-25 11:59:37 +02:00
|
|
|
//Create a new collation for natural sorting
|
|
|
|
|
$native_connection->createCollation('NATURAL_CMP', strnatcmp(...));
|
|
|
|
|
|
|
|
|
|
//Create a function for SI prefix value sorting
|
|
|
|
|
$native_connection->createFunction('SI_VALUE', SiValueSort::sqliteSiValue(...), 1, Sqlite::DETERMINISTIC);
|
|
|
|
|
} else {
|
|
|
|
|
$native_connection->sqliteCreateFunction('REGEXP', self::regexp(...), 2, \PDO::SQLITE_DETERMINISTIC);
|
|
|
|
|
$native_connection->sqliteCreateFunction('FIELD', self::field(...), -1, \PDO::SQLITE_DETERMINISTIC);
|
|
|
|
|
$native_connection->sqliteCreateFunction('FIELD2', self::field2(...), 2, \PDO::SQLITE_DETERMINISTIC);
|
|
|
|
|
|
|
|
|
|
//Create a new collation for natural sorting
|
|
|
|
|
$native_connection->sqliteCreateCollation('NATURAL_CMP', strnatcmp(...));
|
|
|
|
|
|
|
|
|
|
//Create a function for SI prefix value sorting
|
|
|
|
|
$native_connection->sqliteCreateFunction('SI_VALUE', SiValueSort::sqliteSiValue(...), 1, \PDO::SQLITE_DETERMINISTIC);
|
|
|
|
|
}
|
2022-08-20 02:43:15 +02:00
|
|
|
}
|
|
|
|
|
}
|
2023-12-05 23:50:07 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
return $connection;
|
2022-08-20 02:43:15 +02:00
|
|
|
}
|
2023-07-23 01:19:48 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* This function emulates the MySQL regexp function for SQLite
|
|
|
|
|
* @param string $pattern
|
|
|
|
|
* @param string $value
|
|
|
|
|
* @return int
|
|
|
|
|
*/
|
2023-12-05 23:51:54 +01:00
|
|
|
final public static function regexp(string $pattern, ?string $value): int
|
2023-07-23 01:19:48 +02:00
|
|
|
{
|
2023-12-05 23:51:54 +01:00
|
|
|
if ($value === null) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-23 01:19:48 +02:00
|
|
|
try {
|
|
|
|
|
return (mb_ereg($pattern, $value)) ? 1 : 0;
|
2023-07-26 23:39:53 +02:00
|
|
|
|
2023-07-23 01:19:48 +02:00
|
|
|
} catch (\ErrorException $e) {
|
|
|
|
|
throw InvalidRegexException::fromMBRegexError($e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-26 23:23:25 +02:00
|
|
|
/**
|
|
|
|
|
* Very similar to the field function, but takes the array values as a comma separated string.
|
|
|
|
|
* This is needed as SQLite has a pretty low argument count limit.
|
|
|
|
|
* @param string|int|null $value
|
|
|
|
|
* @param string $imploded_array
|
|
|
|
|
* @return int
|
|
|
|
|
*/
|
2023-07-26 23:39:53 +02:00
|
|
|
final public static function field2(string|int|null $value, string $imploded_array): int
|
2023-07-26 23:23:25 +02:00
|
|
|
{
|
|
|
|
|
$array = explode(',', $imploded_array);
|
2023-07-26 23:39:53 +02:00
|
|
|
return self::field($value, ...$array);
|
2023-07-26 23:23:25 +02:00
|
|
|
}
|
|
|
|
|
|
2023-07-23 01:19:48 +02:00
|
|
|
/**
|
|
|
|
|
* This function emulates the MySQL field function for SQLite
|
2023-07-26 23:39:53 +02:00
|
|
|
* This function returns the index (position) of the first argument in the subsequent arguments.
|
2023-07-23 01:19:48 +02:00
|
|
|
* If the first argument is not found or is NULL, 0 is returned.
|
|
|
|
|
* @param string|int|null $value
|
|
|
|
|
* @return int
|
|
|
|
|
*/
|
2024-06-22 00:31:43 +02:00
|
|
|
final public static function field(string|int|null $value, mixed ...$array): int
|
2023-07-23 01:19:48 +02:00
|
|
|
{
|
|
|
|
|
if ($value === null) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//We are loose with the types here
|
2023-07-23 01:20:38 +02:00
|
|
|
//@phpstan-ignore-next-line
|
2023-07-23 01:19:48 +02:00
|
|
|
$index = array_search($value, $array, false);
|
|
|
|
|
|
|
|
|
|
if ($index === false) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $index + 1;
|
|
|
|
|
}
|
2026-06-25 11:59:37 +02:00
|
|
|
}
|