Made it possible to sort on multiple columns in db:list. Cleaned up output API.

This commit is contained in:
Abdulmhsen B. A. A
2022-06-09 17:18:06 +03:00
parent f98685eaac
commit b34deed051
11 changed files with 180 additions and 290 deletions

View File

@@ -34,11 +34,19 @@ class Cli extends Application
}
$definition->addOption(
new InputOption('with-context', null, InputOption::VALUE_NEGATABLE, 'Add context to output messages.')
new InputOption('context', null, InputOption::VALUE_NEGATABLE, 'Add context to output messages.')
);
$definition->addOption(new InputOption('trace', null, InputOption::VALUE_NONE, 'Enable tracing mode.'));
$definition->addOption(
new InputOption(
'output', 'o', InputOption::VALUE_REQUIRED,
sprintf('Output mode. Can be [%s].', implode(', ', Command::DISPLAY_OUTPUT)),
Command::DISPLAY_OUTPUT[0]
)
);
return $definition;
}
}

View File

@@ -23,7 +23,7 @@ class Command extends BaseCommand
{
use LockableTrait;
protected array $outputs = [
public const DISPLAY_OUTPUT = [
'table',
'json',
'yaml',
@@ -31,11 +31,11 @@ class Command extends BaseCommand
protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($input->hasOption('with-context') && true === $input->getOption('with-context')) {
if ($input->hasOption('context') && true === $input->getOption('context')) {
Config::save('logs.context', true);
}
if ($input->hasOption('no-with-context') && true === $input->getOption('no-with-context')) {
if ($input->hasOption('no-context') && true === $input->getOption('no-context')) {
Config::save('logs.context', false);
}
@@ -249,5 +249,19 @@ class Command extends BaseCommand
$suggestions->suggestValues($suggest);
}
if ($input->mustSuggestOptionValuesFor('output')) {
$currentValue = $input->getCompletionValue();
$suggest = [];
foreach (self::DISPLAY_OUTPUT as $name) {
if (empty($currentValue) || str_starts_with($name, $currentValue)) {
$suggest[] = $name;
}
}
$suggestions->suggestValues($suggest);
}
}
}

View File

@@ -8,8 +8,6 @@ use App\Command;
use App\Libs\Config;
use App\Libs\Options;
use RuntimeException;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -22,13 +20,6 @@ final class ListCommand extends Command
{
$this->setName('backend:library:list')
->setDescription('Get Backend libraries list.')
->addOption(
'output',
'o',
InputOption::VALUE_OPTIONAL,
sprintf('Output mode. Can be [%s].', implode(', ', $this->outputs)),
$this->outputs[0],
)
->addOption('include-raw-response', null, InputOption::VALUE_NONE, 'Include unfiltered raw response.')
->addOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use Alternative config file.')
->addArgument('backend', InputArgument::REQUIRED, 'Backend name.');
@@ -102,29 +93,4 @@ final class ListCommand extends Command
return self::FAILURE;
}
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
parent::complete($input, $suggestions);
$methods = [
'output' => 'outputs',
];
foreach ($methods as $key => $of) {
if ($input->mustSuggestOptionValuesFor($key)) {
$currentValue = $input->getCompletionValue();
$suggest = [];
foreach ($this->{$of} as $name) {
if (empty($currentValue) || str_starts_with($name, $currentValue)) {
$suggest[] = $name;
}
}
$suggestions->suggestValues($suggest);
}
}
}
}

View File

