Skip to content

Commit 0b78c0f

Browse files
authored
Merge pull request #12 from byjg/2.1.0
2.1.0
2 parents 3dfa8ee + 7abea70 commit 0b78c0f

23 files changed

+299
-50
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,6 @@ crashlytics-build.properties
4848
fabric.properties
4949
/composer.lock
5050
/vendor/
51+
node_modules
52+
package-lock.json
53+
.usdocker

.idea/runConfigurations/Test_Postgres.xml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.travis.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,18 @@ before_install:
2626

2727
install:
2828
- composer install
29-
- node_modules/.bin/usdocker mssql status
3029
- node_modules/.bin/usdocker postgres status
3130
- node_modules/.bin/usdocker mysql status
31+
- node_modules/.bin/usdocker mssql status
3232

3333
script:
3434
- vendor/bin/phpunit
35-
# - vendor/bin/phpunit tests/SqlServerDatabaseTest.php
3635
- vendor/bin/phpunit tests/PostgresDatabaseTest.php
3736
- vendor/bin/phpunit tests/MysqlDatabaseTest.php
37+
- vendor/bin/phpunit tests/SqlServerDatabaseTest.php
38+
39+
- vendor/bin/phpunit tests/SqliteDatabaseCustomTest.php
40+
- vendor/bin/phpunit tests/PostgresDatabaseCustomTest.php
41+
- vendor/bin/phpunit tests/MysqlDatabaseCustomTest.php
42+
- vendor/bin/phpunit tests/SqlServerDatabaseCustomTest.php
3843

README.md

Lines changed: 161 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,158 @@ $migration->reset();
241241
$migration->up();
242242
```
243243

244-
The Migration object controls the database version.
244+
The Migration object controls the database version.
245245

246246

247+
### Tips on writing SQL migrations
248+
249+
#### Rely on explicit transactions
250+
251+
```sql
252+
-- DO
253+
BEGIN;
254+
255+
ALTER TABLE 1;
256+
UPDATE 1;
257+
UPDATE 2;
258+
UPDATE 3;
259+
ALTER TABLE 2;
260+
261+
COMMIT;
262+
263+
264+
-- DON'T
265+
ALTER TABLE 1;
266+
UPDATE 1;
267+
UPDATE 2;
268+
UPDATE 3;
269+
ALTER TABLE 2;
270+
```
271+
272+
It is generally desirable to wrap migration scripts inside a `BEGIN; ... COMMIT;` block.
273+
This way, if _any_ of the inner statements fail, _none_ of them are committed and the
274+
database does not end up in an inconsistent state.
275+
276+
Mind that in case of a failure `byjg/migration` will always mark the migration as `partial`
277+
and warn you when you attempt to run it again. The difference is that with explicit
278+
transactions you know that the database cannot be in an inconsistent state after an
279+
unexpected failure.
280+
281+
#### On creating triggers and SQL functions
282+
283+
```sql
284+
-- DO
285+
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
286+
BEGIN
287+
-- Check that empname and salary are given
288+
IF NEW.empname IS NULL THEN
289+
RAISE EXCEPTION 'empname cannot be null'; -- it doesn't matter if these comments are blank or not
290+
END IF; --
291+
IF NEW.salary IS NULL THEN
292+
RAISE EXCEPTION '% cannot have null salary', NEW.empname; --
293+
END IF; --
294+
295+
-- Who works for us when they must pay for it?
296+
IF NEW.salary < 0 THEN
297+
RAISE EXCEPTION '% cannot have a negative salary', NEW.empname; --
298+
END IF; --
299+
300+
-- Remember who changed the payroll when
301+
NEW.last_date := current_timestamp; --
302+
NEW.last_user := current_user; --
303+
RETURN NEW; --
304+
END; --
305+
$emp_stamp$ LANGUAGE plpgsql;
306+
307+
308+
-- DON'T
309+
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
310+
BEGIN
311+
-- Check that empname and salary are given
312+
IF NEW.empname IS NULL THEN
313+
RAISE EXCEPTION 'empname cannot be null';
314+
END IF;
315+
IF NEW.salary IS NULL THEN
316+
RAISE EXCEPTION '% cannot have null salary', NEW.empname;
317+
END IF;
318+
319+
-- Who works for us when they must pay for it?
320+
IF NEW.salary < 0 THEN
321+
RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;
322+
END IF;
323+
324+
-- Remember who changed the payroll when
325+
NEW.last_date := current_timestamp;
326+
NEW.last_user := current_user;
327+
RETURN NEW;
328+
END;
329+
$emp_stamp$ LANGUAGE plpgsql;
330+
```
331+
332+
Since the `PDO` database abstraction layer cannot run batches of SQL statements,
333+
when `byjg/migration` reads a migration file it has to split up the whole contents of the SQL
334+
file at the semicolons, and run the statements one by one. However, there is one kind of
335+
statement that can have multiple semicolons in-between its body: functions.
336+
337+
In order to be able to parse functions correctly, `byjg/migration` 2.1.0 started splitting migration
338+
files at the `semicolon + EOL` sequence instead of just the semicolon. This way, if you append an empty
339+
comment after every inner semicolon of a function definition `byjg/migration` will be able to parse it.
340+
341+
Unfortunately, if you forget to add any of these comments the library will split the `CREATE FUNCTION` statement in
342+
multiple parts and the migration will fail.
343+
344+
#### Avoid the colon character (`:`)
345+
346+
```sql
347+
-- DO
348+
CREATE TABLE bookings (
349+
booking_id UUID PRIMARY KEY,
350+
booked_at TIMESTAMPTZ NOT NULL CHECK (CAST(booked_at AS DATE) <= check_in),
351+
check_in DATE NOT NULL
352+
);
353+
354+
355+
-- DON'T
356+
CREATE TABLE bookings (
357+
booking_id UUID PRIMARY KEY,
358+
booked_at TIMESTAMPTZ NOT NULL CHECK (booked_at::DATE <= check_in),
359+
check_in DATE NOT NULL
360+
);
361+
```
362+
363+
Since `PDO` uses the colon character to prefix named parameters in prepared statements, its use will trip it
364+
up in other contexts.
365+
366+
For instance, PostgreSQL statements can use `::` to cast values between types. On the other hand `PDO` will
367+
read this as an invalid named parameter in an invalid context and fail when it tries to run it.
368+
369+
The only way to fix this inconsistency is avoiding colons altogether (in this case, PostgreSQL also has an alternative
370+
syntax: `CAST(value AS type)`).
371+
372+
#### Use an SQL editor
373+
374+
Finally, writing manual SQL migrations can be tiresome, but it is significantly easier if
375+
you use an editor capable of understanding the SQL syntax, providing autocomplete,
376+
introspecting your current database schema and/or autoformatting your code.
377+
378+
379+
### Handle different migration inside one schema
380+
381+
If you need to create different migration scripts and version inside the same schema it is possible
382+
but is too risky and I do not recommend at all.
383+
384+
To do this, you need to create different "migration tables" by passing the parameter to the constructor.
385+
386+
```php
387+
<?php
388+
$migration = new \ByJG\DbMigration\Migration("db:/uri", "/path", true, "NEW_MIGRATION_TABLE_NAME");
389+
```
390+
391+
For security reasons, this feature is not available at command line, but you can use the environment variable
392+
`MIGRATION_VERSION` to store the name.
393+
394+
We really recommend do not use this feature. The recommendation is one migration for one schema.
395+
247396

248397
## Unit Tests
249398

@@ -252,10 +401,10 @@ This library has integrated tests and need to be setup for each database you wan
252401
Basiclly you have the follow tests:
253402

254403
```
255-
phpunit tests/SqliteDatabaseTest.php
256-
phpunit tests/MysqlDatabaseTest.php
257-
phpunit tests/PostgresDatabaseTest.php
258-
phpunit tests/SqlServerDatabaseTest.php
404+
vendor/bin/phpunit tests/SqliteDatabaseTest.php
405+
vendor/bin/phpunit tests/MysqlDatabaseTest.php
406+
vendor/bin/phpunit tests/PostgresDatabaseTest.php
407+
vendor/bin/phpunit tests/SqlServerDatabaseTest.php
259408
```
260409

261410
### Using Docker for testing
@@ -264,7 +413,8 @@ phpunit tests/SqlServerDatabaseTest.php
264413

265414
```bash
266415
npm i @usdocker/usdocker @usdocker/mysql
267-
./node_modules/.bin/usdocker --refresh mysql up --home /tmp
416+
./node_modules/.bin/usdocker --refresh --home /tmp
417+
./node_modules/.bin/usdocker mysql up --home /tmp
268418

