From ea10f17079fc3188b4371cba0497276bbc95aea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=99=AF=E7=AB=8B?= Date: Thu, 21 May 2026 00:17:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(database):=20=E9=80=82=E9=85=8D=E5=A4=9A?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E7=BB=B4=E6=8A=A4=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 DatabaseMaintenance 适配器,按数据库类型生成维护语句:MySQL/MariaDB 支持 REPAIR 与 OPTIMIZE,SQLite 使用 PRAGMA optimize 与 VACUUM,PostgreSQL 使用 VACUUM ANALYZE,SQL Server 使用索引重组与统计信息更新。 重构 DatabaseCommand,将 repair 与 optimize 统一收敛到维护适配器执行,针对不支持的数据库返回友好队列成功提示,避免 SQLite 等环境直接报错中断任务。 补充 DatabaseMaintenanceTest 覆盖各数据库语句生成、标识符转义和不支持提示,并将测试加入 phpunit.xml.dist 的套件列表。 --- phpunit.xml.dist | 1 + .../command/database/DatabaseCommand.php | 79 ++++--- .../command/database/DatabaseMaintenance.php | 208 ++++++++++++++++++ .../tests/helper/DatabaseMaintenanceTest.php | 118 ++++++++++ 4 files changed, 374 insertions(+), 32 deletions(-) create mode 100644 plugin/think-plugs-system/src/helper/command/database/DatabaseMaintenance.php create mode 100644 plugin/think-plugs-system/tests/helper/DatabaseMaintenanceTest.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 69d6953a9..937a3ee39 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -20,6 +20,7 @@ plugin/think-library/tests/RequestTokenServiceTest.php plugin/think-plugs-install/tests/InstallCommandTest.php plugin/think-plugs-system/tests/helper/PublishTest.php + plugin/think-plugs-system/tests/helper/DatabaseMaintenanceTest.php plugin/think-plugs-system/tests/helper/IndexNameServiceTest.php plugin/think-plugs-system/tests/helper/PluginMenuServiceTest.php plugin/think-plugs-system/tests/RbacAccessTest.php diff --git a/plugin/think-plugs-system/src/helper/command/database/DatabaseCommand.php b/plugin/think-plugs-system/src/helper/command/database/DatabaseCommand.php index 2635a4c51..c43c9fdb2 100644 --- a/plugin/think-plugs-system/src/helper/command/database/DatabaseCommand.php +++ b/plugin/think-plugs-system/src/helper/command/database/DatabaseCommand.php @@ -49,12 +49,9 @@ class DatabaseCommand extends Command */ protected function execute(Input $input, Output $output): int { - if ($this->app->db->connect()->getConfig('type') === 'sqlite') { - $this->setQueueError('Sqlite 数据库不支持 REPAIR 和 OPTIMIZE 操作!'); - } - $action = $input->getArgument('action'); - if (method_exists($this, $method = "_{$action}")) { - $this->{$method}(); + $action = strtolower(strval($input->getArgument('action'))); + if (in_array($action, [DatabaseMaintenance::ACTION_REPAIR, DatabaseMaintenance::ACTION_OPTIMIZE], true)) { + $this->maintain($action); } else { $this->output->error('Wrong operation, currently allow repair|optimize'); } @@ -62,44 +59,62 @@ class DatabaseCommand extends Command } /** - * 修复所有数据表. + * 执行数据库维护。 * @throws Exception */ - protected function _repair(): void + private function maintain(string $action): void { $connection = $this->app->db->connect(); if (!$connection instanceof PDOConnection) { - throw new Exception('当前数据库连接不支持 REPAIR 操作'); + throw new Exception("当前数据库连接不支持 {$action} 操作"); } - $this->setQueueProgress('正在获取需要修复的数据表', '0'); - [$tables, $total, $count] = SystemService::getTables(); - $this->setQueueProgress("总共需要修复 {$total} 张数据表", '0'); - foreach ($tables as $table) { - $this->setQueueMessage($total, ++$count, "正在修复数据表 {$table}"); - $connection->query("REPAIR TABLE `{$table}`"); - $this->setQueueMessage($total, $count, "完成修复数据表 {$table}", 1); + + $adapter = new DatabaseMaintenance(strval($connection->getConfig('type') ?: '')); + if (!$adapter->supports($action)) { + $message = $adapter->unsupportedMessage($action); + $this->setQueueProgress($message, '100'); + $this->setQueueSuccess($message); + return; } - $this->setQueueSuccess("已完成对 {$total} 张数据表修复操作"); + + $label = DatabaseMaintenance::actionLabel($action); + $tables = []; + if ($adapter->requiresTables($action)) { + $this->setQueueProgress("正在获取需要{$label}的数据表", '0'); + [$tables] = SystemService::getTables(); + } + $operations = $adapter->operations($action, $tables); + $total = count($operations); + $count = 0; + + if ($total < 1) { + $this->setQueueSuccess("没有需要{$label}的数据表,已跳过处理"); + return; + } + + $this->setQueueProgress("总共需要执行 {$total} 项数据库{$label}操作", '0'); + foreach ($operations as $operation) { + $target = $operation['target']; + $this->setQueueMessage($total, ++$count, "正在{$label}{$target}"); + foreach ($operation['statements'] as $statement) { + $this->runStatement($connection, $statement); + } + $this->setQueueMessage($total, $count, "完成{$label}{$target}", 1); + } + $this->setQueueSuccess("已完成 {$total} 项数据库{$label}操作"); } /** - * 优化所有数据表. - * @throws Exception + * 执行维护语句。 + * + * @param array{sql:string, mode:string} $statement */ - protected function _optimize(): void + private function runStatement(PDOConnection $connection, array $statement): void { - $connection = $this->app->db->connect(); - if (!$connection instanceof PDOConnection) { - throw new Exception('当前数据库连接不支持 OPTIMIZE 操作'); + if ($statement['mode'] === DatabaseMaintenance::MODE_QUERY) { + $connection->query($statement['sql']); + } else { + $connection->execute($statement['sql']); } - $this->setQueueProgress('正在获取需要优化的数据表', '0'); - [$tables, $total, $count] = SystemService::getTables(); - $this->setQueueProgress("总共需要优化 {$total} 张数据表", '0'); - foreach ($tables as $table) { - $this->setQueueMessage($total, ++$count, "正在优化数据表 {$table}"); - $connection->query("OPTIMIZE TABLE `{$table}`"); - $this->setQueueMessage($total, $count, "完成优化数据表 {$table}", 1); - } - $this->setQueueSuccess("已完成对 {$total} 张数据表优化操作"); } } diff --git a/plugin/think-plugs-system/src/helper/command/database/DatabaseMaintenance.php b/plugin/think-plugs-system/src/helper/command/database/DatabaseMaintenance.php new file mode 100644 index 000000000..874cb6210 --- /dev/null +++ b/plugin/think-plugs-system/src/helper/command/database/DatabaseMaintenance.php @@ -0,0 +1,208 @@ +type = $this->normalizeType($type); + } + + /** + * 操作名称。 + */ + public static function actionLabel(string $action): string + { + return $action === self::ACTION_REPAIR ? '修复' : '优化'; + } + + /** + * 是否支持指定维护操作。 + */ + public function supports(string $action): bool + { + return match ($this->type) { + 'mysql', 'mariadb' => in_array($action, [self::ACTION_OPTIMIZE, self::ACTION_REPAIR], true), + 'sqlite', 'pgsql', 'sqlsrv' => $action === self::ACTION_OPTIMIZE, + default => false, + }; + } + + /** + * 是否需要遍历数据表。 + */ + public function requiresTables(string $action): bool + { + return $this->supports($action) && in_array($this->type, ['mysql', 'mariadb', 'pgsql', 'sqlsrv'], true); + } + + /** + * 生成不支持时的提示。 + */ + public function unsupportedMessage(string $action): string + { + return sprintf('%s 数据库暂不支持 %s 操作,已跳过处理。', strtoupper($this->type ?: 'unknown'), strtoupper($action)); + } + + /** + * 生成维护操作。 + * + * @param array $tables + * @return array}> + */ + public function operations(string $action, array $tables = []): array + { + if (!$this->supports($action)) { + return []; + } + + return match ($this->type) { + 'mysql', 'mariadb' => $this->mysqlOperations($action, $tables), + 'sqlite' => $this->sqliteOperations($action), + 'pgsql' => $this->pgsqlOperations($action, $tables), + 'sqlsrv' => $this->sqlsrvOperations($action, $tables), + default => [], + }; + } + + private function normalizeType(string $type): string + { + $type = strtolower(trim($type)); + return match ($type) { + 'postgres', 'postgresql' => 'pgsql', + 'mssql', 'sqlserver' => 'sqlsrv', + default => $type, + }; + } + + /** + * @param array $tables + * @return array}> + */ + private function mysqlOperations(string $action, array $tables): array + { + $prefix = $action === self::ACTION_REPAIR ? 'REPAIR TABLE' : 'OPTIMIZE TABLE'; + $operations = []; + foreach ($tables as $table) { + $operations[] = [ + 'target' => "数据表 {$table}", + 'statements' => [[ + 'sql' => "{$prefix} {$this->quoteTable($table)}", + 'mode' => self::MODE_QUERY, + ]], + ]; + } + return $operations; + } + + /** + * @return array}> + */ + private function sqliteOperations(string $action): array + { + if ($action !== self::ACTION_OPTIMIZE) { + return []; + } + + return [[ + 'target' => '数据库', + 'statements' => [ + ['sql' => 'PRAGMA optimize', 'mode' => self::MODE_EXECUTE], + ['sql' => 'VACUUM', 'mode' => self::MODE_EXECUTE], + ], + ]]; + } + + /** + * @param array $tables + * @return array}> + */ + private function pgsqlOperations(string $action, array $tables): array + { + if ($action !== self::ACTION_OPTIMIZE) { + return []; + } + + $operations = []; + foreach ($tables as $table) { + $operations[] = [ + 'target' => "数据表 {$table}", + 'statements' => [[ + 'sql' => "VACUUM (ANALYZE) {$this->quoteTable($table)}", + 'mode' => self::MODE_EXECUTE, + ]], + ]; + } + return $operations; + } + + /** + * @param array $tables + * @return array}> + */ + private function sqlsrvOperations(string $action, array $tables): array + { + if ($action !== self::ACTION_OPTIMIZE) { + return []; + } + + $operations = []; + foreach ($tables as $table) { + $quoted = $this->quoteTable($table); + $operations[] = [ + 'target' => "数据表 {$table}", + 'statements' => [ + ['sql' => "ALTER INDEX ALL ON {$quoted} REORGANIZE", 'mode' => self::MODE_EXECUTE], + ['sql' => "UPDATE STATISTICS {$quoted}", 'mode' => self::MODE_EXECUTE], + ], + ]; + } + return $operations; + } + + private function quoteTable(string $table): string + { + $parts = array_map('trim', explode('.', $table)); + $parts = array_filter($parts, static fn (string $part): bool => $part !== ''); + + return implode('.', array_map(fn (string $part): string => $this->quoteIdentifier($part), $parts)); + } + + private function quoteIdentifier(string $identifier): string + { + return match ($this->type) { + 'pgsql' => '"' . str_replace('"', '""', $identifier) . '"', + 'sqlsrv' => '[' . str_replace(']', ']]', $identifier) . ']', + default => '`' . str_replace('`', '``', $identifier) . '`', + }; + } +} diff --git a/plugin/think-plugs-system/tests/helper/DatabaseMaintenanceTest.php b/plugin/think-plugs-system/tests/helper/DatabaseMaintenanceTest.php new file mode 100644 index 000000000..78d2f1bf9 --- /dev/null +++ b/plugin/think-plugs-system/tests/helper/DatabaseMaintenanceTest.php @@ -0,0 +1,118 @@ +assertTrue($adapter->supports(DatabaseMaintenance::ACTION_OPTIMIZE)); + $this->assertTrue($adapter->supports(DatabaseMaintenance::ACTION_REPAIR)); + $this->assertTrue($adapter->requiresTables(DatabaseMaintenance::ACTION_OPTIMIZE)); + + $optimize = $adapter->operations(DatabaseMaintenance::ACTION_OPTIMIZE, [ + 'system_queue', + 'admin.system_user', + 'weird`name', + ]); + $repair = $adapter->operations(DatabaseMaintenance::ACTION_REPAIR, ['system_queue']); + + $this->assertSame('OPTIMIZE TABLE `system_queue`', $optimize[0]['statements'][0]['sql']); + $this->assertSame('OPTIMIZE TABLE `admin`.`system_user`', $optimize[1]['statements'][0]['sql']); + $this->assertSame('OPTIMIZE TABLE `weird``name`', $optimize[2]['statements'][0]['sql']); + $this->assertSame(DatabaseMaintenance::MODE_QUERY, $optimize[0]['statements'][0]['mode']); + $this->assertSame('REPAIR TABLE `system_queue`', $repair[0]['statements'][0]['sql']); + } + + public function testSqliteBuildsDatabaseScopedOptimizeAndSkipsRepair(): void + { + $adapter = new DatabaseMaintenance('sqlite'); + + $this->assertTrue($adapter->supports(DatabaseMaintenance::ACTION_OPTIMIZE)); + $this->assertFalse($adapter->supports(DatabaseMaintenance::ACTION_REPAIR)); + $this->assertFalse($adapter->requiresTables(DatabaseMaintenance::ACTION_OPTIMIZE)); + + $operations = $adapter->operations(DatabaseMaintenance::ACTION_OPTIMIZE, ['system_queue']); + + $this->assertSame('数据库', $operations[0]['target']); + $this->assertSame('PRAGMA optimize', $operations[0]['statements'][0]['sql']); + $this->assertSame('VACUUM', $operations[0]['statements'][1]['sql']); + $this->assertSame(DatabaseMaintenance::MODE_EXECUTE, $operations[0]['statements'][0]['mode']); + $this->assertSame([], $adapter->operations(DatabaseMaintenance::ACTION_REPAIR, ['system_queue'])); + $this->assertSame('SQLITE 数据库暂不支持 REPAIR 操作,已跳过处理。', $adapter->unsupportedMessage(DatabaseMaintenance::ACTION_REPAIR)); + } + + public function testPgsqlBuildsVacuumAnalyzeWithDoubleQuotedTables(): void + { + $adapter = new DatabaseMaintenance('postgresql'); + + $this->assertTrue($adapter->supports(DatabaseMaintenance::ACTION_OPTIMIZE)); + $this->assertFalse($adapter->supports(DatabaseMaintenance::ACTION_REPAIR)); + $this->assertTrue($adapter->requiresTables(DatabaseMaintenance::ACTION_OPTIMIZE)); + + $operations = $adapter->operations(DatabaseMaintenance::ACTION_OPTIMIZE, [ + 'public.system_queue', + 'weird"name', + ]); + + $this->assertSame('VACUUM (ANALYZE) "public"."system_queue"', $operations[0]['statements'][0]['sql']); + $this->assertSame('VACUUM (ANALYZE) "weird""name"', $operations[1]['statements'][0]['sql']); + $this->assertSame(DatabaseMaintenance::MODE_EXECUTE, $operations[0]['statements'][0]['mode']); + } + + public function testSqlsrvBuildsIndexAndStatisticsStatementsWithBracketQuotedTables(): void + { + $adapter = new DatabaseMaintenance('mssql'); + + $this->assertTrue($adapter->supports(DatabaseMaintenance::ACTION_OPTIMIZE)); + $this->assertFalse($adapter->supports(DatabaseMaintenance::ACTION_REPAIR)); + $this->assertTrue($adapter->requiresTables(DatabaseMaintenance::ACTION_OPTIMIZE)); + + $operations = $adapter->operations(DatabaseMaintenance::ACTION_OPTIMIZE, [ + 'dbo.system_queue', + 'weird]name', + ]); + + $this->assertSame('ALTER INDEX ALL ON [dbo].[system_queue] REORGANIZE', $operations[0]['statements'][0]['sql']); + $this->assertSame('UPDATE STATISTICS [dbo].[system_queue]', $operations[0]['statements'][1]['sql']); + $this->assertSame('ALTER INDEX ALL ON [weird]]name] REORGANIZE', $operations[1]['statements'][0]['sql']); + $this->assertSame(DatabaseMaintenance::MODE_EXECUTE, $operations[0]['statements'][0]['mode']); + } + + public function testUnsupportedDatabaseReturnsNoOperations(): void + { + $adapter = new DatabaseMaintenance('oracle'); + + $this->assertFalse($adapter->supports(DatabaseMaintenance::ACTION_OPTIMIZE)); + $this->assertFalse($adapter->supports(DatabaseMaintenance::ACTION_REPAIR)); + $this->assertSame([], $adapter->operations(DatabaseMaintenance::ACTION_OPTIMIZE, ['system_queue'])); + $this->assertSame('ORACLE 数据库暂不支持 OPTIMIZE 操作,已跳过处理。', $adapter->unsupportedMessage(DatabaseMaintenance::ACTION_OPTIMIZE)); + } +}