fix(migration): 支持复合索引与前缀索引同步迁移

This commit is contained in:
Anyon 2026-04-02 12:55:15 +08:00
parent 9ff9895449
commit 4e3c460694
3 changed files with 160 additions and 32 deletions

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/**
* +----------------------------------------------------------------------
* | ThinkAdmin Plugin for ThinkAdmin
* +----------------------------------------------------------------------
* | 版权所有 2014~2026 ThinkAdmin [ thinkadmin.top ]
* +----------------------------------------------------------------------
* | 官方网站: https://thinkadmin.top
* +----------------------------------------------------------------------
* | 开源协议 ( https://mit-license.org )
* | 免责声明 ( https://thinkadmin.top/disclaimer )
* | 会员特权 ( https://thinkadmin.top/vip-introduce )
* +----------------------------------------------------------------------
* | gitee 代码仓库https://gitee.com/zoujingli/ThinkAdmin
* | github 代码仓库https://github.com/zoujingli/ThinkAdmin
* +----------------------------------------------------------------------
*/
namespace think\admin\extend;
final class IndexNameService
{
/**
* 生成符合长度限制的索引名称,支持单列与复合索引。
* @param array<int, string>|string $columns
*/
public static function generate(string $table, array|string $columns, bool $unique = false): string
{
$columns = is_array($columns) ? $columns : [$columns];
$columns = array_values(array_filter(array_map(static function ($column): string {
return trim((string)$column);
}, $columns), 'strlen'));
$abbr = implode('', array_map(static function (string $word): string {
return $word[0] ?? '';
}, array_values(array_filter(explode('_', $table), 'strlen'))));
$prefix = $unique ? 'uni_' : 'idx_';
$tableHash = substr(md5($table), -4);
$firstColumn = $columns[0] ?? 'col';
$columnsKey = implode(',', $columns);
if (count($columns) <= 1) {
$candidate = "{$prefix}{$abbr}_{$tableHash}_{$firstColumn}";
} else {
$candidate = "{$prefix}{$abbr}_{$tableHash}_{$firstColumn}_" . substr(md5($columnsKey), 0, 8);
}
if (strlen($candidate) <= 64) {
return $candidate;
}
$hash = substr(md5($table . '|' . $columnsKey . '|' . ($unique ? '1' : '0')), 0, 16);
return "{$prefix}{$abbr}_{$tableHash}_{$hash}";
}
}

View File

