Skip to content

Commit 6715530

Browse files
committed
doctrine to Schema conversion
1 parent 0b6026c commit 6715530

File tree

5 files changed

+128
-56
lines changed

5 files changed

+128
-56
lines changed

src/Console/Commands/FindInvalidDatabaseValues.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,21 @@ final class FindInvalidDatabaseValues extends DatabaseInspectionCommand
2020
private const CHECK_TYPE_LONG_TEXT = 'long_text';
2121
private const CHECK_TYPE_LONG_STRING = 'long_string';
2222

23-
/** @var string The name and signature of the console command. */
23+
/**
24+
* @var string The name and signature of the console command.
25+
*/
2426
protected $signature = 'database:find-invalid-values {connection=default} {--check=* : Check only specific types of issues. Available types: {null, datetime, long_text, long_string}}';
2527

26-
/** @var string The console command description. */
28+
/**
29+
* @var string The console command description.
30+
*/
2731
protected $description = 'Find invalid data created in non-strict SQL mode.';
2832

2933
private int $valuesWithIssuesFound = 0;
3034

31-
/** @throws \Doctrine\DBAL\Exception */
35+
/**
36+
* @throws \Doctrine\DBAL\Exception
37+
*/
3238
public function handle(ConnectionResolverInterface $connections): int
3339
{
3440
$connection = $this->getConnection($connections);
@@ -106,8 +112,7 @@ private function checkNullOnNotNullableColumn(Column $column, Connection $connec
106112
private function checkForInvalidDatetimeValues(Column $column, Connection $connection, Table $table): void
107113
{
108114
$integerProbablyUsedForTimestamp = in_array($column->getType()->getName(), [Types::INTEGER, Types::BIGINT], true) && (str_contains($column->getName(), 'timestamp') || str_ends_with($column->getName(), '_at'));
109-
if (
110-
$integerProbablyUsedForTimestamp
115+
if ($integerProbablyUsedForTimestamp
111116
|| in_array($column->getType()->getName(), [Types::DATE_MUTABLE, Types::DATE_IMMUTABLE, Types::DATETIME_MUTABLE, Types::DATETIME_IMMUTABLE, Types::DATETIMETZ_MUTABLE, Types::DATETIMETZ_IMMUTABLE], true)
112117
) {
113118
$columnName = $column->getName();

src/Console/Commands/FindRiskyDatabaseColumns.php

Lines changed: 51 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
namespace InteractionDesignFoundation\LaravelDatabaseToolkit\Console\Commands;
44

5-
use Doctrine\DBAL\Schema\Column;
6-
use Doctrine\DBAL\Schema\Table;
75
use Illuminate\Database\Connection;
86
use Illuminate\Database\ConnectionResolverInterface;
97
use Illuminate\Database\Console\DatabaseInspectionCommand;
108
use Illuminate\Database\MySqlConnection;
9+
use Illuminate\Support\Arr;
1110
use Illuminate\Support\Facades\DB;
11+
use Illuminate\Support\Facades\Schema;
1212
use Symfony\Component\Console\Attribute\AsCommand;
1313

1414
/**
@@ -26,59 +26,65 @@
2626
#[AsCommand('database:find-risky-columns')]
2727
final class FindRiskyDatabaseColumns extends DatabaseInspectionCommand
2828
{
29-
/** @var string The name and signature of the console command. */
29+
/**
30+
* @var string The name and signature of the console command.
31+
*/
3032
protected $signature = 'database:find-risky-columns {connection=default} {--threshold=70 : Percentage occupied rows number on which the command should treat it as an issue}';
3133

32-
/** @var string The console command description. */
34+
/**
35+
* @var string The console command description.
36+
*/
3337
protected $description = 'Find risky auto-incremental columns on databases which values are close to max possible values.';
3438

35-
/** @var array<string, array{min: int|float, max: int|float}> */
39+
/**
40+
* @var array<string, array{min: int|float, max: int|float}>
41+
*/
3642
private array $columnMinsAndMaxs = [
3743
'integer' => [
3844
'min' => -2_147_483_648,
3945
'max' => 2_147_483_647,
4046
],
41-
'unsigned integer' => [
47+
'int unsigned' => [
4248
'min' => 0,
4349
'max' => 4_294_967_295,
4450
],
4551
'bigint' => [
4652
'min' => -9_223_372_036_854_775_808,
4753
'max' => 9_223_372_036_854_775_807,
4854
],
49-
'unsigned bigint' => [
55+
'bigint unsigned' => [
5056
'min' => 0,
51-
'max' => 18_446_744_073_709_551_615,
57+
'max' => 180_709_551_615,
5258
],
5359
'tinyint' => [
5460
'min' => -128,
5561
'max' => 127,
5662
],
57-
'unsigned tinyint' => [
63+
'tinyint unsigned' => [
5864
'min' => 0,
5965
'max' => 255,
6066
],
6167
'smallint' => [
6268
'min' => -32_768,
6369
'max' => 32_767,
6470
],
65-
'unsigned smallint' => [
71+
'smallint unsigned' => [
6672
'min' => 0,
6773
'max' => 65_535,
6874
],
6975
'mediumint' => [
7076
'min' => -8_388_608,
7177
'max' => 8_388_607,
7278
],
73-
'unsigned mediumint' => [
79+
'mediumint unsigned' => [
7480
'min' => 0,
7581
'max' => 16_777_215,
7682
],
7783
'decimal' => [
7884
'min' => -99999999999999999999999999999.99999999999999999999999999999,
7985
'max' => 99999999999999999999999999999.99999999999999999999999999999,
8086
],
81-
'unsigned decimal' => [
87+
'decimal unsigned' => [
8288
'min' => 0,
8389
'max' => 99999999999999999999999999999.99999999999999999999999999999,
8490
],
@@ -87,19 +93,15 @@ final class FindRiskyDatabaseColumns extends DatabaseInspectionCommand
8793
public function handle(ConnectionResolverInterface $connections): int
8894
{
8995
$thresholdAlarmPercentage = (float) $this->option('threshold');
90-
91-
$connection = $this->getConnection($connections);
92-
$schema = $connection->getDoctrineSchemaManager();
96+
$connection = Schema::getConnection();
9397
if (! $connection instanceof MySqlConnection) {
9498
throw new \InvalidArgumentException('Command supports MySQL DBs only.');
9599
}
96100

97-
$this->registerTypeMappings($schema->getDatabasePlatform());
98-
99101
$outputTable = [];
100102

101-
foreach ($schema->listTables() as $table) {
102-
$riskyColumnsInfo = $this->processTable($table, $connection, $thresholdAlarmPercentage);
103+
foreach (Schema::getTables() as $table) {
104+
$riskyColumnsInfo = $this->processTable(Arr::get($table, 'name'), $connection, $thresholdAlarmPercentage);
103105
if (is_array($riskyColumnsInfo)) {
104106
$outputTable = [...$outputTable, ...$riskyColumnsInfo];
105107
}
@@ -120,41 +122,44 @@ public function handle(ConnectionResolverInterface $connections): int
120122
return self::FAILURE;
121123
}
122124

123-
/** @return list<array<string, string>>|null */
124-
private function processTable(Table $table, Connection $connection, float $thresholdAlarmPercentage): ?array
125+
/**
126+
* @return list<array<string, string>>|null
127+
*/
128+
private function processTable(string $tableName, Connection $connection, float $thresholdAlarmPercentage): ?array
125129
{
126-
$this->comment("Table {$connection->getDatabaseName()}.{$table->getName()}: checking...", 'v');
130+
$this->comment("Table {$connection->getDatabaseName()}.{$tableName}: checking...", 'v');
131+
132+
$tableSize = $this->getTableSize($connection, $tableName);
127133

128-
$tableSize = $this->getTableSize($connection, $table->getName());
129134
if ($tableSize === null) {
130135
$tableSize = -1; // not critical info, we can skip this issue
131136
}
132137

133-
/** @var \Illuminate\Support\Collection<int, \Doctrine\DBAL\Schema\Column> $columns */
134-
$columns = collect($table->getColumns())
135-
->filter(static fn(Column $column): bool => $column->getAutoincrement());
138+
/**
139+
* @var \Illuminate\Support\Collection<int, Schema> $getColumns
140+
*/
141+
$columns = collect(Schema::getColumns($tableName))->filter(
142+
static fn($column): bool => Arr::get($column, 'auto_increment') === true
143+
);
136144

137145
$riskyColumnsInfo = [];
138146

139147
foreach ($columns as $column) {
140-
$columnName = $column->getName();
141-
$columnType = $column->getType()->getName();
142-
if ($column->getUnsigned()) {
143-
$columnType = "unsigned {$columnType}";
144-
}
148+
$columnName = Arr::get($column, 'name');
149+
$columnType = Arr::get($column, 'type');
145150

146151
$this->comment("\t{$columnName} is autoincrement.", 'vvv');
147152

148153
$maxValueForColumnKey = $this->getMaxValueForColumn($columnType);
149-
$currentHighestValue = $this->getCurrentHighestValueForColumn($connection->getDatabaseName(), $table->getName(), $columnName);
154+
$currentHighestValue = $this->getCurrentHighestValueForColumn($connection->getDatabaseName(), $tableName, $columnName);
150155

151156
$percentageUsed = round($currentHighestValue / $maxValueForColumnKey * 100, 4);
152157

153158
if ($percentageUsed >= $thresholdAlarmPercentage) {
154-
$this->error("{$connection->getDatabaseName()}.{$table->getName()}.{$columnName} is full for {$percentageUsed}% (threshold for allowed usage is {$thresholdAlarmPercentage}%)", 'quiet');
159+
$this->error("{$connection->getDatabaseName()}.{$tableName}.{$columnName} is full for {$percentageUsed}% (threshold for allowed usage is {$thresholdAlarmPercentage}%)", 'quiet');
155160

156161
$riskyColumnsInfo[] = [
157-
'table' => "{$connection->getDatabaseName()}.{$table->getName()}",
162+
'table' => "{$connection->getDatabaseName()}.{$tableName}",
158163
'column' => $columnName,
159164
'type' => $columnType,
160165
'size' => $this->formatBytes($tableSize, 2),
@@ -165,26 +170,13 @@ private function processTable(Table $table, Connection $connection, float $thres
165170
}
166171
}
167172

168-
$this->comment("Table {$connection->getDatabaseName()}.{$table->getName()}: OK", 'vv');
173+
$this->comment("Table {$connection->getDatabaseName()}.{$tableName}: OK", 'vv');
169174

170175
return count($riskyColumnsInfo) > 0
171176
? $riskyColumnsInfo
172177
: null;
173178
}
174179

175-
private function getConnection(ConnectionResolverInterface $connections): Connection
176-
{
177-
$connectionName = $this->argument('connection');
178-
if ($connectionName === 'default') {
179-
$connectionName = config('database.default');
180-
}
181-
182-
$connection = $connections->connection($connectionName);
183-
assert($connection instanceof Connection);
184-
185-
return $connection;
186-
}
187-
188180
private function getMaxValueForColumn(string $columnType): int | float
189181
{
190182
if (array_key_exists($columnType, $this->columnMinsAndMaxs)) {
@@ -218,4 +210,15 @@ private function formatBytes(int $size, int $precision): string
218210
$suffix = $suffixes[$index];
219211
return round(1024 ** ($base - floor($base)), $precision).$suffix;
220212
}
213+
protected function getTableSize($connection, string $table)
214+
{
215+
$result = $connection->selectOne(
216+
'SELECT (data_length + index_length) AS size FROM information_schema.TABLES WHERE table_schema = ? AND table_name = ?', [
217+
$connection->getDatabaseName(),
218+
$table,
219+
]
220+
);
221+
222+
return Arr::wrap((array) $result)['size'];
223+
}
221224
}

src/DatabaseToolkitServiceProvider.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@ final class DatabaseToolkitServiceProvider extends ServiceProvider
1010
{
1111
/**
1212
* Bootstrap any package services.
13+
*
1314
* @see https://laravel.com/docs/master/packages#commands
1415
*/
1516
public function boot(): void
1617
{
1718
if ($this->app->runningInConsole()) {
18-
$this->commands([
19+
$this->commands(
20+
[
1921
FindInvalidDatabaseValues::class,
2022
FindRiskyDatabaseColumns::class,
21-
]);
23+
]
24+
);
2225
}
2326
}
2427
}

tests/Console/Commands/FindRiskyDatabaseColumnsTest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
namespace Tests\Console\Commands;
44

5+
use Illuminate\Database\Schema\Blueprint;
56
use Illuminate\Foundation\Testing\Concerns\InteractsWithConsole;
7+
use Illuminate\Support\Facades\DB;
8+
use Illuminate\Support\Facades\Schema;
69
use Illuminate\Testing\PendingCommand;
710
use InteractionDesignFoundation\LaravelDatabaseToolkit\Console\Commands\FindRiskyDatabaseColumns;
811
use PHPUnit\Framework\Attributes\CoversClass;
@@ -17,9 +20,51 @@ final class FindRiskyDatabaseColumnsTest extends TestCase
1720
#[Test]
1821
public function it_works_with_default_threshold(): void
1922
{
23+
Schema::create(
24+
'dummy_table_1', function (Blueprint $table) {
25+
$table->tinyIncrements('id')->startingValue(100);
26+
$table->string('name')->nullable();
27+
}
28+
);
29+
DB::table('dummy_table_1')->insert(['name' => 'foo']);
30+
2031
$pendingCommand = $this->artisan(FindRiskyDatabaseColumns::class);
2132

2233
assert($pendingCommand instanceof PendingCommand);
2334
$pendingCommand->assertExitCode(0);
2435
}
36+
37+
#[Test]
38+
public function it_works_with_custom_threshold(): void
39+
{
40+
Schema::create(
41+
'dummy_table_2', function (Blueprint $table) {
42+
$table->tinyIncrements('id')->startingValue(130);
43+
$table->string('name')->nullable();
44+
}
45+
);
46+
DB::table('dummy_table_2')->insert(['name' => 'foo']);
47+
48+
$pendingCommand = $this->artisan(FindRiskyDatabaseColumns::class, ['--threshold' => 50]);
49+
50+
assert($pendingCommand instanceof PendingCommand);
51+
$pendingCommand->assertExitCode(1);
52+
}
53+
54+
#[Test]
55+
public function it_fails_with_exceeding_threshold_tinyint(): void
56+
{
57+
Schema::create(
58+
'dummy_table_3', function (Blueprint $table) {
59+
$table->tinyIncrements('id')->startingValue(200);
60+
$table->string('name')->nullable();
61+
}
62+
);
63+
DB::table('dummy_table_3')->insert(['name' => 'foo']);
64+
65+
$pendingCommand = $this->artisan(FindRiskyDatabaseColumns::class);
66+
67+
assert($pendingCommand instanceof PendingCommand);
68+
$pendingCommand->assertExitCode(1);
69+
}
2570
}

tests/TestCase.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@
22

33
namespace Tests;
44

5+
use Illuminate\Support\Facades\Schema;
6+
use Illuminate\Support\Str;
57
use InteractionDesignFoundation\LaravelDatabaseToolkit\DatabaseToolkitServiceProvider;
68

79
abstract class TestCase extends \Orchestra\Testbench\TestCase
810
{
11+
protected function setUp(): void
12+
{
13+
parent::setUp();
14+
}
915
/**
1016
* Load package service provider.
11-
* @param \Illuminate\Foundation\Application $app
17+
*
18+
* @param \Illuminate\Foundation\Application $app
1219
* @return list<string>
1320
*/
1421
protected function getPackageProviders($app): array
@@ -17,4 +24,13 @@ protected function getPackageProviders($app): array
1724
DatabaseToolkitServiceProvider::class,
1825
];
1926
}
27+
28+
protected function tearDown(): void
29+
{
30+
collect(Schema::getTableListing())
31+
->filter(fn($table) => Str::startsWith($table, 'dummy'))
32+
->each(fn($tableName) => Schema::dropIfExists($tableName));
33+
34+
parent::tearDown();
35+
}
2036
}

0 commit comments

Comments
 (0)