Skip to content

Commit 80f515c

Browse files
authored
Reset hashing (skip certain migrations) (#374)
* wip: reset hashing for #114 * feature: check integrity with optional skip-to point for #114 * test: cli tests for reset flag * feature: reset hashing in database closes #114
1 parent 4a84792 commit 80f515c

5 files changed

Lines changed: 529 additions & 52 deletions

File tree

src/Cli/ExecuteCommand.php

Lines changed: 76 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,52 +14,102 @@
1414

1515
class ExecuteCommand extends Command {
1616
public function run(?ArgumentValueList $arguments = null):void {
17-
$forced = $arguments->contains("force");
18-
1917
$repoBasePath = getcwd();
2018
$defaultPath = $this->getDefaultPath($repoBasePath);
21-
2219
$config = $this->getConfig($repoBasePath, $defaultPath);
2320

24-
$settings = new Settings(
21+
$settings = $this->buildSettingsFromConfig($config, $repoBasePath);
22+
[$migrationPath, $migrationTable] = $this->getMigrationLocation($config, $repoBasePath);
23+
24+
$migrator = new Migrator($settings, $migrationPath, $migrationTable);
25+
$migrator->setOutput(
26+
$this->stream->getOutStream(),
27+
$this->stream->getErrorStream()
28+
);
29+
30+
if($this->isForced($arguments)) {
31+
$migrator->deleteAndRecreateSchema();
32+
}
33+
34+
$migrator->selectSchema();
35+
$migrator->createMigrationTable();
36+
$migrationCount = $migrator->getMigrationCount();
37+
$migrationFileList = $migrator->getMigrationFileList();
38+
39+
$runFrom = $this->calculateResetNumber($arguments, $migrationFileList, $migrator, $migrationCount);
40+
41+
$this->executeMigrations($migrator, $migrationFileList, $runFrom);
42+
}
43+
44+
/** Determine whether the --force flag was provided. */
45+
private function isForced(?ArgumentValueList $arguments):bool {
46+
return $arguments?->contains("force") ?? false;
47+
}
48+
49+
/** Build Settings from config for the current repository. */
50+
private function buildSettingsFromConfig(\Gt\Config\Config $config, string $repoBasePath): Settings {
51+
return new Settings(
2552
implode(DIRECTORY_SEPARATOR, [
2653
$repoBasePath,
2754
$config->get("database.query_path")
2855
]),
29-
3056
$config->get("database.driver") ?? 'mysql',
3157
$config->get("database.schema"),
3258
$config->get("database.host") ?? "localhost",
3359
(int)($config->get("database.port") ?? "3306"),
3460
$config->get("database.username"),
3561
$config->get("database.password")
3662
);
63+
}
3764

65+
/**
66+
* Return [migrationPath, migrationTable] derived from config.
67+
*
68+
* @return list<string>
69+
*/
70+
private function getMigrationLocation(\Gt\Config\Config $config, string $repoBasePath): array {
3871
$migrationPath = implode(DIRECTORY_SEPARATOR, [
3972
$repoBasePath,
4073
$config->get("database.query_path") ?? "query",
4174
$config->get("database.migration_path") ?? "_migration",
4275
]);
4376
$migrationTable = $config->get("database.migration_table") ?? "_migration";
77+
return [$migrationPath, $migrationTable];
78+
}
4479

45-
$migrator = new Migrator($settings, $migrationPath, $migrationTable);
46-
$migrator->setOutput(
47-
$this->stream->getOutStream(),
48-
$this->stream->getErrorStream()
49-
);
50-
51-
if($forced) {
52-
$migrator->deleteAndRecreateSchema();
80+
/**
81+
* Calculate the migration start point from --reset or current migration count.
82+
*
83+
* @param list<string> $migrationFileList
84+
*/
85+
private function calculateResetNumber(
86+
?ArgumentValueList $arguments,
87+
array $migrationFileList,
88+
Migrator $migrator,
89+
int $migrationCount
90+
): int {
91+
$resetNumber = null;
92+
if($arguments?->contains("reset")) {
93+
$resetNumber = $arguments->get("reset")->get();
94+
if(!$resetNumber) {
95+
$lastKey = array_key_last($migrationFileList);
96+
$lastNumber = $migrator->extractNumberFromFilename($migrationFileList[$lastKey]);
97+
$resetNumber = max(0, $lastNumber - 1);
98+
}
99+
$resetNumber = (int)$resetNumber;
53100
}
101+
return $resetNumber ?? $migrationCount;
102+
}
54103

55-
$migrator->selectSchema();
56-
$migrator->createMigrationTable();
57-
$migrationCount = $migrator->getMigrationCount();
58-
$migrationFileList = $migrator->getMigrationFileList();
59-
104+
/**
105+
* Wrap integrity check and perform migration with error handling.
106+
*
107+
* @param list<string> $migrationFileList
108+
*/
109+
private function executeMigrations(Migrator $migrator, array $migrationFileList, int $runFrom): void {
60110
try {
61-
$migrator->checkIntegrity($migrationFileList, $migrationCount);
62-
$migrator->performMigration($migrationFileList, $migrationCount);
111+
$migrator->checkIntegrity($migrationFileList, $runFrom);
112+
$migrator->performMigration($migrationFileList, $runFrom);
63113
}
64114
catch(MigrationIntegrityException $exception) {
65115
$this->writeLine(
@@ -106,6 +156,12 @@ public function getOptionalParameterList():array {
106156
"force",
107157
"f",
108158
"Forcefully drop the current schema and run from migration 1"
159+
),
160+
new Parameter(
161+
true,
162+
"reset",
163+
"r",
164+
"Reset the integrity checks to a specific migration number"
109165
)
110166
];
111167
}

src/Migration/Migrator.php

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public function createMigrationTable():void {
8989
$this->dbClient->executeSql(implode("\n", [
9090
"create table if not exists `{$this->tableName}` (",
9191
"`" . self::COLUMN_QUERY_NUMBER . "` int primary key,",
92-
"`" . self::COLUMN_QUERY_HASH . "` varchar(32) not null,",
92+
"`" . self::COLUMN_QUERY_HASH . "` varchar(32) null,",
9393
"`" . self::COLUMN_MIGRATED_AT . "` datetime not null )",
9494
]));
9595
}
@@ -125,54 +125,62 @@ public function getMigrationFileList():array {
125125

126126
/** @param array<string> $fileList */
127127
public function checkFileListOrder(array $fileList):void {
128-
$counter = 0;
128+
$previousNumber = null;
129129
$sequence = [];
130130

131131
foreach($fileList as $file) {
132-
$counter++;
133132
$migrationNumber = $this->extractNumberFromFilename($file);
134133
$sequence []= $migrationNumber;
135134

136-
if($counter !== $migrationNumber) {
137-
throw new MigrationSequenceOrderException(
138-
"Missing: $counter"
139-
);
135+
if(!is_null($previousNumber)) {
136+
if($migrationNumber === $previousNumber) {
137+
throw new MigrationSequenceOrderException("Duplicate: $migrationNumber");
138+
}
139+
if($migrationNumber < $previousNumber) {
140+
throw new MigrationSequenceOrderException("Out of order: $migrationNumber before $previousNumber");
141+
}
140142
}
143+
144+
$previousNumber = $migrationNumber;
141145
}
142146
}
143147

144148
/** @param array<string> $migrationFileList */
145149
public function checkIntegrity(
146150
array $migrationFileList,
147-
?int $migrationCount = null
151+
?int $migrationStartFrom = null
148152
):int {
149153
$fileNumber = 0;
154+
155+
foreach($migrationFileList as $file) {
156+
$fileNumber = $this->extractNumberFromFilename($file);
157+
158+
// If a start point is provided, skip files at or before that number
159+
// and only verify files AFTER the provided migration count.
160+
if(!is_null($migrationStartFrom) && $fileNumber <= $migrationStartFrom) {
161+
continue;
162+
}
150163

151-
foreach($migrationFileList as $i => $file) {
152-
$fileNumber = $i + 1;
153164
$md5 = md5_file($file);
154165

155-
if(is_null($migrationCount)
156-
|| $fileNumber <= $migrationCount) {
157-
$result = $this->dbClient->executeSql(implode("\n", [
158-
"select `" . self::COLUMN_QUERY_HASH . "`",
159-
"from `{$this->tableName}`",
160-
"where `" . self::COLUMN_QUERY_NUMBER . "` = ?",
161-
"limit 1",
162-
]), [$fileNumber]);
166+
$result = $this->dbClient->executeSql(implode("\n", [
167+
"select `" . self::COLUMN_QUERY_HASH . "`",
168+
"from `{$this->tableName}`",
169+
"where `" . self::COLUMN_QUERY_NUMBER . "` = ?",
170+
"limit 1",
171+
]), [$fileNumber]);
163172

164-
$hashInDb = ($result->fetch())->getString(self::COLUMN_QUERY_HASH);
173+
$hashInDb = ($result->fetch())?->getString(self::COLUMN_QUERY_HASH);
165174

166-
if($hashInDb !== $md5) {
167-
throw new MigrationIntegrityException($file);
168-
}
175+
if($hashInDb && $hashInDb !== $md5) {
176+
throw new MigrationIntegrityException($file);
169177
}
170178
}
171179

172180
return $fileNumber;
173181
}
174182

175-
protected function extractNumberFromFilename(string $pathName):int {
183+
public function extractNumberFromFilename(string $pathName):int {
176184
$file = new SplFileInfo($pathName);
177185
$filename = $file->getFilename();
178186
preg_match("/(\d+)-?.*\.sql/", $filename, $matches);
@@ -187,15 +195,15 @@ protected function extractNumberFromFilename(string $pathName):int {
187195
/** @param array<string> $migrationFileList */
188196
public function performMigration(
189197
array $migrationFileList,
190-
int $existingMigrationCount = 0
198+
int $existingFileNumber = 0
191199
):int {
192200
$fileNumber = 0;
193201
$numCompleted = 0;
202+
203+
foreach($migrationFileList as $file) {
204+
$fileNumber = $this->extractNumberFromFilename($file);
194205

195-
foreach($migrationFileList as $i => $file) {
196-
$fileNumber = $i + 1;
197-
198-
if($fileNumber <= $existingMigrationCount) {
206+
if($fileNumber <= $existingFileNumber) {
199207
continue;
200208
}
201209

@@ -282,7 +290,7 @@ public function selectSchema():void {
282290
}
283291
}
284292

285-
protected function recordMigrationSuccess(int $number, string $hash):void {
293+
protected function recordMigrationSuccess(int $number, ?string $hash):void {
286294
$now = "now()";
287295

288296
if($this->driver === Settings::DRIVER_SQLITE) {
@@ -300,6 +308,15 @@ protected function recordMigrationSuccess(int $number, string $hash):void {
300308
]), [$number, $hash]);
301309
}
302310

311+
/**
312+
* @param int $numberToForce A null-hashed migration will be marked as
313+
* successful with this number. This will allow the next number to be
314+
* executed out of sequence.
315+
*/
316+
public function resetMigrationSequence(int $numberToForce):void {
317+
$this->recordMigrationSuccess($numberToForce, null);
318+
}
319+
303320
/**
304321
* @codeCoverageIgnore
305322
*/

src/Query/QueryCollectionFactory.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ public function create(string $name):QueryCollection {
2727
}
2828

2929
return $this->queryCollectionCache[$name];
30-
3130
}
3231

3332
public function directoryExists(string $name):bool {

0 commit comments

Comments
 (0)