Last active
October 12, 2015 20:08
-
-
Save sycobuny/4080750 to your computer and use it in GitHub Desktop.
Basic PHP database migration script
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 connections and other libraries | |
*/ | |
require( | |
join(DIRECTORY_SEPARATOR, | |
array(dirname(__FILE__), '..', '..', 'pg_connect.php')) | |
); | |
/** | |
* The main controlling class for the process of running migrations. | |
*/ | |
class Migrator { | |
/** | |
* @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 | |
* | |
* @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 DBCannotQueryException( | |
'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) { | |
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 DBCannotQueryException( | |
'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 DBCannotQueryException 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 DBCannotQueryException( | |
'Could not retrieve schema version: ' . | |
pg_last_error($db) | |
); | |
} | |
} | |
} | |
else { | |
throw new DBCannotQueryException( | |
'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($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); | |
} | |
/** | |
* A wrapper around the database connection logic, provided for | |
* convenience | |
* | |
* @throws DBCannotConnectException When the database (re)connection | |
* fails | |
* @return resource(PGSQL connection) | |
*/ | |
private function db() { | |
global $pg_connection; | |
return $pg_connection; | |
} | |
} | |
/** | |
* 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(); | |
} | |
public function run() { | |
$db = $this->db(); | |
try { | |
foreach ($this->run_statements() as $statement) { | |
if ($res = pg_query($db, $statement)) { | |
pg_free_result($res); | |
} | |
else { | |
throw new DBCannotQueryException( | |
"Could not execute query $statement: " . | |
pg_last_error($db) | |
); | |
} | |
} | |
} | |
catch (Exception $e) { | |
if ($this->transaction) { | |
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 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); | |
} | |
/** | |
* 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); | |
} | |
/** | |
* 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); | |
} | |
/** | |
* A wrapper around the database connection logic, provided for | |
* convenience | |
* | |
* @throws DBCannotConnectException When the database (re)connection | |
* fails | |
* @return resource(PGSQL connection) | |
*/ | |
private function db() { | |
global $pg_connection; | |
return $pg_connection; | |
} | |
} | |
/** | |
* 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 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) { | |
$action = $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']; | |
} | |
} | |
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 ? 'SET' : 'DROP'; | |
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; | |
} | |
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 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; | |
} | |
} | |
if (php_sapi_name() === 'cli') { | |
$version = $argv[1]; | |
$migrator = new Migrator($version, TRUE); | |
$migrator->run(); | |
} | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment