Skip to content

Commit

Permalink
Make sure the Migrations class uses the CakeAdapter and add a tempora…
Browse files Browse the repository at this point in the history
…ry fix to the SQLite adapter to prevent it from dropping tables when using dropForeignKey

Also change implementation on code tested using the Migrations class (which used to use the Phinx adapter and not the Cake one, leading to unexpected results when the Cake one is used)
  • Loading branch information
HavokInspiration committed Sep 18, 2015
1 parent d0d8d89 commit 155d42c
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 3 deletions.
141 changes: 139 additions & 2 deletions src/CakeAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Cake\Database\Connection;
use Phinx\Db\Adapter\AdapterInterface;
use Phinx\Db\Adapter\SQLiteAdapter;
use Phinx\Db\Table;
use Phinx\Db\Table\Column;
use Phinx\Db\Table\ForeignKey;
Expand Down Expand Up @@ -55,6 +56,11 @@ public function __construct(AdapterInterface $adapter, Connection $connection)
$connection->driver()->connection($pdo);
}

public function getConnection()
{
return $this->adapter->getConnection();
}

/**
* Get all migrated version numbers.
*
Expand Down Expand Up @@ -129,6 +135,60 @@ public function getOutput()
return $this->adapter->getOutput();
}

/**
* Sets the command start time
*
* @param int $time
* @return AdapterInterface
*/
public function setCommandStartTime($time)
{
return $this->adapter->setCommandStartTime($time);
}

/**
* Gets the command start time
*
* @return int
*/
public function getCommandStartTime()
{
return $this->adapter->getCommandStartTime();
}

/**
* Start timing a command.
*
* @return void
*/
public function startCommandTimer()
{
$this->adapter->startCommandTimer();
}

/**
* Stop timing the current command and write the elapsed time to the
* output.
*
* @return void
*/
public function endCommandTimer()
{
$this->adapter->endCommandTimer();
}

/**
* Write a Phinx command to the output.
*
* @param string $command Command Name
* @param array $args Command Args
* @return void
*/
public function writeCommand($command, $args = array())
{
$this->adapter->writeCommand($command, $args);
}

/**
* Records a migration being run.
*
Expand Down Expand Up @@ -506,6 +566,9 @@ public function addForeignKey(Table $table, ForeignKey $foreignKey)

/**
* Drops the specified foreign key from a database table.
* If the adapter property is an instance of the \Phinx\Db\Adapter\SQLiteAdapter,
* a specific method will be called. The original one from Phinx contains a bug
* that can drop a table in certain conditions.
*
* @param string $tableName
* @param string[] $columns Column(s)
Expand All @@ -514,7 +577,81 @@ public function addForeignKey(Table $table, ForeignKey $foreignKey)
*/
public function dropForeignKey($tableName, $columns, $constraint = null)
{
return $this->adapter->dropForeignKey($tableName, $columns, $constraint);
if (!($this->adapter instanceof SQLiteAdapter)) {
return $this->adapter->dropForeignKey($tableName, $columns, $constraint);
}

$this->dropForeignKeySqlite($tableName, $columns, $constraint);
}

/**
* Specific method to drop a foreign key when SQLite is being used.
* This is to prevent a bug from Phinx that could drop tables.
*
* @param string $tableName
* @param string[] $columns Column(s)
* @param string $constraint Constraint name
* @return void
*/
public function dropForeignKeySqlite($tableName, $columns, $constraint = null)
{
$this->startCommandTimer();
if (is_string($columns)) {
$columns = [$columns]; // str to array
}

$this->writeCommand('dropForeignKey', [$tableName, $columns]);

$tmpTableName = 'tmp_' . $tableName;

$rows = $this->fetchAll('select * from sqlite_master where `type` = \'table\'');

$sql = '';
foreach ($rows as $table) {
if ($table['tbl_name'] == $tableName) {
$sql = $table['sql'];
}
}

$rows = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
$replaceColumns = [];
foreach ($rows as $row) {
if (!in_array($row['name'], $columns)) {
$replaceColumns[] = $row['name'];
} else {
$found = true;
}
}

if (!isset($found)) {
throw new \InvalidArgumentException(sprintf(
'The specified column doesn\'t exist: '
));
}

$this->execute(sprintf('ALTER TABLE %s RENAME TO %s', $this->quoteTableName($tableName), $tmpTableName));

foreach ($columns as $columnName) {
$search = sprintf(
"/,[^,]*\(%s(?:,`?(.*)`?)?\) REFERENCES[^,]*\([^\)]*\)[^,)]*/",
$this->quoteColumnName($columnName)
);
$sql = preg_replace($search, '', $sql, 1);
}

$this->execute($sql);

$sql = sprintf(
'INSERT INTO %s(%s) SELECT %s FROM %s',
$tableName,
implode(', ', $columns),
implode(', ', $columns),
$tmpTableName
);

$this->execute($sql);
$this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tmpTableName)));
$this->endCommandTimer();
}

/**
Expand Down Expand Up @@ -557,7 +694,7 @@ public function getSqlType($type, $limit = null)
* @param array $options Options
* @return void
*/
public function createDatabase($name, $options = array())
public function createDatabase($name, $options = [])
{
return $this->adapter->createDatabase($name, $options);
}
Expand Down
26 changes: 26 additions & 0 deletions src/Migrations.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/
namespace Migrations;

use Cake\Datasource\ConnectionManager;
use Phinx\Config\Config;
use Phinx\Config\ConfigInterface;
use Symfony\Component\Console\Input\ArrayInput;
Expand Down Expand Up @@ -193,6 +194,7 @@ protected function run($method, $params, $input)
$this->setInput($input);
$newConfig = $this->getConfig(true);
$manager = $this->getManager($newConfig);

if (isset($migrationPath) && $newConfig->getMigrationPath() !== $migrationPath) {
$manager->resetMigrations();
}
Expand Down Expand Up @@ -220,9 +222,33 @@ public function getManager($config = null)
$this->manager->setConfig($config);
}

$this->setAdapter();
return $this->manager;
}

/**
* Sets the adapter the manager is going to need to operate on the DB
* This will make sure the adapter instance is a \Migrations\CakeAdapter instance
*
* @return void
*/
public function setAdapter()
{
if ($this->input !== null) {
$connectionName = 'default';
if ($this->input->getOption('connection')) {
$connectionName = $this->input->getOption('connection');
}
$connection = ConnectionManager::get($connectionName);

$env = $this->manager->getEnvironment('default');
$adapter = $env->getAdapter();
if (!$adapter instanceof CakeAdapter) {
$env->setAdapter(new CakeAdapter($adapter, $connection));
}
}
}

/**
* Get the input needed for each commands to be run
*
Expand Down
2 changes: 1 addition & 1 deletion src/Table.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public function create()
*/
protected function filterPrimaryKey()
{
if (!($this->getAdapter() instanceof SQLiteAdapter) || empty($this->options['primary_key'])) {
if ($this->getAdapter()->getAdapterType() !== 'sqlite' || empty($this->options['primary_key'])) {
return;
}

Expand Down
7 changes: 7 additions & 0 deletions tests/TestCase/MigrationsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ public function testStatus()
]
];
$this->assertEquals($expected, $result);

$adapter = $this->migrations
->getManager()
->getEnvironment('default')
->getAdapter();

$this->assertInstanceOf('\Migrations\CakeAdapter', $adapter);
}

/**
Expand Down

0 comments on commit 155d42c

Please sign in to comment.