: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* @param string $table_name The table name.
* @param string $column_name The column name.
* @param string $type The column type.
* @param array $options Column options.
public function change_column( $table_name, $column_name, $type, $options = [] ) {
if ( empty( $table_name ) || empty( $column_name ) || empty( $type ) ) {
$column_info = $this->column_info( $table_name, $column_name );
if ( ! \array_key_exists( 'limit', $options ) ) {
$options['limit'] = null;
if ( ! \array_key_exists( 'precision', $options ) ) {
$options['precision'] = null;
if ( ! \array_key_exists( 'scale', $options ) ) {
$options['scale'] = null;
$sql = \sprintf( 'ALTER TABLE `%s` CHANGE `%s` `%s` %s', $table_name, $column_name, $column_name, $this->type_to_sql( $type, $options ) );
$sql .= $this->add_column_options( $type, $options );
return $this->execute_ddl( $sql );
* Returns the database information for a column.
* @param string $table The table name.
* @param string $column The column name.
public function column_info( $table, $column ) {
if ( empty( $table ) || empty( $column ) ) {
$sql = \sprintf( "SHOW FULL COLUMNS FROM %s LIKE '%s'", $this->identifier( $table ), $column );
$result = $this->select_one( $sql );
if ( \is_array( $result ) ) {
$result = \array_change_key_case( $result, \CASE_LOWER );
} catch ( Exception $e ) {
* @param string $table_name The table name.
* @param array|string $column_name The column name(s).
* @param array $options Index options.
public function add_index( $table_name, $column_name, $options = [] ) {
if ( empty( $table_name ) || empty( $column_name ) ) {
if ( \is_array( $options ) && \array_key_exists( 'unique', $options ) && $options['unique'] === true ) {
// Did the user specify an index name?
if ( \is_array( $options ) && \array_key_exists( 'name', $options ) ) {
$index_name = $options['name'];
$index_name = $this->get_index_name( $table_name, $column_name );
if ( \strlen( $index_name ) > Constants::MYSQL_MAX_IDENTIFIER_LENGTH ) {
if ( ! \is_array( $column_name ) ) {
$column_names = [ $column_name ];
$column_names = $column_name;
foreach ( $column_names as $name ) {
$cols[] = $this->identifier( $name );
'CREATE %sINDEX %s ON %s(%s)',
( $unique === true ) ? 'UNIQUE ' : '',
$this->identifier( $index_name ),
$this->identifier( $table_name ),
return $this->execute_ddl( $sql );
* @param string $table_name The table name.
* @param array|string $column_name The column name(s).
* @param array $options Index options.
public function remove_index( $table_name, $column_name, $options = [] ) {
if ( empty( $table_name ) || empty( $column_name ) ) {
// Did the user specify an index name?
if ( \is_array( $options ) && \array_key_exists( 'name', $options ) ) {
$index_name = $options['name'];
$index_name = $this->get_index_name( $table_name, $column_name );
$sql = \sprintf( 'DROP INDEX %s ON %s', $this->identifier( $index_name ), $this->identifier( $table_name ) );
return $this->execute_ddl( $sql );
* @param string $table_name The table name.
* @param string $created_column_name Created at column name.
* @param string $updated_column_name Updated at column name.
public function add_timestamps( $table_name, $created_column_name, $updated_column_name ) {
if ( empty( $table_name ) || empty( $created_column_name ) || empty( $updated_column_name ) ) {
$created_at = $this->add_column( $table_name, $created_column_name, 'datetime' );
$updated_at = $this->add_column(
'default' => 'CURRENT_TIMESTAMP',
'extra' => 'ON UPDATE CURRENT_TIMESTAMP',
return $created_at && $updated_at;
* @param string $table_name The table name.
* @param string $created_column_name Created at column name.
* @param string $updated_column_name Updated at column name.
* @return bool Whether or not the timestamps were removed.
public function remove_timestamps( $table_name, $created_column_name, $updated_column_name ) {
if ( empty( $table_name ) || empty( $created_column_name ) || empty( $updated_column_name ) ) {
$updated_at = $this->remove_column( $table_name, $created_column_name );
$created_at = $this->remove_column( $table_name, $updated_column_name );
return $created_at && $updated_at;
* @param string $table_name The table name.
* @param array|string $column_name The column name(s).
* @param array $options Index options.
* @return bool Whether or not the index exists.
public function has_index( $table_name, $column_name, $options = [] ) {
if ( empty( $table_name ) || empty( $column_name ) ) {
// Did the user specify an index name?
if ( \is_array( $options ) && \array_key_exists( 'name', $options ) ) {
$index_name = $options['name'];
$index_name = $this->get_index_name( $table_name, $column_name );
$indexes = $this->indexes( $table_name );
foreach ( $indexes as $idx ) {
if ( $idx['name'] === $index_name ) {
* Returns all indexes of a table.
* @param string $table_name The table name.
public function indexes( $table_name ) {
$sql = \sprintf( 'SHOW KEYS FROM %s', $this->identifier( $table_name ) );
$result = $this->select_all( $sql );
foreach ( $result as $row ) {
if ( $row['Key_name'] === 'PRIMARY' ) {
'name' => $row['Key_name'],
'unique' => (int) $row['Non_unique'] === 0,
* Converts a type to sql. Default options:
* $limit = null, $precision = null, $scale = null
* @param string $type The native type.
* @param array $options The options.
* @return string The SQL type.
* @throws Exception If invalid arguments are supplied.
public function type_to_sql( $type, $options = [] ) {
$natives = $this->native_database_types();
if ( ! \array_key_exists( $type, $natives ) ) {
$error = \sprintf( "Error:I dont know what column type of '%s' maps to for MySQL.", $type );
$error .= "\nYou provided: {$type}\n";
$error .= "Valid types are: \n";
$types = \array_keys( $natives );
foreach ( $types as $t ) {
if ( $t === 'primary_key' ) {
throw new Exception( $error );
if ( isset( $options['precision'] ) ) {
$precision = $options['precision'];
if ( isset( $options['scale'] ) ) {
$scale = $options['scale'];
if ( isset( $options['limit'] ) ) {
$limit = $options['limit'];
if ( isset( $options['values'] ) ) {
$values = $options['values'];
$native_type = $natives[ $type ];
if ( \is_array( $native_type ) && \array_key_exists( 'name', $native_type ) ) {
$column_type_sql = $native_type['name'];
if ( $type === 'decimal' || $type === 'float' ) {
// Ignore limit, use precison and scale.
if ( $precision === null && \array_key_exists( 'precision', $native_type ) ) {
$precision = $native_type['precision'];
if ( $scale === null && \array_key_exists( 'scale', $native_type ) ) {
$scale = $native_type['scale'];
if ( $precision !== null ) {
if ( \is_int( $scale ) ) {
$column_type_sql .= \sprintf( '(%d, %d)', $precision, $scale );
$column_type_sql .= \sprintf( '(%d)', $precision );
throw new Exception( "Error adding $type column: precision cannot be empty if scale is specified" );
elseif ( $type === 'enum' ) {
if ( empty( $values ) ) {
throw new Exception( 'Error adding enum column: there must be at least one value defined' );
$column_type_sql .= \sprintf(
\implode( "','", \array_map( [ $this, 'quote_string' ], $values ) )
if ( $limit === null && \array_key_exists( 'limit', $native_type ) ) {
$limit = $native_type['limit'];
$column_type_sql .= \sprintf( '(%d)', $limit );
* @param string $type The native type.
* @param array $options The options.
* @return string The SQL statement for the column options.
* @throws Exception If invalid arguments are supplied.
public function add_column_options( $type, $options ) {
if ( ! \is_array( $options ) ) {
if ( \array_key_exists( 'unsigned', $options ) && $options['unsigned'] === true ) {
if ( \array_key_exists( 'character', $options ) ) {
$sql .= \sprintf( ' CHARACTER SET %s', $this->identifier( $options['character'] ) );
if ( \array_key_exists( 'collate', $options ) ) {
$sql .= \sprintf( ' COLLATE %s', $this->identifier( $options['collate'] ) );
if ( \array_key_exists( 'auto_increment', $options ) && $options['auto_increment'] === true ) {
$sql .= ' auto_increment';
if ( \array_key_exists( 'default', $options ) && $options['default'] !== null ) {
if ( $this->is_sql_method_call( $options['default'] ) ) {
throw new Exception( 'MySQL does not support function calls as default values, constants only.' );
if ( \is_int( $options['default'] ) ) {
elseif ( \is_bool( $options['default'] ) ) {
$default_format = "'%d'";
elseif ( $options['default'] === 'CURRENT_TIMESTAMP' ) {
$default_format = "'%s'";
$default_value = \sprintf( $default_format, $options['default'] );
$sql .= \sprintf( ' DEFAULT %s', $default_value );
if ( \array_key_exists( 'null', $options ) ) {
if ( $options['null'] === false || $options['null'] === 'NO' ) {
elseif ( $type === 'timestamp' ) {
if ( \array_key_exists( 'comment', $options ) ) {
$sql .= \sprintf( " COMMENT '%s'", $this->quote_string( $options['comment'] ) );
if ( \array_key_exists( 'extra', $options ) ) {
$sql .= \sprintf( ' %s', $this->quote_string( $options['extra'] ) );
if ( \array_key_exists( 'after', $options ) ) {
$sql .= \sprintf( ' AFTER %s', $this->identifier( $options['after'] ) );
* Returns a list of all versions that have been migrated.
* @return string[] The version numbers that have been migrated.
public function get_migrated_versions() {
$result = $this->select_all( \sprintf( 'SELECT version FROM %s', $this->get_schema_version_table_name() ) );
return \array_column( $result, 'version' );
* Adds a migrated version.
* @param string $version The version.
* @return bool Whether or not the version was succesfully set.
public function add_version( $version ) {
$sql = \sprintf( "INSERT INTO %s (version) VALUES ('%s')", $this->get_schema_version_table_name(), $version );
return $this->execute_ddl( $sql );
* Removes a migrated version.
* @param string $version The version.
* @return bool Whether or not the version was succesfully removed.
public function remove_version( $version ) {
$sql = \sprintf( "DELETE FROM %s WHERE version = '%s'", $this->get_schema_version_table_name(), $version );
return $this->execute_ddl( $sql );
* Returns a message displaying the current version
public function __toString() {
return self::class . ', version ' . $this->version;
* @param string $table_name The table name.
* @param string $column_name The column name.
* @return string The index name.
private function get_index_name( $table_name, $column_name ) {
$name = \preg_replace( '/\\W/', '_', $table_name );
$name = \preg_replace( '/\\_{2,}/', '_', $name );
// If the column parameter is an array then the user wants to create a multi-column index.
if ( \is_array( $column_name ) ) {
$column_str = \implode( '_and_', $column_name );
$column_str = $column_name;
$name .= \sprintf( '_%s', $column_str );
* Returns the type of a query.
* @param string $query The query to run.
* @return int The query type.
private function determine_query_type( $query ) {
$query = \strtolower( \trim( $query ) );
\preg_match( '/^(\\w)*/i', $query, $match );
return Constants::SQL_SELECT;
return Constants::SQL_UPDATE;
return Constants::SQL_DELETE;
return Constants::SQL_INSERT;
return Constants::SQL_ALTER;
return Constants::SQL_DROP;
return Constants::SQL_CREATE;
return Constants::SQL_SHOW;
return Constants::SQL_RENAME;
return Constants::SQL_SET;
return Constants::SQL_UNKNOWN_QUERY_TYPE;