Skip to content

Commit 68f0ccd

Browse files
authored
0 parents  commit 68f0ccd

File tree

1 file changed

+274
-0
lines changed

1 file changed

+274
-0
lines changed

simple-db-migrator.php

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
<?php
2+
/*
3+
* ॐ Om Brahmarppanam ॐ
4+
*
5+
* schema/migrator.php
6+
* Created at: Thu Jul 20 2022 19:34:40 GMT+0530 (GMT+05:30)
7+
*
8+
* Copyright 2022 Harish Karumuthil <harish2704@gmail.com>
9+
*
10+
* Use of this source code is governed by an MIT-style
11+
* license that can be found in the LICENSE file or at
12+
* https://opensource.org/licenses/MIT.
13+
*
14+
*/
15+
16+
require_once __DIR__ . "/../vendor/autoload.php";
17+
18+
// Load database credentials from database.php
19+
// where $dbPass, $dbName, $dbHost, $dbUser are defined.
20+
include __DIR__ . "/../database.php";
21+
22+
use Symfony\Component\Console\Command\Command;
23+
use Symfony\Component\Console\Input\InputInterface;
24+
use Symfony\Component\Console\Input\InputOption;
25+
use Symfony\Component\Console\Output\OutputInterface;
26+
use Symfony\Component\Console\Logger\ConsoleLogger;
27+
use Symfony\Component\Console\Application;
28+
29+
$MIGRAION_ROOT = __DIR__ . "/migrations";
30+
$L;
31+
32+
class MigrationItem
33+
{
34+
/**
35+
* @param $v {int} Version number
36+
*/
37+
public function __construct($v)
38+
{
39+
global $MIGRAION_ROOT;
40+
$this->v = $v;
41+
$this->upFile = sprintf("%s/up/%03d.sql", $MIGRAION_ROOT, $v);
42+
$this->downFile = sprintf("%s/down/%03d.sql", $MIGRAION_ROOT, $v);
43+
}
44+
45+
public function getSQL($fname)
46+
{
47+
return file_get_contents($fname);
48+
}
49+
50+
public function getUpSQL()
51+
{
52+
return $this->getSQL($this->upFile);
53+
}
54+
55+
public function getDownSql()
56+
{
57+
return $this->getSQL($this->downFile);
58+
}
59+
}
60+
61+
class Migrator
62+
{
63+
public function __construct()
64+
{
65+
global $dbPass, $dbName, $dbHost, $dbUser, $L;
66+
$this->db = new PDO("mysql:host=$dbHost;dbname=$dbName", $dbUser, $dbPass);
67+
$this->L = $L;
68+
}
69+
70+
private function runSQLTransaction($sql)
71+
{
72+
$sql = "BEGIN;
73+
$sql
74+
COMMIT;";
75+
76+
$this->L->debug("Runing SQL");
77+
$this->L->debug($sql);
78+
return $this->db->query($sql);
79+
}
80+
81+
/*
82+
* Get array of pending version numbers
83+
* @return int[]
84+
*/
85+
public function getPendingMigrations()
86+
{
87+
$lastRanMigration = $this->getLastRanMigration();
88+
$availableMigrations = $this->getAvailableMigrations();
89+
90+
if ($lastRanMigration == 0) {
91+
return $availableMigrations;
92+
}
93+
94+
$lastMigrationIdx = array_search($lastRanMigration, $availableMigrations);
95+
96+
if ($lastMigrationIdx === null) {
97+
throw new Exception(
98+
"Inconsistent state: Last migration is missing in filesystem"
99+
);
100+
}
101+
return array_slice($availableMigrations, $lastMigrationIdx + 1);
102+
}
103+
104+
/*
105+
* Get array of available verion numbers
106+
* @return int[]
107+
*/
108+
private function getAvailableMigrations()
109+
{
110+
global $MIGRAION_ROOT;
111+
$files = scandir("$MIGRAION_ROOT/up");
112+
$out = [];
113+
foreach ($files as $fname) {
114+
$match = [];
115+
$matches = preg_match('/^([0-9])*.sql$/', $fname, $match);
116+
if ($matches > 0) {
117+
$out[] = (int) $match[1];
118+
}
119+
}
120+
sort($out);
121+
return $out;
122+
}
123+
124+
/*
125+
* @return int
126+
*/
127+
private function getLastRanMigration()
128+
{
129+
try {
130+
$result = $this->db
131+
->query(
132+
"SELECT version from db_migrations order by version desc limit 1",
133+
PDO::FETCH_ASSOC
134+
)
135+
->fetchAll();
136+
} catch (Exception $e) {
137+
if ($e->errorInfo[0] == "42S02") {
138+
throw new Exception(
139+
"db_migrations table doesn't exists. Please run setup"
140+
);
141+
}
142+
}
143+
if ($result) {
144+
return $result[0]["version"];
145+
}
146+
return 0;
147+
}
148+
149+
public function runUp()
150+
{
151+
$this->L->warning("Running up");
152+
$pendingMigrations = $this->getPendingMigrations();
153+
$this->L->warning("Pending migrations " . json_encode($pendingMigrations));
154+
foreach ($pendingMigrations as $migrationV) {
155+
$this->L->warning("Running migration " . $migrationV);
156+
$migrationItem = new MigrationItem($migrationV);
157+
$sql = $migrationItem->getUpSQL();
158+
$this->runSQLTransaction($sql);
159+
$this->db
160+
->prepare(
161+
"INSERT INTO db_migrations
162+
(version, created_at, up_sql, down_sql) VALUES (?, now(), ?, ?)"
163+
)
164+
->execute([$migrationV, $sql, $migrationItem->getDownSql()]);
165+
}
166+
$this->L->warning("executed all pending migrations");
167+
}
168+
169+
public function setup()
170+
{
171+
$this->L->info("Creating db_migrations table ...");
172+
return $this->db->query("
173+
CREATE TABLE db_migrations (
174+
version int unsigned NOT NULL,
175+
created_at datetime DEFAULT NULL,
176+
up_sql longtext DEFAULT NULL,
177+
down_sql longtext DEFAULT NULL,
178+
PRIMARY KEY (version)
179+
)");
180+
}
181+
182+
/*
183+
* @return string
184+
*/
185+
private function getDownSqlFromDb($v)
186+
{
187+
$res = $this->db
188+
->query(
189+
"select down_sql from db_migrations where version = $v",
190+
PDO::FETCH_ASSOC
191+
)
192+
->fetchAll();
193+
return $res[0]["down_sql"];
194+
}
195+
196+
public function runDown()
197+
{
198+
$this->L->warning("Rolling back last migration ...");
199+
$lastRanMigration = $this->getLastRanMigration();
200+
if (!$lastRanMigration) {
201+
throw new Exception("There is no migration to rollback");
202+
}
203+
$this->L->warning("last migration is $lastRanMigration");
204+
$migrationItem = new MigrationItem($lastRanMigration);
205+
$downSqlFromDisk = $migrationItem->getDownSql();
206+
$downSqlFromDb = $this->getDownSqlFromDb($lastRanMigration);
207+
if ($downSqlFromDisk != $downSqlFromDb) {
208+
$this->L->error(
209+
"rollback sql stored in db does not match with the sql in filesystem"
210+
);
211+
$this->L->error("SQL from db");
212+
$this->L->error($downSqlFromDb);
213+
$this->L->error("SQL from filesystem");
214+
$this->L->error($downSqlFromDisk);
215+
$this->L->error("Please manually fix this error and run again");
216+
throw new Exception(
217+
"rollback sql stored in db does not match with the sql in filesystem"
218+
);
219+
}
220+
221+
$this->runSQLTransaction($downSqlFromDisk);
222+
$this->db
223+
->prepare("DELETE FROM db_migrations WHERE version = ?")
224+
->execute([$lastRanMigration]);
225+
226+
$this->L->warning("Rollback completed");
227+
}
228+
}
229+
230+
class DbMigrate extends Command
231+
{
232+
protected function configure()
233+
{
234+
$this->setName("db:migrate");
235+
$this->setDescription("Migrate DB to the latest version.");
236+
$this->addOption(
237+
"setup",
238+
"s",
239+
InputOption::VALUE_NONE,
240+
"Create db_migrations table in db"
241+
);
242+
$this->addOption(
243+
"down",
244+
"d",
245+
InputOption::VALUE_NONE,
246+
"Roll back last migration"
247+
);
248+
}
249+
250+
protected function execute(InputInterface $input, OutputInterface $output)
251+
{
252+
global $L;
253+
$L = new ConsoleLogger($output);
254+
$L->info("Starting migrator");
255+
$runSetup = $input->getOption("setup");
256+
$migrator = new Migrator();
257+
258+
if ($runSetup) {
259+
$migrator->setup();
260+
}
261+
if ($input->getOption("down")) {
262+
$migrator->runDown();
263+
} else {
264+
$migrator->runUp();
265+
}
266+
return 0;
267+
}
268+
}
269+
270+
$application = new Application("Migrator", "1.0.0");
271+
$command = new DbMigrate();
272+
$application->add($command);
273+
$application->setDefaultCommand($command->getName(), true);
274+
$application->run();

0 commit comments

Comments
 (0)