Skip to content

Commit 6ba4ba6

Browse files
authored
Merge pull request #4 from gangaiamaran/workflow-tests
Actions and Doctrine to Schema Changes
2 parents d6c6463 + 235eebf commit 6ba4ba6

7 files changed

+159
-60
lines changed

.github/workflows/run-tests.yml

+40-6
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,42 @@
11
name: run-tests
22

3-
on: [ push, pull_request ]
3+
on: [push, pull_request]
44

55
jobs:
66
test:
77
runs-on: ubuntu-latest
8+
services:
9+
mysql:
10+
image: mysql:8.0
11+
env:
12+
MYSQL_ROOT_PASSWORD: root
13+
MYSQL_DATABASE: test_db
14+
ports:
15+
- 3306:3306
16+
options: >-
17+
--health-cmd="mysqladmin ping --silent"
18+
--health-interval=10s
19+
--health-timeout=5s
20+
--health-retries=3
21+
822
strategy:
923
fail-fast: true
1024
matrix:
11-
php: [ 8.1, 8.2, 8.3 ]
12-
laravel: [ 10.* ]
13-
testbench: [ 8.* ]
14-
dependency-version: [ prefer-stable ]
25+
php: [8.1, 8.2, 8.3]
26+
laravel: [10.*, 11.*]
27+
dependency-version: [prefer-stable]
28+
include:
29+
- laravel: 10.*
30+
testbench: 8.*
31+
- laravel: 11.*
32+
testbench: 9.*
33+
exclude:
34+
- laravel: 10.*
35+
php: 8.0
36+
- laravel: 11.*
37+
php: 8.1
38+
- laravel: 11.*
39+
php: 8.0
1540

1641
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }}
1742

@@ -23,7 +48,7 @@ jobs:
2348
uses: shivammathur/setup-php@v2
2449
with:
2550
php-version: ${{ matrix.php }}
26-
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo
51+
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo, pdo_mysql
2752
coverage: none
2853

2954
- name: Setup problem matchers
@@ -38,5 +63,14 @@ jobs:
3863
env:
3964
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
4065

66+
- name: Set up environment variables
67+
run: |
68+
echo "DB_CONNECTION=mysql" >> $GITHUB_ENV
69+
echo "DB_HOST=127.0.0.1" >> $GITHUB_ENV
70+
echo "DB_PORT=3306" >> $GITHUB_ENV
71+
echo "DB_DATABASE=test_db" >> $GITHUB_ENV
72+
echo "DB_USERNAME=root" >> $GITHUB_ENV
73+
echo "DB_PASSWORD=root" >> $GITHUB_ENV
74+
4175
- name: Execute tests
4276
run: composer test

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ auth.json
1010
phpunit.xml
1111
.phpunit.result.cache
1212
.phpunit.cache
13+
14+
.idea

src/Console/Commands/FindInvalidDatabaseValues.php

+10-5
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

+40-46
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
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;
13+
use function Laravel\Prompts\table;
1314

1415
/**
1516
* Inspired by @see https://medium.com/beyn-technology/ill-never-forget-this-number-4294967295-0xffffffff-c9ad4b72f53a
@@ -26,59 +27,65 @@
2627
#[AsCommand('database:find-risky-columns')]
2728
final class FindRiskyDatabaseColumns extends DatabaseInspectionCommand
2829
{
29-
/** @var string The name and signature of the console command. */
30+
/**
31+
* @var string The name and signature of the console command.
32+
*/
3033
protected $signature = 'database:find-risky-columns {connection=default} {--threshold=70 : Percentage occupied rows number on which the command should treat it as an issue}';
3134

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