@ -97,10 +97,12 @@ class PhinxExtend
} }
} }
// 生成索引规则 // 生成索引规则
foreach ($indexs as $field) { foreach ($indexs as $spec) {
if (empty($isExists) || !$table->hasIndex($field)) { [$columns, $options] = self::parseIndexSpec($table->getName(), $spec);
$table->addIndex($field, ['name' => self::genIndexName($table->getName(), $field)]); if (empty($columns) || (!empty($isExists) && $table->hasIndex($columns))) {
continue;
} }
$table->addIndex($columns, $options);
} }
$isExists ? $table->update() : $table->create(); $isExists ? $table->update() : $table->create();
if ($table->hasColumn('id')) { if ($table->hasColumn('id')) {
@ -216,17 +218,38 @@ class PhinxExtend
* 缩写规则: 取每个下划线分隔部分的第一个字母 * 缩写规则: 取每个下划线分隔部分的第一个字母
* *
* @param string $table 表名 * @param string $table 表名
* @param string $name 字段名 * @param array<int, string>|string $name 字段名
* @return string 生成的索引名称 * @return string 生成的索引名称
*/ */
private static function genIndexName(string $table, string $name): string private static function genIndexName(string $table, array|string $name, bool $unique = false): string
{ {
$getInitials = function (string $str): string { return IndexNameService::generate($table, $name, $unique);
return implode('', array_map(function ($part) { }
return $part[0] ?? '';
}, explode('_', $str))); /**
}; * @return array{0:array<int, string>,1:array<string, mixed>}
return sprintf('idx_%s_%s_%s', substr(md5($table), -4), $getInitials($table), $name); */
private static function parseIndexSpec(string $table, mixed $spec): array
{
if (is_string($spec)) {
$columns = [$spec];
return [$columns, ['name' => self::genIndexName($table, $columns)]];
}
if (is_array($spec) && array_is_list($spec)) {
$columns = array_values(array_filter($spec, 'is_string'));
return [$columns, ['name' => self::genIndexName($table, $columns)]];
}
if (is_array($spec)) {
$columns = array_values(array_filter((array)($spec['columns'] ?? []), 'is_string'));
$unique = !empty($spec['unique']);
$options = array_diff_key($spec, ['columns' => true]);
$options['name'] = $options['name'] ?? self::genIndexName($table, $columns, $unique);
return [$columns, $options];
}
return [[], []];
} }
/** /**
@ -343,16 +366,50 @@ CODE;
// 生成索引内容 // 生成索引内容
$_indexs = []; $_indexs = [];
foreach (Library::$sapp->db->connect()->query("show index from {$table}") as $index) { foreach (Library::$sapp->db->connect()->query("show index from {$table}") as $index) {
$index['Key_name'] !== 'PRIMARY' && $_indexs[] = $index['Column_name']; $keyName = strval($index['Key_name'] ?? '');
if ($keyName === '' || $keyName === 'PRIMARY') {
continue;
}
$_indexs[$keyName]['unique'] = intval($index['Non_unique'] ?? 1) === 0;
$column = strval($index['Column_name'] ?? '');
$_indexs[$keyName]['columns'][intval($index['Seq_in_index'] ?? 0)] = $column;
if (is_numeric($index['Sub_part'] ?? null) && intval($index['Sub_part']) > 0) {
$_indexs[$keyName]['limits'][$column] = intval($index['Sub_part']);
}
} }
usort($_indexs, function ($a, $b) { ksort($_indexs);
return strlen($a) <=> strlen($b); $_indexSpecs = [];
});
$_indexString = '[' . PHP_EOL . "\t\t\t";
foreach ($_indexs as $index) { foreach ($_indexs as $index) {
$_indexString .= "'{$index}', "; $columns = $index['columns'] ?? [];
ksort($columns);
$columns = array_values(array_filter($columns, 'strlen'));
if (empty($columns)) {
continue;
}
$options = [];
if (!empty($index['limits'])) {
$limits = [];
foreach ($columns as $column) {
if (isset($index['limits'][$column])) {
$limits[$column] = intval($index['limits'][$column]);
}
}
if (!empty($limits)) {
$options['limit'] = $limits;
}
}
if (!empty($index['unique'])) {
$options['unique'] = true;
}
if (count($columns) === 1 && empty($options)) {
$_indexSpecs[] = $columns[0];
} elseif (count($columns) > 1 && empty($options)) {
$_indexSpecs[] = $columns;
} else {
$_indexSpecs[] = array_merge(['columns' => $columns], $options);
}
} }
$_indexString .= PHP_EOL . "\t\t]"; $_indexString = self::_arr2str($_indexSpecs);
$content = str_replace(['_FIELDS_', '_INDEXS_', '__FORCE__'], [$_fieldString, $_indexString, $force ? 'true' : 'false'], $content) . PHP_EOL . PHP_EOL; $content = str_replace(['_FIELDS_', '_INDEXS_', '__FORCE__'], [$_fieldString, $_indexString, $force ? 'true' : 'false'], $content) . PHP_EOL . PHP_EOL;
} }
return $rehtml ? $content : highlight_string($content, true); return $rehtml ? $content : highlight_string($content, true);

View File

@ -20,6 +20,7 @@ declare(strict_types=1);
namespace plugin\helper; namespace plugin\helper;
use think\admin\extend\IndexNameService;
use think\admin\service\SystemService; use think\admin\service\SystemService;
use think\console\Command; use think\console\Command;
use think\facade\Db; use think\facade\Db;
@ -50,36 +51,48 @@ class DbIndexStruct extends Command
*/ */
public function handle(): void public function handle(): void
{ {
[$tables, $total, $count] = SystemService::getTables(); [$tables, $total] = SystemService::getTables();
$number = 1;
$renamed = 0;
foreach ($tables as $table) { foreach ($tables as $table) {
$this->output->writeln(sprintf("[%s/%s] 开始处理表 {$table}", $count++, $total)); $this->output->writeln(sprintf("[%s/%s] 开始处理表 %s", $number++, $total, $table));
$indexes = [];
foreach (Db::query(sprintf('SHOW INDEX FROM `%s`', $table)) as $index) { foreach (Db::query(sprintf('SHOW INDEX FROM `%s`', $table)) as $index) {
$keyName = $index['Key_name'] ?? ''; $keyName = strval($index['Key_name'] ?? '');
if ($keyName === 'PRIMARY') { if ($keyName === '' || $keyName === 'PRIMARY') {
continue; continue;
} }
$newName = $this->genIndexName($table, (array)$index); $indexes[$keyName]['unique'] = intval($index['Non_unique'] ?? 1) === 0;
if ($keyName === $newName) { $indexes[$keyName]['columns'][intval($index['Seq_in_index'] ?? 0)] = strval($index['Column_name'] ?? '');
}
$exists = array_fill_keys(array_keys($indexes), true);
foreach ($indexes as $keyName => $index) {
$columns = $index['columns'] ?? [];
ksort($columns);
$columns = array_values(array_filter($columns, 'strlen'));
if (empty($columns)) {
continue;
}
$newName = $this->genIndexName($table, $columns, !empty($index['unique']));
if ($keyName === $newName || isset($exists[$newName])) {
continue; continue;
} }
Db::execute(sprintf('ALTER TABLE `%s` RENAME INDEX `%s` TO `%s`', $table, $keyName, $newName)); Db::execute(sprintf('ALTER TABLE `%s` RENAME INDEX `%s` TO `%s`', $table, $keyName, $newName));
++$count; $exists[$newName] = true;
++$renamed;
} }
} }
$this->output->writeln("✅ 完成 {$count} 个索引重命名"); $this->output->writeln("✅ 完成 {$renamed} 个索引重命名");
} }
/** /**
* 生成索引名称. * 生成索引名称.
* @param string $table 表名 * @param string $table 表名
* @param array $index 索引信息 * @param array<int, string>|string $columns 索引字段
* @return string 生成的索引名称 * @return string 生成的索引名称
*/ */
private function genIndexName(string $table, array $index): string private function genIndexName(string $table, array|string $columns, bool $unique = false): string
{ {
$abbr = implode('', array_map(function ($word) { return IndexNameService::generate($table, $columns, $unique);
return $word[0];
}, explode('_', $table)));
return ($index['Non_unique'] ? 'idx_' : 'uni_') . $abbr . '_' . substr(md5($table), -4) . '_' . $index['Column_name'];
} }
} }