From 5a12ed76bb1bfd342f68d9fbf21740bd09587d58 Mon Sep 17 00:00:00 2001 From: Richard Anderson Date: Sun, 2 Mar 2025 16:18:27 +0000 Subject: [PATCH] Database collations (#489) * SyncDatabases * Collation on Create inc WordPress * Refactored Enum * Resolve sync issue * Fix for PostgreSQL * pint * reversed enum * style adjustments * add unit tests * style * fix tests * more tests --------- Co-authored-by: Saeed Vaziry <61919774+saeedvaziry@users.noreply.github.com> Co-authored-by: Saeed Vaziry --- app/Actions/Database/CreateDatabase.php | 12 +- .../Controllers/API/DatabaseController.php | 2 + app/Models/Database.php | 4 + .../Services/Database/AbstractDatabase.php | 137 +++++++++++++++++- app/SSH/Services/Database/Database.php | 6 +- app/SSH/Services/Database/Mariadb.php | 4 +- app/SSH/Services/Database/Mysql.php | 4 +- app/SSH/Services/Database/Postgresql.php | 10 +- app/SiteTypes/Wordpress.php | 10 ++ app/Web/Pages/Servers/Databases/Index.php | 59 ++++++++ .../Databases/Widgets/DatabasesList.php | 6 + app/Web/Pages/Servers/Sites/Index.php | 2 + ...25_02_15_120213_update_databases_table.php | 50 +++++++ .../database/mariadb/create-user.blade.php | 4 +- .../database/mariadb/create.blade.php | 2 +- .../database/mariadb/delete-user.blade.php | 4 +- .../database/mariadb/delete.blade.php | 2 +- .../database/mariadb/get-charsets.blade.php | 4 + .../database/mariadb/get-db-list.blade.php | 10 ++ .../services/database/mariadb/link.blade.php | 4 +- .../database/mariadb/restore.blade.php | 2 +- .../database/mariadb/uninstall.blade.php | 2 +- .../database/mariadb/unlink.blade.php | 2 +- .../services/database/mysql/create.blade.php | 2 +- .../database/mysql/get-charsets.blade.php | 3 + .../database/mysql/get-db-list.blade.php | 8 + .../database/postgresql/create.blade.php | 2 +- .../postgresql/get-charsets.blade.php | 13 ++ .../database/postgresql/get-db-list.blade.php | 8 + tests/Feature/API/DatabaseTest.php | 2 + tests/Feature/DatabaseTest.php | 4 + tests/Feature/SitesTest.php | 4 + tests/TestCase.php | 5 + .../Services/Database/SyncDatabasesTest.php | 80 ++++++++++ .../Services/Database/UpdateCharsetsTest.php | 131 +++++++++++++++++ 35 files changed, 585 insertions(+), 19 deletions(-) create mode 100644 database/migrations/2025_02_15_120213_update_databases_table.php create mode 100644 resources/views/ssh/services/database/mariadb/get-charsets.blade.php create mode 100644 resources/views/ssh/services/database/mariadb/get-db-list.blade.php create mode 100644 resources/views/ssh/services/database/mysql/get-charsets.blade.php create mode 100644 resources/views/ssh/services/database/mysql/get-db-list.blade.php create mode 100644 resources/views/ssh/services/database/postgresql/get-charsets.blade.php create mode 100644 resources/views/ssh/services/database/postgresql/get-db-list.blade.php create mode 100644 tests/Unit/SSH/Services/Database/SyncDatabasesTest.php create mode 100644 tests/Unit/SSH/Services/Database/UpdateCharsetsTest.php diff --git a/app/Actions/Database/CreateDatabase.php b/app/Actions/Database/CreateDatabase.php index 066c85a..fe2632f 100755 --- a/app/Actions/Database/CreateDatabase.php +++ b/app/Actions/Database/CreateDatabase.php @@ -14,11 +14,13 @@ public function create(Server $server, array $input): Database { $database = new Database([ 'server_id' => $server->id, + 'charset' => $input['charset'], + 'collation' => $input['collation'], 'name' => $input['name'], ]); /** @var \App\SSH\Services\Database\Database $databaseHandler */ $databaseHandler = $server->database()->handler(); - $databaseHandler->create($database->name); + $databaseHandler->create($database->name, $database->charset, $database->collation); $database->status = DatabaseStatus::READY; $database->save(); @@ -42,6 +44,14 @@ public static function rules(Server $server, array $input): array 'alpha_dash', Rule::unique('databases', 'name')->where('server_id', $server->id), ], + 'charset' => [ + 'required', + 'string', + ], + 'collation' => [ + 'required', + 'string', + ], ]; if (isset($input['user']) && $input['user']) { $rules['username'] = [ diff --git a/app/Http/Controllers/API/DatabaseController.php b/app/Http/Controllers/API/DatabaseController.php index e1a6593..4deeefe 100644 --- a/app/Http/Controllers/API/DatabaseController.php +++ b/app/Http/Controllers/API/DatabaseController.php @@ -41,6 +41,8 @@ public function index(Project $project, Server $server): ResourceCollection #[Post('/', name: 'api.projects.servers.databases.create', middleware: 'ability:write')] #[Endpoint(title: 'create', description: 'Create a new database.')] #[BodyParam(name: 'name', required: true)] + #[BodyParam(name: 'charset', required: true)] + #[BodyParam(name: 'collation', required: true)] #[ResponseFromApiResource(DatabaseResource::class, Database::class)] public function create(Request $request, Project $project, Server $server): DatabaseResource { diff --git a/app/Models/Database.php b/app/Models/Database.php index 72ebac1..b319f52 100755 --- a/app/Models/Database.php +++ b/app/Models/Database.php @@ -12,6 +12,8 @@ /** * @property int $server_id * @property string $name + * @property string $collation + * @property string $charset * @property string $status * @property Server $server * @property Backup[] $backups @@ -25,6 +27,8 @@ class Database extends AbstractModel protected $fillable = [ 'server_id', 'name', + 'collation', + 'charset', 'status', ]; diff --git a/app/SSH/Services/Database/AbstractDatabase.php b/app/SSH/Services/Database/AbstractDatabase.php index 3f572a8..8c8b542 100755 --- a/app/SSH/Services/Database/AbstractDatabase.php +++ b/app/SSH/Services/Database/AbstractDatabase.php @@ -11,6 +11,16 @@ abstract class AbstractDatabase extends AbstractService implements Database { + protected array $systemDbs = []; + + protected string $defaultCharset; + + protected string $separator = "\t"; + + protected int $headerLines = 1; + + protected bool $removeLastRow = false; + protected function getScriptView(string $script): string { return 'ssh.services.database.'.$this->service->name.'.'.$script; @@ -43,6 +53,9 @@ public function install(): void $status = $this->service->server->systemd()->status($this->service->unit); $this->service->validateInstall($status); $this->service->server->os()->cleanup(); + + $this->updateCharsets(); + $this->syncDatabases(); } public function deletionRules(): array @@ -83,11 +96,13 @@ public function uninstall(): void /** * @throws SSHError */ - public function create(string $name): void + public function create(string $name, string $charset, string $collation): void { $this->service->server->ssh()->exec( view($this->getScriptView('create'), [ 'name' => $name, + 'charset' => $charset, + 'collation' => $collation, ]), 'create-database' ); @@ -219,4 +234,124 @@ public function restoreBackup(BackupFile $backupFile, string $database): void 'restore-database' ); } + + /** + * @throws SSHError + */ + public function updateCharsets(): void + { + $data = $this->service->server->ssh()->exec( + view($this->getScriptView('get-charsets')), + 'get-database-charsets' + ); + + $charsets = $this->tableToArray($data); + + $results = []; + $charsetCollations = []; + + foreach ($charsets as $key => $charset) { + if (empty($charsetCollations[$charset[1]])) { + $charsetCollations[$charset[1]] = []; + } + + $charsetCollations[$charset[1]][] = $charset[0]; + + if ($charset[3] === 'Yes') { + $results[$charset[1]] = [ + 'default' => $charset[0], + 'list' => [], + ]; + + continue; + } + + if ($key == count($charsets) - 1) { + $results[$charset[1]] = [ + 'default' => null, + 'list' => [], + ]; + } + } + + foreach ($results as $charset => $data) { + $results[$charset]['list'] = $charsetCollations[$charset]; + } + + ksort($results); + + $data = array_merge( + $this->service->type_data ?? [], + ['charsets' => $results, 'defaultCharset' => $this->defaultCharset] + ); + + $this->service->update(['type_data' => $data]); + + } + + /** + * @throws SSHError + */ + public function syncDatabases(bool $createNew = true): void + { + $data = $this->service->server->ssh()->exec( + view($this->getScriptView('get-db-list')), + 'get-db-list' + ); + + $databases = $this->tableToArray($data); + + foreach ($databases as $database) { + if (in_array($database[0], $this->systemDbs)) { + continue; + } + + $db = $this->service->server->databases() + ->where('name', $database[0]) + ->first(); + + if ($db === null) { + if ($createNew) { + $this->service->server->databases()->create([ + 'name' => $database[0], + 'collation' => $database[2], + 'charset' => $database[1], + ]); + } + + continue; + } + + if ($db->collation !== $database[2] || $db->charset !== $database[1]) { + $db->update([ + 'collation' => $database[2], + 'charset' => $database[1], + ]); + } + } + } + + protected function tableToArray(string $data, bool $keepHeader = false): array + { + $lines = explode("\n", trim($data)); + + if (! $keepHeader) { + for ($i = 0; $i < $this->headerLines; $i++) { + array_shift($lines); + } + } + + if ($this->removeLastRow) { + array_pop($lines); + } + + $rows = []; + foreach ($lines as $line) { + $row = explode($this->separator, $line); + $row = array_map('trim', $row); + $rows[] = $row; + } + + return $rows; + } } diff --git a/app/SSH/Services/Database/Database.php b/app/SSH/Services/Database/Database.php index a5ab82c..5026509 100755 --- a/app/SSH/Services/Database/Database.php +++ b/app/SSH/Services/Database/Database.php @@ -6,7 +6,7 @@ interface Database { - public function create(string $name): void; + public function create(string $name, string $charset, string $collation): void; public function delete(string $name): void; @@ -21,4 +21,8 @@ public function unlink(string $username, string $host): void; public function runBackup(BackupFile $backupFile): void; public function restoreBackup(BackupFile $backupFile, string $database): void; + + public function updateCharsets(): void; + + public function syncDatabases(bool $createNew = true): void; } diff --git a/app/SSH/Services/Database/Mariadb.php b/app/SSH/Services/Database/Mariadb.php index df1e33e..f007341 100644 --- a/app/SSH/Services/Database/Mariadb.php +++ b/app/SSH/Services/Database/Mariadb.php @@ -4,5 +4,7 @@ class Mariadb extends AbstractDatabase { - // + protected array $systemDbs = ['information_schema', 'performance_schema', 'mysql', 'sys']; + + protected string $defaultCharset = 'utf8mb3'; } diff --git a/app/SSH/Services/Database/Mysql.php b/app/SSH/Services/Database/Mysql.php index 21a685e..34e7add 100755 --- a/app/SSH/Services/Database/Mysql.php +++ b/app/SSH/Services/Database/Mysql.php @@ -4,5 +4,7 @@ class Mysql extends AbstractDatabase { - // + protected array $systemDbs = ['information_schema', 'performance_schema', 'mysql', 'sys']; + + protected string $defaultCharset = 'utf8mb3'; } diff --git a/app/SSH/Services/Database/Postgresql.php b/app/SSH/Services/Database/Postgresql.php index 6388b41..71f1f32 100644 --- a/app/SSH/Services/Database/Postgresql.php +++ b/app/SSH/Services/Database/Postgresql.php @@ -4,5 +4,13 @@ class Postgresql extends AbstractDatabase { - // + protected array $systemDbs = ['template0', 'template1', 'postgres']; + + protected string $defaultCharset = 'UTF8'; + + protected int $headerLines = 2; + + protected string $separator = '|'; + + protected bool $removeLastRow = true; } diff --git a/app/SiteTypes/Wordpress.php b/app/SiteTypes/Wordpress.php index 3145766..cf6f545 100755 --- a/app/SiteTypes/Wordpress.php +++ b/app/SiteTypes/Wordpress.php @@ -80,6 +80,8 @@ public function data(array $input): array 'email' => $input['email'], 'password' => $input['password'], 'database' => $input['database'], + 'database_charset' => $input['charset'], + 'database_collation' => $input['collation'], 'database_user' => $input['database_user'], 'database_password' => $input['database_password'], ]; @@ -96,20 +98,28 @@ public function install(): void $webserver = $this->site->server->webserver()->handler(); $webserver->createVHost($this->site); $this->progress(30); + /** @var Database $database */ $database = app(CreateDatabase::class)->create($this->site->server, [ 'name' => $this->site->type_data['database'], + 'charset' => $this->site->type_data['database_charset'], + 'collation' => $this->site->type_data['database_collation'], ]); + /** @var DatabaseUser $databaseUser */ $databaseUser = app(CreateDatabaseUser::class)->create($this->site->server, [ 'username' => $this->site->type_data['database_user'], 'password' => $this->site->type_data['database_password'], + 'collation' => $this->site->type_data['database_collation'], + 'charset' => $this->site->type_data['database_charset'], 'remote' => false, 'host' => 'localhost', ], [$database->name]); + app(LinkUser::class)->link($databaseUser, [ 'databases' => [$database->name], ]); + $this->site->php()?->restart(); $this->progress(60); app(\App\SSH\Wordpress\Wordpress::class)->install($this->site); diff --git a/app/Web/Pages/Servers/Databases/Index.php b/app/Web/Pages/Servers/Databases/Index.php index 1734a9f..bf55a59 100644 --- a/app/Web/Pages/Servers/Databases/Index.php +++ b/app/Web/Pages/Servers/Databases/Index.php @@ -4,11 +4,15 @@ use App\Actions\Database\CreateDatabase; use App\Models\Database; +use App\Models\Server; use App\Web\Contracts\HasSecondSubNav; use App\Web\Pages\Servers\Page; use Filament\Actions\Action; use Filament\Forms\Components\Checkbox; +use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; +use Filament\Forms\Get; +use Filament\Forms\Set; use Filament\Notifications\Notification; use Filament\Support\Enums\MaxWidth; @@ -25,6 +29,59 @@ public function mount(): void $this->authorize('viewAny', [Database::class, $this->server]); } + public static function getCharsetInput(Server $server): Select + { + return Select::make('charset') + ->label('Charset / Encoding') + ->native(false) + ->live() + ->default(function () use ($server) { + $service = $server->defaultService('database'); + + return $service->type_data['defaultCharset'] ?? null; + }) + ->options(function () use ($server) { + $service = $server->defaultService('database'); + $charsets = $service->type_data['charsets'] ?? []; + + return array_combine( + array_keys($charsets), + array_keys($charsets) + ); + }) + ->afterStateUpdated(function (Get $get, Set $set, $state) use ($server) { + $service = $server->defaultService('database'); + $charsets = $service->type_data['charsets'] ?? []; + $set('collation', $charsets[$state]['default'] ?? null); + }) + ->rules(fn (callable $get) => CreateDatabase::rules($server, $get())['charset']); + } + + public static function getCollationInput(Server $server): Select + { + return Select::make('collation') + ->label('Collation') + ->native(false) + ->live() + ->default(function (Get $get) use ($server) { + $service = $server->defaultService('database'); + $charsets = $service->type_data['charsets'] ?? []; + $charset = $get('charset') ?? $service->type_data['default'] ?? 'utf8mb4'; + + return $charsets[$charset]['default'] ?? null; + }) + ->options(function (Get $get) use ($server) { + $service = $server->defaultService('database'); + $collations = $service->type_data['charsets'][$get('charset')]['list'] ?? []; + + return array_combine( + array_values($collations), + array_values($collations) + ); + }) + ->rules(fn (callable $get) => CreateDatabase::rules($server, $get())['collation']); + } + protected function getHeaderActions(): array { return [ @@ -36,6 +93,8 @@ protected function getHeaderActions(): array ->form([ TextInput::make('name') ->rules(fn (callable $get) => CreateDatabase::rules($this->server, $get())['name']), + self::getCharsetInput($this->server), + self::getCollationInput($this->server), Checkbox::make('user') ->label('Create User') ->default(false) diff --git a/app/Web/Pages/Servers/Databases/Widgets/DatabasesList.php b/app/Web/Pages/Servers/Databases/Widgets/DatabasesList.php index 82b73df..e89deb8 100644 --- a/app/Web/Pages/Servers/Databases/Widgets/DatabasesList.php +++ b/app/Web/Pages/Servers/Databases/Widgets/DatabasesList.php @@ -27,6 +27,12 @@ protected function getTableColumns(): array return [ TextColumn::make('name') ->searchable(), + TextColumn::make('charset') + ->label('Charset / Encoding') + ->sortable(), + TextColumn::make('collation') + ->label('Collation') + ->sortable(), TextColumn::make('status') ->label('Status') ->badge() diff --git a/app/Web/Pages/Servers/Sites/Index.php b/app/Web/Pages/Servers/Sites/Index.php index 2a4de06..68f23a2 100644 --- a/app/Web/Pages/Servers/Sites/Index.php +++ b/app/Web/Pages/Servers/Sites/Index.php @@ -199,6 +199,8 @@ private function wordpressFields(): Component TextInput::make('database') ->helperText('It will create a database with this name') ->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['database']), + \App\Web\Pages\Servers\Databases\Index::getCharsetInput($this->server), + \App\Web\Pages\Servers\Databases\Index::getCollationInput($this->server), TextInput::make('database_user') ->helperText('It will create a db user with this username') ->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['database']), diff --git a/database/migrations/2025_02_15_120213_update_databases_table.php b/database/migrations/2025_02_15_120213_update_databases_table.php new file mode 100644 index 0000000..1433de9 --- /dev/null +++ b/database/migrations/2025_02_15_120213_update_databases_table.php @@ -0,0 +1,50 @@ +string('collation')->nullable(); + $table->string('charset')->nullable(); + }); + + $servers = Server::query()->where('status', ServerStatus::READY)->get(); + + /** @var Server $server */ + foreach ($servers as $server) { + $service = $server->database(); + + if (! $service) { + continue; + } + + /** @var Database $db */ + $db = $service->handler(); + + $db->syncDatabases(false); + $db->updateCharsets(); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('databases', function (Blueprint $table) { + $table->dropColumn('collation'); + $table->dropColumn('charset'); + }); + } +}; diff --git a/resources/views/ssh/services/database/mariadb/create-user.blade.php b/resources/views/ssh/services/database/mariadb/create-user.blade.php index e186365..1ee1996 100644 --- a/resources/views/ssh/services/database/mariadb/create-user.blade.php +++ b/resources/views/ssh/services/database/mariadb/create-user.blade.php @@ -1,8 +1,8 @@ -if ! sudo mysql -e "CREATE USER IF NOT EXISTS '{{ $username }}'@'{{ $host }}' IDENTIFIED BY '{{ $password }}'"; then +if ! sudo mariadb -e "CREATE USER IF NOT EXISTS '{{ $username }}'@'{{ $host }}' IDENTIFIED BY '{{ $password }}'"; then echo 'VITO_SSH_ERROR' && exit 1 fi -if ! sudo mysql -e "FLUSH PRIVILEGES"; then +if ! sudo mariadb -e "FLUSH PRIVILEGES"; then echo 'VITO_SSH_ERROR' && exit 1 fi diff --git a/resources/views/ssh/services/database/mariadb/create.blade.php b/resources/views/ssh/services/database/mariadb/create.blade.php index 14c43f2..51dc05b 100644 --- a/resources/views/ssh/services/database/mariadb/create.blade.php +++ b/resources/views/ssh/services/database/mariadb/create.blade.php @@ -1,4 +1,4 @@ -if ! sudo mysql -e "CREATE DATABASE IF NOT EXISTS {{ $name }} CHARACTER SET utf8 COLLATE utf8_general_ci"; then +if ! sudo mariadb -e "CREATE DATABASE IF NOT EXISTS {{ $name }} CHARACTER SET '{{ $charset }}' COLLATE '{{ $collation }}'"; then echo 'VITO_SSH_ERROR' && exit 1 fi diff --git a/resources/views/ssh/services/database/mariadb/delete-user.blade.php b/resources/views/ssh/services/database/mariadb/delete-user.blade.php index 59c3d52..bd21139 100644 --- a/resources/views/ssh/services/database/mariadb/delete-user.blade.php +++ b/resources/views/ssh/services/database/mariadb/delete-user.blade.php @@ -1,8 +1,8 @@ -if ! sudo mysql -e "DROP USER IF EXISTS '{{ $username }}'@'{{ $host }}'"; then +if ! sudo mariadb -e "DROP USER IF EXISTS '{{ $username }}'@'{{ $host }}'"; then echo 'VITO_SSH_ERROR' && exit 1 fi -if ! sudo mysql -e "FLUSH PRIVILEGES"; then +if ! sudo mariadb -e "FLUSH PRIVILEGES"; then echo 'VITO_SSH_ERROR' && exit 1 fi diff --git a/resources/views/ssh/services/database/mariadb/delete.blade.php b/resources/views/ssh/services/database/mariadb/delete.blade.php index 0130f47..406fad5 100644 --- a/resources/views/ssh/services/database/mariadb/delete.blade.php +++ b/resources/views/ssh/services/database/mariadb/delete.blade.php @@ -1,4 +1,4 @@ -if ! sudo mysql -e "DROP DATABASE IF EXISTS {{ $name }}"; then +if ! sudo mariadb -e "DROP DATABASE IF EXISTS {{ $name }}"; then echo 'VITO_SSH_ERROR' && exit 1 fi diff --git a/resources/views/ssh/services/database/mariadb/get-charsets.blade.php b/resources/views/ssh/services/database/mariadb/get-charsets.blade.php new file mode 100644 index 0000000..772b780 --- /dev/null +++ b/resources/views/ssh/services/database/mariadb/get-charsets.blade.php @@ -0,0 +1,4 @@ +if ! sudo mariadb -e "SHOW COLLATION;"; +then + echo 'VITO_SSH_ERROR' && exit 1 +fi diff --git a/resources/views/ssh/services/database/mariadb/get-db-list.blade.php b/resources/views/ssh/services/database/mariadb/get-db-list.blade.php new file mode 100644 index 0000000..c6886f3 --- /dev/null +++ b/resources/views/ssh/services/database/mariadb/get-db-list.blade.php @@ -0,0 +1,10 @@ +if ! sudo mariadb -e "SELECT + SCHEMA_NAME AS database_name, + DEFAULT_CHARACTER_SET_NAME AS charset, + DEFAULT_COLLATION_NAME AS collation + FROM information_schema.SCHEMATA;"; +then + echo 'VITO_SSH_ERROR' && exit 1 +fi + + diff --git a/resources/views/ssh/services/database/mariadb/link.blade.php b/resources/views/ssh/services/database/mariadb/link.blade.php index ce547f2..8bea57e 100644 --- a/resources/views/ssh/services/database/mariadb/link.blade.php +++ b/resources/views/ssh/services/database/mariadb/link.blade.php @@ -1,8 +1,8 @@ -if ! sudo mysql -e "GRANT ALL PRIVILEGES ON {{ $database }}.* TO '{{ $username }}'@'{{ $host }}'"; then +if ! sudo mariadb -e "GRANT ALL PRIVILEGES ON {{ $database }}.* TO '{{ $username }}'@'{{ $host }}'"; then echo 'VITO_SSH_ERROR' && exit 1 fi -if ! sudo mysql -e "FLUSH PRIVILEGES"; then +if ! sudo mariadb -e "FLUSH PRIVILEGES"; then echo 'VITO_SSH_ERROR' && exit 1 fi diff --git a/resources/views/ssh/services/database/mariadb/restore.blade.php b/resources/views/ssh/services/database/mariadb/restore.blade.php index c158f58..782749d 100644 --- a/resources/views/ssh/services/database/mariadb/restore.blade.php +++ b/resources/views/ssh/services/database/mariadb/restore.blade.php @@ -2,7 +2,7 @@ echo 'VITO_SSH_ERROR' && exit 1 fi -if ! sudo DEBIAN_FRONTEND=noninteractive mysql -u root {{ $database }} < {{ $file }}.sql; then +if ! sudo DEBIAN_FRONTEND=noninteractive mariadb -u root {{ $database }} < {{ $file }}.sql; then echo 'VITO_SSH_ERROR' && exit 1 fi diff --git a/resources/views/ssh/services/database/mariadb/uninstall.blade.php b/resources/views/ssh/services/database/mariadb/uninstall.blade.php index e0e51b1..2bb3aad 100644 --- a/resources/views/ssh/services/database/mariadb/uninstall.blade.php +++ b/resources/views/ssh/services/database/mariadb/uninstall.blade.php @@ -1,4 +1,4 @@ -sudo service mysql stop +sudo service mariadb stop sudo DEBIAN_FRONTEND=noninteractive apt-get remove mariadb-server mariadb-backup -y diff --git a/resources/views/ssh/services/database/mariadb/unlink.blade.php b/resources/views/ssh/services/database/mariadb/unlink.blade.php index ff2a879..9a7b035 100644 --- a/resources/views/ssh/services/database/mariadb/unlink.blade.php +++ b/resources/views/ssh/services/database/mariadb/unlink.blade.php @@ -1,4 +1,4 @@ -if ! sudo mysql -e "REVOKE ALL PRIVILEGES, GRANT OPTION FROM '{{ $username }}'@'{{ $host }}'"; then +if ! sudo mariadb -e "REVOKE ALL PRIVILEGES, GRANT OPTION FROM '{{ $username }}'@'{{ $host }}'"; then echo 'VITO_SSH_ERROR' && exit 1 fi diff --git a/resources/views/ssh/services/database/mysql/create.blade.php b/resources/views/ssh/services/database/mysql/create.blade.php index 14c43f2..da09ba8 100755 --- a/resources/views/ssh/services/database/mysql/create.blade.php +++ b/resources/views/ssh/services/database/mysql/create.blade.php @@ -1,4 +1,4 @@ -if ! sudo mysql -e "CREATE DATABASE IF NOT EXISTS {{ $name }} CHARACTER SET utf8 COLLATE utf8_general_ci"; then +if ! sudo mysql -e "CREATE DATABASE IF NOT EXISTS {{ $name }} CHARACTER SET '{{ $charset }}' COLLATE '{{ $collation }}'"; then echo 'VITO_SSH_ERROR' && exit 1 fi diff --git a/resources/views/ssh/services/database/mysql/get-charsets.blade.php b/resources/views/ssh/services/database/mysql/get-charsets.blade.php new file mode 100644 index 0000000..ac9d7ac --- /dev/null +++ b/resources/views/ssh/services/database/mysql/get-charsets.blade.php @@ -0,0 +1,3 @@ +if ! sudo mysql -e "SHOW COLLATION;"; then + echo 'VITO_SSH_ERROR' && exit 1 +fi diff --git a/resources/views/ssh/services/database/mysql/get-db-list.blade.php b/resources/views/ssh/services/database/mysql/get-db-list.blade.php new file mode 100644 index 0000000..c831421 --- /dev/null +++ b/resources/views/ssh/services/database/mysql/get-db-list.blade.php @@ -0,0 +1,8 @@ +if ! sudo mysql -e "SELECT + SCHEMA_NAME AS database_name, + DEFAULT_CHARACTER_SET_NAME AS charset, + DEFAULT_COLLATION_NAME AS collation + FROM information_schema.SCHEMATA;"; +then + echo 'VITO_SSH_ERROR' && exit 1 +fi diff --git a/resources/views/ssh/services/database/postgresql/create.blade.php b/resources/views/ssh/services/database/postgresql/create.blade.php index e5ada6d..2ce80ee 100644 --- a/resources/views/ssh/services/database/postgresql/create.blade.php +++ b/resources/views/ssh/services/database/postgresql/create.blade.php @@ -1,4 +1,4 @@ -if ! sudo -u postgres psql -c "CREATE DATABASE \"{{ $name }}\""; then +if ! sudo -u postgres psql -c "CREATE DATABASE \"{{ $name }}\" WITH ENCODING '{{ $charset }}'"; then echo 'VITO_SSH_ERROR' && exit 1 fi diff --git a/resources/views/ssh/services/database/postgresql/get-charsets.blade.php b/resources/views/ssh/services/database/postgresql/get-charsets.blade.php new file mode 100644 index 0000000..e05ab24 --- /dev/null +++ b/resources/views/ssh/services/database/postgresql/get-charsets.blade.php @@ -0,0 +1,13 @@ +if ! sudo -u postgres psql -c "SELECT collname as collation, + pg_encoding_to_char(collencoding) as charset, + '' as id, + '' as \"default\", + 'Yes' as compiled, + '' as sortlen, + '' as pad_attribute + FROM pg_collation + WHERE not pg_encoding_to_char(collencoding) = '' + ORDER BY charset;"; +then + echo 'VITO_SSH_ERROR' && exit 1 +fi diff --git a/resources/views/ssh/services/database/postgresql/get-db-list.blade.php b/resources/views/ssh/services/database/postgresql/get-db-list.blade.php new file mode 100644 index 0000000..4408a30 --- /dev/null +++ b/resources/views/ssh/services/database/postgresql/get-db-list.blade.php @@ -0,0 +1,8 @@ +if ! sudo -u postgres psql -c "SELECT + datname AS database_name, + pg_encoding_to_char(encoding) AS charset, + datcollate AS collation + FROM pg_database;"; +then + echo 'VITO_SSH_ERROR' && exit 1 +fi diff --git a/tests/Feature/API/DatabaseTest.php b/tests/Feature/API/DatabaseTest.php index 8fdfee0..e1d1ca3 100644 --- a/tests/Feature/API/DatabaseTest.php +++ b/tests/Feature/API/DatabaseTest.php @@ -24,6 +24,8 @@ public function test_create_database(): void 'server' => $this->server, ]), [ 'name' => 'database', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', ]) ->assertSuccessful() ->assertJsonFragment([ diff --git a/tests/Feature/DatabaseTest.php b/tests/Feature/DatabaseTest.php index 3df6351..43f35f6 100644 --- a/tests/Feature/DatabaseTest.php +++ b/tests/Feature/DatabaseTest.php @@ -27,6 +27,8 @@ public function test_create_database(): void ]) ->callAction('create', [ 'name' => 'database', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', ]) ->assertSuccessful(); @@ -47,6 +49,8 @@ public function test_create_database_with_user(): void ]) ->callAction('create', [ 'name' => 'database', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', 'user' => true, 'username' => 'user', 'password' => 'password', diff --git a/tests/Feature/SitesTest.php b/tests/Feature/SitesTest.php index 0363326..6431cef 100644 --- a/tests/Feature/SitesTest.php +++ b/tests/Feature/SitesTest.php @@ -361,6 +361,8 @@ public static function create_data(): array 'email' => 'email@example.com', 'password' => 'password', 'database' => 'example', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', 'database_user' => 'example', 'database_password' => 'password', ], @@ -376,6 +378,8 @@ public static function create_data(): array 'email' => 'email@example.com', 'password' => 'password', 'database' => 'example', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', 'database_user' => 'example', 'database_password' => 'password', 'user' => 'example', diff --git a/tests/TestCase.php b/tests/TestCase.php index 4cf8549..bb6b8db 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -87,6 +87,11 @@ private function setupServer(): void $this->server->services()->update([ 'status' => ServiceStatus::READY, ]); + + $this->server->database()?->update(['type_data' => [ + 'charsets' => ['utf8mb3' => ['default' => 'utf8mb3_general_ci', 'list' => ['utf8mb3_general_ci']]], + 'defaultCharset' => 'utf8mb3', + ]]); } private function setupSite(): void diff --git a/tests/Unit/SSH/Services/Database/SyncDatabasesTest.php b/tests/Unit/SSH/Services/Database/SyncDatabasesTest.php new file mode 100644 index 0000000..50d4a58 --- /dev/null +++ b/tests/Unit/SSH/Services/Database/SyncDatabasesTest.php @@ -0,0 +1,80 @@ +server->database(); + $database->name = $name; + $database->version = $version; + $database->save(); + + SSH::fake($output); + + /** @var Database $databaseHandler */ + $databaseHandler = $database->handler(); + $databaseHandler->syncDatabases(); + + $this->assertDatabaseHas('databases', [ + 'server_id' => $this->server->id, + 'name' => 'vito', + ]); + } + + /** + * @TODO Add more test cases + * + * @return array[] + */ + public static function data(): array + { + return [ + [ + 'mysql', + '8.0', + <<<'EOD' + database_name charset collation + mysql utf8mb4 utf8mb4_0900_ai_ci + information_schema utf8mb3 utf8mb3_general_ci + performance_schema utf8mb4 utf8mb4_0900_ai_ci + sys utf8mb4 utf8mb4_0900_ai_ci + vito utf8mb3 utf8mb3_general_ci + EOD + ], + [ + 'mysql', + '5.7', + <<<'EOD' + database_name charset collation + mysql utf8mb4 utf8mb4_0900_ai_ci + information_schema utf8mb3 utf8mb3_general_ci + performance_schema utf8mb4 utf8mb4_0900_ai_ci + sys utf8mb4 utf8mb4_0900_ai_ci + vito utf8mb3 utf8mb3_general_ci + EOD + ], + [ + 'postgresql', + '16', + <<<'EOD' + database_name | charset | collation + ---------------+---------+------------- + postgres | UTF8 | en_US.UTF-8 + template1 | UTF8 | en_US.UTF-8 + template0 | UTF8 | en_US.UTF-8 + vito | UTF8 | en_US.UTF-8 + (3 rows) + EOD + ], + ]; + } +} diff --git a/tests/Unit/SSH/Services/Database/UpdateCharsetsTest.php b/tests/Unit/SSH/Services/Database/UpdateCharsetsTest.php new file mode 100644 index 0000000..676bf67 --- /dev/null +++ b/tests/Unit/SSH/Services/Database/UpdateCharsetsTest.php @@ -0,0 +1,131 @@ + [ + 'default' => 'armscii8_general_ci', + 'list' => [ + 'armscii8_bin', + 'armscii8_general_ci', + ], + ], + 'ascii' => [ + 'default' => 'ascii_general_ci', + 'list' => [ + 'ascii_bin', + 'ascii_general_ci', + ], + ], + 'big5' => [ + 'default' => 'big5_chinese_ci', + 'list' => [ + 'big5_bin', + 'big5_chinese_ci', + ], + ], + ]; + + /** + * @dataProvider data + */ + public function test_update_charsets(string $name, string $version, string $output, array $expected): void + { + $database = $this->server->database(); + $database->name = $name; + $database->version = $version; + $database->save(); + + SSH::fake($output); + + /** @var Database $databaseHandler */ + $databaseHandler = $database->handler(); + $databaseHandler->updateCharsets(); + + $database->refresh(); + $this->assertEquals($expected, $database->type_data['charsets']); + } + + /** + * @TODO Add more test cases + * + * @return array[] + */ + public static function data(): array + { + return [ + [ + 'mysql', + '8.0', + <<<'EOD' + Collation Charset Id Default Compiled Sortlen Pad_attribute + armscii8_bin armscii8 64 Yes 1 PAD SPACE + armscii8_general_ci armscii8 32 Yes Yes 1 PAD SPACE + ascii_bin ascii 65 Yes 1 PAD SPACE + ascii_general_ci ascii 11 Yes Yes 1 PAD SPACE + big5_bin big5 84 Yes 1 PAD SPACE + big5_chinese_ci big5 1 Yes Yes 1 PAD SPACE + EOD, + static::$mysqlCharsets, + ], + [ + 'mysql', + '5.7', + <<<'EOD' + Collation Charset Id Default Compiled Sortlen Pad_attribute + armscii8_bin armscii8 64 Yes 1 PAD SPACE + armscii8_general_ci armscii8 32 Yes Yes 1 PAD SPACE + ascii_bin ascii 65 Yes 1 PAD SPACE + ascii_general_ci ascii 11 Yes Yes 1 PAD SPACE + big5_bin big5 84 Yes 1 PAD SPACE + big5_chinese_ci big5 1 Yes Yes 1 PAD SPACE + EOD, + static::$mysqlCharsets, + ], + [ + 'mariadb', + '10.5', + <<<'EOD' + Collation Charset Id Default Compiled Sortlen Pad_attribute + armscii8_bin armscii8 64 Yes 1 PAD SPACE + armscii8_general_ci armscii8 32 Yes Yes 1 PAD SPACE + ascii_bin ascii 65 Yes 1 PAD SPACE + ascii_general_ci ascii 11 Yes Yes 1 PAD SPACE + big5_bin big5 84 Yes 1 PAD SPACE + big5_chinese_ci big5 1 Yes Yes 1 PAD SPACE + EOD, + static::$mysqlCharsets, + ], + [ + 'postgresql', + '16', + <<<'EOD' + collation | charset | id | default | compiled | sortlen | pad_attribute + ------------+---------+----+---------+----------+---------+--------------- + ucs_basic | UTF8 | | | Yes | | + C.utf8 | UTF8 | | | Yes | | + en_US.utf8 | UTF8 | | | Yes | | + en_US | UTF8 | | | Yes | | + (4 rows) + EOD, + [ + 'UTF8' => [ + 'default' => null, + 'list' => [ + 'ucs_basic', + 'C.utf8', + 'en_US.utf8', + 'en_US', + ], + ], + ], + ], + ]; + } +}