269419
docker run -it --rm \
270420
--link mysql-container \
@@ -278,7 +428,8 @@ docker run -it --rm \
278428

279429
```bash
280430
npm i @usdocker/usdocker @usdocker/postgres
281-
./node_modules/.bin/usdocker --refresh postgres up --home /tmp
431+
./node_modules/.bin/usdocker --refresh --home /tmp
432+
./node_modules/.bin/usdocker postgres up --home /tmp
282433

283434
docker run -it --rm \
284435
--link postgres-container \
@@ -292,14 +443,15 @@ docker run -it --rm \
292443

293444
```bash
294445
npm i @usdocker/usdocker @usdocker/mssql
295-
./node_modules/.bin/usdocker --refresh mssql up --home /tmp
446+
./node_modules/.bin/usdocker --refresh --home /tmp
447+
./node_modules/.bin/usdocker mssql up --home /tmp
296448

297449
docker run -it --rm \
298450
--link mssql-container \
299451
-v $PWD:/work \
300452
-w /work \
301453
byjg/php:7.2-cli \
302-
phpunit tests/SqlserverDatabaseTest
454+
phpunit tests/SqlServerDatabaseTest
303455
```
304456

305457
## Related Projects

scripts/migrate

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ require_once($autoload);
1212

1313
use Symfony\Component\Console\Application;
1414

15-
$application = new Application('Migrate Script by JG', '2.0.4');
15+
$application = new Application('Migrate Script by JG', '2.1.0');
1616
$application->add(new \ByJG\DbMigration\Console\ResetCommand());
1717
$application->add(new \ByJG\DbMigration\Console\UpCommand());
1818
$application->add(new \ByJG\DbMigration\Console\DownCommand());

src/Console/ConsoleCommand.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ protected function configure()
2929
->addOption(
3030
'path',
3131
'p',
32-
InputOption::VALUE_OPTIONAL,
32+
InputOption::VALUE_REQUIRED,
3333
'Define the path where the base.sql resides. If not set assumes the current folder'
3434
)
3535
->addOption(
3636
'up-to',
3737
'u',
38-
InputOption::VALUE_OPTIONAL,
38+
InputOption::VALUE_REQUIRED,
3939
'Run up to the specified version'
4040
)
4141
->addOption(
@@ -89,8 +89,10 @@ protected function initialize(InputInterface $input, OutputInterface $output)
8989

9090
$requiredBase = !$input->getOption('no-base');
9191

92+
$migrationTable = (empty(getenv('MIGRATE_TABLE')) ? "migration_version" : getenv('MIGRATE_TABLE'));
93+
$this->path = realpath($this->path);
9294
$uri = new Uri($this->connection);
93-
$this->migration = new Migration($uri, $this->path, $requiredBase);
95+
$this->migration = new Migration($uri, $this->path, $requiredBase, $migrationTable);
9496
$this->migration
9597
->registerDatabase('sqlite', SqliteDatabase::class)
9698
->registerDatabase('mysql', MySqlDatabase::class)

src/Console/ResetCommand.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use ByJG\DbMigration\Exception\ResetDisabledException;
66
use Symfony\Component\Console\Input\InputInterface;
7+
use Symfony\Component\Console\Input\InputOption;
78
use Symfony\Component\Console\Output\OutputInterface;
89
use Symfony\Component\Console\Question\ConfirmationQuestion;
910

@@ -15,7 +16,12 @@ protected function configure()
1516
$this
1617
->setName('reset')
1718
->setDescription('Create a fresh new database')
18-
->addOption('yes', null, null, 'Answer yes to any interactive question');
19+
->addOption(
20+
'yes',
21+
null,
22+
InputOption::VALUE_NONE,
23+
'Answer yes to any interactive question'
24+
);
1925
}
2026

2127
/**

src/Database/AbstractDatabase.php

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,29 @@ abstract class AbstractDatabase implements DatabaseInterface
1919
* @var \Psr\Http\Message\UriInterface
2020
*/
2121
private $uri;
22+
/**
23+
* @var string
24+
*/
25+
private $migrationTable;
2226

2327
/**
2428
* Command constructor.
2529
*
2630
* @param UriInterface $uri
31+
* @param string $migrationTable
2732
*/
28-
public function __construct(UriInterface $uri)
33+
public function __construct(UriInterface $uri, $migrationTable = 'migration_version')
2934
{
3035
$this->uri = $uri;
36+
$this->migrationTable = $migrationTable;
37+
}
38+
39+
/**
40+
* @return string
41+
*/
42+
public function getMigrationTable()
43+
{
44+
return $this->migrationTable;
3145
}
3246

3347
/**
@@ -50,13 +64,13 @@ public function getVersion()
5064
{
5165
$result = [];
5266
try {
53-
$result['version'] = $this->getDbDriver()->getScalar('SELECT version FROM migration_version');
67+
$result['version'] = $this->getDbDriver()->getScalar('SELECT version FROM ' . $this->getMigrationTable());
5468
} catch (\Exception $ex) {
5569
throw new DatabaseNotVersionedException('This database does not have a migration version. Please use "migrate reset" or "migrate install" to create one.');
5670
}
5771

5872
try {
59-
$result['status'] = $this->getDbDriver()->getScalar('SELECT status FROM migration_version');
73+
$result['status'] = $this->getDbDriver()->getScalar('SELECT status FROM ' . $this->getMigrationTable());
6074
} catch (\Exception $ex) {
6175
throw new OldVersionSchemaException('This database does not have a migration version. Please use "migrate install" for update it.');
6276
}
@@ -71,10 +85,10 @@ public function getVersion()
7185
public function setVersion($version, $status)
7286
{
7387
$this->getDbDriver()->execute(
74-
'UPDATE migration_version SET version = :version, status = :status',
88+
'UPDATE ' . $this->getMigrationTable() . ' SET version = :version, status = :status',
7589
[
7690
'version' => $version,
77-
'status' => $status
91+
'status' => $status,
7892
]
7993
);
8094
}
@@ -88,7 +102,7 @@ protected function checkExistsVersion()
88102
// Get the version to check if exists
89103
$versionInfo = $this->getVersion();
90104
if (empty($versionInfo['version'])) {
91-
$this->getDbDriver()->execute("insert into migration_version values(0, 'unknow')");
105+
$this->getDbDriver()->execute("insert into " . $this->getMigrationTable() . " values(0, 'unknow')");
92106
}
93107
}
94108

@@ -97,8 +111,8 @@ protected function checkExistsVersion()
97111
*/
98112
public function updateVersionTable()
99113
{
100-
$currentVersion = $this->getDbDriver()->getScalar('select version from migration_version');
101-
$this->getDbDriver()->execute('drop table migration_version');
114+
$currentVersion = $this->getDbDriver()->getScalar('select version from ' . $this->getMigrationTable());
115+
$this->getDbDriver()->execute('drop table ' . $this->getMigrationTable());
102116
$this->createVersion();
103117
$this->setVersion($currentVersion, 'unknow');
104118
}

0 commit comments

Comments
 (0)