@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A model to manage database changes
|
||||
*
|
||||
* @author Time.ly Network Inc.
|
||||
* @since 2.0
|
||||
*
|
||||
* @package AI1EC
|
||||
* @subpackage AI1EC.Database
|
||||
*/
|
||||
class Ai1ec_Database_Applicator extends Ai1ec_Base {
|
||||
|
||||
/**
|
||||
* @var Ai1ec_Dbi Instance of wpdb object
|
||||
*/
|
||||
protected $_db = NULL;
|
||||
|
||||
/**
|
||||
* @var Ai1ec_Database Instance of Ai1ec_Database object
|
||||
*/
|
||||
protected $_database = NULL;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* Initialize object, by storing instance of `wpdb` in local variable
|
||||
*
|
||||
* @return void Constructor does not return
|
||||
*/
|
||||
public function __construct( Ai1ec_Registry_Object $registry ) {
|
||||
parent::__construct( $registry );
|
||||
$this->_db = $registry->get( 'dbi.dbi' );
|
||||
$this->_database = $registry->get( 'database.helper' );
|
||||
}
|
||||
|
||||
/**
|
||||
* remove_instance_duplicates method
|
||||
*
|
||||
* Remove duplicate instances, from `event_instances` table
|
||||
*
|
||||
* @param int $depth Private argument, denoting number of iterations to
|
||||
* try, before reverting to slow approach
|
||||
*
|
||||
* @return bool Success
|
||||
*/
|
||||
public function remove_instance_duplicates( $depth = 5 ) {
|
||||
$use_field = 'id';
|
||||
if ( $depth < 0 ) {
|
||||
$use_field = 'post_id';
|
||||
}
|
||||
$table = $this->_table( 'event_instances' );
|
||||
if ( false === $this->_database->table_exists( $table ) ) {
|
||||
return true;
|
||||
}
|
||||
$duplicates = $this->find_duplicates(
|
||||
$table,
|
||||
$use_field,
|
||||
array( 'post_id', 'start' )
|
||||
);
|
||||
$count = count( $duplicates );
|
||||
if ( $count > 0 ) {
|
||||
$sql_query = 'DELETE FROM ' . $table .
|
||||
' WHERE ' . $use_field . ' IN ( ' .
|
||||
implode( ', ', $duplicates ) . ' )';
|
||||
$this->_db->query( $sql_query );
|
||||
}
|
||||
if ( 'post_id' === $use_field ) { // slow branch
|
||||
$event_instance_model = $this->_registry->get(
|
||||
'model.event.instance'
|
||||
);
|
||||
foreach ( $duplicates as $post_id ) {
|
||||
try {
|
||||
$event_instance_model->recreate(
|
||||
$this->_registry->get( 'model.event', $post_id )
|
||||
);
|
||||
} catch ( Ai1ec_Exception $excpt ) {
|
||||
// discard any internal errors
|
||||
}
|
||||
}
|
||||
} else if ( $count > 0 ) { // retry
|
||||
return $this->remove_instance_duplicates( --$depth );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* find_duplicates method
|
||||
*
|
||||
* Find a list of duplicates in table, given search key and groupping fields
|
||||
*
|
||||
* @param string $table Name of table, to search duplicates in
|
||||
* @param string $primary Column, to return values for
|
||||
* @param array $group List of fields, to group values on
|
||||
*
|
||||
* @return array List of primary field values
|
||||
*/
|
||||
public function find_duplicates( $table, $primary, array $group ) {
|
||||
$sql_query = '
|
||||
SELECT
|
||||
MIN( {{primary}} ) AS dup_primary -- pop oldest
|
||||
FROM {{table}}
|
||||
GROUP BY {{group}}
|
||||
HAVING COUNT( {{primary}} ) > 1
|
||||
';
|
||||
$sql_query = str_replace(
|
||||
array(
|
||||
'{{table}}',
|
||||
'{{primary}}',
|
||||
'{{group}}',
|
||||
),
|
||||
array(
|
||||
$this->_table( $table ),
|
||||
$this->_escape_column( $primary ),
|
||||
implode(
|
||||
', ',
|
||||
array_map( array( $this, '_escape_column' ), $group )
|
||||
),
|
||||
),
|
||||
$sql_query
|
||||
);
|
||||
$result = $this->_db->get_col( $sql_query );
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check list of tables for consistency.
|
||||
*
|
||||
* @return array List of inconsistencies.
|
||||
*/
|
||||
public function check_db_consistency_for_date_migration() {
|
||||
$db_migration = $this->_registry->get( 'database.datetime-migration' );
|
||||
/* @var $db_migration Ai1ecdm_Datetime_Migration */
|
||||
$tables = $db_migration->get_tables();
|
||||
if ( ! is_array( $tables ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// for date migration purposes we can assume
|
||||
// that all columns need to be the same type
|
||||
$info = array();
|
||||
foreach( $tables as $t_name => $t_columns ) {
|
||||
if ( count( $t_columns ) < 2 ) {
|
||||
continue;
|
||||
}
|
||||
$tbl_error = $this->_check_single_table(
|
||||
$t_name,
|
||||
$db_migration->get_columns( $t_name ),
|
||||
$t_columns
|
||||
);
|
||||
if ( null !== $tbl_error ) {
|
||||
$info[] = $tbl_error;
|
||||
}
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if single table columns are the same type.
|
||||
*
|
||||
* @param string $t_name Table name for details purposes.
|
||||
* @param array $db_cols Columns from database.
|
||||
* @param array $t_columns Columns to check from DDL.
|
||||
*
|
||||
* @return string|null Inconsistency description, if any.
|
||||
*/
|
||||
protected function _check_single_table(
|
||||
$t_name,
|
||||
array $db_cols,
|
||||
array $t_columns
|
||||
) {
|
||||
$type = null;
|
||||
foreach ( $db_cols as $c_field => $c_type ) {
|
||||
if ( ! in_array( $c_field, $t_columns ) ) {
|
||||
continue;
|
||||
}
|
||||
if ( null === $type ) {
|
||||
$type = strtolower( $c_type );
|
||||
}
|
||||
if ( strtolower( $c_type ) !== $type ) {
|
||||
return sprintf(
|
||||
Ai1ec_I18n::__(
|
||||
'Date columns in table %s have different types.'
|
||||
),
|
||||
$t_name
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fully qualified table name, to use in queries.
|
||||
*
|
||||
* @param string $table Name of table, to convert.
|
||||
*
|
||||
* @return string Qualified table name.
|
||||
*/
|
||||
protected function _table( $table ) {
|
||||
$prefix = $this->_db->get_table_name( 'ai1ec_' );
|
||||
if ( substr( $table, 0, strlen( $prefix ) ) !== $prefix ) {
|
||||
$table = $prefix . $table;
|
||||
}
|
||||
return $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* _escape_column method
|
||||
*
|
||||
* Escape column, enquoting it in MySQL specific characters
|
||||
*
|
||||
* @param string $name Name of column to quote
|
||||
*
|
||||
* @return string Escaped column name
|
||||
*/
|
||||
protected function _escape_column( $name ) {
|
||||
return '`' . $name . '`';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,473 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* The date-time migration utility layer.
|
||||
*
|
||||
* @author Time.ly Network Inc.
|
||||
* @since 2.0
|
||||
*
|
||||
* @package AI1EC
|
||||
* @subpackage AI1EC.Database
|
||||
*/
|
||||
class Ai1ecdm_Datetime_Migration {
|
||||
|
||||
/**
|
||||
* @var wpdb Instance of wpdb or it's extension.
|
||||
*/
|
||||
protected $_dbi = null;
|
||||
|
||||
/**
|
||||
* @var array List of tables to be processed.
|
||||
*/
|
||||
protected $_tables = array();
|
||||
|
||||
/**
|
||||
* @var array Map of indices on selected tables.
|
||||
*/
|
||||
protected $_indices = array();
|
||||
|
||||
/**
|
||||
* @var string Table suffix used in migration process.
|
||||
*/
|
||||
protected $_table_suffix = '_dt_ui_mig';
|
||||
|
||||
/**
|
||||
* @var string Column suffix used in data transformation.
|
||||
*/
|
||||
protected $_column_suffix = '_transformation';
|
||||
|
||||
/**
|
||||
* Output debug statements.
|
||||
*
|
||||
* @var mixed $arg1 Number of arguments to output.
|
||||
*
|
||||
* @return bool True when debug is in action.
|
||||
*/
|
||||
static public function debug( /** polymorphic arg list **/ ) {
|
||||
if ( ! defined( 'AI1EC_DEBUG' ) || ! AI1EC_DEBUG ) {
|
||||
return false;
|
||||
}
|
||||
$argv = func_get_args();
|
||||
foreach ( $argv as $value ) {
|
||||
echo '<pre class="timely-debug">',
|
||||
'<small>', microtime( true ), '</small>', "\n";
|
||||
var_export( $value );
|
||||
echo '</pre>';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire references of global variables and define non-scalar values.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( Ai1ec_Registry_Object $registry ) {
|
||||
$this->_dbi = $registry->get( 'dbi.dbi' );
|
||||
$this->_tables = array(
|
||||
$this->_dbi->get_table_name( 'ai1ec_events' ) => array(
|
||||
'start',
|
||||
'end',
|
||||
),
|
||||
$this->_dbi->get_table_name( 'ai1ec_event_instances' ) => array(
|
||||
'start',
|
||||
'end',
|
||||
),
|
||||
$this->_dbi->get_table_name( 'ai1ec_facebook_users_events' ) => array(
|
||||
'start',
|
||||
),
|
||||
);
|
||||
$this->_indices = array(
|
||||
$this->_dbi->get_table_name( 'ai1ec_event_instances' ) => array(
|
||||
'evt_instance' => array(
|
||||
'unique' => true,
|
||||
'columns' => array( 'post_id', 'start' ),
|
||||
'name' => 'evt_instance',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to underlying methods to use as a filter callback.
|
||||
*
|
||||
* @wp_hook ai1ec_perform_scheme_update
|
||||
*
|
||||
* @return bool True when database is up to date.
|
||||
*/
|
||||
public function filter_scheme_update() {
|
||||
return ( ! $this->is_change_required() || $this->execute() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve columns for a given table.
|
||||
*
|
||||
* Checks if table exists before attempting to retrieve it.
|
||||
*
|
||||
* @param string $table Name of table to retrieve columns for.
|
||||
*
|
||||
* @return array Map of column names and their types.
|
||||
*/
|
||||
public function get_columns( $table ) {
|
||||
if ( ! $this->_is_table( $table ) ) {
|
||||
return array();
|
||||
}
|
||||
$list = $this->_dbi->get_results(
|
||||
'SHOW COLUMNS FROM `' . $table . '`'
|
||||
);
|
||||
$columns = array();
|
||||
foreach ( $list as $column ) {
|
||||
$columns[$column->Field] = strtolower( $column->Type );
|
||||
}
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve list of indices for a given table.
|
||||
*
|
||||
* Checks if table exists before attempting to retrieve it.
|
||||
*
|
||||
* @param string $table Name of table to retrieve indices for.
|
||||
*
|
||||
* @return array Map of index names.
|
||||
*/
|
||||
public function get_indices( $table ) {
|
||||
if ( ! $this->_is_table( $table ) ) {
|
||||
return array();
|
||||
}
|
||||
$list = $this->_dbi->get_results(
|
||||
'SHOW INDEX FROM `' . $table . '`'
|
||||
);
|
||||
$columns = array();
|
||||
foreach ( $list as $column ) {
|
||||
$columns[ strtolower( $column->Key_name ) ] = $column->Key_name;
|
||||
}
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if database change is required.
|
||||
*
|
||||
* @return bool True if any changes are required.
|
||||
*/
|
||||
public function is_change_required() {
|
||||
foreach ( $this->_tables as $table => $columns ) {
|
||||
$existing = $this->get_columns( $table );
|
||||
foreach ( $existing as $column => $type ) {
|
||||
if (
|
||||
false === array_search( $column, $columns ) ||
|
||||
0 !== stripos( $type, 'datetime' )
|
||||
) {
|
||||
unset( $existing[$column] );
|
||||
}
|
||||
}
|
||||
if ( empty( $existing ) ) {
|
||||
unset( $this->_tables[$table] );
|
||||
}
|
||||
}
|
||||
if ( ! empty( $this->_tables ) ) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single stop for executing database changes.
|
||||
*
|
||||
* @return bool Success.
|
||||
*/
|
||||
public function execute() {
|
||||
return $this->create_copies()
|
||||
&& $this->apply_changes_to_copies()
|
||||
&& $this->swap_tables();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create copies of tables to be transformed.
|
||||
*
|
||||
* @return bool Success.
|
||||
*/
|
||||
public function create_copies() {
|
||||
$tables = array_keys( $this->_tables );
|
||||
foreach ( $tables as $table ) {
|
||||
$suffixed = $table . $this->_table_suffix;
|
||||
if (
|
||||
! $this->drop( $suffixed ) ||
|
||||
! $this->copy( $table, $suffixed )
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
self::debug(
|
||||
'Copies of following tables created successfully:',
|
||||
$tables
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform columns on copied tables.
|
||||
*
|
||||
* @return bool Success.
|
||||
*/
|
||||
public function apply_changes_to_copies() {
|
||||
foreach ( $this->_tables as $table => $columns ) {
|
||||
$name = $table . $this->_table_suffix;
|
||||
if (
|
||||
! (
|
||||
$this->drop_indices( $table, $name )
|
||||
&& $this->out_of_bounds_fix( $table, $name )
|
||||
&& $this->add_columns( $name, $columns )
|
||||
&& $this->transform_dates( $name, $columns )
|
||||
&& $this->replace_columns( $name, $columns )
|
||||
&& $this->restore_indices( $table, $name )
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
self::debug(
|
||||
'Table copies successfully modified:',
|
||||
$this->_tables
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep old table under unique name and move modified into it's place.
|
||||
*
|
||||
* @return bool Success.
|
||||
*/
|
||||
public function swap_tables() {
|
||||
$tables = array_keys( $this->_tables );
|
||||
$renames = array();
|
||||
foreach ( $tables as $table ) {
|
||||
$modified = $table . $this->_table_suffix;
|
||||
$backup = $table . '_' . date( 'Y_m_d' ) . '_' . getmypid();
|
||||
$renames[] = '`' . $table . '` TO `' . $backup . '`';
|
||||
$renames[] = '`' . $modified . '` TO `' . $table . '`';
|
||||
}
|
||||
$sql_query = 'RENAME TABLE ' . implode( ', ', $renames );
|
||||
if ( false === $this->_dbi->query( $sql_query ) ) {
|
||||
return false;
|
||||
}
|
||||
self::debug(
|
||||
'Tables successfully swaped:',
|
||||
$this->_tables
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop given table indices.
|
||||
*
|
||||
* @param string $name Original table name.
|
||||
* @param string $table Table to actually perform changes upon.
|
||||
*
|
||||
* @return bool Success.
|
||||
*/
|
||||
public function drop_indices( $name, $table ) {
|
||||
self::debug( __METHOD__ );
|
||||
if ( ! isset( $this->_indices[$name] ) ) {
|
||||
return true;
|
||||
}
|
||||
$existing = $this->get_indices( $table );
|
||||
foreach ( $this->_indices[$name] as $index => $options ) {
|
||||
if ( isset( $existing[$index] ) ) {
|
||||
$sql_query = 'ALTER TABLE `' . $table . '` DROP INDEX `' .
|
||||
$index . '`';
|
||||
if ( false === $this->_dbi->query( $sql_query ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add intermediate columns to a table.
|
||||
*
|
||||
* @param string $table Name of table to modify.
|
||||
* @param array $columns List of column names to be added.
|
||||
*
|
||||
* @return bool Success.
|
||||
*/
|
||||
public function add_columns( $table, $columns ) {
|
||||
self::debug( __METHOD__ );
|
||||
$column_particles = array();
|
||||
foreach ( $columns as $column ) {
|
||||
$name = $column . $this->_column_suffix;
|
||||
$column_particles[] = 'ADD COLUMN ' . $name .
|
||||
' INT(10) UNSIGNED NOT NULL';
|
||||
}
|
||||
$sql_query = 'ALTER TABLE `' . $table . '` ' .
|
||||
implode( ', ', $column_particles );
|
||||
return ( false !== $this->_dbi->query( $sql_query ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy date values from `DATETIME` to `INT(10)` columns.
|
||||
*
|
||||
* @param string $table Name of table to modify.
|
||||
* @param array $columns List of column names to be copied.
|
||||
*
|
||||
* @return bool Success.
|
||||
*/
|
||||
public function transform_dates( $table, $columns ) {
|
||||
self::debug( __METHOD__ );
|
||||
$update_particles = array();
|
||||
foreach ( $columns as $column ) {
|
||||
$name = $column . $this->_column_suffix;
|
||||
$new_value = '\'1970-01-01 00:00:00\'';
|
||||
if ( 'end' === $column && in_array( 'start', $columns ) ) {
|
||||
$new_value = 'IFNULL(`start`, ' . $new_value . ')';
|
||||
}
|
||||
$update_particles[] = '`' . $name .
|
||||
'` = UNIX_TIMESTAMP( IFNULL(`' . $column . '`, ' . $new_value . ' ))';
|
||||
}
|
||||
$sql_query = 'UPDATE `' . $table . '` SET ' .
|
||||
implode( ', ', $update_particles );
|
||||
return ( false !== $this->_dbi->query( $sql_query ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop old columns and move intermediate columns into their place.
|
||||
*
|
||||
* @param string $table Name of table to modify.
|
||||
* @param array $columns List of column names to be replaced.
|
||||
*
|
||||
* @return bool Success.
|
||||
*/
|
||||
public function replace_columns( $table, $columns ) {
|
||||
self::debug( __METHOD__ );
|
||||
$snippets = array();
|
||||
foreach ( $columns as $column ) {
|
||||
$snippets[] = 'DROP COLUMN `' . $column . '`';
|
||||
$snippets[] = 'CHANGE COLUMN `' . $column . $this->_column_suffix .
|
||||
'` `' . $column . '` INT(10) UNSIGNED NOT NULL';
|
||||
}
|
||||
$sql_query = 'ALTER TABLE `' . $table . '` ' .
|
||||
implode( ', ', $snippets );
|
||||
return ( false !== $this->_dbi->query( $sql_query ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore indices for table processed.
|
||||
*
|
||||
* @param string $name Original table name.
|
||||
* @param string $table Table to actually perform changes upon.
|
||||
*
|
||||
* @return bool Success.
|
||||
*/
|
||||
public function restore_indices( $name, $table ) {
|
||||
self::debug( __METHOD__ );
|
||||
if ( ! isset( $this->_indices[$name] ) ) {
|
||||
return true;
|
||||
}
|
||||
foreach ( $this->_indices[$name] as $index => $options ) {
|
||||
$sql_query = 'ALTER TABLE `' . $table . '` ADD';
|
||||
if ( $options['unique'] ) {
|
||||
$sql_query .= ' UNIQUE';
|
||||
}
|
||||
$sql_query .= ' INDEX `' .
|
||||
$index . '` (`' .
|
||||
implode( '`, `', $options['columns'] ) .
|
||||
'`)';
|
||||
if ( false === $this->_dbi->query( $sql_query ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop table.
|
||||
*
|
||||
* @param string $table Name of table to drop.
|
||||
*
|
||||
* @return bool Success.
|
||||
*/
|
||||
public function drop( $table ) {
|
||||
$sql_query = 'DROP TABLE IF EXISTS ' . $table;
|
||||
return ( false !== $this->_dbi->query( $sql_query ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create table copy with full data set.
|
||||
*
|
||||
* @param string $existing Name of table to copy.
|
||||
* @param string $new_table Name of table to create.
|
||||
*
|
||||
* @return bool Success.
|
||||
*/
|
||||
public function copy( $existing, $new_table ) {
|
||||
$queries = array(
|
||||
'CREATE TABLE ' . $new_table . ' LIKE ' . $existing,
|
||||
'INSERT INTO ' . $new_table . ' SELECT * FROM ' . $existing,
|
||||
);
|
||||
foreach ( $queries as $query ) {
|
||||
self::debug( $query );
|
||||
if ( false === $this->_dbi->query( $query ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$count_new = $this->_dbi->get_var(
|
||||
'SELECT COUNT(*) FROM ' . $new_table
|
||||
);
|
||||
$count_old = $this->_dbi->get_var(
|
||||
'SELECT COUNT(*) FROM ' . $existing
|
||||
);
|
||||
// check if difference between tables records doesn't exceed
|
||||
// several least significant bits of old table entries count
|
||||
if ( absint( $count_new - $count_old ) > ( $count_old >> 4 ) ) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of tables to be processed
|
||||
*
|
||||
* @return array List of tables to be processed
|
||||
*/
|
||||
public function get_tables() {
|
||||
return $this->_tables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete events dated before or at `1970-01-01 00:00:00`.
|
||||
*
|
||||
* @param string $table Original table.
|
||||
* @param string $name Temporary table to replay changes onto.
|
||||
*
|
||||
* @return bool Success.
|
||||
*/
|
||||
public function out_of_bounds_fix( $table, $name ) {
|
||||
static $instances = null;
|
||||
if ( null === $instances ) {
|
||||
$instances = $this->_dbi->get_table_name( 'ai1ec_event_instances' );
|
||||
}
|
||||
if ( $instances !== $table ) {
|
||||
return true;
|
||||
}
|
||||
$query = 'DELETE FROM `' .
|
||||
$this->_dbi->get_table_name( $name ) .
|
||||
'` WHERE `start` <= \'1970-01-01 00:00:00\'';
|
||||
return ( false !== $this->_dbi->query( $query ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if given table exists.
|
||||
*
|
||||
* @param string $table Name of table to check.
|
||||
*
|
||||
* @return bool Existence.
|
||||
*/
|
||||
protected function _is_table( $table ) {
|
||||
$name = $this->_dbi->get_var(
|
||||
$this->_dbi->prepare( 'SHOW TABLES LIKE %s', $table )
|
||||
);
|
||||
return ( (string)$table === (string)$name );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* In case of database update failure this exception is thrown
|
||||
*
|
||||
* @author Time.ly Network Inc.
|
||||
* @since 2.0
|
||||
*
|
||||
* @package AI1EC
|
||||
* @subpackage AI1EC.Database.Exception
|
||||
*/
|
||||
class Ai1ec_Database_Error extends Ai1ec_Exception {
|
||||
|
||||
/**
|
||||
* Override parent method to include tip.
|
||||
*
|
||||
* @return string Message to render.
|
||||
*/
|
||||
public function get_html_message() {
|
||||
$message = '<p>' . Ai1ec_I18n::__(
|
||||
'Database update has failed. Please make sure, that database user, defined in <em>wp-config.php</em> has permissions, to make changes (<strong>ALTER TABLE</strong>) to the database.'
|
||||
) .
|
||||
'</p><p>' . sprintf(
|
||||
Ai1ec_I18n::__( 'Error encountered: %s' ),
|
||||
$this->getMessage()
|
||||
) . '</p>';
|
||||
return $message;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* In case of database schema modification failure this exception is thrown
|
||||
*
|
||||
* @author Time.ly Network Inc.
|
||||
* @since 2.0
|
||||
*
|
||||
* @package AI1EC
|
||||
* @subpackage AI1EC.Database.Exception
|
||||
*/
|
||||
class Ai1ec_Database_Schema_Exception extends Ai1ec_Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* In case of database update failure this exception is thrown
|
||||
*
|
||||
* @author Time.ly Network Inc.
|
||||
* @since 2.0
|
||||
*
|
||||
* @package AI1EC
|
||||
* @subpackage AI1EC.Database.Exception
|
||||
*/
|
||||
class Ai1ec_Database_Update_Exception extends Ai1ec_Exception {
|
||||
}
|
||||
@@ -0,0 +1,756 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Ai1ec_Database class
|
||||
*
|
||||
* Class responsible for generic database operations
|
||||
*
|
||||
* @author Time.ly Network Inc.
|
||||
* @since 2.0
|
||||
*
|
||||
* @package AI1EC
|
||||
* @subpackage AI1EC.Database
|
||||
*/
|
||||
class Ai1ec_Database_Helper {
|
||||
|
||||
/**
|
||||
* @var array Map of tables and their parsed definitions
|
||||
*/
|
||||
protected $_schema_delta = array();
|
||||
|
||||
/**
|
||||
* @var array List of valid table prefixes
|
||||
*/
|
||||
protected $_prefixes = array();
|
||||
|
||||
/**
|
||||
* @var wpdb Localized instance of wpdb object
|
||||
*/
|
||||
protected $_db = NULL;
|
||||
|
||||
/**
|
||||
* @var bool If set to true - no operations will be performed
|
||||
*/
|
||||
protected $_dry_run = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param Ai1ec_Registry_Object $registry
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( Ai1ec_Registry_Object $registry ) {
|
||||
$this->_db = $registry->get( 'dbi.dbi' );
|
||||
$this->_prefixes = array(
|
||||
$this->_db->get_table_name( 'ai1ec_' ),
|
||||
$this->_db->get_table_name(),
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if dry run is enabled
|
||||
*
|
||||
* @param bool $dry Change dryness [optional=NULL]
|
||||
*
|
||||
* @return bool Dryness of run or previous value
|
||||
*/
|
||||
public function is_dry( $dry = NULL ) {
|
||||
if ( NULL !== $dry ) {
|
||||
$previous = $this->_dry_run;
|
||||
$this->_dry_run = (bool)$dry;
|
||||
return $previous;
|
||||
}
|
||||
return $this->_dry_run;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fully-qualified table name given it's abbreviated form
|
||||
*
|
||||
* @param string $name Name (abbreviation) of table to check
|
||||
* @param bool $ignore_check Return longest name if no table exist [false]
|
||||
*
|
||||
* @return string Fully-qualified table name
|
||||
*
|
||||
* @throws Ai1ec_Database_Schema_Exception If no table matches
|
||||
*/
|
||||
public function table( $name, $ignore_check = false ) {
|
||||
$existing = $this->get_all_tables();
|
||||
$table = NULL;
|
||||
$candidate = NULL;
|
||||
foreach ( $this->_prefixes as $prefix ) {
|
||||
$candidate = $prefix . $name;
|
||||
$index = strtolower( $candidate );
|
||||
if ( isset( $existing[$index] ) ) {
|
||||
$table = $existing[$index];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( NULL === $table ) {
|
||||
if ( true === $ignore_check ) {
|
||||
return $candidate;
|
||||
}
|
||||
throw new Ai1ec_Database_Schema_Exception(
|
||||
'Table \'' . $name . '\' does not exist'
|
||||
);
|
||||
}
|
||||
return $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop given indices from table
|
||||
*
|
||||
* @param string $table Name of table to modify
|
||||
* @param string|array $indices List, or single, of indices to remove
|
||||
*
|
||||
* @return bool Success
|
||||
*
|
||||
* @throws Ai1ec_Database_Schema_Exception If table is not found
|
||||
*/
|
||||
public function drop_indices( $table, $indices ) {
|
||||
if ( ! is_array( $indices ) ) {
|
||||
$indices = array( (string)$indices );
|
||||
}
|
||||
$table = $this->table( $table );
|
||||
$existing = $this->get_indices( $table );
|
||||
$removed = 0;
|
||||
foreach ( $indices as $index ) {
|
||||
if (
|
||||
! isset( $existing[$index] ) ||
|
||||
$this->_dry_query(
|
||||
'ALTER TABLE ' . $table . ' DROP INDEX ' . $index
|
||||
)
|
||||
) {
|
||||
++$removed;
|
||||
}
|
||||
}
|
||||
return ( count( $indices ) === $removed );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create indices for given table
|
||||
*
|
||||
* Input ({@see $indices}) must be the same, as output of
|
||||
* method {@see self::get_indices()}.
|
||||
*
|
||||
* @param string $table Name of table to create indices for
|
||||
* @param array $indices Indices representation to be created
|
||||
*
|
||||
* @return bool Success
|
||||
*
|
||||
* @throws Ai1ec_Database_Schema_Exception If table is not found
|
||||
*/
|
||||
public function create_indices( $table, array $indices ) {
|
||||
$table = $this->table( $table );
|
||||
foreach ( $indices as $name => $definition ) {
|
||||
$query = 'ALTER TABLE ' . $table . ' ADD ';
|
||||
if ( $definition['unique'] ) {
|
||||
$query .= 'UNIQUE ';
|
||||
}
|
||||
$query .= 'KEY ' . $name . ' (' .
|
||||
implode( ', ', $definition['columns'] ) .
|
||||
')';
|
||||
if ( ! $this->_dry_query( $query ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* get_indices method
|
||||
*
|
||||
* Get map of indices defined for table.
|
||||
*
|
||||
* @NOTICE: no optimization will be performed here, and response will not
|
||||
* be cached, to allow checking result of DDL statements.
|
||||
*
|
||||
* Returned array structure (example):
|
||||
* array(
|
||||
* 'index_name' => array(
|
||||
* 'name' => 'index_name',
|
||||
* 'columns' => array(
|
||||
* 'column1',
|
||||
* 'column2',
|
||||
* 'column3',
|
||||
* ),
|
||||
* 'unique' => true,
|
||||
* ),
|
||||
* )
|
||||
*
|
||||
* @param string $table Name of table to retrieve index names for
|
||||
*
|
||||
* @return array Map of index names and their representation
|
||||
*
|
||||
* @throws Ai1ec_Database_Schema_Exception If table is not found
|
||||
*/
|
||||
public function get_indices( $table ) {
|
||||
$sql_query = 'SHOW INDEXES FROM ' . $this->table( $table );
|
||||
$result = $this->_db->get_results( $sql_query );
|
||||
$indices = array();
|
||||
foreach ( $result as $index ) {
|
||||
$name = $index->Key_name;
|
||||
if ( ! isset( $indices[$name] ) ) {
|
||||
$indices[$name] = array(
|
||||
'name' => $name,
|
||||
'columns' => array(),
|
||||
'unique' => ! (bool)intval( $index->Non_unique ),
|
||||
);
|
||||
}
|
||||
$indices[$name]['columns'][$index->Column_name] = $index->Sub_part;
|
||||
}
|
||||
return $indices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform query, unless `dry_run` is selected. In later case just output
|
||||
* the final query and return true.
|
||||
*
|
||||
* @param string $query SQL Query to execute
|
||||
*
|
||||
* @return mixed Query state, or true in dry run mode
|
||||
*/
|
||||
public function _dry_query( $query ) {
|
||||
if ( $this->is_dry() ) {
|
||||
pr( $query );
|
||||
return true;
|
||||
}
|
||||
$result = $this->_db->query( $query );
|
||||
if ( AI1EC_DEBUG ) {
|
||||
echo '<h4>', $query, '</h4><pre>', var_export( $result, true ), '</pre>';
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if given table exists
|
||||
*
|
||||
* @param string $table Name of table to check
|
||||
*
|
||||
* @return bool Existance
|
||||
*/
|
||||
public function table_exists( $table ) {
|
||||
$map = $this->get_all_tables();
|
||||
return isset( $map[strtolower( $table )] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of all tables currently present
|
||||
*
|
||||
* @return array Map of tables present
|
||||
*/
|
||||
public function get_all_tables() {
|
||||
/**
|
||||
* @TODO: refactor using dbi.dbi::get_tables
|
||||
*/
|
||||
$sql_query = 'SHOW TABLES LIKE \'' .
|
||||
$this->_db->get_table_name() .
|
||||
'%\'';
|
||||
$result = $this->_db->get_col( $sql_query );
|
||||
$tables = array();
|
||||
foreach ( $result as $table ) {
|
||||
$tables[strtolower( $table )] = $table;
|
||||
}
|
||||
return $tables;
|
||||
}
|
||||
|
||||
/**
|
||||
* apply_delta method
|
||||
*
|
||||
* Attempt to parse and apply given database tables definition, as a delta.
|
||||
* Some validation is made prior to calling DB, and fields/indexes are also
|
||||
* checked for consistency after sending queries to DB.
|
||||
*
|
||||
* NOTICE: only "CREATE TABLE" statements are handled. Others will, likely,
|
||||
* be ignored, if passed through this method.
|
||||
*
|
||||
* @param string|array $query Single or multiple queries to perform on DB
|
||||
*
|
||||
* @return bool Success
|
||||
*
|
||||
* @throws Ai1ec_Database_Error In case of any error
|
||||
*/
|
||||
public function apply_delta( $query ) {
|
||||
if ( ! function_exists( 'dbDelta' ) ) {
|
||||
require_once ABSPATH . 'wp-admin' . DIRECTORY_SEPARATOR .
|
||||
'includes' . DIRECTORY_SEPARATOR . 'upgrade.php';
|
||||
}
|
||||
$success = false;
|
||||
$this->_schema_delta = array();
|
||||
$queries = $this->_prepare_delta( $query );
|
||||
$result = dbDelta( $queries );
|
||||
$success = $this->_check_delta();
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* get_notices_helper method
|
||||
*
|
||||
* DIP implementing method, to give access to Ai1ec_Deferred_Rendering_Helper.
|
||||
*
|
||||
* @param Ai1ec_Deferred_Rendering_Helper $replacement Notices implementor
|
||||
*
|
||||
* @return Ai1ec_Deferred_Rendering_Helper Instance of notices implementor
|
||||
*/
|
||||
public function get_notices_helper(
|
||||
Ai1ec_Deferred_Rendering_Helper $replacement = NULL
|
||||
) {
|
||||
static $helper = NULL;
|
||||
if ( NULL !== $replacement ) {
|
||||
$helper = $replacement;
|
||||
}
|
||||
if ( NULL === $helper ) {
|
||||
$helper = Ai1ec_Deferred_Rendering_Helper::get_instance();
|
||||
}
|
||||
return $helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* _prepare_delta method
|
||||
*
|
||||
* Prepare statements for execution.
|
||||
* Attempt to parse various SQL definitions and compose the one, that is
|
||||
* most likely to be accepted by delta engine.
|
||||
*
|
||||
* @param string|array $queries Single or multiple queries to perform on DB
|
||||
*
|
||||
* @return bool Success
|
||||
*
|
||||
* @throws Ai1ec_Database_Error In case of any error
|
||||
*/
|
||||
protected function _prepare_delta( $queries ) {
|
||||
if ( ! is_array( $queries ) ) {
|
||||
$queries = explode( ';', $queries );
|
||||
$queries = array_filter( $queries );
|
||||
}
|
||||
$current_table = NULL;
|
||||
$ctable_regexp = '#
|
||||
\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?([^ ]+)`?\s*
|
||||
\((.+)\)
|
||||
([^()]*)
|
||||
#six';
|
||||
foreach ( $queries as $query ) {
|
||||
if ( preg_match( $ctable_regexp, $query, $matches ) ) {
|
||||
$this->_schema_delta[$matches[1]] = array(
|
||||
'tblname' => $matches[1],
|
||||
'cryptic' => NULL,
|
||||
'creator' => '',
|
||||
'columns' => array(),
|
||||
'indexes' => array(),
|
||||
'content' => preg_replace( '#`#', '', $matches[2] ),
|
||||
'clauses' => $matches[3],
|
||||
);
|
||||
}
|
||||
}
|
||||
$this->_parse_delta();
|
||||
$sane_queries = array();
|
||||
foreach ( $this->_schema_delta as $table => $definition ) {
|
||||
$create = 'CREATE TABLE ' . $table . " (\n";
|
||||
foreach ( $definition['columns'] as $column ) {
|
||||
$create .= ' ' . $column['create'] . ",\n";
|
||||
}
|
||||
foreach ( $definition['indexes'] as $index ) {
|
||||
$create .= ' ' . $index['create'] . ",\n";
|
||||
}
|
||||
$create = substr( $create, 0, -2 ) . "\n";
|
||||
$create .= ')' . $definition['clauses'];
|
||||
$this->_schema_delta[$table]['creator'] = $create;
|
||||
$this->_schema_delta[$table]['cryptic'] = md5( $create );
|
||||
$sane_queries[] = $create;
|
||||
}
|
||||
return $sane_queries;
|
||||
}
|
||||
|
||||
/**
|
||||
* _parse_delta method
|
||||
*
|
||||
* Parse table application (creation) statements into atomical particles.
|
||||
* Here "atomical particles" stands for either columns, or indexes.
|
||||
*
|
||||
* @return void Method does not return
|
||||
*
|
||||
* @throws Ai1ec_Database_Error In case of any error
|
||||
*/
|
||||
protected function _parse_delta() {
|
||||
foreach ( $this->_schema_delta as $table => $definitions ) {
|
||||
$listing = explode( "\n", $definitions['content'] );
|
||||
$listing = array_filter( $listing, array( $this, '_is_not_empty_line' ) );
|
||||
$lines = count( $listing );
|
||||
$lineno = 0;
|
||||
foreach ( $listing as $line ) {
|
||||
++$lineno;
|
||||
$line = trim( preg_replace( '#\s+#', ' ', $line ) );
|
||||
$line_new = rtrim( $line, ',' );
|
||||
if (
|
||||
$lineno < $lines && $line === $line_new ||
|
||||
$lineno == $lines && $line !== $line_new
|
||||
) {
|
||||
throw new Ai1ec_Database_Error(
|
||||
'Missing comma in line \'' . $line . '\''
|
||||
);
|
||||
}
|
||||
$line = $line_new;
|
||||
unset( $line_new );
|
||||
$type = 'indexes';
|
||||
if ( false === ( $record = $this->_parse_index( $line ) ) ) {
|
||||
$type = 'columns';
|
||||
$record = $this->_parse_column( $line );
|
||||
}
|
||||
if ( isset(
|
||||
$this->_schema_delta[$table][$type][$record['name']]
|
||||
) ) {
|
||||
throw new Ai1ec_Database_Error(
|
||||
'For table `' . $table . '` entry ' . $type .
|
||||
' named `' . $record['name'] . '` was declared twice' .
|
||||
' in ' . $definitions
|
||||
);
|
||||
}
|
||||
$this->_schema_delta[$table][$type][$record['name']] = $record;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* _parse_index method
|
||||
*
|
||||
* Given string attempts to detect, if it is an index, and if yes - parse
|
||||
* it to more navigable index definition for future validations.
|
||||
* Creates modified index create line, for delta application.
|
||||
*
|
||||
* @param string $description Single "line" of CREATE TABLE statement body
|
||||
*
|
||||
* @return array|bool Index definition, or false if input does not look like index
|
||||
*
|
||||
* @throws Ai1ec_Database_Error In case of any error
|
||||
*/
|
||||
protected function _parse_index( $description ) {
|
||||
$description = preg_replace(
|
||||
'#^CONSTRAINT(\s+`?[^ ]+`?)?\s+#six',
|
||||
'',
|
||||
$description
|
||||
);
|
||||
$details = explode( ' ', $description );
|
||||
$index = array(
|
||||
'name' => NULL,
|
||||
'content' => array(),
|
||||
'create' => '',
|
||||
);
|
||||
$details[0] = strtoupper( $details[0] );
|
||||
switch ( $details[0] ) {
|
||||
case 'PRIMARY':
|
||||
$index['name'] = 'PRIMARY';
|
||||
$index['create'] = 'PRIMARY KEY ';
|
||||
break;
|
||||
|
||||
case 'UNIQUE':
|
||||
$name = $details[1];
|
||||
if (
|
||||
0 === strcasecmp( 'KEY', $name ) ||
|
||||
0 === strcasecmp( 'INDEX', $name )
|
||||
) {
|
||||
$name = $details[2];
|
||||
}
|
||||
$index['name'] = $name;
|
||||
$index['create'] = 'UNIQUE KEY ' . $name;
|
||||
break;
|
||||
|
||||
case 'KEY':
|
||||
case 'INDEX':
|
||||
$index['name'] = $details[1];
|
||||
$index['create'] = 'KEY ' . $index['name'];
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
$index['content'] = $this->_parse_index_content( $description );
|
||||
$index['create'] .= ' (';
|
||||
foreach ( $index['content'] as $column => $length ) {
|
||||
$index['create'] .= $column;
|
||||
if ( NULL !== $length ) {
|
||||
$index['create'] .= '(' . $length . ')';
|
||||
}
|
||||
$index['create'] .= ',';
|
||||
}
|
||||
$index['create'] = substr( $index['create'], 0, -1 );
|
||||
$index['create'] .= ')';
|
||||
return $index;
|
||||
}
|
||||
|
||||
/**
|
||||
* _parse_column method
|
||||
*
|
||||
* Parse column to parseable definition.
|
||||
* Some valid definitions may still be not recognizes (namely SET and ENUM)
|
||||
* thus one shall beware, when attempting to create such.
|
||||
* Create alternative create table entry line for delta application.
|
||||
*
|
||||
* @param string $description Single "line" of CREATE TABLE statement body
|
||||
*
|
||||
* @return array Column definition
|
||||
*
|
||||
* @throws Ai1ec_Database_Error In case of any error
|
||||
*/
|
||||
protected function _parse_column( $description ) {
|
||||
$column_regexp = '#^
|
||||
([a-z][a-z_]+)\s+
|
||||
(
|
||||
[A-Z]+
|
||||
(?:\s*\(\s*\d+(?:\s*,\s*\d+\s*)?\s*\))?
|
||||
(?:\s+UNSIGNED)?
|
||||
(?:\s+ZEROFILL)?
|
||||
(?:\s+BINARY)?
|
||||
(?:
|
||||
\s+CHARACTER\s+SET\s+[a-z][a-z_]+
|
||||
(?:\s+COLLATE\s+[a-z][a-z0-9_]+)?
|
||||
)?
|
||||
)
|
||||
(
|
||||
\s+(?:NOT\s+)?NULL
|
||||
)?
|
||||
(
|
||||
\s+DEFAULT\s+[^\s]+
|
||||
)?
|
||||
(\s+ON\s+UPDATE\s+CURRENT_(?:TIMESTAMP|DATE))?
|
||||
(\s+AUTO_INCREMENT)?
|
||||
\s*,?\s*
|
||||
$#six';
|
||||
if ( ! preg_match( $column_regexp, $description, $matches ) ) {
|
||||
throw new Ai1ec_Database_Error(
|
||||
'Invalid column description ' . $description
|
||||
);
|
||||
}
|
||||
$column = array(
|
||||
'name' => $matches[1],
|
||||
'content' => array(),
|
||||
'create' => '',
|
||||
);
|
||||
if ( 0 === strcasecmp( 'boolean', $matches[2] ) ) {
|
||||
$matches[2] = 'tinyint(1)';
|
||||
}
|
||||
$column['content']['type'] = $matches[2];
|
||||
$column['content']['null'] = (
|
||||
! isset( $matches[3] ) ||
|
||||
0 !== strcasecmp( 'NOT NULL', trim( $matches[3] ) )
|
||||
);
|
||||
$column['create'] = $column['name'] . ' ' . $column['content']['type'];
|
||||
if ( isset( $matches[3] ) ) {
|
||||
$column['create'] .= ' ' .
|
||||
implode(
|
||||
' ',
|
||||
array_map(
|
||||
'trim',
|
||||
array_slice( $matches, 3 )
|
||||
)
|
||||
);
|
||||
}
|
||||
return $column;
|
||||
}
|
||||
|
||||
/**
|
||||
* _parse_index_content method
|
||||
*
|
||||
* Parse index content, to a map of columns and their length.
|
||||
* All index (content) cases shall be covered, although it is only tested.
|
||||
*
|
||||
* @param string Single line of CREATE TABLE statement, containing index definition
|
||||
*
|
||||
* @return array Map of columns and their length, as per index definition
|
||||
*
|
||||
* @throws Ai1ec_Database_Error In case of any error
|
||||
*/
|
||||
protected function _parse_index_content( $description ) {
|
||||
if ( ! preg_match( '#^[^(]+\((.+)\)$#', $description, $matches ) ) {
|
||||
throw new Ai1ec_Database_Error(
|
||||
'Invalid index description ' . $description
|
||||
);
|
||||
}
|
||||
$columns = array();
|
||||
$textual = explode( ',', $matches[1] );
|
||||
$column_regexp = '#\s*([^(]+)(?:\s*\(\s*(\d+)\s*\))?\s*#sx';
|
||||
foreach ( $textual as $column ) {
|
||||
if (
|
||||
! preg_match( $column_regexp, $column, $matches ) || (
|
||||
isset( $matches[2] ) &&
|
||||
(string)$matches[2] !== (string)intval( $matches[2] )
|
||||
)
|
||||
) {
|
||||
throw new Ai1ec_Database_Error(
|
||||
'Invalid index (columns) description ' . $description .
|
||||
' as per \'' . $column . '\''
|
||||
);
|
||||
}
|
||||
$matches[1] = trim( $matches[1] );
|
||||
$columns[$matches[1]] = NULL;
|
||||
if ( isset( $matches[2] ) ) {
|
||||
$columns[$matches[1]] = (int)$matches[2];
|
||||
}
|
||||
}
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* _check_delta method
|
||||
*
|
||||
* Given parsed schema definitions (in {@see self::$_schema_delta} map) this
|
||||
* method performs checks, to ensure that table exists, columns are of
|
||||
* expected type, and indexes match their definition in original query.
|
||||
*
|
||||
* @return bool Success
|
||||
*
|
||||
* @throws Ai1ec_Database_Error In case of any error
|
||||
*/
|
||||
protected function _check_delta() {
|
||||
if ( empty( $this->_schema_delta ) ) {
|
||||
return true;
|
||||
}
|
||||
foreach ( $this->_schema_delta as $table => $description ) {
|
||||
|
||||
$columns = $this->_db->get_results( 'SHOW FULL COLUMNS FROM ' . $table );
|
||||
if ( empty( $columns ) ) {
|
||||
throw new Ai1ec_Database_Error(
|
||||
'Required table `' . $table . '` was not created'
|
||||
);
|
||||
}
|
||||
$db_column_names = array();
|
||||
foreach ( $columns as $column ) {
|
||||
if ( ! isset( $description['columns'][$column->Field] ) ) {
|
||||
if ( $this->_db->query(
|
||||
'ALTER TABLE `' . $table .
|
||||
'` DROP COLUMN `' . $column->Field . '`'
|
||||
) ) {
|
||||
continue;
|
||||
}
|
||||
continue; // ignore so far
|
||||
//throw new Ai1ec_Database_Error(
|
||||
// 'Unknown column `' . $column->Field .
|
||||
// '` is present in table `' . $table . '`'
|
||||
//);
|
||||
}
|
||||
$db_column_names[$column->Field] = $column->Field;
|
||||
$type_db = $column->Type;
|
||||
$collation = '';
|
||||
if ( $column->Collation ) {
|
||||
$collation = ' CHARACTER SET ' .
|
||||
substr(
|
||||
$column->Collation,
|
||||
0,
|
||||
strpos( $column->Collation, '_' )
|
||||
) . ' COLLATE ' . $column->Collation;
|
||||
}
|
||||
$type_req = $description['columns'][$column->Field]
|
||||
['content']['type'];
|
||||
if (
|
||||
false !== stripos(
|
||||
$type_req,
|
||||
' COLLATE '
|
||||
)
|
||||
) {
|
||||
// suspend collation checking
|
||||
//$type_db .= $collation;
|
||||
$type_req = preg_replace(
|
||||
'#^
|
||||
(.+)
|
||||
\s+CHARACTER\s+SET\s+[a-z0-9_]+
|
||||
\s+COLLATE\s+[a-z0-9_]+
|
||||
(.+)?\s*
|
||||
$#six',
|
||||
'$1$2',
|
||||
$type_req
|
||||
);
|
||||
}
|
||||
$type_db = strtolower(
|
||||
preg_replace( '#\s+#', '', $type_db )
|
||||
);
|
||||
$type_req = strtolower(
|
||||
preg_replace( '#\s+#', '', $type_req )
|
||||
);
|
||||
if ( 0 !== strcmp( $type_db, $type_req ) ) {
|
||||
throw new Ai1ec_Database_Error(
|
||||
'Field `' . $table . '`.`' . $column->Field .
|
||||
'` is of incompatible type'
|
||||
);
|
||||
}
|
||||
if (
|
||||
'YES' === $column->Null &&
|
||||
false === $description['columns'][$column->Field]
|
||||
['content']['null'] ||
|
||||
'NO' === $column->Null &&
|
||||
true === $description['columns'][$column->Field]
|
||||
['content']['null']
|
||||
) {
|
||||
throw new Ai1ec_Database_Error(
|
||||
'Field `' . $table . '`.`' . $column->Field .
|
||||
'` NULLability is flipped'
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
$missing = array_diff(
|
||||
array_keys( $description['columns'] ),
|
||||
$db_column_names
|
||||
)
|
||||
) {
|
||||
throw new Ai1ec_Database_Error(
|
||||
'In table `' . $table . '` fields are missing: ' .
|
||||
implode( ', ', $missing )
|
||||
);
|
||||
}
|
||||
|
||||
$indexes = $this->get_indices( $table );
|
||||
|
||||
foreach ( $indexes as $name => $definition ) {
|
||||
if ( ! isset( $description['indexes'][$name] ) ) {
|
||||
continue; // ignore so far
|
||||
//throw new Ai1ec_Database_Error(
|
||||
// 'Unknown index `' . $name .
|
||||
// '` is defined for table `' . $table . '`'
|
||||
//);
|
||||
}
|
||||
if (
|
||||
$missed = array_diff_assoc(
|
||||
$description['indexes'][$name]['content'],
|
||||
$definition['columns']
|
||||
)
|
||||
) {
|
||||
throw new Ai1ec_Database_Error(
|
||||
'Index `' . $name .
|
||||
'` definition for table `' . $table . '` has invalid ' .
|
||||
' fields: ' . implode( ', ', array_keys( $missed ) )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
$missing = array_diff(
|
||||
array_keys( $description['indexes'] ),
|
||||
array_keys( $indexes )
|
||||
)
|
||||
) {
|
||||
throw new Ai1ec_Database_Error(
|
||||
'In table `' . $table . '` indexes are missing: ' .
|
||||
implode( ', ', $missing )
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* _is_not_empty_line method
|
||||
*
|
||||
* Helper method, to check that any given line is not empty.
|
||||
* Aids array_filter in detecting empty SQL query lines.
|
||||
*
|
||||
* @param string $line Single line of DB query statement
|
||||
*
|
||||
* @return bool True if line is not empty, false otherwise
|
||||
*/
|
||||
protected function _is_not_empty_line( $line ) {
|
||||
$line = trim( $line );
|
||||
return ! empty( $line );
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user