Skip to content

Instantly share code, notes, and snippets.

@sycobuny
Created April 22, 2014 13:06
Show Gist options
  • Save sycobuny/11178428 to your computer and use it in GitHub Desktop.
Save sycobuny/11178428 to your computer and use it in GitHub Desktop.
An iteration of my migrator script for PHP
<?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