namespace Yoast\WP\Lib\Migrations;
* Yoast migrations table class.
* The SQL representation of this table.
* Whether or not the table has been initialized.
private $initialized = false;
private $primary_keys = [];
* Whether or not to auto generate the id.
private $auto_generate_id = true;
* Creates an instance of the Adapter.
* @param Adapter $adapter The current adapter.
* @param string $name The table name.
* @param array $options The options.
* @throws Exception If invalid arguments are passed.
public function __construct( $adapter, $name, $options = [] ) {
if ( ! $adapter instanceof Adapter ) {
throw new Exception( 'Invalid MySQL Adapter instance.' );
throw new Exception( "Invalid 'name' parameter" );
$this->adapter = $adapter;
$this->options = $options;
$this->init_sql( $name, $options );
if ( \array_key_exists( 'id', $options ) ) {
if ( \is_bool( $options['id'] ) && $options['id'] === false ) {
$this->auto_generate_id = false;
// If its a string then we want to auto-generate an integer-based
// primary key with this name.
if ( \is_string( $options['id'] ) ) {
$this->auto_generate_id = true;
$this->primary_keys[] = $options['id'];
* @param string $column_name The column name.
* @param string $type The column type.
* @param array $options The options.
public function column( $column_name, $type, $options = [] ) {
// If there is already a column by the same name then silently fail and continue.
foreach ( $this->columns as $column ) {
if ( $column->name === $column_name ) {
if ( \array_key_exists( 'primary_key', $options ) ) {
if ( $options['primary_key'] ) {
$this->primary_keys[] = $column_name;
if ( \array_key_exists( 'auto_increment', $options ) ) {
if ( $options['auto_increment'] ) {
$column_options['auto_increment'] = true;
$column_options = \array_merge( $column_options, $options );
$column = new Column( $this->adapter, $column_name, $type, $column_options );
$this->columns[] = $column;
* Shortcut to create timestamps columns (default created_at, updated_at)
* @param string $created_column_name Created at column name.
* @param string $updated_column_name Updated at column name.
public function timestamps( $created_column_name = 'created_at', $updated_column_name = 'updated_at' ) {
$this->column( $created_column_name, 'datetime' );
'default' => 'CURRENT_TIMESTAMP',
'extra' => 'ON UPDATE CURRENT_TIMESTAMP',
private function keys() {
if ( \count( $this->primary_keys ) > 0 ) {
$lead = ' PRIMARY KEY (';
foreach ( $this->primary_keys as $key ) {
$quoted[] = \sprintf( '%s', $this->adapter->identifier( $key ) );
$primary_key_sql = ",\n" . $lead . \implode( ',', $quoted ) . ')';
* @param bool $wants_sql Whether or not to return SQL or execute the query. Defaults to false.
* @throws Exception If the table definition has not been intialized.
public function finish( $wants_sql = false ) {
if ( ! $this->initialized ) {
throw new Exception( \sprintf( "Table Definition: '%s' has not been initialized", $this->name ) );
if ( \is_array( $this->options ) && \array_key_exists( 'options', $this->options ) ) {
$opt_str = $this->options['options'];
if ( isset( $this->adapter->db_info['charset'] ) ) {
$opt_str = ' DEFAULT CHARSET=' . $this->adapter->db_info['charset'];
$opt_str = ' DEFAULT CHARSET=utf8';
$close_sql = \sprintf( ') %s;', $opt_str );
$create_table_sql = $this->sql;
if ( $this->auto_generate_id === true ) {
$this->primary_keys[] = 'id';
$primary_id = new Column(
'auto_increment' => true,
$create_table_sql .= $primary_id->to_sql() . ",\n";
$create_table_sql .= $this->columns_to_str();
$create_table_sql .= $this->keys() . $close_sql;
return $create_table_sql;
return $this->adapter->execute_ddl( $create_table_sql );
* Get SQL for all columns.
* @return string The SQL.
private function columns_to_str() {
$len = \count( $this->columns );
for ( $i = 0; $i < $len; $i++ ) {
$c = $this->columns[ $i ];
$fields[] = $c->__toString();
return \implode( ",\n", $fields );
* Init create sql statement.
* @param string $name The name.
* @param array $options The options.
private function init_sql( $name, $options ) {
// Are we forcing table creation? If so, drop it first.
if ( \array_key_exists( 'force', $options ) && $options['force'] === true ) {
$this->adapter->drop_table( $name );
if ( \array_key_exists( 'temporary', $options ) ) {
$create_sql = \sprintf( 'CREATE%s TABLE ', $temp );
$create_sql .= \sprintf( "%s (\n", $this->adapter->identifier( $name ) );
$this->sql .= $create_sql;
$this->initialized = true;