Add upstream plugins

Signed-off-by: Adrian Nöthlich <git@promasu.tech>
This commit is contained in:
2019-10-25 22:42:20 +02:00
parent 5d3c2ec184
commit 290736650a
1186 changed files with 302577 additions and 0 deletions

View File

@@ -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 . '`';
}
}

View File

@@ -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 );
}
}

View File

@@ -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;
}
}

View File

@@ -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
{
}

View File

@@ -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 {
}

View File

@@ -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 );
}
}