Created
April 22, 2014 13:06
-
-
Save sycobuny/11178428 to your computer and use it in GitHub Desktop.
An iteration of my migrator script for PHP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Set up the database connection. Note that these require either a | |
* connection string be provided by the $PHP_DATABASE_STR env variable, a | |
* filename be provided by the $PHP_CONNECTION_SCRIPT variable, or a | |
* filename matching the parameters demanded by | |
* Migrator::default_connection_script(), the latter two of which must | |
* exist, or the script will fail at this point. | |
*/ | |
if (getenv('PHP_DATABASE_STR')) { | |
Migrator::connect_to_database(getenv('PHP_DATABASE_STR')); | |
} | |
else if (getenv('PHP_CONNECTION_SCRIPT')) { | |
if (file_exists(getenv('PHP_CONNECTION_SCRIPT'))) { | |
require_once(getenv('PHP_CONNECTION_SCRIPT')); | |
} | |
else { | |
throw new RuntimeException( | |
'PHP database connection script ' . | |
getenv('PHP_CONNECTION_SCRIPT') . ' does not exist, cannot ' . | |
'continue.'); | |
} | |
} | |
else { | |
if (file_exists(Migrator::default_connection_script())) { | |
require_once(Migrator::default_connection_script()); | |
} | |
else { | |
throw new RuntimeException( | |
'Default PHP database connection script ' . | |
Migrator::default_connection_script() . ' does not exist, ' . | |
'cannot continue.'); | |
} | |
} | |
/** | |
* The main controlling class for the process of running migrations. | |
*/ | |
class Migrator { | |
/** | |
* @var resource(PGSQL connection) The database connection, as set by | |
* Migrator::set_db_connection() | |
*/ | |
private static $db; | |
/** | |
* @var integer A cached copy of the current version of the database | |
* to limit database queries | |
*/ | |
private $current_version; | |
/** | |
* @var integer The target version for a given set of migrations | |
*/ | |
private $version_to; | |
/** | |
* @var bool Whether to attempt to run all migrations under a single | |
* transaction (otherwise migrations may stop partway through but | |
* the last successfully-completed migration will remain applied | |
*/ | |
private $global_transaction; | |
/** | |
* @var array The list of all migrations to run | |
*/ | |
private $migrations; | |
/** | |
* Create a new Migrator | |
* | |
* @param int $version_to The final version you'd like to arrive at | |
* @param bool $global_transaction Whether to run one single | |
* transaction for the entire migration process | |
* @return Migrator | |
*/ | |
public function __construct($version_to, | |
$global_transaction = FALSE) { | |
$this->version_to = $version_to; | |
$this->global_transaction = $global_transaction; | |
$this->migrations = array(); | |
} | |
/** | |
* Run all applicable migrations | |
* | |
* @throws RuntimeException When a query or transaction cannot be run | |
* @return void | |
*/ | |
public function run() { | |
$way = $this->way(); | |
$db = $this->db(); | |
foreach ($this->files() as $file) { | |
$this->apply($file); | |
} | |
if ($this->global_transaction) { | |
if ($res = pg_query($db, 'BEGIN;')) { | |
pg_free_result($res); | |
} | |
else { | |
throw new RuntimeException( | |
'Could not start global transaction: ' . | |
pg_last_error($db) | |
); | |
} | |
} | |
try { | |
foreach ($this->migrations as $migration) { | |
$migration->run(); | |
if ($way === 'up') { | |
echo "Applied migration " . $migration->version() . | |
"\n"; | |
} | |
else { | |
echo "Downgraded from migration " . | |
$migration->version() . "\n"; | |
} | |
} | |
} | |
catch (Exception $e) { | |
if ($this->global_transaction) { | |
echo "Rolling back globally\n"; | |
if ($res = pg_query($db, 'ROLLBACK;')) { | |
pg_free_result($res); | |
} | |
} | |
throw $e; | |
} | |
if ($this->global_transaction) { | |
if ($res = pg_query($db, 'COMMIT;')) { | |
pg_free_result($res); | |
} | |
else { | |
throw new RuntimeException( | |
'Could not commit global transaction: ' . | |
pg_last_error($db) | |
); | |
} | |
} | |
} | |
/** | |
* Retrieve all applicable migration files to get to a particular | |
* version | |
* | |
* @throws OverflowException When multiple migration files are found | |
* for a single migration version | |
* @throws UnderflowException When a migration file necessary to get | |
* from one version to another is not found | |
* @return array | |
*/ | |
public function files() { | |
$cur = $this->current_version(); | |
$to = $this->version_to; | |
$way = $this->way(); | |
if ($way === NULL) { | |
return array(); | |
} | |
$dir = join(DIRECTORY_SEPARATOR, array(dirname(__FILE__), $way)); | |
$files = array(); | |
foreach (glob("$dir/???_*.php", GLOB_MARK) as $path) { | |
$fn = basename($path); | |
if (preg_match('/^([0-9]{3})_.*\.php$/', $fn, $matches)) { | |
$filever = (int) $matches[1]; | |
if (array_key_exists($filever, $files)) { | |
throw new OverflowException( | |
"Multiple version $filever migrations found!" | |
); | |
} | |
if ((($way === 'up') && ($filever > $cur) && | |
($filever <= $to)) || | |
(($way === 'down') && ($filever <= $cur) && | |
($filever > $to))) { | |
$files[$filever] = $path; | |
} | |
} | |
} | |
if ($way === 'up') { | |
$range = range($cur + 1, $to); | |
} | |
else if ($way === 'down') { | |
$range = range($cur, $to + 1); | |
} | |
$ordered_files = array(); | |
foreach ($range as $ver) { | |
if ((!array_key_exists($ver, $files)) || (!$files[$ver])) { | |
throw new UnderflowException( | |
"Missing migration version files!" | |
); | |
} | |
else { | |
array_push($ordered_files, $files[$ver]); | |
} | |
} | |
return $ordered_files; | |
} | |
/** | |
* Retrieve the current version of the database | |
* | |
* If the schema_version() function exists in the database, then this | |
* function will query it and return the result. If it does not, it | |
* will assume version 0. If the function exists and cannot be queried | |
* for whatever reason, an error will be thrown. | |
* | |
* @throws RuntimeException If the attempt to pull the version number | |
* from the database fails | |
* @return integer | |
*/ | |
public function current_version() { | |
if ($this->current_version !== NULL) { | |
return $this->current_version; | |
} | |
$db = Migrator::db(); | |
$qry = 'SELECT 1 FROM pg_catalog.pg_proc WHERE proname = $1 ' . | |
'LIMIT 1'; | |
$res = pg_query_params($db, $qry, array('schema_version')); | |
if ($res) { | |
if (pg_num_rows($res) == 0) { | |
pg_free_result($res); | |
return 0; | |
} | |
else { | |
pg_free_result($res); | |
$qry = 'SELECT schema_version()'; | |
$res = pg_query($db, $qry); | |
if ($res) { | |
$obj = pg_fetch_object($res); | |
pg_free_result($res); | |
return (int) $obj->schema_version; | |
} | |
else { | |
throw new RuntimeException( | |
'Could not retrieve schema version: ' . | |
pg_last_error($db) | |
); | |
} | |
} | |
} | |
else { | |
throw new RuntimeException( | |
'Could not validate existence of schema version info: ' . | |
pg_last_error($db) | |
); | |
} | |
} | |
/** | |
* Determine the direction this migration must go | |
* | |
* When migrating from a lesser version to a greater one, it's | |
* considered an "up" migration; when migrating the opposite way, it's | |
* considered a "down" migration. If already at the correct version, | |
* it's not a migration at all. This function will determine that | |
* direction. | |
* | |
* @return string | |
*/ | |
private function way() { | |
if ($this->version_to > $this->current_version()) { | |
return 'up'; | |
} | |
else if ($this->version_to == $this->current_version()) { | |
return NULL; | |
} | |
else { | |
return 'down'; | |
} | |
} | |
/** | |
* Load a migration file and process the migration interally (but do | |
* not run it) | |
* | |
* @param string $file The full file path to the migration file | |
* @throws RuntimeException When the file is named improperly or the | |
* migration contained therein is not an appropriate migration | |
* @return void | |
*/ | |
private function apply($file) { | |
$fn = basename($file); | |
preg_match('/^([0-9]{3})/', $fn, $matches); | |
$class = "Migration$matches[1]"; | |
require_once($file); | |
$migration = new $class($this->way(), !$this->global_transaction); | |
if ((!$migration) || | |
($migration->version() !== ((int) $matches[1]))) { | |
throw new RuntimeException( | |
"Migration file $file did not define a proper migration!" | |
); | |
} | |
array_push($this->migrations, $migration); | |
} | |
/** | |
* Sets the database connection used by the migrator | |
* | |
* @param resource(PGSQL connection) The connection to save | |
* @return resource(PGSQL connection) | |
*/ | |
public static function set_db_connection($db) { | |
self::$db = $db; | |
return self::db(); | |
} | |
/** | |
* Creates the database connection used by the migrator | |
* | |
* Note that this function will overwrite any existing connection | |
* without warning. This function or set_db_connection() *should* only | |
* be used once, so make sure you know what you're doing if you call | |
* it multiple times. | |
* | |
* @param string The connection parameters as understood by | |
* pg_connect(). | |
* @throws RuntimeException If the database connection cannot be | |
* established | |
* @return resource(PGSQL connection) | |
*/ | |
public static function connect_to_database($connection_string) { | |
$db = pg_connect($connection_string); | |
if ($db === false) { | |
throw new RuntimeException('Could not connect to database.'); | |
} | |
self::set_db_connection($db); | |
} | |
/** | |
* Returns the saved database connection for use in migrations | |
* | |
* @return resource(PGSQL connection) | |
*/ | |
public static function db() { | |
return self::$db; | |
} | |
/** | |
* Returns the absolute path for the default connection script | |
* | |
* This should be the resolved form of the file | |
* `migration_connector.php` in the same directory as this script. | |
* | |
* @return string | |
*/ | |
public static function default_connection_script() { | |
return join(DIRECTORY_SEPARATOR, | |
array(dirname(__FILE__), 'migration_connector.php')); | |
} | |
} | |
/** | |
* The minimum set of functons necessary to implement for someone who | |
* doesn't want to directly inherit from Migration to write their | |
* migrations. | |
*/ | |
interface MigrationInterface { | |
/** | |
* Return a new MigrationInterface-compatible object | |
* | |
* @return MigrationInterface | |
*/ | |
public function __construct($way, $transaction = TRUE); | |
/** | |
* Run a migration | |
* | |
* @return void | |
*/ | |
public function run(); | |
/** | |
* Return the version of the database that this migration will move to | |
* | |
* @return integer | |
*/ | |
public function version(); | |
/** | |
* Construct the actual migration data (tables, columns, and other | |
* modifications) | |
* | |
* @return void | |
*/ | |
public function construct(); | |
} | |
/** | |
* A class with utility functions for making it easy to construct a | |
* migration. | |
* | |
* It is recommended that you inherit all your migrations from this class | |
* for consistency and ease of use. However, it is possible to implement | |
* the MigrationInterface yourself to create your own migration from | |
* scratch. | |
*/ | |
abstract class Migration implements MigrationInterface { | |
/** | |
* @var string The direction this migration is moving | |
*/ | |
private $way; | |
/** | |
* @var bool Whether this migration should construct its own | |
* transaction | |
*/ | |
private $transaction; | |
/** | |
* @var array The list of all statements to execute to complete this | |
* migration | |
*/ | |
private $statements; | |
/** | |
* Create a new Migration | |
* | |
* Note that this function calls construct() on an unknown object, | |
* which means that exceptions thrown can be unpredictable. One should | |
* catch Exception, if intending to stop all errors emitted from this | |
* function. | |
* | |
* @param string $way 'up' or 'down', the direction of the migration | |
* @param bool $transaction Whether this migration should construct | |
* its own transaction | |
* @throws Exception May throw any error from dynamically-loaded | |
* content | |
* @return Migration | |
*/ | |
public function __construct($way, $transaction = TRUE) { | |
$this->way = $way; | |
$this->transaction = $transaction; | |
$this->statements = array(); | |
$this->construct(); | |
} | |
/** | |
* Run a migration, which means all statements. This includes any | |
* transaction statements. There is feedback echoed to the user for | |
* each statement to the user. | |
* | |
* @throws RuntimeException When a given query cannot be executed | |
* @return void | |
*/ | |
public function run() { | |
$db = $this->db(); | |
try { | |
foreach ($this->run_statements() as $statement) { | |
echo "Running statement: $statement\n"; | |
if ($res = pg_query($db, $statement)) { | |
pg_free_result($res); | |
} | |
else { | |
throw new RuntimeException( | |
"Could not execute query $statement: " . | |
pg_last_error($db) | |
); | |
} | |
} | |
} | |
catch (Exception $e) { | |
echo "Fatal error: " . $e->getMessage() . "\n"; | |
if ($this->transaction) { | |
echo "Rolling back migration " . $this->version() . "\n"; | |
if ($res = pg_query($db, 'ROLLBACK;')) { | |
pg_free_result($res); | |
} | |
} | |
throw $e; | |
} | |
} | |
/** | |
* Return the version from this migration | |
* | |
* This assumes migration subclasses are named Migration###. If they | |
* are not, then this function will return an error. | |
* | |
* @throws RuntimeException If the migration number cannot be | |
* determined | |
* @return integer | |
*/ | |
public function version() { | |
$class = (string) get_class($this); | |
if (preg_match('/^Migration([0-9]{3})$/', $class, $matches)) { | |
return (int) $matches[1]; | |
} | |
else { | |
throw new RuntimeException( | |
"Couldn't guess version number from class: $class" | |
); | |
} | |
} | |
/** | |
* Quote and escape a PostgreSQL identifier | |
* | |
* @param string $ident The unquoted identifier | |
* @return string | |
*/ | |
public static function qident($ident) { | |
$ret = preg_replace('/"/', '""', $ident); | |
return "\"$ret\""; | |
} | |
/** | |
* Quote and escape a PostgreSQL value | |
* | |
* @param string $val The unquoted value | |
* @return string | |
*/ | |
public static function qval($val) { | |
if ($val === NULL) { | |
return 'NULL'; | |
} | |
else { | |
$ret = preg_replace("/'/", "''", $val); | |
return "'$ret'"; | |
} | |
} | |
/** | |
* Convert the list of statement objects into strings that can be | |
* executed on the database | |
* | |
* In addition to the process of converting all strings, this | |
* function adds in transaction statements (if appropriate) to the | |
* start and end of all transactions, and constructs or drops a | |
* schema_version() function (as appropriate) to set the database | |
* version upon successful completion of migrations. | |
* | |
* @return array | |
*/ | |
protected function run_statements() { | |
$statements = array(); | |
foreach ($this->statements as $statement) { | |
array_push($statements, $statement->sql()); | |
} | |
$version = $this->version(); | |
if ($this->way === 'down') { | |
$version--; | |
} | |
if ($version === 0) { | |
$schema_version = 'DROP FUNCTION schema_version();'; | |
} | |
else { | |
$schema_version = <<<SQL | |
CREATE OR REPLACE | |
FUNCTION schema_version() | |
RETURNS INTEGER | |
LANGUAGE SQL | |
IMMUTABLE | |
COST 1 | |
AS 'SELECT $version'; | |
SQL; | |
} | |
array_push($statements, $schema_version); | |
if ($this->transaction) { | |
array_unshift($statements, 'BEGIN;'); | |
array_push($statements, 'COMMIT;'); | |
} | |
return $statements; | |
} | |
/** | |
* Add a "CREATE TABLE" statement into the list of statements to be | |
* executed | |
* | |
* @param string $name The name of the table to create | |
* @param array $attrs The list of attributes (columns, constraints, | |
* etc.) to be added to the table | |
* @param array $opts Any table options | |
* @return void | |
*/ | |
protected function table($name, $attrs, $opts = array()) { | |
$tbl = new MigrationTable($name, $attrs, $opts); | |
array_push($this->statements, $tbl); | |
} | |
/** | |
* Add an "ALTER TABLE ... RENAME TO" statement into the list of | |
* statements to be executed | |
* | |
* @param string $name The current name of the table to change | |
* @param string $new_name What to name the table to | |
* @return void | |
*/ | |
protected function rename_table($name, $new_name) { | |
$rtbl = new MigrationRenameTable($name, $new_name); | |
array_push($this->statements, $rtbl); | |
} | |
/** | |
* Add a series of "ALTER TABLE" statements into the list of | |
* statements to be executed | |
* | |
* @param string $name The name of the table to alter | |
* @param array $attrs The list of attributes (columns, constraints, | |
* etc.) to be added to the table | |
* @return void | |
*/ | |
protected function alter_table($name, $attrs) { | |
$tbl = new MigrationAlterTable($name, $attrs); | |
array_push($this->statements, $tbl); | |
} | |
/** | |
* Add a "DROP TABLE" statement into the list of statements to be | |
* executed | |
* | |
* @param string $name The name of the table to drop | |
* @return void | |
*/ | |
protected function drop_table($name) { | |
$dtbl = new MigrationDropTable($name); | |
array_push($this->statements, $dtbl); | |
} | |
/** | |
* Create a special "primary key" column to add as a table attribute | |
* | |
* @param string $name The name of the column | |
* @return MigrationColumn | |
*/ | |
protected function primary_key($name = 'id') { | |
return new MigrationColumn($name, 'SERIAL', | |
array('pkey' => TRUE)); | |
} | |
/** | |
* Create a regular table column to add as a table attribute | |
* | |
* @param string $name The name of the column to add | |
* @param string $type The PostgreSQL datatype of the column | |
* @param array $options Any column options ('unique', 'null', | |
* 'default') | |
* @return MigrationColumn | |
*/ | |
protected function column($name, $type, $options = array()) { | |
return new MigrationColumn($name, $type, $options); | |
} | |
/** | |
* Create an ALTER COLUMN attribute used to modify a table | |
* | |
* Note that you should only provide the options that you directly | |
* want to modify. Adding redundant options may result in a syntax | |
* error. | |
* | |
* @param string $name The name of the column to modify | |
* @param array $options Any column options to modify ('type', 'null', | |
* 'default') | |
* @return MigrationAlterColumn | |
*/ | |
protected function alter_column($name, $options = array()) { | |
return new MigrationAlterColumn($name, $options); | |
} | |
/** | |
* Create a DROP COLUMN attribute used to modify a table | |
* | |
* @param string $name The name of the column to drop | |
* @return MigrationDropColumn | |
*/ | |
protected function drop_column($name) { | |
return new MigrationDropColumn($name); | |
} | |
/** | |
* Create a foreign key constraint to add as a table attribute | |
* | |
* If you provide an array as either $local or $remote, then the | |
* opposite variable must match in both type and size (that is, there | |
* must be a corresponding column in each table). This function will | |
* not validate types prior to assigning foreign key relationships, so | |
* this may result in errors if you are not careful to validate the | |
* relationship on your own. | |
* | |
* @param mixed $local The string column (or array of string columns) | |
* representing the current table's side of the foreign key | |
* relationship | |
* @param string $table The name of the other table in the | |
* relationship | |
* @param mixed $remote The string column (or array of string columns) | |
* representing the remote table's side of the foreign key | |
* relationship | |
* @return MigrationForeignKey | |
*/ | |
protected function foreign_key($local, $table, $remote = 'id') { | |
return new MigrationForeignKey($local, $table, $remote); | |
} | |
/** | |
* Drop a foreign key constraint from the database | |
* | |
* If you provide one argument to this function, it must be the name | |
* of the constraint to drop. This is useful if you have a foreign key | |
* that was generated through a mechanism other than this migrator | |
* (which uses the default value). If you provide both arguments, then | |
* the constructed default value ("{$table}_{$local}_fkey") will be | |
* assumed. In the latter case, only the first column in a | |
* multi-column constraint will be used to generate the name. Note | |
* that this means you can also simply provide the first column of a | |
* multi-column constraint and successfully drop the full constraint. | |
* | |
* @param mixed $local_or_name The string column (or array of string | |
* columns) representing the current table's side of the foreign | |
* key relationship, OR, the name of the foreign key | |
* relationship's constraint, as stored by the database. | |
* @param string $table The name of the other table in the | |
* relationship | |
* @return MigrationDropForeignKey | |
*/ | |
protected function drop_foreign_key($local_or_name, $table = NULL) { | |
return new MigrationDropForeignKey($local_or_name, $table); | |
} | |
/** | |
* Create a constraint on a column that its value is IN() some other | |
* set of values to add as a table attribute | |
* | |
* This function is a shorthand for a CHECK constraint that is fairly | |
* common: validating a given column is one of a given set of values. | |
* | |
* @param string $name The name of the CHECK constraint | |
* @param string $column The name of the column to which this | |
* constraint will apply | |
* @param array $values The list of values of which the column value | |
* must be a member | |
* @return MigrationConstraint | |
*/ | |
protected function in_constraint($name, $column, $values) { | |
return new MigrationConstraint('in', $name, $column, $values); | |
} | |
/** | |
* A literal statement to run | |
* | |
* Use this method if you have an SQL statement that you'd like to run | |
* that is not covered by any of the other migrator methods. A common | |
* use case would be transferring data from one table to another when | |
* changling the underlying representation in the database (combined | |
* INSERT and SELECT statements) | |
* | |
* @param string $sql The SQL to run | |
* @return MigrationLiteralStatement | |
*/ | |
protected function literal_statement($sql) { | |
return new MigrationLiteralStatement($sql); | |
} | |
/** | |
* Handle any unknown method calls as attempts to create columns to | |
* add as table attributes | |
* | |
* If you want to create a column but don't want to go through the | |
* excess work of specifying the column() method and then a type, you | |
* can simply call a method named after the type you'd like, and skip | |
* naming the datatype. If it does not conflict with an existing | |
* method, a column will be created with that datatype. | |
* | |
* @param string $method The name of the method which was originally | |
* called (the resulting datatype) | |
* @param array $args The arguments to that method. The first value | |
* should be the name of the column, and the second optional value | |
* should be the options list as would be passed to column(). | |
* @return MigrationColumn | |
*/ | |
public function __call($method, $args) { | |
$type = $method; | |
$name = $args[0]; | |
if (count($args) > 1) { | |
$options = $args[1]; | |
} | |
else { | |
$options = array(); | |
} | |
return $this->column($name, $type, $options); | |
} | |
/** | |
* Returns the saved database connection for use in migrations | |
* | |
* @return resource(PGSQL connection) | |
*/ | |
public static function db() { | |
return Migrator::db(); | |
} | |
} | |
/** | |
* Required functions for anything that creates a piece of a migration | |
*/ | |
interface MigrationChunk { | |
/** | |
* Convert the current migration object into an SQL string | |
* | |
* @return string | |
*/ | |
public function sql(); | |
} | |
/** | |
* A representation of a CREATE TABLE statement | |
*/ | |
class MigrationTable implements MigrationChunk { | |
/** | |
* @var string The table name to create | |
*/ | |
private $name; | |
/** | |
* @var array The list of columns, constraints, etc. in the table | |
*/ | |
private $attributes; | |
/** | |
* @var array The list of table options | |
*/ | |
private $options; | |
/** | |
* Create a new CREATE TABLE statement | |
* | |
* @param string $name The name of the table to create | |
* @param array $attrs The list of columns, constraints, etc. in the | |
* table | |
* @param array $opts The list of table options | |
* @return MigrationTable | |
*/ | |
public function __construct($name, $attrs, $opts = array()) { | |
$this->name = $name; | |
$this->attributes = $attrs; | |
$this->options = $opts; | |
} | |
/** | |
* Convert the object into SQL | |
* | |
* @return string | |
*/ | |
public function sql() { | |
$name = Migration::qident($this->name); | |
$attrs = array(); | |
$opts = array(); | |
$sql = "CREATE TABLE $name ("; | |
foreach ($this->attributes as $attr) { | |
array_push($attrs, $attr->sql()); | |
} | |
$sql .= join(', ', $attrs); | |
$sql .= ') WITHOUT OIDS;'; | |
return $sql; | |
} | |
} | |
/** | |
* A representation of an ALTER TABLE ... RENAME TO statement | |
*/ | |
class MigrationRenameTable implements MigrationChunk { | |
/** | |
* @var string The current table name | |
*/ | |
private $name; | |
/** | |
* @var string The name to change the table into | |
*/ | |
private $new_name; | |
/** | |
* Create a new ALTER TABLE ... RENAME TO statement | |
* | |
* @param string $name The current name of the table to change | |
* @param string $new_name What to name the table to | |
* @return MigrationRenameTable | |
*/ | |
public function __construct($name, $new_name) { | |
$this->name = $name; | |
$this->new_name = $new_name; | |
} | |
/** | |
* Convert the object into SQL | |
* | |
* @return string | |
*/ | |
public function sql() { | |
$name = Migration::qident($this->name); | |
$new_name = Migration::qident($this->new_name); | |
return "ALTER TABLE $name RENAME TO $new_name"; | |
} | |
} | |
/** | |
* A representation of an ALTER TABLE statement | |
*/ | |
class MigrationAlterTable implements MigrationChunk { | |
/** | |
* @var string The name of the table to alter | |
*/ | |
/** | |
* @var rray This list of columns/etc. to modify on the table | |
*/ | |
private $attributes; | |
/** | |
* Create a new ALTER TABLE statement | |
* | |
* @param string $name The name of the table to alter | |
* @param array $attrs The list of columns/etc. to modify on the table | |
* @return MigrationAlterTable | |
*/ | |
public function __construct($name, $attrs) { | |
$this->name = $name; | |
$this->attributes = $attrs; | |
} | |
/** | |
* Convert the object into SQL | |
* | |
* @throws UnderflowException When there are no alterations to make | |
* @return String | |
*/ | |
public function sql() { | |
$name = Migration::qident($this->name); | |
$attrs = array(); | |
$sql = "ALTER TABLE $name "; | |
foreach ($this->attributes as $attr) { | |
$action = null; | |
if (($attr instanceof MigrationAlterColumn ) or | |
($attr instanceof MigrationDropColumn ) or | |
($attr instanceof MigrationDropForeignKey)) { | |
$action = $attr->sql(); | |
} | |
else if ($attr instanceof MigrationForeignKey) { | |
$action = 'ADD ' . $attr->sql(); | |
} | |
else if ($attr instanceof MigrationColumn) { | |
$action = 'ADD COLUMN ' . $attr->sql(); | |
} | |
if ($action) | |
array_push($attrs, $action); | |
} | |
if (count($attrs) == 0) { | |
throw new UnderflowException( | |
"There were no modifications to make to $name" | |
); | |
} | |
$sql .= join(', ', $attrs) . ';'; | |
return $sql; | |
} | |
} | |
/** | |
* A representation of a DROP TABLE statement | |
*/ | |
class MigrationDropTable implements MigrationChunk { | |
/** | |
* @var string The name of the table to drop | |
*/ | |
private $name; | |
/** | |
* Create a new DROP TABLE statement | |
* | |
* @param string $name The name of the table to drop | |
* @return MigrationDropTable | |
*/ | |
public function __construct($name) { | |
$this->name = $name; | |
} | |
/** | |
* Convert the object into SQL | |
* | |
* @return string | |
*/ | |
public function sql() { | |
$name = Migration::qident($this->name); | |
return "DROP TABLE $name;"; | |
} | |
} | |
/** | |
* A representation of a column specification for use in a CREATE TABLE | |
* statement | |
*/ | |
class MigrationColumn implements MigrationChunk { | |
/** | |
* @var string The name of the column | |
*/ | |
private $name; | |
/** | |
* @var string The PostgreSQL datatype of the column | |
*/ | |
private $type; | |
/** | |
* @var bool Whether the column should be marked UNIQUE | |
*/ | |
private $unique; | |
/** | |
* @var bool Whether the column should be marked NULL or NOT NULL | |
*/ | |
private $null; | |
/** | |
* @var string What the default value of the column should be | |
*/ | |
private $default; | |
/** | |
* @var bool Whether this column is the table's PRIMARY KEY or not | |
*/ | |
private $pkey; | |
/** | |
* Create a new column specification | |
* | |
* When you construct the column specification, it is assumed that the | |
* field will be marked NOT NULL. This is the reverse of common | |
* database logic. Additionally, if you specify that the column is a | |
* primary key with the 'pkey' value, UNIQUE and NOT NULL will | |
* automatically be set, regardless of the presence of any | |
* configuration on them in the $options array. This is because a | |
* PRIMARY KEY is mostly shorthand for those two parameters. Their | |
* setting will not have any impact on the eventual SQL string that is | |
* generated if a column is a primary key, as well. | |
* | |
* @param string $name The name of the column | |
* @param string $type The PostgreSQL datatype of the column | |
* @param array $options The configuration options for the column; | |
* possible keys are 'unique', 'null', 'default', and 'pkey' | |
* @return MigrationColumn | |
*/ | |
public function __construct($name, $type, $options = array()) { | |
$this->name = $name; | |
$this->type = $type; | |
if ((array_key_exists('null', $options) && $options['null'])) { | |
$this->null = TRUE; | |
} | |
else { | |
$this->null = FALSE; | |
} | |
if (array_key_exists('unique', $options) && $options['unique']) { | |
$this->unique = TRUE; | |
} | |
else { | |
$this->unique = FALSE; | |
} | |
if (array_key_exists('pkey', $options) && $options['pkey']) { | |
$this->null = FALSE; | |
$this->unique = TRUE; | |
$this->pkey = TRUE; | |
} | |
else { | |
$this->pkey = FALSE; | |
} | |
if (array_key_exists('default', $options)) { | |
$this->default = $options['default']; | |
} | |
} | |
/** | |
* Convert the object into SQL | |
* | |
* @return string | |
*/ | |
public function sql() { | |
$name = Migration::qident($this->name); | |
if ($this->pkey) { | |
return "$name SERIAL PRIMARY KEY"; | |
} | |
$ttype = strtolower($this->type); | |
if (array_key_exists($ttype, $this->known_types())) { | |
$types = $this->known_types(); | |
$type = $types[$ttype]; | |
} | |
else { | |
$type = Migration::qident($this->type); | |
} | |
$sql = "$name $type"; | |
if ($this->unique) { | |
$sql .= ' UNIQUE'; | |
} | |
if ($this->null) { | |
$sql .= ' NULL'; | |
} | |
else { | |
$sql .= ' NOT NULL'; | |
} | |
if ($this->default) { | |
$sql .= " DEFAULT " . Migration::qval($this->default); | |
} | |
return $sql; | |
} | |
/** | |
* List possible default PostgreSQL datatypes and the strings that | |
* would represent them in a column specification | |
* | |
* This function is necessary because arrays can't be constants in | |
* PHP. the idea behind the function is that what you provide in a | |
* column specification may not be the actual name of the PostgreSQL | |
* datatype. This is only problematic as we always quote those | |
* identifiers, which removes the shorthand magic PostgreSQL employs. | |
* In this way, we can guarantee that we should wind up with the | |
* proper internal datatype even when we use shorthand. | |
* | |
* @return array | |
*/ | |
private function known_types() { | |
return array( | |
'int' => 'INTEGER', | |
'integer' => 'INTEGER', | |
'date' => 'DATE', | |
'time' => 'TIME', | |
'datetime' => 'TIMESTAMP', | |
'timestamp' => 'TIMESTAMP WITHOUT TIME ZONE', | |
'timestamptz' => 'TIMESTAMP WITH TIME ZONE' | |
); | |
} | |
} | |
/** | |
* A representation of a column specification for use in a CREATE TABLE | |
* statement | |
*/ | |
class MigrationAlterColumn implements MigrationChunk { | |
/** | |
* @var string The name of the column | |
*/ | |
private $name; | |
/** | |
* @var string The PostgreSQL datatype of the column | |
*/ | |
private $type; | |
/** | |
* @var bool Whether the column should be marked NULL or NOT NULL | |
*/ | |
private $null; | |
/** | |
* @var string What the default value of the column should be | |
*/ | |
private $default; | |
/** | |
* Create a new column alteration specification | |
* | |
* It is assumed that you will be giving this method ONLY the options | |
* that you wish to modify. Anything else you don't specify will be | |
* left as its current value. You should not pass anything you don't | |
* want to modify, even if you are giving its current value again, | |
* because this could result in a syntax error (and is unnecessary | |
* duplication of effort besides). | |
* | |
* @param string $name The name of the column | |
* @param array $options The configuration options for the column; | |
* possible keys are 'type', 'null', and 'default' | |
* @return MigrationAlterColumn | |
*/ | |
public function __construct($name, $options = array()) { | |
$this->name = $name; | |
if (array_key_exists('type', $options) && $options['type']) { | |
$this->type = $options['type']; | |
} | |
if (array_key_exists('null', $options)) { | |
$this->null = $options['null'] ? TRUE : FALSE; | |
} | |
if (array_key_exists('default', $options)) { | |
$this->default = $options['default']; | |
} | |
} | |
/** | |
* Convert the object into SQL | |
* | |
* @return string | |
*/ | |
public function sql() { | |
$name = Migration::qident($this->name); | |
$actions = array(); | |
if (isset($this->type)) { | |
$ttype = strtolower($this->type); | |
if (array_key_exists($ttype, $this->known_types())) { | |
$types = $this->known_types(); | |
$type = $types[$ttype]; | |
} | |
else { | |
$type = Migration::qident($this->type); | |
} | |
array_push($actions, "ALTER COLUMN $name SET DATA TYPE " . | |
$type); | |
} | |
if (isset($this->null)) { | |
$alter = $this->null ? 'DROP' : 'SET'; | |
array_push($actions, "ALTER COLUMN $name $alter NOT NULL"); | |
} | |
if (isset($this->default)) { | |
array_push($actions, "ALTER COLUMN $name SET DEFAULT " . | |
Migration::qval($default)); | |
} | |
return join(', ', $actions); | |
} | |
/** | |
* List possible default PostgreSQL datatypes and the strings that | |
* would represent them in a column specification | |
* | |
* This function is necessary because arrays can't be constants in | |
* PHP. the idea behind the function is that what you provide in a | |
* column specification may not be the actual name of the PostgreSQL | |
* datatype. This is only problematic as we always quote those | |
* identifiers, which removes the shorthand magic PostgreSQL employs. | |
* In this way, we can guarantee that we should wind up with the | |
* proper internal datatype even when we use shorthand. | |
* | |
* @return array | |
*/ | |
private function known_types() { | |
return array( | |
'int' => 'INTEGER', | |
'integer' => 'INTEGER', | |
'date' => 'DATE', | |
'time' => 'TIME', | |
'datetime' => 'TIMESTAMP', | |
'timestamp' => 'TIMESTAMP WITHOUT TIME ZONE', | |
'timestamptz' => 'TIMESTAMP WITH TIME ZONE' | |
); | |
} | |
} | |
/** | |
* A representation of a column to drop for use in an ALTER TABLE | |
* statement | |
*/ | |
class MigrationDropColumn implements MigrationChunk { | |
/** | |
* @var string The name of the column | |
*/ | |
private $name; | |
/** | |
* Create a new column drop specification | |
* | |
* @param string $name The name of the column | |
* @return MigrationAlterColumn | |
*/ | |
public function __construct($name) { | |
$this->name = $name; | |
} | |
/** | |
* Convert the object into SQL | |
* | |
* @return string | |
*/ | |
public function sql() { | |
$name = Migration::qident($this->name); | |
return "DROP COLUMN $name"; | |
} | |
} | |
/** | |
* A representation of a foreign key specification for use in a CREATE | |
* TABLE statement | |
*/ | |
class MigrationForeignKey implements MigrationChunk { | |
/** | |
* @var mixed The string column (or array of string columns) | |
* representing the current table's side of the foreign key | |
* relationship | |
*/ | |
private $local; | |
/** | |
* @param string The name of the other table in the relationship | |
*/ | |
private $table; | |
/** | |
* @param mixed The string column (or array of string columns) | |
* representing the remote table's side of the foreign key | |
* relationship | |
*/ | |
private $remote; | |
/** | |
* Create a new foreign key specification | |
* | |
* @param mixed $local The string column (or array of string columns) | |
* representing the current table's side of the foreign key | |
* relationship | |
* @param string $table The name of the other table in the | |
* relationship | |
* @param mixed $remote The string column (or array of string columns) | |
* representing the remote table's side of the foreign key | |
* relationship | |
* @return MigrationForeignKey | |
*/ | |
public function __construct($local, $table, $remote = 'id') { | |
$this->local = $local; | |
$this->table = $table; | |
$this->remote = $remote; | |
} | |
/** | |
* Convert the object into SQL | |
* | |
* @throws ArgumentError If the count of columns in both the local and | |
* remote portions of the specifications does not match | |
* @return string | |
*/ | |
public function sql() { | |
$local = $this->local; | |
$table = Migration::qident($this->table); | |
$remote = $this->remote; | |
$idents = array(&$local, &$remote); | |
foreach ($idents as $x => $i) { | |
if (!is_array($i)) { | |
$idents[$x] = array($i); | |
} | |
} | |
if (count($local) !== count($remote)) { | |
throw new ArgumentError( | |
'Count of foreign key columns from source to ' . | |
'destination does not match' | |
); | |
} | |
foreach ($idents as $x => $i) { | |
foreach ($i as $y => $ident) { | |
$i[$y] = Migration::qident($ident); | |
} | |
$idents[$x] = '(' . join(', ', $i) . ')'; | |
} | |
return "FOREIGN KEY $local REFERENCES $table $remote"; | |
} | |
} | |
/** | |
* A representation of a drop foreign key specification for use in an | |
* ALTER TABLE statement | |
*/ | |
class MigrationDropForeignKey implements MigrationChunk { | |
/** | |
* @var mixed The string column (or array of string columns) | |
* representing the current table's side of the foreign key | |
* relationship - may be null if $name is provided | |
*/ | |
private $local; | |
/** | |
* @param string The name of the other table in the relationship - may | |
* be null if $name is provided | |
*/ | |
private $table; | |
/** | |
* @param string The name of the foreign key constraint - will be null | |
* if $local and $table are provided | |
*/ | |
private $name; | |
/** | |
* Create a new drop foreign key specification | |
* | |
* @param mixed $local_or_name The string column (or array of string | |
* columns) representing the current table's side of the foreign | |
* key relationship, OR, the name of the constraint to drop | |
* @param string $table The name of the other table in the | |
* relationship | |
* @return MigrationDropForeignKey | |
*/ | |
public function __construct($local_or_name, $table = NULL) { | |
if ($table) { | |
$this->local = $local_or_name; | |
$this->table = $table; | |
} | |
else { | |
$this->name = $local_or_name; | |
} | |
} | |
/** | |
* Convert the object into SQL | |
* | |
* @throws ArgumentError If the count of columns in both the local and | |
* remote portions of the specifications does not match | |
* @return string | |
*/ | |
public function sql() { | |
if ($this->name) { | |
$name = Migration::qident($this->name); | |
} | |
else { | |
$local = $this->local; | |
$table = $this->table; | |
if (is_array($local)) { | |
$local = $local[0]; | |
} | |
$name = Migration::qident("{$table}_{$local}_fkey"); | |
} | |
return "DROP CONSTRAINT $name"; | |
} | |
} | |
/** | |
* A representation of a CHECK constraint for use in a CREATE TABLE | |
* statement | |
*/ | |
class MigrationConstraint implements MigrationChunk { | |
/** | |
* @var string The type of constraint - currently only 'in' is | |
* supported | |
*/ | |
private $type; | |
/** | |
* @var string The name of the constraint | |
*/ | |
private $name; | |
/** | |
* @var string The column being affected by this constraint | |
*/ | |
private $column; | |
/** | |
* @var array Values to check against the column, for use in 'in' | |
* constraints | |
*/ | |
private $values; | |
/** | |
* Create a constraint specification | |
* | |
* @param string $type The type of constraint | |
* @param string $name The name of the constraint | |
* @param string $column The name of the column being affected by this | |
* constraint | |
* @param array $values Values to check against the column, for use in | |
* 'in' constraints | |
* @return MigrationConstraint | |
*/ | |
public function __construct($type, $name, $column, $values) { | |
$this->type = $type; | |
$this->name = $name; | |
$this->column = $column; | |
$this->values = $values; | |
} | |
/** | |
* Convert the object into SQL | |
* | |
* @return string | |
*/ | |
public function sql() { | |
$name = Migration::qident($this->name); | |
$column = Migration::qident($this->column); | |
$sql = "CONSTRAINT $name CHECK ("; | |
switch($this->type) { | |
case 'in': | |
$sql .= "$column IN ("; | |
$vals = array(); | |
foreach ($this->values as $val) { | |
array_push($vals, Migration::qval($val)); | |
} | |
$sql .= join(', ', $vals) . ')'; | |
break; | |
} | |
$sql .= ')'; | |
return $sql; | |
} | |
} | |
/** | |
* A representation of a literal SQL statement | |
*/ | |
class MigrationLiteralStatement implements MigrationChunk { | |
/** | |
* @var string The statement to run | |
*/ | |
private $sql; | |
/** | |
* Create a literal statement | |
* | |
* @param string $sql The complete SQL statement to run | |
*/ | |
public function __construct($sql) { | |
$this->sql = $sql; | |
} | |
/** | |
* Convert the object into SQL | |
* | |
* @return string | |
*/ | |
public function sql() { | |
return $this->sql; | |
} | |
} | |
/** | |
* Only run the migration when this file is run from the command line | |
*/ | |
if (php_sapi_name() === 'cli') { | |
if (!$argv[1]) { | |
throw new RuntimeException('Version number must be provided.'); | |
} | |
else { | |
$version = $argv[1]; | |
} | |
$migrator = new Migrator($version, FALSE); | |
$migrator->run(); | |
} | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment