commit 2de051a4e28e180d6cb3a19e33c8067efd79541e Author: ahwelp Date: Mon Jul 30 19:40:45 2018 -0300 First Commit diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..1a528d0 --- /dev/null +++ b/composer.json @@ -0,0 +1,17 @@ +{ + "name": "ahwelp/ddlwrapper", + "description": "From Phinx, just the DDL", + "type": "library", + "authors": [ + { + "name": "ahwelp", + "email": "ahwelp@ahwelp.com" + } + ], + "require": {}, + "autoload": { + "psr-4": { + "DDLWrapper\\": "src/DDLWrapper" + } + } +} diff --git a/src/DDLWrapper/Db/Adapter/AdapterFactory.php b/src/DDLWrapper/Db/Adapter/AdapterFactory.php new file mode 100644 index 0000000..7c6358f --- /dev/null +++ b/src/DDLWrapper/Db/Adapter/AdapterFactory.php @@ -0,0 +1,183 @@ + + */ +class AdapterFactory +{ + /** + * @var AdapterFactory + */ + protected static $instance; + + /** + * Get the factory singleton instance. + * + * @return AdapterFactory + */ + public static function instance() + { + if (!static::$instance) { + static::$instance = new static(); + } + return static::$instance; + } + + /** + * Class map of database adapters, indexed by PDO::ATTR_DRIVER_NAME. + * + * @var array + */ + protected $adapters = array( + 'mysql' => 'Phinx\Db\Adapter\MysqlAdapter', + 'pgsql' => 'Phinx\Db\Adapter\PostgresAdapter', + 'sqlite' => 'Phinx\Db\Adapter\SQLiteAdapter', + 'sqlsrv' => 'Phinx\Db\Adapter\SqlServerAdapter', + ); + + /** + * Class map of adapters wrappers, indexed by name. + * + * @var array + */ + protected $wrappers = array( + 'prefix' => 'Phinx\Db\Adapter\TablePrefixAdapter', + 'proxy' => 'Phinx\Db\Adapter\ProxyAdapter', + ); + + /** + * Add or replace an adapter with a fully qualified class name. + * + * @throws \RuntimeException + * @param string $name + * @param string $class + * @return $this + */ + public function registerAdapter($name, $class) + { + if (!is_subclass_of($class, 'Phinx\Db\Adapter\AdapterInterface')) { + throw new \RuntimeException(sprintf( + 'Adapter class "%s" must implement Phinx\\Db\\Adapter\\AdapterInterface', + $class + )); + } + $this->adapters[$name] = $class; + return $this; + } + + /** + * Get an adapter class by name. + * + * @throws \RuntimeException + * @param string $name + * @return string + */ + protected function getClass($name) + { + if (empty($this->adapters[$name])) { + throw new \RuntimeException(sprintf( + 'Adapter "%s" has not been registered', + $name + )); + } + return $this->adapters[$name]; + } + + /** + * Get an adapter instance by name. + * + * @param string $name + * @param array $options + * @return AdapterInterface + */ + public function getAdapter($name, array $options) + { + $class = $this->getClass($name); + return new $class($options); + } + + /** + * Add or replace a wrapper with a fully qualified class name. + * + * @throws \RuntimeException + * @param string $name + * @param string $class + * @return $this + */ + public function registerWrapper($name, $class) + { + if (!is_subclass_of($class, 'Phinx\Db\Adapter\WrapperInterface')) { + throw new \RuntimeException(sprintf( + 'Wrapper class "%s" must be implement Phinx\\Db\\Adapter\\WrapperInterface', + $class + )); + } + $this->wrappers[$name] = $class; + return $this; + } + + /** + * Get a wrapper class by name. + * + * @throws \RuntimeException + * @param string $name + * @return string + */ + protected function getWrapperClass($name) + { + if (empty($this->wrappers[$name])) { + throw new \RuntimeException(sprintf( + 'Wrapper "%s" has not been registered', + $name + )); + } + return $this->wrappers[$name]; + } + + /** + * Get a wrapper instance by name. + * + * @param string $name + * @param AdapterInterface $adapter + * @return AdapterInterface + */ + public function getWrapper($name, AdapterInterface $adapter) + { + $class = $this->getWrapperClass($name); + return new $class($adapter); + } +} diff --git a/src/DDLWrapper/Db/Adapter/AdapterInterface.php b/src/DDLWrapper/Db/Adapter/AdapterInterface.php new file mode 100644 index 0000000..881def4 --- /dev/null +++ b/src/DDLWrapper/Db/Adapter/AdapterInterface.php @@ -0,0 +1,391 @@ + + */ +interface AdapterInterface +{ + const PHINX_TYPE_STRING = 'string'; + const PHINX_TYPE_CHAR = 'char'; + const PHINX_TYPE_TEXT = 'text'; + const PHINX_TYPE_INTEGER = 'integer'; + const PHINX_TYPE_BIG_INTEGER = 'biginteger'; + const PHINX_TYPE_FLOAT = 'float'; + const PHINX_TYPE_DECIMAL = 'decimal'; + const PHINX_TYPE_DATETIME = 'datetime'; + const PHINX_TYPE_TIMESTAMP = 'timestamp'; + const PHINX_TYPE_TIME = 'time'; + const PHINX_TYPE_DATE = 'date'; + const PHINX_TYPE_BINARY = 'binary'; + const PHINX_TYPE_VARBINARY = 'varbinary'; + const PHINX_TYPE_BLOB = 'blob'; + const PHINX_TYPE_BOOLEAN = 'boolean'; + const PHINX_TYPE_JSON = 'json'; + const PHINX_TYPE_JSONB = 'jsonb'; + const PHINX_TYPE_UUID = 'uuid'; + const PHINX_TYPE_FILESTREAM = 'filestream'; + + // Geospatial database types + const PHINX_TYPE_GEOMETRY = 'geometry'; + const PHINX_TYPE_POINT = 'point'; + const PHINX_TYPE_LINESTRING = 'linestring'; + const PHINX_TYPE_POLYGON = 'polygon'; + + // only for mysql so far + const PHINX_TYPE_ENUM = 'enum'; + const PHINX_TYPE_SET = 'set'; + + /** + * Returns the adapter type. + * + * @return string + */ + public function getAdapterType(); + + /** + * Initializes the database connection. + * + * @throws \RuntimeException When the requested database driver is not installed. + * @return void + */ + public function connect(); + + /** + * Closes the database connection. + * + * @return void + */ + public function disconnect(); + + /** + * Does the adapter support transactions? + * + * @return boolean + */ + public function hasTransactions(); + + /** + * Begin a transaction. + * + * @return void + */ + public function beginTransaction(); + + /** + * Commit a transaction. + * + * @return void + */ + public function commitTransaction(); + + /** + * Rollback a transaction. + * + * @return void + */ + public function rollbackTransaction(); + + /** + * Executes a SQL statement and returns the number of affected rows. + * + * @param string $sql SQL + * @return int + */ + public function execute($sql); + + /** + * Executes a SQL statement and returns the result as an array. + * + * @param string $sql SQL + * @return array + */ + public function query($sql); + + /** + * Executes a query and returns only one row as an array. + * + * @param string $sql SQL + * @return array + */ + public function fetchRow($sql); + + /** + * Executes a query and returns an array of rows. + * + * @param string $sql SQL + * @return array + */ + public function fetchAll($sql); + + /** + * Inserts data into a table. + * + * @param Table $table where to insert data + * @param array $row + * @return void + */ + public function insert(Table $table, $row); + + /** + * Quotes a table name for use in a query. + * + * @param string $tableName Table Name + * @return string + */ + public function quoteTableName($tableName); + + /** + * Quotes a column name for use in a query. + * + * @param string $columnName Table Name + * @return string + */ + public function quoteColumnName($columnName); + + /** + * Checks to see if a table exists. + * + * @param string $tableName Table Name + * @return boolean + */ + public function hasTable($tableName); + + /** + * Creates the specified database table. + * + * @param Table $table Table + * @return void + */ + public function createTable(Table $table); + + /** + * Renames the specified database table. + * + * @param string $tableName Table Name + * @param string $newName New Name + * @return void + */ + public function renameTable($tableName, $newName); + + /** + * Drops the specified database table. + * + * @param string $tableName Table Name + * @return void + */ + public function dropTable($tableName); + + /** + * Returns table columns + * + * @param string $tableName Table Name + * @return Column[] + */ + public function getColumns($tableName); + + /** + * Checks to see if a column exists. + * + * @param string $tableName Table Name + * @param string $columnName Column Name + * @return boolean + */ + public function hasColumn($tableName, $columnName); + + /** + * Adds the specified column to a database table. + * + * @param Table $table Table + * @param Column $column Column + * @return void + */ + public function addColumn(Table $table, Column $column); + + /** + * Renames the specified column. + * + * @param string $tableName Table Name + * @param string $columnName Column Name + * @param string $newColumnName New Column Name + * @return void + */ + public function renameColumn($tableName, $columnName, $newColumnName); + + /** + * Change a table column type. + * + * @param string $tableName Table Name + * @param string $columnName Column Name + * @param Column $newColumn New Column + * @return Table + */ + public function changeColumn($tableName, $columnName, Column $newColumn); + + /** + * Drops the specified column. + * + * @param string $tableName Table Name + * @param string $columnName Column Name + * @return void + */ + public function dropColumn($tableName, $columnName); + + /** + * Checks to see if an index exists. + * + * @param string $tableName Table Name + * @param mixed $columns Column(s) + * @return boolean + */ + public function hasIndex($tableName, $columns); + + /** + * Checks to see if an index specified by name exists. + * + * @param string $tableName Table Name + * @param string $indexName + * @return boolean + */ + public function hasIndexByName($tableName, $indexName); + + /** + * Adds the specified index to a database table. + * + * @param Table $table Table + * @param Index $index Index + * @return void + */ + public function addIndex(Table $table, Index $index); + + /** + * Drops the specified index from a database table. + * + * @param string $tableName + * @param mixed $columns Column(s) + * @return void + */ + public function dropIndex($tableName, $columns); + + /** + * Drops the index specified by name from a database table. + * + * @param string $tableName + * @param string $indexName + * @return void + */ + public function dropIndexByName($tableName, $indexName); + + /** + * Checks to see if a foreign key exists. + * + * @param string $tableName + * @param string[] $columns Column(s) + * @param string $constraint Constraint name + * @return boolean + */ + public function hasForeignKey($tableName, $columns, $constraint = null); + + /** + * Adds the specified foreign key to a database table. + * + * @param Table $table + * @param ForeignKey $foreignKey + * @return void + */ + public function addForeignKey(Table $table, ForeignKey $foreignKey); + + /** + * Drops the specified foreign key from a database table. + * + * @param string $tableName + * @param string[] $columns Column(s) + * @param string $constraint Constraint name + * @return void + */ + public function dropForeignKey($tableName, $columns, $constraint = null); + + /** + * Returns an array of the supported Phinx column types. + * + * @return array + */ + public function getColumnTypes(); + + /** + * Checks that the given column is of a supported type. + * + * @param Column $column + * @return boolean + */ + public function isValidColumnType(Column $column); + + /** + * Converts the Phinx logical type to the adapter's SQL type. + * + * @param string $type + * @param integer $limit + * @return string + */ + public function getSqlType($type, $limit = null); + + /** + * Creates a new database. + * + * @param string $name Database Name + * @param array $options Options + * @return void + */ + public function createDatabase($name, $options = array()); + + /** + * Checks to see if a database exists. + * + * @param string $name Database Name + * @return boolean + */ + public function hasDatabase($name); + + /** + * Drops the specified database. + * + * @param string $name Database Name + * @return void + */ + public function dropDatabase($name); +} diff --git a/src/DDLWrapper/Db/Adapter/AdapterWrapper.php b/src/DDLWrapper/Db/Adapter/AdapterWrapper.php new file mode 100644 index 0000000..b853a76 --- /dev/null +++ b/src/DDLWrapper/Db/Adapter/AdapterWrapper.php @@ -0,0 +1,471 @@ + + */ +abstract class AdapterWrapper implements AdapterInterface, WrapperInterface +{ + /** + * @var AdapterInterface + */ + protected $adapter; + + /** + * {@inheritdoc} + */ + public function __construct(AdapterInterface $adapter) + { + $this->setAdapter($adapter); + } + + /** + * {@inheritdoc} + */ + public function setAdapter(AdapterInterface $adapter) + { + $this->adapter = $adapter; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getAdapter() + { + return $this->adapter; + } + + /** + * {@inheritdoc} + */ + public function setOptions(array $options) + { + $this->adapter->setOptions($options); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getOptions() + { + return $this->adapter->getOptions(); + } + + /** + * {@inheritdoc} + */ + public function hasOption($name) + { + return $this->adapter->hasOption($name); + } + + /** + * {@inheritdoc} + */ + public function getOption($name) + { + return $this->adapter->getOption($name); + } + + /** + * {@inheritdoc} + */ + public function connect() + { + return $this->getAdapter()->connect(); + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + return $this->getAdapter()->disconnect(); + } + + /** + * {@inheritdoc} + */ + public function execute($sql) + { + return $this->getAdapter()->execute($sql); + } + + /** + * {@inheritdoc} + */ + public function query($sql) + { + return $this->getAdapter()->query($sql); + } + + /** + * {@inheritdoc} + */ + public function insert(Table $table, $row) + { + return $this->getAdapter()->insert($table, $row); + } + + /** + * {@inheritdoc} + */ + public function fetchRow($sql) + { + return $this->getAdapter()->fetchRow($sql); + } + + /** + * {@inheritdoc} + */ + public function fetchAll($sql) + { + return $this->getAdapter()->fetchAll($sql); + } + + /** + * {@inheritdoc} + */ + public function getVersions() + { + return $this->getAdapter()->getVersions(); + } + + /** + * {@inheritdoc} + */ + public function getVersionLog() + { + return $this->getAdapter()->getVersionLog(); + } + + /** + * {@inheritdoc} + */ + public function migrated(MigrationInterface $migration, $direction, $startTime, $endTime) + { + $this->getAdapter()->migrated($migration, $direction, $startTime, $endTime); + return $this; + } + + /** + * @inheritDoc + */ + public function toggleBreakpoint(MigrationInterface $migration) + { + $this->getAdapter()->toggleBreakpoint($migration); + return $this; + } + + /** + * @inheritDoc + */ + public function resetAllBreakpoints() + { + return $this->getAdapter()->resetAllBreakpoints(); + } + + /** + * {@inheritdoc} + */ + public function hasSchemaTable() + { + return $this->getAdapter()->hasSchemaTable(); + } + + /** + * {@inheritdoc} + */ + public function createSchemaTable() + { + return $this->getAdapter()->createSchemaTable(); + } + + /** + * {@inheritdoc} + */ + public function getColumnTypes() + { + return $this->getAdapter()->getColumnTypes(); + } + + /** + * {@inheritdoc} + */ + public function isValidColumnType(Column $column) + { + return $this->getAdapter()->isValidColumnType($column); + } + + /** + * {@inheritdoc} + */ + public function hasTransactions() + { + return $this->getAdapter()->hasTransactions(); + } + + /** + * {@inheritdoc} + */ + public function beginTransaction() + { + return $this->getAdapter()->beginTransaction(); + } + + /** + * {@inheritdoc} + */ + public function commitTransaction() + { + return $this->getAdapter()->commitTransaction(); + } + + /** + * {@inheritdoc} + */ + public function rollbackTransaction() + { + return $this->getAdapter()->rollbackTransaction(); + } + + /** + * {@inheritdoc} + */ + public function quoteTableName($tableName) + { + return $this->getAdapter()->quoteTableName($tableName); + } + + /** + * {@inheritdoc} + */ + public function quoteColumnName($columnName) + { + return $this->getAdapter()->quoteColumnName($columnName); + } + + /** + * {@inheritdoc} + */ + public function hasTable($tableName) + { + return $this->getAdapter()->hasTable($tableName); + } + + /** + * {@inheritdoc} + */ + public function createTable(Table $table) + { + return $this->getAdapter()->createTable($table); + } + + /** + * {@inheritdoc} + */ + public function renameTable($tableName, $newTableName) + { + return $this->getAdapter()->renameTable($tableName, $newTableName); + } + + /** + * {@inheritdoc} + */ + public function dropTable($tableName) + { + return $this->getAdapter()->dropTable($tableName); + } + + /** + * {@inheritdoc} + */ + public function getColumns($tableName) + { + return $this->getAdapter()->getColumns($tableName); + } + + /** + * {@inheritdoc} + */ + public function hasColumn($tableName, $columnName) + { + return $this->getAdapter()->hasColumn($tableName, $columnName); + } + + /** + * {@inheritdoc} + */ + public function addColumn(Table $table, Column $column) + { + return $this->getAdapter()->addColumn($table, $column); + } + + /** + * {@inheritdoc} + */ + public function renameColumn($tableName, $columnName, $newColumnName) + { + return $this->getAdapter()->renameColumn($tableName, $columnName, $newColumnName); + } + + /** + * {@inheritdoc} + */ + public function changeColumn($tableName, $columnName, Column $newColumn) + { + return $this->getAdapter()->changeColumn($tableName, $columnName, $newColumn); + } + + /** + * {@inheritdoc} + */ + public function dropColumn($tableName, $columnName) + { + return $this->getAdapter()->dropColumn($tableName, $columnName); + } + + /** + * {@inheritdoc} + */ + public function hasIndex($tableName, $columns) + { + return $this->getAdapter()->hasIndex($tableName, $columns); + } + + /** + * {@inheritdoc} + */ + public function hasIndexByName($tableName, $indexName) + { + return $this->getAdapter()->hasIndexByName($tableName, $indexName); + } + + /** + * {@inheritdoc} + */ + public function addIndex(Table $table, Index $index) + { + return $this->getAdapter()->addIndex($table, $index); + } + + /** + * {@inheritdoc} + */ + public function dropIndex($tableName, $columns, $options = array()) + { + return $this->getAdapter()->dropIndex($tableName, $columns, $options); + } + + /** + * {@inheritdoc} + */ + public function dropIndexByName($tableName, $indexName) + { + return $this->getAdapter()->dropIndexByName($tableName, $indexName); + } + + /** + * {@inheritdoc} + */ + public function hasForeignKey($tableName, $columns, $constraint = null) + { + return $this->getAdapter()->hasForeignKey($tableName, $columns, $constraint); + } + + /** + * {@inheritdoc} + */ + public function addForeignKey(Table $table, ForeignKey $foreignKey) + { + return $this->getAdapter()->addForeignKey($table, $foreignKey); + } + + /** + * {@inheritdoc} + */ + public function dropForeignKey($tableName, $columns, $constraint = null) + { + return $this->getAdapter()->dropForeignKey($tableName, $columns, $constraint); + } + + /** + * {@inheritdoc} + */ + public function getSqlType($type, $limit = null) + { + return $this->getAdapter()->getSqlType($type, $limit); + } + + /** + * {@inheritdoc} + */ + public function createDatabase($name, $options = array()) + { + return $this->getAdapter()->createDatabase($name, $options); + } + + /** + * {@inheritdoc} + */ + public function hasDatabase($name) + { + return $this->getAdapter()->hasDatabase($name); + } + + /** + * {@inheritdoc} + */ + public function dropDatabase($name) + { + return $this->getAdapter()->dropDatabase($name); + } + + /** + * @inheritDoc + */ + public function castToBool($value) + { + return $this->getAdapter()->castToBool($value); + } +} diff --git a/src/DDLWrapper/Db/Adapter/MysqlAdapter.php b/src/DDLWrapper/Db/Adapter/MysqlAdapter.php new file mode 100644 index 0000000..67b938b --- /dev/null +++ b/src/DDLWrapper/Db/Adapter/MysqlAdapter.php @@ -0,0 +1,1148 @@ + + */ +class MysqlAdapter extends PdoAdapter implements AdapterInterface +{ + + protected $signedColumnTypes = array('integer' => true, 'biginteger' => true, 'float' => true, 'decimal' => true, 'boolean' => true); + + const TEXT_TINY = 255; + const TEXT_SMALL = 255; /* deprecated, alias of TEXT_TINY */ + const TEXT_REGULAR = 65535; + const TEXT_MEDIUM = 16777215; + const TEXT_LONG = 4294967295; + + // According to https://dev.mysql.com/doc/refman/5.0/en/blob.html BLOB sizes are the same as TEXT + const BLOB_TINY = 255; + const BLOB_SMALL = 255; /* deprecated, alias of BLOB_TINY */ + const BLOB_REGULAR = 65535; + const BLOB_MEDIUM = 16777215; + const BLOB_LONG = 4294967295; + + const INT_TINY = 255; + const INT_SMALL = 65535; + const INT_MEDIUM = 16777215; + const INT_REGULAR = 4294967295; + const INT_BIG = 18446744073709551615; + + const TYPE_YEAR = 'year'; + + /** + * {@inheritdoc} + */ + public function connect() + { + if (null === $this->connection) { + if (!class_exists('PDO') || !in_array('mysql', \PDO::getAvailableDrivers(), true)) { + // @codeCoverageIgnoreStart + throw new \RuntimeException('You need to enable the PDO_Mysql extension for Phinx to run properly.'); + // @codeCoverageIgnoreEnd + } + + $db = null; + $options = $this->getOptions(); + + $dsn = 'mysql:'; + + if (!empty($options['unix_socket'])) { + // use socket connection + $dsn .= 'unix_socket=' . $options['unix_socket']; + } else { + // use network connection + $dsn .= 'host=' . $options['host']; + if (!empty($options['port'])) { + $dsn .= ';port=' . $options['port']; + } + } + + $dsn .= ';dbname=' . $options['name']; + + // charset support + if (!empty($options['charset'])) { + $dsn .= ';charset=' . $options['charset']; + } + + $driverOptions = array(\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION); + + // support arbitrary \PDO::MYSQL_ATTR_* driver options and pass them to PDO + // http://php.net/manual/en/ref.pdo-mysql.php#pdo-mysql.constants + foreach ($options as $key => $option) { + if (strpos($key, 'mysql_attr_') === 0) { + $driverOptions[constant('\PDO::' . strtoupper($key))] = $option; + } + } + + try { + $db = new \PDO($dsn, $options['user'], $options['pass'], $driverOptions); + } catch (\PDOException $exception) { + throw new \InvalidArgumentException(sprintf( + 'There was a problem connecting to the database: %s', + $exception->getMessage() + )); + } + + $this->setConnection($db); + } + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + $this->connection = null; + } + + /** + * {@inheritdoc} + */ + public function hasTransactions() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function beginTransaction() + { + $this->execute('START TRANSACTION'); + } + + /** + * {@inheritdoc} + */ + public function commitTransaction() + { + $this->execute('COMMIT'); + } + + /** + * {@inheritdoc} + */ + public function rollbackTransaction() + { + $this->execute('ROLLBACK'); + } + + /** + * {@inheritdoc} + */ + public function quoteTableName($tableName) + { + return str_replace('.', '`.`', $this->quoteColumnName($tableName)); + } + + /** + * {@inheritdoc} + */ + public function quoteColumnName($columnName) + { + return '`' . str_replace('`', '``', $columnName) . '`'; + } + + /** + * {@inheritdoc} + */ + public function hasTable($tableName) + { + $options = $this->getOptions(); + + $exists = $this->fetchRow(sprintf( + "SELECT TABLE_NAME + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s'", + $options['name'], $tableName + )); + + return !empty($exists); + } + + /** + * {@inheritdoc} + */ + public function createTable(Table $table) + { + $this->startCommandTimer(); + + // This method is based on the MySQL docs here: http://dev.mysql.com/doc/refman/5.1/en/create-index.html + $defaultOptions = array( + 'engine' => 'InnoDB', + 'collation' => 'utf8_general_ci' + ); + $options = array_merge($defaultOptions, $table->getOptions()); + + // Add the default primary key + $columns = $table->getPendingColumns(); + if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) { + $column = new Column(); + $column->setName('id') + ->setType('integer') + ->setIdentity(true); + + array_unshift($columns, $column); + $options['primary_key'] = 'id'; + + } elseif (isset($options['id']) && is_string($options['id'])) { + // Handle id => "field_name" to support AUTO_INCREMENT + $column = new Column(); + $column->setName($options['id']) + ->setType('integer') + ->setIdentity(true); + + array_unshift($columns, $column); + $options['primary_key'] = $options['id']; + } + + // TODO - process table options like collation etc + + // process table engine (default to InnoDB) + $optionsStr = 'ENGINE = InnoDB'; + if (isset($options['engine'])) { + $optionsStr = sprintf('ENGINE = %s', $options['engine']); + } + + // process table collation + if (isset($options['collation'])) { + $charset = explode('_', $options['collation']); + $optionsStr .= sprintf(' CHARACTER SET %s', $charset[0]); + $optionsStr .= sprintf(' COLLATE %s', $options['collation']); + } + + // set the table comment + if (isset($options['comment'])) { + $optionsStr .= sprintf(" COMMENT=%s ", $this->getConnection()->quote($options['comment'])); + } + + $sql = 'CREATE TABLE '; + $sql .= $this->quoteTableName($table->getName()) . ' ('; + foreach ($columns as $column) { + $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', '; + } + + // set the primary key(s) + if (isset($options['primary_key'])) { + $sql = rtrim($sql); + $sql .= ' PRIMARY KEY ('; + if (is_string($options['primary_key'])) { // handle primary_key => 'id' + $sql .= $this->quoteColumnName($options['primary_key']); + } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id') + // PHP 5.4 will allow access of $this, so we can call quoteColumnName() directly in the + // anonymous function, but for now just hard-code the adapter quotes + $sql .= implode( + ',', + array_map( + function ($v) { + return '`' . $v . '`'; + }, + $options['primary_key'] + ) + ); + } + $sql .= ')'; + } else { + $sql = substr(rtrim($sql), 0, -1); // no primary keys + } + + // set the indexes + $indexes = $table->getIndexes(); + if (!empty($indexes)) { + foreach ($indexes as $index) { + $sql .= ', ' . $this->getIndexSqlDefinition($index); + } + } + + // set the foreign keys + $foreignKeys = $table->getForeignKeys(); + if (!empty($foreignKeys)) { + foreach ($foreignKeys as $foreignKey) { + $sql .= ', ' . $this->getForeignKeySqlDefinition($foreignKey); + } + } + + $sql .= ') ' . $optionsStr; + $sql = rtrim($sql) . ';'; + + // execute the sql + //$this->writeCommand('createTable', array($table->getName())); + $this->execute($sql); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function renameTable($tableName, $newTableName) + { + $this->startCommandTimer(); + //$this->writeCommand('renameTable', array($tableName, $newTableName)); + $this->execute(sprintf('RENAME TABLE %s TO %s', $this->quoteTableName($tableName), $this->quoteTableName($newTableName))); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function dropTable($tableName) + { + $this->startCommandTimer(); + // $this->writeCommand('dropTable', array($tableName)); + $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tableName))); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function getColumns($tableName) + { + $columns = array(); + $rows = $this->fetchAll(sprintf('SHOW COLUMNS FROM %s', $this->quoteTableName($tableName))); + foreach ($rows as $columnInfo) { + + $phinxType = $this->getPhinxType($columnInfo['Type']); + + $column = new Column(); + $column->setName($columnInfo['Field']) + ->setNull($columnInfo['Null'] !== 'NO') + ->setDefault($columnInfo['Default']) + ->setType($phinxType['name']) + ->setLimit($phinxType['limit']); + + if ($columnInfo['Extra'] === 'auto_increment') { + $column->setIdentity(true); + } + + $columns[] = $column; + } + + return $columns; + } + + /** + * {@inheritdoc} + */ + public function hasColumn($tableName, $columnName) + { + $rows = $this->fetchAll(sprintf('SHOW COLUMNS FROM %s', $this->quoteTableName($tableName))); + foreach ($rows as $column) { + if (strcasecmp($column['Field'], $columnName) === 0) { + return true; + } + } + + return false; + } + + /** + * Get the defintion for a `DEFAULT` statement. + * + * @param mixed $default + * @return string + */ + protected function getDefaultValueDefinition($default) + { + if (is_string($default) && 'CURRENT_TIMESTAMP' !== $default) { + $default = $this->getConnection()->quote($default); + } elseif (is_bool($default)) { + $default = $this->castToBool($default); + } + return isset($default) ? ' DEFAULT ' . $default : ''; + } + + /** + * {@inheritdoc} + */ + public function addColumn(Table $table, Column $column) + { + $this->startCommandTimer(); + $sql = sprintf( + 'ALTER TABLE %s ADD %s %s', + $this->quoteTableName($table->getName()), + $this->quoteColumnName($column->getName()), + $this->getColumnSqlDefinition($column) + ); + + if ($column->getAfter()) { + $sql .= ' AFTER ' . $this->quoteColumnName($column->getAfter()); + } + + //$this->writeCommand('addColumn', array($table->getName(), $column->getName(), $column->getType())); + $this->execute($sql); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function renameColumn($tableName, $columnName, $newColumnName) + { + $this->startCommandTimer(); + $rows = $this->fetchAll(sprintf('DESCRIBE %s', $this->quoteTableName($tableName))); + foreach ($rows as $row) { + if (strcasecmp($row['Field'], $columnName) === 0) { + $null = ($row['Null'] == 'NO') ? 'NOT NULL' : 'NULL'; + $extra = ' ' . strtoupper($row['Extra']); + if (!is_null($row['Default'])) { + $extra .= $this->getDefaultValueDefinition($row['Default']); + } + $definition = $row['Type'] . ' ' . $null . $extra; + + //$this->writeCommand('renameColumn', array($tableName, $columnName, $newColumnName)); + $this->execute( + sprintf( + 'ALTER TABLE %s CHANGE COLUMN %s %s %s', + $this->quoteTableName($tableName), + $this->quoteColumnName($columnName), + $this->quoteColumnName($newColumnName), + $definition + ) + ); + $this->endCommandTimer(); + return; + } + } + + throw new \InvalidArgumentException(sprintf( + 'The specified column doesn\'t exist: ' + . $columnName + )); + } + + /** + * {@inheritdoc} + */ + public function changeColumn($tableName, $columnName, Column $newColumn) + { + $this->startCommandTimer(); + //$this->writeCommand('changeColumn', array($tableName, $columnName, $newColumn->getType())); + $after = $newColumn->getAfter() ? ' AFTER ' . $this->quoteColumnName($newColumn->getAfter()) : ''; + $this->execute( + sprintf( + 'ALTER TABLE %s CHANGE %s %s %s%s', + $this->quoteTableName($tableName), + $this->quoteColumnName($columnName), + $this->quoteColumnName($newColumn->getName()), + $this->getColumnSqlDefinition($newColumn), + $after + ) + ); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function dropColumn($tableName, $columnName) + { + $this->startCommandTimer(); + //$this->writeCommand('dropColumn', array($tableName, $columnName)); + $this->execute( + sprintf( + 'ALTER TABLE %s DROP COLUMN %s', + $this->quoteTableName($tableName), + $this->quoteColumnName($columnName) + ) + ); + $this->endCommandTimer(); + } + + /** + * Get an array of indexes from a particular table. + * + * @param string $tableName Table Name + * @return array + */ + protected function getIndexes($tableName) + { + $indexes = array(); + $rows = $this->fetchAll(sprintf('SHOW INDEXES FROM %s', $this->quoteTableName($tableName))); + foreach ($rows as $row) { + if (!isset($indexes[$row['Key_name']])) { + $indexes[$row['Key_name']] = array('columns' => array()); + } + $indexes[$row['Key_name']]['columns'][] = strtolower($row['Column_name']); + } + return $indexes; + } + + /** + * {@inheritdoc} + */ + public function hasIndex($tableName, $columns) + { + if (is_string($columns)) { + $columns = array($columns); // str to array + } + + $columns = array_map('strtolower', $columns); + $indexes = $this->getIndexes($tableName); + + foreach ($indexes as $index) { + if ($columns == $index['columns']) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function hasIndexByName($tableName, $indexName) + { + $indexes = $this->getIndexes($tableName); + + foreach ($indexes as $name => $index) { + if ($name === $indexName) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function addIndex(Table $table, Index $index) + { + $this->startCommandTimer(); + //$this->writeCommand('addIndex', array($table->getName(), $index->getColumns())); + $this->execute( + sprintf( + 'ALTER TABLE %s ADD %s', + $this->quoteTableName($table->getName()), + $this->getIndexSqlDefinition($index) + ) + ); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function dropIndex($tableName, $columns) + { + $this->startCommandTimer(); + if (is_string($columns)) { + $columns = array($columns); // str to array + } + + $this->writeCommand('dropIndex', array($tableName, $columns)); + $indexes = $this->getIndexes($tableName); + $columns = array_map('strtolower', $columns); + + foreach ($indexes as $indexName => $index) { + if ($columns == $index['columns']) { + $this->execute( + sprintf( + 'ALTER TABLE %s DROP INDEX %s', + $this->quoteTableName($tableName), + $this->quoteColumnName($indexName) + ) + ); + $this->endCommandTimer(); + return; + } + } + } + + /** + * {@inheritdoc} + */ + public function dropIndexByName($tableName, $indexName) + { + $this->startCommandTimer(); + $this->writeCommand('dropIndexByName', array($tableName, $indexName)); + $indexes = $this->getIndexes($tableName); + + foreach ($indexes as $name => $index) { + //$a = array_diff($columns, $index['columns']); + if ($name === $indexName) { + $this->execute( + sprintf( + 'ALTER TABLE %s DROP INDEX %s', + $this->quoteTableName($tableName), + $this->quoteColumnName($indexName) + ) + ); + $this->endCommandTimer(); + return; + } + } + } + + /** + * {@inheritdoc} + */ + public function hasForeignKey($tableName, $columns, $constraint = null) + { + if (is_string($columns)) { + $columns = array($columns); // str to array + } + $foreignKeys = $this->getForeignKeys($tableName); + if ($constraint) { + if (isset($foreignKeys[$constraint])) { + return !empty($foreignKeys[$constraint]); + } + return false; + } else { + foreach ($foreignKeys as $key) { + $a = array_diff($columns, $key['columns']); + if ($columns == $key['columns']) { + return true; + } + } + return false; + } + } + + /** + * Get an array of foreign keys from a particular table. + * + * @param string $tableName Table Name + * @return array + */ + protected function getForeignKeys($tableName) + { + $foreignKeys = array(); + $rows = $this->fetchAll(sprintf( + "SELECT + CONSTRAINT_NAME, + TABLE_NAME, + COLUMN_NAME, + REFERENCED_TABLE_NAME, + REFERENCED_COLUMN_NAME + FROM information_schema.KEY_COLUMN_USAGE + WHERE REFERENCED_TABLE_SCHEMA = DATABASE() + AND REFERENCED_TABLE_NAME IS NOT NULL + AND TABLE_NAME = '%s' + ORDER BY POSITION_IN_UNIQUE_CONSTRAINT", + $tableName + )); + foreach ($rows as $row) { + $foreignKeys[$row['CONSTRAINT_NAME']]['table'] = $row['TABLE_NAME']; + $foreignKeys[$row['CONSTRAINT_NAME']]['columns'][] = $row['COLUMN_NAME']; + $foreignKeys[$row['CONSTRAINT_NAME']]['referenced_table'] = $row['REFERENCED_TABLE_NAME']; + $foreignKeys[$row['CONSTRAINT_NAME']]['referenced_columns'][] = $row['REFERENCED_COLUMN_NAME']; + } + return $foreignKeys; + } + + /** + * {@inheritdoc} + */ + public function addForeignKey(Table $table, ForeignKey $foreignKey) + { + $this->startCommandTimer(); + $this->writeCommand('addForeignKey', array($table->getName(), $foreignKey->getColumns())); + $this->execute( + sprintf( + 'ALTER TABLE %s ADD %s', + $this->quoteTableName($table->getName()), + $this->getForeignKeySqlDefinition($foreignKey) + ) + ); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function dropForeignKey($tableName, $columns, $constraint = null) + { + $this->startCommandTimer(); + if (is_string($columns)) { + $columns = array($columns); // str to array + } + + $this->writeCommand('dropForeignKey', array($tableName, $columns)); + + if ($constraint) { + $this->execute( + sprintf( + 'ALTER TABLE %s DROP FOREIGN KEY %s', + $this->quoteTableName($tableName), + $constraint + ) + ); + $this->endCommandTimer(); + return; + } else { + foreach ($columns as $column) { + $rows = $this->fetchAll(sprintf( + "SELECT + CONSTRAINT_NAME + FROM information_schema.KEY_COLUMN_USAGE + WHERE REFERENCED_TABLE_SCHEMA = DATABASE() + AND REFERENCED_TABLE_NAME IS NOT NULL + AND TABLE_NAME = '%s' + AND COLUMN_NAME = '%s' + ORDER BY POSITION_IN_UNIQUE_CONSTRAINT", + $tableName, + $column + )); + foreach ($rows as $row) { + $this->dropForeignKey($tableName, $columns, $row['CONSTRAINT_NAME']); + } + } + } + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function getSqlType($type, $limit = null) + { + switch ($type) { + case static::PHINX_TYPE_STRING: + return array('name' => 'varchar', 'limit' => $limit ? $limit : 255); + break; + case static::PHINX_TYPE_CHAR: + return array('name' => 'char', 'limit' => $limit ? $limit : 255); + break; + case static::PHINX_TYPE_TEXT: + if ($limit) { + $sizes = array( + // Order matters! Size must always be tested from longest to shortest! + 'longtext' => static::TEXT_LONG, + 'mediumtext' => static::TEXT_MEDIUM, + 'text' => static::TEXT_REGULAR, + 'tinytext' => static::TEXT_SMALL, + ); + foreach ($sizes as $name => $length) { + if ($limit >= $length) { + return array('name' => $name); + } + } + } + return array('name' => 'text'); + break; + case static::PHINX_TYPE_BINARY: + return array('name' => 'binary', 'limit' => $limit ? $limit : 255); + break; + case static::PHINX_TYPE_VARBINARY: + return array('name' => 'varbinary', 'limit' => $limit ? $limit : 255); + break; + case static::PHINX_TYPE_BLOB: + if ($limit) { + $sizes = array( + // Order matters! Size must always be tested from longest to shortest! + 'longblob' => static::BLOB_LONG, + 'mediumblob' => static::BLOB_MEDIUM, + 'blob' => static::BLOB_REGULAR, + 'tinyblob' => static::BLOB_SMALL, + ); + foreach ($sizes as $name => $length) { + if ($limit >= $length) { + return array('name' => $name); + } + } + } + return array('name' => 'blob'); + break; + case static::PHINX_TYPE_INTEGER: + if ($limit && $limit >= static::INT_TINY) { + $sizes = array( + // Order matters! Size must always be tested from longest to shortest! + 'bigint' => static::INT_BIG, + 'int' => static::INT_REGULAR, + 'mediumint' => static::INT_MEDIUM, + 'smallint' => static::INT_SMALL, + 'tinyint' => static::INT_TINY, + ); + $limits = array( + 'int' => 11, + 'bigint' => 20, + ); + foreach ($sizes as $name => $length) { + if ($limit >= $length) { + $def = array('name' => $name); + if (isset($limits[$name])) { + $def['limit'] = $limits[$name]; + } + return $def; + } + } + } elseif (!$limit) { + $limit = 11; + } + return array('name' => 'int', 'limit' => $limit); + break; + case static::PHINX_TYPE_BIG_INTEGER: + return array('name' => 'bigint', 'limit' => 20); + break; + case static::PHINX_TYPE_FLOAT: + return array('name' => 'float'); + break; + case static::PHINX_TYPE_DECIMAL: + return array('name' => 'decimal'); + break; + case static::PHINX_TYPE_DATETIME: + return array('name' => 'datetime'); + break; + case static::PHINX_TYPE_TIMESTAMP: + return array('name' => 'timestamp'); + break; + case static::PHINX_TYPE_TIME: + return array('name' => 'time'); + break; + case static::PHINX_TYPE_DATE: + return array('name' => 'date'); + break; + case static::PHINX_TYPE_BOOLEAN: + return array('name' => 'tinyint', 'limit' => 1); + break; + case static::PHINX_TYPE_UUID: + return array('name' => 'char', 'limit' => 36); + // Geospatial database types + case static::PHINX_TYPE_GEOMETRY: + case static::PHINX_TYPE_POINT: + case static::PHINX_TYPE_LINESTRING: + case static::PHINX_TYPE_POLYGON: + return array('name' => $type); + case static::PHINX_TYPE_ENUM: + return array('name' => 'enum'); + break; + case static::PHINX_TYPE_SET: + return array('name' => 'set'); + break; + case static::TYPE_YEAR: + if (!$limit || in_array($limit, array(2, 4))) + $limit = 4; + return array('name' => 'year', 'limit' => $limit); + break; + case static::PHINX_TYPE_JSON: + return array('name' => 'json'); + break; + default: + throw new \RuntimeException('The type: "' . $type . '" is not supported.'); + } + } + + /** + * Returns Phinx type by SQL type + * + * @param string $sqlTypeDef + * @throws \RuntimeException + * @internal param string $sqlType SQL type + * @returns string Phinx type + */ + public function getPhinxType($sqlTypeDef) + { + if (!preg_match('/^([\w]+)(\(([\d]+)*(,([\d]+))*\))*(.+)*$/', $sqlTypeDef, $matches)) { + throw new \RuntimeException('Column type ' . $sqlTypeDef . ' is not supported'); + } else { + $limit = null; + $precision = null; + $type = $matches[1]; + if (count($matches) > 2) { + $limit = $matches[3] ? (int) $matches[3] : null; + } + if (count($matches) > 4) { + $precision = (int) $matches[5]; + } + if ($type === 'tinyint' && $limit === 1) { + $type = static::PHINX_TYPE_BOOLEAN; + $limit = null; + } + switch ($type) { + case 'varchar': + $type = static::PHINX_TYPE_STRING; + if ($limit === 255) { + $limit = null; + } + break; + case 'char': + $type = static::PHINX_TYPE_CHAR; + if ($limit === 255) { + $limit = null; + } + if ($limit === 36) { + $type = static::PHINX_TYPE_UUID; + } + break; + case 'tinyint': + $type = static::PHINX_TYPE_INTEGER; + $limit = static::INT_TINY; + break; + case 'smallint': + $type = static::PHINX_TYPE_INTEGER; + $limit = static::INT_SMALL; + break; + case 'mediumint': + $type = static::PHINX_TYPE_INTEGER; + $limit = static::INT_MEDIUM; + break; + case 'int': + $type = static::PHINX_TYPE_INTEGER; + if ($limit === 11) { + $limit = null; + } + break; + case 'bigint': + if ($limit === 20) { + $limit = null; + } + $type = static::PHINX_TYPE_BIG_INTEGER; + break; + case 'blob': + $type = static::PHINX_TYPE_BINARY; + break; + case 'tinyblob': + $type = static::PHINX_TYPE_BINARY; + $limit = static::BLOB_TINY; + break; + case 'mediumblob': + $type = static::PHINX_TYPE_BINARY; + $limit = static::BLOB_MEDIUM; + break; + case 'longblob': + $type = static::PHINX_TYPE_BINARY; + $limit = static::BLOB_LONG; + break; + case 'tinytext': + $type = static::PHINX_TYPE_TEXT; + $limit = static::TEXT_TINY; + break; + case 'mediumtext': + $type = static::PHINX_TYPE_TEXT; + $limit = static::TEXT_MEDIUM; + break; + case 'longtext': + $type = static::PHINX_TYPE_TEXT; + $limit = static::TEXT_LONG; + break; + } + + $this->getSqlType($type, $limit); + + return array( + 'name' => $type, + 'limit' => $limit, + 'precision' => $precision + ); + } + } + + /** + * {@inheritdoc} + */ + public function createDatabase($name, $options = array()) + { + $this->startCommandTimer(); + $this->writeCommand('createDatabase', array($name)); + $charset = isset($options['charset']) ? $options['charset'] : 'utf8'; + + if (isset($options['collation'])) { + $this->execute(sprintf('CREATE DATABASE `%s` DEFAULT CHARACTER SET `%s` COLLATE `%s`', $name, $charset, $options['collation'])); + } else { + $this->execute(sprintf('CREATE DATABASE `%s` DEFAULT CHARACTER SET `%s`', $name, $charset)); + } + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function hasDatabase($name) + { + $rows = $this->fetchAll( + sprintf( + 'SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = \'%s\'', + $name + ) + ); + + foreach ($rows as $row) { + if (!empty($row)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function dropDatabase($name) + { + $this->startCommandTimer(); + $this->writeCommand('dropDatabase', array($name)); + $this->execute(sprintf('DROP DATABASE IF EXISTS `%s`', $name)); + $this->endCommandTimer(); + } + + /** + * Gets the MySQL Column Definition for a Column object. + * + * @param Column $column Column + * @return string + */ + protected function getColumnSqlDefinition(Column $column) + { + $sqlType = $this->getSqlType($column->getType(), $column->getLimit()); + + $def = ''; + $def .= strtoupper($sqlType['name']); + if ($column->getPrecision() && $column->getScale()) { + $def .= '(' . $column->getPrecision() . ',' . $column->getScale() . ')'; + } elseif (isset($sqlType['limit'])) { + $def .= '(' . $sqlType['limit'] . ')'; + } + if (($values = $column->getValues()) && is_array($values)) { + $def .= "('" . implode("', '", $values) . "')"; + } + $def .= (!$column->isSigned() && isset($this->signedColumnTypes[$column->getType()])) ? ' unsigned' : '' ; + $def .= ($column->isNull() == false) ? ' NOT NULL' : ' NULL'; + $def .= ($column->isIdentity()) ? ' AUTO_INCREMENT' : ''; + $def .= $this->getDefaultValueDefinition($column->getDefault()); + + if ($column->getComment()) { + $def .= ' COMMENT ' . $this->getConnection()->quote($column->getComment()); + } + + if ($column->getUpdate()) { + $def .= ' ON UPDATE ' . $column->getUpdate(); + } + + return $def; + } + + /** + * Gets the MySQL Index Definition for an Index object. + * + * @param Index $index Index + * @return string + */ + protected function getIndexSqlDefinition(Index $index) + { + $def = ''; + $limit = ''; + if ($index->getLimit()) { + $limit = '(' . $index->getLimit() . ')'; + } + + if ($index->getType() == Index::UNIQUE) { + $def .= ' UNIQUE'; + } + + if ($index->getType() == Index::FULLTEXT) { + $def .= ' FULLTEXT'; + } + + $def .= ' KEY'; + + if (is_string($index->getName())) { + $def .= ' `' . $index->getName() . '`'; + } + + $def .= ' (`' . implode('`,`', $index->getColumns()) . '`' . $limit . ')'; + + return $def; + } + + /** + * Gets the MySQL Foreign Key Definition for an ForeignKey object. + * + * @param ForeignKey $foreignKey + * @return string + */ + protected function getForeignKeySqlDefinition(ForeignKey $foreignKey) + { + $def = ''; + if ($foreignKey->getConstraint()) { + $def .= ' CONSTRAINT ' . $this->quoteColumnName($foreignKey->getConstraint()); + } + $columnNames = array(); + foreach ($foreignKey->getColumns() as $column) { + $columnNames[] = $this->quoteColumnName($column); + } + $def .= ' FOREIGN KEY (' . implode(',', $columnNames) . ')'; + $refColumnNames = array(); + foreach ($foreignKey->getReferencedColumns() as $column) { + $refColumnNames[] = $this->quoteColumnName($column); + } + $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()->getName()) . ' (' . implode(',', $refColumnNames) . ')'; + if ($foreignKey->getOnDelete()) { + $def .= ' ON DELETE ' . $foreignKey->getOnDelete(); + } + if ($foreignKey->getOnUpdate()) { + $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate(); + } + return $def; + } + + /** + * Describes a database table. This is a MySQL adapter specific method. + * + * @param string $tableName Table name + * @return array + */ + public function describeTable($tableName) + { + $options = $this->getOptions(); + + // mysql specific + $sql = sprintf( + "SELECT * + FROM information_schema.tables + WHERE table_schema = '%s' + AND table_name = '%s'", + $options['name'], + $tableName + ); + + return $this->fetchRow($sql); + } + + /** + * Returns MySQL column types (inherited and MySQL specified). + * @return array + */ + public function getColumnTypes() + { + return array_merge(parent::getColumnTypes(), array ('enum', 'set', 'year', 'json')); + } +} diff --git a/src/DDLWrapper/Db/Adapter/PdoAdapter.php b/src/DDLWrapper/Db/Adapter/PdoAdapter.php new file mode 100644 index 0000000..2d4c1ad --- /dev/null +++ b/src/DDLWrapper/Db/Adapter/PdoAdapter.php @@ -0,0 +1,302 @@ + + */ +abstract class PdoAdapter implements AdapterInterface +{ + + /** + * @var \PDO + */ + protected $connection; + + /** + * @var float + */ + protected $commandStartTime; + + /** + * Class Constructor. + * + * @param array $options Options + * @param InputInterface $input Input Interface + * @param OutputInterface $output Output Interface + */ + public function __construct() + { + + } + + + + /** + * Sets the database connection. + * + * @param \PDO $connection Connection + * @return AdapterInterface + */ + public function setConnection(\PDO $connection) + { + $this->connection = $connection; + + return $this; + } + + /** + * Gets the database connection + * + * @return \PDO + */ + public function getConnection() + { + if (null === $this->connection) { + $this->connect(); + } + return $this->connection; + } + + /** + * Sets the command start time + * + * @param int $time + * @return AdapterInterface + */ + public function setCommandStartTime($time) + { + $this->commandStartTime = $time; + return $this; + } + + /** + * Gets the command start time + * + * @return int + */ + public function getCommandStartTime() + { + return $this->commandStartTime; + } + + /** + * Start timing a command. + * + * @return void + */ + public function startCommandTimer() + { + $this->setCommandStartTime(microtime(true)); + } + + /** + * Stop timing the current command and write the elapsed time to the + * output. + * + * @return void + */ + public function endCommandTimer() + { + return; + $end = microtime(true); + if (OutputInterface::VERBOSITY_VERBOSE <= $this->getOutput()->getVerbosity()) { + $this->getOutput()->writeln(' -> ' . sprintf('%.4fs', $end - $this->getCommandStartTime())); + } + } + + /** + * Write a Phinx command to the output. + * + * @param string $command Command Name + * @param array $args Command Args + * @return void + */ + public function writeCommand($command, $args = array()) + { + return ''; + if (OutputInterface::VERBOSITY_VERBOSE <= $this->getOutput()->getVerbosity()) { + if (count($args)) { + $outArr = array(); + foreach ($args as $arg) { + if (is_array($arg)) { + $arg = array_map(function ($value) { + return '\'' . $value . '\''; + }, $arg); + $outArr[] = '[' . implode(', ', $arg) . ']'; + continue; + } + + $outArr[] = '\'' . $arg . '\''; + } + $this->getOutput()->writeln(' -- ' . $command . '(' . implode(', ', $outArr) . ')'); + return; + } + $this->getOutput()->writeln(' -- ' . $command); + } + } + + /** + * {@inheritdoc} + */ + public function connect() + { + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + } + + /** + * {@inheritdoc} + */ + public function execute($sql) + { + echo $sql; + return $this->getConnection()->exec($sql); + } + + /** + * {@inheritdoc} + */ + public function query($sql) + { + return $this->getConnection()->query($sql); + } + + /** + * {@inheritdoc} + */ + public function fetchRow($sql) + { + $result = $this->query($sql); + return $result->fetch(); + } + + /** + * {@inheritdoc} + */ + public function fetchAll($sql) + { + $rows = array(); + $result = $this->query($sql); + while ($row = $result->fetch()) { + $rows[] = $row; + } + return $rows; + } + + /** + * {@inheritdoc} + */ + public function insert(Table $table, $row) + { + $this->startCommandTimer(); + //$this->writeCommand('insert', array($table->getName())); + + $sql = sprintf( + "INSERT INTO %s ", + $this->quoteTableName($table->getName()) + ); + + $columns = array_keys($row); + $sql .= "(". implode(', ', array_map(array($this, 'quoteColumnName'), $columns)) . ")"; + $sql .= " VALUES (" . implode(', ', array_fill(0, count($columns), '?')) . ")"; + + $stmt = $this->getConnection()->prepare($sql); + $stmt->execute(array_values($row)); + $this->endCommandTimer(); + } + + + /** + * {@inheritdoc} + */ + public function getAdapterType() + { + return $this->getOption('adapter'); + } + + /** + * {@inheritdoc} + */ + public function getColumnTypes() + { + return array( + 'string', + 'char', + 'text', + 'integer', + 'biginteger', + 'float', + 'decimal', + 'datetime', + 'timestamp', + 'time', + 'date', + 'blob', + 'binary', + 'varbinary', + 'boolean', + 'uuid', + // Geospatial data types + 'geometry', + 'point', + 'linestring', + 'polygon', + ); + } + + /** + * {@inheritdoc} + */ + public function isValidColumnType(Column $column) { + return in_array($column->getType(), $this->getColumnTypes()); + } + + /** + * Cast a value to a boolean appropriate for the adapter. + * + * @param mixed $value The value to be cast + * + * @return mixed + */ + public function castToBool($value) + { + return (bool) $value ? 1 : 0; + } +} diff --git a/src/DDLWrapper/Db/Adapter/PostgresAdapter.php b/src/DDLWrapper/Db/Adapter/PostgresAdapter.php new file mode 100644 index 0000000..6881948 --- /dev/null +++ b/src/DDLWrapper/Db/Adapter/PostgresAdapter.php @@ -0,0 +1,1184 @@ +connection) { + if (!class_exists('PDO') || !in_array('pgsql', \PDO::getAvailableDrivers(), true)) { + // @codeCoverageIgnoreStart + throw new \RuntimeException('You need to enable the PDO_Pgsql extension for Phinx to run properly.'); + // @codeCoverageIgnoreEnd + } + + $db = null; + $options = $this->getOptions(); + + // if port is specified use it, otherwise use the PostgreSQL default + if (isset($options['port'])) { + $dsn = 'pgsql:host=' . $options['host'] . ';port=' . $options['port'] . ';dbname=' . $options['name']; + } else { + $dsn = 'pgsql:host=' . $options['host'] . ';dbname=' . $options['name']; + } + + try { + $db = new \PDO($dsn, $options['user'], $options['pass'], array(\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION)); + } catch (\PDOException $exception) { + throw new \InvalidArgumentException(sprintf( + 'There was a problem connecting to the database: %s', + $exception->getMessage() + )); + } + + $this->setConnection($db); + } + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + $this->connection = null; + } + + /** + * {@inheritdoc} + */ + public function hasTransactions() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function beginTransaction() + { + $this->execute('BEGIN'); + } + + /** + * {@inheritdoc} + */ + public function commitTransaction() + { + $this->execute('COMMIT'); + } + + /** + * {@inheritdoc} + */ + public function rollbackTransaction() + { + $this->execute('ROLLBACK'); + } + + /** + * Quotes a schema name for use in a query. + * + * @param string $schemaName Schema Name + * @return string + */ + public function quoteSchemaName($schemaName) + { + return $this->quoteColumnName($schemaName); + } + + /** + * {@inheritdoc} + */ + public function quoteTableName($tableName) + { + return $this->quoteSchemaName($this->getSchemaName()) . '.' . $this->quoteColumnName($tableName); + } + + /** + * {@inheritdoc} + */ + public function quoteColumnName($columnName) + { + return '"'. $columnName . '"'; + } + + /** + * {@inheritdoc} + */ + public function hasTable($tableName) + { + $result = $this->getConnection()->query( + sprintf( + 'SELECT * + FROM information_schema.tables + WHERE table_schema = %s + AND lower(table_name) = lower(%s)', + $this->getConnection()->quote($this->getSchemaName()), + $this->getConnection()->quote($tableName) + ) + ); + + return $result->rowCount() === 1; + } + + /** + * {@inheritdoc} + */ + public function createTable(Table $table) + { + $this->startCommandTimer(); + $options = $table->getOptions(); + + // Add the default primary key + $columns = $table->getPendingColumns(); + if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) { + $column = new Column(); + $column->setName('id') + ->setType('integer') + ->setIdentity(true); + + array_unshift($columns, $column); + $options['primary_key'] = 'id'; + + } elseif (isset($options['id']) && is_string($options['id'])) { + // Handle id => "field_name" to support AUTO_INCREMENT + $column = new Column(); + $column->setName($options['id']) + ->setType('integer') + ->setIdentity(true); + + array_unshift($columns, $column); + $options['primary_key'] = $options['id']; + } + + // TODO - process table options like collation etc + $sql = 'CREATE TABLE '; + $sql .= $this->quoteTableName($table->getName()) . ' ('; + + $this->columnsWithComments = array(); + foreach ($columns as $column) { + $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', '; + + // set column comments, if needed + if ($column->getComment()) { + $this->columnsWithComments[] = $column; + } + } + + // set the primary key(s) + if (isset($options['primary_key'])) { + $sql = rtrim($sql); + $sql .= sprintf(' CONSTRAINT %s_pkey PRIMARY KEY (', $table->getName()); + if (is_string($options['primary_key'])) { // handle primary_key => 'id' + $sql .= $this->quoteColumnName($options['primary_key']); + } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id') + // PHP 5.4 will allow access of $this, so we can call quoteColumnName() directly in the anonymous function, + // but for now just hard-code the adapter quotes + $sql .= implode( + ',', + array_map( + function ($v) { + return '"' . $v . '"'; + }, + $options['primary_key'] + ) + ); + } + $sql .= ')'; + } else { + $sql = substr(rtrim($sql), 0, -1); // no primary keys + } + + // set the foreign keys + $foreignKeys = $table->getForeignKeys(); + if (!empty($foreignKeys)) { + foreach ($foreignKeys as $foreignKey) { + $sql .= ', ' . $this->getForeignKeySqlDefinition($foreignKey, $table->getName()); + } + } + + $sql .= ');'; + + // process column comments + if (!empty($this->columnsWithComments)) { + foreach ($this->columnsWithComments as $column) { + $sql .= $this->getColumnCommentSqlDefinition($column, $table->getName()); + } + } + + + // set the indexes + $indexes = $table->getIndexes(); + if (!empty($indexes)) { + foreach ($indexes as $index) { + $sql .= $this->getIndexSqlDefinition($index, $table->getName()); + } + } + + // execute the sql + $this->writeCommand('createTable', array($table->getName())); + $this->execute($sql); + + // process table comments + if (isset($options['comment'])) { + $sql = sprintf( + 'COMMENT ON TABLE %s IS %s', + $this->quoteTableName($table->getName()), + $this->getConnection()->quote($options['comment']) + ); + $this->execute($sql); + } + + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function renameTable($tableName, $newTableName) + { + $this->startCommandTimer(); + $this->writeCommand('renameTable', array($tableName, $newTableName)); + $sql = sprintf( + 'ALTER TABLE %s RENAME TO %s', + $this->quoteTableName($tableName), + $this->quoteColumnName($newTableName) + ); + $this->execute($sql); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function dropTable($tableName) + { + $this->startCommandTimer(); + $this->writeCommand('dropTable', array($tableName)); + $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tableName))); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function getColumns($tableName) + { + $columns = array(); + $sql = sprintf( + "SELECT column_name, data_type, is_identity, is_nullable, + column_default, character_maximum_length, numeric_precision, numeric_scale + FROM information_schema.columns + WHERE table_name ='%s'", + $tableName + ); + $columnsInfo = $this->fetchAll($sql); + + foreach ($columnsInfo as $columnInfo) { + $column = new Column(); + $column->setName($columnInfo['column_name']) + ->setType($this->getPhinxType($columnInfo['data_type'])) + ->setNull($columnInfo['is_nullable'] === 'YES') + ->setDefault($columnInfo['column_default']) + ->setIdentity($columnInfo['is_identity'] === 'YES') + ->setPrecision($columnInfo['numeric_precision']) + ->setScale($columnInfo['numeric_scale']); + + if (preg_match('/\bwith time zone$/', $columnInfo['data_type'])) { + $column->setTimezone(true); + } + + if (isset($columnInfo['character_maximum_length'])) { + $column->setLimit($columnInfo['character_maximum_length']); + } + $columns[] = $column; + } + return $columns; + } + + /** + * {@inheritdoc} + */ + public function hasColumn($tableName, $columnName, $options = array()) + { + $sql = sprintf("SELECT count(*) + FROM information_schema.columns + WHERE table_schema = '%s' AND table_name = '%s' AND column_name = '%s'", + $this->getSchemaName(), + $tableName, + $columnName + ); + + $result = $this->fetchRow($sql); + return $result['count'] > 0; + } + + /** + * {@inheritdoc} + */ + public function addColumn(Table $table, Column $column) + { + $this->startCommandTimer(); + $this->writeCommand('addColumn', array($table->getName(), $column->getName(), $column->getType())); + $sql = sprintf( + 'ALTER TABLE %s ADD %s %s', + $this->quoteTableName($table->getName()), + $this->quoteColumnName($column->getName()), + $this->getColumnSqlDefinition($column) + ); + + $this->execute($sql); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function renameColumn($tableName, $columnName, $newColumnName) + { + $this->startCommandTimer(); + $sql = sprintf( + "SELECT CASE WHEN COUNT(*) > 0 THEN 1 ELSE 0 END AS column_exists + FROM information_schema.columns + WHERE table_name ='%s' AND column_name = '%s'", + $tableName, + $columnName + ); + $result = $this->fetchRow($sql); + if (!(bool) $result['column_exists']) { + throw new \InvalidArgumentException("The specified column does not exist: $columnName"); + } + $this->writeCommand('renameColumn', array($tableName, $columnName, $newColumnName)); + $this->execute( + sprintf( + 'ALTER TABLE %s RENAME COLUMN %s TO %s', + $this->quoteTableName($tableName), + $this->quoteColumnName($columnName), + $newColumnName + ) + ); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function changeColumn($tableName, $columnName, Column $newColumn) + { + // TODO - is it possible to merge these 3 queries into less? + $this->startCommandTimer(); + $this->writeCommand('changeColumn', array($tableName, $columnName, $newColumn->getType())); + // change data type + $sql = sprintf( + 'ALTER TABLE %s ALTER COLUMN %s TYPE %s', + $this->quoteTableName($tableName), + $this->quoteColumnName($columnName), + $this->getColumnSqlDefinition($newColumn) + ); + //NULL and DEFAULT cannot be set while changing column type + $sql = preg_replace('/ NOT NULL/', '', $sql); + $sql = preg_replace('/ NULL/', '', $sql); + //If it is set, DEFAULT is the last definition + $sql = preg_replace('/DEFAULT .*/', '', $sql); + $this->execute($sql); + // process null + $sql = sprintf( + 'ALTER TABLE %s ALTER COLUMN %s', + $this->quoteTableName($tableName), + $this->quoteColumnName($columnName) + ); + if ($newColumn->isNull()) { + $sql .= ' DROP NOT NULL'; + } else { + $sql .= ' SET NOT NULL'; + } + $this->execute($sql); + if (!is_null($newColumn->getDefault())) { + //change default + $this->execute( + sprintf( + 'ALTER TABLE %s ALTER COLUMN %s SET %s', + $this->quoteTableName($tableName), + $this->quoteColumnName($columnName), + $this->getDefaultValueDefinition($newColumn->getDefault()) + ) + ); + } + else { + //drop default + $this->execute( + sprintf( + 'ALTER TABLE %s ALTER COLUMN %s DROP DEFAULT', + $this->quoteTableName($tableName), + $this->quoteColumnName($columnName) + ) + ); + } + // rename column + if ($columnName !== $newColumn->getName()) { + $this->execute( + sprintf( + 'ALTER TABLE %s RENAME COLUMN %s TO %s', + $this->quoteTableName($tableName), + $this->quoteColumnName($columnName), + $this->quoteColumnName($newColumn->getName()) + ) + ); + } + + // change column comment if needed + if ($newColumn->getComment()) { + $sql = $this->getColumnCommentSqlDefinition($newColumn, $tableName); + $this->execute($sql); + } + + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function dropColumn($tableName, $columnName) + { + $this->startCommandTimer(); + $this->writeCommand('dropColumn', array($tableName, $columnName)); + $this->execute( + sprintf( + 'ALTER TABLE %s DROP COLUMN %s', + $this->quoteTableName($tableName), + $this->quoteColumnName($columnName) + ) + ); + $this->endCommandTimer(); + } + + /** + * Get an array of indexes from a particular table. + * + * @param string $tableName Table Name + * @return array + */ + protected function getIndexes($tableName) + { + $indexes = array(); + $sql = "SELECT + i.relname AS index_name, + a.attname AS column_name + FROM + pg_class t, + pg_class i, + pg_index ix, + pg_attribute a + WHERE + t.oid = ix.indrelid + AND i.oid = ix.indexrelid + AND a.attrelid = t.oid + AND a.attnum = ANY(ix.indkey) + AND t.relkind = 'r' + AND t.relname = '$tableName' + ORDER BY + t.relname, + i.relname;"; + $rows = $this->fetchAll($sql); + foreach ($rows as $row) { + if (!isset($indexes[$row['index_name']])) { + $indexes[$row['index_name']] = array('columns' => array()); + } + $indexes[$row['index_name']]['columns'][] = strtolower($row['column_name']); + } + return $indexes; + } + + /** + * {@inheritdoc} + */ + public function hasIndex($tableName, $columns) + { + if (is_string($columns)) { + $columns = array($columns); + } + $columns = array_map('strtolower', $columns); + $indexes = $this->getIndexes($tableName); + foreach ($indexes as $index) { + if (array_diff($index['columns'], $columns) === array_diff($columns, $index['columns'])) { + return true; + } + } + return false; + } + + /** + * {@inheritdoc} + */ + public function hasIndexByName($tableName, $indexName) + { + $indexes = $this->getIndexes($tableName); + foreach ($indexes as $name => $index) { + if ($name === $indexName) { + return true; + } + } + return false; + } + + /** + * {@inheritdoc} + */ + public function addIndex(Table $table, Index $index) + { + $this->startCommandTimer(); + $this->writeCommand('addIndex', array($table->getName(), $index->getColumns())); + $sql = $this->getIndexSqlDefinition($index, $table->getName()); + $this->execute($sql); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function dropIndex($tableName, $columns) + { + $this->startCommandTimer(); + if (is_string($columns)) { + $columns = array($columns); // str to array + } + + $this->writeCommand('dropIndex', array($tableName, $columns)); + $indexes = $this->getIndexes($tableName); + $columns = array_map('strtolower', $columns); + + foreach ($indexes as $indexName => $index) { + $a = array_diff($columns, $index['columns']); + if (empty($a)) { + $this->execute( + sprintf( + 'DROP INDEX IF EXISTS %s', + $this->quoteColumnName($indexName) + ) + ); + $this->endCommandTimer(); + return; + } + } + } + + /** + * {@inheritdoc} + */ + public function dropIndexByName($tableName, $indexName) + { + $this->startCommandTimer(); + $this->writeCommand('dropIndexByName', array($tableName, $indexName)); + $sql = sprintf( + 'DROP INDEX IF EXISTS %s', + $indexName + ); + $this->execute($sql); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function hasForeignKey($tableName, $columns, $constraint = null) + { + if (is_string($columns)) { + $columns = array($columns); // str to array + } + $foreignKeys = $this->getForeignKeys($tableName); + if ($constraint) { + if (isset($foreignKeys[$constraint])) { + return !empty($foreignKeys[$constraint]); + } + return false; + } else { + foreach ($foreignKeys as $key) { + $a = array_diff($columns, $key['columns']); + if (empty($a)) { + return true; + } + } + return false; + } + } + + /** + * Get an array of foreign keys from a particular table. + * + * @param string $tableName Table Name + * @return array + */ + protected function getForeignKeys($tableName) + { + $foreignKeys = array(); + $rows = $this->fetchAll(sprintf( + "SELECT + tc.constraint_name, + tc.table_name, kcu.column_name, + ccu.table_name AS referenced_table_name, + ccu.column_name AS referenced_column_name + FROM + information_schema.table_constraints AS tc + JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name + JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name + WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s' + ORDER BY kcu.position_in_unique_constraint", + $tableName + )); + foreach ($rows as $row) { + $foreignKeys[$row['constraint_name']]['table'] = $row['table_name']; + $foreignKeys[$row['constraint_name']]['columns'][] = $row['column_name']; + $foreignKeys[$row['constraint_name']]['referenced_table'] = $row['referenced_table_name']; + $foreignKeys[$row['constraint_name']]['referenced_columns'][] = $row['referenced_column_name']; + } + return $foreignKeys; + } + + /** + * {@inheritdoc} + */ + public function addForeignKey(Table $table, ForeignKey $foreignKey) + { + $this->startCommandTimer(); + $this->writeCommand('addForeignKey', array($table->getName(), $foreignKey->getColumns())); + $sql = sprintf( + 'ALTER TABLE %s ADD %s', + $this->quoteTableName($table->getName()), + $this->getForeignKeySqlDefinition($foreignKey, $table->getName()) + ); + $this->execute($sql); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function dropForeignKey($tableName, $columns, $constraint = null) + { + $this->startCommandTimer(); + if (is_string($columns)) { + $columns = array($columns); // str to array + } + $this->writeCommand('dropForeignKey', array($tableName, $columns)); + + if ($constraint) { + $this->execute( + sprintf( + 'ALTER TABLE %s DROP CONSTRAINT %s', + $this->quoteTableName($tableName), + $constraint + ) + ); + } else { + foreach ($columns as $column) { + $rows = $this->fetchAll(sprintf( + "SELECT CONSTRAINT_NAME + FROM information_schema.KEY_COLUMN_USAGE + WHERE TABLE_SCHEMA = CURRENT_SCHEMA() + AND TABLE_NAME IS NOT NULL + AND TABLE_NAME = '%s' + AND COLUMN_NAME = '%s' + ORDER BY POSITION_IN_UNIQUE_CONSTRAINT", + $tableName, + $column + )); + + foreach ($rows as $row) { + $this->dropForeignKey($tableName, $columns, $row['constraint_name']); + } + } + } + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function getSqlType($type, $limit = null) + { + switch ($type) { + case static::PHINX_TYPE_INTEGER: + if ($limit && $limit == static::INT_SMALL) { + return array( + 'name' => 'smallint', + 'limit' => static::INT_SMALL + ); + } + return array('name' => $type); + case static::PHINX_TYPE_TEXT: + case static::PHINX_TYPE_TIME: + case static::PHINX_TYPE_DATE: + case static::PHINX_TYPE_BOOLEAN: + case static::PHINX_TYPE_JSON: + case static::PHINX_TYPE_JSONB: + case static::PHINX_TYPE_UUID: + return array('name' => $type); + case static::PHINX_TYPE_DECIMAL: + return array('name' => $type, 'precision' => 18, 'scale' => 0); + case static::PHINX_TYPE_STRING: + return array('name' => 'character varying', 'limit' => 255); + case static::PHINX_TYPE_CHAR: + return array('name' => 'character', 'limit' => 255); + case static::PHINX_TYPE_BIG_INTEGER: + return array('name' => 'bigint'); + case static::PHINX_TYPE_FLOAT: + return array('name' => 'real'); + case static::PHINX_TYPE_DATETIME: + case static::PHINX_TYPE_TIMESTAMP: + return array('name' => 'timestamp'); + case static::PHINX_TYPE_BLOB: + case static::PHINX_TYPE_BINARY: + return array('name' => 'bytea'); + // Geospatial database types + // Spatial storage in Postgres is done via the PostGIS extension, + // which enables the use of the "geography" type in combination + // with SRID 4326. + case static::PHINX_TYPE_GEOMETRY: + return array('name' => 'geography', 'geometry', 4326); + break; + case static::PHINX_TYPE_POINT: + return array('name' => 'geography', 'point', 4326); + break; + case static::PHINX_TYPE_LINESTRING: + return array('name' => 'geography', 'linestring', 4326); + break; + case static::PHINX_TYPE_POLYGON: + return array('name' => 'geography', 'polygon', 4326); + break; + default: + if ($this->isArrayType($type)) { + return array('name' => $type); + } + // Return array type + throw new \RuntimeException('The type: "' . $type . '" is not supported'); + } + } + + /** + * Returns Phinx type by SQL type + * + * @param string $sqlType SQL type + * @returns string Phinx type + */ + public function getPhinxType($sqlType) + { + switch ($sqlType) { + case 'character varying': + case 'varchar': + return static::PHINX_TYPE_STRING; + case 'character': + case 'char': + return static::PHINX_TYPE_CHAR; + case 'text': + return static::PHINX_TYPE_TEXT; + case 'json': + return static::PHINX_TYPE_JSON; + case 'jsonb': + return static::PHINX_TYPE_JSONB; + case 'smallint': + return array( + 'name' => 'smallint', + 'limit' => static::INT_SMALL + ); + case 'int': + case 'int4': + case 'integer': + return static::PHINX_TYPE_INTEGER; + case 'decimal': + case 'numeric': + return static::PHINX_TYPE_DECIMAL; + case 'bigint': + case 'int8': + return static::PHINX_TYPE_BIG_INTEGER; + case 'real': + case 'float4': + return static::PHINX_TYPE_FLOAT; + case 'bytea': + return static::PHINX_TYPE_BINARY; + break; + case 'time': + case 'timetz': + case 'time with time zone': + case 'time without time zone': + return static::PHINX_TYPE_TIME; + case 'date': + return static::PHINX_TYPE_DATE; + case 'timestamp': + case 'timestamptz': + case 'timestamp with time zone': + case 'timestamp without time zone': + return static::PHINX_TYPE_DATETIME; + case 'bool': + case 'boolean': + return static::PHINX_TYPE_BOOLEAN; + case 'uuid': + return static::PHINX_TYPE_UUID; + default: + throw new \RuntimeException('The PostgreSQL type: "' . $sqlType . '" is not supported'); + } + } + + /** + * {@inheritdoc} + */ + public function createDatabase($name, $options = array()) + { + $this->startCommandTimer(); + $this->writeCommand('createDatabase', array($name)); + $charset = isset($options['charset']) ? $options['charset'] : 'utf8'; + $this->execute(sprintf("CREATE DATABASE %s WITH ENCODING = '%s'", $name, $charset)); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function hasDatabase($databaseName) + { + $sql = sprintf("SELECT count(*) FROM pg_database WHERE datname = '%s'", $databaseName); + $result = $this->fetchRow($sql); + return $result['count'] > 0; + } + + /** + * {@inheritdoc} + */ + public function dropDatabase($name) + { + $this->startCommandTimer(); + $this->writeCommand('dropDatabase', array($name)); + $this->disconnect(); + $this->execute(sprintf('DROP DATABASE IF EXISTS %s', $name)); + $this->connect(); + $this->endCommandTimer(); + } + + /** + * Get the defintion for a `DEFAULT` statement. + * + * @param mixed $default + * @return string + */ + protected function getDefaultValueDefinition($default) + { + if (is_string($default) && 'CURRENT_TIMESTAMP' !== $default) { + $default = $this->getConnection()->quote($default); + } elseif (is_bool($default)) { + $default = $this->castToBool($default); + } + return isset($default) ? 'DEFAULT ' . $default : ''; + } + + /** + * Gets the PostgreSQL Column Definition for a Column object. + * + * @param Column $column Column + * @return string + */ + protected function getColumnSqlDefinition(Column $column) + { + $buffer = array(); + if ($column->isIdentity()) { + $buffer[] = $column->getType() == 'biginteger' ? 'BIGSERIAL' : 'SERIAL'; + } else { + $sqlType = $this->getSqlType($column->getType(), $column->getLimit()); + $buffer[] = strtoupper($sqlType['name']); + // integers cant have limits in postgres + if (static::PHINX_TYPE_DECIMAL === $sqlType['name'] && ($column->getPrecision() || $column->getScale())) { + $buffer[] = sprintf( + '(%s, %s)', + $column->getPrecision() ? $column->getPrecision() : $sqlType['precision'], + $column->getScale() ? $column->getScale() : $sqlType['scale'] + ); + } elseif (!in_array($sqlType['name'], array('integer', 'smallint'))) { + if ($column->getLimit() || isset($sqlType['limit'])) { + $buffer[] = sprintf('(%s)', $column->getLimit() ? $column->getLimit() : $sqlType['limit']); + } + } + + $timeTypes = array( + 'time', + 'timestamp', + ); + if (in_array($sqlType['name'], $timeTypes) && $column->isTimezone()) { + $buffer[] = strtoupper('with time zone'); + } + } + + $buffer[] = $column->isNull() ? 'NULL' : 'NOT NULL'; + + if (!is_null($column->getDefault())) { + $buffer[] = $this->getDefaultValueDefinition($column->getDefault()); + } + + return implode(' ', $buffer); + } + + /** + * Gets the PostgreSQL Column Comment Defininition for a column object. + * + * @param Column $column Column + * @param string $tableName Table name + * @return string + */ + protected function getColumnCommentSqlDefinition(Column $column, $tableName) + { + // passing 'null' is to remove column comment + $comment = (strcasecmp($column->getComment(), 'NULL') !== 0) + ? $this->getConnection()->quote($column->getComment()) + : 'NULL'; + + return sprintf( + 'COMMENT ON COLUMN %s.%s IS %s;', + $tableName, + $column->getName(), + $comment + ); + } + + /** + * Gets the PostgreSQL Index Definition for an Index object. + * + * @param Index $index Index + * @param string $tableName Table name + * @return string + */ + protected function getIndexSqlDefinition(Index $index, $tableName) + { + if (is_string($index->getName())) { + $indexName = $index->getName(); + } else { + $columnNames = $index->getColumns(); + if (is_string($columnNames)) { + $columnNames = array($columnNames); + } + $indexName = sprintf('%s_%s', $tableName, implode('_', $columnNames)); + } + $def = sprintf( + "CREATE %s INDEX %s ON %s (%s);", + ($index->getType() === Index::UNIQUE ? 'UNIQUE' : ''), + $indexName, + $this->quoteTableName($tableName), + implode(',', $index->getColumns()) + ); + return $def; + } + + /** + * Gets the MySQL Foreign Key Definition for an ForeignKey object. + * + * @param ForeignKey $foreignKey + * @param string $tableName Table name + * @return string + */ + protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, $tableName) + { + $constraintName = $foreignKey->getConstraint() ?: $tableName . '_' . implode('_', $foreignKey->getColumns()); + $def = ' CONSTRAINT "' . $constraintName . '" FOREIGN KEY ("' . implode('", "', $foreignKey->getColumns()) . '")'; + $def .= " REFERENCES {$this->quoteTableName($foreignKey->getReferencedTable()->getName())} (\"" . implode('", "', $foreignKey->getReferencedColumns()) . '")'; + if ($foreignKey->getOnDelete()) { + $def .= " ON DELETE {$foreignKey->getOnDelete()}"; + } + if ($foreignKey->getOnUpdate()) { + $def .= " ON UPDATE {$foreignKey->getOnUpdate()}"; + } + return $def; + } + + /** + * {@inheritdoc} + */ + public function createSchemaTable() + { + // Create the public/custom schema if it doesn't already exist + if (false === $this->hasSchema($this->getSchemaName())) { + $this->createSchema($this->getSchemaName()); + } + + $this->fetchAll(sprintf('SET search_path TO %s', $this->getSchemaName())); + + return parent::createSchemaTable(); + } + + /** + * Creates the specified schema. + * + * @param string $schemaName Schema Name + * @return void + */ + public function createSchema($schemaName = 'public') + { + $this->startCommandTimer(); + $this->writeCommand('addSchema', array($schemaName)); + $sql = sprintf('CREATE SCHEMA %s;', $this->quoteSchemaName($schemaName)); // from postgres 9.3 we can use "CREATE SCHEMA IF NOT EXISTS schema_name" + $this->execute($sql); + $this->endCommandTimer(); + } + + /** + * Checks to see if a schema exists. + * + * @param string $schemaName Schema Name + * @return boolean + */ + public function hasSchema($schemaName) + { + $sql = sprintf( + "SELECT count(*) + FROM pg_namespace + WHERE nspname = '%s'", + $schemaName + ); + $result = $this->fetchRow($sql); + return $result['count'] > 0; + } + + /** + * Drops the specified schema table. + * + * @param string $schemaName Schema name + * @return void + */ + public function dropSchema($schemaName) + { + $this->startCommandTimer(); + $this->writeCommand('dropSchema', array($schemaName)); + $sql = sprintf("DROP SCHEMA IF EXISTS %s CASCADE;", $this->quoteSchemaName($schemaName)); + $this->execute($sql); + $this->endCommandTimer(); + } + + /** + * Drops all schemas. + * + * @return void + */ + public function dropAllSchemas() + { + $this->startCommandTimer(); + $this->writeCommand('dropAllSchemas'); + foreach ($this->getAllSchemas() as $schema) { + $this->dropSchema($schema); + } + $this->endCommandTimer(); + } + + /** + * Returns schemas. + * + * @return array + */ + public function getAllSchemas() + { + $sql = "SELECT schema_name + FROM information_schema.schemata + WHERE schema_name <> 'information_schema' AND schema_name !~ '^pg_'"; + $items = $this->fetchAll($sql); + $schemaNames = array(); + foreach ($items as $item) { + $schemaNames[] = $item['schema_name']; + } + return $schemaNames; + } + + /** + * {@inheritdoc} + */ + public function getColumnTypes() + { + return array_merge(parent::getColumnTypes(), array('json', 'jsonb')); + } + + /** + * {@inheritdoc} + */ + public function isValidColumnType(Column $column) + { + // If not a standard column type, maybe it is array type? + return (parent::isValidColumnType($column) || $this->isArrayType($column->getType())); + } + + /** + * Check if the given column is an array of a valid type. + * + * @param string $columnType + * @return bool + */ + protected function isArrayType($columnType) + { + if (!preg_match('/^([a-z]+)(?:\[\]){1,}$/', $columnType, $matches)) { + return false; + } + + $baseType = $matches[1]; + return in_array($baseType, $this->getColumnTypes()); + } + + /** + * Gets the schema name. + * + * @return string + */ + private function getSchemaName() + { + $options = $this->getOptions(); + return empty($options['schema']) ? 'public' : $options['schema']; + } + + /** + * Cast a value to a boolean appropriate for the adapter. + * + * @param mixed $value The value to be cast + * + * @return mixed + */ + public function castToBool($value) + { + return (bool) $value ? 'TRUE' : 'FALSE'; + } + +} diff --git a/src/DDLWrapper/Db/Adapter/ProxyAdapter.php b/src/DDLWrapper/Db/Adapter/ProxyAdapter.php new file mode 100644 index 0000000..5650441 --- /dev/null +++ b/src/DDLWrapper/Db/Adapter/ProxyAdapter.php @@ -0,0 +1,325 @@ + + */ +class ProxyAdapter extends AdapterWrapper +{ + /** + * @var array + */ + protected $commands; + + /** + * {@inheritdoc} + */ + public function getAdapterType() + { + return 'ProxyAdapter'; + } + /** + * {@inheritdoc} + */ + public function createTable(Table $table) + { + $this->recordCommand('createTable', array($table->getName())); + } + + /** + * {@inheritdoc} + */ + public function renameTable($tableName, $newTableName) + { + $this->recordCommand('renameTable', array($tableName, $newTableName)); + } + + /** + * {@inheritdoc} + */ + public function dropTable($tableName) + { + $this->recordCommand('dropTable', array($tableName)); + } + + /** + * {@inheritdoc} + */ + public function addColumn(Table $table, Column $column) + { + $this->recordCommand('addColumn', array($table, $column)); + } + + /** + * {@inheritdoc} + */ + public function renameColumn($tableName, $columnName, $newColumnName) + { + $this->recordCommand('renameColumn', array($tableName, $columnName, $newColumnName)); + } + + /** + * {@inheritdoc} + */ + public function changeColumn($tableName, $columnName, Column $newColumn) + { + $this->recordCommand('changeColumn', array($tableName, $columnName, $newColumn)); + } + + /** + * {@inheritdoc} + */ + public function dropColumn($tableName, $columnName) + { + $this->recordCommand('dropColumn', array($tableName, $columnName)); + } + + /** + * {@inheritdoc} + */ + public function addIndex(Table $table, Index $index) + { + $this->recordCommand('addIndex', array($table, $index)); + } + + /** + * {@inheritdoc} + */ + public function dropIndex($tableName, $columns, $options = array()) + { + $this->recordCommand('dropIndex', array($tableName, $columns, $options)); + } + + /** + * {@inheritdoc} + */ + public function dropIndexByName($tableName, $indexName) + { + $this->recordCommand('dropIndexByName', array($tableName, $indexName)); + } + + /** + * {@inheritdoc} + */ + public function addForeignKey(Table $table, ForeignKey $foreignKey) + { + $this->recordCommand('addForeignKey', array($table, $foreignKey)); + } + + /** + * {@inheritdoc} + */ + public function dropForeignKey($tableName, $columns, $constraint = null) + { + $this->recordCommand('dropForeignKey', array($columns, $constraint)); + } + + /** + * {@inheritdoc} + */ + public function createDatabase($name, $options = array()) + { + $this->recordCommand('createDatabase', array($name, $options)); + } + + /** + * Record a command for execution later. + * + * @param string $name Command Name + * @param array $arguments Command Arguments + * @return void + */ + public function recordCommand($name, $arguments) + { + $this->commands[] = array( + 'name' => $name, + 'arguments' => $arguments + ); + } + + /** + * Sets an array of recorded commands. + * + * @param array $commands Commands + * @return ProxyAdapter + */ + public function setCommands($commands) + { + $this->commands = $commands; + return $this; + } + + /** + * Gets an array of the recorded commands. + * + * @return array + */ + public function getCommands() + { + return $this->commands; + } + + /** + * Gets an array of the recorded commands in reverse. + * + * @throws IrreversibleMigrationException if a command cannot be reversed. + * @return array + */ + /*public function getInvertedCommands() + { + if (null === $this->getCommands()) { + return array(); + } + + $invCommands = array(); + $supportedCommands = array( + 'createTable', 'renameTable', 'addColumn', + 'renameColumn', 'addIndex', 'addForeignKey' + ); + foreach (array_reverse($this->getCommands()) as $command) { + if (!in_array($command['name'], $supportedCommands)) { + throw new IrreversibleMigrationException(sprintf( + 'Cannot reverse a "%s" command', + $command['name'] + )); + } + $invertMethod = 'invert' . ucfirst($command['name']); + $invertedCommand = $this->$invertMethod($command['arguments']); + $invCommands[] = array( + 'name' => $invertedCommand['name'], + 'arguments' => $invertedCommand['arguments'] + ); + } + + return $invCommands; + }*/ + + /** + * Execute the recorded commands. + * + * @return void + */ + public function executeCommands() + { + $commands = $this->getCommands(); + foreach ($commands as $command) { + call_user_func_array(array($this->getAdapter(), $command['name']), $command['arguments']); + } + } + + /** + * Execute the recorded commands in reverse. + * + * @return void + */ + /*public function executeInvertedCommands() + { + $commands = $this->getInvertedCommands(); + foreach ($commands as $command) { + call_user_func_array(array($this->getAdapter(), $command['name']), $command['arguments']); + } + }*/ + + /** + * Returns the reverse of a createTable command. + * + * @param array $args Method Arguments + * @return array + */ + public function invertCreateTable($args) + { + return array('name' => 'dropTable', 'arguments' => array($args[0])); + } + + /** + * Returns the reverse of a renameTable command. + * + * @param array $args Method Arguments + * @return array + */ + public function invertRenameTable($args) + { + return array('name' => 'renameTable', 'arguments' => array($args[1], $args[0])); + } + + /** + * Returns the reverse of a addColumn command. + * + * @param array $args Method Arguments + * @return array + */ + public function invertAddColumn($args) + { + return array('name' => 'dropColumn', 'arguments' => array($args[0]->getName(), $args[1]->getName())); + } + + /** + * Returns the reverse of a renameColumn command. + * + * @param array $args Method Arguments + * @return array + */ + public function invertRenameColumn($args) + { + return array('name' => 'renameColumn', 'arguments' => array($args[0], $args[2], $args[1])); + } + + /** + * Returns the reverse of a addIndex command. + * + * @param array $args Method Arguments + * @return array + */ + public function invertAddIndex($args) + { + return array('name' => 'dropIndex', 'arguments' => array($args[0]->getName(), $args[1]->getColumns())); + } + + /** + * Returns the reverse of a addForeignKey command. + * + * @param array $args Method Arguments + * @return array + */ + public function invertAddForeignKey($args) + { + return array('name' => 'dropForeignKey', 'arguments' => array($args[0]->getName(), $args[1]->getColumns())); + } +} diff --git a/src/DDLWrapper/Db/Adapter/SQLiteAdapter.php b/src/DDLWrapper/Db/Adapter/SQLiteAdapter.php new file mode 100644 index 0000000..17d7314 --- /dev/null +++ b/src/DDLWrapper/Db/Adapter/SQLiteAdapter.php @@ -0,0 +1,1143 @@ + + * @author Richard McIntyre + */ +class SQLiteAdapter extends PdoAdapter implements AdapterInterface +{ + protected $definitionsWithLimits = array( + 'CHARACTER', + 'VARCHAR', + 'VARYING CHARACTER', + 'NCHAR', + 'NATIVE CHARACTER', + 'NVARCHAR' + ); + + /** + * {@inheritdoc} + */ + public function connect() + { + if (null === $this->connection) { + if (!class_exists('PDO') || !in_array('sqlite', \PDO::getAvailableDrivers(), true)) { + // @codeCoverageIgnoreStart + throw new \RuntimeException('You need to enable the PDO_SQLITE extension for Phinx to run properly.'); + // @codeCoverageIgnoreEnd + } + + $db = null; + $options = $this->getOptions(); + + // if port is specified use it, otherwise use the MySQL default + if (isset($options['memory'])) { + $dsn = 'sqlite::memory:'; + } else { + $dsn = 'sqlite:' . $options['name']; + if (file_exists($options['name'] . '.sqlite3')) { + $dsn = 'sqlite:' . $options['name'] . '.sqlite3'; + } + } + + try { + $db = new \PDO($dsn); + } catch (\PDOException $exception) { + throw new \InvalidArgumentException(sprintf( + 'There was a problem connecting to the database: %s', + $exception->getMessage() + )); + } + + $this->setConnection($db); + } + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + $this->connection = null; + } + + /** + * {@inheritdoc} + */ + public function hasTransactions() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function beginTransaction() + { + $this->execute('BEGIN TRANSACTION'); + } + + /** + * {@inheritdoc} + */ + public function commitTransaction() + { + $this->execute('COMMIT'); + } + + /** + * {@inheritdoc} + */ + public function rollbackTransaction() + { + $this->execute('ROLLBACK'); + } + + /** + * {@inheritdoc} + */ + public function quoteTableName($tableName) + { + return str_replace('.', '`.`', $this->quoteColumnName($tableName)); + } + + /** + * {@inheritdoc} + */ + public function quoteColumnName($columnName) + { + return '`' . str_replace('`', '``', $columnName) . '`'; + } + + /** + * {@inheritdoc} + */ + public function hasTable($tableName) + { + $tables = array(); + $rows = $this->fetchAll(sprintf('SELECT name FROM sqlite_master WHERE type=\'table\' AND name=\'%s\'', $tableName)); + foreach ($rows as $row) { + $tables[] = strtolower($row[0]); + } + + return in_array(strtolower($tableName), $tables); + } + + /** + * {@inheritdoc} + */ + public function createTable(Table $table) + { + $this->startCommandTimer(); + + // Add the default primary key + $columns = $table->getPendingColumns(); + $options = $table->getOptions(); + if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) { + $column = new Column(); + $column->setName('id') + ->setType('integer') + ->setIdentity(true); + + array_unshift($columns, $column); + + } elseif (isset($options['id']) && is_string($options['id'])) { + // Handle id => "field_name" to support AUTO_INCREMENT + $column = new Column(); + $column->setName($options['id']) + ->setType('integer') + ->setIdentity(true); + + array_unshift($columns, $column); + } + + + $sql = 'CREATE TABLE '; + $sql .= $this->quoteTableName($table->getName()) . ' ('; + foreach ($columns as $column) { + $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', '; + } + + // set the primary key(s) + if (isset($options['primary_key'])) { + $sql = rtrim($sql); + $sql .= ' PRIMARY KEY ('; + if (is_string($options['primary_key'])) { // handle primary_key => 'id' + $sql .= $this->quoteColumnName($options['primary_key']); + } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id') + // PHP 5.4 will allow access of $this, so we can call quoteColumnName() directly in the anonymous function, + // but for now just hard-code the adapter quotes + $sql .= implode( + ',', + array_map( + function ($v) { + return '`' . $v . '`'; + }, + $options['primary_key'] + ) + ); + } + $sql .= ')'; + } else { + $sql = substr(rtrim($sql), 0, -1); // no primary keys + } + + // set the foreign keys + $foreignKeys = $table->getForeignKeys(); + if (!empty($foreignKeys)) { + foreach ($foreignKeys as $foreignKey) { + $sql .= ', ' . $this->getForeignKeySqlDefinition($foreignKey); + } + } + + $sql = rtrim($sql) . ');'; + // execute the sql + $this->writeCommand('createTable', array($table->getName())); + $this->execute($sql); + $this->endCommandTimer(); + + foreach ($table->getIndexes() as $index) { + $this->addIndex($table, $index); + } + } + + /** + * {@inheritdoc} + */ + public function renameTable($tableName, $newTableName) + { + $this->startCommandTimer(); + $this->writeCommand('renameTable', array($tableName, $newTableName)); + $this->execute(sprintf('ALTER TABLE %s RENAME TO %s', $this->quoteTableName($tableName), $this->quoteTableName($newTableName))); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function dropTable($tableName) + { + $this->startCommandTimer(); + $this->writeCommand('dropTable', array($tableName)); + $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tableName))); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function getColumns($tableName) + { + $columns = array(); + $rows = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName))); + + foreach ($rows as $columnInfo) { + $column = new Column(); + $type = strtolower($columnInfo['type']); + $column->setName($columnInfo['name']) + ->setNull($columnInfo['notnull'] !== '1') + ->setDefault($columnInfo['dflt_value']); + + $phinxType = $this->getPhinxType($type); + $column->setType($phinxType['name']) + ->setLimit($phinxType['limit']); + + if ($columnInfo['pk'] == 1) { + $column->setIdentity(true); + } + + $columns[] = $column; + } + + return $columns; + } + + /** + * {@inheritdoc} + */ + public function hasColumn($tableName, $columnName) + { + $rows = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName))); + foreach ($rows as $column) { + if (strcasecmp($column['name'], $columnName) === 0) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function addColumn(Table $table, Column $column) + { + $this->startCommandTimer(); + + $sql = sprintf( + 'ALTER TABLE %s ADD COLUMN %s %s', + $this->quoteTableName($table->getName()), + $this->quoteColumnName($column->getName()), + $this->getColumnSqlDefinition($column) + ); + + $this->writeCommand('addColumn', array($table->getName(), $column->getName(), $column->getType())); + $this->execute($sql); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function renameColumn($tableName, $columnName, $newColumnName) + { + $tmpTableName = 'tmp_' . $tableName; + + $rows = $this->fetchAll('select * from sqlite_master where `type` = \'table\''); + + $sql = ''; + foreach ($rows as $table) { + if ($table['tbl_name'] === $tableName) { + $sql = $table['sql']; + } + } + + $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName))); + $selectColumns = array(); + $writeColumns = array(); + foreach ($columns as $column) { + $selectName = $column['name']; + $writeName = ($selectName == $columnName)? $newColumnName : $selectName; + $selectColumns[] = $this->quoteColumnName($selectName); + $writeColumns[] = $this->quoteColumnName($writeName); + } + + if (!in_array($this->quoteColumnName($columnName), $selectColumns)) { + throw new \InvalidArgumentException(sprintf( + 'The specified column doesn\'t exist: ' . $columnName + )); + } + + $this->execute(sprintf('ALTER TABLE %s RENAME TO %s', $tableName, $tmpTableName)); + + $sql = str_replace( + $this->quoteColumnName($columnName), + $this->quoteColumnName($newColumnName), + $sql + ); + $this->execute($sql); + + + $sql = sprintf( + 'INSERT INTO %s(%s) SELECT %s FROM %s', + $tableName, + implode(', ', $writeColumns), + implode(', ', $selectColumns), + $tmpTableName + ); + + $this->execute($sql); + + $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tmpTableName))); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function changeColumn($tableName, $columnName, Column $newColumn) + { + + // TODO: DRY this up.... + + $this->startCommandTimer(); + $this->writeCommand('changeColumn', array($tableName, $columnName, $newColumn->getType())); + + $tmpTableName = 'tmp_' . $tableName; + + $rows = $this->fetchAll('select * from sqlite_master where `type` = \'table\''); + + $sql = ''; + foreach ($rows as $table) { + if ($table['tbl_name'] === $tableName) { + $sql = $table['sql']; + } + } + + $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName))); + $selectColumns = array(); + $writeColumns = array(); + foreach ($columns as $column) { + $selectName = $column['name']; + $writeName = ($selectName === $columnName)? $newColumn->getName() : $selectName; + $selectColumns[] = $this->quoteColumnName($selectName); + $writeColumns[] = $this->quoteColumnName($writeName); + } + + if (!in_array($this->quoteColumnName($columnName), $selectColumns)) { + throw new \InvalidArgumentException(sprintf( + 'The specified column doesn\'t exist: ' . $columnName + )); + } + + $this->execute(sprintf('ALTER TABLE %s RENAME TO %s', $tableName, $tmpTableName)); + + $sql = preg_replace( + sprintf("/%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']+'|[^,])+([,)])/", $this->quoteColumnName($columnName)), + sprintf('%s %s$1', $this->quoteColumnName($newColumn->getName()), $this->getColumnSqlDefinition($newColumn)), + $sql, + 1 + ); + + $this->execute($sql); + + $sql = sprintf( + 'INSERT INTO %s(%s) SELECT %s FROM %s', + $tableName, + implode(', ', $writeColumns), + implode(', ', $selectColumns), + $tmpTableName + ); + + $this->execute($sql); + $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tmpTableName))); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function dropColumn($tableName, $columnName) + { + // TODO: DRY this up.... + + $this->startCommandTimer(); + $this->writeCommand('dropColumn', array($tableName, $columnName)); + + $tmpTableName = 'tmp_' . $tableName; + + $rows = $this->fetchAll('select * from sqlite_master where `type` = \'table\''); + + $sql = ''; + foreach ($rows as $table) { + if ($table['tbl_name'] === $tableName) { + $sql = $table['sql']; + } + } + + $rows = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName))); + $columns = array(); + $columnType = null; + foreach ($rows as $row) { + if ($row['name'] !== $columnName) { + $columns[] = $row['name']; + } else { + $found = true; + $columnType = $row['type']; + } + } + + if (!isset($found)) { + throw new \InvalidArgumentException(sprintf( + 'The specified column doesn\'t exist: ' . $columnName + )); + } + + $this->execute(sprintf('ALTER TABLE %s RENAME TO %s', $tableName, $tmpTableName)); + + $sql = preg_replace( + sprintf("/%s\s%s[^,)]*(,\s|\))/", preg_quote($this->quoteColumnName($columnName)), preg_quote($columnType)), + "", + $sql + ); + + if (substr($sql, -2) === ', ') { + $sql = substr($sql, 0, -2) . ')'; + } + + $this->execute($sql); + + $sql = sprintf( + 'INSERT INTO %s(%s) SELECT %s FROM %s', + $tableName, + implode(', ', $columns), + implode(', ', $columns), + $tmpTableName + ); + + $this->execute($sql); + $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tmpTableName))); + $this->endCommandTimer(); + } + + /** + * Get an array of indexes from a particular table. + * + * @param string $tableName Table Name + * @return array + */ + protected function getIndexes($tableName) + { + $indexes = array(); + $rows = $this->fetchAll(sprintf('pragma index_list(%s)', $tableName)); + + foreach ($rows as $row) { + $indexData = $this->fetchAll(sprintf('pragma index_info(%s)', $row['name'])); + if (!isset($indexes[$tableName])) { + $indexes[$tableName] = array('index' => $row['name'], 'columns' => array()); + } + foreach ($indexData as $indexItem) { + $indexes[$tableName]['columns'][] = strtolower($indexItem['name']); + } + } + return $indexes; + } + + /** + * {@inheritdoc} + */ + public function hasIndex($tableName, $columns) + { + if (is_string($columns)) { + $columns = array($columns); // str to array + } + + $columns = array_map('strtolower', $columns); + $indexes = $this->getIndexes($tableName); + + foreach ($indexes as $index) { + $a = array_diff($columns, $index['columns']); + if (empty($a)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function hasIndexByName($tableName, $indexName) + { + $indexes = $this->getIndexes($tableName); + + foreach ($indexes as $index) { + if ($indexName === $index['index']) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function addIndex(Table $table, Index $index) + { + $this->startCommandTimer(); + $this->writeCommand('addIndex', array($table->getName(), $index->getColumns())); + $indexColumnArray = array(); + foreach ($index->getColumns() as $column) { + $indexColumnArray []= sprintf('`%s` ASC', $column); + } + $indexColumns = implode(',', $indexColumnArray); + $this->execute( + sprintf( + 'CREATE %s ON %s (%s)', + $this->getIndexSqlDefinition($table, $index), + $this->quoteTableName($table->getName()), + $indexColumns + ) + ); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function dropIndex($tableName, $columns) + { + $this->startCommandTimer(); + if (is_string($columns)) { + $columns = array($columns); // str to array + } + + $this->writeCommand('dropIndex', array($tableName, $columns)); + $indexes = $this->getIndexes($tableName); + $columns = array_map('strtolower', $columns); + + foreach ($indexes as $index) { + $a = array_diff($columns, $index['columns']); + if (empty($a)) { + $this->execute( + sprintf( + 'DROP INDEX %s', + $this->quoteColumnName($index['index']) + ) + ); + $this->endCommandTimer(); + return; + } + } + } + + /** + * {@inheritdoc} + */ + public function dropIndexByName($tableName, $indexName) + { + $this->startCommandTimer(); + + $this->writeCommand('dropIndexByName', array($tableName, $indexName)); + $indexes = $this->getIndexes($tableName); + + foreach ($indexes as $index) { + if ($indexName === $index['index']) { + $this->execute( + sprintf( + 'DROP INDEX %s', + $this->quoteColumnName($indexName) + ) + ); + $this->endCommandTimer(); + return; + } + } + } + + /** + * {@inheritdoc} + */ + public function hasForeignKey($tableName, $columns, $constraint = null) + { + if (is_string($columns)) { + $columns = array($columns); // str to array + } + $foreignKeys = $this->getForeignKeys($tableName); + + $a = array_diff($columns, $foreignKeys); + if (empty($a)) { + return true; + } + return false; + } + + /** + * Get an array of foreign keys from a particular table. + * + * @param string $tableName Table Name + * @return array + */ + protected function getForeignKeys($tableName) + { + $foreignKeys = array(); + $rows = $this->fetchAll( + "SELECT sql, tbl_name + FROM ( + SELECT sql sql, type type, tbl_name tbl_name, name name + FROM sqlite_master + UNION ALL + SELECT sql, type, tbl_name, name + FROM sqlite_temp_master + ) + WHERE type != 'meta' + AND sql NOTNULL + AND name NOT LIKE 'sqlite_%' + ORDER BY substr(type, 2, 1), name" + ); + + foreach ($rows as $row) { + if ($row['tbl_name'] === $tableName) { + + if (strpos($row['sql'], 'REFERENCES') !== false) { + preg_match_all("/\(`([^`]*)`\) REFERENCES/", $row['sql'], $matches); + foreach ($matches[1] as $match) { + $foreignKeys[] = $match; + } + } + } + } + return $foreignKeys; + } + + /** + * {@inheritdoc} + */ + public function addForeignKey(Table $table, ForeignKey $foreignKey) + { + // TODO: DRY this up.... + $this->startCommandTimer(); + $this->writeCommand('addForeignKey', array($table->getName(), $foreignKey->getColumns())); + $this->execute('pragma foreign_keys = ON'); + + $tmpTableName = 'tmp_' . $table->getName(); + $rows = $this->fetchAll('select * from sqlite_master where `type` = \'table\''); + + $sql = ''; + foreach ($rows as $row) { + if ($row['tbl_name'] === $table->getName()) { + $sql = $row['sql']; + } + } + + $rows = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($table->getName()))); + $columns = array(); + foreach ($rows as $column) { + $columns[] = $this->quoteColumnName($column['name']); + } + + $this->execute(sprintf('ALTER TABLE %s RENAME TO %s', $this->quoteTableName($table->getName()), $tmpTableName)); + + $sql = substr($sql, 0, -1) . ',' . $this->getForeignKeySqlDefinition($foreignKey) . ')'; + $this->execute($sql); + + $sql = sprintf( + 'INSERT INTO %s(%s) SELECT %s FROM %s', + $this->quoteTableName($table->getName()), + implode(', ', $columns), + implode(', ', $columns), + $this->quoteTableName($tmpTableName) + ); + + $this->execute($sql); + $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tmpTableName))); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function dropForeignKey($tableName, $columns, $constraint = null) + { + // TODO: DRY this up.... + + $this->startCommandTimer(); + if (is_string($columns)) { + $columns = array($columns); // str to array + } + + $this->writeCommand('dropForeignKey', array($tableName, $columns)); + + $tmpTableName = 'tmp_' . $tableName; + + $rows = $this->fetchAll('select * from sqlite_master where `type` = \'table\''); + + $sql = ''; + foreach ($rows as $table) { + if ($table['tbl_name'] === $tableName) { + $sql = $table['sql']; + } + } + + $rows = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName))); + $replaceColumns = array(); + foreach ($rows as $row) { + if (!in_array($row['name'], $columns)) { + $replaceColumns[] = $row['name']; + } else { + $found = true; + } + } + + if (!isset($found)) { + throw new \InvalidArgumentException(sprintf( + 'The specified column doesn\'t exist: ' + )); + } + + $this->execute(sprintf('ALTER TABLE %s RENAME TO %s', $this->quoteTableName($tableName), $tmpTableName)); + + foreach ($columns as $columnName) { + $search = sprintf( + "/,[^,]*\(%s(?:,`?(.*)`?)?\) REFERENCES[^,]*\([^\)]*\)[^,)]*/", + $this->quoteColumnName($columnName) + ); + $sql = preg_replace($search, '', $sql, 1); + } + + $this->execute($sql); + + $sql = sprintf( + 'INSERT INTO %s(%s) SELECT %s FROM %s', + $tableName, + implode(', ', $columns), + implode(', ', $columns), + $tmpTableName + ); + + $this->execute($sql); + $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tmpTableName))); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function insert(Table $table, $row) + { + $this->startCommandTimer(); + $this->writeCommand('insert', array($table->getName())); + + $sql = sprintf( + "INSERT INTO %s ", + $this->quoteTableName($table->getName()) + ); + + $columns = array_keys($row); + $sql .= "(". implode(', ', array_map(array($this, 'quoteColumnName'), $columns)) . ")"; + $sql .= " VALUES "; + + $sql .= "(" . implode(', ', array_map(function ($value) { + if (is_numeric($value)) { + return $value; + } + + if ($value === null) { + return 'null'; + } + + return $this->getConnection()->quote($value); + }, $row)) . ")"; + + $this->execute($sql); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function getSqlType($type, $limit = null) + { + switch ($type) { + case static::PHINX_TYPE_STRING: + return array('name' => 'varchar', 'limit' => 255); + break; + case static::PHINX_TYPE_CHAR: + return array('name' => 'char', 'limit' => 255); + break; + case static::PHINX_TYPE_TEXT: + return array('name' => 'text'); + break; + case static::PHINX_TYPE_INTEGER: + return array('name' => 'integer'); + break; + case static::PHINX_TYPE_BIG_INTEGER: + return array('name' => 'bigint'); + break; + case static::PHINX_TYPE_FLOAT: + return array('name' => 'float'); + break; + case static::PHINX_TYPE_DECIMAL: + return array('name' => 'decimal'); + break; + case static::PHINX_TYPE_DATETIME: + return array('name' => 'datetime'); + break; + case static::PHINX_TYPE_TIMESTAMP: + return array('name' => 'datetime'); + break; + case static::PHINX_TYPE_TIME: + return array('name' => 'time'); + break; + case static::PHINX_TYPE_DATE: + return array('name' => 'date'); + break; + case static::PHINX_TYPE_BLOB: + case static::PHINX_TYPE_BINARY: + return array('name' => 'blob'); + break; + case static::PHINX_TYPE_BOOLEAN: + return array('name' => 'boolean'); + break; + case static::PHINX_TYPE_UUID: + return array('name' => 'char', 'limit' => 36); + case static::PHINX_TYPE_ENUM: + return array('name' => 'enum'); + // Geospatial database types + // No specific data types exist in SQLite, instead all geospatial + // functionality is handled in the client. See also: SpatiaLite. + case static::PHINX_TYPE_GEOMETRY: + case static::PHINX_TYPE_POLYGON: + return array('name' => 'text'); + return; + case static::PHINX_TYPE_LINESTRING: + return array('name' => 'varchar', 'limit' => 255); + break; + case static::PHINX_TYPE_POINT: + return array('name' => 'float'); + default: + throw new \RuntimeException('The type: "' . $type . '" is not supported.'); + } + } + + /** + * Returns Phinx type by SQL type + * + * @param string $sqlTypeDef SQL type + * @returns string Phinx type + */ + public function getPhinxType($sqlTypeDef) + { + if (!preg_match('/^([\w]+)(\(([\d]+)*(,([\d]+))*\))*$/', $sqlTypeDef, $matches)) { + throw new \RuntimeException('Column type ' . $sqlTypeDef . ' is not supported'); + } else { + $limit = null; + $precision = null; + $type = $matches[1]; + if (count($matches) > 2) { + $limit = $matches[3] ? $matches[3] : null; + } + if (count($matches) > 4) { + $precision = $matches[5]; + } + switch ($matches[1]) { + case 'varchar': + $type = static::PHINX_TYPE_STRING; + if ($limit === 255) { + $limit = null; + } + break; + case 'char': + $type = static::PHINX_TYPE_CHAR; + if ($limit === 255) { + $limit = null; + } + if ($limit === 36) { + $type = static::PHINX_TYPE_UUID; + } + break; + case 'int': + $type = static::PHINX_TYPE_INTEGER; + if ($limit === 11) { + $limit = null; + } + break; + case 'bigint': + if ($limit === 11) { + $limit = null; + } + $type = static::PHINX_TYPE_BIG_INTEGER; + break; + case 'blob': + $type = static::PHINX_TYPE_BINARY; + break; + } + if ($type === 'tinyint') { + if ($matches[3] === 1) { + $type = static::PHINX_TYPE_BOOLEAN; + $limit = null; + } + } + + $this->getSqlType($type); + + return array( + 'name' => $type, + 'limit' => $limit, + 'precision' => $precision + ); + } + } + + /** + * {@inheritdoc} + */ + public function createDatabase($name, $options = array()) + { + $this->startCommandTimer(); + $this->writeCommand('createDatabase', array($name)); + touch($name . '.sqlite3'); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function hasDatabase($name) + { + return is_file($name . '.sqlite3'); + } + + /** + * {@inheritdoc} + */ + public function dropDatabase($name) + { + $this->startCommandTimer(); + $this->writeCommand('dropDatabase', array($name)); + if (file_exists($name . '.sqlite3')) { + unlink($name . '.sqlite3'); + } + $this->endCommandTimer(); + } + + /** + * Get the definition for a `DEFAULT` statement. + * + * @param mixed $default + * @return string + */ + protected function getDefaultValueDefinition($default) + { + if (is_string($default) && 'CURRENT_TIMESTAMP' !== $default) { + $default = $this->getConnection()->quote($default); + } elseif (is_bool($default)) { + $default = $this->castToBool($default); + } + return isset($default) ? ' DEFAULT ' . $default : ''; + } + + /** + * Gets the SQLite Column Definition for a Column object. + * + * @param Column $column Column + * @return string + */ + protected function getColumnSqlDefinition(Column $column) + { + $sqlType = $this->getSqlType($column->getType()); + $def = ''; + $def .= strtoupper($sqlType['name']); + if ($column->getPrecision() && $column->getScale()) { + $def .= '(' . $column->getPrecision() . ',' . $column->getScale() . ')'; + } + $limitable = in_array(strtoupper($sqlType['name']), $this->definitionsWithLimits); + if (($column->getLimit() || isset($sqlType['limit'])) && $limitable) { + $def .= '(' . ($column->getLimit() ? $column->getLimit() : $sqlType['limit']) . ')'; + } + if (($values = $column->getValues()) && is_array($values)) { + $def .= " CHECK({$column->getName()} IN ('" . implode("', '", $values) . "'))"; + } + + $default = $column->getDefault(); + + $def .= ($column->isNull() || is_null($default)) ? ' NULL' : ' NOT NULL'; + $def .= $this->getDefaultValueDefinition($default); + $def .= ($column->isIdentity()) ? ' PRIMARY KEY AUTOINCREMENT' : ''; + + if ($column->getUpdate()) { + $def .= ' ON UPDATE ' . $column->getUpdate(); + } + + $def .= $this->getCommentDefinition($column); + + return $def; + } + + /** + * Gets the comment Definition for a Column object. + * + * @param Column $column Column + * @return string + */ + protected function getCommentDefinition(Column $column) + { + if ($column->getComment()) { + return ' /* ' . $column->getComment() . ' */ '; + } + return ''; + } + + /** + * Gets the SQLite Index Definition for an Index object. + * + * @param Index $index Index + * @return string + */ + protected function getIndexSqlDefinition(Table $table, Index $index) + { + if ($index->getType() === Index::UNIQUE) { + $def = 'UNIQUE INDEX'; + } else { + $def = 'INDEX'; + } + if (is_string($index->getName())) { + $indexName = $index->getName(); + } else { + $indexName = $table->getName() . '_'; + foreach ($index->getColumns() as $column) { + $indexName .= $column . '_'; + } + $indexName .= 'index'; + } + $def .= ' `' . $indexName . '`'; + return $def; + } + + /** + * {@inheritdoc} + */ + public function getColumnTypes() + { + return array_merge(parent::getColumnTypes(), array('enum')); + } + + /** + * Gets the SQLite Foreign Key Definition for an ForeignKey object. + * + * @param ForeignKey $foreignKey + * @return string + */ + protected function getForeignKeySqlDefinition(ForeignKey $foreignKey) + { + $def = ''; + if ($foreignKey->getConstraint()) { + $def .= ' CONSTRAINT ' . $this->quoteColumnName($foreignKey->getConstraint()); + } else { + $columnNames = array(); + foreach ($foreignKey->getColumns() as $column) { + $columnNames[] = $this->quoteColumnName($column); + } + $def .= ' FOREIGN KEY (' . implode(',', $columnNames) . ')'; + $refColumnNames = array(); + foreach ($foreignKey->getReferencedColumns() as $column) { + $refColumnNames[] = $this->quoteColumnName($column); + } + $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()->getName()) . ' (' . implode(',', $refColumnNames) . ')'; + if ($foreignKey->getOnDelete()) { + $def .= ' ON DELETE ' . $foreignKey->getOnDelete(); + } + if ($foreignKey->getOnUpdate()) { + $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate(); + } + } + return $def; + } +} diff --git a/src/DDLWrapper/Db/Adapter/SqlServerAdapter.php b/src/DDLWrapper/Db/Adapter/SqlServerAdapter.php new file mode 100644 index 0000000..a6c8468 --- /dev/null +++ b/src/DDLWrapper/Db/Adapter/SqlServerAdapter.php @@ -0,0 +1,1166 @@ + + */ +class SqlServerAdapter extends PdoAdapter implements AdapterInterface +{ + protected $schema = 'dbo'; + + protected $signedColumnTypes = array('integer' => true, 'biginteger' => true, 'float' => true, 'decimal' => true); + + /** + * {@inheritdoc} + */ + public function connect() + { + if (null === $this->connection) { + if (!class_exists('PDO') || !in_array('sqlsrv', \PDO::getAvailableDrivers(), true)) { + // try our connection via freetds (Mac/Linux) + return $this->connectDblib(); + } + + $db = null; + $options = $this->getOptions(); + + // if port is specified use it, otherwise use the SqlServer default + if (empty($options['port'])) { + $dsn = 'sqlsrv:server=' . $options['host'] . ';database=' . $options['name']; + } else { + $dsn = 'sqlsrv:server=' . $options['host'] . ',' . $options['port'] . ';database=' . $options['name']; + } + $dsn .= ';MultipleActiveResultSets=false'; + + $driverOptions = array(\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION); + + // charset support + if (isset($options['charset'])) { + $driverOptions[\PDO::SQLSRV_ATTR_ENCODING] = $options['charset']; + } + + // support arbitrary \PDO::SQLSRV_ATTR_* driver options and pass them to PDO + // http://php.net/manual/en/ref.pdo-sqlsrv.php#pdo-sqlsrv.constants + foreach ($options as $key => $option) { + if (strpos($key, 'sqlsrv_attr_') === 0) { + $driverOptions[constant('\PDO::' . strtoupper($key))] = $option; + } + } + + try { + $db = new \PDO($dsn, $options['user'], $options['pass'], $driverOptions); + } catch (\PDOException $exception) { + throw new \InvalidArgumentException(sprintf( + 'There was a problem connecting to the database: %s', + $exception->getMessage() + )); + } + + $this->setConnection($db); + } + } + + /** + * Connect to MSSQL using dblib/freetds. + * + * The "sqlsrv" driver is not available on Unix machines. + * + * @throws \InvalidArgumentException + */ + protected function connectDblib() + { + if (!class_exists('PDO') || !in_array('dblib', \PDO::getAvailableDrivers(), true)) { + // @codeCoverageIgnoreStart + throw new \RuntimeException('You need to enable the PDO_Dblib extension for Phinx to run properly.'); + // @codeCoverageIgnoreEnd + } + + $options = $this->getOptions(); + + // if port is specified use it, otherwise use the SqlServer default + if (empty($options['port'])) { + $dsn = 'dblib:host=' . $options['host'] . ';dbname=' . $options['name']; + } else { + $dsn = 'dblib:host=' . $options['host'] . ':' . $options['port'] . ';dbname=' . $options['name']; + } + + $driverOptions = array(\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION); + + + try { + $db = new \PDO($dsn, $options['user'], $options['pass'], $driverOptions); + } catch (\PDOException $exception) { + throw new \InvalidArgumentException(sprintf( + 'There was a problem connecting to the database: %s', + $exception->getMessage() + )); + } + + $this->setConnection($db); + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + $this->connection = null; + } + + /** + * {@inheritdoc} + */ + public function hasTransactions() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function beginTransaction() + { + $this->execute('BEGIN TRANSACTION'); + } + + /** + * {@inheritdoc} + */ + public function commitTransaction() + { + $this->execute('COMMIT TRANSACTION'); + } + + /** + * {@inheritdoc} + */ + public function rollbackTransaction() + { + $this->execute('ROLLBACK TRANSACTION'); + } + + /** + * {@inheritdoc} + */ + public function quoteTableName($tableName) + { + return str_replace('.', '].[', $this->quoteColumnName($tableName)); + } + + /** + * {@inheritdoc} + */ + public function quoteColumnName($columnName) + { + return '[' . str_replace(']', '\]', $columnName) . ']'; + } + + /** + * {@inheritdoc} + */ + public function hasTable($tableName) + { + $result = $this->fetchRow(sprintf('SELECT count(*) as [count] FROM information_schema.tables WHERE table_name = \'%s\';', $tableName)); + return $result['count'] > 0; + } + + /** + * {@inheritdoc} + */ + public function createTable(Table $table) + { + $this->startCommandTimer(); + + $options = $table->getOptions(); + + // Add the default primary key + $columns = $table->getPendingColumns(); + if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) { + $column = new Column(); + $column->setName('id') + ->setType('integer') + ->setIdentity(true); + + array_unshift($columns, $column); + $options['primary_key'] = 'id'; + + } elseif (isset($options['id']) && is_string($options['id'])) { + // Handle id => "field_name" to support AUTO_INCREMENT + $column = new Column(); + $column->setName($options['id']) + ->setType('integer') + ->setIdentity(true); + + array_unshift($columns, $column); + $options['primary_key'] = $options['id']; + } + + $sql = 'CREATE TABLE '; + $sql .= $this->quoteTableName($table->getName()) . ' ('; + $sqlBuffer = array(); + $columnsWithComments = array(); + foreach ($columns as $column) { + $sqlBuffer[] = $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column); + + // set column comments, if needed + if ($column->getComment()) { + $columnsWithComments[] = $column; + } + } + + // set the primary key(s) + if (isset($options['primary_key'])) { + $pkSql = sprintf('CONSTRAINT PK_%s PRIMARY KEY (', $table->getName()); + if (is_string($options['primary_key'])) { // handle primary_key => 'id' + $pkSql .= $this->quoteColumnName($options['primary_key']); + } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id') + // PHP 5.4 will allow access of $this, so we can call quoteColumnName() directly in the anonymous function, + // but for now just hard-code the adapter quotes + $pkSql .= implode( + ',', + array_map( + function ($v) { + return '[' . $v . ']'; + }, + $options['primary_key'] + ) + ); + } + $pkSql .= ')'; + $sqlBuffer[] = $pkSql; + } + + // set the foreign keys + $foreignKeys = $table->getForeignKeys(); + if (!empty($foreignKeys)) { + foreach ($foreignKeys as $foreignKey) { + $sqlBuffer[] = $this->getForeignKeySqlDefinition($foreignKey, $table->getName()); + } + } + + $sql .= implode(', ', $sqlBuffer); + $sql .= ');'; + + // process column comments + if (!empty($columnsWithComments)) { + foreach ($columnsWithComments as $column) { + $sql .= $this->getColumnCommentSqlDefinition($column, $table->getName()); + } + } + + // set the indexes + $indexes = $table->getIndexes(); + if (!empty($indexes)) { + foreach ($indexes as $index) { + $sql .= $this->getIndexSqlDefinition($index, $table->getName()); + } + } + + // execute the sql + $this->writeCommand('createTable', array($table->getName())); + $this->execute($sql); + $this->endCommandTimer(); + } + + /** + * Gets the SqlServer Column Comment Defininition for a column object. + * + * @param Column $column Column + * @param string $tableName Table name + * + * @return string + */ + protected function getColumnCommentSqlDefinition(Column $column, $tableName) + { + // passing 'null' is to remove column comment + $currentComment = $this->getColumnComment($tableName, $column->getName()); + + $comment = (strcasecmp($column->getComment(), 'NULL') !== 0) ? $this->getConnection()->quote($column->getComment()) : '\'\''; + $command = $currentComment === false ? 'sp_addextendedproperty' : 'sp_updateextendedproperty'; + return sprintf( + "EXECUTE %s N'MS_Description', N%s, N'SCHEMA', N'%s', N'TABLE', N'%s', N'COLUMN', N'%s';", + $command, + $comment, + $this->schema, + $tableName, + $column->getName() + ); + } + + /** + * {@inheritdoc} + */ + public function renameTable($tableName, $newTableName) + { + $this->startCommandTimer(); + $this->writeCommand('renameTable', array($tableName, $newTableName)); + $this->execute(sprintf('EXEC sp_rename \'%s\', \'%s\'', $tableName, $newTableName)); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function dropTable($tableName) + { + $this->startCommandTimer(); + $this->writeCommand('dropTable', array($tableName)); + $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tableName))); + $this->endCommandTimer(); + } + + public function getColumnComment($tableName, $columnName) + { + $sql = sprintf("SELECT cast(extended_properties.[value] as nvarchar(4000)) comment + FROM sys.schemas + INNER JOIN sys.tables + ON schemas.schema_id = tables.schema_id + INNER JOIN sys.columns + ON tables.object_id = columns.object_id + INNER JOIN sys.extended_properties + ON tables.object_id = extended_properties.major_id + AND columns.column_id = extended_properties.minor_id + AND extended_properties.name = 'MS_Description' + WHERE schemas.[name] = '%s' AND tables.[name] = '%s' AND columns.[name] = '%s'", $this->schema, $tableName, $columnName); + $row = $this->fetchRow($sql); + + if ($row) { + return $row['comment']; + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getColumns($tableName) + { + $columns = array(); + $sql = sprintf( + "SELECT DISTINCT TABLE_SCHEMA AS [schema], TABLE_NAME as [table_name], COLUMN_NAME AS [name], DATA_TYPE AS [type], + IS_NULLABLE AS [null], COLUMN_DEFAULT AS [default], + CHARACTER_MAXIMUM_LENGTH AS [char_length], + NUMERIC_PRECISION AS [precision], + NUMERIC_SCALE AS [scale], ORDINAL_POSITION AS [ordinal_position], + COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') as [identity] + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = '%s' + ORDER BY ordinal_position", + $tableName + ); + $rows = $this->fetchAll($sql); + foreach ($rows as $columnInfo) { + $column = new Column(); + $column->setName($columnInfo['name']) + ->setType($this->getPhinxType($columnInfo['type'])) + ->setNull($columnInfo['null'] !== 'NO') + ->setDefault($this->parseDefault($columnInfo['default'])) + ->setIdentity($columnInfo['identity'] === '1') + ->setComment($this->getColumnComment($columnInfo['table_name'], $columnInfo['name'])); + + if (!empty($columnInfo['char_length'])) { + $column->setLimit($columnInfo['char_length']); + } + + $columns[$columnInfo['name']] = $column; + } + + return $columns; + } + + protected function parseDefault($default) + { + $default = preg_replace(array("/\('(.*)'\)/", "/\(\((.*)\)\)/", "/\((.*)\)/"), '$1', $default); + + if (strtoupper($default) === 'NULL') { + $default = null; + } elseif (is_numeric($default)) { + $default = (int) $default; + } + + return $default; + } + + /** + * {@inheritdoc} + */ + public function hasColumn($tableName, $columnName, $options = array()) + { + $sql = sprintf( + "SELECT count(*) as [count] + FROM information_schema.columns + WHERE table_name = '%s' AND column_name = '%s'", + $tableName, + $columnName + ); + $result = $this->fetchRow($sql); + + return $result['count'] > 0; + } + + /** + * {@inheritdoc} + */ + public function addColumn(Table $table, Column $column) + { + $this->startCommandTimer(); + $sql = sprintf( + 'ALTER TABLE %s ADD %s %s', + $this->quoteTableName($table->getName()), + $this->quoteColumnName($column->getName()), + $this->getColumnSqlDefinition($column) + ); + + $this->writeCommand('addColumn', array($table->getName(), $column->getName(), $column->getType())); + $this->execute($sql); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function renameColumn($tableName, $columnName, $newColumnName) + { + $this->startCommandTimer(); + + if (!$this->hasColumn($tableName, $columnName)) { + throw new \InvalidArgumentException("The specified column does not exist: $columnName"); + } + $this->writeCommand('renameColumn', array($tableName, $columnName, $newColumnName)); + $this->renameDefault($tableName, $columnName, $newColumnName); + $this->execute( + sprintf( + "EXECUTE sp_rename N'%s.%s', N'%s', 'COLUMN' ", + $tableName, + $columnName, + $newColumnName + ) + ); + $this->endCommandTimer(); + } + + protected function renameDefault($tableName, $columnName, $newColumnName) + { + $oldConstraintName = "DF_{$tableName}_{$columnName}"; + $newConstraintName = "DF_{$tableName}_{$newColumnName}"; + $sql = <<execute(sprintf( + $sql, + $oldConstraintName, + $newConstraintName + )); + } + + public function changeDefault($tableName, Column $newColumn) + { + $constraintName = "DF_{$tableName}_{$newColumn->getName()}"; + $default = $newColumn->getDefault(); + + if ($default === null) { + $default = 'DEFAULT NULL'; + } else { + $default = $this->getDefaultValueDefinition($default); + } + + if (empty($default)) { + return; + } + + $this->execute(sprintf( + 'ALTER TABLE %s ADD CONSTRAINT %s %s FOR %s', + $this->quoteTableName($tableName), + $constraintName, + $default, + $this->quoteColumnName($newColumn->getName()) + )); + } + + /** + * {@inheritdoc} + */ + public function changeColumn($tableName, $columnName, Column $newColumn) + { + $this->startCommandTimer(); + $this->writeCommand('changeColumn', array($tableName, $columnName, $newColumn->getType())); + $columns = $this->getColumns($tableName); + $changeDefault = $newColumn->getDefault() !== $columns[$columnName]->getDefault() || $newColumn->getType() !== $columns[$columnName]->getType(); + if ($columnName !== $newColumn->getName()) { + $this->renameColumn($tableName, $columnName, $newColumn->getName()); + } + + if ($changeDefault) { + $this->dropDefaultConstraint($tableName, $newColumn->getName()); + } + + $this->execute( + sprintf( + 'ALTER TABLE %s ALTER COLUMN %s %s', + $this->quoteTableName($tableName), + $this->quoteColumnName($newColumn->getName()), + $this->getColumnSqlDefinition($newColumn, false) + ) + ); + // change column comment if needed + if ($newColumn->getComment()) { + $sql = $this->getColumnCommentSqlDefinition($newColumn, $tableName); + $this->execute($sql); + } + + if ($changeDefault) { + $this->changeDefault($tableName, $newColumn); + } + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function dropColumn($tableName, $columnName) + { + $this->startCommandTimer(); + $this->writeCommand('dropColumn', array($tableName, $columnName)); + $this->dropDefaultConstraint($tableName, $columnName); + + $this->execute( + sprintf( + 'ALTER TABLE %s DROP COLUMN %s', + $this->quoteTableName($tableName), + $this->quoteColumnName($columnName) + ) + ); + $this->endCommandTimer(); + } + + protected function dropDefaultConstraint($tableName, $columnName) + { + $defaultConstraint = $this->getDefaultConstraint($tableName, $columnName); + + if (!$defaultConstraint) { + return; + } + + $this->dropForeignKey($tableName, $columnName, $defaultConstraint); + } + + protected function getDefaultConstraint($tableName, $columnName) + { + $sql = "SELECT + default_constraints.name +FROM + sys.all_columns + + INNER JOIN + sys.tables + ON all_columns.object_id = tables.object_id + + INNER JOIN + sys.schemas + ON tables.schema_id = schemas.schema_id + + INNER JOIN + sys.default_constraints + ON all_columns.default_object_id = default_constraints.object_id + +WHERE + schemas.name = 'dbo' + AND tables.name = '{$tableName}' + AND all_columns.name = '{$columnName}'"; + + $rows = $this->fetchAll($sql); + return empty($rows) ? false : $rows[0]['name']; + } + + protected function getIndexColums($tableId, $indexId) + { + $sql = "SELECT AC.[name] AS [column_name] +FROM sys.[index_columns] IC + INNER JOIN sys.[all_columns] AC ON IC.[column_id] = AC.[column_id] +WHERE AC.[object_id] = {$tableId} AND IC.[index_id] = {$indexId} AND IC.[object_id] = {$tableId} +ORDER BY IC.[key_ordinal];"; + + $rows = $this->fetchAll($sql); + $columns = array(); + foreach($rows as $row) { + $columns[] = strtolower($row['column_name']); + } + return $columns; + } + + /** + * Get an array of indexes from a particular table. + * + * @param string $tableName Table Name + * @return array + */ + public function getIndexes($tableName) + { + $indexes = array(); + $sql = "SELECT I.[name] AS [index_name], I.[index_id] as [index_id], T.[object_id] as [table_id] +FROM sys.[tables] AS T + INNER JOIN sys.[indexes] I ON T.[object_id] = I.[object_id] +WHERE T.[is_ms_shipped] = 0 AND I.[type_desc] <> 'HEAP' AND T.[name] = '{$tableName}' +ORDER BY T.[name], I.[index_id];"; + + $rows = $this->fetchAll($sql); + foreach ($rows as $row) { + $columns = $this->getIndexColums($row['table_id'], $row['index_id']); + $indexes[$row['index_name']] = array('columns' => $columns); + } + + return $indexes; + } + + /** + * {@inheritdoc} + */ + public function hasIndex($tableName, $columns) + { + if (is_string($columns)) { + $columns = array($columns); // str to array + } + + $columns = array_map('strtolower', $columns); + $indexes = $this->getIndexes($tableName); + + foreach ($indexes as $index) { + $a = array_diff($columns, $index['columns']); + + if (empty($a)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function hasIndexByName($tableName, $indexName) + { + $indexes = $this->getIndexes($tableName); + + foreach ($indexes as $name => $index) { + if ($name === $indexName) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function addIndex(Table $table, Index $index) + { + $this->startCommandTimer(); + $this->writeCommand('addIndex', array($table->getName(), $index->getColumns())); + $sql = $this->getIndexSqlDefinition($index, $table->getName()); + $this->execute($sql); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function dropIndex($tableName, $columns) + { + $this->startCommandTimer(); + if (is_string($columns)) { + $columns = array($columns); // str to array + } + + $this->writeCommand('dropIndex', array($tableName, $columns)); + $indexes = $this->getIndexes($tableName); + $columns = array_map('strtolower', $columns); + + foreach ($indexes as $indexName => $index) { + $a = array_diff($columns, $index['columns']); + if (empty($a)) { + $this->execute( + sprintf( + 'DROP INDEX %s ON %s', + $this->quoteColumnName($indexName), + $this->quoteTableName($tableName) + ) + ); + $this->endCommandTimer(); + return; + } + } + } + + /** + * {@inheritdoc} + */ + public function dropIndexByName($tableName, $indexName) + { + $this->startCommandTimer(); + + $this->writeCommand('dropIndexByName', array($tableName, $indexName)); + $indexes = $this->getIndexes($tableName); + + foreach ($indexes as $name => $index) { + if ($name === $indexName) { + $this->execute( + sprintf( + 'DROP INDEX %s ON %s', + $this->quoteColumnName($indexName), + $this->quoteTableName($tableName) + ) + ); + $this->endCommandTimer(); + return; + } + } + } + + /** + * {@inheritdoc} + */ + public function hasForeignKey($tableName, $columns, $constraint = null) + { + if (is_string($columns)) { + $columns = array($columns); // str to array + } + $foreignKeys = $this->getForeignKeys($tableName); + if ($constraint) { + if (isset($foreignKeys[$constraint])) { + return !empty($foreignKeys[$constraint]); + } + return false; + } else { + foreach ($foreignKeys as $key) { + $a = array_diff($columns, $key['columns']); + if (empty($a)) { + return true; + } + } + return false; + } + } + + /** + * Get an array of foreign keys from a particular table. + * + * @param string $tableName Table Name + * @return array + */ + protected function getForeignKeys($tableName) + { + $foreignKeys = array(); + $rows = $this->fetchAll(sprintf( + "SELECT + tc.constraint_name, + tc.table_name, kcu.column_name, + ccu.table_name AS referenced_table_name, + ccu.column_name AS referenced_column_name + FROM + information_schema.table_constraints AS tc + JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name + JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name + WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s' + ORDER BY kcu.ordinal_position", + $tableName + )); + foreach ($rows as $row) { + $foreignKeys[$row['constraint_name']]['table'] = $row['table_name']; + $foreignKeys[$row['constraint_name']]['columns'][] = $row['column_name']; + $foreignKeys[$row['constraint_name']]['referenced_table'] = $row['referenced_table_name']; + $foreignKeys[$row['constraint_name']]['referenced_columns'][] = $row['referenced_column_name']; + } + + return $foreignKeys; + } + + /** + * {@inheritdoc} + */ + public function addForeignKey(Table $table, ForeignKey $foreignKey) + { + $this->startCommandTimer(); + $this->writeCommand('addForeignKey', array($table->getName(), $foreignKey->getColumns())); + $this->execute( + sprintf( + 'ALTER TABLE %s ADD %s', + $this->quoteTableName($table->getName()), + $this->getForeignKeySqlDefinition($foreignKey, $table->getName()) + ) + ); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function dropForeignKey($tableName, $columns, $constraint = null) + { + $this->startCommandTimer(); + if (is_string($columns)) { + $columns = array($columns); // str to array + } + + $this->writeCommand('dropForeignKey', array($tableName, $columns)); + + if ($constraint) { + $this->execute( + sprintf( + 'ALTER TABLE %s DROP CONSTRAINT %s', + $this->quoteTableName($tableName), + $constraint + ) + ); + $this->endCommandTimer(); + return; + } else { + foreach ($columns as $column) { + $rows = $this->fetchAll(sprintf( + "SELECT + tc.constraint_name, + tc.table_name, kcu.column_name, + ccu.table_name AS referenced_table_name, + ccu.column_name AS referenced_column_name + FROM + information_schema.table_constraints AS tc + JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name + JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name + WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s' and ccu.column_name='%s' + ORDER BY kcu.ordinal_position", + $tableName, + $column + )); + foreach ($rows as $row) { + $this->dropForeignKey($tableName, $columns, $row['constraint_name']); + } + } + } + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function getSqlType($type, $limit = null) + { + switch ($type) { + case static::PHINX_TYPE_STRING: + return array('name' => 'nvarchar', 'limit' => 255); + break; + case static::PHINX_TYPE_CHAR: + return array('name' => 'nchar', 'limit' => 255); + break; + case static::PHINX_TYPE_TEXT: + return array('name' => 'ntext'); + break; + case static::PHINX_TYPE_INTEGER: + return array('name' => 'int'); + break; + case static::PHINX_TYPE_BIG_INTEGER: + return array('name' => 'bigint'); + break; + case static::PHINX_TYPE_FLOAT: + return array('name' => 'float'); + break; + case static::PHINX_TYPE_DECIMAL: + return array('name' => 'decimal'); + break; + case static::PHINX_TYPE_DATETIME: + case static::PHINX_TYPE_TIMESTAMP: + return array('name' => 'datetime'); + break; + case static::PHINX_TYPE_TIME: + return array('name' => 'time'); + break; + case static::PHINX_TYPE_DATE: + return array('name' => 'date'); + break; + case static::PHINX_TYPE_BLOB: + case static::PHINX_TYPE_BINARY: + return array('name' => 'varbinary'); + break; + case static::PHINX_TYPE_BOOLEAN: + return array('name' => 'bit'); + break; + case static::PHINX_TYPE_UUID: + return array('name' => 'uniqueidentifier'); + case static::PHINX_TYPE_FILESTREAM: + return array('name' => 'varbinary', 'limit' => 'max'); + // Geospatial database types + case static::PHINX_TYPE_GEOMETRY: + case static::PHINX_TYPE_POINT: + case static::PHINX_TYPE_LINESTRING: + case static::PHINX_TYPE_POLYGON: + // SQL Server stores all spatial data using a single data type. + // Specific types (point, polygon, etc) are set at insert time. + return array('name' => 'geography'); + break; + default: + throw new \RuntimeException('The type: "' . $type . '" is not supported.'); + } + } + + /** + * Returns Phinx type by SQL type + * + * @param $sqlTypeDef + * @throws \RuntimeException + * @internal param string $sqlType SQL type + * @returns string Phinx type + */ + public function getPhinxType($sqlType) + { + switch ($sqlType) { + case 'nvarchar': + case 'varchar': + return static::PHINX_TYPE_STRING; + case 'char': + case 'nchar': + return static::PHINX_TYPE_CHAR; + case 'text': + case 'ntext': + return static::PHINX_TYPE_TEXT; + case 'int': + case 'integer': + return static::PHINX_TYPE_INTEGER; + case 'decimal': + case 'numeric': + case 'money': + return static::PHINX_TYPE_DECIMAL; + case 'bigint': + return static::PHINX_TYPE_BIG_INTEGER; + case 'real': + case 'float': + return static::PHINX_TYPE_FLOAT; + case 'binary': + case 'image': + case 'varbinary': + return static::PHINX_TYPE_BINARY; + break; + case 'time': + return static::PHINX_TYPE_TIME; + case 'date': + return static::PHINX_TYPE_DATE; + case 'datetime': + case 'timestamp': + return static::PHINX_TYPE_DATETIME; + case 'bit': + return static::PHINX_TYPE_BOOLEAN; + case 'uniqueidentifier': + return static::PHINX_TYPE_UUID; + case 'filestream': + return static::PHINX_TYPE_FILESTREAM; + default: + throw new \RuntimeException('The SqlServer type: "' . $sqlType . '" is not supported'); + } + } + + /** + * {@inheritdoc} + */ + public function createDatabase($name, $options = array()) + { + $this->startCommandTimer(); + $this->writeCommand('createDatabase', array($name)); + + if (isset($options['collation'])) { + $this->execute(sprintf('CREATE DATABASE [%s] COLLATE [%s]', $name, $options['collation'])); + } else { + $this->execute(sprintf('CREATE DATABASE [%s]', $name)); + } + $this->execute(sprintf('USE [%s]', $name)); + $this->endCommandTimer(); + } + + /** + * {@inheritdoc} + */ + public function hasDatabase($name) + { + $result = $this->fetchRow( + sprintf( + 'SELECT count(*) as [count] FROM master.dbo.sysdatabases WHERE [name] = \'%s\'', + $name + ) + ); + + return $result['count'] > 0; + } + + /** + * {@inheritdoc} + */ + public function dropDatabase($name) + { + $this->startCommandTimer(); + $this->writeCommand('dropDatabase', array($name)); + $sql = <<execute($sql); + $this->endCommandTimer(); + } + + /** + * Get the defintion for a `DEFAULT` statement. + * + * @param mixed $default + * @return string + */ + protected function getDefaultValueDefinition($default) + { + if (is_string($default) && 'CURRENT_TIMESTAMP' !== $default) { + $default = $this->getConnection()->quote($default); + } elseif (is_bool($default)) { + $default = $this->castToBool($default); + } + return isset($default) ? ' DEFAULT ' . $default : ''; + } + + /** + * Gets the SqlServer Column Definition for a Column object. + * + * @param Column $column Column + * @return string + */ + protected function getColumnSqlDefinition(Column $column, $create = true) + { + $buffer = array(); + + $sqlType = $this->getSqlType($column->getType()); + $buffer[] = strtoupper($sqlType['name']); + // integers cant have limits in SQlServer + $noLimits = array( + 'bigint', + 'int', + 'tinyint' + ); + if (!in_array($sqlType['name'], $noLimits) && ($column->getLimit() || isset($sqlType['limit']))) { + $buffer[] = sprintf('(%s)', $column->getLimit() ? $column->getLimit() : $sqlType['limit']); + } + if ($column->getPrecision() && $column->getScale()) { + $buffer[] = '(' . $column->getPrecision() . ',' . $column->getScale() . ')'; + } + + $properties = $column->getProperties(); + $buffer[] = $column->getType() === 'filestream' ? 'FILESTREAM' : ''; + $buffer[] = isset($properties['rowguidcol']) ? 'ROWGUIDCOL' : ''; + + $buffer[] = $column->isNull() ? 'NULL' : 'NOT NULL'; + + if ($create === true) { + if ($column->getDefault() === null && $column->isNull()) { + $buffer[] = ' DEFAULT NULL'; + } else { + $buffer[] = $this->getDefaultValueDefinition($column->getDefault()); + } + } + + if ($column->isIdentity()) { + $buffer[] = 'IDENTITY(1, 1)'; + } + + return implode(' ', $buffer); + } + + /** + * Gets the SqlServer Index Definition for an Index object. + * + * @param Index $index Index + * @return string + */ + protected function getIndexSqlDefinition(Index $index, $tableName) + { + if (is_string($index->getName())) { + $indexName = $index->getName(); + } else { + $columnNames = $index->getColumns(); + if (is_string($columnNames)) { + $columnNames = array($columnNames); + } + $indexName = sprintf('%s_%s', $tableName, implode('_', $columnNames)); + } + $def = sprintf( + "CREATE %s INDEX %s ON %s (%s);", + ($index->getType() === Index::UNIQUE ? 'UNIQUE' : ''), + $indexName, + $this->quoteTableName($tableName), + '[' . implode('],[', $index->getColumns()) . ']' + ); + + return $def; + } + + /** + * Gets the SqlServer Foreign Key Definition for an ForeignKey object. + * + * @param ForeignKey $foreignKey + * @return string + */ + protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, $tableName) + { + $def = ' CONSTRAINT "'; + $def .= $tableName . '_' . implode('_', $foreignKey->getColumns()); + $def .= '" FOREIGN KEY ("' . implode('", "', $foreignKey->getColumns()) . '")'; + $def .= " REFERENCES {$this->quoteTableName($foreignKey->getReferencedTable()->getName())} (\"" . implode('", "', $foreignKey->getReferencedColumns()) . '")'; + if ($foreignKey->getOnDelete()) { + $def .= " ON DELETE {$foreignKey->getOnDelete()}"; + } + if ($foreignKey->getOnUpdate()) { + $def .= " ON UPDATE {$foreignKey->getOnUpdate()}"; + } + + return $def; + } + + /** + * {@inheritdoc} + */ + public function getColumnTypes() + { + return array_merge(parent::getColumnTypes(), array('filestream')); + } +} diff --git a/src/DDLWrapper/Db/Adapter/TablePrefixAdapter.php b/src/DDLWrapper/Db/Adapter/TablePrefixAdapter.php new file mode 100644 index 0000000..28d99dd --- /dev/null +++ b/src/DDLWrapper/Db/Adapter/TablePrefixAdapter.php @@ -0,0 +1,272 @@ + + */ +class TablePrefixAdapter extends AdapterWrapper +{ + /** + * {@inheritdoc} + */ + public function getAdapterType() + { + return 'TablePrefixAdapter'; + } + + /** + * {@inheritdoc} + */ + public function hasTable($tableName) + { + $adapterTableName = $this->getAdapterTableName($tableName); + return parent::hasTable($adapterTableName); + } + + /** + * {@inheritdoc} + */ + public function createTable(Table $table) + { + $adapterTable = clone $table; + $adapterTableName = $this->getAdapterTableName($table->getName()); + $adapterTable->setName($adapterTableName); + + foreach ($adapterTable->getForeignKeys() as $fk) { + $adapterReferenceTable = $fk->getReferencedTable(); + $adapterReferenceTableName = $this->getAdapterTableName($adapterReferenceTable->getName()); + $adapterReferenceTable->setName($adapterReferenceTableName); + } + + return parent::createTable($adapterTable); + } + + /** + * {@inheritdoc} + */ + public function renameTable($tableName, $newTableName) + { + $adapterTableName = $this->getAdapterTableName($tableName); + $adapterNewTableName = $this->getAdapterTableName($newTableName); + return parent::renameTable($adapterTableName, $adapterNewTableName); + } + + /** + * {@inheritdoc} + */ + public function dropTable($tableName) + { + $adapterTableName = $this->getAdapterTableName($tableName); + return parent::dropTable($adapterTableName); + } + + /** + * {@inheritdoc} + */ + public function getColumns($tableName) + { + $adapterTableName = $this->getAdapterTableName($tableName); + return parent::getColumns($adapterTableName); + } + + /** + * {@inheritdoc} + */ + public function hasColumn($tableName, $columnName) + { + $adapterTableName = $this->getAdapterTableName($tableName); + return parent::hasColumn($adapterTableName, $columnName); + } + + /** + * {@inheritdoc} + */ + public function addColumn(Table $table, Column $column) + { + $adapterTable = clone $table; + $adapterTableName = $this->getAdapterTableName($table->getName()); + $adapterTable->setName($adapterTableName); + return parent::addColumn($adapterTable, $column); + } + + /** + * {@inheritdoc} + */ + public function renameColumn($tableName, $columnName, $newColumnName) + { + $adapterTableName = $this->getAdapterTableName($tableName); + return parent::renameColumn($adapterTableName, $columnName, $newColumnName); + } + + /** + * {@inheritdoc} + */ + public function changeColumn($tableName, $columnName, Column $newColumn) + { + $adapterTableName = $this->getAdapterTableName($tableName); + return parent::changeColumn($adapterTableName, $columnName, $newColumn); + } + + /** + * {@inheritdoc} + */ + public function dropColumn($tableName, $columnName) + { + $adapterTableName = $this->getAdapterTableName($tableName); + return parent::dropColumn($adapterTableName, $columnName); + } + + /** + * {@inheritdoc} + */ + public function hasIndex($tableName, $columns) + { + $adapterTableName = $this->getAdapterTableName($tableName); + return parent::hasIndex($adapterTableName, $columns); + } + + /** + * {@inheritdoc} + */ + public function hasIndexByName($tableName, $indexName) + { + $adapterTableName = $this->getAdapterTableName($tableName); + return parent::hasIndexByName($adapterTableName, $indexName); + } + + /** + * {@inheritdoc} + */ + public function addIndex(Table $table, Index $index) + { + $adapterTable = clone $table; + $adapterTableName = $this->getAdapterTableName($table->getName()); + $adapterTable->setName($adapterTableName); + return parent::addIndex($adapterTable, $index); + } + + /** + * {@inheritdoc} + */ + public function dropIndex($tableName, $columns, $options = array()) + { + $adapterTableName = $this->getAdapterTableName($tableName); + return parent::dropIndex($adapterTableName, $columns, $options); + } + + /** + * {@inheritdoc} + */ + public function dropIndexByName($tableName, $indexName) + { + $adapterTableName = $this->getAdapterTableName($tableName); + return parent::dropIndexByName($adapterTableName, $indexName); + } + + /** + * {@inheritdoc} + */ + public function hasForeignKey($tableName, $columns, $constraint = null) + { + $adapterTableName = $this->getAdapterTableName($tableName); + return parent::hasForeignKey($adapterTableName, $columns, $constraint); + } + + /** + * {@inheritdoc} + */ + public function addForeignKey(Table $table, ForeignKey $foreignKey) + { + $adapterTable = clone $table; + $adapterTableName = $this->getAdapterTableName($table->getName()); + $adapterTable->setName($adapterTableName); + return parent::addForeignKey($adapterTable, $foreignKey); + } + + /** + * {@inheritdoc} + */ + public function dropForeignKey($tableName, $columns, $constraint = null) + { + $adapterTableName = $this->getAdapterTableName($tableName); + return parent::dropForeignKey($adapterTableName, $columns, $constraint); + } + + /** + * {@inheritdoc} + */ + public function insert(Table $table, $row) + { + $adapterTable = clone $table; + $adapterTableName = $this->getAdapterTableName($table->getName()); + $adapterTable->setName($adapterTableName); + return parent::insert($adapterTable, $row); + } + + /** + * Gets the table prefix. + * + * @return string + */ + public function getPrefix() + { + return (string) $this->getOption('table_prefix'); + } + + /** + * Gets the table suffix. + * + * @return string + */ + public function getSuffix() + { + return (string) $this->getOption('table_suffix'); + } + + /** + * Applies the prefix and suffix to the table name. + * + * @param string $tableName + * @return string + */ + public function getAdapterTableName($tableName) + { + return $this->getPrefix() . $tableName . $this->getSuffix(); + } +} diff --git a/src/DDLWrapper/Db/Adapter/WrapperInterface.php b/src/DDLWrapper/Db/Adapter/WrapperInterface.php new file mode 100644 index 0000000..0040096 --- /dev/null +++ b/src/DDLWrapper/Db/Adapter/WrapperInterface.php @@ -0,0 +1,60 @@ + + */ +interface WrapperInterface +{ + /** + * Class constructor, must always wrap another adapter. + * + * @param AdapterInterface $adapter + */ + public function __construct(AdapterInterface $adapter); + + /** + * Sets the database adapter to proxy commands to. + * + * @param AdapterInterface $adapter + * @return AdapterInterface + */ + public function setAdapter(AdapterInterface $adapter); + + /** + * Gets the database adapter. + * + * @throws \RuntimeException if the adapter has not been set + * @return AdapterInterface + */ + public function getAdapter(); +} diff --git a/src/DDLWrapper/Db/Table.php b/src/DDLWrapper/Db/Table.php new file mode 100644 index 0000000..cdeee49 --- /dev/null +++ b/src/DDLWrapper/Db/Table.php @@ -0,0 +1,675 @@ +setName($name); + $this->setOptions($options); + + if (null !== $adapter) { + echo 'Adapter definido'; + $this->setAdapter($adapter); + } + } + + /** + * Sets the table name. + * + * @param string $name Table Name + * @return Table + */ + public function setName($name) + { + $this->name = $name; + return $this; + } + + /** + * Gets the table name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Sets the table options. + * + * @param array $options + * @return Table + */ + public function setOptions($options) + { + $this->options = $options; + return $this; + } + + /** + * Gets the table options. + * + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * Sets the database adapter. + * + * @param AdapterInterface $adapter Database Adapter + * @return Table + */ + public function setAdapter(AdapterInterface $adapter) + { + $this->adapter = $adapter; + return $this; + } + + /** + * Gets the database adapter. + * + * @return AdapterInterface + */ + public function getAdapter() + { + return $this->adapter; + } + + /** + * Does the table exist? + * + * @return boolean + */ + public function exists() + { + return $this->getAdapter()->hasTable($this->getName()); + } + + /** + * Drops the database table. + * + * @return void + */ + public function drop() + { + $this->getAdapter()->dropTable($this->getName()); + } + + /** + * Renames the database table. + * + * @param string $newTableName New Table Name + * @return Table + */ + public function rename($newTableName) + { + $this->getAdapter()->renameTable($this->getName(), $newTableName); + $this->setName($newTableName); + return $this; + } + + /** + * Sets an array of columns waiting to be committed. + * Use setPendingColumns + * + * @deprecated + * @param array $columns Columns + * @return Table + */ + public function setColumns($columns) + { + $this->setPendingColumns($columns); + } + + /** + * Gets an array of the table columns. + * + * @return Column[] + */ + public function getColumns() + { + return $this->getAdapter()->getColumns($this->getName()); + } + + /** + * Sets an array of columns waiting to be committed. + * + * @param array $columns Columns + * @return Table + */ + public function setPendingColumns($columns) + { + $this->columns = $columns; + return $this; + } + + /** + * Gets an array of columns waiting to be committed. + * + * @return Column[] + */ + public function getPendingColumns() + { + return $this->columns; + } + + /** + * Sets an array of columns waiting to be indexed. + * + * @param array $indexes Indexes + * @return Table + */ + public function setIndexes($indexes) + { + $this->indexes = $indexes; + return $this; + } + + /** + * Gets an array of indexes waiting to be committed. + * + * @return array + */ + public function getIndexes() + { + return $this->indexes; + } + + /** + * Sets an array of foreign keys waiting to be commited. + * + * @param ForeignKey[] $foreignKeys foreign keys + * @return Table + */ + public function setForeignKeys($foreignKeys) + { + $this->foreignKeys = $foreignKeys; + return $this; + } + + /** + * Gets an array of foreign keys waiting to be commited. + * + * @return array|ForeignKey[] + */ + public function getForeignKeys() + { + return $this->foreignKeys; + } + + /** + * Sets an array of data to be inserted. + * + * @param array $data Data + * @return Table + */ + public function setData($data) + { + $this->data = $data; + return $this; + } + + /** + * Gets the data waiting to be inserted. + * + * @return array + */ + public function getData() + { + return $this->data; + } + + /** + * Resets all of the pending table changes. + * + * @return void + */ + public function reset() + { + $this->setPendingColumns(array()); + $this->setIndexes(array()); + $this->setForeignKeys(array()); + $this->setData(array()); + } + + /** + * Add a table column. + * + * Type can be: string, text, integer, float, decimal, datetime, timestamp, + * time, date, binary, boolean. + * + * Valid options can be: limit, default, null, precision or scale. + * + * @param string|Column $columnName Column Name + * @param string $type Column Type + * @param array $options Column Options + * @throws \RuntimeException + * @throws \InvalidArgumentException + * @return Table + */ + public function addColumn($columnName, $type = null, $options = array()) + { + // we need an adapter set to add a column + if (null === $this->getAdapter()) { + throw new \RuntimeException('An adapter must be specified to add a column.'); + } + + // create a new column object if only strings were supplied + if (!$columnName instanceof Column) { + $column = new Column(); + $column->setName($columnName); + $column->setType($type); + $column->setOptions($options); // map options to column methods + } else { + $column = $columnName; + } + + // Delegate to Adapters to check column type + if (!$this->getAdapter()->isValidColumnType($column)) { + throw new \InvalidArgumentException(sprintf( + 'An invalid column type "%s" was specified for column "%s".', + $column->getType(), + $column->getName() + )); + } + + $this->columns[] = $column; + return $this; + } + + /** + * Remove a table column. + * + * @param string $columnName Column Name + * @return Table + */ + public function removeColumn($columnName) + { + $this->getAdapter()->dropColumn($this->getName(), $columnName); + return $this; + } + + /** + * Rename a table column. + * + * @param string $oldName Old Column Name + * @param string $newName New Column Name + * @return Table + */ + public function renameColumn($oldName, $newName) + { + $this->getAdapter()->renameColumn($this->getName(), $oldName, $newName); + return $this; + } + + /** + * Change a table column type. + * + * @param string $columnName Column Name + * @param string|Column $newColumnType New Column Type + * @param array $options Options + * @return Table + */ + public function changeColumn($columnName, $newColumnType, $options = array()) + { + // create a column object if one wasn't supplied + if (!$newColumnType instanceof Column) { + $newColumn = new Column(); + $newColumn->setType($newColumnType); + $newColumn->setOptions($options); + } else { + $newColumn = $newColumnType; + } + + // if the name was omitted use the existing column name + if (null === $newColumn->getName() || strlen($newColumn->getName()) === 0) { + $newColumn->setName($columnName); + } + + $this->getAdapter()->changeColumn($this->getName(), $columnName, $newColumn); + return $this; + } + + /** + * Checks to see if a column exists. + * + * @param string $columnName Column Name + * @param array $options Options + * @return boolean + */ + public function hasColumn($columnName, $options = array()) + { + return $this->getAdapter()->hasColumn($this->getName(), $columnName, $options); + } + + /** + * Add an index to a database table. + * + * In $options you can specific unique = true/false or name (index name). + * + * @param string|array|Index $columns Table Column(s) + * @param array $options Index Options + * @return Table + */ + public function addIndex($columns, $options = array()) + { + // create a new index object if strings or an array of strings were supplied + if (!$columns instanceof Index) { + $index = new Index(); + if (is_string($columns)) { + $columns = array($columns); // str to array + } + $index->setColumns($columns); + $index->setOptions($options); + } else { + $index = $columns; + } + + $this->indexes[] = $index; + return $this; + } + + /** + * Removes the given index from a table. + * + * @param array $columns Columns + * @param array $options Options + * @return Table + */ + public function removeIndex($columns, $options = array()) + { + $this->getAdapter()->dropIndex($this->getName(), $columns, $options); + return $this; + } + + /** + * Removes the given index identified by its name from a table. + * + * @param string $name Index name + * @return Table + */ + public function removeIndexByName($name) + { + $this->getAdapter()->dropIndexByName($this->getName(), $name); + return $this; + } + + /** + * Checks to see if an index exists. + * + * @param string|array $columns Columns + * @param array $options Options + * @return boolean + */ + public function hasIndex($columns, $options = array()) + { + return $this->getAdapter()->hasIndex($this->getName(), $columns, $options); + } + + /** + * Add a foreign key to a database table. + * + * In $options you can specify on_delete|on_delete = cascade|no_action .., + * on_update, constraint = constraint name. + * + * @param string|array $columns Columns + * @param string|Table $referencedTable Referenced Table + * @param string|array $referencedColumns Referenced Columns + * @param array $options Options + * @return Table + */ + public function addForeignKey($columns, $referencedTable, $referencedColumns = array('id'), $options = array()) + { + if (is_string($referencedColumns)) { + $referencedColumns = array($referencedColumns); // str to array + } + $fk = new ForeignKey(); + if ($referencedTable instanceof Table) { + $fk->setReferencedTable($referencedTable); + } else { + $fk->setReferencedTable(new Table($referencedTable, array(), $this->adapter)); + } + $fk->setColumns($columns) + ->setReferencedColumns($referencedColumns) + ->setOptions($options); + $this->foreignKeys[] = $fk; + + return $this; + } + + /** + * Removes the given foreign key from the table. + * + * @param string|array $columns Column(s) + * @param null|string $constraint Constraint names + * @return Table + */ + public function dropForeignKey($columns, $constraint = null) + { + if (is_string($columns)) { + $columns = array($columns); + } + if ($constraint) { + $this->getAdapter()->dropForeignKey($this->getName(), array(), $constraint); + } else { + $this->getAdapter()->dropForeignKey($this->getName(), $columns); + } + + return $this; + } + + /** + * Checks to see if a foreign key exists. + * + * @param string|array $columns Column(s) + * @param null|string $constraint Constraint names + * @return boolean + */ + public function hasForeignKey($columns, $constraint = null) + { + return $this->getAdapter()->hasForeignKey($this->getName(), $columns, $constraint); + } + + /** + * Add timestamp columns created_at and updated_at to the table. + * + * @param string $createdAtColumnName + * @param string $updatedAtColumnName + * + * @return Table + */ + public function addTimestamps($createdAtColumnName = 'created_at', $updatedAtColumnName = 'updated_at') + { + $createdAtColumnName = is_null($createdAtColumnName) ? 'created_at' : $createdAtColumnName; + $updatedAtColumnName = is_null($updatedAtColumnName) ? 'updated_at' : $updatedAtColumnName; + $this->addColumn($createdAtColumnName, 'timestamp', array( + 'default' => 'CURRENT_TIMESTAMP', + 'update' => '' + )) + ->addColumn($updatedAtColumnName, 'timestamp', array( + 'null' => true, + 'default' => null + )); + + return $this; + } + + /** + * Insert data into the table. + * + * @param $data array of data in the form: + * array( + * array("col1" => "value1", "col2" => "anotherValue1"), + * array("col2" => "value2", "col2" => "anotherValue2"), + * ) + * or array("col1" => "value1", "col2" => "anotherValue1") + * + * @return Table + */ + public function insert($data) + { + // handle array of array situations + if (isset($data[0]) && is_array($data[0])) { + foreach ($data as $row) { + $this->data[] = $row; + } + return $this; + } + $this->data[] = $data; + return $this; + } + + /** + * Creates a table from the object instance. + * + * @return void + */ + public function create() + { + $this->getAdapter()->createTable($this); + $this->saveData(); + $this->reset(); // reset pending changes + } + + /** + * Updates a table from the object instance. + * + * @throws \RuntimeException + * @return void + */ + public function update() + { + if (!$this->exists()) { + throw new \RuntimeException('Cannot update a table that doesn\'t exist!'); + } + + // update table + foreach ($this->getPendingColumns() as $column) { + $this->getAdapter()->addColumn($this, $column); + } + + foreach ($this->getIndexes() as $index) { + $this->getAdapter()->addIndex($this, $index); + } + + foreach ($this->getForeignKeys() as $foreignKey) { + $this->getAdapter()->addForeignKey($this, $foreignKey); + } + + $this->saveData(); + $this->reset(); // reset pending changes + } + + /** + * Commit the pending data waiting for insertion. + * + * @return void + */ + public function saveData() + { + foreach ($this->getData() as $row) { + $this->getAdapter()->insert($this, $row); + } + } + + /** + * Commits the table changes. + * + * If the table doesn't exist it is created otherwise it is updated. + * + * @return void + */ + public function save() + { + if ($this->exists()) { + $this->update(); // update the table + } else { + $this->create(); // create the table + } + + $this->reset(); // reset pending changes + } +} diff --git a/src/DDLWrapper/Db/Table/Column.php b/src/DDLWrapper/Db/Table/Column.php new file mode 100644 index 0000000..0a94101 --- /dev/null +++ b/src/DDLWrapper/Db/Table/Column.php @@ -0,0 +1,550 @@ +name = $name; + return $this; + } + + /** + * Gets the column name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Sets the column type. + * + * @param string $type + * @return Column + */ + public function setType($type) + { + $this->type = $type; + return $this; + } + + /** + * Gets the column type. + * + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Sets the column limit. + * + * @param integer $limit + * @return Column + */ + public function setLimit($limit) + { + $this->limit = $limit; + return $this; + } + + /** + * Gets the column limit. + * + * @return integer + */ + public function getLimit() + { + return $this->limit; + } + + /** + * Sets whether the column allows nulls. + * + * @param boolean $null + * @return Column + */ + public function setNull($null) + { + $this->null = (bool) $null; + return $this; + } + + /** + * Gets whether the column allows nulls. + * + * @return boolean + */ + public function getNull() + { + return $this->null; + } + + /** + * Does the column allow nulls? + * + * @return boolean + */ + public function isNull() + { + return $this->getNull(); + } + + /** + * Sets the default column value. + * + * @param mixed $default + * @return Column + */ + public function setDefault($default) + { + $this->default = $default; + return $this; + } + + /** + * Gets the default column value. + * + * @return mixed + */ + public function getDefault() + { + return $this->default; + } + + /** + * Sets whether or not the column is an identity column. + * + * @param boolean $identity + * @return Column + */ + public function setIdentity($identity) + { + $this->identity = $identity; + return $this; + } + + /** + * Gets whether or not the column is an identity column. + * + * @return boolean + */ + public function getIdentity() + { + return $this->identity; + } + + /** + * Is the column an identity column? + * + * @return boolean + */ + public function isIdentity() + { + return $this->getIdentity(); + } + + /** + * Sets the name of the column to add this column after. + * + * @param string $after After + * @return Column + */ + public function setAfter($after) + { + $this->after = $after; + return $this; + } + + /** + * Returns the name of the column to add this column after. + * + * @return string + */ + public function getAfter() + { + return $this->after; + } + + /** + * Sets the 'ON UPDATE' mysql column function. + * + * @param string $update On Update function + * @return Column + */ + public function setUpdate($update) + { + $this->update = $update; + return $this; + } + + /** + * Returns the value of the ON UPDATE column function. + * + * @return string + */ + public function getUpdate() + { + return $this->update; + } + + /** + * Sets the column precision for decimal. + * + * @param integer $precision + * @return Column + */ + public function setPrecision($precision) + { + $this->precision = $precision; + return $this; + } + + /** + * Gets the column precision for decimal. + * + * @return integer + */ + public function getPrecision() + { + return $this->precision; + } + + /** + * Sets the column scale for decimal. + * + * @param integer $scale + * @return Column + */ + public function setScale($scale) + { + $this->scale = $scale; + return $this; + } + + /** + * Gets the column scale for decimal. + * + * @return integer + */ + public function getScale() + { + return $this->scale; + } + + /** + * Sets the column comment. + * + * @param string $comment + * @return Column + */ + public function setComment($comment) + { + $this->comment = $comment; + return $this; + } + + /** + * Gets the column comment. + * + * @return string + */ + public function getComment() + { + return $this->comment; + } + + /** + * Sets whether field should be signed. + * + * @param bool $signed + * @return Column + */ + public function setSigned($signed) + { + $this->signed = (bool) $signed; + return $this; + } + + /** + * Gets whether field should be signed. + * + * @return string + */ + public function getSigned() + { + return $this->signed; + } + + /** + * Should the column be signed? + * + * @return boolean + */ + public function isSigned() + { + return $this->getSigned(); + } + + /** + * Sets whether the field should have a timezone identifier. + * Used for date/time columns only! + * + * @param bool $timezone + * @return Column + */ + public function setTimezone($timezone) + { + $this->timezone = (bool) $timezone; + return $this; + } + + /** + * Gets whether field has a timezone identifier. + * + * @return boolean + */ + public function getTimezone() + { + return $this->timezone; + } + + /** + * Should the column have a timezone? + * + * @return boolean + */ + public function isTimezone() + { + return $this->getTimezone(); + } + + /** + * Sets field properties. + * + * @param array $properties + * + * @return Column + */ + public function setProperties($properties) + { + $this->properties = $properties; + return $this; + } + + /** + * Gets field properties + * + * @return array + */ + public function getProperties() + { + return $this->properties; + } + + /** + * Sets field values. + * + * @param mixed (array|string) $values + * + * @return Column + */ + public function setValues($values) + { + if (!is_array($values)) { + $values = preg_split('/,\s*/', $values); + } + $this->values = $values; + return $this; + } + + /** + * Gets field values + * + * @return string + */ + public function getValues() + { + return $this->values; + } + + /** + * Gets all allowed options. Each option must have a corresponding `setFoo` method. + * + * @return array + */ + protected function getValidOptions() + { + return array( + 'limit', + 'default', + 'null', + 'identity', + 'precision', + 'scale', + 'after', + 'update', + 'comment', + 'signed', + 'timezone', + 'properties', + 'values', + ); + } + + /** + * Gets all aliased options. Each alias must reference a valid option. + * + * @return array + */ + protected function getAliasedOptions() + { + return array( + 'length' => 'limit', + ); + } + + /** + * Utility method that maps an array of column options to this objects methods. + * + * @param array $options Options + * @return Column + */ + public function setOptions($options) + { + $validOptions = $this->getValidOptions(); + $aliasOptions = $this->getAliasedOptions(); + + foreach ($options as $option => $value) { + if (isset($aliasOptions[$option])) { + // proxy alias -> option + $option = $aliasOptions[$option]; + } + + if (!in_array($option, $validOptions, true)) { + throw new \RuntimeException(sprintf('"%s" is not a valid column option.', $option)); + } + + $method = 'set' . ucfirst($option); + $this->$method($value); + } + return $this; + } +} diff --git a/src/DDLWrapper/Db/Table/ForeignKey.php b/src/DDLWrapper/Db/Table/ForeignKey.php new file mode 100644 index 0000000..49e613b --- /dev/null +++ b/src/DDLWrapper/Db/Table/ForeignKey.php @@ -0,0 +1,252 @@ + + */ +namespace DDLWrapper\Db\Table; + +use DDLWrapper\Db\Table; + +class ForeignKey +{ + const CASCADE = 'CASCADE'; + const RESTRICT = 'RESTRICT'; + const SET_NULL = 'SET NULL'; + const NO_ACTION = 'NO ACTION'; + + /** + * @var array + */ + protected $columns = array(); + + /** + * @var Table + */ + protected $referencedTable; + + /** + * @var array + */ + protected $referencedColumns = array(); + + /** + * @var string + */ + protected $onDelete; + + /** + * @var string + */ + protected $onUpdate; + + /** + * @var string|boolean + */ + protected $constraint; + + /** + * Sets the foreign key columns. + * + * @param array|string $columns + * @return ForeignKey + */ + public function setColumns($columns) + { + if (is_string($columns)) { + $columns = array($columns); + } + $this->columns = $columns; + return $this; + } + + /** + * Gets the foreign key columns. + * + * @return array + */ + public function getColumns() + { + return $this->columns; + } + + /** + * Sets the foreign key referenced table. + * + * @param Table $table + * @return ForeignKey + */ + public function setReferencedTable(Table $table) + { + $this->referencedTable = $table; + return $this; + } + + /** + * Gets the foreign key referenced table. + * + * @return Table + */ + public function getReferencedTable() + { + return $this->referencedTable; + } + + /** + * Sets the foreign key referenced columns. + * + * @param array $referencedColumns + * @return ForeignKey + */ + public function setReferencedColumns(array $referencedColumns) + { + $this->referencedColumns = $referencedColumns; + return $this; + } + + /** + * Gets the foreign key referenced columns. + * + * @return array + */ + public function getReferencedColumns() + { + return $this->referencedColumns; + } + + /** + * Sets ON DELETE action for the foreign key. + * + * @param string $onDelete + * @return ForeignKey + */ + public function setOnDelete($onDelete) + { + $this->onDelete = $this->normalizeAction($onDelete); + return $this; + } + + /** + * Gets ON DELETE action for the foreign key. + * + * @return string + */ + public function getOnDelete() + { + return $this->onDelete; + } + + /** + * Gets ON UPDATE action for the foreign key. + * + * @return string + */ + public function getOnUpdate() + { + return $this->onUpdate; + } + + /** + * Sets ON UPDATE action for the foreign key. + * + * @param string $onUpdate + * @return ForeignKey + */ + public function setOnUpdate($onUpdate) + { + $this->onUpdate = $this->normalizeAction($onUpdate); + return $this; + } + + /** + * Sets constraint for the foreign key. + * + * @param string $constraint + * @return ForeignKey + */ + public function setConstraint($constraint) + { + $this->constraint = $constraint; + return $this; + } + + /** + * Gets constraint name for the foreign key. + * + * @return string + */ + public function getConstraint() + { + return $this->constraint; + } + + /** + * Utility method that maps an array of index options to this objects methods. + * + * @param array $options Options + * @throws \RuntimeException + * @throws \InvalidArgumentException + * @return ForeignKey + */ + public function setOptions($options) + { + // Valid Options + $validOptions = array('delete', 'update', 'constraint'); + foreach ($options as $option => $value) { + if (!in_array($option, $validOptions, true)) { + throw new \RuntimeException(sprintf('"%s" is not a valid foreign key option.', $option)); + } + + // handle $options['delete'] as $options['update'] + if ('delete' === $option) { + $this->setOnDelete($value); + } elseif ('update' === $option) { + $this->setOnUpdate($value); + } else { + $method = 'set' . ucfirst($option); + $this->$method($value); + } + } + + return $this; + } + + /** + * From passed value checks if it's correct and fixes if needed + * + * @param string $action + * @throws \InvalidArgumentException + * @return string + */ + protected function normalizeAction($action) + { + $constantName = 'static::' . str_replace(' ', '_', strtoupper(trim($action))); + if (!defined($constantName)) { + throw new \InvalidArgumentException('Unknown action passed: ' . $action); + } + return constant($constantName); + } +} diff --git a/src/DDLWrapper/Db/Table/Index.php b/src/DDLWrapper/Db/Table/Index.php new file mode 100644 index 0000000..8ab66cd --- /dev/null +++ b/src/DDLWrapper/Db/Table/Index.php @@ -0,0 +1,185 @@ +columns = $columns; + return $this; + } + + /** + * Gets the index columns. + * + * @return array + */ + public function getColumns() + { + return $this->columns; + } + + /** + * Sets the index type. + * + * @param string $type + * @return Index + */ + public function setType($type) + { + $this->type = $type; + return $this; + } + + /** + * Gets the index type. + * + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Sets the index name. + * + * @param string $name + * @return Index + */ + public function setName($name) + { + $this->name = $name; + return $this; + } + + /** + * Gets the index name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Sets the index limit. + * + * @param integer $limit + * @return Index + */ + public function setLimit($limit) + { + $this->limit = $limit; + return $this; + } + + /** + * Gets the index limit. + * + * @return integer + */ + public function getLimit() + { + return $this->limit; + } + + /** + * Utility method that maps an array of index options to this objects methods. + * + * @param array $options Options + * @throws \RuntimeException + * @return Index + */ + public function setOptions($options) + { + // Valid Options + $validOptions = array('type', 'unique', 'name', 'limit'); + foreach ($options as $option => $value) { + if (!in_array($option, $validOptions, true)) { + throw new \RuntimeException(sprintf('"%s" is not a valid index option.', $option)); + } + + // handle $options['unique'] + if (strcasecmp($option, self::UNIQUE) === 0) { + if ((bool) $value) { + $this->setType(self::UNIQUE); + } + continue; + } + + $method = 'set' . ucfirst($option); + $this->$method($value); + } + return $this; + } +} diff --git a/src/DDLWrapper/Wrapper.php b/src/DDLWrapper/Wrapper.php new file mode 100644 index 0000000..5925847 --- /dev/null +++ b/src/DDLWrapper/Wrapper.php @@ -0,0 +1,66 @@ +driver){ + case 'sqlite': + $instance->_adapter = new SQLiteAdapter(); + $instance-> _adapter->setConnection($driver); + break; + case 'pgsql': + break; + case 'mysql': + break; + + default: + break; + } + } + + public static function get_table($name){ + $instance = self::getInstance(); + return new Table($name, Array(), $instance->_adapter); + } + + public static function begin(){ + $instance = self::getInstance(); + $instance->_adapter->beginTransaction(); + } + public static function commit(){ + $instance = self::getInstance(); + $instance->_adapter->commitTransaction(); + } +} \ No newline at end of file