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 <mr.saeedvaziry@gmail.com>
This commit is contained in:
Richard Anderson 2025-03-02 16:18:27 +00:00 committed by GitHub
parent 269ee8d962
commit 5a12ed76bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 585 additions and 19 deletions

View File

@ -14,11 +14,13 @@ public function create(Server $server, array $input): Database
{ {
$database = new Database([ $database = new Database([
'server_id' => $server->id, 'server_id' => $server->id,
'charset' => $input['charset'],
'collation' => $input['collation'],
'name' => $input['name'], 'name' => $input['name'],
]); ]);
/** @var \App\SSH\Services\Database\Database $databaseHandler */ /** @var \App\SSH\Services\Database\Database $databaseHandler */
$databaseHandler = $server->database()->handler(); $databaseHandler = $server->database()->handler();
$databaseHandler->create($database->name); $databaseHandler->create($database->name, $database->charset, $database->collation);
$database->status = DatabaseStatus::READY; $database->status = DatabaseStatus::READY;
$database->save(); $database->save();
@ -42,6 +44,14 @@ public static function rules(Server $server, array $input): array
'alpha_dash', 'alpha_dash',
Rule::unique('databases', 'name')->where('server_id', $server->id), Rule::unique('databases', 'name')->where('server_id', $server->id),
], ],
'charset' => [
'required',
'string',
],
'collation' => [
'required',
'string',
],
]; ];
if (isset($input['user']) && $input['user']) { if (isset($input['user']) && $input['user']) {
$rules['username'] = [ $rules['username'] = [

View File

@ -41,6 +41,8 @@ public function index(Project $project, Server $server): ResourceCollection
#[Post('/', name: 'api.projects.servers.databases.create', middleware: 'ability:write')] #[Post('/', name: 'api.projects.servers.databases.create', middleware: 'ability:write')]
#[Endpoint(title: 'create', description: 'Create a new database.')] #[Endpoint(title: 'create', description: 'Create a new database.')]
#[BodyParam(name: 'name', required: true)] #[BodyParam(name: 'name', required: true)]
#[BodyParam(name: 'charset', required: true)]
#[BodyParam(name: 'collation', required: true)]
#[ResponseFromApiResource(DatabaseResource::class, Database::class)] #[ResponseFromApiResource(DatabaseResource::class, Database::class)]
public function create(Request $request, Project $project, Server $server): DatabaseResource public function create(Request $request, Project $project, Server $server): DatabaseResource
{ {

View File

@ -12,6 +12,8 @@
/** /**
* @property int $server_id * @property int $server_id
* @property string $name * @property string $name
* @property string $collation
* @property string $charset
* @property string $status * @property string $status
* @property Server $server * @property Server $server
* @property Backup[] $backups * @property Backup[] $backups
@ -25,6 +27,8 @@ class Database extends AbstractModel
protected $fillable = [ protected $fillable = [
'server_id', 'server_id',
'name', 'name',
'collation',
'charset',
'status', 'status',
]; ];

View File

@ -11,6 +11,16 @@
abstract class AbstractDatabase extends AbstractService implements Database 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 protected function getScriptView(string $script): string
{ {
return 'ssh.services.database.'.$this->service->name.'.'.$script; 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); $status = $this->service->server->systemd()->status($this->service->unit);
$this->service->validateInstall($status); $this->service->validateInstall($status);
$this->service->server->os()->cleanup(); $this->service->server->os()->cleanup();
$this->updateCharsets();
$this->syncDatabases();
} }
public function deletionRules(): array public function deletionRules(): array
@ -83,11 +96,13 @@ public function uninstall(): void
/** /**
* @throws SSHError * @throws SSHError
*/ */
public function create(string $name): void public function create(string $name, string $charset, string $collation): void
{ {
$this->service->server->ssh()->exec( $this->service->server->ssh()->exec(
view($this->getScriptView('create'), [ view($this->getScriptView('create'), [
'name' => $name, 'name' => $name,
'charset' => $charset,
'collation' => $collation,
]), ]),
'create-database' 'create-database'
); );
@ -219,4 +234,124 @@ public function restoreBackup(BackupFile $backupFile, string $database): void
'restore-database' '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;
}
} }

View File

@ -6,7 +6,7 @@
interface Database interface Database
{ {
public function create(string $name): void; public function create(string $name, string $charset, string $collation): void;
public function delete(string $name): 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 runBackup(BackupFile $backupFile): void;
public function restoreBackup(BackupFile $backupFile, string $database): void; public function restoreBackup(BackupFile $backupFile, string $database): void;
public function updateCharsets(): void;
public function syncDatabases(bool $createNew = true): void;
} }

View File

@ -4,5 +4,7 @@
class Mariadb extends AbstractDatabase class Mariadb extends AbstractDatabase
{ {
// protected array $systemDbs = ['information_schema', 'performance_schema', 'mysql', 'sys'];
protected string $defaultCharset = 'utf8mb3';
} }

View File

@ -4,5 +4,7 @@
class Mysql extends AbstractDatabase class Mysql extends AbstractDatabase
{ {
// protected array $systemDbs = ['information_schema', 'performance_schema', 'mysql', 'sys'];
protected string $defaultCharset = 'utf8mb3';
} }

View File

@ -4,5 +4,13 @@
class Postgresql extends AbstractDatabase 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;
} }

View File

@ -80,6 +80,8 @@ public function data(array $input): array
'email' => $input['email'], 'email' => $input['email'],
'password' => $input['password'], 'password' => $input['password'],
'database' => $input['database'], 'database' => $input['database'],
'database_charset' => $input['charset'],
'database_collation' => $input['collation'],
'database_user' => $input['database_user'], 'database_user' => $input['database_user'],
'database_password' => $input['database_password'], 'database_password' => $input['database_password'],
]; ];
@ -96,20 +98,28 @@ public function install(): void
$webserver = $this->site->server->webserver()->handler(); $webserver = $this->site->server->webserver()->handler();
$webserver->createVHost($this->site); $webserver->createVHost($this->site);
$this->progress(30); $this->progress(30);
/** @var Database $database */ /** @var Database $database */
$database = app(CreateDatabase::class)->create($this->site->server, [ $database = app(CreateDatabase::class)->create($this->site->server, [
'name' => $this->site->type_data['database'], 'name' => $this->site->type_data['database'],
'charset' => $this->site->type_data['database_charset'],
'collation' => $this->site->type_data['database_collation'],
]); ]);
/** @var DatabaseUser $databaseUser */ /** @var DatabaseUser $databaseUser */
$databaseUser = app(CreateDatabaseUser::class)->create($this->site->server, [ $databaseUser = app(CreateDatabaseUser::class)->create($this->site->server, [
'username' => $this->site->type_data['database_user'], 'username' => $this->site->type_data['database_user'],
'password' => $this->site->type_data['database_password'], 'password' => $this->site->type_data['database_password'],
'collation' => $this->site->type_data['database_collation'],
'charset' => $this->site->type_data['database_charset'],
'remote' => false, 'remote' => false,
'host' => 'localhost', 'host' => 'localhost',
], [$database->name]); ], [$database->name]);
app(LinkUser::class)->link($databaseUser, [ app(LinkUser::class)->link($databaseUser, [
'databases' => [$database->name], 'databases' => [$database->name],
]); ]);
$this->site->php()?->restart(); $this->site->php()?->restart();
$this->progress(60); $this->progress(60);
app(\App\SSH\Wordpress\Wordpress::class)->install($this->site); app(\App\SSH\Wordpress\Wordpress::class)->install($this->site);

View File

@ -4,11 +4,15 @@
use App\Actions\Database\CreateDatabase; use App\Actions\Database\CreateDatabase;
use App\Models\Database; use App\Models\Database;
use App\Models\Server;
use App\Web\Contracts\HasSecondSubNav; use App\Web\Contracts\HasSecondSubNav;
use App\Web\Pages\Servers\Page; use App\Web\Pages\Servers\Page;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Support\Enums\MaxWidth; use Filament\Support\Enums\MaxWidth;
@ -25,6 +29,59 @@ public function mount(): void
$this->authorize('viewAny', [Database::class, $this->server]); $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 protected function getHeaderActions(): array
{ {
return [ return [
@ -36,6 +93,8 @@ protected function getHeaderActions(): array
->form([ ->form([
TextInput::make('name') TextInput::make('name')
->rules(fn (callable $get) => CreateDatabase::rules($this->server, $get())['name']), ->rules(fn (callable $get) => CreateDatabase::rules($this->server, $get())['name']),
self::getCharsetInput($this->server),
self::getCollationInput($this->server),
Checkbox::make('user') Checkbox::make('user')
->label('Create User') ->label('Create User')
->default(false) ->default(false)

View File

@ -27,6 +27,12 @@ protected function getTableColumns(): array
return [ return [
TextColumn::make('name') TextColumn::make('name')
->searchable(), ->searchable(),
TextColumn::make('charset')
->label('Charset / Encoding')
->sortable(),
TextColumn::make('collation')
->label('Collation')
->sortable(),
TextColumn::make('status') TextColumn::make('status')
->label('Status') ->label('Status')
->badge() ->badge()

View File

@ -199,6 +199,8 @@ private function wordpressFields(): Component
TextInput::make('database') TextInput::make('database')
->helperText('It will create a database with this name') ->helperText('It will create a database with this name')
->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['database']), ->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') TextInput::make('database_user')
->helperText('It will create a db user with this username') ->helperText('It will create a db user with this username')
->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['database']), ->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['database']),

View File

@ -0,0 +1,50 @@
<?php
use App\Enums\ServerStatus;
use App\Models\Server;
use App\SSH\Services\Database\Database;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('databases', function (Blueprint $table) {
$table->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');
});
}
};

View File

@ -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 echo 'VITO_SSH_ERROR' && exit 1
fi fi
if ! sudo mysql -e "FLUSH PRIVILEGES"; then if ! sudo mariadb -e "FLUSH PRIVILEGES"; then
echo 'VITO_SSH_ERROR' && exit 1 echo 'VITO_SSH_ERROR' && exit 1
fi fi

View File

@ -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 echo 'VITO_SSH_ERROR' && exit 1
fi fi

View File

@ -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 echo 'VITO_SSH_ERROR' && exit 1
fi fi
if ! sudo mysql -e "FLUSH PRIVILEGES"; then if ! sudo mariadb -e "FLUSH PRIVILEGES"; then
echo 'VITO_SSH_ERROR' && exit 1 echo 'VITO_SSH_ERROR' && exit 1
fi fi

View File

@ -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 echo 'VITO_SSH_ERROR' && exit 1
fi fi

View File

@ -0,0 +1,4 @@
if ! sudo mariadb -e "SHOW COLLATION;";
then
echo 'VITO_SSH_ERROR' && exit 1
fi

View File

@ -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

View File

@ -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 echo 'VITO_SSH_ERROR' && exit 1
fi fi
if ! sudo mysql -e "FLUSH PRIVILEGES"; then if ! sudo mariadb -e "FLUSH PRIVILEGES"; then
echo 'VITO_SSH_ERROR' && exit 1 echo 'VITO_SSH_ERROR' && exit 1
fi fi

View File

@ -2,7 +2,7 @@
echo 'VITO_SSH_ERROR' && exit 1 echo 'VITO_SSH_ERROR' && exit 1
fi 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 echo 'VITO_SSH_ERROR' && exit 1
fi fi

View File

@ -1,4 +1,4 @@
sudo service mysql stop sudo service mariadb stop
sudo DEBIAN_FRONTEND=noninteractive apt-get remove mariadb-server mariadb-backup -y sudo DEBIAN_FRONTEND=noninteractive apt-get remove mariadb-server mariadb-backup -y

View File

@ -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 echo 'VITO_SSH_ERROR' && exit 1
fi fi

View File

@ -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 echo 'VITO_SSH_ERROR' && exit 1
fi fi

View File

@ -0,0 +1,3 @@
if ! sudo mysql -e "SHOW COLLATION;"; then
echo 'VITO_SSH_ERROR' && exit 1
fi

View File

@ -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

View File

@ -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 echo 'VITO_SSH_ERROR' && exit 1
fi fi

View File

@ -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

View File

@ -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

View File

@ -24,6 +24,8 @@ public function test_create_database(): void
'server' => $this->server, 'server' => $this->server,
]), [ ]), [
'name' => 'database', 'name' => 'database',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
]) ])
->assertSuccessful() ->assertSuccessful()
->assertJsonFragment([ ->assertJsonFragment([

View File

@ -27,6 +27,8 @@ public function test_create_database(): void
]) ])
->callAction('create', [ ->callAction('create', [
'name' => 'database', 'name' => 'database',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
]) ])
->assertSuccessful(); ->assertSuccessful();
@ -47,6 +49,8 @@ public function test_create_database_with_user(): void
]) ])
->callAction('create', [ ->callAction('create', [
'name' => 'database', 'name' => 'database',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'user' => true, 'user' => true,
'username' => 'user', 'username' => 'user',
'password' => 'password', 'password' => 'password',

View File

@ -361,6 +361,8 @@ public static function create_data(): array
'email' => 'email@example.com', 'email' => 'email@example.com',
'password' => 'password', 'password' => 'password',
'database' => 'example', 'database' => 'example',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'database_user' => 'example', 'database_user' => 'example',
'database_password' => 'password', 'database_password' => 'password',
], ],
@ -376,6 +378,8 @@ public static function create_data(): array
'email' => 'email@example.com', 'email' => 'email@example.com',
'password' => 'password', 'password' => 'password',
'database' => 'example', 'database' => 'example',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'database_user' => 'example', 'database_user' => 'example',
'database_password' => 'password', 'database_password' => 'password',
'user' => 'example', 'user' => 'example',

View File

@ -87,6 +87,11 @@ private function setupServer(): void
$this->server->services()->update([ $this->server->services()->update([
'status' => ServiceStatus::READY, '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 private function setupSite(): void

View File

@ -0,0 +1,80 @@
<?php
namespace Tests\Unit\SSH\Services\Database;
use App\Facades\SSH;
use App\SSH\Services\Database\Database;
use Tests\TestCase;
class SyncDatabasesTest extends TestCase
{
/**
* @dataProvider data
*/
public function test_sync_databases(string $name, string $version, string $output): void
{
$database = $this->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
],
];
}
}

View File

@ -0,0 +1,131 @@
<?php
namespace Tests\Unit\SSH\Services\Database;
use App\Facades\SSH;
use App\SSH\Services\Database\Database;
use Tests\TestCase;
class UpdateCharsetsTest extends TestCase
{
protected static array $mysqlCharsets = [
'armscii8' => [
'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',
],
],
],
],
];
}
}