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));
+ }
+}