@@ -30,16 +30,6 @@ final class MismatchCommand extends Command
->setDescription(
'Find possible mis-identified item in a library. This only works for Media that follow Plex naming format.'
)
->addOption(
'output',
'o',
InputOption::VALUE_OPTIONAL,
sprintf(
'Output mode. Can be [%s]. Modes other than table mode gives more info.',
implode(', ', $this->outputs)
),
$this->outputs[0],
)
->addOption('percentage', 'p', InputOption::VALUE_OPTIONAL, 'Acceptable percentage.', 50.0)
->addOption(
'method',
@@ -269,7 +259,6 @@ final class MismatchCommand extends Command
parent::complete($input, $suggestions);
$methods = [
'output' => 'outputs',
'method' => 'methods',
];

View File

@@ -8,8 +8,6 @@ use App\Command;
use App\Libs\Config;
use App\Libs\Options;
use RuntimeException;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -24,16 +22,6 @@ final class UnmatchedCommand extends Command
$this->setName('backend:library:unmatched')
->setDescription('Find top level Items in library that has no external ids.')
->addOption('show-all', null, InputOption::VALUE_NONE, 'Show all items regardless of the match status.')
->addOption(
'output',
'o',
InputOption::VALUE_OPTIONAL,
sprintf(
'Output mode. Can be [%s]. Modes other than table mode gives more info.',
implode(', ', $this->outputs)
),
$this->outputs[0],
)
->addOption(
'timeout',
null,
@@ -151,29 +139,4 @@ final class UnmatchedCommand extends Command
return self::SUCCESS;
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
parent::complete($input, $suggestions);
$methods = [
'output' => 'outputs',
];
foreach ($methods as $key => $of) {
if ($input->mustSuggestOptionValuesFor($key)) {
$currentValue = $input->getCompletionValue();
$suggest = [];
foreach ($this->{$of} as $name) {
if (empty($currentValue) || str_starts_with($name, $currentValue)) {
$suggest[] = $name;
}
}
$suggestions->suggestValues($suggest);
}
}
}
}

View File

@@ -8,8 +8,6 @@ use App\Command;
use App\Libs\Config;
use App\Libs\Options;
use RuntimeException;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -22,13 +20,6 @@ final class IdCommand extends Command
{
$this->setName('backend:search:id')
->setDescription('Get backend metadata related to specific id.')
->addOption(
'output',
'o',
InputOption::VALUE_OPTIONAL,
sprintf('Output mode. Can be [%s].', implode(', ', $this->outputs)),
$this->outputs[0],
)
->addOption('include-raw-response', null, InputOption::VALUE_NONE, 'Include unfiltered raw response.')
->addOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use Alternative config file.')
->addArgument('backend', InputArgument::REQUIRED, 'Backend name.')
@@ -89,29 +80,4 @@ final class IdCommand extends Command
return self::FAILURE;
}
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
parent::complete($input, $suggestions);
$methods = [
'output' => 'outputs',
];
foreach ($methods as $key => $of) {
if ($input->mustSuggestOptionValuesFor($key)) {
$currentValue = $input->getCompletionValue();
$suggest = [];
foreach ($this->{$of} as $name) {
if (empty($currentValue) || str_starts_with($name, $currentValue)) {
$suggest[] = $name;
}
}
$suggestions->suggestValues($suggest);
}
}
}
}

View File

@@ -8,8 +8,6 @@ use App\Command;
use App\Libs\Config;
use App\Libs\Options;
use RuntimeException;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -22,13 +20,6 @@ final class QueryCommand extends Command
{
$this->setName('backend:search:query')
->setDescription('Search backend libraries for specific title keyword.')
->addOption(
'output',
'o',
InputOption::VALUE_OPTIONAL,
sprintf('Output mode. Can be [%s].', implode(', ', $this->outputs)),
$this->outputs[0],
)
->addOption('include-raw-response', null, InputOption::VALUE_NONE, 'Include unfiltered raw response.')
->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Limit returned results.', 25)
->addOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use Alternative config file.')
@@ -94,29 +85,4 @@ final class QueryCommand extends Command
return self::FAILURE;
}
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
parent::complete($input, $suggestions);
$methods = [
'output' => 'outputs',
];
foreach ($methods as $key => $of) {
if ($input->mustSuggestOptionValuesFor($key)) {
$currentValue = $input->getCompletionValue();
$suggest = [];
foreach ($this->{$of} as $name) {
if (empty($currentValue) || str_starts_with($name, $currentValue)) {
$suggest[] = $name;
}
}
$suggestions->suggestValues($suggest);
}
}
}
}

