Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 61 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ $container = new Container(
fn(): Migrator => new Migrator(
collection: new MigrationCollection(
new Migration(
path: __DIR__ . '/migration/sqlite/memory',
path: __DIR__ . '/migration/sqlite/db',
driver: new PdoDriver(
dsn: 'sqlite:' . __DIR__ . '/data/sqlite/db.sqlite3',
)
Expand Down Expand Up @@ -137,19 +137,70 @@ make app

```shell
/example $ php cli.php migrate:init
[sqlite/memory] initialization: setup.sql done
[sqlite/db] initialization: setup.sql done

/example $ php cli.php migrate:up
[sqlite/memory] up: 202501011024_entity_create.sql done
[sqlite/memory] up: 202501021024_account_create.sql done
[sqlite/memory] up: 202501021025_account_email.sql done
[sqlite/memory] repeatable: 202501011024_entity_correction.sql done
[sqlite/memory] repeatable: 202501011024_entity_correction_2.sql done
[sqlite/db] up: 202501011024_entity_create.sql done
[sqlite/db] up: 202501021024_account_create.sql done
[sqlite/db] up: 202501021025_account_email.sql done

/example $ php cli.php migrate:down
[sqlite/memory] down: 202501021025_account_email.sql done
[sqlite/memory] down: 202501021024_account_create.sql done
[sqlite/memory] down: 202501011024_entity_create.sql done
[sqlite/db] down: 202501021025_account_email.sql done
[sqlite/db] down: 202501021024_account_create.sql done
[sqlite/db] down: 202501011024_entity_create.sql done
```

#### With exactly all

If any migration fails, the entire batch is rolled back, leaving the database unchanged.

```shell
/example $ php cli.php migrate:up --exactly-all
[sqlite/db] up: 202501011024_entity_create.sql done
[sqlite/db] up: 202501021024_account_create.sql done
[sqlite/db] up: 202501021025_account_email.sql done
```

#### With repeatable

```shell
/example $ php cli.php migrate:up --with-repeatable
[sqlite/db] up: 202501011024_entity_create.sql done
[sqlite/db] up: 202501021024_account_create.sql done
[sqlite/db] up: 202501021025_account_email.sql done
[sqlite/db] repeatable: 202501011024_entity_correction.sql done
[sqlite/db] repeatable: 202501011024_entity_correction_2.sql done
```

#### Down with latest version

```shell
/example $ php cli.php migrate:up --limit=1
[sqlite/db] up: 202501011024_entity_create.sql, vers: 1772723563954 done

/example $ php cli.php migrate:up --limit=2
[sqlite/db] up: 202501021024_account_create.sql, vers: 1772723566084 done
[sqlite/db] up: 202501021025_account_email.sql, vers: 1772723566084 done

/example $ php cli.php migrate:down --latest-version
[sqlite/db] down: 202501021025_account_email.sql, vers: 1772723566084 done
[sqlite/db] down: 202501021024_account_create.sql, vers: 1772723566084 done

```

#### Redo with latest version

```shell
/example $ php cli.php migrate:up
[sqlite/db] up: 202501021024_account_create.sql, vers: 1772723718828 done
[sqlite/db] up: 202501021025_account_email.sql, vers: 1772723718828 done

/example $ php cli.php migrate:redo --latest-version
[sqlite/db] down: 202501021025_account_email.sql, vers: 1772723718828 done
[sqlite/db] down: 202501021024_account_create.sql, vers: 1772723718828 done
[sqlite/db] up: 202501021024_account_create.sql, vers: 1772723727397 done
[sqlite/db] up: 202501021025_account_email.sql, vers: 1772723727397 done

```

### Static analysis
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"league/climate": "^3.10"
},
"require-dev": {
"buggregator/trap": "^1.15",
"infection/infection": "^0.32.4",
"php-di/php-di": "^7.0",
"phpstan/phpstan": "^2.0",
Expand Down
5 changes: 0 additions & 5 deletions example/migration/mysql/setup/setup.sql

This file was deleted.

5 changes: 0 additions & 5 deletions example/migration/postgres/setup/setup.sql

This file was deleted.

5 changes: 0 additions & 5 deletions example/migration/sqlite/setup/setup.sql

This file was deleted.

32 changes: 28 additions & 4 deletions example/presentation/CommandOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,16 @@ protected function getArguments(InputInterface $input): InputArgs
$options['dbName'] = $this->getOptionDbName($input);
}

if ($input->hasOption('without-repeatable')) {
$options['hasRepeatable'] = $this->getOptionWithoutRepeatable($input) === false; // inversion
if ($input->hasOption('with-repeatable')) {
$options['hasRepeatable'] = $this->getOptionWithRepeatable($input);
}

if ($input->hasOption('latest-version')) {
$options['applyLatestVersion'] = $this->getOptionApplyLatestVersion($input);
}

if ($input->hasOption('exactly-all')) {
$options['exactlyAll'] = $this->getOptionExactlyAll($input);
}

return new InputArgs(...$options);
Expand Down Expand Up @@ -61,9 +69,25 @@ private function getOptionDryRun(InputInterface $input): bool
/**
* @throws InvalidArgumentException
*/
private function getOptionWithoutRepeatable(InputInterface $input): bool
private function getOptionApplyLatestVersion(InputInterface $input): bool
{
return $input->getOption('latest-version') === true;
}

/**
* @throws InvalidArgumentException
*/
private function getOptionExactlyAll(InputInterface $input): bool
{
return $input->getOption('exactly-all') === true;
}

/**
* @throws InvalidArgumentException
*/
private function getOptionWithRepeatable(InputInterface $input): bool
{
return $input->getOption('without-repeatable') === true;
return $input->getOption('with-repeatable') === true;
}

/**
Expand Down
20 changes: 18 additions & 2 deletions example/presentation/DownCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,24 @@ public function __construct(private readonly MigratorInterface $migrator)
protected function configure(): void
{
$this->addOption('db', null, InputOption::VALUE_OPTIONAL, 'Name database');
$this->addOption('limit', null, InputOption::VALUE_OPTIONAL, 'Number of files processed');
$this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Dry run');
$this->addOption(
'limit',
null,
InputOption::VALUE_OPTIONAL,
'Sets the maximum number of migrations to be executed or rolled back.'
);
$this->addOption(
'latest-version',
null,
InputOption::VALUE_NONE,
'Targets the most recent version for rollback.'
);
$this->addOption(
'dry-run',
null,
InputOption::VALUE_NONE,
'Simulates the migration process without applying any changes to the database.'
);
}

#[Override]
Expand Down
20 changes: 18 additions & 2 deletions example/presentation/RedoCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,24 @@ public function __construct(private readonly MigratorInterface $migrator)
protected function configure(): void
{
$this->addOption('db', null, InputOption::VALUE_OPTIONAL, 'Name database');
$this->addOption('limit', null, InputOption::VALUE_OPTIONAL, 'Number of files processed');
$this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Dry run');
$this->addOption(
'limit',
null,
InputOption::VALUE_OPTIONAL,
'Sets the maximum number of migrations to be executed or rolled back.'
);
$this->addOption(
'latest-version',
null,
InputOption::VALUE_NONE,
'Targets the most recent version for rollback.'
);
$this->addOption(
'dry-run',
null,
InputOption::VALUE_NONE,
'Simulates the migration process without applying any changes to the database.'
);
}

#[Override]
Expand Down
29 changes: 26 additions & 3 deletions example/presentation/UpCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,32 @@ public function __construct(private readonly MigratorInterface $migrator)
protected function configure(): void
{
$this->addOption('db', null, InputOption::VALUE_OPTIONAL, 'Name database');
$this->addOption('limit', null, InputOption::VALUE_OPTIONAL, 'Number of files processed');
$this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Dry run');
$this->addOption('without-repeatable', null, InputOption::VALUE_NONE, 'Repeatable disable');
$this->addOption(
'limit',
null,
InputOption::VALUE_OPTIONAL,
'Sets the maximum number of migrations to be executed or rolled back.'
);
$this->addOption(
'dry-run',
null,
InputOption::VALUE_NONE,
'Simulates the migration process without applying any changes to the database.'
);
$this->addOption(
'exactly-all',
null,
InputOption::VALUE_NONE,
'Ensures atomic execution of all migrations. ' .
'If any migration fails, the entire batch is rolled back, leaving the database unchanged.'
);
$this->addOption(
'with-repeatable',
null,
InputOption::VALUE_NONE,
'Includes migrations from the repeatable directory in the execution. ' .
'These scripts typically run every time their content changes, regardless of versioning.'
);
}

#[Override]
Expand Down
2 changes: 1 addition & 1 deletion infection.json.dist
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
]
},
"threads": "max",
"minCoveredMsi": 97,
"minCoveredMsi": 99,
"phpUnit": {
"configDir": "."
},
Expand Down
10 changes: 8 additions & 2 deletions src/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,25 @@

namespace kuaukutsu\poc\migration;

/**
* @infection-ignore-all IncrementInteger
*/
final readonly class Context
{
/**
* @param non-empty-string $dbName
* @param non-empty-string $filename
* @param non-empty-string $queryString
* @param non-empty-string $query
* @param non-negative-int $version
*/
public function __construct(
public string $dbName,
public string $filename,
public string $queryString,
public string $query,
public int $version = 0,
public bool $dryRun = false,
) {
assert($this->version >= 0);
}

public function getName(): string
Expand Down
24 changes: 22 additions & 2 deletions src/InputArgs.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,42 @@
{
/**
* @param non-negative-int $limit
* @param non-negative-int $version
*/
public function __construct(
public int $limit = 0,
public int $version = 0,
public bool $dryRun = false,
public bool $hasRepeatable = true,
public ?string $dbName = null,
public bool $exactlyAll = false,
private bool $hasRepeatable = false,
private bool $applyLatestVersion = false,
) {
assert($this->limit >= 0);
assert($this->version >= 0);
}

public function withResetLimit(): self
{
return new self(
version: $this->version,
dryRun: $this->dryRun,
hasRepeatable: $this->hasRepeatable,
dbName: $this->dbName,
exactlyAll: $this->exactlyAll,
hasRepeatable: $this->hasRepeatable,
applyLatestVersion: $this->applyLatestVersion,
);
}

public function hasApplyLatestVersion(): bool
{
return $this->applyLatestVersion
&& $this->version === 0
&& ($this->limit === 0 || $this->limit > 1);
}

public function hasRepeatable(): bool
{
return $this->hasRepeatable && $this->dryRun === false;
}
}
4 changes: 0 additions & 4 deletions src/Migrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,6 @@ public function up(InputArgs $args = new InputArgs()): void
{
foreach ($this->selectDb($args) as $migration) {
$this->actionWorkflow->up($migration, $args);

if ($args->hasRepeatable) {
$this->actionWorkflow->repeatable($migration, $args);
}
}
}

Expand Down
9 changes: 7 additions & 2 deletions src/connection/StatementInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@
interface StatementInterface
{
/**
* @return list<non-empty-string>
* @param non-empty-string $query
* @param array<string, scalar|null> $params
* @return array<non-empty-string, non-negative-int> <key: filename; value: version>
*/
public function query(string $query): array;
public function fetchRecord(string $query, array $params = []): array;

/**
* @param non-empty-string $query
*/
public function exec(string $query): void;
}
4 changes: 3 additions & 1 deletion src/connection/mysql/migration/setup.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
CREATE TABLE IF NOT EXISTS `%SYSTEM_TABLE%`
(
`name` varchar(512) COLLATE utf8mb4_unicode_ci NOT NULL,
`version` BIGINT UNSIGNED DEFAULT 0,
`atime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`name`)
PRIMARY KEY (`name`),
KEY `i_%SYSTEM_TABLE%_version` (`version`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
3 changes: 3 additions & 0 deletions src/connection/pgsql/migration/setup.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
CREATE TABLE IF NOT EXISTS "%SYSTEM_TABLE%"
(
"name" varchar(512) NOT NULL PRIMARY KEY,
"version" bigint DEFAULT 0,
"atime" timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS "i_%SYSTEM_TABLE%_version" ON %SYSTEM_TABLE% ("version");
5 changes: 4 additions & 1 deletion src/connection/sqlite/migration/setup.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
CREATE TABLE IF NOT EXISTS %SYSTEM_TABLE%
(
name TEXT PRIMARY KEY,
version INT DEFAULT 0,
atime TEXT
)
);

CREATE INDEX IF NOT EXISTS i_%SYSTEM_TABLE%_version ON %SYSTEM_TABLE%(version);
Loading