35-
/** @var array<string, array{min: int|float, max: int|float}> */
40+
/**
41+
* @var array<string, array{min: int|float, max: int|float}>
42+
*/
3643
private array $columnMinsAndMaxs = [
3744
'integer' => [
3845
'min' => -2_147_483_648,
3946
'max' => 2_147_483_647,
4047
],
41-
'unsigned integer' => [
48+
'int unsigned' => [
4249
'min' => 0,
4350
'max' => 4_294_967_295,
4451
],
4552
'bigint' => [
4653
'min' => -9_223_372_036_854_775_808,
4754
'max' => 9_223_372_036_854_775_807,
4855
],
49-
'unsigned bigint' => [
56+
'bigint unsigned' => [
5057
'min' => 0,
5158
'max' => 18_446_744_073_709_551_615,
5259
],
5360
'tinyint' => [
5461
'min' => -128,
5562
'max' => 127,
5663
],
57-
'unsigned tinyint' => [
64+
'tinyint unsigned' => [
5865
'min' => 0,
5966
'max' => 255,
6067
],
6168
'smallint' => [
6269
'min' => -32_768,
6370
'max' => 32_767,
6471
],
65-
'unsigned smallint' => [
72+
'smallint unsigned' => [
6673
'min' => 0,
6774
'max' => 65_535,
6875
],
6976
'mediumint' => [
7077
'min' => -8_388_608,
7178
'max' => 8_388_607,
7279
],
73-
'unsigned mediumint' => [
80+
'mediumint unsigned' => [
7481
'min' => 0,
7582
'max' => 16_777_215,
7683
],
7784
'decimal' => [
7885
'min' => -99999999999999999999999999999.99999999999999999999999999999,
7986
'max' => 99999999999999999999999999999.99999999999999999999999999999,
8087
],
81-
'unsigned decimal' => [
88+
'decimal unsigned' => [
8289
'min' => 0,
8390
'max' => 99999999999999999999999999999.99999999999999999999999999999,
8491
],
@@ -87,18 +94,14 @@ final class FindRiskyDatabaseColumns extends DatabaseInspectionCommand
8794
public function handle(ConnectionResolverInterface $connections): int
8895
{
8996
$thresholdAlarmPercentage = (float) $this->option('threshold');
90-
91-
$connection = $this->getConnection($connections);
92-
$schema = $connection->getDoctrineSchemaManager();
97+
$connection = Schema::getConnection();
9398
if (! $connection instanceof MySqlConnection) {
9499
throw new \InvalidArgumentException('Command supports MySQL DBs only.');
95100
}
96101

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

101-
foreach ($schema->listTables() as $table) {
104+
foreach (Schema::getTables() as $table) {
102105
$riskyColumnsInfo = $this->processTable($table, $connection, $thresholdAlarmPercentage);
103106
if (is_array($riskyColumnsInfo)) {
104107
$outputTable = [...$outputTable, ...$riskyColumnsInfo];
@@ -120,41 +123,45 @@ public function handle(ConnectionResolverInterface $connections): int
120123
return self::FAILURE;
121124
}
122125

123-
/** @return list<array<string, string>>|null */
124-
private function processTable(Table $table, Connection $connection, float $thresholdAlarmPercentage): ?array
126+
/**
127+
* @return list<array<string, string>>|null
128+
*/
129+
private function processTable(array $table, Connection $connection, float $thresholdAlarmPercentage): ?array
125130
{
126-
$this->comment("Table {$connection->getDatabaseName()}.{$table->getName()}: checking...", 'v');
131+
$tableName = Arr::get($table, 'name');
132+
$this->comment("Table {$connection->getDatabaseName()}.{$tableName}: checking...", 'v');
133+
134+
$tableSize = Arr::get($table, 'size');
127135

128-
$tableSize = $this->getTableSize($connection, $table->getName());
129136
if ($tableSize === null) {
130137
$tableSize = -1; // not critical info, we can skip this issue
131138
}
132139

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());
140+
/**
141+
* @var \Illuminate\Support\Collection<int, Schema> $getColumns
142+
*/
143+
$columns = collect(Schema::getColumns($tableName))->filter(
144+
static fn($column): bool => Arr::get($column, 'auto_increment') === true
145+
);
136146

137147
$riskyColumnsInfo = [];
138148

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

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

148155
$maxValueForColumnKey = $this->getMaxValueForColumn($columnType);
149-
$currentHighestValue = $this->getCurrentHighestValueForColumn($connection->getDatabaseName(), $table->getName(), $columnName);
156+
$currentHighestValue = $this->getCurrentHighestValueForColumn($connection->getDatabaseName(), $tableName, $columnName);
150157

151158
$percentageUsed = round($currentHighestValue / $maxValueForColumnKey * 100, 4);
152159

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

156163
$riskyColumnsInfo[] = [
157-
'table' => "{$connection->getDatabaseName()}.{$table->getName()}",
164+
'table' => "{$connection->getDatabaseName()}.{$tableName}",
158165
'column' => $columnName,
159166
'type' => $columnType,
160167
'size' => $this->formatBytes($tableSize, 2),
@@ -165,26 +172,13 @@ private function processTable(Table $table, Connection $connection, float $thres
165172
}
166173
}
167174

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

170177
return count($riskyColumnsInfo) > 0
171178
? $riskyColumnsInfo
172179
: null;
173180
}
174181

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-
188182
private function getMaxValueForColumn(string $columnType): int | float
189183
{
190184
if (array_key_exists($columnType, $this->columnMinsAndMaxs)) {

src/DatabaseToolkitServiceProvider.php

+5-2
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

+45
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
}

0 commit comments

Comments
 (0)