View File

@@ -8,8 +8,6 @@ use App\Command;
use App\Libs\Config;
use App\Libs\Options;
use RuntimeException;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -23,13 +21,6 @@ final class ListCommand extends Command
{
$this->setName('backend:users:list')
->setDescription('Get backend users list.')
->addOption(
'output',
'o',
InputOption::VALUE_OPTIONAL,
sprintf('Output mode. Can be [%s].', implode(', ', $this->outputs)),
$this->outputs[0],
)
->addOption('with-tokens', 't', InputOption::VALUE_NONE, 'Include access tokens in response.')
->addOption('include-raw-response', null, InputOption::VALUE_NONE, 'Include unfiltered raw response.')
->addOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use Alternative config file.')
@@ -108,29 +99,4 @@ final class ListCommand extends Command
return self::FAILURE;
}
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
parent::complete($input, $suggestions);
$methods = [
'output' => 'outputs',
];
foreach ($methods as $key => $of) {
if ($input->mustSuggestOptionValuesFor($key)) {
$currentValue = $input->getCompletionValue();
$suggest = [];
foreach ($this->{$of} as $name) {
if (empty($currentValue) || str_starts_with($name, $currentValue)) {
$suggest[] = $name;
}
}
$suggestions->suggestValues($suggest);
}
}
}
}

View File

