initial port of Mad migration libs
authorChuck Hagenbuch <chuck@horde.org>
Mon, 16 Feb 2009 20:05:08 +0000 (15:05 -0500)
committerChuck Hagenbuch <chuck@horde.org>
Mon, 16 Feb 2009 20:05:08 +0000 (15:05 -0500)
framework/Db/lib/Horde/Db/Migration/Base.php [new file with mode: 0644]
framework/Db/lib/Horde/Db/Migration/Exception.php [new file with mode: 0644]
framework/Db/lib/Horde/Db/Migration/Migrator.php [new file with mode: 0644]

diff --git a/framework/Db/lib/Horde/Db/Migration/Base.php b/framework/Db/lib/Horde/Db/Migration/Base.php
new file mode 100644 (file)
index 0000000..b569cdd
--- /dev/null
@@ -0,0 +1,161 @@
+<?php
+/**
+ * Copyright 2007 Maintainable Software, LLC
+ * Copyright 2006-2009 The Horde Project (http://www.horde.org/)
+ *
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Db
+ * @subpackage Migration
+ */
+
+/**
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Db
+ * @subpackage Migration
+ */
+class Horde_Db_Migration_Base
+{
+    /**
+     * Print messages as migrations happen
+     * @var boolean
+     */
+    public static $verbose = true;
+
+    /**
+     * The migration version
+     * @var integer
+     */
+    public $version = null;
+
+    protected $_connection;
+
+
+    /*##########################################################################
+    # Constructor
+    ##########################################################################*/
+
+    /**
+     */
+    public function __construct($context)
+    {
+        $this->version = $context['version'];
+        $this->_connection = $context['connection'];
+    }
+
+
+    /*##########################################################################
+    # Public
+    ##########################################################################*/
+
+    /**
+     * Proxy methods over to the connection
+     * @param   string  $method
+     * @param   array   $args
+     */
+    public function __call($method, $args)
+    {
+        foreach ($args as $arg) {
+            if (is_array($arg)) {
+                $vals = array();
+                foreach ($arg as $key => $value) {
+                    $vals[] = "$key => " . var_export($value, true);
+                }
+                $a[] = 'array(' . implode(', ', $vals) . ')';
+            } else {
+                $a[] = $arg;
+            }
+        }
+        $this->say("$method(" . implode(", ", $a) . ")");
+
+        // benchmark method call
+        $t = new Horde_Support_Timer();
+        $t->start();
+            $result = call_user_func_array(array($this->_connection, $method), $args);
+        $time = $t->finish();
+
+        // print stats
+        $this->say(sprintf("%.4fs", $time), 'subitem');
+        if (is_int($result)) {
+            $this->say("$result rows", 'subitem');
+        }
+
+        return $result;
+    }
+
+    public function upWithBechmarks()
+    {
+        $this->migrate('up');
+    }
+
+    public function downWithBenchmarks()
+    {
+        $this->migrate('down');
+    }
+
+    /**
+     * Execute this migration in the named direction
+     */
+    public function migrate($direction)
+    {
+        if (!method_exists($this, $direction)) { return; }
+
+        if ($direction == 'up')   { $this->announce("migrating"); }
+        if ($direction == 'down') { $this->announce("reverting"); }
+
+        $result = null;
+        $t = new Horde_Support_Timer();
+        $t->start();
+            $result = $this->$direction();
+        $time = $t->finish();
+
+        if ($direction == 'up') {
+            $this->announce("migrated (" . sprintf("%.4fs", $time) . ")");
+            $this->write();
+        }
+        if ($direction == 'down') {
+            $this->announce("reverted (" . sprintf("%.4fs", $time) . ")");
+            $this->write();
+        }
+        return $result;
+    }
+
+    /**
+     * @param   string  $text
+     */
+    public function write($text = '')
+    {
+        if (self::$verbose) {
+            echo "$text\n";
+        }
+    }
+
+    /**
+     * Announce migration
+     * @param   string  $message
+     */
+    public function announce($message)
+    {
+        $text = "$this->version " . get_class($this) . ": $message";
+        $length = 75 - strlen($text) > 0 ? 75 - strlen($text) : 0;
+
+        $this->write(sprintf("== %s %s", $text, str_repeat('=', $length)));
+    }
+
+    /**
+     * @param   string  $message
+     * @param   boolean $subitem
+     */
+    public function say($message, $subitem = false)
+    {
+        $this->write(($subitem ? "   ->" : "--") . " $message");
+    }
+
+}
diff --git a/framework/Db/lib/Horde/Db/Migration/Exception.php b/framework/Db/lib/Horde/Db/Migration/Exception.php
new file mode 100644 (file)
index 0000000..314643c
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+/**
+ * Copyright 2007 Maintainable Software, LLC
+ * Copyright 2006-2009 The Horde Project (http://www.horde.org/)
+ *
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Db
+ * @subpackage Migration
+ */
+
+/**
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Db
+ * @subpackage Migration
+ */
+class Horde_Db_Migration_Exception extends Horde_Db_Exception
+{
+}
diff --git a/framework/Db/lib/Horde/Db/Migration/Migrator.php b/framework/Db/lib/Horde/Db/Migration/Migrator.php
new file mode 100644 (file)
index 0000000..a1f0c92
--- /dev/null
@@ -0,0 +1,265 @@
+<?php
+/**
+ * Copyright 2007 Maintainable Software, LLC
+ * Copyright 2006-2009 The Horde Project (http://www.horde.org/)
+ *
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Db
+ * @subpackage Migration
+ */
+
+/**
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Db
+ * @subpackage Migration
+ */
+class Horde_Db_Migration_Migrator
+{
+    /**
+     * @var string
+     */
+    protected $_direction = null;
+
+    /**
+     * @var string
+     */
+    protected $_migrationsPath = null;
+
+    /**
+     * @var int
+     */
+    protected $_targetVersion = null;
+
+
+    /*##########################################################################
+    # Constructor
+    ##########################################################################*/
+
+    /**
+     * @param   string  $direction
+     * @param   string  $migrationsPath
+     * @param   int     $targetVersion
+     */
+    public function __construct($connection, $migrationsPath)
+    {
+        if (!$connection->supportsMigrations()) {
+            $msg = 'This database does not yet support migrations';
+            throw new Horde_Db_Migration_Exception($msg);
+        }
+
+        $this->_connection     = $connection;
+        $this->_migrationsPath = $migrationsPath;
+        $this->_logger         = $logger;
+
+        $this->_connection->initializeSchemaInformation();
+    }
+
+
+    /*##########################################################################
+    # Public
+    ##########################################################################*/
+
+    /**
+     * @param   string            $targetVersion
+     */
+    public function migrate($targetVersion = null)
+    {
+        $currentVersion = $this->getCurrentVersion();
+
+        if ($targetVersion == null || $currentVersion < $targetVersion) {
+            $this->up($targetVersion);
+
+        // migrate down
+        } elseif ($currentVersion > $targetVersion) {
+            $this->down($targetVersion);
+
+        // You're on the right version
+        } elseif ($currentVersion == $targetVersion) {
+            return;
+        }
+    }
+
+    /**
+     * @param   string  $targetVersion
+     */
+    public function up($targetVersion = null)
+    {
+        if (!is_null($targetVersion)) {
+            $this->_targetVersion = $targetVersion;
+        }
+        $this->_direction = 'up';
+        $this->_doMigrate();
+    }
+
+    /**
+     * @param   string  $targetVersion
+     */
+    public function down($targetVersion = null)
+    {
+        if (!is_null($targetVersion)) {
+            $this->_targetVersion = $targetVersion;
+        }
+        $this->_direction = 'down';
+        $this->_doMigrate();
+    }
+
+    /**
+     * @return  int
+     */
+    public function getCurrentVersion()
+    {
+        $sql = 'SELECT version FROM schema_info';
+        return $this->_connection->selectValue($sql);
+    }
+
+
+    /*##########################################################################
+    # Protected
+    ##########################################################################*/
+
+    /**
+     * Perform migration
+     */
+    protected function _doMigrate()
+    {
+        foreach ($this->_getMigrationClasses() as $migration) {
+            if ($this->_hasReachedTargetVersion($migration->version)) {
+                $msg = "Reached target version: $this->_targetVersion";
+                $this->_logger->info($msg);
+                return;
+            }
+            if ($this->_isIrrelevantMigration($migration->version)) { continue; }
+
+            // log
+            $msg = "Migrating to ".get_class($migration)." (".$migration->version.")";
+            $this->_logger->info($msg);
+
+            // migrate
+            $migration->migrate($this->_direction);
+            $this->_setSchemaVersion($migration->version);
+        }
+    }
+
+    /**
+     * @return  array
+     */
+    protected function _getMigrationClasses()
+    {
+        $migrations = array();
+        foreach ($this->_getMigrationFiles() as $migrationFile) {
+            require_once $migrationFile;
+            list($version, $name) = $this->_getMigrationVersionAndName($migrationFile);
+            $this->_assertUniqueMigrationVersion($migrations, $version);
+            $migrations[$version] = $this->_getMigrationClass($name, $version);
+        }
+
+        // sort by version
+        ksort($migrations);
+        $sorted = array_values($migrations);
+        return $this->_isDown() ? array_reverse($sorted) : $sorted;
+    }
+
+    /**
+     * @param   array   $migrations
+     * @param   integer $version
+     */
+    protected function _assertUniqueMigrationVersion($migrations, $version)
+    {
+        if (isset($migrations[$version])) {
+            $msg = "Multiple migrations have the version number $version";
+            throw new Horde_Db_Migration_Exception($msg);
+        }
+    }
+
+    /**
+     * Get the list of migration files
+     * @return  array
+     */
+    protected function _getMigrationFiles()
+    {
+        $files = glob("$this->_migrationsPath/[0-9]*_*.php");
+        return $this->_isDown() ? array_reverse($files) : $files;
+    }
+
+    /**
+     * Actually return object, and not class
+     *
+     * @param   string  $migrationName
+     * @param   int     $version
+     * @return  Horde_Db_Migration_Base
+     */
+    protected function _getMigrationClass($migrationName, $version)
+    {
+        $className = Horde_Support_Inflector::camelize($migrationName);
+        return new $className(array(
+            'connection' => $this->_connection,
+            'version' => $version,
+        ));
+    }
+
+    /**
+     * @param   string  $migrationFile
+     * @return  array   ($version, $name)
+     */
+    protected function _getMigrationVersionAndName($migrationFile)
+    {
+        preg_match_all('/([0-9]+)_([_a-z0-9]*).php/', $migrationFile, $matches);
+        return array($matches[1][0], $matches[2][0]);
+    }
+
+    /**
+     * @param   integer $version
+     */
+    protected function _setSchemaVersion($version)
+    {
+        $version = $this->_isDown() ? $version - 1 : $version;
+        $sql = "UPDATE schema_info SET version = " . (int)$version;
+        $this->_connection->update($sql);
+    }
+
+    /**
+     * @return  boolean
+     */
+    protected function _isUp()
+    {
+        return $this->_direction == 'up';
+    }
+
+    /**
+     * @return  boolean
+     */
+    protected function _isDown()
+    {
+        return $this->_direction == 'down';
+    }
+
+    /**
+     * @return  boolean
+     */
+    protected function _hasReachedTargetVersion($version)
+    {
+        if ($this->_targetVersion === null) { return false; }
+
+        return ($this->_isUp()   && $version-1 >= $this->_targetVersion) ||
+               ($this->_isDown() && $version   <= $this->_targetVersion);
+    }
+
+    /**
+     * @param   integer $version
+     * @return  boolean
+     */
+    protected function _isIrrelevantMigration($version)
+    {
+        return ($this->_isUp()   && $version <= self::getCurrentVersion()) ||
+               ($this->_isDown() && $version >  self::getCurrentVersion());
+    }
+
+}