', '', microtime( true ), '', "\n"; var_export( $value ); echo ''; } 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 ); } }