@@ -24,7 +24,7 @@ use Symfony\Component\Yaml\Yaml;
final class ListCommand extends Command
{
public const CHANGEABLE_COLUMNS = [
private const COLUMNS_CHANGEABLE = [
iFace::COLUMN_WATCHED,
iFace::COLUMN_VIA,
iFace::COLUMN_TITLE,
@@ -34,6 +34,18 @@ final class ListCommand extends Command
iFace::COLUMN_UPDATED,
];
private const COLUMNS_SORTABLE = [
iFace::COLUMN_ID,
iFace::COLUMN_TYPE,
iFace::COLUMN_UPDATED,
iFace::COLUMN_WATCHED,
iFace::COLUMN_VIA,
iFace::COLUMN_TITLE,
iFace::COLUMN_YEAR,
iFace::COLUMN_SEASON,
iFace::COLUMN_EPISODE,
];
private PDO $pdo;
public function __construct(private StorageInterface $storage)
@@ -51,9 +63,8 @@ final class ListCommand extends Command
'via',
null,
InputOption::VALUE_REQUIRED,
'Limit results to this specified server. This filter is not reliable. and changes based on last server query.'
'Limit results to this specified backend. This filter is not reliable. and changes based on last backend query.'
)
->addOption('output', null, InputOption::VALUE_REQUIRED, 'Display output as [json, yaml, table]', 'table')
->addOption(
'type',
null,
@@ -65,14 +76,17 @@ final class ListCommand extends Command
->addOption('episode', null, InputOption::VALUE_REQUIRED, 'Select episode number')
->addOption('year', null, InputOption::VALUE_REQUIRED, 'Select year.')
->addOption('id', null, InputOption::VALUE_REQUIRED, 'Select db record number')
->addOption('sort', null, InputOption::VALUE_REQUIRED, 'sort order by [id, updated]', 'updated')
->addOption('asc', null, InputOption::VALUE_NONE, 'Sort records in ascending order.')
->addOption('desc', null, InputOption::VALUE_NONE, 'Sort records in descending order. (Default)')
->addOption(
'sort',
null,
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
'Set sort by columns. for example, <comment>--sort season:asc --sort episode:desc</comment>',
)
->addOption(
'metadata-as',
null,
InputOption::VALUE_REQUIRED,
'Display metadata from this server instead of latest.'
'Display metadata from this backend instead of latest.'
)
->setDescription('List Database entries.');
@@ -93,13 +107,19 @@ final class ListCommand extends Command
'metadata',
null,
InputOption::VALUE_NONE,
'Search in (metadata) provided by servers JSON Field using (--key, --value) options.'
'Search in (metadata) provided by backends JSON Field using (--key, --value) options.'
)
->addOption(
'extra',
null,
InputOption::VALUE_NONE,
'Search in (extra information) JSON Field using (--key, --value) options.'
)
->addOption(
'dump-query',
null,
InputOption::VALUE_NONE,
'Dump the generated query and exit.'
);
}
@@ -108,35 +128,35 @@ final class ListCommand extends Command
*/
protected function runCommand(InputInterface $input, OutputInterface $output): int
{
$list = [];
$limit = (int)$input->getOption('limit');
$es = fn(string $val) => $this->storage->identifier($val);
$params = [
'limit' => $limit <= 0 ? 20 : $limit,
];
$where = [];
$sql = $where = [];
$sql = "SELECT * FROM state ";
$sql[] = sprintf('SELECT * FROM %s', $es('state'));
if ($input->getOption('id')) {
$where[] = iFace::COLUMN_ID . ' = :id';
$where[] = $es(iFace::COLUMN_ID) . ' = :id';
$params['id'] = $input->getOption('id');
}
if ($input->getOption('via')) {
$where[] = iFace::COLUMN_VIA . ' = :via';
$where[] = $es(iFace::COLUMN_VIA) . ' = :via';
$params['via'] = $input->getOption('via');
}
if ($input->getOption('year')) {
$where[] = iFace::COLUMN_YEAR . ' = :year';
$where[] = $es(iFace::COLUMN_YEAR) . ' = :year';
$params['year'] = $input->getOption('year');
}
if ($input->getOption('type')) {
$where[] = iFace::COLUMN_TYPE . ' = :type';
$where[] = $es(iFace::COLUMN_TYPE) . ' = :type';
$params['type'] = match ($input->getOption('type')) {
iFace::TYPE_MOVIE => iFace::TYPE_MOVIE,
default => iFace::TYPE_EPISODE,
@@ -144,17 +164,17 @@ final class ListCommand extends Command
}
if ($input->getOption('title')) {
$where[] = iFace::COLUMN_TITLE . " LIKE '%' || :title || '%'";
$where[] = $es(iFace::COLUMN_TITLE) . ' LIKE "%" || :title || "%"';
$params['title'] = $input->getOption('title');
}
if (null !== $input->getOption('season')) {
$where[] = iFace::COLUMN_SEASON . ' = :season';
$where[] = $es(iFace::COLUMN_SEASON) . ' = :season';
$params['season'] = $input->getOption('season');
}
if (null !== $input->getOption('episode')) {
$where[] = iFace::COLUMN_EPISODE . ' = :episode';
$where[] = $es(iFace::COLUMN_EPISODE) . ' = :episode';
$params['episode'] = $input->getOption('episode');
}
@@ -203,20 +223,53 @@ final class ListCommand extends Command
}
if (count($where) >= 1) {
$sql .= 'WHERE ' . implode(' AND ', $where);
$sql[] = 'WHERE ' . implode(' AND ', $where);
}
$sort = match ($input->getOption('sort')) {
'id' => iFace::COLUMN_ID,
'season' => iFace::COLUMN_SEASON,
'episode' => iFace::COLUMN_EPISODE,
'type' => iFace::COLUMN_TYPE,
default => iFace::COLUMN_UPDATED,
};
$sorts = [];
$sortOrder = ($input->getOption('asc')) ? 'ASC' : 'DESC';
foreach ($input->getOption('sort') as $sort) {
if (1 !== preg_match('/(?P<field>\w+)(:(?P<dir>\w+))?/', $sort, $matches)) {
continue;
}
$sql .= " ORDER BY {$sort} {$sortOrder} LIMIT :limit";
if (null === ($matches['field'] ?? null) || false === in_array($matches['field'], self::COLUMNS_SORTABLE)) {
continue;
}
$sorts[] = sprintf(
'%s %s',
$es($matches['field']),
match (strtolower($matches['dir'] ?? 'desc')) {
default => 'DESC',
'asc' => 'ASC',
}
);
}
if (count($sorts) < 1) {
$sorts[] = sprintf('%s DESC', $es('updated'));
}
$sql[] = 'ORDER BY ' . implode(', ', $sorts) . ' LIMIT :limit';
$sql = implode(' ', $sql);
if ($input->getOption('dump-query')) {
$arr = [
'query' => $sql,
'parameters' => $params,
'raw' => $this->storage->getRawSQLString($sql, $params),
];
if ('table' === $input->getOption('output')) {
$arr['parameters'] = arrayToString($params);
unset($arr['raw']);
$arr = [$arr];
}
$this->displayContent($arr, $output, $input->getOption('output'));
return self::SUCCESS;
}
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
@@ -262,15 +315,16 @@ final class ListCommand extends Command
if (null !== ($via = $input->getOption('metadata-as'))) {
$path = $row[iFace::COLUMN_META_DATA][$via] ?? [];
foreach (self::CHANGEABLE_COLUMNS as $column) {
foreach (self::COLUMNS_CHANGEABLE as $column) {
if (null === ($path[$column] ?? null)) {
continue;
}
$row[$column] = 'int' === get_debug_type($row[$column]) ? (int)$path[$column] : $path[$column];
}
if (null !== ($row[iFace::COLUMN_EXTRA][$via][iFace::COLUMN_EXTRA_DATE] ?? null)) {
$row[iFace::COLUMN_UPDATED] = $row[iFace::COLUMN_EXTRA][$via][iFace::COLUMN_EXTRA_DATE];
if (null !== ($dateFromBackend = $path[iFace::COLUMN_META_DATA_PLAYED_AT] ?? $path[iFace::COLUMN_META_DATA_ADDED_AT] ?? null)) {
$row[iFace::COLUMN_UPDATED] = $dateFromBackend;
}
}
@@ -280,44 +334,37 @@ final class ListCommand extends Command
unset($row);
if ('json' === $input->getOption('output')) {
$output->writeln(
json_encode(
1 === count($rows) ? $rows[0] : $rows,
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
)
);
} elseif ('yaml' === $input->getOption('output')) {
$output->writeln(Yaml::dump(1 === count($rows) ? $rows[0] : $rows, 8, 2));
} else {
$x = 0;
if ('table' === $input->getOption('output')) {
$list = [];
foreach ($rows as $row) {
$row[iFace::COLUMN_UPDATED] = $row[iFace::COLUMN_UPDATED]->getTimestamp();
$row[iFace::COLUMN_WATCHED] = (int)$row[iFace::COLUMN_WATCHED];
$entity = Container::get(iFace::class)->fromArray($row);
$x++;
$list[] = [
$entity->id,
ucfirst($entity->type),
$entity->getName(),
$entity->via ?? '??',
makeDate($entity->updated)->format('Y-m-d H:i:s T'),
$entity->isWatched() ? 'Yes' : 'No',
ag($entity->extra[$entity->via] ?? [], iFace::COLUMN_EXTRA_EVENT, '-'),
$item = [
'id' => $entity->id,
'Type' => ucfirst($entity->type),
'Title' => $entity->getName(),
'Via (Last)' => $entity->via ?? '??',
'Date' => makeDate($entity->updated)->format('Y-m-d H:i:s T'),
'Played' => $entity->isWatched() ? 'Yes' : 'No',
'Via (Event)' => ag($entity->extra[$entity->via] ?? [], iFace::COLUMN_EXTRA_EVENT, '-'),
];
if ($x < $rowCount) {
$list[] = new TableSeparator();
}
$list[] = $item;
$list[] = new TableSeparator();
}
$rows = null;
(new Table($output))->setHeaders(['Id', 'Type', 'Title', 'Via (Last)', 'Date', 'Played', 'Via Event'])
->setStyle('box')->setRows($list)->render();
if (count($list) >= 2) {
array_pop($list);
}
(new Table($output))->setHeaders(array_keys($list[0] ?? []))->setStyle('box')->setRows($list)->render();
} else {
$this->displayContent($rows, $output, $input->getOption('output'));
}
return self::SUCCESS;
@@ -355,14 +402,16 @@ final class ListCommand extends Command
$suggestions->suggestValues($suggest);
}
if ($input->mustSuggestOptionValuesFor('output')) {
if ($input->mustSuggestOptionValuesFor('sort')) {
$currentValue = $input->getCompletionValue();
$suggest = [];
foreach (['json', 'yaml', 'table'] as $name) {
if (empty($currentValue) || str_starts_with($name, $currentValue)) {
$suggest[] = $name;
foreach (self::COLUMNS_SORTABLE as $name) {
foreach ([$name . ':desc', $name . ':asc'] as $subName) {
if (empty($currentValue) || true === str_starts_with($subName, $currentValue)) {
$suggest[] = $subName;
}
}
}

View File

@@ -5,10 +5,7 @@ declare(strict_types=1);
namespace App\Commands\System;
use App\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
final class EnvCommand extends Command
@@ -16,13 +13,6 @@ final class EnvCommand extends Command
protected function configure(): void
{
$this->setName('system:env')
->addOption(
'output',
'o',
InputOption::VALUE_OPTIONAL,
sprintf('Output mode. Can be [%s].', implode(', ', $this->outputs)),
$this->outputs[0],
)
->setDescription('Dump loaded environment variables.');
}
@@ -53,30 +43,4 @@ final class EnvCommand extends Command
return self::SUCCESS;
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
parent::complete($input, $suggestions);
$methods = [
'output' => 'outputs',
];
foreach ($methods as $key => $of) {
if ($input->mustSuggestOptionValuesFor($key)) {
$currentValue = $input->getCompletionValue();
$suggest = [];
foreach ($this->{$of} as $name) {
if (empty($currentValue) || str_starts_with($name, $currentValue)) {
$suggest[] = $name;
}
}
$suggestions->suggestValues($suggest);
}
}
}
}

View File

@@ -33,8 +33,15 @@ final class PDOAdapter implements StorageInterface
'update' => null,
];
private string $driver = 'sqlite';
public function __construct(private LoggerInterface $logger, private PDO $pdo)
{
$driver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
if (is_string($driver)) {
$this->driver = $driver;
}
}
public function setOptions(array $options): self
@@ -515,13 +522,45 @@ final class PDOAdapter implements StorageInterface
*/
public function getRawSQLString(string $sql, array $parameters): string
{
$keys = $replace = [];
$replacer = [];
foreach ($parameters as $key => $val) {
$keys[] = '/(\:' . preg_quote($key, '/') . ')(?:\b|\,)/';
$replace[] = ctype_digit((string)$val) ? $val : "'{$val}'";
$replacer['/(\:' . preg_quote($key, '/') . ')(?:\b|\,)/'] = ctype_digit(
(string)$val
) ? (int)$val : '"' . $val . '"';
}
return preg_replace($keys, $replace, $sql);
return preg_replace(array_keys($replacer), array_values($replacer), $sql);
}
public function identifier(string $text, bool $quote = true): string
{
// table or column has to be valid ASCII name.
// this is opinionated, but we only allow [a-zA-Z0-9_] in column/table name.
if (!\preg_match('#\w#', $text)) {
throw new \RuntimeException(
sprintf(
'Invalid identifier "%s": Column/table must be valid ASCII code.',
$text
)
);
}
// The first character cannot be [0-9]:
if (\preg_match('/^\d/', $text)) {
throw new \RuntimeException(
sprintf(
'Invalid identifier "%s": Must begin with a letter or underscore.',
$text
)
);
}
return !$quote ? $text : match ($this->driver) {
'mssql' => '[' . $text . ']',
'mysql' => '`' . $text . '`',
default => '"' . $text . '"',
};
}
}