diff --git a/admin/load.php b/admin/load.php index 8b80f899be..bdda5d2487 100644 --- a/admin/load.php +++ b/admin/load.php @@ -163,7 +163,7 @@ function perflab_render_modules_page_field( $module_slug, $module_data, $module_ - +

diff --git a/modules/database/sqlite/can-load.php b/modules/database/sqlite/can-load.php index d375c1f181..ca52eee515 100644 --- a/modules/database/sqlite/can-load.php +++ b/modules/database/sqlite/can-load.php @@ -13,8 +13,9 @@ */ return function() { - // If the PERFLAB_SQLITE_DB_DROPIN_VERSION constant is defined, then the module is already active. - if ( defined( 'PERFLAB_SQLITE_DB_DROPIN_VERSION' ) ) { + // If the PERFLAB_SQLITE_DB_DROPIN_VERSION or SQLITE_DB_DROPIN_VERSION constants + // are defined, then the module is already active. + if ( defined( 'PERFLAB_SQLITE_DB_DROPIN_VERSION' ) || defined( 'SQLITE_DB_DROPIN_VERSION' ) ) { return true; } diff --git a/modules/database/sqlite/constants.php b/modules/database/sqlite/constants.php index 4c443c3831..5a6118544c 100644 --- a/modules/database/sqlite/constants.php +++ b/modules/database/sqlite/constants.php @@ -6,12 +6,23 @@ * @package performance-lab */ +// Backwards-compatibility. +if ( ! defined( 'PERFLAB_SQLITE_DB_DROPIN_VERSION' ) && defined( 'SQLITE_DB_DROPIN_VERSION' ) ) { + define( 'PERFLAB_SQLITE_DB_DROPIN_VERSION', SQLITE_DB_DROPIN_VERSION ); +} + // Temporary - This will be in wp-config.php once SQLite is merged in Core. -if ( ! defined( 'DATABASE_TYPE' ) ) { - if ( defined( 'PERFLAB_SQLITE_DB_DROPIN_VERSION' ) ) { - define( 'DATABASE_TYPE', 'sqlite' ); +if ( ! defined( 'DB_ENGINE' ) ) { + if ( defined( 'SQLITE_DB_DROPIN_VERSION' ) ) { + define( 'DB_ENGINE', 'sqlite' ); + } elseif ( defined( 'DATABASE_ENGINE' ) ) { + // Backwards compatibility with previous versions of the standalone plugin. + define( 'DB_ENGINE', DATABASE_ENGINE ); + } elseif ( defined( 'DATABASE_TYPE' ) ) { + // Backwards compatibility with previous versions of the performance-lab plugin. + define( 'DB_ENGINE', DATABASE_TYPE ); } else { - define( 'DATABASE_TYPE', 'mysql' ); + define( 'DB_ENGINE', 'mysql' ); } } diff --git a/modules/database/sqlite/site-health.php b/modules/database/sqlite/site-health.php index 0cbe0d9c9c..c0b9977374 100644 --- a/modules/database/sqlite/site-health.php +++ b/modules/database/sqlite/site-health.php @@ -1,40 +1,34 @@ 'DATABASE_TYPE', - 'value' => ( defined( 'DATABASE_TYPE' ) ? DATABASE_TYPE : __( 'Undefined', 'performance-lab' ) ), - 'debug' => ( defined( 'DATABASE_TYPE' ) ? DATABASE_TYPE : 'undefined' ), + $info['wp-constants']['fields']['DB_ENGINE'] = array( + 'label' => 'DB_ENGINE', + 'value' => ( defined( 'DB_ENGINE' ) ? DB_ENGINE : __( 'Undefined', 'performance-lab' ) ), + 'debug' => ( defined( 'DB_ENGINE' ) ? DB_ENGINE : 'undefined' ), ); - $info['wp-database']['fields']['database_type'] = array( + $info['wp-database']['fields']['db_engine'] = array( 'label' => __( 'Database type', 'performance-lab' ), - 'value' => 'sqlite' === $database_type ? 'SQLite' : 'MySQL/MariaDB', + 'value' => 'sqlite' === $db_engine ? 'SQLite' : 'MySQL/MariaDB', ); - if ( 'sqlite' === $database_type ) { + if ( 'sqlite' === $db_engine ) { $info['wp-database']['fields']['database_version'] = array( 'label' => __( 'SQLite version', 'performance-lab' ), 'value' => class_exists( 'SQLite3' ) ? SQLite3::version()['versionString'] : null, @@ -65,4 +59,24 @@ function perflab_sqlite_plugin_filter_debug_data( $info ) { return $info; } -add_filter( 'debug_information', 'perflab_sqlite_plugin_filter_debug_data' ); // Filter debug data in site-health screen. +add_filter( 'debug_information', 'sqlite_plugin_filter_debug_data' ); // Filter debug data in site-health screen. + +/** + * Filter site_status tests in site-health screen. + * + * When the plugin gets merged in wp-core, these should be merged in src/wp-admin/includes/class-wp-site-health.php + * + * @param array $tests The tests. + * @return array + */ +function sqlite_plugin_filter_site_status_tests( $tests ) { + $db_engine = defined( 'DB_ENGINE' ) && 'sqlite' === DB_ENGINE ? 'sqlite' : 'mysql'; + + if ( 'sqlite' === $db_engine ) { + unset( $tests['direct']['utf8mb4_support'] ); + unset( $tests['direct']['sql_server'] ); + } + + return $tests; +} +add_filter( 'site_status_tests', 'sqlite_plugin_filter_site_status_tests' ); diff --git a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-alter-query.php b/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-alter-query.php deleted file mode 100644 index d9909732fa..0000000000 --- a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-alter-query.php +++ /dev/null @@ -1,611 +0,0 @@ -_query = 'SELECT 1=1'; - return $this->_query; - } - - $tmp_query = array(); - $re_command = ''; - $command = str_ireplace( $match[0], '', $query ); - $tmp_tokens['query_type'] = trim( $match[1] ); - $tmp_tokens['table_name'] = trim( $match[2] ); - $command_array = explode( ',', $command ); - - $single_command = array_shift( $command_array ); - if ( ! empty( $command_array ) ) { - $re_command = "ALTER TABLE {$tmp_tokens['table_name']} "; - $re_command .= implode( ',', $command_array ); - } - $command_tokens = $this->command_tokenizer( $single_command ); - if ( empty( $command_tokens ) ) { - $this->_query = 'SELECT 1=1'; - return $this->_query; - } - $tokens = array_merge( $tmp_tokens, $command_tokens ); - $command_name = strtolower( $tokens['command'] ); - switch ( $command_name ) { - case 'add column': - case 'rename to': - case 'add index': - case 'drop index': - $tmp_query = $this->handle_single_command( $tokens ); - break; - - case 'add primary key': - $tmp_query = $this->handle_add_primary_key( $tokens ); - break; - - case 'drop primary key': - $tmp_query = $this->handle_drop_primary_key( $tokens ); - break; - - case 'modify column': - $tmp_query = $this->handle_modify_command( $tokens ); - break; - - case 'change column': - $tmp_query = $this->handle_change_command( $tokens ); - break; - - case 'alter column': - $tmp_query = $this->handle_alter_command( $tokens ); - break; - - default: - break; - } - if ( ! is_array( $tmp_query ) ) { - $this->_query[] = $tmp_query; - } else { - $this->_query = $tmp_query; - } - if ( '' !== $re_command ) { - $this->_query = array_merge( $this->_query, array( 'recursion' => $re_command ) ); - } - - return $this->_query; - } - - /** - * Function to analyze ALTER TABLE command and sets the data to an array. - * - * @param string $command The command string. - * - * @return array - * @access private - */ - private function command_tokenizer( $command ) { - $tokens = array(); - $pattern = '/^(ADD|DROP|RENAME|MODIFY|CHANGE|ALTER)\\s*(\\w+)?\\s*(\\w+(\(.+\)|))?\\s*/ims'; - if ( preg_match( $pattern, $command, $match ) ) { - $the_rest = str_ireplace( $match[0], '', $command ); - $match_1 = trim( $match[1] ); - $match_2 = trim( $match[2] ); - $match_3 = isset( $match[3] ) ? trim( $match[3] ) : ''; - switch ( strtolower( $match_1 ) ) { - case 'add': - if ( in_array( strtolower( $match_2 ), array( 'fulltext', 'constraint', 'foreign' ), true ) ) { - break; - } - - if ( stripos( 'column', $match_2 ) !== false ) { - $tokens['command'] = $match_1 . ' ' . $match_2; - $tokens['column_name'] = $match_3; - $tokens['column_def'] = trim( $the_rest ); - } elseif ( stripos( 'primary', $match_2 ) !== false ) { - $tokens['command'] = $match_1 . ' ' . $match_2 . ' ' . $match_3; - $tokens['column_name'] = $the_rest; - } elseif ( stripos( 'unique', $match_2 ) !== false ) { - list($index_name, $col_name) = preg_split( '/[\(\)]/s', trim( $the_rest ), -1, PREG_SPLIT_DELIM_CAPTURE ); - $tokens['unique'] = true; - $tokens['command'] = $match_1 . ' ' . $match_3; - $tokens['index_name'] = trim( $index_name ); - $tokens['column_name'] = '(' . trim( $col_name ) . ')'; - } elseif ( in_array( strtolower( $match_2 ), array( 'index', 'key' ), true ) ) { - $tokens['command'] = $match_1 . ' ' . $match_2; - $tokens['index_name'] = $match_3; - if ( '' === $match_3 ) { - $tokens['index_name'] = str_replace( array( '(', ')' ), '', $the_rest ); - } - $tokens['column_name'] = trim( $the_rest ); - } else { - $tokens['command'] = $match_1 . ' COLUMN'; - $tokens['column_name'] = $match_2; - $tokens['column_def'] = $match_3 . ' ' . $the_rest; - } - break; - - case 'drop': - if ( stripos( 'column', $match_2 ) !== false ) { - $tokens['command'] = $match_1 . ' ' . $match_2; - $tokens['column_name'] = trim( $match_3 ); - } elseif ( stripos( 'primary', $match_2 ) !== false ) { - $tokens['command'] = $match_1 . ' ' . $match_2 . ' ' . $match_3; - } elseif ( in_array( strtolower( $match_2 ), array( 'index', 'key' ), true ) ) { - $tokens['command'] = $match_1 . ' ' . $match_2; - $tokens['index_name'] = $match_3; - } elseif ( stripos( 'primary', $match_2 ) !== false ) { - $tokens['command'] = $match_1 . ' ' . $match_2 . ' ' . $match_3; - } else { - $tokens['command'] = $match_1 . ' COLUMN'; - $tokens['column_name'] = $match_2; - } - break; - - case 'rename': - if ( stripos( 'to', $match_2 ) !== false ) { - $tokens['command'] = $match_1 . ' ' . $match_2; - $tokens['column_name'] = $match_3; - } else { - $tokens['command'] = $match_1 . ' TO'; - $tokens['column_name'] = $match_2; - } - break; - - case 'modify': - if ( stripos( 'column', $match_2 ) !== false ) { - $tokens['command'] = $match_1 . ' ' . $match_2; - $tokens['column_name'] = $match_3; - $tokens['column_def'] = trim( $the_rest ); - } else { - $tokens['command'] = $match_1 . ' COLUMN'; - $tokens['column_name'] = $match_2; - $tokens['column_def'] = $match_3 . ' ' . trim( $the_rest ); - } - break; - - case 'change': - $the_rest = trim( $the_rest ); - if ( stripos( 'column', $match_2 ) !== false ) { - $tokens['command'] = $match_1 . ' ' . $match_2; - $tokens['old_column'] = $match_3; - list($new_col) = explode( ' ', $the_rest ); - $tmp_col = preg_replace( '/\(.+?\)/im', '', $new_col ); - if ( array_key_exists( strtolower( $tmp_col ), $this->array_types ) ) { - $tokens['column_def'] = $the_rest; - } else { - $tokens['new_column'] = $new_col; - $col_def = str_replace( $new_col, '', $the_rest ); - $tokens['column_def'] = trim( $col_def ); - } - } else { - $tokens['command'] = $match_1 . ' column'; - $tokens['old_column'] = $match_2; - $tmp_col = preg_replace( '/\(.+?\)/im', '', $match_3 ); - if ( array_key_exists( strtolower( $tmp_col ), $this->array_types ) ) { - $tokens['column_def'] = $match_3 . ' ' . $the_rest; - } else { - $tokens['new_column'] = $match_3; - $tokens['column_def'] = $the_rest; - } - } - break; - - case 'alter': - $tokens['default_command'] = 'DROP DEFAULT'; - if ( stripos( 'column', $match_2 ) !== false ) { - $tokens['command'] = $match_1 . ' ' . $match_2; - $tokens['column_name'] = $match_3; - list($set_or_drop) = explode( ' ', $the_rest ); - if ( stripos( 'set', $set_or_drop ) !== false ) { - $tokens['default_command'] = 'SET DEFAULT'; - $default_value = str_ireplace( 'set default', '', $the_rest ); - $tokens['default_value'] = trim( $default_value ); - } - } else { - $tokens['command'] = $match_1 . ' COLUMN'; - $tokens['column_name'] = $match_2; - if ( stripos( 'set', $match_3 ) !== false ) { - $tokens['default_command'] = 'SET DEFAULT'; - $default_value = str_ireplace( 'default', '', $the_rest ); - $tokens['default_value'] = trim( $default_value ); - } - } - break; - - default: - break; - } - } - return $tokens; - } - - /** - * Function to handle single command. - * - * @access private - * - * @param array $queries An array of string queries. - * - * @return string - */ - private function handle_single_command( $queries ) { - $tokenized_query = $queries; - - // Command is "add column". - if ( stripos( $tokenized_query['command'], 'add column' ) !== false ) { - $column_def = $this->convert_field_types( $tokenized_query['column_name'], $tokenized_query['column_def'] ); - return "ALTER TABLE {$tokenized_query['table_name']} ADD COLUMN {$tokenized_query['column_name']} $column_def"; - } - - // Command is "rename". - if ( stripos( $tokenized_query['command'], 'rename' ) !== false ) { - return "ALTER TABLE {$tokenized_query['table_name']} RENAME TO {$tokenized_query['column_name']}"; - } - - // Command is "add index". - if ( stripos( $tokenized_query['command'], 'add index' ) !== false ) { - $unique = isset( $tokenized_query['unique'] ) ? 'UNIQUE' : ''; - return "CREATE $unique INDEX IF NOT EXISTS {$tokenized_query['index_name']} ON {$tokenized_query['table_name']} {$tokenized_query['column_name']}"; - } - - // Command is "drop index". - if ( stripos( $tokenized_query['command'], 'drop index' ) !== false ) { - return "DROP INDEX IF EXISTS {$tokenized_query['index_name']}"; - } - - // Fallback. - return 'SELECT 1=1'; - } - - /** - * Function to handle ADD PRIMARY KEY. - * - * @access private - * - * @param array $queries An array of string queries. - * - * @return array - */ - private function handle_add_primary_key( $queries ) { - $tokenized_query = $queries; - $tbl_name = $tokenized_query['table_name']; - $temp_table = 'temp_' . $tokenized_query['table_name']; - $query_obj = ( new Perflab_SQLite_DB() )->get_results( "SELECT sql FROM sqlite_master WHERE tbl_name='$tbl_name'" ); - for ( $i = 0; $i < count( $query_obj ); $i++ ) { - $index_queries[ $i ] = $query_obj[ $i ]->sql; - } - $table_query = ", PRIMARY KEY {$tokenized_query['column_name']}"; - $query = array( - $table_query, - "INSERT INTO $temp_table SELECT * FROM {$tokenized_query['table_name']}", - "DROP TABLE IF EXISTS {$tokenized_query['table_name']}", - "ALTER TABLE $temp_table RENAME TO {$tokenized_query['table_name']}", - ); - - foreach ( $index_queries as $index ) { - $query[] = $index; - } - - return $query; - } - - /** - * Function to handle DROP PRIMARY KEY. - * - * @access private - * - * @param array $queries An of string $queries. - * - * @return array - */ - private function handle_drop_primary_key( $queries ) { - $tokenized_query = $queries; - $temp_table = 'temp_' . $tokenized_query['table_name']; - $query_obj = ( new Perflab_SQLite_DB() )->get_results( "SELECT sql FROM sqlite_master WHERE tbl_name='{$tokenized_query['table_name']}'" ); - for ( $i = 0; $i < count( $query_obj ); $i++ ) { - $index_queries[ $i ] = $query_obj[ $i ]->sql; - } - $table_query = array_shift( $index_queries ); - $pattern1 = '/^\\s*PRIMARY\\s*KEY\\s*\(.*\)/im'; - $pattern2 = '/^\\s*.*(PRIMARY\\s*KEY\\s*(:?AUTOINCREMENT|))\\s*(?!\()/im'; - if ( preg_match( $pattern1, $table_query, $match ) ) { - $table_query = str_replace( $match[0], '', $table_query ); - } elseif ( preg_match( $pattern2, $table_query, $match ) ) { - $table_query = str_replace( $match[1], '', $table_query ); - } - $table_query = str_replace( $tokenized_query['table_name'], $temp_table, $table_query ); - $query = array( - $table_query, - "INSERT INTO $temp_table SELECT * FROM {$tokenized_query['table_name']}", - "DROP TABLE IF EXISTS {$tokenized_query['table_name']}", - "ALTER TABLE $temp_table RENAME TO {$tokenized_query['table_name']}", - ); - foreach ( $index_queries as $index ) { - $query[] = $index; - } - - return $query; - } - - /** - * Function to handle MODIFY COLUMN. - * - * @access private - * - * @param array $queries An array of string queries. - * - * @return string|array - */ - private function handle_modify_command( $queries ) { - $tokenized_query = $queries; - $temp_table = 'temp_' . $tokenized_query['table_name']; - $column_def = $this->convert_field_types( $tokenized_query['column_name'], $tokenized_query['column_def'] ); - $query_obj = ( new Perflab_SQLite_DB() )->get_results( "SELECT sql FROM sqlite_master WHERE tbl_name='{$tokenized_query['table_name']}'" ); - for ( $i = 0; $i < count( $query_obj ); $i++ ) { - $index_queries[ $i ] = $query_obj[ $i ]->sql; - } - $create_query = array_shift( $index_queries ); - if ( stripos( $create_query, $tokenized_query['column_name'] ) === false ) { - return 'SELECT 1=1'; - } - $pattern = "/{$tokenized_query['column_name']}\\s*{$column_def}\\s*[,)]/i"; - if ( preg_match( $pattern, $create_query ) ) { - return 'SELECT 1=1'; - } - $pattern_1 = "/{$tokenized_query['table_name']}/i"; - $pattern_2 = "/\\b{$tokenized_query['column_name']}\\s*.*(?=,)/ims"; - $pattern_3 = "/\\b{$tokenized_query['column_name']}\\s*.*(?=\))/ims"; - - $create_query = preg_replace( $pattern_1, $temp_table, $create_query ); - if ( preg_match( $pattern_2, $create_query ) ) { - $create_query = preg_replace( $pattern_2, "{$tokenized_query['column_name']} {$column_def}", $create_query ); - } elseif ( preg_match( $pattern_3, $create_query ) ) { - $create_query = preg_replace( $pattern_3, "{$tokenized_query['column_name']} {$column_def}", $create_query ); - } - $query = array( - $create_query, - "INSERT INTO $temp_table SELECT * FROM {$tokenized_query['table_name']}", - "DROP TABLE IF EXISTS {$tokenized_query['table_name']}", - "ALTER TABLE $temp_table RENAME TO {$tokenized_query['table_name']}", - ); - foreach ( $index_queries as $index ) { - $query[] = $index; - } - - return $query; - } - - /** - * Function to handle CHANGE COLUMN. - * - * @access private - * - * @param array $queries An array of string queries. - * - * @return string|array - */ - private function handle_change_command( $queries ) { - $col_check = false; - $old_fields = ''; - $tokenized_query = $queries; - $temp_table = 'temp_' . $tokenized_query['table_name']; - $column_name = $tokenized_query['old_column']; - if ( isset( $tokenized_query['new_column'] ) ) { - $column_name = $tokenized_query['new_column']; - } - $column_def = $this->convert_field_types( $column_name, $tokenized_query['column_def'] ); - $col_obj = ( new Perflab_SQLite_DB() )->get_results( "SHOW COLUMNS FROM {$tokenized_query['table_name']}" ); - foreach ( $col_obj as $col ) { - if ( stripos( $col->Field, $tokenized_query['old_column'] ) !== false ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $col_check = true; - } - $old_fields .= $col->Field . ','; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - } - if ( false === $col_check ) { - return 'SELECT 1=1'; - } - $old_fields = rtrim( $old_fields, ',' ); - $new_fields = str_ireplace( $tokenized_query['old_column'], $column_name, $old_fields ); - $query_obj = ( new Perflab_SQLite_DB() )->get_results( "SELECT sql FROM sqlite_master WHERE tbl_name='{$tokenized_query['table_name']}'" ); - for ( $i = 0; $i < count( $query_obj ); $i++ ) { - $index_queries[ $i ] = $query_obj[ $i ]->sql; - } - $create_query = array_shift( $index_queries ); - $create_query = preg_replace( "/{$tokenized_query['table_name']}/i", $temp_table, $create_query ); - - $pattern_1 = "/\\b{$tokenized_query['old_column']}\\s*(.+?)(?=,)/ims"; - $pattern_2 = "/\\b{$tokenized_query['old_column']}\\s*(.+?)(?=\))/ims"; - if ( preg_match( $pattern_1, $create_query, $match ) ) { - if ( stripos( trim( $match[1] ), $column_def ) !== false ) { - return 'SELECT 1=1'; - } - $pattern = "/\\b{$tokenized_query['old_column']}\\s*.+?(?=,)/ims"; - $create_query = preg_replace( $pattern, "{$column_name} {$column_def}", $create_query, 1 ); - } elseif ( preg_match( $pattern_2, $create_query, $match ) ) { - if ( stripos( trim( $match[1] ), $column_def ) !== false ) { - return 'SELECT 1=1'; - } - $pattern = "/\\b{$tokenized_query['old_column']}\\s*.*(?=\))/ims"; - $create_query = preg_replace( $pattern, "{$column_name} {$column_def}", $create_query, 1 ); - } - $query = array( - $create_query, - "INSERT INTO $temp_table ($new_fields) SELECT $old_fields FROM {$tokenized_query['table_name']}", - "DROP TABLE IF EXISTS {$tokenized_query['table_name']}", - "ALTER TABLE $temp_table RENAME TO {$tokenized_query['table_name']}", - ); - foreach ( $index_queries as $index ) { - $query[] = $index; - } - - return $query; - } - - /** - * Function to handle ALTER COLUMN. - * - * @access private - * - * @param array $queries An array of string queries. - * - * @return string|array - */ - private function handle_alter_command( $queries ) { - $tokenized_query = $queries; - $temp_table = 'temp_' . $tokenized_query['table_name']; - $def_value = null; - if ( isset( $tokenized_query['default_value'] ) ) { - $def_value = $this->convert_field_types( $tokenized_query['column_name'], $tokenized_query['default_value'] ); - $def_value = 'DEFAULT ' . $def_value; - } - $query_obj = ( new Perflab_SQLite_DB() )->get_results( "SELECT sql FROM sqlite_master WHERE tbl_name='{$tokenized_query['table_name']}'" ); - for ( $i = 0; $i < count( $query_obj ); $i++ ) { - $index_queries[ $i ] = $query_obj[ $i ]->sql; - } - $create_query = array_shift( $index_queries ); - if ( stripos( $create_query, $tokenized_query['column_name'] ) === false ) { - return 'SELECT 1=1'; - } - $pattern_1 = "/\\s*({$tokenized_query['column_name']})\\s*(.*)?(DEFAULT\\s*.*)[,)]/im"; - $pattern_2 = "/\\s*({$tokenized_query['column_name']})\\s*(.*)?[,)]/im"; - if ( preg_match( $pattern_1, $create_query, $match ) ) { - $col_name = trim( $match[1] ); - $col_def = trim( $match[2] ); - $col_def_esc = str_replace( array( '(', ')' ), array( '\(', '\)' ), $col_def ); - $checked_col_def = $this->convert_field_types( $col_name, $col_def ); - $old_default = trim( $match[3] ); - $replacement = $col_name . ' ' . $checked_col_def; - if ( ! is_null( $def_value ) ) { - $replacement .= ' ' . $def_value; - } - $pattern = "/$col_name\\s*$col_def_esc\\s*$old_default/im"; - $create_query = preg_replace( $pattern, $replacement, $create_query ); - $create_query = str_ireplace( $tokenized_query['table_name'], $temp_table, $create_query ); - } elseif ( preg_match( $pattern_2, $create_query, $match ) ) { - $col_name = trim( $match[1] ); - $col_def = trim( $match[2] ); - $col_def_esc = str_replace( array( '(', ')' ), array( '\(', '\)' ), $col_def ); - $checked_col_def = $this->convert_field_types( $col_name, $col_def ); - $replacement = $col_name . ' ' . $checked_col_def; - if ( ! is_null( $def_value ) ) { - $replacement .= ' ' . $def_value; - } - $pattern = "/$col_name\\s*$col_def_esc/im"; - $create_query = preg_replace( $pattern, $replacement, $create_query ); - $create_query = str_ireplace( $tokenized_query['table_name'], $temp_table, $create_query ); - } else { - return 'SELECT 1=1'; - } - $query = array( - $create_query, - "INSERT INTO $temp_table SELECT * FROM {$tokenized_query['table_name']}", - "DROP TABLE IF EXISTS {$tokenized_query['table_name']}", - "ALTER TABLE $temp_table RENAME TO {$tokenized_query['table_name']}", - ); - foreach ( $index_queries as $index ) { - $query[] = $index; - } - - return $query; - } - - /** - * Function to change the field definition to SQLite compatible data type. - * - * @access private - * - * @param string $col_name Column name. - * @param string $col_def Column definition. - * - * @return string - */ - private function convert_field_types( $col_name, $col_def ) { - $array_curtime = array( 'current_timestamp', 'current_time', 'current_date' ); - $array_reptime = array( "'0000-00-00 00:00:00'", "'0000-00-00 00:00:00'", "'0000-00-00'" ); - $def_string = str_replace( '`', '', $col_def ); - foreach ( $this->array_types as $o => $r ) { - $pattern = "/\\b$o\\s*(\([^\)]*\)*)?\\s*/ims"; - if ( preg_match( $pattern, $def_string ) ) { - $def_string = preg_replace( $pattern, "$r ", $def_string ); - break; - } - } - $def_string = preg_replace( '/unsigned/im', '', $def_string ); - $def_string = preg_replace( '/auto_increment/im', 'PRIMARY KEY AUTOINCREMENT', $def_string ); - // When you use ALTER TABLE ADD, you can't use current_*. so we replace. - $def_string = str_ireplace( $array_curtime, $array_reptime, $def_string ); - // This is enum. - $pattern_enum = '/enum\((.*?)\)([^,\)]*)/ims'; - if ( preg_match( $pattern_enum, $col_def, $matches ) ) { - $def_string = 'TEXT' . $matches[2] . ' CHECK (' . $col_name . ' IN (' . $matches[1] . '))'; - } - - return $def_string; - } - - /** - * Variable to store the data definition table. - * - * @access private - * @var associative array - */ - private $array_types = array( - 'bit' => 'INTEGER', - 'bool' => 'INTEGER', - 'boolean' => 'INTEGER', - 'tinyint' => 'INTEGER', - 'smallint' => 'INTEGER', - 'mediumint' => 'INTEGER', - 'bigint' => 'INTEGER', - 'integer' => 'INTEGER', - 'int' => 'INTEGER', - 'float' => 'REAL', - 'double' => 'REAL', - 'decimal' => 'REAL', - 'dec' => 'REAL', - 'numeric' => 'REAL', - 'fixed' => 'REAL', - 'datetime' => 'TEXT', - 'date' => 'TEXT', - 'timestamp' => 'TEXT', - 'time' => 'TEXT', - 'year' => 'TEXT', - 'varchar' => 'TEXT', - 'char' => 'TEXT', - 'varbinary' => 'BLOB', - 'binary' => 'BLOB', - 'tinyblob' => 'BLOB', - 'mediumblob' => 'BLOB', - 'longblob' => 'BLOB', - 'blob' => 'BLOB', - 'tinytext' => 'TEXT', - 'mediumtext' => 'TEXT', - 'longtext' => 'TEXT', - 'text' => 'TEXT', - ); -} diff --git a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-create-query.php b/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-create-query.php deleted file mode 100644 index c9823d655f..0000000000 --- a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-create-query.php +++ /dev/null @@ -1,497 +0,0 @@ -_query = $query; - $this->_errors [] = ''; - if ( preg_match( '/^CREATE\\s*(UNIQUE|FULLTEXT|)\\s*INDEX/ims', $this->_query, $match ) ) { - // We manipulate CREATE INDEX query in the Perflab_SQLite_PDO_Engine class. - // FULLTEXT index creation is simply ignored. - if ( isset( $match[1] ) && stripos( $match[1], 'fulltext' ) !== false ) { - return 'SELECT 1=1'; - } - return $this->_query; - } - if ( preg_match( '/^CREATE\\s*(TEMP|TEMPORARY|)\\s*TRIGGER\\s*/im', $this->_query ) ) { - // If WordPress comes to use foreign key constraint, trigger will be needed. - // We don't use it for now. - return $this->_query; - } - $this->strip_backticks(); - $this->quote_illegal_field(); - $this->get_table_name(); - $this->rewrite_comments(); - $this->rewrite_field_types(); - $this->rewrite_character_set(); - $this->rewrite_engine_info(); - $this->rewrite_unsigned(); - $this->rewrite_autoincrement(); - $this->rewrite_primary_key(); - $this->rewrite_foreign_key(); - $this->rewrite_unique_key(); - $this->rewrite_enum(); - $this->rewrite_set(); - $this->rewrite_key(); - $this->add_if_not_exists(); - - return $this->post_process(); - } - - /** - * Method to get table name from the query string. - * - * 'IF NOT EXISTS' clause is removed for the easy regular expression usage. - * It will be added at the end of the process. - * - * @access private - */ - private function get_table_name() { - // $pattern = '/^\\s*CREATE\\s*(TEMP|TEMPORARY)?\\s*TABLE\\s*(IF NOT EXISTS)?\\s*([^\(]*)/imsx'; - $pattern = '/^\\s*CREATE\\s*(?:TEMP|TEMPORARY)?\\s*TABLE\\s*(?:IF\\s*NOT\\s*EXISTS)?\\s*([^\(]*)/imsx'; - if ( preg_match( $pattern, $this->_query, $matches ) ) { - $this->table_name = trim( $matches[1] ); - } - } - - /** - * Method to change the MySQL field types to SQLite compatible types. - * - * If column name is the same as the key value, e.g. "date" or "timestamp", - * and the column is on the top of the line, we add a single quote and avoid - * to be replaced. But this doesn't work if that column name is in the middle - * of the line. - * Order of the key value is important. Don't change it. - * - * @access private - */ - private function rewrite_field_types() { - $array_types = array( - 'bit' => 'integer', - 'bool' => 'integer', - 'boolean' => 'integer', - 'tinyint' => 'integer', - 'smallint' => 'integer', - 'mediumint' => 'integer', - 'int' => 'integer', - 'integer' => 'integer', - 'bigint' => 'integer', - 'float' => 'real', - 'double' => 'real', - 'decimal' => 'real', - 'dec' => 'real', - 'numeric' => 'real', - 'fixed' => 'real', - 'date' => 'text', - 'datetime' => 'text', - 'timestamp' => 'text', - 'time' => 'text', - 'year' => 'text', - 'char' => 'text', - 'varchar' => 'text', - 'binary' => 'integer', - 'varbinary' => 'blob', - 'tinyblob' => 'blob', - 'tinytext' => 'text', - 'blob' => 'blob', - 'text' => 'text', - 'mediumblob' => 'blob', - 'mediumtext' => 'text', - 'longblob' => 'blob', - 'longtext' => 'text', - ); - foreach ( $array_types as $o => $r ) { - if ( preg_match( "/^\\s*(?_query, $match ) ) { - $ptrn = "/$match[1]/im"; - $replaced = str_ireplace( $ptrn, '#placeholder#', $this->_query ); - $replaced = str_ireplace( $o, "'{$o}'", $replaced ); - $this->_query = str_replace( '#placeholder#', $ptrn, $replaced ); - } - $pattern = "/\\b(?_query ) ) { - // ; - } else { - $this->_query = preg_replace( $pattern, " $r ", $this->_query ); - } - } - } - - /** - * Method for stripping the comments from the SQL statement. - * - * @access private - */ - private function rewrite_comments() { - $this->_query = preg_replace( - '/# --------------------------------------------------------/', - '-- ******************************************************', - $this->_query - ); - $this->_query = preg_replace( '/#/', '--', $this->_query ); - } - - /** - * Method for stripping the engine and other stuffs. - * - * TYPE, ENGINE and AUTO_INCREMENT are removed here. - * - * @access private - */ - private function rewrite_engine_info() { - $this->_query = preg_replace( '/\\s*(TYPE|ENGINE)\\s*=\\s*.*(?_query ); - $this->_query = preg_replace( '/ AUTO_INCREMENT\\s*=\\s*[0-9]*/ims', '', $this->_query ); - } - - /** - * Method for stripping unsigned. - * - * SQLite doesn't have unsigned int data type. So UNSIGNED INT(EGER) is converted - * to INTEGER here. - * - * @access private - */ - private function rewrite_unsigned() { - $this->_query = preg_replace( '/\\bunsigned\\b/ims', ' ', $this->_query ); - } - - /** - * Method for rewriting primary key auto_increment. - * - * If the field type is 'INTEGER PRIMARY KEY', it is automatically autoincremented - * by SQLite. There's a little difference between PRIMARY KEY and AUTOINCREMENT, so - * we may well convert to PRIMARY KEY only. - * - * @access private - */ - private function rewrite_autoincrement() { - $this->_query = preg_replace( - '/\\bauto_increment\\s*primary\\s*key\\s*(,)?/ims', - ' PRIMARY KEY AUTOINCREMENT \\1', - $this->_query, - -1, - $count - ); - $this->_query = preg_replace( - '/\\bauto_increment\\b\\s*(,)?/ims', - ' PRIMARY KEY AUTOINCREMENT $1', - $this->_query, - -1, - $count - ); - if ( $count > 0 ) { - $this->has_primary_key = true; - } - } - - /** - * Method for rewriting primary key. - * - * @access private - */ - private function rewrite_primary_key() { - if ( $this->has_primary_key ) { - $this->_query = preg_replace( '/\\s*primary key\\s*.*?\([^\)]*\)\\s*(,|)/i', ' ', $this->_query ); - } else { - // If primary key has an index name, we remove that name. - $this->_query = preg_replace( '/\\bprimary\\s*key\\s*.*?\\s*(\(.*?\))/im', 'PRIMARY KEY \\1', $this->_query ); - } - } - - /** - * Method for rewriting foreign key. - * - * @access private - */ - private function rewrite_foreign_key() { - $pattern = '/\\s*foreign\\s*key\\s*(|.*?)\([^\)]+?\)\\s*references\\s*.*/i'; - if ( preg_match_all( $pattern, $this->_query, $match ) ) { - if ( isset( $match[1] ) ) { - $this->_query = str_ireplace( $match[1], '', $this->_query ); - } - } - } - - /** - * Method for rewriting unique key. - * - * @access private - */ - private function rewrite_unique_key() { - $this->_query = preg_replace_callback( - '/\\bunique key\\b([^\(]*)(\(.*\))/im', - array( $this, '_rewrite_unique_key' ), - $this->_query - ); - } - - /** - * Callback method for rewrite_unique_key. - * - * @access private - * - * @param array $matches An array of matches from the Regex. - * @return string - */ - private function _rewrite_unique_key( $matches ) { - $index_name = trim( $matches[1] ); - $col_name = trim( $matches[2] ); - $tbl_name = $this->table_name; - if ( preg_match( '/\(\\d+?\)/', $col_name ) ) { - $col_name = preg_replace( '/\(\\d+?\)/', '', $col_name ); - } - $_wpdb = new Perflab_SQLite_DB(); - $results = $_wpdb->get_results( "SELECT name FROM sqlite_master WHERE type='index'" ); - $_wpdb = null; - if ( $results ) { - foreach ( $results as $result ) { - if ( $result->name === $index_name ) { - $r = rand( 0, 50 ); - $index_name = $index_name . "_$r"; - break; - } - } - } - $index_name = str_replace( ' ', '', $index_name ); - $this->index_queries[] = "CREATE UNIQUE INDEX $index_name ON " . $tbl_name . $col_name; - - return ''; - } - - /** - * Method for handling ENUM fields. - * - * SQLite doesn't support enum, so we change it to check constraint. - * - * @access private - */ - private function rewrite_enum() { - $pattern = '/(,|\))([^,]*)enum\((.*?)\)([^,\)]*)/ims'; - $this->_query = preg_replace_callback( $pattern, array( $this, '_rewrite_enum' ), $this->_query ); - } - - /** - * Call back method for rewrite_enum() and rewrite_set(). - * - * @access private - * - * @param array $matches An array of matches from the Regex. - * - * @return string - */ - private function _rewrite_enum( $matches ) { - $output = $matches[1] . ' ' . $matches[2] . ' TEXT ' . $matches[4] . ' CHECK (' . $matches[2] . ' IN (' . $matches[3] . ')) '; - - return $output; - } - - /** - * Method for rewriting usage of set. - * - * It is similar but not identical to enum. SQLite does not support either. - * - * @access private - */ - private function rewrite_set() { - $pattern = '/\b(\w)*\bset\\s*\((.*?)\)\\s*(.*?)(,)*/ims'; - $this->_query = preg_replace_callback( $pattern, array( $this, '_rewrite_enum' ), $this->_query ); - } - - /** - * Method for rewriting usage of key to create an index. - * - * SQLite cannot create non-unique indices as part of the create query, - * so we need to create an index by hand and append it to the create query. - * - * @access private - */ - private function rewrite_key() { - $this->_query = preg_replace_callback( - '/,\\s*(KEY|INDEX)\\s*(\\w+)?\\s*(\(.+\))/im', - array( $this, '_rewrite_key' ), - $this->_query - ); - } - - /** - * Callback method for rewrite_key. - * - * @param array $matches an array of matches from the Regex. - * - * @access private - * @return string - */ - private function _rewrite_key( $matches ) { - $index_name = trim( $matches[2] ); - $col_name = trim( $matches[3] ); - if ( preg_match( '/\([0-9]+?\)/', $col_name, $match ) ) { - $col_name = preg_replace_callback( '/\([0-9]+?\)/', array( $this, '_remove_length' ), $col_name ); - } - $tbl_name = $this->table_name; - $_wpdb = new Perflab_SQLite_DB(); - $results = $_wpdb->get_results( "SELECT name FROM sqlite_master WHERE type='index'" ); - $_wpdb = null; - if ( $results ) { - foreach ( $results as $result ) { - if ( $result->name === $index_name ) { - $r = rand( 0, 50 ); - $index_name = $index_name . "_$r"; - break; - } - } - } - $this->index_queries[] = 'CREATE INDEX ' . $index_name . ' ON ' . $tbl_name . $col_name; - - return ''; - } - - /** - * Call back method to remove unnecessary string. - * - * This method is deprecated. - * - * @param string $match The string to be removed. Not used. - * - * @return string whose length is zero - * @access private - */ - private function _remove_length( $match ) { - return ''; - } - - /** - * Method to assemble the main query and index queries into an array. - * - * It return the array of the queries to be executed separately. - * - * @return array - * @access private - */ - private function post_process() { - $mainquery = $this->_query; - do { - $count = 0; - $mainquery = preg_replace( '/,\\s*\)/imsx', ')', $mainquery, -1, $count ); - } while ( $count > 0 ); - do { - $count = 0; - $mainquery = preg_replace( '/\(\\s*?,/imsx', '(', $mainquery, -1, $count ); - } while ( $count > 0 ); - $return_val[] = $mainquery; - $return_val = array_merge( $return_val, $this->index_queries ); - - return $return_val; - } - - /** - * Method to add IF NOT EXISTS to query string. - * - * This adds IF NOT EXISTS to every query string, which prevent the exception - * from being thrown. - * - * @access private - */ - private function add_if_not_exists() { - $pattern_table = '/^\\s*CREATE\\s*(TEMP|TEMPORARY)?\\s*TABLE\\s*(IF NOT EXISTS)?\\s*/ims'; - $this->_query = preg_replace( $pattern_table, 'CREATE $1 TABLE IF NOT EXISTS ', $this->_query ); - $pattern_index = '/^\\s*CREATE\\s*(UNIQUE)?\\s*INDEX\\s*(IF NOT EXISTS)?\\s*/ims'; - for ( $i = 0; $i < count( $this->index_queries ); $i++ ) { - $this->index_queries[ $i ] = preg_replace( - $pattern_index, - 'CREATE $1 INDEX IF NOT EXISTS ', - $this->index_queries[ $i ] - ); - } - } - - /** - * Method to strip back quotes. - * - * @access private - */ - private function strip_backticks() { - $this->_query = str_replace( '`', '', $this->_query ); - foreach ( $this->index_queries as &$query ) { - $query = str_replace( '`', '', $query ); - } - } - - /** - * Method to remove the character set information from within mysql queries. - * - * This removes DEFAULT CHAR(ACTER) SET and COLLATE, which is meaningless for - * SQLite. - * - * @access private - */ - private function rewrite_character_set() { - $pattern_charset = '/\\b(default\\s*character\\s*set|default\\s*charset|character\\s*set)\\s*(?_query = preg_replace( $patterns, '', $this->_query ); - } - - /** - * Method to quote illegal field name for SQLite - * - * @access private - */ - private function quote_illegal_field() { - $this->_query = preg_replace( "/^\\s*(?_query ); - } -} diff --git a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-object-array.php b/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-object-array.php deleted file mode 100644 index d09c81bdd1..0000000000 --- a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-object-array.php +++ /dev/null @@ -1,35 +0,0 @@ - $value ) { - if ( ! $node ) { - $node =& $this; - } - if ( is_array( $value ) ) { - $node->$key = new stdClass(); - self::__construct( $value, $node->$key ); - } else { - $node->$key = $value; - } - } - } -} diff --git a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-pdo-driver.php b/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-pdo-driver.php deleted file mode 100644 index f05d229bda..0000000000 --- a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-pdo-driver.php +++ /dev/null @@ -1,793 +0,0 @@ -query_type = $query_type; - $this->_query = $query; - $this->parse_query(); - switch ( $this->query_type ) { - case 'truncate': - $this->handle_truncate_query(); - break; - - case 'alter': - $this->handle_alter_query(); - break; - - case 'create': - $this->handle_create_query(); - break; - - case 'describe': - case 'desc': - $this->handle_describe_query(); - break; - - case 'show': - $this->handle_show_query(); - break; - - case 'showcolumns': - $this->handle_show_columns_query(); - break; - - case 'showindex': - $this->handle_show_index(); - break; - - case 'select': - // $this->strip_backticks(); - $this->handle_sql_count(); - $this->rewrite_date_sub(); - $this->delete_index_hints(); - $this->rewrite_regexp(); - // $this->rewrite_boolean(); - $this->fix_date_quoting(); - $this->rewrite_between(); - $this->handle_orderby_field(); - break; - - case 'insert': - // $this->safe_strip_backticks(); - $this->execute_duplicate_key_update(); - $this->rewrite_insert_ignore(); - $this->rewrite_regexp(); - $this->fix_date_quoting(); - break; - - case 'update': - // $this->safe_strip_backticks(); - $this->rewrite_update_ignore(); - // $this->_rewrite_date_sub(); - $this->rewrite_limit_usage(); - $this->rewrite_order_by_usage(); - $this->rewrite_regexp(); - $this->rewrite_between(); - break; - - case 'delete': - // $this->strip_backticks(); - $this->rewrite_limit_usage(); - $this->rewrite_order_by_usage(); - $this->rewrite_date_sub(); - $this->rewrite_regexp(); - $this->delete_workaround(); - break; - - case 'replace': - // $this->safe_strip_backticks(); - $this->rewrite_date_sub(); - $this->rewrite_regexp(); - break; - - case 'optimize': - $this->rewrite_optimize(); - break; - - case 'pragma': - break; - - default: - if ( defined( WP_DEBUG ) && WP_DEBUG ) { - break; - } - $this->return_true(); - break; - } - - return $this->_query; - } - - /** - * Method to parse query string and determine which operation is needed. - * - * Remove backticks and change true/false values into 1/0. And determines - * if rewriting CALC_FOUND_ROWS or ON DUPLICATE KEY UPDATE etc is needed. - * - * @access private - */ - private function parse_query() { - $tokens = preg_split( "/(\\\'|''|')/s", $this->_query, -1, PREG_SPLIT_DELIM_CAPTURE ); - $literal = false; - $query_string = ''; - foreach ( $tokens as $token ) { - if ( "'" === $token ) { - $literal = ! $literal; - } else { - if ( false === $literal ) { - if ( strpos( $token, '`' ) !== false ) { - $token = str_replace( '`', '', $token ); - } - if ( preg_match( '/\\bTRUE\\b/i', $token ) ) { - $token = str_ireplace( 'TRUE', '1', $token ); - } - if ( preg_match( '/\\bFALSE\\b/i', $token ) ) { - $token = str_ireplace( 'FALSE', '0', $token ); - } - if ( stripos( $token, 'SQL_CALC_FOUND_ROWS' ) !== false ) { - $this->rewrite_calc_found = true; - } - if ( stripos( $token, 'ON DUPLICATE KEY UPDATE' ) !== false ) { - $this->rewrite_duplicate_key = true; - } - if ( stripos( $token, 'USE INDEX' ) !== false ) { - $this->rewrite_index_hint = true; - } - if ( stripos( $token, 'IGNORE INDEX' ) !== false ) { - $this->rewrite_index_hint = true; - } - if ( stripos( $token, 'FORCE INDEX' ) !== false ) { - $this->rewrite_index_hint = true; - } - if ( stripos( $token, 'BETWEEN' ) !== false ) { - $this->rewrite_between = true; - $this->num_of_rewrite_between++; - } - if ( stripos( $token, 'ORDER BY FIELD' ) !== false ) { - $this->orderby_field = true; - } - } - } - $query_string .= $token; - } - $this->_query = $query_string; - } - - /** - * Method to handle SHOW TABLES query. - * - * @access private - */ - private function handle_show_query() { - $this->_query = str_ireplace( ' FULL', '', $this->_query ); - $table_name = ''; - $pattern = '/^\\s*SHOW\\s*TABLES\\s*.*?(LIKE\\s*(.*))$/im'; - if ( preg_match( $pattern, $this->_query, $matches ) ) { - $table_name = str_replace( array( "'", ';' ), '', $matches[2] ); - } - $suffix = empty( $table_name ) ? '' : ' AND name LIKE ' . "'" . $table_name . "'"; - $this->_query = "SELECT name FROM sqlite_master WHERE type='table'" . $suffix . ' ORDER BY name DESC'; - } - - /** - * Method to emulate the SQL_CALC_FOUND_ROWS placeholder for MySQL. - * - * This is a kind of tricky play. - * 1. remove SQL_CALC_FOUND_ROWS option, and give it to the pdo engine - * 2. make another $wpdb instance, and execute the rewritten query - * 3. give the returned value (integer: number of the rows) to the original instance variable without LIMIT - * - * We no longer use SELECT COUNT query, because it returns the inexact values when used with WP_Meta_Query(). - * - * This kind of statement is required for WordPress to calculate the paging information. - * see also WP_Query class in wp-includes/query.php - */ - private function handle_sql_count() { - if ( ! $this->rewrite_calc_found ) { - return; - } - global $wpdb; - // First strip the code. this is the end of rewriting process. - $this->_query = str_ireplace( 'SQL_CALC_FOUND_ROWS', '', $this->_query ); - // We make the data for next SELECE FOUND_ROWS() statement. - $unlimited_query = preg_replace( '/\\bLIMIT\\s*.*/imsx', '', $this->_query ); - $_wpdb = new Perflab_SQLite_DB(); - $result = $_wpdb->query( $unlimited_query ); - $wpdb->dbh->found_rows_result = $result; - $_wpdb = null; - } - - /** - * Method to rewrite INSERT IGNORE to INSERT OR IGNORE. - * - * @access private - */ - private function rewrite_insert_ignore() { - $this->_query = str_ireplace( 'INSERT IGNORE', 'INSERT OR IGNORE ', $this->_query ); - } - - /** - * Method to rewrite UPDATE IGNORE to UPDATE OR IGNORE. - * - * @access private - */ - private function rewrite_update_ignore() { - $this->_query = str_ireplace( 'UPDATE IGNORE', 'UPDATE OR IGNORE ', $this->_query ); - } - - /** - * Method to rewrite DATE_ADD() function. - * - * DATE_ADD has a parameter PHP function can't parse, so we quote the list and - * pass it to the user defined function. - * - * @access private - */ - private function rewrite_date_add() { - // (date,interval expression unit) - $pattern = '/\\s*date_add\\s*\(([^,]*),([^\)]*)\)/imsx'; - if ( preg_match( $pattern, $this->_query, $matches ) ) { - $expression = "'" . trim( $matches[2] ) . "'"; - $this->_query = preg_replace( $pattern, " date_add($matches[1], $expression) ", $this->_query ); - } - } - - /** - * Method to rewrite DATE_SUB() function. - * - * DATE_SUB has a parameter PHP function can't parse, so we quote the list and - * pass it to the user defined function. - * - * @access private - */ - private function rewrite_date_sub() { - // (date,interval expression unit) - $pattern = '/\\s*date_sub\\s*\(([^,]*),([^\)]*)\)/imsx'; - if ( preg_match( $pattern, $this->_query, $matches ) ) { - $expression = "'" . trim( $matches[2] ) . "'"; - $this->_query = preg_replace( $pattern, " date_sub($matches[1], $expression) ", $this->_query ); - } - } - - /** - * Method to handle CREATE query. - * - * If the query is CREATE query, it will be passed to the query_create.class.php. - * So this method can't be used. It's here for safety. - * - * @access private - */ - private function handle_create_query() { - $engine = new Perflab_SQLite_Create_Query(); - $this->_query = $engine->rewrite_query( $this->_query ); - $engine = null; - } - - /** - * Method to handle ALTER query. - * - * If the query is ALTER query, it will be passed ot the query_alter.class.php. - * So this method can't be used. It is here for safety. - * - * @access private - */ - private function handle_alter_query() { - $engine = new Perflab_SQLite_Alter_Query(); - $this->_query = $engine->rewrite_query( $this->_query, 'alter' ); - $engine = null; - } - - /** - * Method to handle DESCRIBE or DESC query. - * - * DESCRIBE is required for WordPress installation process. DESC is - * an alias for DESCRIBE, but it is not used in core WordPress. - * - * @access private - */ - private function handle_describe_query() { - $pattern = '/^\\s*(DESCRIBE|DESC)\\s*(.*)/i'; - if ( preg_match( $pattern, $this->_query, $match ) ) { - $tablename = preg_replace( '/[\';]/', '', $match[2] ); - $this->_query = "PRAGMA table_info($tablename)"; - } - } - - /** - * Method to remove LIMIT clause from DELETE or UPDATE query. - * - * The author of the original 'PDO for WordPress' says update method of wpdb - * insists on adding LIMIT. But the newest version of WordPress doesn't do that. - * Nevertheless some plugins use DELETE with LIMIT, UPDATE with LIMIT. - * We need to exclude sub query's LIMIT. And if SQLite is compiled with - * ENABLE_UPDATE_DELETE_LIMIT option, we don't remove it. - * - * @access private - */ - private function rewrite_limit_usage() { - $_wpdb = new Perflab_SQLite_DB(); - $options = $_wpdb->get_results( 'PRAGMA compile_options' ); - foreach ( $options as $opt ) { - if ( isset( $opt->compile_option ) && stripos( $opt->compile_option, 'ENABLE_UPDATE_DELETE_LIMIT' ) !== false ) { - return; - } - } - if ( stripos( $this->_query, '(select' ) === false ) { - $this->_query = preg_replace( '/\\s*LIMIT\\s*[0-9]$/i', '', $this->_query ); - } - } - - /** - * Method to remove ORDER BY clause from DELETE or UPDATE query. - * - * SQLite compiled without SQLITE_ENABLE_UPDATE_DELETE_LIMIT option can't - * execute UPDATE with ORDER BY, DELETE with GROUP BY. - * We need to exclude sub query's GROUP BY. - * - * @access private - */ - private function rewrite_order_by_usage() { - $_wpdb = new Perflab_SQLite_DB(); - $options = $_wpdb->get_results( 'PRAGMA compile_options' ); - foreach ( $options as $opt ) { - if ( isset( $opt->compile_option ) && stripos( $opt->compile_option, 'ENABLE_UPDATE_DELETE_LIMIT' ) !== false ) { - return; - } - } - if ( stripos( $this->_query, '(select' ) === false ) { - $this->_query = preg_replace( '/\\s+ORDER\\s+BY\\s*.*$/i', '', $this->_query ); - } - } - - /** - * Method to handle TRUNCATE query. - * - * @access private - */ - private function handle_truncate_query() { - $pattern = '/TRUNCATE TABLE (.*)/im'; - $this->_query = preg_replace( $pattern, 'DELETE FROM $1', $this->_query ); - } - - /** - * Method to handle OPTIMIZE query. - * - * Original query has the table names, but they are simply ignored. - * Table names are meaningless in SQLite. - * - * @access private - */ - private function rewrite_optimize() { - $this->_query = 'VACUUM'; - } - - /** - * Method to rewrite day. - * - * Jusitn Adie says: some wp UI interfaces (notably the post interface) - * badly composes the day part of the date leading to problems in sqlite - * sort ordering etc. - * - * I don't understand that... - * - * @access private - */ - private function rewrite_badly_formed_dates() { - $pattern = '/([12]\d{3,}-\d{2}-)(\d )/ims'; - $this->_query = preg_replace( $pattern, '${1}0$2', $this->_query ); - } - - /** - * Method to remove INDEX HINT. - * - * @access private - */ - private function delete_index_hints() { - $pattern = '/\\s*(use|ignore|force)\\s+index\\s*\(.*?\)/i'; - $this->_query = preg_replace( $pattern, '', $this->_query ); - } - - /** - * Method to fix the date string and quoting. - * - * This is required for the calendar widget. - * - * WHERE month(fieldname)=08 is converted to month(fieldname)='8' - * WHERE month(fieldname)='08' is converted to month(fieldname)='8' - * - * I use preg_replace_callback instead of 'e' option because of security reason. - * cf. PHP manual (regular expression) - * - * @access private - */ - private function fix_date_quoting() { - $pattern = '/(month|year|second|day|minute|hour|dayofmonth)\\s*\((.*?)\)\\s*=\\s*["\']?(\d{1,4})[\'"]?\\s*/im'; - $this->_query = preg_replace_callback( $pattern, array( $this, '_fix_date_quoting' ), $this->_query ); - } - - /** - * Call back method to rewrite date string. - * - * @param string $match The match from preg_replace_callback. - * - * @return string - * @access private - */ - private function _fix_date_quoting( $match ) { - return "{$match[1]}({$match[2]})='" . intval( $match[3] ) . "' "; - } - - /** - * Method to rewrite REGEXP() function. - * - * This method changes function name to regexpp() and pass it to the user defined - * function. - * - * @access private - */ - private function rewrite_regexp() { - $pattern = '/\s([^\s]*)\s*regexp\s*(\'.*?\')/im'; - $this->_query = preg_replace( $pattern, ' regexpp(\1, \2)', $this->_query ); - } - - /** - * Method to handle SHOW COLUMN query. - * - * @access private - */ - private function handle_show_columns_query() { - $this->_query = str_ireplace( ' FULL', '', $this->_query ); - $pattern_like = '/^\\s*SHOW\\s*(COLUMNS|FIELDS)\\s*FROM\\s*(.*)?\\s*LIKE\\s*(.*)?/i'; - $pattern = '/^\\s*SHOW\\s*(COLUMNS|FIELDS)\\s*FROM\\s*(.*)?/i'; - if ( preg_match( $pattern_like, $this->_query, $matches ) ) { - $table_name = str_replace( "'", '', trim( $matches[2] ) ); - $column_name = str_replace( "'", '', trim( $matches[3] ) ); - $query_string = "SELECT sql FROM sqlite_master WHERE tbl_name='$table_name' AND sql LIKE '%$column_name%'"; - $this->_query = $query_string; - } elseif ( preg_match( $pattern, $this->_query, $matches ) ) { - $table_name = $matches[2]; - $query_string = preg_replace( $pattern, "PRAGMA table_info($table_name)", $this->_query ); - $this->_query = $query_string; - } - } - - /** - * Method to handle SHOW INDEX query. - * - * Moved the WHERE clause manipulation to pdoengin.class.php (ver 1.3.1) - * - * @access private - */ - private function handle_show_index() { - $pattern = '/^\\s*SHOW\\s*(?:INDEX|INDEXES|KEYS)\\s*FROM\\s*(\\w+)?/im'; - if ( preg_match( $pattern, $this->_query, $match ) ) { - $table_name = preg_replace( "/[\';]/", '', $match[1] ); - $table_name = trim( $table_name ); - $this->_query = "SELECT * FROM sqlite_master WHERE tbl_name='$table_name'"; - } - } - - /** - * Method to handle ON DUPLICATE KEY UPDATE statement. - * - * First we use SELECT query and check if INSERT is allowed or not. - * Rewriting procedure looks like a detour, but I've got no other ways. - * - * Added the literal check since the version 1.5.1. - * - * @access private - */ - private function execute_duplicate_key_update() { - if ( ! $this->rewrite_duplicate_key ) { - return; - } - $unique_keys_for_cond = array(); - $unique_keys_for_check = array(); - $pattern = '/^\\s*INSERT\\s*INTO\\s*(\\w+)?\\s*(.*)\\s*ON\\s*DUPLICATE\\s*KEY\\s*UPDATE\\s*(.*)$/ims'; - if ( preg_match( $pattern, $this->_query, $match_0 ) ) { - $table_name = trim( $match_0[1] ); - $insert_data = trim( $match_0[2] ); - $update_data = trim( $match_0[3] ); - // Prepare two unique key data for the table. - // 1. array('col1', 'col2, col3', etc) 2. array('col1', 'col2', 'col3', etc). - $_wpdb = new Perflab_SQLite_DB(); - $indexes = $_wpdb->get_results( "SHOW INDEX FROM {$table_name}" ); - if ( ! empty( $indexes ) ) { - foreach ( $indexes as $index ) { - if ( 0 === $index->Non_unique ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $unique_keys_for_cond[] = $index->Column_name; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - if ( strpos( $index->Column_name, ',' ) !== false ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $unique_keys_for_check = array_merge( - $unique_keys_for_check, - explode( ',', $index->Column_name ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - ); - } else { - $unique_keys_for_check[] = $index->Column_name; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - } - } - } - $unique_keys_for_check = array_map( 'trim', $unique_keys_for_check ); - } else { - // Without unique key or primary key, UPDATE statement will affect all the rows! - $query = "INSERT INTO $table_name $insert_data"; - $this->_query = $query; - $_wpdb = null; - - return; - } - // Data check. - if ( preg_match( '/^\((.*)\)\\s*VALUES\\s*\((.*)\)$/ims', $insert_data, $match_1 ) ) { - $col_array = explode( ',', $match_1[1] ); - $ins_data_array = explode( ',', $match_1[2] ); - foreach ( $col_array as $col ) { - $val = trim( array_shift( $ins_data_array ) ); - $ins_data_assoc[ trim( $col ) ] = $val; - } - $condition = ''; - foreach ( $unique_keys_for_cond as $unique_key ) { - if ( strpos( $unique_key, ',' ) !== false ) { - $unique_key_array = explode( ',', $unique_key ); - $counter = count( $unique_key_array ); - for ( $i = 0; $i < $counter; ++$i ) { - $col = trim( $unique_key_array[ $i ] ); - if ( isset( $ins_data_assoc[ $col ] ) && $i === $counter - 1 ) { - $condition .= $col . '=' . $ins_data_assoc[ $col ] . ' OR '; - } elseif ( isset( $ins_data_assoc[ $col ] ) ) { - $condition .= $col . '=' . $ins_data_assoc[ $col ] . ' AND '; - } else { - continue; - } - } - } else { - $col = trim( $unique_key ); - if ( ! isset( $ins_data_assoc[ $col ] ) ) { - continue; - } - $condition .= $col . '=' . $ins_data_assoc[ $col ] . ' OR '; - } - } - $condition = rtrim( $condition, ' OR ' ); - $test_query = "SELECT * FROM {$table_name} WHERE {$condition}"; - $results = $_wpdb->query( $test_query ); - $_wpdb = null; - if ( 0 == $results ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison - $this->_query = "INSERT INTO $table_name $insert_data"; - return; - } - - $ins_array_assoc = array(); - - if ( preg_match( '/^\((.*)\)\\s*VALUES\\s*\((.*)\)$/im', $insert_data, $match_2 ) ) { - $col_array = explode( ',', $match_2[1] ); - $ins_array = explode( ',', $match_2[2] ); - $count = count( $col_array ); - for ( $i = 0; $i < $count; $i++ ) { - $col = trim( $col_array[ $i ] ); - $val = trim( $ins_array[ $i ] ); - $ins_array_assoc[ $col ] = $val; - } - } - $update_data = rtrim( $update_data, ';' ); - $tmp_array = explode( ',', $update_data ); - foreach ( $tmp_array as $pair ) { - list($col, $value) = explode( '=', $pair ); - $col = trim( $col ); - $value = trim( $value ); - $update_array_assoc[ $col ] = $value; - } - foreach ( $update_array_assoc as $key => &$value ) { - if ( preg_match( '/^VALUES\\s*\((.*)\)$/im', $value, $match_3 ) ) { - $col = trim( $match_3[1] ); - $value = $ins_array_assoc[ $col ]; - } - } - foreach ( $ins_array_assoc as $key => $val ) { - if ( in_array( $key, $unique_keys_for_check, true ) ) { - $where_array[] = $key . '=' . $val; - } - } - $update_strings = ''; - foreach ( $update_array_assoc as $key => $val ) { - if ( in_array( $key, $unique_keys_for_check, true ) ) { - $where_array[] = $key . '=' . $val; - } else { - $update_strings .= $key . '=' . $val . ','; - } - } - $update_strings = rtrim( $update_strings, ',' ); - $unique_where = array_unique( $where_array, SORT_REGULAR ); - $where_string = ' WHERE ' . implode( ' AND ', $unique_where ); - $update_query = 'UPDATE ' . $table_name . ' SET ' . $update_strings . $where_string; - $this->_query = $update_query; - } - } - } - - /** - * Method to rewrite BETWEEN A AND B clause. - * - * This clause is the same form as natural language, so we have to check if it is - * in the data or SQL statement. - * - * @access private - */ - private function rewrite_between() { - if ( ! $this->rewrite_between ) { - return; - } - $pattern = '/\\s*(CAST\([^\)]+?\)|[^\\s\(]*)?\\s*BETWEEN\\s*([^\\s]*)?\\s*AND\\s*([^\\s\)]*)?\\s*/ims'; - do { - if ( preg_match( $pattern, $this->_query, $match ) ) { - $column_name = trim( $match[1] ); - $min_value = trim( $match[2] ); - $max_value = trim( $match[3] ); - $max_value = rtrim( $max_value ); - $replacement = " ($column_name >= $min_value AND $column_name <= $max_value)"; - $this->_query = str_ireplace( $match[0], $replacement, $this->_query ); - } - $this->num_of_rewrite_between--; - } while ( $this->num_of_rewrite_between > 0 ); - } - - /** - * Method to handle ORDER BY FIELD() clause. - * - * When FIELD() function has column name to compare, we can't rewrite it with - * use defined functions. When this function detect column name in the argument, - * it creates another instance, does the query withuot ORDER BY clause and gives - * the result array sorted to the main instance. - * - * If FIELD() function doesn't have column name, it will use the user defined - * function. usort() function closure function to compare the items. - * - * @access private - */ - private function handle_orderby_field() { - if ( ! $this->orderby_field ) { - return; - } - global $wpdb; - $pattern = '/\\s+ORDER\\s+BY\\s+FIELD\\s*\(\\s*([^\)]+?)\\s*\)/i'; - if ( preg_match( $pattern, $this->_query, $match ) ) { - $params = explode( ',', $match[1] ); - $params = array_map( 'trim', $params ); - $tbl_col = array_shift( $params ); - $flipped = array_flip( $params ); - $tbl_name = substr( $tbl_col, 0, strpos( $tbl_col, '.' ) ); - $tbl_name = str_replace( $wpdb->prefix, '', $tbl_name ); - - if ( $tbl_name && in_array( $tbl_name, $wpdb->tables, true ) ) { - $query = str_replace( $match[0], '', $this->_query ); - $_wpdb = new Perflab_SQLite_DB(); - $results = $_wpdb->get_results( $query ); - $_wpdb = null; - usort( - $results, - function ( $a, $b ) use ( $flipped ) { - return $flipped[ $a->ID ] - $flipped[ $b->ID ]; - } - ); - } - $wpdb->dbh->pre_ordered_results = $results; - } - } - - /** - * Method to avoid DELETE with JOIN statement. - * - * The wp-admin/includes/upgrade.php contains a 'DELETE ... JOIN' statement. - * This query can't be replaced with regular expression or udf, so we - * replace all the statement with another. But this query was used in - * the very old version of WordPress when it was upgraded. So we won't - * have no chance that this method should be used. - * - * @access private - */ - private function delete_workaround() { - global $wpdb; - $pattern = "DELETE o1 FROM $wpdb->options AS o1 JOIN $wpdb->options AS o2"; - $pattern2 = "DELETE a, b FROM $wpdb->sitemeta AS a, $wpdb->sitemeta AS b"; - $rewritten = "DELETE FROM $wpdb->options WHERE option_id IN (SELECT MIN(option_id) FROM $wpdb->options GROUP BY option_name HAVING COUNT(*) > 1)"; - if ( stripos( $this->_query, $pattern ) !== false ) { - $this->_query = $rewritten; - } elseif ( stripos( $this->_query, $pattern2 ) !== false ) { - $time = time(); - $prep_query = "SELECT a.meta_id AS aid, b.meta_id AS bid FROM $wpdb->sitemeta AS a INNER JOIN $wpdb->sitemeta AS b ON a.meta_key='_site_transient_timeout_'||substr(b.meta_key, 17) WHERE b.meta_key='_site_transient_'||substr(a.meta_key, 25) AND a.meta_value < $time"; - $_wpdb = new Perflab_SQLite_DB(); - $ids = $_wpdb->get_results( $prep_query ); - foreach ( $ids as $id ) { - $ids_to_delete[] = $id->aid; - $ids_to_delete[] = $id->bid; - } - $rewritten = "DELETE FROM $wpdb->sitemeta WHERE meta_id IN (" . implode( ',', $ids_to_delete ) . ')'; - $this->_query = $rewritten; - } - } - - /** - * Method to suppress errors. - * - * When the query string is the one that this class can't manipulate, - * the query string is replaced with the one that always returns true - * and does nothing. - * - * @access private - */ - private function return_true() { - $this->_query = 'SELECT 1=1'; - } -} diff --git a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-pdo-engine.php b/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-pdo-engine.php deleted file mode 100644 index e72b5d27b4..0000000000 --- a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-pdo-engine.php +++ /dev/null @@ -1,1527 +0,0 @@ -client_info = SQLite3::version()['versionString']; - - register_shutdown_function( array( $this, '__destruct' ) ); - if ( ! is_file( FQDB ) ) { - $this->prepare_directory(); - } - $dsn = 'sqlite:' . FQDB; - if ( isset( $GLOBALS['@pdo'] ) ) { - $this->pdo = $GLOBALS['@pdo']; - $this->init(); - return; - } - - $locked = false; - $status = 0; - $err_message = ''; - do { - try { - $this->pdo = new PDO( $dsn, null, null, array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ) ); // phpcs:ignore WordPress.DB.RestrictedClasses - new Perflab_SQLite_PDO_User_Defined_Functions( $this->pdo ); - $GLOBALS['@pdo'] = $this->pdo; - } catch ( PDOException $ex ) { - $status = $ex->getCode(); - if ( 5 === $status || 6 === $status ) { - $locked = true; - } else { - $err_message = $ex->getMessage(); - } - } - } while ( $locked ); - - if ( $status > 0 ) { - $message = sprintf( - '

%s

%s

%s

', - __( 'Database initialization error!', 'performance-lab' ), - /* translators: %d: error code */ - sprintf( __( 'Code: %d', 'performance-lab' ), $status ), - /* translators: %s: error message */ - sprintf( __( 'Error Message: %s', 'performance-lab' ), $err_message ) - ); - $this->set_error( __LINE__, __FILE__, $message ); - - return false; - } - $this->init(); - } - - /** - * Destructor - * - * If SQLITE_MEM_DEBUG constant is defined, append information about - * memory usage into database/mem_debug.txt. - * - * This definition is changed since version 1.7. - * - * @return boolean - */ - function __destruct() { - if ( defined( 'SQLITE_MEM_DEBUG' ) && SQLITE_MEM_DEBUG ) { - $max = ini_get( 'memory_limit' ); - if ( is_null( $max ) ) { - $message = sprintf( - '[%s] Memory_limit is not set in php.ini file.', - gmdate( 'Y-m-d H:i:s', $_SERVER['REQUEST_TIME'] ) - ); - error_log( $message ); - return true; - } - if ( stripos( $max, 'M' ) !== false ) { - $max = (int) $max * MB_IN_BYTES; - } - $peak = memory_get_peak_usage( true ); - $used = round( (int) $peak / (int) $max * 100, 2 ); - if ( $used > 90 ) { - $message = sprintf( - "[%s] Memory peak usage warning: %s %% used. (max: %sM, now: %sM)\n", - gmdate( 'Y-m-d H:i:s', $_SERVER['REQUEST_TIME'] ), - $used, - $max, - $peak - ); - error_log( $message ); - } - } - - return true; - } - - /** - * Method to initialize database, executed in the constructor. - * - * It checks if WordPress is in the installing process and does the required - * jobs. SQLite library version specific settings are also in this function. - * - * Some developers use WP_INSTALLING constant for other purposes, if so, this - * function will do no harms. - */ - private function init() { - if ( version_compare( SQLite3::version()['versionString'], '3.7.11', '>=' ) ) { - $this->can_insert_multiple_rows = true; - } - $statement = $this->pdo->query( 'PRAGMA foreign_keys' ); - if ( $statement->fetchColumn( 0 ) == '0' ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison - $this->pdo->query( 'PRAGMA foreign_keys = ON' ); - } - } - - /** - * This method makes database directory and .htaccess file. - * - * It is executed only once when the installation begins. - */ - private function prepare_directory() { - global $wpdb; - $u = umask( 0000 ); - if ( ! is_dir( FQDBDIR ) ) { - if ( ! @mkdir( FQDBDIR, 0704, true ) ) { - umask( $u ); - $message = __( 'Unable to create the required directory! Please check your server settings.', 'performance-lab' ); - wp_die( $message, 'Error!' ); - } - } - if ( ! is_writable( FQDBDIR ) ) { - umask( $u ); - $message = __( 'Unable to create a file in the directory! Please check your server settings.', 'performance-lab' ); - wp_die( $message, 'Error!' ); - } - if ( ! is_file( FQDBDIR . '.htaccess' ) ) { - $fh = fopen( FQDBDIR . '.htaccess', 'w' ); - if ( ! $fh ) { - umask( $u ); - $message = __( 'Unable to create a file in the directory! Please check your server settings.', 'performance-lab' ); - echo $message; - - return false; - } - fwrite( $fh, 'DENY FROM ALL' ); - fclose( $fh ); - } - if ( ! is_file( FQDBDIR . 'index.php' ) ) { - $fh = fopen( FQDBDIR . 'index.php', 'w' ); - if ( ! $fh ) { - umask( $u ); - $message = __( 'Unable to create a file in the directory! Please check your server settings.', 'performance-lab' ); - echo $message; - - return false; - } - fwrite( $fh, '' ); - fclose( $fh ); - } - umask( $u ); - - return true; - } - - /** - * Method to execute query(). - * - * Divide the query types into seven different ones. That is to say: - * - * 1. SELECT SQL_CALC_FOUND_ROWS - * 2. INSERT - * 3. CREATE TABLE(INDEX) - * 4. ALTER TABLE - * 5. SHOW VARIABLES - * 6. DROP INDEX - * 7. THE OTHERS - * - * #1 is just a tricky play. See the private function handle_sql_count() in query.class.php. - * From #2 through #5 call different functions respectively. - * #6 call the ALTER TABLE query. - * #7 is a normal process: sequentially call prepare_query() and execute_query(). - * - * #1 process has been changed since version 1.5.1. - * - * @param string $statement Full SQL statement string. - * @param int $mode Not used. - * @param array ...$fetch_mode_args Not used. - * - * @return mixed according to the query type - * @see PDO::query() - */ - public function query( $statement, $mode = PDO::ATTR_DEFAULT_FETCH_MODE, ...$fetch_mode_args ) { // phpcs:ignore WordPress.DB.RestrictedClasses - $this->flush(); - - $this->queries[] = "Raw query:\n$statement"; - $res = $this->determine_query_type( $statement ); - if ( ! $res && defined( 'PDO_DEBUG' ) && PDO_DEBUG ) { - $bailout_string = sprintf( - /* translators: %s: SQL statement */ - '

' . __( 'Unknown query type', 'performance-lab' ) . '

' . __( 'Sorry, we cannot determine the type of query that is requested (%s).', 'performance-lab' ) . '

', - $statement - ); - $this->set_error( __LINE__, __FUNCTION__, $bailout_string ); - } - switch ( strtolower( $this->query_type ) ) { - case 'set': - $this->return_value = false; - break; - - case 'foundrows': - $_column = array( 'FOUND_ROWS()' => '' ); - $column = array(); - if ( ! is_null( $this->found_rows_result ) ) { - $this->num_rows = $this->found_rows_result; - $_column['FOUND_ROWS()'] = $this->num_rows; - $column[] = new Perflab_SQLite_Object_Array( $_column ); - $this->results = $column; - $this->found_rows_result = null; - } - break; - - case 'insert': - if ( $this->can_insert_multiple_rows ) { - $this->execute_insert_query_new( $statement ); - break; - } - $this->execute_insert_query( $statement ); - break; - - case 'create': - $this->return_value = $this->execute_create_query( $statement ); - break; - - case 'alter': - $this->return_value = $this->execute_alter_query( $statement ); - break; - - case 'show_variables': - $this->return_value = $this->show_variables_workaround( $statement ); - break; - - case 'showstatus': - $this->return_value = $this->show_status_workaround( $statement ); - break; - - case 'drop_index': - $this->return_value = false; - if ( preg_match( '/^\\s*(DROP\\s*INDEX\\s*.*?)\\s*ON\\s*(.*)/im', $statement, $match ) ) { - $this->query_type = 'alter'; - $this->return_value = $this->execute_alter_query( 'ALTER TABLE ' . trim( $match[2] ) . ' ' . trim( $match[1] ) ); - } - break; - - default: - $engine = $this->prepare_engine( $this->query_type ); - $this->rewritten_query = $engine->rewrite_query( $statement, $this->query_type ); - if ( ! is_null( $this->pre_ordered_results ) ) { - $this->results = $this->pre_ordered_results; - $this->num_rows = count( $this->results ); - $this->return_value = $this->num_rows; - $this->pre_ordered_results = null; - break; - } - $this->queries[] = "Rewritten:\n$this->rewritten_query"; - $this->extract_variables(); - $prepared_query = $this->prepare_query(); - $this->execute_query( $prepared_query ); - if ( ! $this->is_error ) { - $this->process_results( $engine ); - } - break; - } - $debug_string = $this->get_debug_info(); - if ( defined( 'PDO_DEBUG' ) && PDO_DEBUG === true && $debug_string ) { - error_log( $debug_string ); - } - - return $this->return_value; - } - - /** - * Method to return inserted row id. - */ - public function get_insert_id() { - return $this->last_insert_id; - } - - /** - * Method to return the number of rows affected. - */ - public function get_affected_rows() { - return $this->affected_rows; - } - - /** - * Method to return the queried column names. - * - * These data are meaningless for SQLite. So they are dummy emulating - * MySQL columns data. - * - * @return array of the object - */ - public function get_columns() { - if ( ! empty( $this->results ) ) { - $primary_key = array( - 'meta_id', - 'comment_ID', - 'link_ID', - 'option_id', - 'blog_id', - 'option_name', - 'ID', - 'term_id', - 'object_id', - 'term_taxonomy_id', - 'umeta_id', - 'id', - ); - $unique_key = array( 'term_id', 'taxonomy', 'slug' ); - $data = array( - 'name' => '', // Column name. - 'table' => '', // Table name. - 'max_length' => 0, // Max length of the column. - 'not_null' => 1, // 1 if not null. - 'primary_key' => 0, // 1 if column has primary key. - 'unique_key' => 0, // 1 if column has unique key. - 'multiple_key' => 0, // 1 if column doesn't have unique key. - 'numeric' => 0, // 1 if column has numeric value. - 'blob' => 0, // 1 if column is blob. - 'type' => '', // Type of the column. - 'unsigned' => 0, // 1 if column is unsigned integer. - 'zerofill' => 0, // 1 if column is zero-filled. - ); - $table_name = ''; - if ( preg_match( '/\s*FROM\s*(.*)?\s*/i', $this->rewritten_query, $match ) ) { - $table_name = trim( $match[1] ); - } - foreach ( $this->results[0] as $key => $value ) { - $data['name'] = $key; - $data['table'] = $table_name; - if ( in_array( $key, $primary_key, true ) ) { - $data['primary_key'] = 1; - } elseif ( in_array( $key, $unique_key, true ) ) { - $data['unique_key'] = 1; - } else { - $data['multiple_key'] = 1; - } - $this->column_data[] = new Perflab_SQLite_Object_Array( $data ); - - // Reset data for next iteration. - $data['name'] = ''; - $data['table'] = ''; - $data['primary_key'] = 0; - $data['unique_key'] = 0; - $data['multiple_key'] = 0; - } - - return $this->column_data; - } - return null; - } - - /** - * Method to return the queried result data. - * - * @return mixed - */ - public function get_query_results() { - return $this->results; - } - - /** - * Method to return the number of rows from the queried result. - */ - public function get_num_rows() { - return $this->num_rows; - } - - /** - * Method to return the queried results according to the query types. - * - * @return mixed - */ - public function get_return_value() { - return $this->return_value; - } - - /** - * Method to return error messages. - * - * @return string - */ - public function get_error_message() { - if ( count( $this->error_messages ) === 0 ) { - $this->is_error = false; - $this->error_messages = array(); - return ''; - } - - if ( false === $this->is_error ) { - return ''; - } - - $output = '
 
'; - $output .= '
'; - $output .= '

' . __( 'Queries made or created this session were:', 'performance-lab' ) . '

'; - $output .= '
    '; - foreach ( $this->queries as $q ) { - $output .= '
  1. ' . $q . '
  2. '; - } - $output .= '
'; - $output .= '
'; - foreach ( $this->error_messages as $num => $m ) { - $output .= '
'; - $output .= sprintf( - /* translators: %1$d: line number, %2$s: function name, %3$s: error message */ - __( 'Error occurred at line %1$d in Function %2$s. Error message was: %3$s.', 'performance-lab' ), - (int) $this->errors[ $num ]['line'], - '' . esc_html( $this->errors[ $num ]['function'] ) . '', - $m - ); - $output .= '
'; - } - - ob_start(); - debug_print_backtrace(); - $output .= '
' . ob_get_contents() . '
'; - ob_end_clean(); - - return $output; - } - - /** - * Method to return information about query string for debugging. - * - * @return string - */ - private function get_debug_info() { - $output = ''; - foreach ( $this->queries as $q ) { - $output .= $q . "\n"; - } - - return $output; - } - - /** - * Method to clear previous data. - */ - private function flush() { - $this->rewritten_query = ''; - $this->query_type = ''; - $this->results = null; - $this->_results = null; - $this->last_insert_id = null; - $this->affected_rows = null; - $this->column_data = array(); - $this->num_rows = null; - $this->return_value = null; - $this->extracted_variables = array(); - $this->error_messages = array(); - $this->is_error = false; - $this->queries = array(); - $this->param_num = 0; - } - - /** - * Method to include the apropreate class files. - * - * It is not a good habit to change the include files programmatically. - * Needs to be fixed some other way. - * - * @param string $query_type The query type (create, alter, etc). - * - * @return object reference to apropreate driver - */ - private function prepare_engine( $query_type = null ) { - if ( stripos( $query_type, 'create' ) !== false ) { - return new Perflab_SQLite_Create_Query(); - } - if ( stripos( $query_type, 'alter' ) !== false ) { - return new Perflab_SQLite_Alter_Query(); - } - return new Perflab_SQLite_PDO_Driver(); - } - - /** - * Method to create a PDO statement object from the query string. - * - * @return PDOStatement - */ - private function prepare_query() { - $this->queries[] = "Prepare:\n" . $this->prepared_query; - $reason = 0; - $message = ''; - $statement = null; - do { - try { - $statement = $this->pdo->prepare( $this->prepared_query ); - } catch ( PDOException $err ) { - $reason = $err->getCode(); - $message = $err->getMessage(); - } - } while ( 5 === $reason || 6 === $reason ); - - if ( $reason > 0 ) { - $err_message = sprintf( 'Problem preparing the PDO SQL Statement. Error was: %s', $message ); - $this->set_error( __LINE__, __FUNCTION__, $err_message ); - } - - return $statement; - } - - /** - * Method to execute PDO statement object. - * - * This function executes query and sets the variables to give back to WordPress. - * The variables are class fields. So if success, no return value. If failure, it - * returns void and stops. - * - * @param object $statement The PDO statement. - * - * @return boolean|void - */ - private function execute_query( $statement ) { - $reason = 0; - $message = ''; - if ( ! is_object( $statement ) ) { - return false; - } - if ( count( $this->extracted_variables ) > 0 ) { - $this->queries[] = "Executing:\n" . var_export( $this->extracted_variables, true ); - do { - if ( 'update' === $this->query_type || 'replace' === $this->query_type ) { - try { - $this->beginTransaction(); - $statement->execute( $this->extracted_variables ); - $this->commit(); - } catch ( PDOException $err ) { - $reason = $err->getCode(); - $message = $err->getMessage(); - $this->rollBack(); - } - } else { - try { - $statement->execute( $this->extracted_variables ); - } catch ( PDOException $err ) { - $reason = $err->getCode(); - $message = $err->getMessage(); - } - } - } while ( 5 == $reason || 6 == $reason ); // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison - } else { - $this->queries[] = 'Executing: (no parameters)'; - do { - if ( 'update' === $this->query_type || 'replace' === $this->query_type ) { - try { - $this->beginTransaction(); - $statement->execute(); - $this->commit(); - } catch ( PDOException $err ) { - $reason = $err->getCode(); - $message = $err->getMessage(); - $this->rollBack(); - } - } else { - try { - $statement->execute(); - } catch ( PDOException $err ) { - $reason = $err->getCode(); - $message = $err->getMessage(); - } - } - } while ( 5 == $reason || 6 == $reason ); // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison - } - if ( $reason > 0 ) { - $err_message = sprintf( 'Error while executing query! Error message was: %s', $message ); - $this->set_error( __LINE__, __FUNCTION__, $err_message ); - return false; - } - $this->_results = $statement->fetchAll( PDO::FETCH_OBJ ); // phpcs:ignore WordPress.DB.RestrictedClasses - - // Generate the results that $wpdb will want to see. - switch ( $this->query_type ) { - case 'insert': - case 'update': - case 'replace': - $this->last_insert_id = $this->pdo->lastInsertId(); - $this->affected_rows = $statement->rowCount(); - $this->return_value = $this->affected_rows; - break; - - case 'select': - case 'show': - case 'showcolumns': - case 'showindex': - case 'describe': - case 'desc': - case 'check': - case 'analyze': - $this->num_rows = count( $this->_results ); - $this->return_value = $this->num_rows; - break; - - case 'delete': - $this->affected_rows = $statement->rowCount(); - $this->return_value = $this->affected_rows; - break; - - case 'alter': - case 'drop': - case 'create': - case 'optimize': - case 'truncate': - $this->return_value = true; - if ( $this->is_error ) { - $this->return_value = false; - } - break; - } - } - - /** - * Method to extract field data to an array and prepare the query statement. - * - * If original SQL statement is CREATE query, this function does nothing. - */ - private function extract_variables() { - if ( 'create' === $this->query_type ) { - $this->prepared_query = $this->rewritten_query; - return; - } - - // Long queries can really kill this. - $pattern = '/(? 10000000 ) { - $query = preg_replace_callback( - $pattern, - array( $this, 'replace_variables_with_placeholders' ), - $this->rewritten_query - ); - } else { - do { - if ( $limit > 10000000 ) { - $this->set_error( __LINE__, __FUNCTION__, 'The query is too big to parse properly' ); - break; // No point in continuing execution, would get into a loop. - } - ini_set( 'pcre.backtrack_limit', $limit ); - $query = preg_replace_callback( - $pattern, - array( $this, 'replace_variables_with_placeholders' ), - $this->rewritten_query - ); - $limit = $limit * 10; - } while ( is_null( $query ) ); - - // Reset the pcre.backtrack_limit. - ini_set( 'pcre.backtrack_limit', $_limit ); - } - - if ( isset( $query ) ) { - $this->queries[] = "With Placeholders:\n" . $query; - $this->prepared_query = $query; - } - } - - /** - * Call back function to replace field data with PDO parameter. - * - * @param string $matches Matched string. - * - * @return string - */ - private function replace_variables_with_placeholders( $matches ) { - // Remove the WordPress escaping mechanism. - $param = stripslashes( $matches[0] ); - - // Remove trailing spaces. - $param = trim( $param ); - - // Remove the quotes at the end and the beginning. - if ( in_array( $param[ strlen( $param ) - 1 ], array( "'", '"' ), true ) ) { - $param = substr( $param, 0, -1 ); // End. - } - if ( in_array( $param[0], array( "'", '"' ), true ) ) { - $param = substr( $param, 1 ); // Start. - } - $key = ':param_' . $this->param_num++; - $this->extracted_variables[] = $param; - // Return the placeholder. - return ' ' . $key . ' '; - } - - /** - * Method to determine which query type the argument is. - * - * It takes the query string ,determines the type and returns the type string. - * If the query is the type that SQLite Integration can't executes, returns false. - * - * @param string $query The query string. - * - * @return boolean - */ - private function determine_query_type( $query ) { - $result = preg_match( - '/^\\s*(SET|EXPLAIN|PRAGMA|SELECT\\s*FOUND_ROWS|SELECT|INSERT|UPDATE|REPLACE|DELETE|ALTER|CREATE|DROP\\s*INDEX|DROP|SHOW\\s*\\w+\\s*\\w+\\s*|DESCRIBE|DESC|TRUNCATE|OPTIMIZE|CHECK|ANALYZE)/i', - $query, - $match - ); - - if ( ! $result ) { - return false; - } - $this->query_type = strtolower( $match[1] ); - if ( stripos( $this->query_type, 'found' ) !== false ) { - $this->query_type = 'foundrows'; - } - if ( stripos( $this->query_type, 'show' ) !== false ) { - if ( stripos( $this->query_type, 'show table status' ) !== false ) { - $this->query_type = 'showstatus'; - } elseif ( - stripos( $this->query_type, 'show tables' ) !== false || - stripos( $this->query_type, 'show full tables' ) !== false - ) { - $this->query_type = 'show'; - } elseif ( - stripos( $this->query_type, 'show columns' ) !== false || - stripos( $this->query_type, 'show fields' ) !== false || - stripos( $this->query_type, 'show full columns' ) !== false - ) { - $this->query_type = 'showcolumns'; - } elseif ( - stripos( $this->query_type, 'show index' ) !== false || - stripos( $this->query_type, 'show indexes' ) !== false || - stripos( $this->query_type, 'show keys' ) !== false - ) { - $this->query_type = 'showindex'; - } elseif ( - stripos( $this->query_type, 'show variables' ) !== false || - stripos( $this->query_type, 'show global variables' ) !== false || - stripos( $this->query_type, 'show session variables' ) !== false - ) { - $this->query_type = 'show_variables'; - } else { - return false; - } - } - if ( stripos( $this->query_type, 'drop index' ) !== false ) { - $this->query_type = 'drop_index'; - } - - return true; - } - - /** - * Method to execute INSERT query for SQLite version 3.7.11 or later. - * - * SQLite version 3.7.11 began to support multiple rows insert with values - * clause. This is for that version or later. - * - * @param string $query The query string. - */ - private function execute_insert_query_new( $query ) { - $engine = $this->prepare_engine( $this->query_type ); - $this->rewritten_query = $engine->rewrite_query( $query, $this->query_type ); - $this->queries[] = "Rewritten:\n" . $this->rewritten_query; - $this->extract_variables(); - $statement = $this->prepare_query(); - $this->execute_query( $statement ); - } - - /** - * Method to execute INSERT query for SQLite version 3.7.10 or lesser. - * - * It executes the INSERT query for SQLite version 3.7.10 or lesser. It is - * necessary to rewrite multiple row values. - * - * @param string $query The query string. - */ - private function execute_insert_query( $query ) { - global $wpdb; - $multi_insert = false; - $statement = null; - $engine = $this->prepare_engine( $this->query_type ); - if ( preg_match( '/(INSERT.*?VALUES\\s*)(\(.*\))/imsx', $query, $matched ) ) { - $query_prefix = $matched[1]; - $values_data = $matched[2]; - if ( stripos( $values_data, 'ON DUPLICATE KEY' ) !== false ) { - $exploded_parts = $values_data; - } elseif ( stripos( $query_prefix, "INSERT INTO $wpdb->comments" ) !== false ) { - $exploded_parts = $values_data; - } else { - $exploded_parts = $this->parse_multiple_inserts( $values_data ); - } - $count = count( $exploded_parts ); - if ( $count > 1 ) { - $multi_insert = true; - } - } - if ( $multi_insert ) { - $first = true; - foreach ( $exploded_parts as $value ) { - $suffix = ( substr( $value, -1, 1 ) === ')' ) ? '' : ')'; - - $query_string = $query_prefix . ' ' . $value . $suffix; - $this->rewritten_query = $engine->rewrite_query( $query_string, $this->query_type ); - $this->queries[] = "Rewritten:\n" . $this->rewritten_query; - $this->extracted_variables = array(); - $this->extract_variables(); - if ( $first ) { - $statement = $this->prepare_query(); - $this->execute_query( $statement ); - $first = false; - } else { - $this->execute_query( $statement ); - } - } - } else { - $this->rewritten_query = $engine->rewrite_query( $query, $this->query_type ); - $this->queries[] = "Rewritten:\n" . $this->rewritten_query; - $this->extract_variables(); - $statement = $this->prepare_query(); - $this->execute_query( $statement ); - } - } - - /** - * Method to help rewriting multiple row values insert query. - * - * It splits the values clause into an array to execute separately. - * - * @param string $values The values to be split. - * - * @return array - */ - private function parse_multiple_inserts( $values ) { - $tokens = preg_split( "/(''|(?prepare_engine( $this->query_type ); - $rewritten_query = $engine->rewrite_query( $query ); - $reason = 0; - $message = ''; - - try { - $this->beginTransaction(); - foreach ( (array) $rewritten_query as $single_query ) { - $this->queries[] = "Executing:\n" . $single_query; - $single_query = trim( $single_query ); - if ( empty( $single_query ) ) { - continue; - } - $this->pdo->exec( $single_query ); - } - $this->commit(); - } catch ( PDOException $err ) { - $reason = $err->getCode(); - $message = $err->getMessage(); - if ( 5 == $reason || 6 == $reason ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison - $this->commit(); - } else { - $this->rollBack(); - } - } - if ( $reason > 0 ) { - $err_message = sprintf( 'Problem in creating table or index. Error was: %s', $message ); - $this->set_error( __LINE__, __FUNCTION__, $err_message ); - - return false; - } - - return true; - } - - /** - * Method to execute ALTER TABLE query. - * - * @param string $query The query to execute. - * - * @return boolean - */ - private function execute_alter_query( $query ) { - $engine = $this->prepare_engine( $this->query_type ); - $reason = 0; - $message = ''; - $re_query = ''; - $rewritten_query = $engine->rewrite_query( $query, $this->query_type ); - if ( is_array( $rewritten_query ) && array_key_exists( 'recursion', $rewritten_query ) ) { - $re_query = $rewritten_query['recursion']; - unset( $rewritten_query['recursion'] ); - } - try { - $this->beginTransaction(); - if ( is_array( $rewritten_query ) ) { - foreach ( $rewritten_query as $single_query ) { - $this->queries[] = "Executing:\n" . $single_query; - $single_query = trim( $single_query ); - if ( empty( $single_query ) ) { - continue; - } - $this->pdo->exec( $single_query ); - } - } else { - $this->queries[] = "Executing:\n" . $rewritten_query; - $rewritten_query = trim( $rewritten_query ); - $this->pdo->exec( $rewritten_query ); - } - $this->commit(); - } catch ( PDOException $err ) { - $reason = $err->getCode(); - $message = $err->getMessage(); - if ( 5 == $reason || 6 == $reason ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison - $this->commit(); - usleep( 10000 ); - } else { - $this->rollBack(); - } - } - if ( '' !== $re_query ) { - $this->query( $re_query ); - } - if ( $reason > 0 ) { - $err_message = sprintf( 'Problem in executing alter query. Error was: %s', $message ); - $this->set_error( __LINE__, __FUNCTION__, $err_message ); - - return false; - } - - return true; - } - - /** - * Method to execute SHOW VARIABLES query - * - * This query is meaningless for SQLite. This function returns null data with some - * exceptions and only avoids the error message. - * - * @param string $query The query to execute. - * - * @return bool - */ - private function show_variables_workaround( $query ) { - $dummy_data = array( - 'Variable_name' => '', - 'Value' => null, - ); - $pattern = '/SHOW\\s*VARIABLES\\s*LIKE\\s*(.*)?$/im'; - if ( preg_match( $pattern, $query, $match ) ) { - $value = str_replace( "'", '', $match[1] ); - $dummy_data['Variable_name'] = trim( $value ); - // This is set for Wordfence Security Plugin. - $dummy_data['Value'] = ''; - if ( 'max_allowed_packet' === $value ) { - $dummy_data['Value'] = 1047552; - } - } - $_results[] = new Perflab_SQLite_Object_Array( $dummy_data ); - $this->results = $_results; - $this->num_rows = count( $this->results ); - $this->return_value = $this->num_rows; - - return true; - } - - /** - * Method to execute SHOW TABLE STATUS query. - * - * This query is meaningless for SQLite. This function returns dummy data. - * - * @param string $query The query to execute. - * - * @return bool - */ - private function show_status_workaround( $query ) { - $pattern = '/^SHOW\\s*TABLE\\s*STATUS\\s*LIKE\\s*(.*?)$/im'; - $table_name = ''; - if ( preg_match( $pattern, $query, $match ) ) { - $table_name = str_replace( "'", '', $match[1] ); - } - $dummy_data = array( - 'Name' => $table_name, - 'Engine' => '', - 'Version' => '', - 'Row_format' => '', - 'Rows' => 0, - 'Avg_row_length' => 0, - 'Data_length' => 0, - 'Max_data_length' => 0, - 'Index_length' => 0, - 'Data_free' => 0, - 'Auto_increment' => 0, - 'Create_time' => '', - 'Update_time' => '', - 'Check_time' => '', - 'Collation' => '', - 'Checksum' => '', - 'Create_options' => '', - 'Comment' => '', - ); - $_results[] = new Perflab_SQLite_Object_Array( $dummy_data ); - $this->results = $_results; - $this->num_rows = count( $this->results ); - $this->return_value = $this->num_rows; - - return true; - } - - /** - * Method to format the queried data to that of MySQL. - * - * @param string $engine Not used. - */ - private function process_results( $engine ) { - if ( in_array( $this->query_type, array( 'describe', 'desc', 'showcolumns' ), true ) ) { - $this->convert_to_columns_object(); - } elseif ( 'showindex' === $this->query_type ) { - $this->convert_to_index_object(); - } elseif ( in_array( $this->query_type, array( 'check', 'analyze' ), true ) ) { - $this->convert_result_check_or_analyze(); - } else { - $this->results = $this->_results; - } - } - - /** - * Method to format the error messages and put out to the file. - * - * When $wpdb::suppress_errors is set to true or $wpdb::show_errors is set to false, - * the error messages are ignored. - * - * @param string $line Where the error occurred. - * @param string $function Indicate the function name where the error occurred. - * @param string $message The message. - * - * @return boolean|void - */ - private function set_error( $line, $function, $message ) { - global $wpdb; - $this->errors[] = array( - 'line' => $line, - 'function' => $function, - ); - $this->error_messages[] = $message; - $this->is_error = true; - if ( $wpdb->suppress_errors || ! $wpdb->show_errors ) { - return false; - } - error_log( "Line $line, Function: $function, Message: $message" ); - } - - /** - * Method to change the queried data to PHP object format. - * - * It takes the associative array of query results and creates a numeric - * array of anonymous objects - * - * @access private - */ - private function convert_to_object() { - $_results = array(); - if ( count( $this->results ) === 0 ) { - echo $this->get_error_message(); - } else { - foreach ( $this->results as $row ) { - $_results[] = new Perflab_SQLite_Object_Array( $row ); - } - } - $this->results = $_results; - } - - /** - * Method to convert the SHOW COLUMNS query data to an object. - * - * It rewrites pragma results to mysql compatible array - * when query_type is describe, we use sqlite pragma function. - * - * @access private - */ - private function convert_to_columns_object() { - $_results = array(); - $_columns = array( // Field names MySQL SHOW COLUMNS returns. - 'Field' => '', - 'Type' => '', - 'Null' => '', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ); - if ( empty( $this->_results ) ) { - echo $this->get_error_message(); - } else { - foreach ( $this->_results as $row ) { - if ( ! is_object( $row ) ) { - continue; - } - if ( property_exists( $row, 'name' ) ) { - $_columns['Field'] = $row->name; - } - if ( property_exists( $row, 'type' ) ) { - $_columns['Type'] = $row->type; - } - if ( property_exists( $row, 'notnull' ) ) { - $_columns['Null'] = $row->notnull ? 'NO' : 'YES'; - } - if ( property_exists( $row, 'pk' ) ) { - $_columns['Key'] = $row->pk ? 'PRI' : ''; - } - if ( property_exists( $row, 'dflt_value' ) ) { - $_columns['Default'] = $row->dflt_value; - } - $_results[] = new Perflab_SQLite_Object_Array( $_columns ); - } - } - $this->results = $_results; - } - - /** - * Method to convert SHOW INDEX query data to PHP object. - * - * It rewrites the result of SHOW INDEX to the Object compatible with MySQL - * added the WHERE clause manipulation (ver 1.3.1) - * - * @access private - */ - private function convert_to_index_object() { - $_results = array(); - $_columns = array( - 'Table' => '', - 'Non_unique' => '', // Unique -> 0, not unique -> 1. - 'Key_name' => '', // The name of the index. - 'Seq_in_index' => '', // Column sequence number in the index. begins at 1. - 'Column_name' => '', - 'Collation' => '', // A(scend) or NULL. - 'Cardinality' => '', - 'Sub_part' => '', // Set to NULL. - 'Packed' => '', // How to pack key or else NULL. - 'Null' => '', // If column contains null, YES. If not, NO. - 'Index_type' => '', // BTREE, FULLTEXT, HASH, RTREE. - 'Comment' => '', - ); - if ( 0 === count( $this->_results ) ) { - echo $this->get_error_message(); - } else { - foreach ( $this->_results as $row ) { - if ( 'table' === $row->type && ! stripos( $row->sql, 'primary' ) ) { - continue; - } - if ( 'index' === $row->type && stripos( $row->name, 'sqlite_autoindex' ) !== false ) { - continue; - } - switch ( $row->type ) { - case 'table': - $pattern1 = '/^\\s*PRIMARY.*\((.*)\)/im'; - $pattern2 = '/^\\s*(\\w+)?\\s*.*PRIMARY.*(?!\()/im'; - if ( preg_match( $pattern1, $row->sql, $match ) ) { - $col_name = trim( $match[1] ); - $_columns['Key_name'] = 'PRIMARY'; - $_columns['Non_unique'] = 0; - $_columns['Column_name'] = $col_name; - } elseif ( preg_match( $pattern2, $row->sql, $match ) ) { - $col_name = trim( $match[1] ); - $_columns['Key_name'] = 'PRIMARY'; - $_columns['Non_unique'] = 0; - $_columns['Column_name'] = $col_name; - } - break; - - case 'index': - $_columns['Non_unique'] = 1; - if ( stripos( $row->sql, 'unique' ) !== false ) { - $_columns['Non_unique'] = 0; - } - if ( preg_match( '/^.*\((.*)\)/i', $row->sql, $match ) ) { - $col_name = str_replace( "'", '', $match[1] ); - $_columns['Column_name'] = trim( $col_name ); - } - $_columns['Key_name'] = $row->name; - break; - - } - $_columns['Table'] = $row->tbl_name; - $_columns['Collation'] = null; - $_columns['Cardinality'] = 0; - $_columns['Sub_part'] = null; - $_columns['Packed'] = null; - $_columns['Null'] = 'NO'; - $_columns['Index_type'] = 'BTREE'; - $_columns['Comment'] = ''; - $_results[] = new Perflab_SQLite_Object_Array( $_columns ); - } - if ( stripos( $this->queries[0], 'WHERE' ) !== false ) { - preg_match( '/WHERE\\s*(.*)$/im', $this->queries[0], $match ); - list($key, $value) = explode( '=', $match[1] ); - $key = trim( $key ); - $value = trim( preg_replace( "/[\';]/", '', $value ) ); - foreach ( $_results as $result ) { - if ( ! empty( $result->$key ) && is_scalar( $result->$key ) && stripos( $value, $result->$key ) !== false ) { - unset( $_results ); - $_results[] = $result; - break; - } - } - } - } - - $this->results = $_results; - } - - /** - * Method to the CHECK query data to an object. - * - * @access private - */ - private function convert_result_check_or_analyze() { - $is_check = 'check' === $this->query_type; - $_results[] = new Perflab_SQLite_Object_Array( - array( - 'Table' => '', - 'Op' => $is_check ? 'check' : 'analyze', - 'Msg_type' => 'status', - 'Msg_text' => $is_check ? __( 'OK', 'performance-lab' ) : __( 'Table is already up to date', 'performance-lab' ), - ) - ); - $this->results = $_results; - } - - /** - * Method to check SQLite library version. - * - * This is used for checking if SQLite can execute multiple rows insert. - * - * @return version number string or 0 - * @access private - */ - private function get_sqlite_version() { - try { - $statement = $this->pdo->prepare( 'SELECT sqlite_version()' ); - $statement->execute(); - $result = $statement->fetch( PDO::FETCH_NUM ); // phpcs:ignore WordPress.DB.RestrictedClasses - - return $result[0]; - } catch ( PDOException $err ) { - return '0'; - } - } - - /** - * Method to call PDO::beginTransaction(). - * - * @see PDO::beginTransaction() - * @return boolean - */ - public function beginTransaction() { - if ( $this->has_active_transaction ) { - return false; - } - $this->has_active_transaction = $this->pdo->beginTransaction(); - return $this->has_active_transaction; - } - - /** - * Method to call PDO::commit(). - * - * @see PDO::commit() - * @return void - */ - public function commit() { - $this->pdo->commit(); - $this->has_active_transaction = false; - } - - /** - * Method to call PDO::rollBack(). - * - * @see PDO::rollBack() - * @return void - */ - public function rollBack() { - $this->pdo->rollBack(); - $this->has_active_transaction = false; - } -} diff --git a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-db.php b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-db.php similarity index 85% rename from modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-db.php rename to modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-db.php index bee6b09285..f3a066aed1 100644 --- a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-db.php +++ b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-db.php @@ -11,14 +11,14 @@ * * It also rewrites some methods that use mysql specific functions. */ -class Perflab_SQLite_DB extends wpdb { +class WP_SQLite_DB extends wpdb { /** * Database Handle * * @access protected * - * @var Perflab_SQLite_PDO_Engine + * @var WP_SQLite_Translator */ protected $dbh; @@ -29,6 +29,7 @@ class Perflab_SQLite_DB extends wpdb { */ public function __construct() { parent::__construct( '', '', '', '' ); + $this->charset = 'utf8mb4'; } /** @@ -45,6 +46,20 @@ public function __construct() { public function set_charset( $dbh, $charset = null, $collate = null ) { } + /** + * Method to get the character set for the database. + * Hardcoded to utf8mb4 for now. + * + * @param string $table The table name. + * @param string $column The column name. + * + * @return string The character set. + */ + public function get_col_charset( $table, $column ) { + // Hardcoded for now. + return 'utf8mb4'; + } + /** * Method to dummy out wpdb::set_sql_mode() * @@ -55,6 +70,16 @@ public function set_charset( $dbh, $charset = null, $collate = null ) { public function set_sql_mode( $modes = array() ) { } + /** + * Closes the current database connection. + * Noop in SQLite. + * + * @return bool True to indicate the connection was successfully closed. + */ + public function close() { + return true; + } + /** * Method to select the database connection. * @@ -133,11 +158,10 @@ public function print_error( $str = '' ) { wp_load_translations_early(); $caller = $this->get_caller(); - $caller = $caller ? $caller : __( '(unknown)', 'performance-lab' ); + $caller = $caller ? $caller : '(unknown)'; $error_str = sprintf( - /* translators: 1: Database error message, 2: SQL query, 3: Caller. */ - __( 'WordPress database error %1$s for query %2$s made by %3$s', 'performance-lab' ), + 'WordPress database error %1$s for query %2$s made by %3$s', $str, $this->last_query, $caller @@ -162,8 +186,7 @@ public function print_error( $str = '' ) { $query = htmlspecialchars( $this->last_query, ENT_QUOTES ); printf( - /* translators: 1: Database error message, 2: SQL query. */ - '

' . __( 'WordPress database error: [%1$s] %2$s', 'performance-lab' ) . '

', + '

WordPress database error: [%1$s] %2$s

', $str, '' . $query . '' ); @@ -199,9 +222,22 @@ public function flush() { * @return void */ public function db_connect( $allow_bail = true ) { + if ( $this->dbh ) { + return; + } $this->init_charset(); - $this->dbh = new Perflab_SQLite_PDO_Engine(); - $this->ready = true; + + $pdo = null; + if ( isset( $GLOBALS['@pdo'] ) ) { + $pdo = $GLOBALS['@pdo']; + } + $this->dbh = new WP_SQLite_Translator( $pdo ); + $this->last_error = $this->dbh->get_error_message(); + if ( $this->last_error ) { + return false; + } + $GLOBALS['@pdo'] = $this->dbh->get_pdo(); + $this->ready = true; } /** @@ -251,12 +287,12 @@ public function query( $query ) { } $this->last_error = $this->dbh->get_error_message(); - if ( $this->last_error && ( ! defined( 'WP_INSTALLING' ) || ! WP_INSTALLING ) ) { + if ( $this->last_error ) { $this->print_error( $this->last_error ); return false; } - if ( preg_match( '/^\\s*(create|alter|truncate|drop|optimize)\\s*/i', $query ) ) { + if ( preg_match( '/^\\s*(set|create|alter|truncate|drop|optimize)\\s*/i', $query ) ) { return $this->dbh->get_return_value(); } diff --git a/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-lexer.php b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-lexer.php new file mode 100644 index 0000000000..d16fa8a3d9 --- /dev/null +++ b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-lexer.php @@ -0,0 +1,2580 @@ + + */ + public static $operators = array( + + /* + * Some operators (*, =) may have ambiguous flags, because they depend on + * the context they are being used in. + * For example: 1. SELECT * FROM table; # SQL specific (wildcard) + * SELECT 2 * 3; # arithmetic + * 2. SELECT * FROM table WHERE foo = 'bar'; + * SET @i = 0; + */ + + // @see WP_SQLite_Token::FLAG_OPERATOR_ARITHMETIC + '%' => 1, + '*' => 1, + '+' => 1, + '-' => 1, + '/' => 1, + + // @see WP_SQLite_Token::FLAG_OPERATOR_LOGICAL + '!' => 2, + '!=' => 2, + '&&' => 2, + '<' => 2, + '<=' => 2, + '<=>' => 2, + '<>' => 2, + '=' => 2, + '>' => 2, + '>=' => 2, + '||' => 2, + + // @see WP_SQLite_Token::FLAG_OPERATOR_BITWISE + '&' => 4, + '<<' => 4, + '>>' => 4, + '^' => 4, + '|' => 4, + '~' => 4, + + // @see WP_SQLite_Token::FLAG_OPERATOR_ASSIGNMENT + ':=' => 8, + + // @see WP_SQLite_Token::FLAG_OPERATOR_SQL + '(' => 16, + ')' => 16, + '.' => 16, + ',' => 16, + ';' => 16, + ); + + /** + * List of keywords. + * + * The value associated to each keyword represents its flags. + * + * @see WP_SQLite_Token::FLAG_KEYWORD_RESERVED + * WP_SQLite_Token::FLAG_KEYWORD_COMPOSED + * WP_SQLite_Token::FLAG_KEYWORD_DATA_TYPE + * ∂WP_SQLite_Token::FLAG_KEYWORD_KEY + * WP_SQLite_Token::FLAG_KEYWORD_FUNCTION + * + * @var array + */ + public static $keywords = array( + 'AT' => 1, + 'DO' => 1, + 'IO' => 1, + 'NO' => 1, + 'XA' => 1, + 'ANY' => 1, + 'CPU' => 1, + 'END' => 1, + 'IPC' => 1, + 'NDB' => 1, + 'NEW' => 1, + 'ONE' => 1, + 'ROW' => 1, + 'XID' => 1, + 'BOOL' => 1, + 'BYTE' => 1, + 'CODE' => 1, + 'CUBE' => 1, + 'DATA' => 1, + 'DISK' => 1, + 'ENDS' => 1, + 'FAST' => 1, + 'FILE' => 1, + 'FULL' => 1, + 'HASH' => 1, + 'HELP' => 1, + 'HOST' => 1, + 'LAST' => 1, + 'LESS' => 1, + 'LIST' => 1, + 'LOGS' => 1, + 'MODE' => 1, + 'NAME' => 1, + 'NEXT' => 1, + 'NONE' => 1, + 'ONLY' => 1, + 'OPEN' => 1, + 'PAGE' => 1, + 'PORT' => 1, + 'PREV' => 1, + 'ROWS' => 1, + 'SLOW' => 1, + 'SOME' => 1, + 'STOP' => 1, + 'THAN' => 1, + 'TYPE' => 1, + 'VIEW' => 1, + 'WAIT' => 1, + 'WORK' => 1, + 'X509' => 1, + 'AFTER' => 1, + 'BEGIN' => 1, + 'BLOCK' => 1, + 'BTREE' => 1, + 'CACHE' => 1, + 'CHAIN' => 1, + 'CLOSE' => 1, + 'ERROR' => 1, + 'EVENT' => 1, + 'EVERY' => 1, + 'FIRST' => 1, + 'FIXED' => 1, + 'FLUSH' => 1, + 'FOUND' => 1, + 'HOSTS' => 1, + 'LEVEL' => 1, + 'LOCAL' => 1, + 'LOCKS' => 1, + 'MERGE' => 1, + 'MUTEX' => 1, + 'NAMES' => 1, + 'NCHAR' => 1, + 'NEVER' => 1, + 'OWNER' => 1, + 'PHASE' => 1, + 'PROXY' => 1, + 'QUERY' => 1, + 'QUICK' => 1, + 'RELAY' => 1, + 'RESET' => 1, + 'RTREE' => 1, + 'SHARE' => 1, + 'SLAVE' => 1, + 'START' => 1, + 'SUPER' => 1, + 'SWAPS' => 1, + 'TYPES' => 1, + 'UNTIL' => 1, + 'VALUE' => 1, + 'ACTION' => 1, + 'ALWAYS' => 1, + 'BACKUP' => 1, + 'BINLOG' => 1, + 'CIPHER' => 1, + 'CLIENT' => 1, + 'COMMIT' => 1, + 'ENABLE' => 1, + 'ENGINE' => 1, + 'ERRORS' => 1, + 'ESCAPE' => 1, + 'EVENTS' => 1, + 'EXPIRE' => 1, + 'EXPORT' => 1, + 'FAULTS' => 1, + 'FIELDS' => 1, + 'FILTER' => 1, + 'GLOBAL' => 1, + 'GRANTS' => 1, + 'IMPORT' => 1, + 'ISSUER' => 1, + 'LEAVES' => 1, + 'MASTER' => 1, + 'MEDIUM' => 1, + 'MEMORY' => 1, + 'MODIFY' => 1, + 'NUMBER' => 1, + 'OFFSET' => 1, + 'PARSER' => 1, + 'PLUGIN' => 1, + 'RELOAD' => 1, + 'REMOVE' => 1, + 'REPAIR' => 1, + 'RESUME' => 1, + 'ROLLUP' => 1, + 'SERVER' => 1, + 'SIGNED' => 1, + 'SIMPLE' => 1, + 'SOCKET' => 1, + 'SONAME' => 1, + 'SOUNDS' => 1, + 'SOURCE' => 1, + 'STARTS' => 1, + 'STATUS' => 1, + 'STRING' => 1, + 'TABLES' => 1, + 'ACCOUNT' => 1, + 'ANALYSE' => 1, + 'CHANGED' => 1, + 'CHANNEL' => 1, + 'COLUMNS' => 1, + 'COMMENT' => 1, + 'COMPACT' => 1, + 'CONTEXT' => 1, + 'CURRENT' => 1, + 'DEFINER' => 1, + 'DISABLE' => 1, + 'DISCARD' => 1, + 'DYNAMIC' => 1, + 'ENGINES' => 1, + 'EXECUTE' => 1, + 'FOLLOWS' => 1, + 'GENERAL' => 1, + 'HANDLER' => 1, + 'INDEXES' => 1, + 'INSTALL' => 1, + 'INVOKER' => 1, + 'LOGFILE' => 1, + 'MIGRATE' => 1, + 'NO_WAIT' => 1, + 'OPTIONS' => 1, + 'PARTIAL' => 1, + 'PERSIST' => 1, + 'PLUGINS' => 1, + 'PREPARE' => 1, + 'PROFILE' => 1, + 'REBUILD' => 1, + 'RECOVER' => 1, + 'RESTORE' => 1, + 'RETURNS' => 1, + 'ROUTINE' => 1, + 'SESSION' => 1, + 'STACKED' => 1, + 'STORAGE' => 1, + 'SUBJECT' => 1, + 'SUSPEND' => 1, + 'UNICODE' => 1, + 'UNKNOWN' => 1, + 'UPGRADE' => 1, + 'USE_FRM' => 1, + 'WITHOUT' => 1, + 'WRAPPER' => 1, + 'CASCADED' => 1, + 'CHECKSUM' => 1, + 'DATAFILE' => 1, + 'DUMPFILE' => 1, + 'EXCHANGE' => 1, + 'EXTENDED' => 1, + 'FUNCTION' => 1, + 'LANGUAGE' => 1, + 'MAX_ROWS' => 1, + 'MAX_SIZE' => 1, + 'MIN_ROWS' => 1, + 'NATIONAL' => 1, + 'NVARCHAR' => 1, + 'PRECEDES' => 1, + 'PRESERVE' => 1, + 'PROFILES' => 1, + 'REDOFILE' => 1, + 'RELAYLOG' => 1, + 'ROLLBACK' => 1, + 'SCHEDULE' => 1, + 'SECURITY' => 1, + 'SEQUENCE' => 1, + 'SHUTDOWN' => 1, + 'SNAPSHOT' => 1, + 'SWITCHES' => 1, + 'TRIGGERS' => 1, + 'UNDOFILE' => 1, + 'WARNINGS' => 1, + 'AGGREGATE' => 1, + 'ALGORITHM' => 1, + 'COMMITTED' => 1, + 'DIRECTORY' => 1, + 'DUPLICATE' => 1, + 'EXPANSION' => 1, + 'INVISIBLE' => 1, + 'IO_THREAD' => 1, + 'ISOLATION' => 1, + 'NODEGROUP' => 1, + 'PACK_KEYS' => 1, + 'READ_ONLY' => 1, + 'REDUNDANT' => 1, + 'SAVEPOINT' => 1, + 'SQL_CACHE' => 1, + 'TEMPORARY' => 1, + 'TEMPTABLE' => 1, + 'UNDEFINED' => 1, + 'UNINSTALL' => 1, + 'VARIABLES' => 1, + 'COMPLETION' => 1, + 'COMPRESSED' => 1, + 'CONCURRENT' => 1, + 'CONNECTION' => 1, + 'CONSISTENT' => 1, + 'DEALLOCATE' => 1, + 'IDENTIFIED' => 1, + 'MASTER_SSL' => 1, + 'NDBCLUSTER' => 1, + 'PARTITIONS' => 1, + 'PERSISTENT' => 1, + 'PLUGIN_DIR' => 1, + 'PRIVILEGES' => 1, + 'REORGANIZE' => 1, + 'REPEATABLE' => 1, + 'ROW_FORMAT' => 1, + 'SQL_THREAD' => 1, + 'TABLESPACE' => 1, + 'TABLE_NAME' => 1, + 'VALIDATION' => 1, + 'COLUMN_NAME' => 1, + 'COMPRESSION' => 1, + 'CURSOR_NAME' => 1, + 'DIAGNOSTICS' => 1, + 'EXTENT_SIZE' => 1, + 'MASTER_HOST' => 1, + 'MASTER_PORT' => 1, + 'MASTER_USER' => 1, + 'MYSQL_ERRNO' => 1, + 'NONBLOCKING' => 1, + 'PROCESSLIST' => 1, + 'REPLICATION' => 1, + 'SCHEMA_NAME' => 1, + 'SQL_TSI_DAY' => 1, + 'TRANSACTION' => 1, + 'UNCOMMITTED' => 1, + 'CATALOG_NAME' => 1, + 'CLASS_ORIGIN' => 1, + 'DEFAULT_AUTH' => 1, + 'DES_KEY_FILE' => 1, + 'INITIAL_SIZE' => 1, + 'MASTER_DELAY' => 1, + 'MESSAGE_TEXT' => 1, + 'PARTITIONING' => 1, + 'PERSIST_ONLY' => 1, + 'RELAY_THREAD' => 1, + 'SERIALIZABLE' => 1, + 'SQL_NO_CACHE' => 1, + 'SQL_TSI_HOUR' => 1, + 'SQL_TSI_WEEK' => 1, + 'SQL_TSI_YEAR' => 1, + 'SUBPARTITION' => 1, + 'COLUMN_FORMAT' => 1, + 'INSERT_METHOD' => 1, + 'MASTER_SSL_CA' => 1, + 'RELAY_LOG_POS' => 1, + 'SQL_TSI_MONTH' => 1, + 'SUBPARTITIONS' => 1, + 'AUTO_INCREMENT' => 1, + 'AVG_ROW_LENGTH' => 1, + 'KEY_BLOCK_SIZE' => 1, + 'MASTER_LOG_POS' => 1, + 'MASTER_SSL_CRL' => 1, + 'MASTER_SSL_KEY' => 1, + 'RELAY_LOG_FILE' => 1, + 'SQL_TSI_MINUTE' => 1, + 'SQL_TSI_SECOND' => 1, + 'TABLE_CHECKSUM' => 1, + 'USER_RESOURCES' => 1, + 'AUTOEXTEND_SIZE' => 1, + 'CONSTRAINT_NAME' => 1, + 'DELAY_KEY_WRITE' => 1, + 'FILE_BLOCK_SIZE' => 1, + 'MASTER_LOG_FILE' => 1, + 'MASTER_PASSWORD' => 1, + 'MASTER_SSL_CERT' => 1, + 'PARSE_GCOL_EXPR' => 1, + 'REPLICATE_DO_DB' => 1, + 'SQL_AFTER_GTIDS' => 1, + 'SQL_TSI_QUARTER' => 1, + 'SUBCLASS_ORIGIN' => 1, + 'MASTER_SERVER_ID' => 1, + 'REDO_BUFFER_SIZE' => 1, + 'SQL_BEFORE_GTIDS' => 1, + 'STATS_PERSISTENT' => 1, + 'UNDO_BUFFER_SIZE' => 1, + 'CONSTRAINT_SCHEMA' => 1, + 'GROUP_REPLICATION' => 1, + 'IGNORE_SERVER_IDS' => 1, + 'MASTER_SSL_CAPATH' => 1, + 'MASTER_SSL_CIPHER' => 1, + 'RETURNED_SQLSTATE' => 1, + 'SQL_BUFFER_RESULT' => 1, + 'STATS_AUTO_RECALC' => 1, + 'CONSTRAINT_CATALOG' => 1, + 'MASTER_RETRY_COUNT' => 1, + 'MASTER_SSL_CRLPATH' => 1, + 'MAX_STATEMENT_TIME' => 1, + 'REPLICATE_DO_TABLE' => 1, + 'SQL_AFTER_MTS_GAPS' => 1, + 'STATS_SAMPLE_PAGES' => 1, + 'REPLICATE_IGNORE_DB' => 1, + 'MASTER_AUTO_POSITION' => 1, + 'MASTER_CONNECT_RETRY' => 1, + 'MAX_QUERIES_PER_HOUR' => 1, + 'MAX_UPDATES_PER_HOUR' => 1, + 'MAX_USER_CONNECTIONS' => 1, + 'REPLICATE_REWRITE_DB' => 1, + 'REPLICATE_IGNORE_TABLE' => 1, + 'MASTER_HEARTBEAT_PERIOD' => 1, + 'REPLICATE_WILD_DO_TABLE' => 1, + 'MAX_CONNECTIONS_PER_HOUR' => 1, + 'REPLICATE_WILD_IGNORE_TABLE' => 1, + + 'AS' => 3, + 'BY' => 3, + 'IS' => 3, + 'ON' => 3, + 'OR' => 3, + 'TO' => 3, + 'ADD' => 3, + 'ALL' => 3, + 'AND' => 3, + 'ASC' => 3, + 'DEC' => 3, + 'DIV' => 3, + 'FOR' => 3, + 'GET' => 3, + 'NOT' => 3, + 'OUT' => 3, + 'SQL' => 3, + 'SSL' => 3, + 'USE' => 3, + 'XOR' => 3, + 'BOTH' => 3, + 'CALL' => 3, + 'CASE' => 3, + 'DESC' => 3, + 'DROP' => 3, + 'DUAL' => 3, + 'EACH' => 3, + 'ELSE' => 3, + 'EXIT' => 3, + 'FROM' => 3, + 'INT1' => 3, + 'INT2' => 3, + 'INT3' => 3, + 'INT4' => 3, + 'INT8' => 3, + 'INTO' => 3, + 'JOIN' => 3, + 'KEYS' => 3, + 'KILL' => 3, + 'LIKE' => 3, + 'LOAD' => 3, + 'LOCK' => 3, + 'LONG' => 3, + 'LOOP' => 3, + 'NULL' => 3, + 'OVER' => 3, + 'READ' => 3, + 'SHOW' => 3, + 'THEN' => 3, + 'TRUE' => 3, + 'UNDO' => 3, + 'WHEN' => 3, + 'WITH' => 3, + 'ALTER' => 3, + 'CHECK' => 3, + 'CROSS' => 3, + 'FALSE' => 3, + 'FETCH' => 3, + 'FORCE' => 3, + 'GRANT' => 3, + 'GROUP' => 3, + 'INNER' => 3, + 'INOUT' => 3, + 'LEAVE' => 3, + 'LIMIT' => 3, + 'LINES' => 3, + 'ORDER' => 3, + 'OUTER' => 3, + 'PURGE' => 3, + 'RANGE' => 3, + 'READS' => 3, + 'RLIKE' => 3, + 'TABLE' => 3, + 'UNION' => 3, + 'USAGE' => 3, + 'USING' => 3, + 'WHERE' => 3, + 'WHILE' => 3, + 'WRITE' => 3, + 'BEFORE' => 3, + 'CHANGE' => 3, + 'COLUMN' => 3, + 'CREATE' => 3, + 'CURSOR' => 3, + 'DELETE' => 3, + 'ELSEIF' => 3, + 'EXCEPT' => 3, + 'FLOAT4' => 3, + 'FLOAT8' => 3, + 'HAVING' => 3, + 'IGNORE' => 3, + 'INFILE' => 3, + 'LINEAR' => 3, + 'OPTION' => 3, + 'REGEXP' => 3, + 'RENAME' => 3, + 'RETURN' => 3, + 'REVOKE' => 3, + 'SELECT' => 3, + 'SIGNAL' => 3, + 'STORED' => 3, + 'UNLOCK' => 3, + 'UPDATE' => 3, + 'ANALYZE' => 3, + 'BETWEEN' => 3, + 'CASCADE' => 3, + 'COLLATE' => 3, + 'DECLARE' => 3, + 'DELAYED' => 3, + 'ESCAPED' => 3, + 'EXPLAIN' => 3, + 'FOREIGN' => 3, + 'ITERATE' => 3, + 'LEADING' => 3, + 'NATURAL' => 3, + 'OUTFILE' => 3, + 'PRIMARY' => 3, + 'RELEASE' => 3, + 'REQUIRE' => 3, + 'SCHEMAS' => 3, + 'TRIGGER' => 3, + 'VARYING' => 3, + 'VIRTUAL' => 3, + 'CONTINUE' => 3, + 'DAY_HOUR' => 3, + 'DESCRIBE' => 3, + 'DISTINCT' => 3, + 'ENCLOSED' => 3, + 'MAXVALUE' => 3, + 'MODIFIES' => 3, + 'OPTIMIZE' => 3, + 'RESIGNAL' => 3, + 'RESTRICT' => 3, + 'SPECIFIC' => 3, + 'SQLSTATE' => 3, + 'STARTING' => 3, + 'TRAILING' => 3, + 'UNSIGNED' => 3, + 'ZEROFILL' => 3, + 'CONDITION' => 3, + 'DATABASES' => 3, + 'GENERATED' => 3, + 'INTERSECT' => 3, + 'MIDDLEINT' => 3, + 'PARTITION' => 3, + 'PRECISION' => 3, + 'PROCEDURE' => 3, + 'RECURSIVE' => 3, + 'SENSITIVE' => 3, + 'SEPARATOR' => 3, + 'ACCESSIBLE' => 3, + 'ASENSITIVE' => 3, + 'CONSTRAINT' => 3, + 'DAY_MINUTE' => 3, + 'DAY_SECOND' => 3, + 'OPTIONALLY' => 3, + 'READ_WRITE' => 3, + 'REFERENCES' => 3, + 'SQLWARNING' => 3, + 'TERMINATED' => 3, + 'YEAR_MONTH' => 3, + 'DISTINCTROW' => 3, + 'HOUR_MINUTE' => 3, + 'HOUR_SECOND' => 3, + 'INSENSITIVE' => 3, + 'MASTER_BIND' => 3, + 'LOW_PRIORITY' => 3, + 'SQLEXCEPTION' => 3, + 'VARCHARACTER' => 3, + 'DETERMINISTIC' => 3, + 'HIGH_PRIORITY' => 3, + 'MINUTE_SECOND' => 3, + 'STRAIGHT_JOIN' => 3, + 'IO_AFTER_GTIDS' => 3, + 'SQL_BIG_RESULT' => 3, + 'DAY_MICROSECOND' => 3, + 'IO_BEFORE_GTIDS' => 3, + 'OPTIMIZER_COSTS' => 3, + 'HOUR_MICROSECOND' => 3, + 'SQL_SMALL_RESULT' => 3, + 'MINUTE_MICROSECOND' => 3, + 'NO_WRITE_TO_BINLOG' => 3, + 'SECOND_MICROSECOND' => 3, + 'SQL_CALC_FOUND_ROWS' => 3, + 'MASTER_SSL_VERIFY_SERVER_CERT' => 3, + + 'NO SQL' => 7, + 'GROUP BY' => 7, + 'NOT NULL' => 7, + 'ORDER BY' => 7, + 'SET NULL' => 7, + 'AND CHAIN' => 7, + 'FULL JOIN' => 7, + 'IF EXISTS' => 7, + 'LEFT JOIN' => 7, + 'LESS THAN' => 7, + 'LOAD DATA' => 7, + 'NO ACTION' => 7, + 'ON DELETE' => 7, + 'ON UPDATE' => 7, + 'UNION ALL' => 7, + 'CROSS JOIN' => 7, + 'ESCAPED BY' => 7, + 'FOR UPDATE' => 7, + 'INNER JOIN' => 7, + 'LINEAR KEY' => 7, + 'NO RELEASE' => 7, + 'OR REPLACE' => 7, + 'RIGHT JOIN' => 7, + 'ENCLOSED BY' => 7, + 'LINEAR HASH' => 7, + 'ON SCHEDULE' => 7, + 'STARTING BY' => 7, + 'AND NO CHAIN' => 7, + 'CONTAINS SQL' => 7, + 'FOR EACH ROW' => 7, + 'NATURAL JOIN' => 7, + 'PARTITION BY' => 7, + 'SET PASSWORD' => 7, + 'SQL SECURITY' => 7, + 'CHARACTER SET' => 7, + 'IF NOT EXISTS' => 7, + 'TERMINATED BY' => 7, + 'DATA DIRECTORY' => 7, + 'READS SQL DATA' => 7, + 'UNION DISTINCT' => 7, + 'DEFAULT CHARSET' => 7, + 'DEFAULT COLLATE' => 7, + 'FULL OUTER JOIN' => 7, + 'INDEX DIRECTORY' => 7, + 'LEFT OUTER JOIN' => 7, + 'SUBPARTITION BY' => 7, + 'DISABLE ON SLAVE' => 7, + 'GENERATED ALWAYS' => 7, + 'RIGHT OUTER JOIN' => 7, + 'MODIFIES SQL DATA' => 7, + 'NATURAL LEFT JOIN' => 7, + 'START TRANSACTION' => 7, + 'LOCK IN SHARE MODE' => 7, + 'NATURAL RIGHT JOIN' => 7, + 'SELECT TRANSACTION' => 7, + 'DEFAULT CHARACTER SET' => 7, + 'ON COMPLETION PRESERVE' => 7, + 'NATURAL LEFT OUTER JOIN' => 7, + 'NATURAL RIGHT OUTER JOIN' => 7, + 'WITH CONSISTENT SNAPSHOT' => 7, + 'ON COMPLETION NOT PRESERVE' => 7, + + 'BIT' => 9, + 'XML' => 9, + 'ENUM' => 9, + 'JSON' => 9, + 'TEXT' => 9, + 'ARRAY' => 9, + 'SERIAL' => 9, + 'BOOLEAN' => 9, + 'DATETIME' => 9, + 'GEOMETRY' => 9, + 'MULTISET' => 9, + 'MULTILINEPOINT' => 9, + 'MULTILINEPOLYGON' => 9, + + 'INT' => 11, + 'SET' => 11, + 'BLOB' => 11, + 'REAL' => 11, + 'FLOAT' => 11, + 'BIGINT' => 11, + 'DOUBLE' => 11, + 'DECIMAL' => 11, + 'INTEGER' => 11, + 'NUMERIC' => 11, + 'TINYINT' => 11, + 'VARCHAR' => 11, + 'LONGBLOB' => 11, + 'LONGTEXT' => 11, + 'SMALLINT' => 11, + 'TINYBLOB' => 11, + 'TINYTEXT' => 11, + 'CHARACTER' => 11, + 'MEDIUMINT' => 11, + 'VARBINARY' => 11, + 'MEDIUMBLOB' => 11, + 'MEDIUMTEXT' => 11, + + 'BINARY VARYING' => 15, + + 'KEY' => 19, + 'INDEX' => 19, + 'UNIQUE' => 19, + 'SPATIAL' => 19, + 'FULLTEXT' => 19, + + 'INDEX KEY' => 23, + 'UNIQUE KEY' => 23, + 'FOREIGN KEY' => 23, + 'PRIMARY KEY' => 23, + 'SPATIAL KEY' => 23, + 'FULLTEXT KEY' => 23, + 'UNIQUE INDEX' => 23, + 'SPATIAL INDEX' => 23, + 'FULLTEXT INDEX' => 23, + + 'X' => 33, + 'Y' => 33, + 'LN' => 33, + 'PI' => 33, + 'ABS' => 33, + 'AVG' => 33, + 'BIN' => 33, + 'COS' => 33, + 'COT' => 33, + 'DAY' => 33, + 'ELT' => 33, + 'EXP' => 33, + 'HEX' => 33, + 'LOG' => 33, + 'MAX' => 33, + 'MD5' => 33, + 'MID' => 33, + 'MIN' => 33, + 'NOW' => 33, + 'OCT' => 33, + 'ORD' => 33, + 'POW' => 33, + 'SHA' => 33, + 'SIN' => 33, + 'STD' => 33, + 'SUM' => 33, + 'TAN' => 33, + 'ACOS' => 33, + 'AREA' => 33, + 'ASIN' => 33, + 'ATAN' => 33, + 'CAST' => 33, + 'CEIL' => 33, + 'CONV' => 33, + 'HOUR' => 33, + 'LOG2' => 33, + 'LPAD' => 33, + 'RAND' => 33, + 'RPAD' => 33, + 'SHA1' => 33, + 'SHA2' => 33, + 'SIGN' => 33, + 'SQRT' => 33, + 'SRID' => 33, + 'ST_X' => 33, + 'ST_Y' => 33, + 'TRIM' => 33, + 'USER' => 33, + 'UUID' => 33, + 'WEEK' => 33, + 'ASCII' => 33, + 'ASWKB' => 33, + 'ASWKT' => 33, + 'ATAN2' => 33, + 'COUNT' => 33, + 'CRC32' => 33, + 'FIELD' => 33, + 'FLOOR' => 33, + 'INSTR' => 33, + 'LCASE' => 33, + 'LEAST' => 33, + 'LOG10' => 33, + 'LOWER' => 33, + 'LTRIM' => 33, + 'MONTH' => 33, + 'POWER' => 33, + 'QUOTE' => 33, + 'ROUND' => 33, + 'RTRIM' => 33, + 'SLEEP' => 33, + 'SPACE' => 33, + 'UCASE' => 33, + 'UNHEX' => 33, + 'UPPER' => 33, + 'ASTEXT' => 33, + 'BIT_OR' => 33, + 'BUFFER' => 33, + 'CONCAT' => 33, + 'DECODE' => 33, + 'ENCODE' => 33, + 'EQUALS' => 33, + 'FORMAT' => 33, + 'IFNULL' => 33, + 'ISNULL' => 33, + 'LENGTH' => 33, + 'LOCATE' => 33, + 'MINUTE' => 33, + 'NULLIF' => 33, + 'POINTN' => 33, + 'SECOND' => 33, + 'STDDEV' => 33, + 'STRCMP' => 33, + 'SUBSTR' => 33, + 'WITHIN' => 33, + 'ADDDATE' => 33, + 'ADDTIME' => 33, + 'AGAINST' => 33, + 'BIT_AND' => 33, + 'BIT_XOR' => 33, + 'CEILING' => 33, + 'CHARSET' => 33, + 'CROSSES' => 33, + 'CURDATE' => 33, + 'CURTIME' => 33, + 'DAYNAME' => 33, + 'DEGREES' => 33, + 'ENCRYPT' => 33, + 'EXTRACT' => 33, + 'GLENGTH' => 33, + 'ISEMPTY' => 33, + 'IS_IPV4' => 33, + 'IS_IPV6' => 33, + 'IS_UUID' => 33, + 'QUARTER' => 33, + 'RADIANS' => 33, + 'REVERSE' => 33, + 'SOUNDEX' => 33, + 'ST_AREA' => 33, + 'ST_SRID' => 33, + 'SUBDATE' => 33, + 'SUBTIME' => 33, + 'SYSDATE' => 33, + 'TOUCHES' => 33, + 'TO_DAYS' => 33, + 'VAR_POP' => 33, + 'VERSION' => 33, + 'WEEKDAY' => 33, + 'ASBINARY' => 33, + 'CENTROID' => 33, + 'COALESCE' => 33, + 'COMPRESS' => 33, + 'CONTAINS' => 33, + 'DATEDIFF' => 33, + 'DATE_ADD' => 33, + 'DATE_SUB' => 33, + 'DISJOINT' => 33, + 'DISTANCE' => 33, + 'ENDPOINT' => 33, + 'ENVELOPE' => 33, + 'GET_LOCK' => 33, + 'GREATEST' => 33, + 'ISCLOSED' => 33, + 'ISSIMPLE' => 33, + 'JSON_SET' => 33, + 'MAKEDATE' => 33, + 'MAKETIME' => 33, + 'MAKE_SET' => 33, + 'MBREQUAL' => 33, + 'OVERLAPS' => 33, + 'PASSWORD' => 33, + 'POSITION' => 33, + 'ST_ASWKB' => 33, + 'ST_ASWKT' => 33, + 'ST_UNION' => 33, + 'TIMEDIFF' => 33, + 'TRUNCATE' => 33, + 'VARIANCE' => 33, + 'VAR_SAMP' => 33, + 'YEARWEEK' => 33, + 'ANY_VALUE' => 33, + 'BENCHMARK' => 33, + 'BIT_COUNT' => 33, + 'COLLATION' => 33, + 'CONCAT_WS' => 33, + 'DAYOFWEEK' => 33, + 'DAYOFYEAR' => 33, + 'DIMENSION' => 33, + 'FROM_DAYS' => 33, + 'GEOMETRYN' => 33, + 'INET_ATON' => 33, + 'INET_NTOA' => 33, + 'JSON_KEYS' => 33, + 'JSON_TYPE' => 33, + 'LOAD_FILE' => 33, + 'MBRCOVERS' => 33, + 'MBREQUALS' => 33, + 'MBRWITHIN' => 33, + 'MONTHNAME' => 33, + 'NUMPOINTS' => 33, + 'ROW_COUNT' => 33, + 'ST_ASTEXT' => 33, + 'ST_BUFFER' => 33, + 'ST_EQUALS' => 33, + 'ST_LENGTH' => 33, + 'ST_POINTN' => 33, + 'ST_WITHIN' => 33, + 'SUBSTRING' => 33, + 'TO_BASE64' => 33, + 'UPDATEXML' => 33, + 'BIT_LENGTH' => 33, + 'CONVERT_TZ' => 33, + 'CONVEXHULL' => 33, + 'DAYOFMONTH' => 33, + 'EXPORT_SET' => 33, + 'FOUND_ROWS' => 33, + 'GET_FORMAT' => 33, + 'INET6_ATON' => 33, + 'INET6_NTOA' => 33, + 'INTERSECTS' => 33, + 'JSON_ARRAY' => 33, + 'JSON_DEPTH' => 33, + 'JSON_MERGE' => 33, + 'JSON_QUOTE' => 33, + 'JSON_VALID' => 33, + 'MBRTOUCHES' => 33, + 'NAME_CONST' => 33, + 'PERIOD_ADD' => 33, + 'STARTPOINT' => 33, + 'STDDEV_POP' => 33, + 'ST_CROSSES' => 33, + 'ST_GEOHASH' => 33, + 'ST_ISEMPTY' => 33, + 'ST_ISVALID' => 33, + 'ST_TOUCHES' => 33, + 'TO_SECONDS' => 33, + 'UNCOMPRESS' => 33, + 'UUID_SHORT' => 33, + 'WEEKOFYEAR' => 33, + 'AES_DECRYPT' => 33, + 'AES_ENCRYPT' => 33, + 'BIN_TO_UUID' => 33, + 'CHAR_LENGTH' => 33, + 'DATE_FORMAT' => 33, + 'DES_DECRYPT' => 33, + 'DES_ENCRYPT' => 33, + 'FIND_IN_SET' => 33, + 'FROM_BASE64' => 33, + 'GEOMFROMWKB' => 33, + 'GTID_SUBSET' => 33, + 'JSON_INSERT' => 33, + 'JSON_LENGTH' => 33, + 'JSON_OBJECT' => 33, + 'JSON_PRETTY' => 33, + 'JSON_REMOVE' => 33, + 'JSON_SEARCH' => 33, + 'LINEFROMWKB' => 33, + 'MBRCONTAINS' => 33, + 'MBRDISJOINT' => 33, + 'MBROVERLAPS' => 33, + 'MICROSECOND' => 33, + 'PERIOD_DIFF' => 33, + 'POLYFROMWKB' => 33, + 'SEC_TO_TIME' => 33, + 'STDDEV_SAMP' => 33, + 'STR_TO_DATE' => 33, + 'ST_ASBINARY' => 33, + 'ST_CENTROID' => 33, + 'ST_CONTAINS' => 33, + 'ST_DISJOINT' => 33, + 'ST_DISTANCE' => 33, + 'ST_ENDPOINT' => 33, + 'ST_ENVELOPE' => 33, + 'ST_ISCLOSED' => 33, + 'ST_ISSIMPLE' => 33, + 'ST_OVERLAPS' => 33, + 'ST_SIMPLIFY' => 33, + 'ST_VALIDATE' => 33, + 'SYSTEM_USER' => 33, + 'TIME_FORMAT' => 33, + 'TIME_TO_SEC' => 33, + 'UUID_TO_BIN' => 33, + 'COERCIBILITY' => 33, + 'EXTERIORRING' => 33, + 'EXTRACTVALUE' => 33, + 'GEOMETRYTYPE' => 33, + 'GEOMFROMTEXT' => 33, + 'GROUP_CONCAT' => 33, + 'IS_FREE_LOCK' => 33, + 'IS_USED_LOCK' => 33, + 'JSON_EXTRACT' => 33, + 'JSON_REPLACE' => 33, + 'JSON_UNQUOTE' => 33, + 'LINEFROMTEXT' => 33, + 'MBRCOVEREDBY' => 33, + 'MLINEFROMWKB' => 33, + 'MPOLYFROMWKB' => 33, + 'OCTET_LENGTH' => 33, + 'OLD_PASSWORD' => 33, + 'POINTFROMWKB' => 33, + 'POLYFROMTEXT' => 33, + 'RANDOM_BYTES' => 33, + 'RELEASE_LOCK' => 33, + 'SESSION_USER' => 33, + 'ST_ASGEOJSON' => 33, + 'ST_DIMENSION' => 33, + 'ST_GEOMETRYN' => 33, + 'ST_NUMPOINTS' => 33, + 'TIMESTAMPADD' => 33, + 'CONNECTION_ID' => 33, + 'FROM_UNIXTIME' => 33, + 'GTID_SUBTRACT' => 33, + 'INTERIORRINGN' => 33, + 'JSON_CONTAINS' => 33, + 'MBRINTERSECTS' => 33, + 'MLINEFROMTEXT' => 33, + 'MPOINTFROMWKB' => 33, + 'MPOLYFROMTEXT' => 33, + 'NUMGEOMETRIES' => 33, + 'POINTFROMTEXT' => 33, + 'ST_CONVEXHULL' => 33, + 'ST_DIFFERENCE' => 33, + 'ST_INTERSECTS' => 33, + 'ST_STARTPOINT' => 33, + 'TIMESTAMPDIFF' => 33, + 'WEIGHT_STRING' => 33, + 'IS_IPV4_COMPAT' => 33, + 'IS_IPV4_MAPPED' => 33, + 'LAST_INSERT_ID' => 33, + 'MPOINTFROMTEXT' => 33, + 'POLYGONFROMWKB' => 33, + 'ST_GEOMFROMWKB' => 33, + 'ST_LINEFROMWKB' => 33, + 'ST_POLYFROMWKB' => 33, + 'UNIX_TIMESTAMP' => 33, + 'GEOMCOLLFROMWKB' => 33, + 'MASTER_POS_WAIT' => 33, + 'POLYGONFROMTEXT' => 33, + 'ST_EXTERIORRING' => 33, + 'ST_GEOMETRYTYPE' => 33, + 'ST_GEOMFROMTEXT' => 33, + 'ST_INTERSECTION' => 33, + 'ST_LINEFROMTEXT' => 33, + 'ST_MAKEENVELOPE' => 33, + 'ST_MLINEFROMWKB' => 33, + 'ST_MPOLYFROMWKB' => 33, + 'ST_POINTFROMWKB' => 33, + 'ST_POLYFROMTEXT' => 33, + 'SUBSTRING_INDEX' => 33, + 'CHARACTER_LENGTH' => 33, + 'GEOMCOLLFROMTEXT' => 33, + 'GEOMETRYFROMTEXT' => 33, + 'JSON_MERGE_PATCH' => 33, + 'NUMINTERIORRINGS' => 33, + 'ST_INTERIORRINGN' => 33, + 'ST_MLINEFROMTEXT' => 33, + 'ST_MPOINTFROMWKB' => 33, + 'ST_MPOLYFROMTEXT' => 33, + 'ST_NUMGEOMETRIES' => 33, + 'ST_POINTFROMTEXT' => 33, + 'ST_SYMDIFFERENCE' => 33, + 'JSON_ARRAY_APPEND' => 33, + 'JSON_ARRAY_INSERT' => 33, + 'JSON_STORAGE_FREE' => 33, + 'JSON_STORAGE_SIZE' => 33, + 'LINESTRINGFROMWKB' => 33, + 'MULTIPOINTFROMWKB' => 33, + 'RELEASE_ALL_LOCKS' => 33, + 'ST_LATFROMGEOHASH' => 33, + 'ST_MPOINTFROMTEXT' => 33, + 'ST_POLYGONFROMWKB' => 33, + 'JSON_CONTAINS_PATH' => 33, + 'MULTIPOINTFROMTEXT' => 33, + 'ST_BUFFER_STRATEGY' => 33, + 'ST_DISTANCE_SPHERE' => 33, + 'ST_GEOMCOLLFROMTXT' => 33, + 'ST_GEOMCOLLFROMWKB' => 33, + 'ST_GEOMFROMGEOJSON' => 33, + 'ST_LONGFROMGEOHASH' => 33, + 'ST_POLYGONFROMTEXT' => 33, + 'JSON_MERGE_PRESERVE' => 33, + 'MULTIPOLYGONFROMWKB' => 33, + 'ST_GEOMCOLLFROMTEXT' => 33, + 'ST_GEOMETRYFROMTEXT' => 33, + 'ST_NUMINTERIORRINGS' => 33, + 'ST_POINTFROMGEOHASH' => 33, + 'UNCOMPRESSED_LENGTH' => 33, + 'MULTIPOLYGONFROMTEXT' => 33, + 'ST_LINESTRINGFROMWKB' => 33, + 'ST_MULTIPOINTFROMWKB' => 33, + 'ST_MULTIPOINTFROMTEXT' => 33, + 'MULTILINESTRINGFROMWKB' => 33, + 'ST_MULTIPOLYGONFROMWKB' => 33, + 'MULTILINESTRINGFROMTEXT' => 33, + 'ST_MULTIPOLYGONFROMTEXT' => 33, + 'GEOMETRYCOLLECTIONFROMWKB' => 33, + 'ST_MULTILINESTRINGFROMWKB' => 33, + 'GEOMETRYCOLLECTIONFROMTEXT' => 33, + 'ST_MULTILINESTRINGFROMTEXT' => 33, + 'VALIDATE_PASSWORD_STRENGTH' => 33, + 'WAIT_FOR_EXECUTED_GTID_SET' => 33, + 'ST_GEOMETRYCOLLECTIONFROMWKB' => 33, + 'ST_GEOMETRYCOLLECTIONFROMTEXT' => 33, + 'WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS' => 33, + + 'IF' => 35, + 'IN' => 35, + 'MOD' => 35, + 'LEFT' => 35, + 'MATCH' => 35, + 'RIGHT' => 35, + 'EXISTS' => 35, + 'INSERT' => 35, + 'REPEAT' => 35, + 'SCHEMA' => 35, + 'VALUES' => 35, + 'CONVERT' => 35, + 'DEFAULT' => 35, + 'REPLACE' => 35, + 'DATABASE' => 35, + 'UTC_DATE' => 35, + 'UTC_TIME' => 35, + 'LOCALTIME' => 35, + 'CURRENT_DATE' => 35, + 'CURRENT_TIME' => 35, + 'CURRENT_USER' => 35, + 'UTC_TIMESTAMP' => 35, + 'LOCALTIMESTAMP' => 35, + 'CURRENT_TIMESTAMP' => 35, + + 'NOT IN' => 39, + + 'DATE' => 41, + 'TIME' => 41, + 'YEAR' => 41, + 'POINT' => 41, + 'POLYGON' => 41, + 'TIMESTAMP' => 41, + 'LINESTRING' => 41, + 'MULTIPOINT' => 41, + 'MULTIPOLYGON' => 41, + 'MULTILINESTRING' => 41, + 'GEOMETRYCOLLECTION' => 41, + + 'CHAR' => 43, + 'BINARY' => 43, + 'INTERVAL' => 43, + ); + + /** + * All data type options. + * + * @var array> + */ + public static $data_type_options = array( + 'BINARY' => 1, + 'CHARACTER SET' => array( + 2, + 'var', + ), + 'CHARSET' => array( + 2, + 'var', + ), + 'COLLATE' => array( + 3, + 'var', + ), + 'UNSIGNED' => 4, + 'ZEROFILL' => 5, + ); + + /** + * All field options. + * + * @var array>> + */ + public static $field_options = array( + + /* + * Tells the `OptionsArray` to not sort the options. + * See the note below. + */ + '_UNSORTED' => true, + + 'NOT NULL' => 1, + 'NULL' => 1, + 'DEFAULT' => array( + 2, + 'expr', + array( 'breakOnAlias' => true ), + ), + + // Following are not according to grammar, but MySQL happily accepts these at any location. + 'CHARSET' => array( + 2, + 'var', + ), + 'COLLATE' => array( + 3, + 'var', + ), + 'AUTO_INCREMENT' => 3, + 'PRIMARY' => 4, + 'PRIMARY KEY' => 4, + 'UNIQUE' => 4, + 'UNIQUE KEY' => 4, + 'COMMENT' => array( + 5, + 'var', + ), + 'COLUMN_FORMAT' => array( + 6, + 'var', + ), + 'ON UPDATE' => array( + 7, + 'expr', + ), + + // Generated columns options. + 'GENERATED ALWAYS' => 8, + 'AS' => array( + 9, + 'expr', + array( 'parenthesesDelimited' => true ), + ), + 'VIRTUAL' => 10, + 'PERSISTENT' => 11, + 'STORED' => 11, + 'CHECK' => array( + 12, + 'expr', + array( 'parenthesesDelimited' => true ), + ), + 'INVISIBLE' => 13, + 'ENFORCED' => 14, + 'NOT' => 15, + 'COMPRESSED' => 16, + + /* + * Common entries. + * + * NOTE: Some of the common options are not in the same order which + * causes troubles when checking if the options are in the right order. + * I should find a way to define multiple sets of options and make the + * parser select the right set. + * + * 'UNIQUE' => 4, + * 'UNIQUE KEY' => 4, + * 'COMMENT' => [5, 'var'], + * 'NOT NULL' => 1, + * 'NULL' => 1, + * 'PRIMARY' => 4, + * 'PRIMARY KEY' => 4, + */ + ); + + /** + * Quotes mode. + * + * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_ansi_quotes + * @link https://mariadb.com/kb/en/sql-mode/#ansi_quotes + */ + const SQL_MODE_ANSI_QUOTES = 2; + + /** + * The array of tokens. + * + * @var stdClass[] + */ + public $tokens = array(); + + /** + * The count of tokens. + * + * @var int + */ + public $tokens_count = 0; + + /** + * The index of the next token to be returned. + * + * @var int + */ + public $tokens_index = 0; + + /** + * The object constructor. + * + * @param string $str The query to be lexed. + * @param string $delimiter The delimiter to be used. + */ + public function __construct( $str, $delimiter = null ) { + $this->str = $str; + // `strlen` is used instead of `mb_strlen` because the lexer needs to parse each byte of the input. + $this->string_length = strlen( $str ); + + // Setting the delimiter. + $this->set_delimiter( ! empty( $delimiter ) ? $delimiter : static::$default_delimiter ); + + $this->lex(); + } + + /** + * Sets the delimiter. + * + * @param string $delimiter The new delimiter. + * + * @return void + */ + public function set_delimiter( $delimiter ) { + $this->delimiter = $delimiter; + $this->delimiter_length = strlen( $delimiter ); + } + + /** + * Parses the string and extracts lexemes. + * + * @return void + */ + public function lex() { + /* + * TODO: Sometimes, static::parse* functions make unnecessary calls to + * is* functions. For a better performance, some rules can be deduced + * from context. + * For example, in `parse_bool` there is no need to compare the token + * every time with `true` and `false`. The first step would be to + * compare with 'true' only and just after that add another letter from + * context and compare again with `false`. + * Another example is `parse_comment`. + */ + + /** + * Last processed token. + * + * @var WP_SQLite_Token + */ + $last_token = null; + + for ( $this->last = 0, $last_idx = 0; $this->last < $this->string_length; $last_idx = ++$this->last ) { + /** + * The new token. + * + * @var WP_SQLite_Token + */ + $token = null; + + foreach ( static::$parser_methods as $method ) { + $token = $this->$method(); + + if ( $token ) { + break; + } + } + + if ( null === $token ) { + $token = new WP_SQLite_Token( $this->str[ $this->last ] ); + $this->error( 'Unexpected character.', $this->str[ $this->last ], $this->last ); + } elseif ( + null !== $last_token + && WP_SQLite_Token::TYPE_SYMBOL === $token->type + && $token->flags & WP_SQLite_Token::FLAG_SYMBOL_VARIABLE + && ( + WP_SQLite_Token::TYPE_STRING === $last_token->type + || ( + WP_SQLite_Token::TYPE_SYMBOL === $last_token->type + && $last_token->flags & WP_SQLite_Token::FLAG_SYMBOL_BACKTICK + ) + ) + ) { + // Handles ```... FROM 'user'@'%' ...```. + $last_token->token .= $token->token; + $last_token->type = WP_SQLite_Token::TYPE_SYMBOL; + $last_token->flags = WP_SQLite_Token::FLAG_SYMBOL_USER; + $last_token->value .= '@' . $token->value; + continue; + } elseif ( + null !== $last_token + && WP_SQLite_Token::TYPE_KEYWORD === $token->type + && WP_SQLite_Token::TYPE_OPERATOR === $last_token->type + && '.' === $last_token->value + ) { + // Handles ```... tbl.FROM ...```. In this case, FROM is not a reserved word. + $token->type = WP_SQLite_Token::TYPE_NONE; + $token->flags = 0; + $token->value = $token->token; + } + + $token->position = $last_idx; + + $this->tokens[ $this->tokens_count++ ] = $token; + + // Handling delimiters. + if ( WP_SQLite_Token::TYPE_NONE === $token->type && 'DELIMITER' === $token->value ) { + if ( $this->last + 1 >= $this->string_length ) { + $this->error( 'Expected whitespace(s) before delimiter.', '', $this->last + 1 ); + continue; + } + + /* + * Skipping last R (from `delimiteR`) and whitespaces between + * the keyword `DELIMITER` and the actual delimiter. + */ + $pos = ++$this->last; + $token = $this->parse_whitespace(); + + if ( null !== $token ) { + $token->position = $pos; + $this->tokens[ $this->tokens_count++ ] = $token; + } + + // Preparing the token that holds the new delimiter. + if ( $this->last + 1 >= $this->string_length ) { + $this->error( 'Expected delimiter.', '', $this->last + 1 ); + continue; + } + + $pos = $this->last + 1; + + // Parsing the delimiter. + $this->delimiter = null; + $delimiter_length = 0; + while ( + ++$this->last < $this->string_length + && ! static::is_whitespace( $this->str[ $this->last ] ) + && $delimiter_length < 15 + ) { + $this->delimiter .= $this->str[ $this->last ]; + ++$delimiter_length; + } + + if ( empty( $this->delimiter ) ) { + $this->error( 'Expected delimiter.', '', $this->last ); + $this->delimiter = ';'; + } + + --$this->last; + + // Saving the delimiter and its token. + $this->delimiter_length = strlen( $this->delimiter ); + $token = new WP_SQLite_Token( $this->delimiter, WP_SQLite_Token::TYPE_DELIMITER ); + $token->position = $pos; + $this->tokens[ $this->tokens_count++ ] = $token; + } + + $last_token = $token; + } + + // Adding a final delimiter to mark the ending. + $this->tokens[ $this->tokens_count++ ] = new WP_SQLite_Token( null, WP_SQLite_Token::TYPE_DELIMITER ); + + $this->solve_ambiguity_on_star_operator(); + $this->solve_ambiguity_on_function_keywords(); + } + + /** + * Resolves the ambiguity when dealing with the "*" operator. + * + * In SQL statements, the "*" operator can be an arithmetic operator (like in 2*3) or an SQL wildcard (like in + * SELECT a.* FROM ...). To solve this ambiguity, the solution is to find the next token, excluding whitespaces and + * comments, right after the "*" position. The "*" is for sure an SQL wildcard if the next token found is any of: + * - "FROM" (the FROM keyword like in "SELECT * FROM..."); + * - "USING" (the USING keyword like in "DELETE table_name.* USING..."); + * - "," (a comma separator like in "SELECT *, field FROM..."); + * - ")" (a closing parenthesis like in "COUNT(*)"). + * This methods will change the flag of the "*" tokens when any of those condition above is true. Otherwise, the + * default flag (arithmetic) will be kept. + * + * @return void + */ + private function solve_ambiguity_on_star_operator() { + $i_bak = $this->tokens_index; + while ( true ) { + $star_token = $this->tokens_get_next_of_type_and_value( WP_SQLite_Token::TYPE_OPERATOR, '*' ); + if ( null === $star_token ) { + break; + } + // tokens_get_next() already gets rid of whitespaces and comments. + $next = $this->tokens_get_next(); + + if ( null === $next ) { + continue; + } + + if ( + ( WP_SQLite_Token::TYPE_KEYWORD !== $next->type || ! in_array( $next->value, array( 'FROM', 'USING' ), true ) ) + && ( WP_SQLite_Token::TYPE_OPERATOR !== $next->type || ! in_array( $next->value, array( ',', ')' ), true ) ) + ) { + continue; + } + + $star_token->flags = WP_SQLite_Token::FLAG_OPERATOR_SQL; + } + + $this->tokens_index = $i_bak; + } + + /** + * Resolves the ambiguity when dealing with the functions keywords. + * + * In SQL statements, the function keywords might be used as table names or columns names. + * To solve this ambiguity, the solution is to find the next token, excluding whitespaces and + * comments, right after the function keyword position. The function keyword is for sure used + * as column name or table name if the next token found is any of: + * + * - "FROM" (the FROM keyword like in "SELECT Country x, AverageSalary avg FROM..."); + * - "WHERE" (the WHERE keyword like in "DELETE FROM emp x WHERE x.salary = 20"); + * - "SET" (the SET keyword like in "UPDATE Country x, City y set x.Name=x.Name"); + * - "," (a comma separator like 'x,' in "UPDATE Country x, City y set x.Name=x.Name"); + * - "." (a dot separator like in "x.asset_id FROM (SELECT evt.asset_id FROM evt)". + * - "NULL" (when used as a table alias like in "avg.col FROM (SELECT ev.col FROM ev) avg"). + * + * This method will change the flag of the function keyword tokens when any of those + * condition above is true. Otherwise, the + * default flag (function keyword) will be kept. + * + * @return void + */ + private function solve_ambiguity_on_function_keywords() { + $i_bak = $this->tokens_index; + $keyword_function = WP_SQLite_Token::TYPE_KEYWORD | WP_SQLite_Token::FLAG_KEYWORD_FUNCTION; + while ( true ) { + $keyword_token = $this->tokens_get_next_of_type_and_flag( WP_SQLite_Token::TYPE_KEYWORD, $keyword_function ); + if ( null === $keyword_token ) { + break; + } + $next = $this->tokens_get_next(); + if ( + ( WP_SQLite_Token::TYPE_KEYWORD !== $next->type + || ! in_array( $next->value, $this->keyword_name_indicators, true ) + ) + && ( WP_SQLite_Token::TYPE_OPERATOR !== $next->type + || ! in_array( $next->value, $this->operator_name_indicators, true ) + ) + && ( null !== $next->value ) + ) { + continue; + } + + $keyword_token->type = WP_SQLite_Token::TYPE_NONE; + $keyword_token->flags = WP_SQLite_Token::TYPE_NONE; + $keyword_token->keyword = $keyword_token->value; + } + + $this->tokens_index = $i_bak; + } + + /** + * Creates a new error log. + * + * @param string $msg The error message. + * @param string $str The character that produced the error. + * @param int $pos The position of the character. + * @param int $code The code of the error. + * + * @throws Exception The error log. + * @return void + */ + public function error( $msg, $str = '', $pos = 0, $code = 0 ) { + throw new Exception( + print_r( + array( + 'query' => $this->str, + 'message' => $msg, + 'str' => $str, + 'position' => $pos, + 'code' => $code, + ), + true + ) + ); + } + + /** + * Parses a keyword. + * + * @return WP_SQLite_Token|null + */ + public function parse_keyword() { + $token = ''; + + /** + * Value to be returned. + * + * @var WP_SQLite_Token + */ + $ret = null; + + // The value of `$this->last` where `$token` ends in `$this->str`. + $i_end = $this->last; + + // Whether last parsed character is a whitespace. + $last_space = false; + + for ( $j = 1; $j < static::KEYWORD_MAX_LENGTH && $this->last < $this->string_length; ++$j, ++$this->last ) { + $last_space = false; + // Composed keywords shouldn't have more than one whitespace between keywords. + if ( static::is_whitespace( $this->str[ $this->last ] ) ) { + if ( $last_space ) { + --$j; // The size of the keyword didn't increase. + continue; + } + + $last_space = true; + } + + $token .= $this->str[ $this->last ]; + $flags = static::is_keyword( $token ); + + if ( ( $this->last + 1 !== $this->string_length && ! static::is_separator( $this->str[ $this->last + 1 ] ) ) || ! $flags ) { + continue; + } + + $ret = new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_KEYWORD, $flags ); + $i_end = $this->last; + + /* + * We don't break so we find longest keyword. + * For example, `OR` and `ORDER` have a common prefix `OR`. + * If we stopped at `OR`, the parsing would be invalid. + */ + } + + $this->last = $i_end; + + return $ret; + } + + /** + * Parses a label. + * + * @return WP_SQLite_Token|null + */ + public function parse_label() { + $token = ''; + + /** + * Value to be returned. + * + * @var WP_SQLite_Token + */ + $ret = null; + + // The value of `$this->last` where `$token` ends in `$this->str`. + $i_end = $this->last; + for ( $j = 1; $j < static::LABEL_MAX_LENGTH && $this->last < $this->string_length; ++$j, ++$this->last ) { + if ( ':' === $this->str[ $this->last ] && $j > 1 ) { + // End of label. + $token .= $this->str[ $this->last ]; + $ret = new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_LABEL ); + $i_end = $this->last; + break; + } + + if ( static::is_whitespace( $this->str[ $this->last ] ) && $j > 1 ) { + /* + * Whitespace between label and `:`. + * The size of the keyword didn't increase. + */ + --$j; + } elseif ( static::is_separator( $this->str[ $this->last ] ) ) { + // Any other separator. + break; + } + + $token .= $this->str[ $this->last ]; + } + + $this->last = $i_end; + + return $ret; + } + + /** + * Parses an operator. + * + * @return WP_SQLite_Token|null + */ + public function parse_operator() { + $token = ''; + + /** + * Value to be returned. + * + * @var WP_SQLite_Token + */ + $ret = null; + + // The value of `$this->last` where `$token` ends in `$this->str`. + $i_end = $this->last; + + for ( $j = 1; $j < static::OPERATOR_MAX_LENGTH && $this->last < $this->string_length; ++$j, ++$this->last ) { + $token .= $this->str[ $this->last ]; + $flags = static::is_operator( $token ); + + if ( ! $flags ) { + continue; + } + + $ret = new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_OPERATOR, $flags ); + $i_end = $this->last; + } + + $this->last = $i_end; + + return $ret; + } + + /** + * Parses a whitespace. + * + * @return WP_SQLite_Token|null + */ + public function parse_whitespace() { + $token = $this->str[ $this->last ]; + + if ( ! static::is_whitespace( $token ) ) { + return null; + } + + while ( ++$this->last < $this->string_length && static::is_whitespace( $this->str[ $this->last ] ) ) { + $token .= $this->str[ $this->last ]; + } + + --$this->last; + + return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_WHITESPACE ); + } + + /** + * Parses a comment. + * + * @return WP_SQLite_Token|null + */ + public function parse_comment() { + $i_bak = $this->last; + $token = $this->str[ $this->last ]; + + // Bash style comments (#comment\n). + if ( static::is_comment( $token ) ) { + while ( ++$this->last < $this->string_length && "\n" !== $this->str[ $this->last ] ) { + $token .= $this->str[ $this->last ]; + } + + // Include trailing \n as whitespace token. + if ( $this->last < $this->string_length ) { + --$this->last; + } + + return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_COMMENT, WP_SQLite_Token::FLAG_COMMENT_BASH ); + } + + // C style comments (/*comment*\/). + if ( ++$this->last < $this->string_length ) { + $token .= $this->str[ $this->last ]; + if ( static::is_comment( $token ) ) { + // There might be a conflict with "*" operator here, when string is "*/*". + // This can occurs in the following statements: + // - "SELECT */* comment */ FROM ..." + // - "SELECT 2*/* comment */3 AS `six`;". + $next = $this->last + 1; + if ( ( $next < $this->string_length ) && '*' === $this->str[ $next ] ) { + // Conflict in "*/*": first "*" was not for ending a comment. + // Stop here and let other parsing method define the true behavior of that first star. + $this->last = $i_bak; + + return null; + } + + $flags = WP_SQLite_Token::FLAG_COMMENT_C; + + // This comment already ended. It may be a part of a previous MySQL specific command. + if ( '*/' === $token ) { + return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_COMMENT, $flags ); + } + + // Checking if this is a MySQL-specific command. + if ( $this->last + 1 < $this->string_length && '!' === $this->str[ $this->last + 1 ] ) { + $flags |= WP_SQLite_Token::FLAG_COMMENT_MYSQL_CMD; + $token .= $this->str[ ++$this->last ]; + + while ( + ++$this->last < $this->string_length + && $this->str[ $this->last ] >= '0' + && $this->str[ $this->last ] <= '9' + ) { + $token .= $this->str[ $this->last ]; + } + + --$this->last; + + // We split this comment and parse only its beginning here. + return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_COMMENT, $flags ); + } + + // Parsing the comment. + while ( + ++$this->last < $this->string_length + && ( '*' !== $this->str[ $this->last - 1 ] || '/' !== $this->str[ $this->last ] ) + ) { + $token .= $this->str[ $this->last ]; + } + + // Adding the ending. + if ( $this->last < $this->string_length ) { + $token .= $this->str[ $this->last ]; + } + + return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_COMMENT, $flags ); + } + } + + // SQL style comments (-- comment\n). + if ( ++$this->last < $this->string_length ) { + $token .= $this->str[ $this->last ]; + $end = false; + } else { + --$this->last; + $end = true; + } + + if ( static::is_comment( $token, $end ) ) { + // Checking if this comment did not end already (```--\n```). + if ( "\n" !== $this->str[ $this->last ] ) { + while ( ++$this->last < $this->string_length && "\n" !== $this->str[ $this->last ] ) { + $token .= $this->str[ $this->last ]; + } + } + + // Include trailing \n as whitespace token. + if ( $this->last < $this->string_length ) { + --$this->last; + } + + return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_COMMENT, WP_SQLite_Token::FLAG_COMMENT_SQL ); + } + + $this->last = $i_bak; + + return null; + } + + /** + * Parses a boolean. + * + * @return WP_SQLite_Token|null + */ + public function parse_bool() { + if ( $this->last + 3 >= $this->string_length ) { + // At least `min(strlen('TRUE'), strlen('FALSE'))` characters are required. + return null; + } + + $i_bak = $this->last; + $token = $this->str[ $this->last ] . $this->str[ ++$this->last ] + . $this->str[ ++$this->last ] . $this->str[ ++$this->last ]; // _TRUE_ or _FALS_e. + + if ( static::is_bool( $token ) ) { + return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_BOOL ); + } + + if ( ++$this->last < $this->string_length ) { + $token .= $this->str[ $this->last ]; // fals_E_. + if ( static::is_bool( $token ) ) { + return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_BOOL, 1 ); + } + } + + $this->last = $i_bak; + + return null; + } + + /** + * Parses a number. + * + * @return WP_SQLite_Token|null + */ + public function parse_number() { + /* + * A rudimentary state machine is being used to parse numbers due to + * the various forms of their notation. + * + * Below are the states of the machines and the conditions to change + * the state. + * + * 1 --------------------[ + or - ]-------------------> 1 + * 1 -------------------[ 0x or 0X ]------------------> 2 + * 1 --------------------[ 0 to 9 ]-------------------> 3 + * 1 -----------------------[ . ]---------------------> 4 + * 1 -----------------------[ b ]---------------------> 7 + * + * 2 --------------------[ 0 to F ]-------------------> 2 + * + * 3 --------------------[ 0 to 9 ]-------------------> 3 + * 3 -----------------------[ . ]---------------------> 4 + * 3 --------------------[ e or E ]-------------------> 5 + * + * 4 --------------------[ 0 to 9 ]-------------------> 4 + * 4 --------------------[ e or E ]-------------------> 5 + * + * 5 ---------------[ + or - or 0 to 9 ]--------------> 6 + * + * 7 -----------------------[ ' ]---------------------> 8 + * + * 8 --------------------[ 0 or 1 ]-------------------> 8 + * 8 -----------------------[ ' ]---------------------> 9 + * + * State 1 may be reached by negative numbers. + * State 2 is reached only by hex numbers. + * State 4 is reached only by float numbers. + * State 5 is reached only by numbers in approximate form. + * State 7 is reached only by numbers in bit representation. + * + * Valid final states are: 2, 3, 4 and 6. Any parsing that finished in a + * state other than these is invalid. + * Also, negative states are invalid states. + */ + $i_bak = $this->last; + $token = ''; + $flags = 0; + $state = 1; + for ( ; $this->last < $this->string_length; ++$this->last ) { + if ( 1 === $state ) { + if ( '-' === $this->str[ $this->last ] ) { + $flags |= WP_SQLite_Token::FLAG_NUMBER_NEGATIVE; + } elseif ( + $this->last + 1 < $this->string_length + && '0' === $this->str[ $this->last ] + && ( 'x' === $this->str[ $this->last + 1 ] || 'X' === $this->str[ $this->last + 1 ] ) + ) { + $token .= $this->str[ $this->last++ ]; + $state = 2; + } elseif ( $this->str[ $this->last ] >= '0' && $this->str[ $this->last ] <= '9' ) { + $state = 3; + } elseif ( '.' === $this->str[ $this->last ] ) { + $state = 4; + } elseif ( 'b' === $this->str[ $this->last ] ) { + $state = 7; + } elseif ( '+' !== $this->str[ $this->last ] ) { + // `+` is a valid character in a number. + break; + } + } elseif ( 2 === $state ) { + $flags |= WP_SQLite_Token::FLAG_NUMBER_HEX; + if ( + ! ( + ( $this->str[ $this->last ] >= '0' && $this->str[ $this->last ] <= '9' ) + || ( $this->str[ $this->last ] >= 'A' && $this->str[ $this->last ] <= 'F' ) + || ( $this->str[ $this->last ] >= 'a' && $this->str[ $this->last ] <= 'f' ) + ) + ) { + break; + } + } elseif ( 3 === $state ) { + if ( '.' === $this->str[ $this->last ] ) { + $state = 4; + } elseif ( 'e' === $this->str[ $this->last ] || 'E' === $this->str[ $this->last ] ) { + $state = 5; + } elseif ( + ( $this->str[ $this->last ] >= 'a' && $this->str[ $this->last ] <= 'z' ) + || ( $this->str[ $this->last ] >= 'A' && $this->str[ $this->last ] <= 'Z' ) + ) { + // A number can't be directly followed by a letter. + $state = -$state; + } elseif ( $this->str[ $this->last ] < '0' || $this->str[ $this->last ] > '9' ) { + // Just digits and `.`, `e` and `E` are valid characters. + break; + } + } elseif ( 4 === $state ) { + $flags |= WP_SQLite_Token::FLAG_NUMBER_FLOAT; + if ( 'e' === $this->str[ $this->last ] || 'E' === $this->str[ $this->last ] ) { + $state = 5; + } elseif ( + ( $this->str[ $this->last ] >= 'a' && $this->str[ $this->last ] <= 'z' ) + || ( $this->str[ $this->last ] >= 'A' && $this->str[ $this->last ] <= 'Z' ) + ) { + // A number can't be directly followed by a letter. + $state = -$state; + } elseif ( $this->str[ $this->last ] < '0' || $this->str[ $this->last ] > '9' ) { + // Just digits, `e` and `E` are valid characters. + break; + } + } elseif ( 5 === $state ) { + $flags |= WP_SQLite_Token::FLAG_NUMBER_APPROXIMATE; + if ( + '+' === $this->str[ $this->last ] || '-' === $this->str[ $this->last ] + || ( $this->str[ $this->last ] >= '0' && $this->str[ $this->last ] <= '9' ) + ) { + $state = 6; + } elseif ( + ( $this->str[ $this->last ] >= 'a' && $this->str[ $this->last ] <= 'z' ) + || ( $this->str[ $this->last ] >= 'A' && $this->str[ $this->last ] <= 'Z' ) + ) { + // A number can't be directly followed by a letter. + $state = -$state; + } else { + break; + } + } elseif ( 6 === $state ) { + if ( $this->str[ $this->last ] < '0' || $this->str[ $this->last ] > '9' ) { + // Just digits are valid characters. + break; + } + } elseif ( 7 === $state ) { + $flags |= WP_SQLite_Token::FLAG_NUMBER_BINARY; + if ( '\'' !== $this->str[ $this->last ] ) { + break; + } + + $state = 8; + } elseif ( 8 === $state ) { + if ( '\'' === $this->str[ $this->last ] ) { + $state = 9; + } elseif ( '0' !== $this->str[ $this->last ] && '1' !== $this->str[ $this->last ] ) { + break; + } + } elseif ( 9 === $state ) { + break; + } + + $token .= $this->str[ $this->last ]; + } + + if ( 2 === $state || 3 === $state || ( '.' !== $token && 4 === $state ) || 6 === $state || 9 === $state ) { + --$this->last; + + return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_NUMBER, $flags ); + } + + $this->last = $i_bak; + + return null; + } + + /** + * Parses a string. + * + * @param string $quote Additional starting symbol. + * + * @return WP_SQLite_Token|null + */ + public function parse_string( $quote = '' ) { + $token = $this->str[ $this->last ]; + $flags = static::is_string( $token ); + + if ( ! $flags && $token !== $quote ) { + return null; + } + + $quote = $token; + + while ( ++$this->last < $this->string_length ) { + if ( + $this->last + 1 < $this->string_length + && ( + ( $this->str[ $this->last ] === $quote && $this->str[ $this->last + 1 ] === $quote ) + || ( '\\' === $this->str[ $this->last ] && '`' !== $quote ) + ) + ) { + $token .= $this->str[ $this->last ] . $this->str[ ++$this->last ]; + } else { + if ( $this->str[ $this->last ] === $quote ) { + break; + } + + $token .= $this->str[ $this->last ]; + } + } + + if ( $this->last >= $this->string_length || $this->str[ $this->last ] !== $quote ) { + $this->error( + sprintf( + 'Ending quote %1$s was expected.', + $quote + ), + '', + $this->last + ); + } else { + $token .= $this->str[ $this->last ]; + } + + return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_STRING, $flags ); + } + + /** + * Parses a symbol. + * + * @return WP_SQLite_Token|null + */ + public function parse_symbol() { + $token = $this->str[ $this->last ]; + $flags = static::is_symbol( $token ); + + if ( ! $flags ) { + return null; + } + + if ( $flags & WP_SQLite_Token::FLAG_SYMBOL_VARIABLE ) { + if ( $this->last + 1 < $this->string_length && '@' === $this->str[ ++$this->last ] ) { + // This is a system variable (e.g. `@@hostname`). + $token .= $this->str[ $this->last++ ]; + $flags |= WP_SQLite_Token::FLAG_SYMBOL_SYSTEM; + } + } elseif ( $flags & WP_SQLite_Token::FLAG_SYMBOL_PARAMETER ) { + if ( '?' !== $token && $this->last + 1 < $this->string_length ) { + ++$this->last; + } + } else { + $token = ''; + } + + $str = null; + + if ( $this->last < $this->string_length ) { + $str = $this->parse_string( '`' ); + + if ( null === $str ) { + $str = $this->parse_unknown(); + + if ( null === $str ) { + $this->error( 'Variable name was expected.', $this->str[ $this->last ], $this->last ); + } + } + } + + if ( null !== $str ) { + $token .= $str->token; + } + + return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_SYMBOL, $flags ); + } + + /** + * Parses unknown parts of the query. + * + * @return WP_SQLite_Token|null + */ + public function parse_unknown() { + $token = $this->str[ $this->last ]; + if ( static::is_separator( $token ) ) { + return null; + } + + while ( ++$this->last < $this->string_length && ! static::is_separator( $this->str[ $this->last ] ) ) { + $token .= $this->str[ $this->last ]; + + // Test if end of token equals the current delimiter. If so, remove it from the token. + if ( str_ends_with( $token, $this->delimiter ) ) { + $token = substr( $token, 0, -$this->delimiter_length ); + $this->last -= $this->delimiter_length - 1; + break; + } + } + + --$this->last; + + return new WP_SQLite_Token( $token ); + } + + /** + * Parses the delimiter of the query. + * + * @return WP_SQLite_Token|null + */ + public function parse_delimiter() { + $index = 0; + + while ( $index < $this->delimiter_length && $this->last + $index < $this->string_length ) { + if ( $this->delimiter[ $index ] !== $this->str[ $this->last + $index ] ) { + return null; + } + + ++$index; + } + + $this->last += $this->delimiter_length - 1; + + return new WP_SQLite_Token( $this->delimiter, WP_SQLite_Token::TYPE_DELIMITER ); + } + + /** + * Checks if the given string is a keyword. + * + * @param string $str String to be checked. + * @param bool $is_reserved Checks if the keyword is reserved. + * + * @return int|null + */ + public static function is_keyword( $str, $is_reserved = false ) { + $str = strtoupper( $str ); + + if ( isset( static::$keywords[ $str ] ) ) { + if ( $is_reserved && ! ( static::$keywords[ $str ] & WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) ) { + return null; + } + + return static::$keywords[ $str ]; + } + + return null; + } + + /** + * Checks if the given string is an operator. + * + * @param string $str String to be checked. + * + * @return int|null The appropriate flag for the operator. + */ + public static function is_operator( $str ) { + if ( ! isset( static::$operators[ $str ] ) ) { + return null; + } + + return static::$operators[ $str ]; + } + + /** + * Checks if the given character is a whitespace. + * + * @param string $str String to be checked. + * + * @return bool + */ + public static function is_whitespace( $str ) { + return ( ' ' === $str ) || ( "\r" === $str ) || ( "\n" === $str ) || ( "\t" === $str ); + } + + /** + * Checks if the given string is the beginning of a whitespace. + * + * @param string $str String to be checked. + * @param mixed $end Whether this is the end of the string. + * + * @return int|null The appropriate flag for the comment type. + */ + public static function is_comment( $str, $end = false ) { + $string_length = strlen( $str ); + if ( 0 === $string_length ) { + return null; + } + + // If comment is Bash style (#). + if ( '#' === $str[0] ) { + return WP_SQLite_Token::FLAG_COMMENT_BASH; + } + + // If comment is opening C style (/*), warning, it could be a MySQL command (/*!). + if ( ( $string_length > 1 ) && ( '/' === $str[0] ) && ( '*' === $str[1] ) ) { + return ( $string_length > 2 ) && ( '!' === $str[2] ) ? + WP_SQLite_Token::FLAG_COMMENT_MYSQL_CMD : WP_SQLite_Token::FLAG_COMMENT_C; + } + + // If comment is closing C style (*/), warning, it could conflicts with wildcard and a real opening C style. + // It would looks like the following valid SQL statement: "SELECT */* comment */ FROM...". + if ( ( $string_length > 1 ) && ( '*' === $str[0] ) && ( '/' === $str[1] ) ) { + return WP_SQLite_Token::FLAG_COMMENT_C; + } + + // If comment is SQL style (--\s?). + if ( ( $string_length > 2 ) && ( '-' === $str[0] ) && ( '-' === $str[1] ) && static::is_whitespace( $str[2] ) ) { + return WP_SQLite_Token::FLAG_COMMENT_SQL; + } + + if ( ( 2 === $string_length ) && $end && ( '-' === $str[0] ) && ( '-' === $str[1] ) ) { + return WP_SQLite_Token::FLAG_COMMENT_SQL; + } + + return null; + } + + /** + * Checks if the given string is a boolean value. + * This actually checks only for `TRUE` and `FALSE` because `1` or `0` are + * numbers and are parsed by specific methods. + * + * @param string $str String to be checked. + * + * @return bool + */ + public static function is_bool( $str ) { + $str = strtoupper( $str ); + + return ( 'TRUE' === $str ) || ( 'FALSE' === $str ); + } + + /** + * Checks if the given character can be a part of a number. + * + * @param string $str String to be checked. + * + * @return bool + */ + public static function is_number( $str ) { + return ( $str >= '0' ) && ( $str <= '9' ) || ( '.' === $str ) + || ( '-' === $str ) || ( '+' === $str ) || ( 'e' === $str ) || ( 'E' === $str ); + } + + /** + * Checks if the given character is the beginning of a symbol. A symbol + * can be either a variable or a field name. + * + * @param string $str String to be checked. + * + * @return int|null The appropriate flag for the symbol type. + */ + public static function is_symbol( $str ) { + if ( 0 === strlen( $str ) ) { + return null; + } + + if ( '@' === $str[0] ) { + return WP_SQLite_Token::FLAG_SYMBOL_VARIABLE; + } + + if ( '`' === $str[0] ) { + return WP_SQLite_Token::FLAG_SYMBOL_BACKTICK; + } + + if ( ':' === $str[0] || '?' === $str[0] ) { + return WP_SQLite_Token::FLAG_SYMBOL_PARAMETER; + } + + return null; + } + + /** + * Checks if the given character is the beginning of a string. + * + * @param string $str String to be checked. + * + * @return int|null The appropriate flag for the string type. + */ + public static function is_string( $str ) { + if ( strlen( $str ) === 0 ) { + return null; + } + + if ( '\'' === $str[0] ) { + return WP_SQLite_Token::FLAG_STRING_SINGLE_QUOTES; + } + + if ( '"' === $str[0] ) { + return WP_SQLite_Token::FLAG_STRING_DOUBLE_QUOTES; + } + + return null; + } + + /** + * Checks if the given character can be a separator for two lexeme. + * + * @param string $str String to be checked. + * + * @return bool + */ + public static function is_separator( $str ) { + /* + * NOTES: Only non alphanumeric ASCII characters may be separators. + * `~` is the last printable ASCII character. + */ + return ( $str <= '~' ) + && ( '_' !== $str ) + && ( '$' !== $str ) + && ( ( $str < '0' ) || ( $str > '9' ) ) + && ( ( $str < 'a' ) || ( $str > 'z' ) ) + && ( ( $str < 'A' ) || ( $str > 'Z' ) ); + } + + /** + * Constructor. + * + * @param stdClass[] $tokens The initial array of tokens. + * @param int $count The count of tokens in the initial array. + */ + public function tokens( array $tokens = array(), $count = -1 ) { + if ( empty( $tokens ) ) { + return; + } + + $this->tokens = $tokens; + $this->tokens_count = -1 === $count ? count( $tokens ) : $count; + } + + /** + * Gets the next token. + * + * @param int $type The type of the token. + * @param int $flag The flag of the token. + */ + public function tokens_get_next_of_type_and_flag( $type, $flag ) { + for ( ; $this->tokens_index < $this->tokens_count; ++$this->tokens_index ) { + if ( ( $this->tokens[ $this->tokens_index ]->type === $type ) && ( $this->tokens[ $this->tokens_index ]->flags === $flag ) ) { + return $this->tokens[ $this->tokens_index++ ]; + } + } + + return null; + } + + /** + * Gets the next token. + * + * @param int $type The type of the token. + * @param string $value The value of the token. + * + * @return stdClass|null + */ + public function tokens_get_next_of_type_and_value( $type, $value ) { + for ( ; $this->tokens_index < $this->tokens_count; ++$this->tokens_index ) { + if ( ( $this->tokens[ $this->tokens_index ]->type === $type ) && ( $this->tokens[ $this->tokens_index ]->value === $value ) ) { + return $this->tokens[ $this->tokens_index++ ]; + } + } + + return null; + } + + /** + * Gets the next token. Skips any irrelevant token (whitespaces and + * comments). + * + * @return stdClass|null + */ + public function tokens_get_next() { + for ( ; $this->tokens_index < $this->tokens_count; ++$this->tokens_index ) { + if ( + ( WP_SQLite_Token::TYPE_WHITESPACE !== $this->tokens[ $this->tokens_index ]->type ) + && ( WP_SQLite_Token::TYPE_COMMENT !== $this->tokens[ $this->tokens_index ]->type ) + ) { + return $this->tokens[ $this->tokens_index++ ]; + } + } + + return null; + } +} diff --git a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-pdo-user-defined-functions.php b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php similarity index 64% rename from modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-pdo-user-defined-functions.php rename to modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php index d1f4421c14..fbe7bbb62d 100644 --- a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-pdo-user-defined-functions.php +++ b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php @@ -14,13 +14,13 @@ * Usage: * * - * new Perflab_SQLite_PDO_User_Defined_Functions(ref_to_pdo_obj); + * new WP_SQLite_PDO_User_Defined_Functions(ref_to_pdo_obj); * * * This automatically enables ref_to_pdo_obj to replace the function in the SQL statement * to the ones defined here. */ -class Perflab_SQLite_PDO_User_Defined_Functions { +class WP_SQLite_PDO_User_Defined_Functions { /** * The class constructor @@ -47,31 +47,27 @@ public function __construct( $pdo ) { */ private $functions = array( 'month' => 'month', + 'monthnum' => 'month', 'year' => 'year', 'day' => 'day', + 'hour' => 'hour', + 'minute' => 'minute', + 'second' => 'second', + 'week' => 'week', + 'weekday' => 'weekday', + 'dayofweek' => 'dayofweek', + 'dayofmonth' => 'dayofmonth', 'unix_timestamp' => 'unix_timestamp', 'now' => 'now', - 'char_length' => 'char_length', 'md5' => 'md5', 'curdate' => 'curdate', 'rand' => 'rand', - 'substring' => 'substring', - 'dayofmonth' => 'day', - 'second' => 'second', - 'minute' => 'minute', - 'hour' => 'hour', - 'date_format' => 'dateformat', 'from_unixtime' => 'from_unixtime', - 'date_add' => 'date_add', - 'date_sub' => 'date_sub', - 'adddate' => 'date_add', - 'subdate' => 'date_sub', 'localtime' => 'now', 'localtimestamp' => 'now', 'isnull' => 'isnull', 'if' => '_if', - 'regexpp' => 'regexp', - 'concat' => 'concat', + 'regexp' => 'regexp', 'field' => 'field', 'log' => 'log', 'least' => 'least', @@ -90,39 +86,6 @@ public function __construct( $pdo ) { 'version' => 'version', ); - /** - * Method to extract the month value from the date. - * - * @param string $field representing the date formatted as 0000-00-00. - * - * @return string representing the number of the month between 1 and 12. - */ - public function month( $field ) { - return gmdate( 'n', strtotime( $field ) ); - } - - /** - * Method to extract the year value from the date. - * - * @param string $field representing the date formatted as 0000-00-00. - * - * @return string representing the number of the year. - */ - public function year( $field ) { - return gmdate( 'Y', strtotime( $field ) ); - } - - /** - * Method to extract the day value from the date. - * - * @param string $field Representing the date formatted as 0000-00-00. - * - * @return string representing the number of the day of the month from 1 and 31. - */ - public function day( $field ) { - return gmdate( 'j', strtotime( $field ) ); - } - /** * Method to return the unix timestamp. * @@ -138,48 +101,13 @@ public function unix_timestamp( $field = null ) { return is_null( $field ) ? time() : strtotime( $field ); } - /** - * Method to emulate MySQL SECOND() function. - * - * @param string $field Representing the time formatted as '00:00:00'. - * - * @return number of unsigned integer - */ - public function second( $field ) { - return intval( gmdate( 's', strtotime( $field ) ) ); - } - - /** - * Method to emulate MySQL MINUTE() function. - * - * @param string $field Representing the time formatted as '00:00:00'. - * - * @return number of unsigned integer - */ - public function minute( $field ) { - return intval( gmdate( 'i', strtotime( $field ) ) ); - } - - /** - * Method to emulate MySQL HOUR() function. - * - * @param string $time Representing the time formatted as '00:00:00'. - * - * @return number - */ - public function hour( $time ) { - list($hours) = explode( ':', $time ); - - return intval( $hours ); - } - /** * Method to emulate MySQL FROM_UNIXTIME() function. * - * @param integer $field The unix timestamp. - * @param string $format Indicate the way of formatting(optional). + * @param int $field The unix timestamp. + * @param string $format Indicate the way of formatting(optional). * - * @return string formatted as '0000-00-00 00:00:00'. + * @return string */ public function from_unixtime( $field, $format = null ) { // Convert to ISO time. @@ -206,17 +134,6 @@ public function curdate() { return gmdate( 'Y-m-d' ); } - /** - * Method to emulate MySQL CHAR_LENGTH() function. - * - * @param string $field The string to be measured. - * - * @return int unsigned integer for the length of the argument. - */ - public function char_length( $field ) { - return strlen( $field ); - } - /** * Method to emulate MySQL MD5() function. * @@ -244,22 +161,6 @@ public function rand() { return mt_rand( 0, 1 ); } - /** - * Method to emulate MySQL SUBSTRING() function. - * - * This function rewrites the function name to SQLite compatible substr(), - * which can manipulate UTF-8 characters. - * - * @param string $text The text to be processed. - * @param integer $pos Representing the start point. - * @param integer $len Representing the length of the substring(optional). - * - * @return string - */ - public function substring( $text, $pos, $len = null ) { - return "substr($text, $pos, $len)"; - } - /** * Method to emulate MySQL DATEFORMAT() function. * @@ -309,146 +210,216 @@ public function dateformat( $date, $format ) { } /** - * Method to emulate MySQL DATE_ADD() function. - * - * This function adds the time value of $interval expression to $date. - * $interval is a single quoted strings rewritten by SQLiteQueryDriver::rewrite_query(). - * It is calculated in the private function derive_interval(). + * Method to extract the month value from the date. * - * @param string $date representing the start date. - * @param string $interval representing the expression of the time to add. + * @param string $field Representing the date formatted as 0000-00-00. * - * @return string date formatted as '0000-00-00 00:00:00'. + * @return string Representing the number of the month between 1 and 12. */ - public function date_add( $date, $interval ) { - $interval = $this->derive_interval( $interval ); - switch ( strtolower( $date ) ) { - case 'curdate()': - $date_object = new DateTime( $this->curdate() ); - $date_object->add( new DateInterval( $interval ) ); - return $date_object->format( 'Y-m-d' ); + public function month( $field ) { + /* + * From https://www.php.net/manual/en/datetime.format.php: + * + * n - Numeric representation of a month, without leading zeros. + * 1 through 12 + */ + return intval( gmdate( 'n', strtotime( $field ) ) ); + } - case 'now()': - $date_object = new DateTime( $this->now() ); - $date_object->add( new DateInterval( $interval ) ); - return $date_object->format( 'Y-m-d H:i:s' ); + /** + * Method to extract the year value from the date. + * + * @param string $field Representing the date formatted as 0000-00-00. + * + * @return string Representing the number of the year. + */ + public function year( $field ) { + /* + * From https://www.php.net/manual/en/datetime.format.php: + * + * Y - A full numeric representation of a year, 4 digits. + */ + return intval( gmdate( 'Y', strtotime( $field ) ) ); + } - default: - $date_object = new DateTime( $date ); - $date_object->add( new DateInterval( $interval ) ); - return $date_object->format( 'Y-m-d H:i:s' ); - } + /** + * Method to extract the day value from the date. + * + * @param string $field Representing the date formatted as 0000-00-00. + * + * @return string Representing the number of the day of the month from 1 and 31. + */ + public function day( $field ) { + /* + * From https://www.php.net/manual/en/datetime.format.php: + * + * j - Day of the month without leading zeros. + * 1 to 31. + */ + return intval( gmdate( 'j', strtotime( $field ) ) ); } /** - * Method to emulate MySQL DATE_SUB() function. + * Method to emulate MySQL SECOND() function. * - * This function subtracts the time value of $interval expression from $date. - * $interval is a single quoted strings rewritten by SQLiteQueryDriver::rewrite_query(). - * It is calculated in the private function derive_interval(). + * @see https://www.php.net/manual/en/datetime.format.php * - * @param string $date representing the start date. - * @param string $interval representing the expression of the time to subtract. + * @param string $field Representing the time formatted as '00:00:00'. * - * @return string date formatted as '0000-00-00 00:00:00'. + * @return number Unsigned integer */ - public function date_sub( $date, $interval ) { - $interval = $this->derive_interval( $interval ); - switch ( strtolower( $date ) ) { - case 'curdate()': - $date_object = new DateTime( $this->curdate() ); - $date_object->sub( new DateInterval( $interval ) ); - return $date_object->format( 'Y-m-d' ); - - case 'now()': - $date_object = new DateTime( $this->now() ); - $date_object->sub( new DateInterval( $interval ) ); - return $date_object->format( 'Y-m-d H:i:s' ); + public function second( $field ) { + /* + * From https://www.php.net/manual/en/datetime.format.php: + * + * s - Seconds, with leading zeros (00 to 59) + */ + return intval( gmdate( 's', strtotime( $field ) ) ); + } - default: - $date_object = new DateTime( $date ); - $date_object->sub( new DateInterval( $interval ) ); - return $date_object->format( 'Y-m-d H:i:s' ); - } + /** + * Method to emulate MySQL MINUTE() function. + * + * @param string $field Representing the time formatted as '00:00:00'. + * + * @return int + */ + public function minute( $field ) { + /* + * From https://www.php.net/manual/en/datetime.format.php: + * + * i - Minutes with leading zeros. + * 00 to 59. + */ + return intval( gmdate( 'i', strtotime( $field ) ) ); } /** - * Method to calculate the interval time between two dates value. + * Method to emulate MySQL HOUR() function. * - * @access private + * Returns the hour for time, in 24-hour format, from 0 to 23. + * Importantly, midnight is 0, not 24. * - * @param string $interval white space separated expression. + * @param string $time Representing the time formatted, like '14:08:12'. * - * @return string representing the time to add or substract. + * @return int */ - private function derive_interval( $interval ) { - $interval = trim( substr( trim( $interval ), 8 ) ); - $parts = explode( ' ', $interval ); - foreach ( $parts as $part ) { - if ( ! empty( $part ) ) { - $_parts[] = $part; - } - } - $type = strtolower( end( $_parts ) ); - switch ( $type ) { - case 'second': - return 'PT' . $_parts[0] . 'S'; - - case 'minute': - return 'PT' . $_parts[0] . 'M'; - - case 'hour': - return 'PT' . $_parts[0] . 'H'; - - case 'day': - return 'P' . $_parts[0] . 'D'; - - case 'week': - return 'P' . $_parts[0] . 'W'; - - case 'month': - return 'P' . $_parts[0] . 'M'; - - case 'year': - return 'P' . $_parts[0] . 'Y'; - - case 'minute_second': - list($minutes, $seconds) = explode( ':', $_parts[0] ); - return 'PT' . $minutes . 'M' . $seconds . 'S'; - - case 'hour_second': - list($hours, $minutes, $seconds) = explode( ':', $_parts[0] ); - return 'PT' . $hours . 'H' . $minutes . 'M' . $seconds . 'S'; - - case 'hour_minute': - list($hours, $minutes) = explode( ':', $_parts[0] ); - return 'PT' . $hours . 'H' . $minutes . 'M'; - - case 'day_second': - $days = intval( $_parts[0] ); - list($hours, $minutes, $seconds) = explode( ':', $_parts[1] ); - return 'P' . $days . 'D' . 'T' . $hours . 'H' . $minutes . 'M' . $seconds . 'S'; - - case 'day_minute': - $days = intval( $_parts[0] ); - list($hours, $minutes) = explode( ':', $parts[1] ); - return 'P' . $days . 'D' . 'T' . $hours . 'H' . $minutes . 'M'; + public function hour( $time ) { + /* + * From https://www.php.net/manual/en/datetime.format.php: + * + * H 24-hour format of an hour with leading zeros. + * 00 through 23. + */ + return intval( gmdate( 'H', strtotime( $time ) ) ); + } + + /** + * Covers MySQL WEEK() function. + * + * Always assumes $mode = 1. + * + * @TODO: Support other modes. + * + * From https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_week: + * + * > Returns the week number for date. The two-argument form of WEEK() + * > enables you to specify whether the week starts on Sunday or Monday + * > and whether the return value should be in the range from 0 to 53 + * > or from 1 to 53. If the mode argument is omitted, the value of the + * > default_week_format system variable is used. + * > + * > The following table describes how the mode argument works: + * > + * > Mode First day of week Range Week 1 is the first week … + * > 0 Sunday 0-53 with a Sunday in this year + * > 1 Monday 0-53 with 4 or more days this year + * > 2 Sunday 1-53 with a Sunday in this year + * > 3 Monday 1-53 with 4 or more days this year + * > 4 Sunday 0-53 with 4 or more days this year + * > 5 Monday 0-53 with a Monday in this year + * > 6 Sunday 1-53 with 4 or more days this year + * > 7 Monday 1-53 with a Monday in this year + * + * @param string $field Representing the date. + * @param int $mode The mode argument. + */ + public function week( $field, $mode ) { + /* + * From https://www.php.net/manual/en/datetime.format.php: + * + * W - ISO-8601 week number of year, weeks starting on Monday. + * Example: 42 (the 42nd week in the year) + * + * Week 1 is the first week with a Thursday in it. + */ + return intval( gmdate( 'W', strtotime( $field ) ) ); + } + + /** + * Simulates WEEKDAY() function in MySQL. + * + * Returns the day of the week as an integer. + * The days of the week are numbered 0 to 6: + * * 0 for Monday + * * 1 for Tuesday + * * 2 for Wednesday + * * 3 for Thursday + * * 4 for Friday + * * 5 for Saturday + * * 6 for Sunday + * + * @param string $field Representing the date. + * + * @return int + */ + public function weekday( $field ) { + /* + * date('N') returns 1 (for Monday) through 7 (for Sunday) + * That's one more than MySQL. + * Let's subtract one to make it compatible. + */ + return intval( gmdate( 'N', strtotime( $field ) ) ) - 1; + } - case 'day_hour': - $days = intval( $_parts[0] ); - $hours = intval( $_parts[1] ); - return 'P' . $days . 'D' . 'T' . $hours . 'H'; + /** + * Method to emulate MySQL DAYOFMONTH() function. + * + * @see https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_dayofmonth + * + * @param string $field Representing the date. + * + * @return int Returns the day of the month for date as a number in the range 1 to 31. + */ + public function dayofmonth( $field ) { + return intval( gmdate( 'j', strtotime( $field ) ) ); + } - case 'year_month': - list($years, $months) = explode( '-', $_parts[0] ); - return 'P' . $years . 'Y' . $months . 'M'; - } - return ''; + /** + * Method to emulate MySQL DAYOFWEEK() function. + * + * > Returns the weekday index for date (1 = Sunday, 2 = Monday, …, 7 = Saturday). + * > These index values correspond to the ODBC standard. Returns NULL if date is NULL. + * + * @param string $field Representing the date. + * + * @return int Returns the weekday index for date (1 = Sunday, 2 = Monday, …, 7 = Saturday). + */ + public function dayofweek( $field ) { + /** + * From https://www.php.net/manual/en/datetime.format.php: + * + * `w` – Numeric representation of the day of the week + * 0 (for Sunday) through 6 (for Saturday) + */ + return intval( gmdate( 'w', strtotime( $field ) ) ) + 1; } /** * Method to emulate MySQL DATE() function. * + * @see https://www.php.net/manual/en/datetime.format.php + * * @param string $date formatted as unix time. * * @return string formatted as '0000-00-00'. @@ -488,40 +459,39 @@ public function _if( $expression, $true, $false ) { /** * Method to emulate MySQL REGEXP() function. * - * @param string $field Haystack. * @param string $pattern Regular expression to match. + * @param string $field Haystack. * * @return integer 1 if matched, 0 if not matched. */ - public function regexp( $field, $pattern ) { + public function regexp( $pattern, $field ) { + /* + * If the original query says REGEXP BINARY + * the comparison is byte-by-byte and letter casing now + * matters since lower- and upper-case letters have different + * byte codes. + * + * The REGEXP function can't be easily made to accept two + * parameters, so we'll have to use a hack to get around this. + * + * If the first character of the pattern is a null byte, we'll + * remove it and make the comparison case-sensitive. This should + * be reasonably safe since PHP does not allow null bytes in + * regular expressions anyway. + */ + if ( "\x00" === $pattern[0] ) { + $pattern = substr( $pattern, 1 ); + $flags = ''; + } else { + // Otherwise, the search is case-insensitive. + $flags = 'i'; + } $pattern = str_replace( '/', '\/', $pattern ); - $pattern = '/' . $pattern . '/i'; + $pattern = '/' . $pattern . '/' . $flags; return preg_match( $pattern, $field ); } - /** - * Method to emulate MySQL CONCAT() function. - * - * SQLite does have CONCAT() function, but it has a different syntax from MySQL. - * So this function must be manipulated here. - * - * @return null|string Return null if the argument is null, or a concatenated string if the argument is given. - */ - public function concat() { - $return_value = ''; - $args_num = func_num_args(); - $args_list = func_get_args(); - for ( $i = 0; $i < $args_num; $i++ ) { - if ( is_null( $args_list[ $i ] ) ) { - return null; - } - $return_value .= $args_list[ $i ]; - } - - return $return_value; - } - /** * Method to emulate MySQL FIELD() function. * @@ -532,18 +502,13 @@ public function concat() { * @return int */ public function field() { - global $wpdb; $num_args = func_num_args(); if ( $num_args < 2 || is_null( func_get_arg( 0 ) ) ) { return 0; } $arg_list = func_get_args(); - $search_string = array_shift( $arg_list ); - $str_to_check = substr( $search_string, 0, strpos( $search_string, '.' ) ); - $str_to_check = str_replace( $wpdb->prefix, '', $str_to_check ); - if ( $str_to_check && in_array( trim( $str_to_check ), $wpdb->tables, true ) ) { - return 0; - } + $search_string = strtolower( array_shift( $arg_list ) ); + for ( $i = 0; $i < $num_args - 1; $i++ ) { if ( strtolower( $arg_list[ $i ] ) === $search_string ) { return $i + 1; @@ -597,7 +562,7 @@ public function log() { public function least() { $arg_list = func_get_args(); - return "min($arg_list)"; + return min( $arg_list ); } /** @@ -610,7 +575,7 @@ public function least() { public function greatest() { $arg_list = func_get_args(); - return "max($arg_list)"; + return max( $arg_list ); } /** diff --git a/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php new file mode 100644 index 0000000000..b085315a95 --- /dev/null +++ b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php @@ -0,0 +1,343 @@ +input_tokens = $input_tokens; + $this->max = count( $input_tokens ); + } + + /** + * Returns the updated query. + * + * @return string + */ + public function get_updated_query() { + $query = ''; + foreach ( $this->output_tokens as $token ) { + $query .= $token->token; + } + return $query; + } + + /** + * Add a token to the output. + * + * @param WP_SQLite_Token $token Token object. + */ + public function add( $token ) { + if ( $token ) { + $this->output_tokens[] = $token; + } + } + + /** + * Add multiple tokens to the output. + * + * @param WP_SQLite_Token[] $tokens Array of token objects. + */ + public function add_many( $tokens ) { + $this->output_tokens = array_merge( $this->output_tokens, $tokens ); + } + + /** + * Replaces all tokens. + * + * @param WP_SQLite_Token[] $tokens Array of token objects. + */ + public function replace_all( $tokens ) { + $this->output_tokens = $tokens; + } + + /** + * Peek at the next tokens and return one that matches the given criteria. + * + * @param array $query Optional. Search query. + * [ + * 'type' => string|null, // Token type. + * 'flags' => int|null, // Token flags. + * 'values' => string|null, // Token values. + * ]. + * + * @return WP_SQLite_Token + */ + public function peek( $query = array() ) { + $type = isset( $query['type'] ) ? $query['type'] : null; + $flags = isset( $query['flags'] ) ? $query['flags'] : null; + $values = isset( $query['value'] ) + ? ( is_array( $query['value'] ) ? $query['value'] : array( $query['value'] ) ) + : null; + + $i = $this->index; + while ( ++$i < $this->max ) { + if ( $this->input_tokens[ $i ]->matches( $type, $flags, $values ) ) { + return $this->input_tokens[ $i ]; + } + } + } + + /** + * Move forward and return the next tokens that match the given criteria. + * + * @param int $nth The nth token to return. + * + * @return WP_SQLite_Token + */ + public function peek_nth( $nth ) { + $found = 0; + for ( $i = $this->index + 1;$i < $this->max;$i++ ) { + $token = $this->input_tokens[ $i ]; + if ( ! $token->is_semantically_void() ) { + ++$found; + } + if ( $found === $nth ) { + return $this->input_tokens[ $i ]; + } + } + } + + /** + * Consume all the tokens. + * + * @param array $query Search query. + * + * @return void + */ + public function consume_all( $query = array() ) { + while ( $this->consume( $query ) ) { + // Do nothing. + } + } + + /** + * Consume the next tokens and return one that matches the given criteria. + * + * @param array $query Search query. + * [ + * 'type' => null, // Optional. Token type. + * 'flags' => null, // Optional. Token flags. + * 'values' => null, // Optional. Token values. + * ]. + * + * @return WP_SQLite_Token|null + */ + public function consume( $query = array() ) { + $tokens = $this->move_forward( $query ); + $this->output_tokens = array_merge( $this->output_tokens, $tokens ); + return $this->token; + } + + /** + * Drop the last consumed token and return it. + * + * @return WP_SQLite_Token|null + */ + public function drop_last() { + return array_pop( $this->output_tokens ); + } + + /** + * Skip over the next tokens and return one that matches the given criteria. + * + * @param array $query Search query. + * [ + * 'type' => null, // Optional. Token type. + * 'flags' => null, // Optional. Token flags. + * 'values' => null, // Optional. Token values. + * ]. + * + * @return WP_SQLite_Token|null + */ + public function skip( $query = array() ) { + $this->skip_and_return_all( $query ); + return $this->token; + } + + /** + * Skip over the next tokens until one matches the given criteria, + * and return all the skipped tokens. + * + * @param array $query Search query. + * [ + * 'type' => null, // Optional. Token type. + * 'flags' => null, // Optional. Token flags. + * 'values' => null, // Optional. Token values. + * ]. + * + * @return WP_SQLite_Token[] + */ + public function skip_and_return_all( $query = array() ) { + $tokens = $this->move_forward( $query ); + + /* + * When skipping over whitespaces, make sure to consume + * at least one to avoid SQL syntax errors. + */ + foreach ( $tokens as $token ) { + if ( $token->matches( WP_SQLite_Token::TYPE_WHITESPACE ) ) { + $this->add( $token ); + break; + } + } + + return $tokens; + } + + /** + * Returns the next tokens that match the given criteria. + * + * @param array $query Search query. + * [ + * 'type' => string|null, // Optional. Token type. + * 'flags' => int|null, // Optional. Token flags. + * 'values' => string|null, // Optional. Token values. + * ]. + * + * @return array + */ + private function move_forward( $query = array() ) { + $type = isset( $query['type'] ) ? $query['type'] : null; + $flags = isset( $query['flags'] ) ? $query['flags'] : null; + $values = isset( $query['value'] ) + ? ( is_array( $query['value'] ) ? $query['value'] : array( $query['value'] ) ) + : null; + $depth = isset( $query['depth'] ) ? $query['depth'] : null; + + $buffered = array(); + while ( true ) { + if ( ++$this->index >= $this->max ) { + $this->token = null; + $this->call_stack = array(); + break; + } + $this->token = $this->input_tokens[ $this->index ]; + $this->update_call_stack(); + $buffered[] = $this->token; + if ( + ( null === $depth || $this->depth === $depth ) + && $this->token->matches( $type, $flags, $values ) + ) { + break; + } + } + + return $buffered; + } + + /** + * Returns the last call stack element. + * + * @return array|null + */ + public function last_call_stack_element() { + return count( $this->call_stack ) ? $this->call_stack[ count( $this->call_stack ) - 1 ] : null; + } + + /** + * Updates the call stack. + * + * @return void + */ + private function update_call_stack() { + if ( $this->token->flags & WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ) { + $this->last_function_call = $this->token->value; + } + if ( WP_SQLite_Token::TYPE_OPERATOR === $this->token->type ) { + switch ( $this->token->value ) { + case '(': + if ( $this->last_function_call ) { + array_push( + $this->call_stack, + array( + 'function' => $this->last_function_call, + 'depth' => $this->depth, + ) + ); + $this->last_function_call = null; + } + ++$this->depth; + break; + + case ')': + --$this->depth; + $call_parent = $this->last_call_stack_element(); + if ( + $call_parent && + $call_parent['depth'] === $this->depth + ) { + array_pop( $this->call_stack ); + } + break; + } + } + } +} diff --git a/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-token.php b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-token.php new file mode 100644 index 0000000000..70e2428306 --- /dev/null +++ b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-token.php @@ -0,0 +1,328 @@ +, !==, etc. + * Bitwise operators: &, |, ^, etc. + * Assignment operators: =, +=, -=, etc. + * SQL specific operators: . (e.g. .. WHERE database.table ..), + * * (e.g. SELECT * FROM ..) + */ + const TYPE_OPERATOR = 2; + + /** + * Spaces, tabs, new lines, etc. + */ + const TYPE_WHITESPACE = 3; + + /** + * Any type of legal comment. + * + * Bash (#), C (/* *\/) or SQL (--) comments: + * + * -- SQL-comment + * + * #Bash-like comment + * + * /*C-like comment*\/ + * + * or: + * + * /*C-like + * comment*\/ + * + * Backslashes were added to respect PHP's comments syntax. + */ + const TYPE_COMMENT = 4; + + /** + * Boolean values: true or false. + */ + const TYPE_BOOL = 5; + + /** + * Numbers: 4, 0x8, 15.16, 23e42, etc. + */ + const TYPE_NUMBER = 6; + + /** + * Literal strings: 'string', "test". + * Some of these strings are actually symbols. + */ + const TYPE_STRING = 7; + + /** + * Database, table names, variables, etc. + * For example: ```SELECT `foo`, `bar` FROM `database`.`table`;```. + */ + const TYPE_SYMBOL = 8; + + /** + * Delimits an unknown string. + * For example: ```SELECT * FROM test;```, `test` is a delimiter. + */ + const TYPE_DELIMITER = 9; + + /** + * Labels in LOOP statement, ITERATE statement etc. + * For example (only for begin label): + * begin_label: BEGIN [statement_list] END [end_label] + * begin_label: LOOP [statement_list] END LOOP [end_label] + * begin_label: REPEAT [statement_list] ... END REPEAT [end_label] + * begin_label: WHILE ... DO [statement_list] END WHILE [end_label]. + */ + const TYPE_LABEL = 10; + + // Flags that describe the tokens in more detail. + // All keywords must have flag 1 so `Context::isKeyword` method doesn't + // require strict comparison. + const FLAG_KEYWORD_RESERVED = 2; + const FLAG_KEYWORD_COMPOSED = 4; + const FLAG_KEYWORD_DATA_TYPE = 8; + const FLAG_KEYWORD_KEY = 16; + const FLAG_KEYWORD_FUNCTION = 32; + + // Numbers related flags. + const FLAG_NUMBER_HEX = 1; + const FLAG_NUMBER_FLOAT = 2; + const FLAG_NUMBER_APPROXIMATE = 4; + const FLAG_NUMBER_NEGATIVE = 8; + const FLAG_NUMBER_BINARY = 16; + + // Strings related flags. + const FLAG_STRING_SINGLE_QUOTES = 1; + const FLAG_STRING_DOUBLE_QUOTES = 2; + + // Comments related flags. + const FLAG_COMMENT_BASH = 1; + const FLAG_COMMENT_C = 2; + const FLAG_COMMENT_SQL = 4; + const FLAG_COMMENT_MYSQL_CMD = 8; + + // Operators related flags. + const FLAG_OPERATOR_ARITHMETIC = 1; + const FLAG_OPERATOR_LOGICAL = 2; + const FLAG_OPERATOR_BITWISE = 4; + const FLAG_OPERATOR_ASSIGNMENT = 8; + const FLAG_OPERATOR_SQL = 16; + + // Symbols related flags. + const FLAG_SYMBOL_VARIABLE = 1; + const FLAG_SYMBOL_BACKTICK = 2; + const FLAG_SYMBOL_USER = 4; + const FLAG_SYMBOL_SYSTEM = 8; + const FLAG_SYMBOL_PARAMETER = 16; + + /** + * The token it its raw string representation. + * + * @var string + */ + public $token; + + /** + * The value this token contains (i.e. token after some evaluation). + * + * @var mixed + */ + public $value; + + /** + * The keyword value this token contains, always uppercase. + * + * @var mixed|string|null + */ + public $keyword; + + /** + * The type of this token. + * + * @var int + */ + public $type; + + /** + * The flags of this token. + * + * @var int + */ + public $flags; + + /** + * The position in the initial string where this token started. + * + * The position is counted in chars, not bytes, so you should + * use mb_* functions to properly handle utf-8 multibyte chars. + * + * @var int|null + */ + public $position; + + /** + * Constructor. + * + * @param string $token The value of the token. + * @param int $type The type of the token. + * @param int $flags The flags of the token. + */ + public function __construct( $token, $type = 0, $flags = 0 ) { + $this->token = $token; + $this->type = $type; + $this->flags = $flags; + $this->keyword = null; + $this->value = $this->extract(); + } + + /** + * Check if the token matches the given parameters. + * + * @param int|null $type The type of the token. + * @param int|null $flags The flags of the token. + * @param array|null $values The values of the token. + * + * @return bool + */ + public function matches( $type = null, $flags = null, $values = null ) { + if ( null === $type && null === $flags && ( null === $values || array() === $values ) ) { + return ! $this->is_semantically_void(); + } + + return ( + ( null === $type || $this->type === $type ) + && ( null === $flags || ( $this->flags & $flags ) ) + && ( null === $values || in_array( strtoupper( $this->value ), $values, true ) ) + ); + } + + /** + * Check if the token is semantically void (i.e. whitespace or comment). + * + * @return bool + */ + public function is_semantically_void() { + return $this->matches( self::TYPE_WHITESPACE ) || $this->matches( self::TYPE_COMMENT ); + } + + /** + * Does little processing to the token to extract a value. + * + * If no processing can be done it will return the initial string. + * + * @return mixed + */ + private function extract() { + switch ( $this->type ) { + case self::TYPE_KEYWORD: + $this->keyword = strtoupper( $this->token ); + if ( ! ( $this->flags & self::FLAG_KEYWORD_RESERVED ) ) { + /* + * Unreserved keywords should stay the way they are + * because they might represent field names. + */ + return $this->token; + } + + return $this->keyword; + + case self::TYPE_WHITESPACE: + return ' '; + + case self::TYPE_BOOL: + return strtoupper( $this->token ) === 'TRUE'; + + case self::TYPE_NUMBER: + $ret = str_replace( '--', '', $this->token ); // e.g. ---42 === -42. + if ( $this->flags & self::FLAG_NUMBER_HEX ) { + if ( $this->flags & self::FLAG_NUMBER_NEGATIVE ) { + $ret = str_replace( '-', '', $this->token ); + $ret = -hexdec( $ret ); + } else { + $ret = hexdec( $ret ); + } + } elseif ( ( $this->flags & self::FLAG_NUMBER_APPROXIMATE ) || ( $this->flags & self::FLAG_NUMBER_FLOAT ) ) { + $ret = (float) $ret; + } elseif ( ! ( $this->flags & self::FLAG_NUMBER_BINARY ) ) { + $ret = (int) $ret; + } + + return $ret; + + case self::TYPE_STRING: + // Trims quotes. + $str = $this->token; + $str = mb_substr( $str, 1, -1, 'UTF-8' ); + + // Removes surrounding quotes. + $quote = $this->token[0]; + $str = str_replace( $quote . $quote, $quote, $str ); + + /* + * Finally unescapes the string. + * + * `stripcslashes` replaces escape sequences with their + * representation. + */ + $str = stripcslashes( $str ); + + return $str; + + case self::TYPE_SYMBOL: + $str = $this->token; + if ( isset( $str[0] ) && ( '@' === $str[0] ) ) { + /* + * `mb_strlen($str)` must be used instead of `null` because + * in PHP 5.3- the `null` parameter isn't handled correctly. + */ + $str = mb_substr( + $str, + ! empty( $str[1] ) && ( '@' === $str[1] ) ? 2 : 1, + mb_strlen( $str ), + 'UTF-8' + ); + } + + if ( isset( $str[0] ) && ( ':' === $str[0] ) ) { + $str = mb_substr( $str, 1, mb_strlen( $str ), 'UTF-8' ); + } + + if ( isset( $str[0] ) && ( ( '`' === $str[0] ) || ( '"' === $str[0] ) || ( '\'' === $str[0] ) ) ) { + $quote = $str[0]; + $str = str_replace( $quote . $quote, $quote, $str ); + $str = mb_substr( $str, 1, -1, 'UTF-8' ); + } + + return $str; + } + + return $this->token; + } +} diff --git a/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-translator.php b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-translator.php new file mode 100644 index 0000000000..afe3161fc3 --- /dev/null +++ b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -0,0 +1,3624 @@ + 'integer', + 'bool' => 'integer', + 'boolean' => 'integer', + 'tinyint' => 'integer', + 'smallint' => 'integer', + 'mediumint' => 'integer', + 'int' => 'integer', + 'integer' => 'integer', + 'bigint' => 'integer', + 'float' => 'real', + 'double' => 'real', + 'decimal' => 'real', + 'dec' => 'real', + 'numeric' => 'real', + 'fixed' => 'real', + 'date' => 'text', + 'datetime' => 'text', + 'timestamp' => 'text', + 'time' => 'text', + 'year' => 'text', + 'char' => 'text', + 'varchar' => 'text', + 'binary' => 'integer', + 'varbinary' => 'blob', + 'tinyblob' => 'blob', + 'tinytext' => 'text', + 'blob' => 'blob', + 'text' => 'text', + 'mediumblob' => 'blob', + 'mediumtext' => 'text', + 'longblob' => 'blob', + 'longtext' => 'text', + 'geomcollection' => 'text', + 'geometrycollection' => 'text', + ); + + /** + * The MySQL to SQLite date formats translation. + * + * Maps MySQL formats to SQLite strftime() formats. + * + * For MySQL formats, see: + * * https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_date-format + * + * For SQLite formats, see: + * * https://www.sqlite.org/lang_datefunc.html + * * https://strftime.org/ + * + * @var array + */ + private $mysql_date_format_to_sqlite_strftime = array( + '%a' => '%D', + '%b' => '%M', + '%c' => '%n', + '%D' => '%jS', + '%d' => '%d', + '%e' => '%j', + '%H' => '%H', + '%h' => '%h', + '%I' => '%h', + '%i' => '%M', + '%j' => '%z', + '%k' => '%G', + '%l' => '%g', + '%M' => '%F', + '%m' => '%m', + '%p' => '%A', + '%r' => '%h:%i:%s %A', + '%S' => '%s', + '%s' => '%s', + '%T' => '%H:%i:%s', + '%U' => '%W', + '%u' => '%W', + '%V' => '%W', + '%v' => '%W', + '%W' => '%l', + '%w' => '%w', + '%X' => '%Y', + '%x' => '%o', + '%Y' => '%Y', + '%y' => '%y', + ); + + /** + * Number of rows found by the last SELECT query. + * + * @var int + */ + private $last_select_found_rows; + + /** + * Number of rows found by the last SQL_CALC_FOUND_ROW query. + * + * @var int integer + */ + private $last_sql_calc_found_rows = null; + + /** + * The query rewriter. + * + * @var WP_SQLite_Query_Rewriter + */ + private $rewriter; + + /** + * Last executed MySQL query. + * + * @var string + */ + public $mysql_query; + + /** + * A list of executed SQLite queries. + * + * @var array + */ + public $executed_sqlite_queries = array(); + + /** + * The columns to insert. + * + * @var array + */ + private $insert_columns = array(); + + /** + * Class variable to store the result of the query. + * + * @access private + * + * @var array reference to the PHP object + */ + private $results = null; + + /** + * Class variable to check if there is an error. + * + * @var boolean + */ + public $is_error = false; + + /** + * Class variable to store the file name and function to cause error. + * + * @access private + * + * @var array + */ + private $errors; + + /** + * Class variable to store the error messages. + * + * @access private + * + * @var array + */ + private $error_messages = array(); + + /** + * Class variable to store the affected row id. + * + * @var int integer + * @access private + */ + private $last_insert_id; + + /** + * Class variable to store the number of rows affected. + * + * @var int integer + */ + private $affected_rows; + + /** + * Class variable to store the queried column info. + * + * @var array + */ + private $column_data; + + /** + * Variable to emulate MySQL affected row. + * + * @var integer + */ + private $num_rows; + + /** + * Return value from query(). + * + * Each query has its own return value. + * + * @var mixed + */ + private $return_value; + + /** + * Variable to keep track of nested transactions level. + * + * @var number + */ + private $transaction_level = 0; + + /** + * Value returned by the last exec(). + * + * @var mixed + */ + private $last_exec_returned; + + /** + * The PDO fetch mode passed to query(). + * + * @var mixed + */ + private $pdo_fetch_mode; + + /** + * The last reserved keyword seen in an SQL query. + * + * @var mixed + */ + private $last_reserved_keyword; + + /** + * True if a VACUUM operation should be done on shutdown, + * to handle OPTIMIZE TABLE and similar operations. + * + * @var bool + */ + private $vacuum_requested = false; + + /** + * True if the present query is metadata + * + * @var bool + */ + private $is_information_schema_query = false; + + /** + * True if a GROUP BY clause is detected. + * + * @var bool + */ + private $has_group_by = false; + + /** + * 0 if no LIKE is in progress, otherwise counts nested parentheses. + * + * @todo A generic stack of expression would scale better. There's already a call_stack in WP_SQLite_Query_Rewriter. + * @var int + */ + private $like_expression_nesting = 0; + + /** + * 0 if no LIKE is in progress, otherwise counts nested parentheses. + * + * @var int + */ + private $like_escape_count = 0; + + /** + * Associative array with list of system (non-WordPress) tables. + * + * @var array [tablename => tablename] + */ + private $sqlite_system_tables = array(); + + /** + * Constructor. + * + * Create PDO object, set user defined functions and initialize other settings. + * Don't use parent::__construct() because this class does not only returns + * PDO instance but many others jobs. + * + * @param PDO $pdo The PDO object. + */ + public function __construct( $pdo = null ) { + if ( ! $pdo ) { + if ( ! is_file( FQDB ) ) { + $this->prepare_directory(); + } + + $locked = false; + $status = 0; + $err_message = ''; + do { + try { + $options = array( + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // phpcs:ignore WordPress.DB.RestrictedClasses + PDO::ATTR_STRINGIFY_FETCHES => true, // phpcs:ignore WordPress.DB.RestrictedClasses + PDO::ATTR_TIMEOUT => 5, // phpcs:ignore WordPress.DB.RestrictedClasses + ); + + $dsn = 'sqlite:' . FQDB; + $pdo = new PDO( $dsn, null, null, $options ); // phpcs:ignore WordPress.DB.RestrictedClasses + } catch ( PDOException $ex ) { + $status = $ex->getCode(); + if ( self::SQLITE_BUSY === $status || self::SQLITE_LOCKED === $status ) { + $locked = true; + } else { + $err_message = $ex->getMessage(); + } + } + } while ( $locked ); + + if ( $status > 0 ) { + $message = sprintf( + '

%s

%s

%s

', + 'Database initialization error!', + "Code: $status", + "Error Message: $err_message" + ); + $this->is_error = true; + $this->error_messages[] = $message; + return; + } + } + + new WP_SQLite_PDO_User_Defined_Functions( $pdo ); + + // MySQL data comes across stringified by default. + $pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO + $pdo->query( WP_SQLite_Translator::CREATE_DATA_TYPES_CACHE_TABLE ); + + /* + * A list of system tables lets us emulate information_schema + * queries without returning extra tables. + */ + $this->sqlite_system_tables ['sqlite_sequence'] = 'sqlite_sequence'; + $this->sqlite_system_tables [ self::DATA_TYPES_CACHE_TABLE ] = self::DATA_TYPES_CACHE_TABLE; + + $this->pdo = $pdo; + + // Fixes a warning in the site-health screen. + $this->client_info = SQLite3::version()['versionString']; + + register_shutdown_function( array( $this, '__destruct' ) ); + + // WordPress happens to use no foreign keys. + $statement = $this->pdo->query( 'PRAGMA foreign_keys' ); + if ( $statement->fetchColumn( 0 ) == '0' ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison + $this->pdo->query( 'PRAGMA foreign_keys = ON' ); + } + $this->pdo->query( 'PRAGMA encoding="UTF-8";' ); + } + + /** + * Destructor + * + * If SQLITE_MEM_DEBUG constant is defined, append information about + * memory usage into database/mem_debug.txt. + * + * This definition is changed since version 1.7. + */ + function __destruct() { + if ( defined( 'SQLITE_MEM_DEBUG' ) && SQLITE_MEM_DEBUG ) { + $max = ini_get( 'memory_limit' ); + if ( is_null( $max ) ) { + $message = sprintf( + '[%s] Memory_limit is not set in php.ini file.', + gmdate( 'Y-m-d H:i:s', $_SERVER['REQUEST_TIME'] ) + ); + error_log( $message ); + return; + } + if ( stripos( $max, 'M' ) !== false ) { + $max = (int) $max * MB_IN_BYTES; + } + $peak = memory_get_peak_usage( true ); + $used = round( (int) $peak / (int) $max * 100, 2 ); + if ( $used > 90 ) { + $message = sprintf( + "[%s] Memory peak usage warning: %s %% used. (max: %sM, now: %sM)\n", + gmdate( 'Y-m-d H:i:s', $_SERVER['REQUEST_TIME'] ), + $used, + $max, + $peak + ); + error_log( $message ); + } + } + } + + /** + * Get the PDO object. + * + * @return PDO + */ + public function get_pdo() { + return $this->pdo; + } + + /** + * Method to return inserted row id. + */ + public function get_insert_id() { + return $this->last_insert_id; + } + + /** + * Method to return the number of rows affected. + */ + public function get_affected_rows() { + return $this->affected_rows; + } + + /** + * This method makes database directory and .htaccess file. + * + * It is executed only once when the installation begins. + */ + private function prepare_directory() { + global $wpdb; + $u = umask( 0000 ); + if ( ! is_dir( FQDBDIR ) ) { + if ( ! @mkdir( FQDBDIR, 0704, true ) ) { + umask( $u ); + wp_die( 'Unable to create the required directory! Please check your server settings.', 'Error!' ); + } + } + if ( ! is_writable( FQDBDIR ) ) { + umask( $u ); + $message = 'Unable to create a file in the directory! Please check your server settings.'; + wp_die( $message, 'Error!' ); + } + if ( ! is_file( FQDBDIR . '.htaccess' ) ) { + $fh = fopen( FQDBDIR . '.htaccess', 'w' ); + if ( ! $fh ) { + umask( $u ); + echo 'Unable to create a file in the directory! Please check your server settings.'; + + return false; + } + fwrite( $fh, 'DENY FROM ALL' ); + fclose( $fh ); + } + if ( ! is_file( FQDBDIR . 'index.php' ) ) { + $fh = fopen( FQDBDIR . 'index.php', 'w' ); + if ( ! $fh ) { + umask( $u ); + echo 'Unable to create a file in the directory! Please check your server settings.'; + + return false; + } + fwrite( $fh, '' ); + fclose( $fh ); + } + umask( $u ); + + return true; + } + + /** + * Method to execute query(). + * + * Divide the query types into seven different ones. That is to say: + * + * 1. SELECT SQL_CALC_FOUND_ROWS + * 2. INSERT + * 3. CREATE TABLE(INDEX) + * 4. ALTER TABLE + * 5. SHOW VARIABLES + * 6. DROP INDEX + * 7. THE OTHERS + * + * #1 is just a tricky play. See the private function handle_sql_count() in query.class.php. + * From #2 through #5 call different functions respectively. + * #6 call the ALTER TABLE query. + * #7 is a normal process: sequentially call prepare_query() and execute_query(). + * + * #1 process has been changed since version 1.5.1. + * + * @param string $statement Full SQL statement string. + * @param int $mode Not used. + * @param array ...$fetch_mode_args Not used. + * + * @see PDO::query() + * + * @throws Exception If the query could not run. + * @throws PDOException If the translated query could not run. + * + * @return mixed according to the query type + */ + public function query( $statement, $mode = PDO::FETCH_OBJ, ...$fetch_mode_args ) { // phpcs:ignore WordPress.DB.RestrictedClasses + $this->flush(); + if ( function_exists( 'apply_filters' ) ) { + /** + * Filters queries before they are translated and run. + * + * Return a non-null value to cause query() to return early with that result. + * Use this filter to intercept queries that don't work correctly in SQLite. + * + * From within the filter you can do + * function filter_sql ($result, $translator, $statement, $mode, $fetch_mode_args) { + * if ( intercepting this query ) { + * return $translator->execute_sqlite_query( $statement ); + * } + * return $result; + * } + * + * @param null|array $result Default null to continue with the query. + * @param object $translator The translator object. You can call $translator->execute_sqlite_query(). + * @param string $statement The statement passed. + * @param int $mode Fetch mode: PDO::FETCH_OBJ, PDO::FETCH_CLASS, etc. + * @param array $fetch_mode_args Variable arguments passed to query. + * + * @returns null|array Null to proceed, or an array containing a resultset. + * @since 2.1.0 + */ + $pre = apply_filters( 'pre_query_sqlite_db', null, $this, $statement, $mode, $fetch_mode_args ); + if ( null !== $pre ) { + return $pre; + } + } + $this->pdo_fetch_mode = $mode; + $this->mysql_query = $statement; + if ( + preg_match( '/^\s*START TRANSACTION/i', $statement ) + || preg_match( '/^\s*BEGIN/i', $statement ) + ) { + return $this->begin_transaction(); + } + if ( preg_match( '/^\s*COMMIT/i', $statement ) ) { + return $this->commit(); + } + if ( preg_match( '/^\s*ROLLBACK/i', $statement ) ) { + return $this->rollback(); + } + + try { + // Perform all the queries in a nested transaction. + $this->begin_transaction(); + + do { + $error = null; + try { + $this->execute_mysql_query( + $statement + ); + } catch ( PDOException $error ) { + if ( $error->getCode() !== self::SQLITE_BUSY ) { + throw $error; + } + } + } while ( $error ); + + // Commit the nested transaction. + $this->commit(); + return $this->return_value; + } catch ( Exception $err ) { + // Rollback the nested transaction. + $this->rollback(); + if ( defined( 'PDO_DEBUG' ) && PDO_DEBUG === true ) { + throw $err; + } + return $this->handle_error( $err ); + } + } + + /** + * Method to return the queried column names. + * + * These data are meaningless for SQLite. So they are dummy emulating + * MySQL columns data. + * + * @return array|null of the object + */ + public function get_columns() { + if ( ! empty( $this->results ) ) { + $primary_key = array( + 'meta_id', + 'comment_ID', + 'link_ID', + 'option_id', + 'blog_id', + 'option_name', + 'ID', + 'term_id', + 'object_id', + 'term_taxonomy_id', + 'umeta_id', + 'id', + ); + $unique_key = array( 'term_id', 'taxonomy', 'slug' ); + $data = array( + 'name' => '', // Column name. + 'table' => '', // Table name. + 'max_length' => 0, // Max length of the column. + 'not_null' => 1, // 1 if not null. + 'primary_key' => 0, // 1 if column has primary key. + 'unique_key' => 0, // 1 if column has unique key. + 'multiple_key' => 0, // 1 if column doesn't have unique key. + 'numeric' => 0, // 1 if column has numeric value. + 'blob' => 0, // 1 if column is blob. + 'type' => '', // Type of the column. + 'int' => 0, // 1 if column is int integer. + 'zerofill' => 0, // 1 if column is zero-filled. + ); + $table_name = ''; + $sql = ''; + $query = end( $this->executed_sqlite_queries ); + if ( $query ) { + $sql = $query['sql']; + } + if ( preg_match( '/\s*FROM\s*(.*)?\s*/i', $sql, $match ) ) { + $table_name = trim( $match[1] ); + } + foreach ( $this->results[0] as $key => $value ) { + $data['name'] = $key; + $data['table'] = $table_name; + if ( in_array( $key, $primary_key, true ) ) { + $data['primary_key'] = 1; + } elseif ( in_array( $key, $unique_key, true ) ) { + $data['unique_key'] = 1; + } else { + $data['multiple_key'] = 1; + } + $this->column_data[] = json_decode( json_encode( $data ) ); + + // Reset data for next iteration. + $data['name'] = ''; + $data['table'] = ''; + $data['primary_key'] = 0; + $data['unique_key'] = 0; + $data['multiple_key'] = 0; + } + + return $this->column_data; + } + return null; + } + + /** + * Method to return the queried result data. + * + * @return mixed + */ + public function get_query_results() { + return $this->results; + } + + /** + * Method to return the number of rows from the queried result. + */ + public function get_num_rows() { + return $this->num_rows; + } + + /** + * Method to return the queried results according to the query types. + * + * @return mixed + */ + public function get_return_value() { + return $this->return_value; + } + + /** + * Executes a MySQL query in SQLite. + * + * @param string $query The query. + * + * @throws Exception If the query is not supported. + */ + private function execute_mysql_query( $query ) { + $tokens = ( new WP_SQLite_Lexer( $query ) )->tokens; + $this->rewriter = new WP_SQLite_Query_Rewriter( $tokens ); + $query_type = $this->rewriter->peek()->value; + + switch ( $query_type ) { + case 'ALTER': + $this->execute_alter(); + break; + + case 'CREATE': + $this->execute_create(); + break; + + case 'SELECT': + $this->execute_select(); + break; + + case 'INSERT': + case 'REPLACE': + $this->execute_insert_or_replace(); + break; + + case 'UPDATE': + $this->execute_update(); + break; + + case 'DELETE': + $this->execute_delete(); + break; + + case 'CALL': + case 'SET': + /* + * It would be lovely to support at least SET autocommit, + * but I don't think that is even possible with SQLite. + */ + $this->results = 0; + break; + + case 'TRUNCATE': + $this->execute_truncate(); + break; + + case 'BEGIN': + case 'START TRANSACTION': + $this->results = $this->begin_transaction(); + break; + + case 'COMMIT': + $this->results = $this->commit(); + break; + + case 'ROLLBACK': + $this->results = $this->rollback(); + break; + + case 'DROP': + $this->execute_drop(); + break; + + case 'SHOW': + $this->execute_show(); + break; + + case 'DESCRIBE': + $this->execute_describe(); + break; + + case 'CHECK': + $this->execute_check(); + break; + + case 'OPTIMIZE': + case 'REPAIR': + case 'ANALYZE': + $this->execute_optimize( $query_type ); + break; + + default: + throw new Exception( 'Unknown query type: ' . $query_type ); + } + } + + /** + * Executes a MySQL CREATE TABLE query in SQLite. + * + * @throws Exception If the query is not supported. + */ + private function execute_create_table() { + $table = $this->parse_create_table(); + + $definitions = array(); + foreach ( $table->fields as $field ) { + /* + * Do not include the inline PRIMARY KEY definition + * if there is more than one primary key. + */ + if ( $field->primary_key && count( $table->primary_key ) > 1 ) { + $field->primary_key = false; + } + if ( $field->auto_increment && count( $table->primary_key ) > 1 ) { + throw new Exception( 'Cannot combine AUTOINCREMENT and multiple primary keys in SQLite' ); + } + + $definitions[] = $this->make_sqlite_field_definition( $field ); + $this->update_data_type_cache( + $table->name, + $field->name, + $field->mysql_data_type + ); + } + + if ( count( $table->primary_key ) > 1 ) { + $definitions[] = 'PRIMARY KEY ("' . implode( '", "', $table->primary_key ) . '")'; + } + + $create_query = ( + $table->create_table . + '"' . $table->name . '" (' . "\n" . + implode( ",\n", $definitions ) . + ')' + ); + $this->execute_sqlite_query( $create_query ); + $this->results = $this->last_exec_returned; + $this->return_value = $this->results; + + foreach ( $table->constraints as $constraint ) { + $index_type = $this->mysql_index_type_to_sqlite_type( $constraint->value ); + $unique = ''; + if ( 'UNIQUE INDEX' === $index_type ) { + $unique = 'UNIQUE '; + } + $index_name = "{$table->name}__{$constraint->name}"; + $this->execute_sqlite_query( + "CREATE $unique INDEX \"$index_name\" ON \"{$table->name}\" (\"" . implode( '", "', $constraint->columns ) . '")' + ); + $this->update_data_type_cache( + $table->name, + $index_name, + $constraint->value + ); + } + } + + /** + * Parse the CREATE TABLE query. + * + * @return stdClass Structured data. + */ + private function parse_create_table() { + $this->rewriter = clone $this->rewriter; + $result = new stdClass(); + $result->create_table = null; + $result->name = null; + $result->fields = array(); + $result->constraints = array(); + $result->primary_key = array(); + + /* + * The query starts with CREATE TABLE [IF NOT EXISTS]. + * Consume everything until the table name. + */ + while ( true ) { + $token = $this->rewriter->consume(); + if ( ! $token ) { + break; + } + // The table name is the first non-keyword token. + if ( WP_SQLite_Token::TYPE_KEYWORD !== $token->type ) { + // Store the table name for later. + $result->name = $this->normalize_column_name( $token->value ); + + // Drop the table name and store the CREATE TABLE command. + $this->rewriter->drop_last(); + $result->create_table = $this->rewriter->get_updated_query(); + break; + } + } + + /* + * Move to the opening parenthesis: + * CREATE TABLE wp_options ( + * ^ here. + */ + $this->rewriter->skip( + array( + 'type' => WP_SQLite_Token::TYPE_OPERATOR, + 'value' => '(', + ) + ); + + /* + * We're in the table definition now. + * Read everything until the closing parenthesis. + */ + $declarations_depth = $this->rewriter->depth; + do { + /* + * We want to capture a rewritten line of the query. + * Let's clear any data we might have captured so far. + */ + $this->rewriter->replace_all( array() ); + + /* + * Decide how to parse the current line. We expect either: + * + * Field definition, e.g.: + * `my_field` varchar(255) NOT NULL DEFAULT 'foo' + * Constraint definition, e.g.: + * PRIMARY KEY (`my_field`) + * + * Lexer does not seem to reliably understand whether the + * first token is a field name or a reserved keyword, so + * instead we'll check whether the second non-whitespace + * token is a data type. + */ + $second_token = $this->rewriter->peek_nth( 2 ); + + if ( $second_token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_DATA_TYPE + ) ) { + $result->fields[] = $this->parse_mysql_create_table_field(); + } else { + $result->constraints[] = $this->parse_mysql_create_table_constraint(); + } + + /* + * If we're back at the initial depth, we're done. + * Also, MySQL supports a trailing comma – if we see one, + * then we're also done. + */ + } while ( + $token + && $this->rewriter->depth >= $declarations_depth + && $this->rewriter->peek()->token !== ')' + ); + + // Merge all the definitions of the primary key. + foreach ( $result->constraints as $k => $constraint ) { + if ( 'PRIMARY' === $constraint->value ) { + $result->primary_key = array_merge( + $result->primary_key, + $constraint->columns + ); + unset( $result->constraints[ $k ] ); + } + } + + // Inline primary key in a field definition. + foreach ( $result->fields as $k => $field ) { + if ( $field->primary_key ) { + $result->primary_key[] = $field->name; + } elseif ( in_array( $field->name, $result->primary_key, true ) ) { + $field->primary_key = true; + } + } + + // Remove duplicates. + $result->primary_key = array_unique( $result->primary_key ); + + return $result; + } + + /** + * Parses a CREATE TABLE query. + * + * @throws Exception If the query is not supported. + * + * @return stdClass + */ + private function parse_mysql_create_table_field() { + $result = new stdClass(); + $result->name = ''; + $result->sqlite_data_type = ''; + $result->not_null = false; + $result->default = null; + $result->auto_increment = false; + $result->primary_key = false; + + $field_name_token = $this->rewriter->skip(); // Field name. + $this->rewriter->add( new WP_SQLite_Token( "\n", WP_SQLite_Token::TYPE_WHITESPACE ) ); + $result->name = $this->normalize_column_name( $field_name_token->value ); + + $definition_depth = $this->rewriter->depth; + + $skip_mysql_data_type_parts = $this->skip_mysql_data_type(); + $result->sqlite_data_type = $skip_mysql_data_type_parts[0]; + $result->mysql_data_type = $skip_mysql_data_type_parts[1]; + + // Look for the NOT NULL and AUTO_INCREMENT flags. + while ( true ) { + $token = $this->rewriter->skip(); + if ( ! $token ) { + break; + } + if ( $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'NOT NULL' ) + ) ) { + $result->not_null = true; + continue; + } + + if ( $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'PRIMARY KEY' ) + ) ) { + $result->primary_key = true; + continue; + } + + if ( $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + null, + array( 'AUTO_INCREMENT' ) + ) ) { + $result->primary_key = true; + $result->auto_increment = true; + continue; + } + + if ( $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, + array( 'DEFAULT' ) + ) ) { + $result->default = $this->rewriter->consume()->token; + continue; + } + + if ( $this->is_create_table_field_terminator( $token, $definition_depth ) ) { + $this->rewriter->add( $token ); + break; + } + } + + return $result; + } + + /** + * Translate field definitions. + * + * @param stdClass $field Field definition. + * + * @return string + */ + private function make_sqlite_field_definition( $field ) { + $definition = '"' . $field->name . '" ' . $field->sqlite_data_type; + if ( $field->auto_increment ) { + $definition .= ' PRIMARY KEY AUTOINCREMENT'; + } elseif ( $field->primary_key ) { + $definition .= ' PRIMARY KEY '; + } + if ( $field->not_null ) { + $definition .= ' NOT NULL'; + } + if ( null !== $field->default ) { + $definition .= ' DEFAULT ' . $field->default; + } + + /* + * In MySQL, text fields are case-insensitive by default. + * COLLATE NOCASE emulates the same behavior in SQLite. + */ + if ( 'text' === $field->sqlite_data_type ) { + $definition .= ' COLLATE NOCASE'; + } + return $definition; + } + + /** + * Parses a CREATE TABLE constraint. + * + * @throws Exception If the query is not supported. + * + * @return stdClass + */ + private function parse_mysql_create_table_constraint() { + $result = new stdClass(); + $result->name = ''; + $result->value = ''; + $result->columns = array(); + + $definition_depth = $this->rewriter->depth; + $constraint = $this->rewriter->peek(); + if ( ! $constraint->matches( WP_SQLite_Token::TYPE_KEYWORD ) ) { + /* + * Not a constraint declaration, but we're not finished + * with the table declaration yet. + */ + throw new Exception( 'Unexpected token in MySQL query: ' . $this->rewriter->peek()->value ); + } + + $result->value = $this->normalize_mysql_index_type( $constraint->value ); + if ( $result->value ) { + $this->rewriter->skip(); // Constraint type. + if ( 'PRIMARY' !== $result->value ) { + $result->name = $this->rewriter->skip()->value; + } + + $constraint_depth = $this->rewriter->depth; + $this->rewriter->skip(); // `(` + do { + $result->columns[] = $this->normalize_column_name( $this->rewriter->skip()->value ); + $paren_maybe = $this->rewriter->peek(); + if ( $paren_maybe && '(' === $paren_maybe->token ) { + $this->rewriter->skip(); + $this->rewriter->skip(); + $this->rewriter->skip(); + } + $this->rewriter->skip(); // `,` or `)` + } while ( $this->rewriter->depth > $constraint_depth ); + } + + do { + $token = $this->rewriter->skip(); + } while ( ! $this->is_create_table_field_terminator( $token, $definition_depth ) ); + + return $result; + } + + /** + * Checks if the current token is the terminator of a CREATE TABLE field. + * + * @param WP_SQLite_Token $token The current token. + * @param int $definition_depth The initial depth. + * @param int|null $current_depth The current depth. + * + * @return bool + */ + private function is_create_table_field_terminator( $token, $definition_depth, $current_depth = null ) { + if ( null === $current_depth ) { + $current_depth = $this->rewriter->depth; + } + return ( + // Reached the end of the query. + null === $token + + // The field-terminating ",". + || ( + $current_depth === $definition_depth && + WP_SQLite_Token::TYPE_OPERATOR === $token->type && + ',' === $token->value + ) + + // The definitions-terminating ")". + || $current_depth === $definition_depth - 1 + + // The query-terminating ";". + || ( + WP_SQLite_Token::TYPE_DELIMITER === $token->type && + ';' === $token->value + ) + ); + } + + /** + * Executes a DELETE statement. + * + * @throws Exception If the table could not be found. + */ + private function execute_delete() { + $this->rewriter->consume(); // DELETE. + + // Process expressions and extract bound parameters. + $params = array(); + while ( true ) { + $token = $this->rewriter->peek(); + if ( ! $token ) { + break; + } + + $this->remember_last_reserved_keyword( $token ); + + if ( + $this->extract_bound_parameter( $token, $params ) + || $this->translate_expression( $token ) + ) { + continue; + } + + $this->rewriter->consume(); + } + $this->rewriter->consume_all(); + + $updated_query = $this->rewriter->get_updated_query(); + + // Perform DELETE-specific translations. + + // Naive rewriting of DELETE JOIN query. + // @TODO: Actually rewrite the query instead of using a hardcoded workaround. + if ( str_contains( $updated_query, ' JOIN ' ) ) { + $table_prefix = isset( $GLOBALS['table_prefix'] ) ? $GLOBALS['table_prefix'] : 'wp_'; + $this->execute_sqlite_query( + "DELETE FROM {$table_prefix}options WHERE option_id IN (SELECT MIN(option_id) FROM {$table_prefix}options GROUP BY option_name HAVING COUNT(*) > 1)" + ); + $this->set_result_from_affected_rows(); + return; + } + + $rewriter = new WP_SQLite_Query_Rewriter( $this->rewriter->output_tokens ); + + $comma = $rewriter->peek( + array( + 'type' => WP_SQLite_Token::TYPE_OPERATOR, + 'value' => ',', + ) + ); + $from = $rewriter->peek( + array( + 'type' => WP_SQLite_Token::TYPE_KEYWORD, + 'value' => 'FROM', + ) + ); + // The DELETE query targets a single table if there's no comma before the FROM. + if ( ! $comma || ! $from || $comma->position >= $from->position ) { + $this->execute_sqlite_query( + $updated_query, + $params + ); + $this->set_result_from_affected_rows(); + return; + } + + // The DELETE query targets multiple tables – rewrite it into a + // SELECT to fetch the IDs of the rows to delete, then delete them + // using a separate DELETE query. + + $table_name = $rewriter->skip()->value; + $rewriter->add( new WP_SQLite_Token( 'SELECT', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) ); + + /* + * Get table name. + */ + $from = $rewriter->peek( + array( + 'type' => WP_SQLite_Token::TYPE_KEYWORD, + 'value' => 'FROM', + ) + ); + $index = array_search( $from, $rewriter->input_tokens, true ); + for ( $i = $index + 1; $i < $rewriter->max; $i++ ) { + // Assume the table name is the first token after FROM. + if ( ! $rewriter->input_tokens[ $i ]->is_semantically_void() ) { + $table_name = $rewriter->input_tokens[ $i ]->value; + break; + } + } + if ( ! $table_name ) { + throw new Exception( 'Could not find table name for dual delete query.' ); + } + + /* + * Now, let's figure out the primary key name. + * This assumes that all listed table names are the same. + */ + $q = $this->execute_sqlite_query( 'SELECT l.name FROM pragma_table_info("' . $table_name . '") as l WHERE l.pk = 1;' ); + $pk_name = $q->fetch()['name']; + + /* + * Good, we can finally create the SELECT query. + * Let's rewrite DELETE a, b FROM ... to SELECT a.id, b.id FROM ... + */ + $alias_nb = 0; + while ( true ) { + $token = $rewriter->consume(); + if ( WP_SQLite_Token::TYPE_KEYWORD === $token->type && 'FROM' === $token->value ) { + break; + } + + /* + * Between DELETE and FROM we only expect commas and table aliases. + * If it's not a comma, it must be a table alias. + */ + if ( ',' !== $token->value ) { + // Insert .id AS id_1 after the table alias. + $rewriter->add_many( + array( + new WP_SQLite_Token( '.', WP_SQLite_Token::TYPE_OPERATOR, WP_SQLite_Token::FLAG_OPERATOR_SQL ), + new WP_SQLite_Token( $pk_name, WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'AS', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'id_' . $alias_nb, WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ), + ) + ); + ++$alias_nb; + } + } + $rewriter->consume_all(); + + // Select the IDs to delete. + $select = $rewriter->get_updated_query(); + $stmt = $this->execute_sqlite_query( $select ); + $stmt->execute( $params ); + $rows = $stmt->fetchAll(); + $ids_to_delete = array(); + foreach ( $rows as $id ) { + $ids_to_delete[] = $id['id_0']; + $ids_to_delete[] = $id['id_1']; + } + + $query = ( + count( $ids_to_delete ) + ? "DELETE FROM {$table_name} WHERE {$pk_name} IN (" . implode( ',', $ids_to_delete ) . ')' + : "DELETE FROM {$table_name} WHERE 0=1" + ); + $this->execute_sqlite_query( $query ); + $this->set_result_from_affected_rows( + count( $ids_to_delete ) + ); + } + + /** + * Executes a SELECT statement. + */ + private function execute_select() { + $this->rewriter->consume(); // SELECT. + + $params = array(); + $table_name = null; + $has_sql_calc_found_rows = false; + + // Consume and record the table name. + while ( true ) { + $token = $this->rewriter->peek(); + if ( ! $token ) { + break; + } + + $this->remember_last_reserved_keyword( $token ); + + if ( ! $table_name ) { + $table_name = $this->peek_table_name( $token ); + } + + if ( $this->skip_sql_calc_found_rows( $token ) ) { + $has_sql_calc_found_rows = true; + continue; + } + + if ( + $this->extract_bound_parameter( $token, $params ) + || $this->translate_expression( $token ) + ) { + continue; + } + + $this->rewriter->consume(); + } + $this->rewriter->consume_all(); + + $updated_query = $this->rewriter->get_updated_query(); + + if ( $table_name && str_starts_with( strtolower( $table_name ), 'information_schema' ) ) { + $this->is_information_schema_query = true; + $updated_query = $this->get_information_schema_query( $updated_query ); + $params = array(); + } elseif ( + strpos( $updated_query, '@@SESSION.sql_mode' ) !== false + || strpos( $updated_query, 'CONVERT( ' ) !== false + ) { + /* + * If the query contains a function that is not supported by SQLite, + * return a dummy select. This check must be done after the query + * has been rewritten to use parameters to avoid false positives + * on queries such as `SELECT * FROM table WHERE field='CONVERT('`. + */ + $updated_query = 'SELECT 1=0'; + $params = array(); + } elseif ( $has_sql_calc_found_rows ) { + // Emulate SQL_CALC_FOUND_ROWS for now. + $query = $updated_query; + // We make the data for next SELECT FOUND_ROWS() statement. + $unlimited_query = preg_replace( '/\\bLIMIT\\s\d+(?:\s*,\s*\d+)?$/imsx', '', $query ); + $stmt = $this->execute_sqlite_query( $unlimited_query ); + $stmt->execute( $params ); + $this->last_sql_calc_found_rows = count( $stmt->fetchAll() ); + } + + // Emulate FOUND_ROWS() by counting the rows in the result set. + if ( strpos( $updated_query, 'FOUND_ROWS(' ) !== false ) { + $last_found_rows = ( $this->last_sql_calc_found_rows ? $this->last_sql_calc_found_rows : 0 ) . ''; + $updated_query = "SELECT {$last_found_rows} AS `FOUND_ROWS()`"; + } + + $stmt = $this->execute_sqlite_query( $updated_query, $params ); + if ( $this->is_information_schema_query ) { + $this->set_results_from_fetched_data( + $this->strip_sqlite_system_tables( + $stmt->fetchAll( $this->pdo_fetch_mode ) + ) + ); + } else { + $this->set_results_from_fetched_data( + $stmt->fetchAll( $this->pdo_fetch_mode ) + ); + } + } + + /** + * Executes a TRUNCATE statement. + */ + private function execute_truncate() { + $this->rewriter->skip(); // TRUNCATE. + $this->rewriter->skip(); // TABLE. + $this->rewriter->add( new WP_SQLite_Token( 'DELETE', WP_SQLite_Token::TYPE_KEYWORD ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + $this->rewriter->add( new WP_SQLite_Token( 'FROM', WP_SQLite_Token::TYPE_KEYWORD ) ); + $this->rewriter->consume_all(); + $this->execute_sqlite_query( $this->rewriter->get_updated_query() ); + $this->results = true; + $this->return_value = true; + } + + /** + * Executes a DESCRIBE statement. + * + * @throws PDOException When the table is not found. + */ + private function execute_describe() { + $this->rewriter->skip(); + $table_name = $this->rewriter->consume()->value; + $stmt = $this->execute_sqlite_query( + "SELECT + `name` as `Field`, + ( + CASE `notnull` + WHEN 0 THEN 'YES' + WHEN 1 THEN 'NO' + END + ) as `Null`, + IFNULL( + d.`mysql_type`, + ( + CASE `type` + WHEN 'INTEGER' THEN 'int' + WHEN 'TEXT' THEN 'text' + WHEN 'BLOB' THEN 'blob' + WHEN 'REAL' THEN 'real' + ELSE `type` + END + ) + ) as `Type`, + TRIM(`dflt_value`, \"'\") as `Default`, + '' as Extra, + ( + CASE `pk` + WHEN 0 THEN '' + ELSE 'PRI' + END + ) as `Key` + FROM pragma_table_info(\"$table_name\") p + LEFT JOIN " . self::DATA_TYPES_CACHE_TABLE . " d + ON d.`table` = \"$table_name\" + AND d.`column_or_index` = p.`name` + ; + " + ); + $this->set_results_from_fetched_data( + $stmt->fetchAll( $this->pdo_fetch_mode ) + ); + if ( ! $this->results ) { + throw new PDOException( 'Table not found' ); + } + } + + /** + * Executes an UPDATE statement. + */ + private function execute_update() { + $this->rewriter->consume(); // Update. + + $params = array(); + while ( true ) { + $token = $this->rewriter->peek(); + if ( ! $token ) { + break; + } + + $this->remember_last_reserved_keyword( $token ); + + if ( + $this->extract_bound_parameter( $token, $params ) + || $this->translate_expression( $token ) + ) { + continue; + } + + $this->rewriter->consume(); + } + $this->rewriter->consume_all(); + + $updated_query = $this->rewriter->get_updated_query(); + $this->execute_sqlite_query( $updated_query, $params ); + $this->set_result_from_affected_rows(); + } + + /** + * Executes a INSERT or REPLACE statement. + */ + private function execute_insert_or_replace() { + $params = array(); + $is_in_duplicate_section = false; + $table_name = null; + + $this->rewriter->consume(); // INSERT or REPLACE. + + // Consume the query type. + if ( 'IGNORE' === $this->rewriter->peek()->value ) { + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + $this->rewriter->add( new WP_SQLite_Token( 'OR', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) ); + $this->rewriter->consume(); // IGNORE. + } + + // Consume and record the table name. + $this->insert_columns = array(); + $this->rewriter->consume(); // INTO. + $table_name = $this->rewriter->consume()->value; // Table name. + + /* + * A list of columns is given if the opening parenthesis + * is earlier than the VALUES keyword. + */ + $paren = $this->rewriter->peek( + array( + 'type' => WP_SQLite_Token::TYPE_OPERATOR, + 'value' => '(', + ) + ); + $values = $this->rewriter->peek( + array( + 'type' => WP_SQLite_Token::TYPE_KEYWORD, + 'value' => 'VALUES', + ) + ); + if ( $paren && $values && $paren->position <= $values->position ) { + $this->rewriter->consume( + array( + 'type' => WP_SQLite_Token::TYPE_OPERATOR, + 'value' => '(', + ) + ); + while ( true ) { + $token = $this->rewriter->consume(); + if ( $token->matches( WP_SQLite_Token::TYPE_OPERATOR, null, array( ')' ) ) ) { + break; + } + if ( ! $token->matches( WP_SQLite_Token::TYPE_OPERATOR ) ) { + $this->insert_columns[] = $token->value; + } + } + } + + while ( true ) { + $token = $this->rewriter->peek(); + if ( ! $token ) { + break; + } + + $this->remember_last_reserved_keyword( $token ); + + if ( + ( $is_in_duplicate_section && $this->translate_values_function( $token ) ) + || $this->extract_bound_parameter( $token, $params ) + || $this->translate_expression( $token ) + ) { + continue; + } + + if ( $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + null, + array( 'DUPLICATE' ) + ) + ) { + $is_in_duplicate_section = true; + $this->translate_on_duplicate_key( $table_name ); + continue; + } + + $this->rewriter->consume(); + } + + $this->rewriter->consume_all(); + + $updated_query = $this->rewriter->get_updated_query(); + $this->execute_sqlite_query( $updated_query, $params ); + $this->set_result_from_affected_rows(); + $this->last_insert_id = $this->pdo->lastInsertId(); + if ( is_numeric( $this->last_insert_id ) ) { + $this->last_insert_id = (int) $this->last_insert_id; + } + } + + /** + * Preprocesses a string literal. + * + * @param string $value The string literal. + * + * @return string The preprocessed string literal. + */ + private function preprocess_string_literal( $value ) { + /* + * The code below converts the date format to one preferred by SQLite. + * + * MySQL accepts ISO 8601 date strings: 'YYYY-MM-DDTHH:MM:SSZ' + * SQLite prefers a slightly different format: 'YYYY-MM-DD HH:MM:SS' + * + * SQLite date and time functions can understand the ISO 8601 notation, but + * lookups don't. To keep the lookups working, we need to store all dates + * in UTC without the "T" and "Z" characters. + * + * Caveat: It will adjust every string that matches the pattern, not just dates. + * + * In theory, we could only adjust semantic dates, e.g. the data inserted + * to a date column or compared against a date column. + * + * In practice, this is hard because dates are just text – SQLite has no separate + * datetime field. We'd need to cache the MySQL data type from the original + * CREATE TABLE query and then keep refreshing the cache after each ALTER TABLE query. + * + * That's a lot of complexity that's perhaps not worth it. Let's just convert + * everything for now. The regexp assumes "Z" is always at the end of the string, + * which is true in the unit test suite, but there could also be a timezone offset + * like "+00:00" or "+01:00". We could add support for that later if needed. + */ + if ( 1 === preg_match( '/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})Z$/', $value, $matches ) ) { + $value = $matches[1] . ' ' . $matches[2]; + } + + /* + * Mimic MySQL's behavior and truncate invalid dates. + * + * "2020-12-41 14:15:27" becomes "0000-00-00 00:00:00" + * + * WARNING: We have no idea whether the truncated value should + * be treated as a date in the first place. + * In SQLite dates are just strings. This could be a perfectly + * valid string that just happens to contain a date-like value. + * + * At the same time, WordPress seems to rely on MySQL's behavior + * and even tests for it in Tests_Post_wpInsertPost::test_insert_empty_post_date. + * Let's truncate the dates for now. + * + * In the future, let's update WordPress to do its own date validation + * and stop relying on this MySQL feature, + */ + if ( 1 === preg_match( '/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})$/', $value, $matches ) ) { + /* + * Calling strtotime("0000-00-00 00:00:00") in 32-bit environments triggers + * an "out of integer range" warning – let's avoid that call for the popular + * case of "zero" dates. + */ + if ( '0000-00-00 00:00:00' !== $value && false === strtotime( $value ) ) { + $value = '0000-00-00 00:00:00'; + } + } + return $value; + } + + /** + * Preprocesses a LIKE expression. + * + * @param WP_SQLite_Token $token The token to preprocess. + * @return string + */ + private function preprocess_like_expr( &$token ) { + /* + * This code handles escaped wildcards in LIKE clauses. + * If we are within a LIKE experession, we look for \_ and \%, the + * escaped LIKE wildcards, the ones where we want a literal, not a + * wildcard match. We change the \ escape for an ASCII \x1a (SUB) character, + * so the \ characters won't get munged. + * These \_ and \% escape sequences are in the token name, because + * the lexer has already done stripcslashes on the value. + */ + if ( $this->like_expression_nesting > 0 ) { + /* Remove the quotes around the name. */ + $unescaped_value = mb_substr( $token->token, 1, -1, 'UTF-8' ); + if ( str_contains( $unescaped_value, '\_' ) || str_contains( $unescaped_value, '\%' ) ) { + $this->like_escape_count ++; + return str_replace( + array( '\_', '\%' ), + array( self::LIKE_ESCAPE_CHAR . '_', self::LIKE_ESCAPE_CHAR . '%' ), + $unescaped_value + ); + } + } + return $token->value; + } + /** + * Translate CAST() function when we want to cast to BINARY. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_cast_as_binary( $token ) { + if ( ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_DATA_TYPE, + array( 'BINARY' ) + ) + ) { + return false; + } + + $call_parent = $this->rewriter->last_call_stack_element(); + if ( + ! $call_parent + || 'CAST' !== $call_parent['function'] + ) { + return false; + } + + // Rewrite AS BINARY to AS BLOB inside CAST() calls. + $this->rewriter->skip(); + $this->rewriter->add( new WP_SQLite_Token( 'BLOB', $token->type, $token->flags ) ); + return true; + } + + /** + * Translates an expression in an SQL statement if the token is the start of an expression. + * + * @param WP_SQLite_Token $token The first token of an expression. + * + * @return bool True if the expression was translated successfully, false otherwise. + */ + private function translate_expression( $token ) { + return ( + $this->skip_from_dual( $token ) + || $this->translate_concat_function( $token ) + || $this->translate_concat_comma_to_pipes( $token ) + || $this->translate_function_aliases( $token ) + || $this->translate_cast_as_binary( $token ) + || $this->translate_date_add_sub( $token ) + || $this->translate_date_format( $token ) + || $this->translate_interval( $token ) + || $this->translate_regexp_functions( $token ) + || $this->capture_group_by( $token ) + || $this->translate_ungrouped_having( $token ) + || $this->translate_like_escape( $token ) + ); + } + + /** + * Skips the `FROM DUAL` clause in the SQL statement. + * + * @param WP_SQLite_Token $token The token to check for the `FROM DUAL` clause. + * + * @return bool True if the `FROM DUAL` clause was skipped, false otherwise. + */ + private function skip_from_dual( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'FROM' ) + ) + ) { + return false; + } + $from_table = $this->rewriter->peek_nth( 2 )->value; + if ( 'DUAL' !== strtoupper( $from_table ) ) { + return false; + } + + // FROM DUAL is a MySQLism that means "no tables". + $this->rewriter->skip(); + $this->rewriter->skip(); + return true; + } + + /** + * Peeks at the table name in the SQL statement. + * + * @param WP_SQLite_Token $token The token to check for the table name. + * + * @return string|bool The table name if it was found, false otherwise. + */ + private function peek_table_name( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'FROM' ) + ) + ) { + return false; + } + $table_name = $this->rewriter->peek_nth( 2 )->value; + if ( 'dual' === strtolower( $table_name ) ) { + return false; + } + return $table_name; + } + + /** + * Skips the `SQL_CALC_FOUND_ROWS` keyword in the SQL statement. + * + * @param WP_SQLite_Token $token The token to check for the `SQL_CALC_FOUND_ROWS` keyword. + * + * @return bool True if the `SQL_CALC_FOUND_ROWS` keyword was skipped, false otherwise. + */ + private function skip_sql_calc_found_rows( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + null, + array( 'SQL_CALC_FOUND_ROWS' ) + ) + ) { + return false; + } + $this->rewriter->skip(); + return true; + } + + /** + * Remembers the last reserved keyword encountered in the SQL statement. + * + * @param WP_SQLite_Token $token The token to check for the reserved keyword. + */ + private function remember_last_reserved_keyword( $token ) { + if ( + $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED + ) + ) { + $this->last_reserved_keyword = $token->value; + } + } + + /** + * Extracts the bound parameter from the given token and adds it to the `$params` array. + * + * @param WP_SQLite_Token $token The token to extract the bound parameter from. + * @param array $params An array of parameters to be bound to the SQL statement. + * + * @return bool True if the parameter was extracted successfully, false otherwise. + */ + private function extract_bound_parameter( $token, &$params ) { + if ( ! $token->matches( + WP_SQLite_Token::TYPE_STRING, + WP_SQLite_Token::FLAG_STRING_SINGLE_QUOTES + ) + || 'AS' === $this->last_reserved_keyword + ) { + return false; + } + + $param_name = ':param' . count( $params ); + $value = $this->preprocess_like_expr( $token ); + $value = $this->preprocess_string_literal( $value ); + $params[ $param_name ] = $value; + $this->rewriter->skip(); + $this->rewriter->add( new WP_SQLite_Token( $param_name, WP_SQLite_Token::TYPE_STRING, WP_SQLite_Token::FLAG_STRING_SINGLE_QUOTES ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + return true; + } + + /** + * Translate CONCAT() function. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_concat_function( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, + array( 'CONCAT' ) + ) + ) { + return false; + } + + /* + * Skip the CONCAT function but leave the parentheses. + * There is another code block below that replaces the + * , operators between the CONCAT arguments with ||. + */ + $this->rewriter->skip(); + return true; + } + + /** + * Translate CONCAT() function arguments. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_concat_comma_to_pipes( $token ) { + if ( ! $token->matches( + WP_SQLite_Token::TYPE_OPERATOR, + WP_SQLite_Token::FLAG_OPERATOR_SQL, + array( ',' ) + ) + ) { + return false; + } + + $call_parent = $this->rewriter->last_call_stack_element(); + if ( + ! $call_parent + || 'CONCAT' !== $call_parent['function'] + ) { + return false; + } + + // Rewrite commas to || in CONCAT() calls. + $this->rewriter->skip(); + $this->rewriter->add( new WP_SQLite_Token( '||', WP_SQLite_Token::TYPE_OPERATOR ) ); + return true; + } + + /** + * Translate DATE_ADD() and DATE_SUB() functions. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_date_add_sub( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, + array( 'DATE_ADD', 'DATE_SUB' ) + ) + ) { + return false; + } + + $this->rewriter->skip(); + $this->rewriter->add( new WP_SQLite_Token( 'DATETIME', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ) ); + return true; + } + + /** + * Convert function aliases. + * + * @param object $token The current token. + * + * @return bool False when no match, true when this function consumes the token. + * + * @todo LENGTH and CHAR_LENGTH aren't always the same in MySQL for utf8 characters. They are in SQLite. + */ + private function translate_function_aliases( $token ) { + if ( ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, + array( 'SUBSTRING', 'CHAR_LENGTH' ) + ) + ) { + return false; + } + switch ( $token->value ) { + case 'SUBSTRING': + $name = 'SUBSTR'; + break; + case 'CHAR_LENGTH': + $name = 'LENGTH'; + break; + default: + $name = $token->value; + break; + } + $this->rewriter->skip(); + $this->rewriter->add( new WP_SQLite_Token( $name, $token->type, $token->flags ) ); + + return true; + } + + /** + * Translate VALUES() function. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_values_function( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, + array( 'VALUES' ) + ) + ) { + return false; + } + + /* + * Rewrite: VALUES(`option_name`) + * to: excluded.option_name + */ + $this->rewriter->skip(); + $this->rewriter->add( new WP_SQLite_Token( 'excluded', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ) ); + $this->rewriter->add( new WP_SQLite_Token( '.', WP_SQLite_Token::TYPE_OPERATOR ) ); + + $this->rewriter->skip(); // Skip the opening `(`. + // Consume the column name. + $this->rewriter->consume( + array( + 'type' => WP_SQLite_Token::TYPE_OPERATOR, + 'value' => ')', + ) + ); + // Drop the consumed ')' token. + $this->rewriter->drop_last(); + return true; + } + + /** + * Translate DATE_FORMAT() function. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @throws Exception If the token is not a DATE_FORMAT() function. + * + * @return bool + */ + private function translate_date_format( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, + array( 'DATE_FORMAT' ) + ) + ) { + return false; + } + + // Rewrite DATE_FORMAT( `post_date`, '%Y-%m-%d' ) to STRFTIME( '%Y-%m-%d', `post_date` ). + + // Skip the DATE_FORMAT function name. + $this->rewriter->skip(); + // Skip the opening `(`. + $this->rewriter->skip(); + + // Skip the first argument so we can read the second one. + $first_arg = $this->rewriter->skip_and_return_all( + array( + 'type' => WP_SQLite_Token::TYPE_OPERATOR, + 'value' => ',', + ) + ); + + // Make sure we actually found the comma. + $comma = array_pop( $first_arg ); + if ( ',' !== $comma->value ) { + throw new Exception( 'Could not parse the DATE_FORMAT() call' ); + } + + // Skip the second argument but capture the token. + $format = $this->rewriter->skip()->value; + $new_format = strtr( $format, $this->mysql_date_format_to_sqlite_strftime ); + if ( ! $new_format ) { + throw new Exception( "Could not translate a DATE_FORMAT() format to STRFTIME format ($format)" ); + } + + /* + * MySQL supports comparing strings and floats, e.g. + * + * > SELECT '00.42' = 0.4200 + * 1 + * + * SQLite does not support that. At the same time, + * WordPress likes to filter dates by comparing numeric + * outputs of DATE_FORMAT() to floats, e.g.: + * + * -- Filter by hour and minutes + * DATE_FORMAT( + * STR_TO_DATE('2014-10-21 00:42:29', '%Y-%m-%d %H:%i:%s'), + * '%H.%i' + * ) = 0.4200; + * + * Let's cast the STRFTIME() output to a float if + * the date format is typically used for string + * to float comparisons. + * + * In the future, let's update WordPress to avoid comparing + * strings and floats. + */ + $cast_to_float = '%H.%i' === $format; + if ( $cast_to_float ) { + $this->rewriter->add( new WP_SQLite_Token( 'CAST', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ) ); + $this->rewriter->add( new WP_SQLite_Token( '(', WP_SQLite_Token::TYPE_OPERATOR ) ); + } + + $this->rewriter->add( new WP_SQLite_Token( 'STRFTIME', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ) ); + $this->rewriter->add( new WP_SQLite_Token( '(', WP_SQLite_Token::TYPE_OPERATOR ) ); + $this->rewriter->add( new WP_SQLite_Token( "'$new_format'", WP_SQLite_Token::TYPE_STRING ) ); + $this->rewriter->add( new WP_SQLite_Token( ',', WP_SQLite_Token::TYPE_OPERATOR ) ); + + // Add the buffered tokens back to the stream. + $this->rewriter->add_many( $first_arg ); + + // Consume the closing ')'. + $this->rewriter->consume( + array( + 'type' => WP_SQLite_Token::TYPE_OPERATOR, + 'value' => ')', + ) + ); + + if ( $cast_to_float ) { + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + $this->rewriter->add( new WP_SQLite_Token( 'as', WP_SQLite_Token::TYPE_OPERATOR ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + $this->rewriter->add( new WP_SQLite_Token( 'FLOAT', WP_SQLite_Token::TYPE_KEYWORD ) ); + $this->rewriter->add( new WP_SQLite_Token( ')', WP_SQLite_Token::TYPE_OPERATOR ) ); + } + + return true; + } + + /** + * Translate INTERVAL keyword with DATE_ADD() and DATE_SUB(). + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_interval( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + null, + array( 'INTERVAL' ) + ) + ) { + return false; + } + // Skip the INTERVAL keyword from the output stream. + $this->rewriter->skip(); + + $num = $this->rewriter->skip()->value; + $unit = $this->rewriter->skip()->value; + + /* + * In MySQL, we say: + * DATE_ADD(d, INTERVAL 1 YEAR) + * DATE_SUB(d, INTERVAL 1 YEAR) + * + * In SQLite, we say: + * DATE(d, '+1 YEAR') + * DATE(d, '-1 YEAR') + * + * The sign of the interval is determined by the date_* function + * that is closest in the call stack. + * + * Let's find it. + */ + $interval_op = '+'; // Default to adding. + for ( $j = count( $this->rewriter->call_stack ) - 1; $j >= 0; $j-- ) { + $call = $this->rewriter->call_stack[ $j ]; + if ( 'DATE_ADD' === $call['function'] ) { + $interval_op = '+'; + break; + } + if ( 'DATE_SUB' === $call['function'] ) { + $interval_op = '-'; + break; + } + } + + $this->rewriter->add( new WP_SQLite_Token( "'{$interval_op}$num $unit'", WP_SQLite_Token::TYPE_STRING ) ); + return true; + } + + /** + * Translate REGEXP and RLIKE keywords. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_regexp_functions( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + null, + array( 'REGEXP', 'RLIKE' ) + ) + ) { + return false; + } + $this->rewriter->skip(); + $this->rewriter->add( new WP_SQLite_Token( 'REGEXP', WP_SQLite_Token::TYPE_KEYWORD ) ); + + $next = $this->rewriter->peek(); + + /* + * If the query says REGEXP BINARY, the comparison is byte-by-byte + * and letter casing matters – lowercase and uppercase letters are + * represented using different byte codes. + * + * The REGEXP function can't be easily made to accept two + * parameters, so we'll have to use a hack to get around this. + * + * If the first character of the pattern is a null byte, we'll + * remove it and make the comparison case-sensitive. This should + * be reasonably safe since PHP does not allow null bytes in + * regular expressions anyway. + */ + if ( $next->matches( WP_SQLite_Token::TYPE_KEYWORD, null, array( 'BINARY' ) ) ) { + // Skip the "BINARY" keyword. + $this->rewriter->skip(); + // Prepend a null byte to the pattern. + $this->rewriter->add_many( + array( + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'char', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ), + new WP_SQLite_Token( '(', WP_SQLite_Token::TYPE_OPERATOR ), + new WP_SQLite_Token( '0', WP_SQLite_Token::TYPE_NUMBER ), + new WP_SQLite_Token( ')', WP_SQLite_Token::TYPE_OPERATOR ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( '||', WP_SQLite_Token::TYPE_OPERATOR ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + ) + ); + } + return true; + } + + /** + * Detect GROUP BY. + * + * @todo edgecase Fails on a statement with GROUP BY nested in an outer HAVING without GROUP BY. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function capture_group_by( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'GROUP' ) + ) + ) { + return false; + } + $next = $this->rewriter->peek_nth( 2 )->value; + if ( 'BY' !== strtoupper( $next ) ) { + return false; + } + + $this->has_group_by = true; + + return false; + } + + /** + * Translate WHERE something HAVING something to WHERE something AND something. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_ungrouped_having( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'HAVING' ) + ) + ) { + return false; + } + if ( $this->has_group_by ) { + return false; + } + $this->rewriter->skip(); + $this->rewriter->add( new WP_SQLite_Token( 'AND', WP_SQLite_Token::TYPE_KEYWORD ) ); + + return true; + } + + /** + * Rewrite LIKE '\_whatever' as LIKE '\_whatever' ESCAPE '\' . + * + * We look for keyword LIKE. On seeing it we set a flag. + * If the flag is set, we emit ESCAPE '\' before the next keyword. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_like_escape( $token ) { + + if ( 0 === $this->like_expression_nesting ) { + $is_like = $token->matches( WP_SQLite_Token::TYPE_KEYWORD, null, array( 'LIKE' ) ); + /* is this the LIKE keyword? If so set the flag. */ + if ( $is_like ) { + $this->like_expression_nesting = 1; + } + } else { + /* open parenthesis during LIKE parameter, count it. */ + if ( $token->matches( WP_SQLite_Token::TYPE_OPERATOR, null, array( '(' ) ) ) { + $this->like_expression_nesting ++; + + return false; + } + + /* close parenthesis matching open parenthesis during LIKE parameter, count it. */ + if ( $this->like_expression_nesting > 1 && $token->matches( WP_SQLite_Token::TYPE_OPERATOR, null, array( ')' ) ) ) { + $this->like_expression_nesting --; + + return false; + } + + /* a keyword, a commo, a semicolon, the end of the statement, or a close parenthesis */ + $is_like_finished = $token->matches( WP_SQLite_Token::TYPE_KEYWORD ) + || $token->matches( WP_SQLite_Token::TYPE_DELIMITER, null, array( ';' ) ) || ( WP_SQLite_Token::TYPE_DELIMITER === $token->type && null === $token->value ) + || $token->matches( WP_SQLite_Token::TYPE_OPERATOR, null, array( ')', ',' ) ); + + if ( $is_like_finished ) { + /* + * Here we have another keyword encountered with the LIKE in progress. + * Emit the ESCAPE clause. + */ + if ( $this->like_escape_count > 0 ) { + /* If we need the ESCAPE clause emit it. */ + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_DELIMITER ) ); + $this->rewriter->add( new WP_SQLite_Token( 'ESCAPE', WP_SQLite_Token::TYPE_KEYWORD ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_DELIMITER ) ); + $this->rewriter->add( new WP_SQLite_Token( "'" . self::LIKE_ESCAPE_CHAR . "'", WP_SQLite_Token::TYPE_STRING ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_DELIMITER ) ); + } + $this->like_escape_count = 0; + $this->like_expression_nesting = 0; + } + } + + return false; + } + + /** + * Rewrite a query from the MySQL information_schema. + * + * @param string $updated_query The query to rewrite. + * + * @return string The query for use by SQLite + */ + private function get_information_schema_query( $updated_query ) { + // @TODO: Actually rewrite the columns. + $normalized_query = preg_replace( '/\s+/', ' ', strtolower( $updated_query ) ); + if ( str_contains( $normalized_query, 'bytes' ) ) { + // Count rows per table. + $tables = + $this->execute_sqlite_query( "SELECT name as `table_name` FROM sqlite_master WHERE type='table' ORDER BY name" )->fetchAll(); + $tables = $this->strip_sqlite_system_tables( $tables ); + + $rows = '(CASE '; + foreach ( $tables as $table ) { + $table_name = $table['table_name']; + $count = $this->execute_sqlite_query( "SELECT COUNT(1) as `count` FROM $table_name" )->fetch(); + $rows .= " WHEN name = '$table_name' THEN {$count['count']} "; + } + $rows .= 'ELSE 0 END) '; + $updated_query = + "SELECT name as `table_name`, $rows as `rows`, 0 as `bytes` FROM sqlite_master WHERE type='table' ORDER BY name"; + } elseif ( str_contains( $normalized_query, 'count(*)' ) && ! str_contains( $normalized_query, 'table_name =' ) ) { + // @TODO This is a guess that the caller wants a count of tables. + $list = array(); + foreach ( $this->sqlite_system_tables as $system_table => $name ) { + $list [] = "'" . $system_table . "'"; + } + $list = implode( ', ', $list ); + $sql = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name NOT IN ($list)"; + $table_count = $this->execute_sqlite_query( $sql )->fetch(); + $updated_query = 'SELECT ' . $table_count[0] . ' AS num'; + + $this->is_information_schema_query = false; + } else { + $updated_query = + "SELECT name as `table_name`, 'myisam' as `engine`, 0 as `data_length`, 0 as `index_length`, 0 as `data_free` FROM sqlite_master WHERE type='table' ORDER BY name"; + } + + return $updated_query; + } + + /** + * Remove system table rows from resultsets of information_schema tables. + * + * @param array $tables The result set. + * + * @return array The filtered result set. + */ + private function strip_sqlite_system_tables( $tables ) { + return array_values( + array_filter( + $tables, + function ( $table ) { + $table_name = property_exists( $table, 'Name' ) ? $table->Name : $table->table_name; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + return ! array_key_exists( $table_name, $this->sqlite_system_tables ); + }, + ARRAY_FILTER_USE_BOTH + ) + ); + } + + /** + * Translate the ON DUPLICATE KEY UPDATE clause. + * + * @param string $table_name The table name. + * + * @return void + */ + private function translate_on_duplicate_key( $table_name ) { + /* + * Rewrite: + * ON DUPLICATE KEY UPDATE `option_name` = VALUES(`option_name`) + * to: + * ON CONFLICT(ip) DO UPDATE SET option_name = excluded.option_name + */ + + // Find the conflicting column. + $pk_columns = array(); + foreach ( $this->get_primary_keys( $table_name ) as $row ) { + $pk_columns[] = $row['name']; + } + + $unique_columns = array(); + foreach ( $this->get_keys( $table_name, true ) as $row ) { + foreach ( $row['columns'] as $column ) { + $unique_columns[] = $column['name']; + } + } + + // Guess the conflict column based on the query details. + + // 1. Listed INSERT columns that are either PK or UNIQUE. + $conflict_columns = array_intersect( + $this->insert_columns, + array_merge( $pk_columns, $unique_columns ) + ); + // 2. Composite Primary Key columns. + if ( ! $conflict_columns && count( $pk_columns ) > 1 ) { + $conflict_columns = $pk_columns; + } + // 3. The first unique column. + if ( ! $conflict_columns && count( $unique_columns ) > 0 ) { + $conflict_columns = array( $unique_columns[0] ); + } + // 4. Regular Primary Key column. + if ( ! $conflict_columns ) { + $conflict_columns = $pk_columns; + } + + /* + * If we still haven't found any conflict column, we + * can't rewrite the ON DUPLICATE KEY statement. + * Let's default to a regular INSERT to mimic MySQL + * which would still insert the row without throwing + * an error. + */ + if ( ! $conflict_columns ) { + // Drop the consumed "ON". + $this->rewriter->drop_last(); + // Skip over "DUPLICATE", "KEY", and "UPDATE". + $this->rewriter->skip(); + $this->rewriter->skip(); + $this->rewriter->skip(); + while ( $this->rewriter->skip() ) { + // Skip over the rest of the query. + } + return; + } + + // Skip over "DUPLICATE", "KEY", and "UPDATE". + $this->rewriter->skip(); + $this->rewriter->skip(); + $this->rewriter->skip(); + + // Add the CONFLICT keyword. + $this->rewriter->add( new WP_SQLite_Token( 'CONFLICT', WP_SQLite_Token::TYPE_KEYWORD ) ); + + // Add "( ) DO UPDATE SET ". + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + $this->rewriter->add( new WP_SQLite_Token( '(', WP_SQLite_Token::TYPE_OPERATOR ) ); + + $max = count( $conflict_columns ); + foreach ( $conflict_columns as $i => $conflict_column ) { + $this->rewriter->add( new WP_SQLite_Token( '"' . $conflict_column . '"', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ) ); + if ( $i !== $max - 1 ) { + $this->rewriter->add( new WP_SQLite_Token( ',', WP_SQLite_Token::TYPE_OPERATOR ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + } + } + $this->rewriter->add( new WP_SQLite_Token( ')', WP_SQLite_Token::TYPE_OPERATOR ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + $this->rewriter->add( new WP_SQLite_Token( 'DO', WP_SQLite_Token::TYPE_KEYWORD ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + $this->rewriter->add( new WP_SQLite_Token( 'UPDATE', WP_SQLite_Token::TYPE_KEYWORD ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + $this->rewriter->add( new WP_SQLite_Token( 'SET', WP_SQLite_Token::TYPE_KEYWORD ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + } + + /** + * Get the primary keys for a table. + * + * @param string $table_name Table name. + * + * @return array + */ + private function get_primary_keys( $table_name ) { + $stmt = $this->execute_sqlite_query( 'SELECT * FROM pragma_table_info(:table_name) as l WHERE l.pk > 0;' ); + $stmt->execute( array( 'table_name' => $table_name ) ); + return $stmt->fetchAll(); + } + + /** + * Get the keys for a table. + * + * @param string $table_name Table name. + * @param bool $only_unique Only return unique keys. + * + * @return array + */ + private function get_keys( $table_name, $only_unique = false ) { + $query = $this->execute_sqlite_query( 'SELECT * FROM pragma_index_list("' . $table_name . '") as l;' ); + $indices = $query->fetchAll(); + $results = array(); + foreach ( $indices as $index ) { + if ( ! $only_unique || '1' === $index['unique'] ) { + $query = $this->execute_sqlite_query( 'SELECT * FROM pragma_index_info("' . $index['name'] . '") as l;' ); + $results[] = array( + 'index' => $index, + 'columns' => $query->fetchAll(), + ); + } + } + return $results; + } + + /** + * Get the CREATE TABLE statement for a table. + * + * @param string $table_name Table name. + * + * @return string + */ + private function get_sqlite_create_table( $table_name ) { + $stmt = $this->execute_sqlite_query( 'SELECT sql FROM sqlite_master WHERE type="table" AND name=:table' ); + $stmt->execute( array( ':table' => $table_name ) ); + $create_table = ''; + foreach ( $stmt->fetchAll() as $row ) { + $create_table .= $row['sql'] . "\n"; + } + return $create_table; + } + + /** + * Translate ALTER query. + * + * @throws Exception If the subject is not 'table', or we're performing an unknown operation. + */ + private function execute_alter() { + $this->rewriter->consume(); + $subject = strtolower( $this->rewriter->consume()->token ); + if ( 'table' !== $subject ) { + throw new Exception( 'Unknown subject: ' . $subject ); + } + + $table_name = $this->normalize_column_name( $this->rewriter->consume()->token ); + do { + /* + * This loop may be executed multiple times if there are multiple operations in the ALTER query. + * Let's reset the initial state on each pass. + */ + $this->rewriter->replace_all( + array( + new WP_SQLite_Token( 'ALTER', WP_SQLite_Token::TYPE_KEYWORD ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'TABLE', WP_SQLite_Token::TYPE_KEYWORD ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( $table_name, WP_SQLite_Token::TYPE_KEYWORD ), + ) + ); + $op_type = strtoupper( $this->rewriter->consume()->token ); + $op_subject = strtoupper( $this->rewriter->consume()->token ); + $mysql_index_type = $this->normalize_mysql_index_type( $op_subject ); + $is_index_op = ! ! $mysql_index_type; + + if ( 'ADD' === $op_type && 'COLUMN' === $op_subject ) { + $column_name = $this->rewriter->consume()->value; + + $skip_mysql_data_type_parts = $this->skip_mysql_data_type(); + $sqlite_data_type = $skip_mysql_data_type_parts[0]; + $mysql_data_type = $skip_mysql_data_type_parts[1]; + + $this->rewriter->add( + new WP_SQLite_Token( + $sqlite_data_type, + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_DATA_TYPE + ) + ); + $this->update_data_type_cache( + $table_name, + $column_name, + $mysql_data_type + ); + } elseif ( 'DROP' === $op_type && 'COLUMN' === $op_subject ) { + $this->rewriter->consume_all(); + } elseif ( 'CHANGE' === $op_type && 'COLUMN' === $op_subject ) { + // Parse the new column definition. + $from_name = $this->normalize_column_name( $this->rewriter->skip()->token ); + $new_field = $this->parse_mysql_create_table_field(); + $alter_terminator = end( $this->rewriter->output_tokens ); + $this->update_data_type_cache( + $table_name, + $new_field->name, + $new_field->mysql_data_type + ); + + /* + * In SQLite, there is no direct equivalent to the CHANGE COLUMN + * statement from MySQL. We need to do a bit of work to emulate it. + * + * The idea is to: + * 1. Get the existing table schema. + * 2. Adjust the column definition. + * 3. Copy the data out of the old table. + * 4. Drop the old table to free up the indexes names. + * 5. Create a new table from the updated schema. + * 6. Copy the data from step 3 to the new table. + * 7. Drop the old table copy. + * 8. Restore any indexes that were dropped in step 4. + */ + + // 1. Get the existing table schema. + $old_schema = $this->get_sqlite_create_table( $table_name ); + $old_indexes = $this->get_keys( $table_name, false ); + + // 2. Adjust the column definition. + + // First, tokenize the old schema. + $tokens = ( new WP_SQLite_Lexer( $old_schema ) )->tokens; + $create_table = new WP_SQLite_Query_Rewriter( $tokens ); + + // Now, replace every reference to the old column name with the new column name. + while ( true ) { + $token = $create_table->consume(); + if ( ! $token ) { + break; + } + if ( WP_SQLite_Token::TYPE_STRING !== $token->type + || $from_name !== $this->normalize_column_name( $token->value ) ) { + continue; + } + + // We found the old column name, let's remove it. + $create_table->drop_last(); + + // If the next token is a data type, we're dealing with a column definition. + $is_column_definition = $create_table->peek()->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_DATA_TYPE + ); + if ( $is_column_definition ) { + // Skip the old field definition. + $field_depth = $create_table->depth; + do { + $field_terminator = $create_table->skip(); + } while ( + ! $this->is_create_table_field_terminator( + $field_terminator, + $field_depth, + $create_table->depth + ) + ); + + // Add an updated field definition. + $definition = $this->make_sqlite_field_definition( $new_field ); + // Technically it's not a token, but it's fine to cheat a little bit. + $create_table->add( new WP_SQLite_Token( $definition, WP_SQLite_Token::TYPE_KEYWORD ) ); + // Restore the terminating "," or ")" token. + $create_table->add( $field_terminator ); + } else { + // Otherwise, just add the new name in place of the old name we dropped. + $create_table->add( + new WP_SQLite_Token( + "`$new_field->name`", + WP_SQLite_Token::TYPE_KEYWORD + ) + ); + } + } + + // 3. Copy the data out of the old table + $cache_table_name = "_tmp__{$table_name}_" . rand( 10000000, 99999999 ); + $this->execute_sqlite_query( + "CREATE TABLE `$cache_table_name` as SELECT * FROM `$table_name`" + ); + + // 4. Drop the old table to free up the indexes names + $this->execute_sqlite_query( "DROP TABLE `$table_name`" ); + + // 5. Create a new table from the updated schema + $this->execute_sqlite_query( $create_table->get_updated_query() ); + + // 6. Copy the data from step 3 to the new table + $this->execute_sqlite_query( "INSERT INTO {$table_name} SELECT * FROM $cache_table_name" ); + + // 7. Drop the old table copy + $this->execute_sqlite_query( "DROP TABLE `$cache_table_name`" ); + + // 8. Restore any indexes that were dropped in step 4 + foreach ( $old_indexes as $row ) { + /* + * Skip indexes prefixed with sqlite_autoindex_ + * (these are automatically created by SQLite). + */ + if ( str_starts_with( $row['index']['name'], 'sqlite_autoindex_' ) ) { + continue; + } + + $columns = array(); + foreach ( $row['columns'] as $column ) { + $columns[] = ( $column['name'] === $from_name ) + ? '`' . $new_field->name . '`' + : '`' . $column['name'] . '`'; + } + + $unique = '1' === $row['index']['unique'] ? 'UNIQUE' : ''; + + /* + * Use IF NOT EXISTS to avoid collisions with indexes that were + * a part of the CREATE TABLE statement + */ + $this->execute_sqlite_query( + "CREATE $unique INDEX IF NOT EXISTS `{$row['index']['name']}` ON $table_name (" . implode( ', ', $columns ) . ')' + ); + } + + if ( ',' === $alter_terminator->token ) { + /* + * If the terminator was a comma, + * we need to continue processing the rest of the ALTER query. + */ + $comma = true; + continue; + } + // We're done. + break; + } elseif ( 'ADD' === $op_type && $is_index_op ) { + $key_name = $this->rewriter->consume()->value; + $sqlite_index_type = $this->mysql_index_type_to_sqlite_type( $mysql_index_type ); + $sqlite_index_name = "{$table_name}__$key_name"; + $this->rewriter->replace_all( + array( + new WP_SQLite_Token( 'CREATE', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( $sqlite_index_type, WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( "\"$sqlite_index_name\"", WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'ON', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( '"' . $table_name . '"', WP_SQLite_Token::TYPE_STRING, WP_SQLite_Token::FLAG_STRING_DOUBLE_QUOTES ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( '(', WP_SQLite_Token::TYPE_OPERATOR ), + ) + ); + $this->update_data_type_cache( + $table_name, + $sqlite_index_name, + $mysql_index_type + ); + + $token = $this->rewriter->consume( + array( + WP_SQLite_Token::TYPE_OPERATOR, + null, + '(', + ) + ); + $this->rewriter->drop_last(); + + // Consume all the fields, skip the sizes like `(20)` in `varchar(20)`. + while ( true ) { + $token = $this->rewriter->consume(); + if ( ! $token ) { + break; + } + // $token is field name. + if ( ! $token->matches( WP_SQLite_Token::TYPE_OPERATOR ) ) { + $token->token = '`' . $this->normalize_column_name( $token->token ) . '`'; + $token->value = '`' . $this->normalize_column_name( $token->token ) . '`'; + } + + /* + * Optionally, it may be followed by a size like `(20)`. + * Let's skip it. + */ + $paren_maybe = $this->rewriter->peek(); + if ( $paren_maybe && '(' === $paren_maybe->token ) { + $this->rewriter->skip(); + $this->rewriter->skip(); + $this->rewriter->skip(); + } + if ( ')' === $token->value ) { + break; + } + } + } elseif ( 'DROP' === $op_type && $is_index_op ) { + $key_name = $this->rewriter->consume()->value; + $this->rewriter->replace_all( + array( + new WP_SQLite_Token( 'DROP', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'INDEX', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( "\"{$table_name}__$key_name\"", WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ), + ) + ); + } else { + throw new Exception( 'Unknown operation: ' . $op_type ); + } + $comma = $this->rewriter->consume( + array( + 'type' => WP_SQLite_Token::TYPE_OPERATOR, + 'value' => ',', + ) + ); + $this->rewriter->drop_last(); + $this->execute_sqlite_query( + $this->rewriter->get_updated_query() + ); + } while ( $comma ); + + $this->results = 1; + $this->return_value = $this->results; + } + + /** + * Translates a CREATE query. + * + * @throws Exception If the query is an unknown create type. + */ + private function execute_create() { + $this->rewriter->consume(); + $what = $this->rewriter->consume()->token; + + /** + * Technically it is possible to support temporary tables as follows: + * ATTACH '' AS 'tempschema'; + * CREATE TABLE tempschema.(...)...; + * However, for now, let's just ignore the TEMPORARY keyword. + */ + if ( 'TEMPORARY' === $what ) { + $this->rewriter->drop_last(); + $what = $this->rewriter->consume()->token; + } + + switch ( $what ) { + case 'TABLE': + $this->execute_create_table(); + break; + + case 'PROCEDURE': + case 'DATABASE': + $this->results = true; + break; + + default: + throw new Exception( 'Unknown create type: ' . $what ); + } + } + + /** + * Translates a DROP query. + * + * @throws Exception If the query is an unknown drop type. + */ + private function execute_drop() { + $this->rewriter->consume(); + $what = $this->rewriter->consume()->token; + + /* + * Technically it is possible to support temporary tables as follows: + * ATTACH '' AS 'tempschema'; + * CREATE TABLE tempschema.(...)...; + * However, for now, let's just ignore the TEMPORARY keyword. + */ + if ( 'TEMPORARY' === $what ) { + $this->rewriter->drop_last(); + $what = $this->rewriter->consume()->token; + } + + switch ( $what ) { + case 'TABLE': + $this->rewriter->consume_all(); + $this->execute_sqlite_query( $this->rewriter->get_updated_query() ); + $this->results = $this->last_exec_returned; + break; + + case 'PROCEDURE': + case 'DATABASE': + $this->results = true; + return; + + default: + throw new Exception( 'Unknown drop type: ' . $what ); + } + } + + /** + * Translates a SHOW query. + * + * @throws Exception If the query is an unknown show type. + */ + private function execute_show() { + $this->rewriter->skip(); + $what1 = $this->rewriter->consume()->token; + $what2 = $this->rewriter->consume()->token; + $what = $what1 . ' ' . $what2; + switch ( $what ) { + case 'CREATE PROCEDURE': + $this->results = true; + return; + + case 'FULL COLUMNS': + $this->rewriter->consume(); + // Fall through. + case 'COLUMNS FROM': + $table_name = $this->rewriter->consume()->token; + $stmt = $this->execute_sqlite_query( + "PRAGMA table_info(\"$table_name\");" + ); + /* @todo we may need to add the Extra column if anybdy needs it. 'auto_increment' is the value */ + $name_map = array( + 'name' => 'Field', + 'type' => 'Type', + 'dflt_value' => 'Default', + 'cid' => null, + 'notnull' => null, + 'pk' => null, + ); + $columns = $stmt->fetchAll( $this->pdo_fetch_mode ); + $columns = array_map( + function ( $row ) use ( $name_map ) { + $new = array(); + $is_object = is_object( $row ); + $row = $is_object ? (array) $row : $row; + foreach ( $row as $k => $v ) { + $k = array_key_exists( $k, $name_map ) ? $name_map [ $k ] : $k; + if ( $k ) { + $new[ $k ] = $v; + } + } + if ( array_key_exists( 'notnull', $row ) ) { + $new['Null'] = ( '1' === $row ['notnull'] ) ? 'NO' : 'YES'; + } + if ( array_key_exists( 'pk', $row ) ) { + $new['Key'] = ( '1' === $row ['pk'] ) ? 'PRI' : ''; + } + return $is_object ? (object) $new : $new; + }, + $columns + ); + $this->set_results_from_fetched_data( $columns ); + return; + + case 'INDEX FROM': + $table_name = $this->rewriter->consume()->token; + $results = array(); + + foreach ( $this->get_primary_keys( $table_name ) as $row ) { + $results[] = array( + 'Table' => $table_name, + 'Non_unique' => '0', + 'Key_name' => 'PRIMARY', + 'Column_name' => $row['name'], + ); + } + foreach ( $this->get_keys( $table_name ) as $row ) { + foreach ( $row['columns'] as $k => $column ) { + $results[] = array( + 'Table' => $table_name, + 'Non_unique' => '1' === $row['index']['unique'] ? '0' : '1', + 'Key_name' => $row['index']['name'], + 'Column_name' => $column['name'], + ); + } + } + for ( $i = 0;$i < count( $results );$i++ ) { + $sqlite_key_name = $results[ $i ]['Key_name']; + $mysql_key_name = $sqlite_key_name; + + /* + * SQLite automatically assigns names to some indexes. + * However, dbDelta in WordPress expects the name to be + * the same as in the original CREATE TABLE. Let's + * translate the name back. + */ + if ( str_starts_with( $mysql_key_name, 'sqlite_autoindex_' ) ) { + $mysql_key_name = substr( $mysql_key_name, strlen( 'sqlite_autoindex_' ) ); + $mysql_key_name = preg_replace( '/_[0-9]+$/', '', $mysql_key_name ); + } + if ( str_starts_with( $mysql_key_name, "{$table_name}__" ) ) { + $mysql_key_name = substr( $mysql_key_name, strlen( "{$table_name}__" ) ); + } + + $mysql_type = $this->get_cached_mysql_data_type( $table_name, $sqlite_key_name ); + if ( 'FULLTEXT' !== $mysql_type && 'SPATIAL' !== $mysql_type ) { + $mysql_type = 'BTREE'; + } + + $results[ $i ] = (object) array_merge( + $results[ $i ], + array( + 'Seq_in_index' => 0, + 'Key_name' => $mysql_key_name, + 'Index_type' => $mysql_type, + + /* + * Many of these details are not available in SQLite, + * so we just shim them with dummy values. + */ + 'Collation' => 'A', + 'Cardinality' => '0', + 'Sub_part' => null, + 'Packed' => null, + 'Null' => '', + 'Comment' => '', + 'Index_comment' => '', + ) + ); + } + $this->set_results_from_fetched_data( + $results + ); + + return; + + case 'TABLE STATUS': // FROM `database`. + $this->rewriter->skip(); + $database_expression = $this->rewriter->skip(); + $stmt = $this->execute_sqlite_query( + "SELECT name as `Name`, 'myisam' as `Engine`, 0 as `Data_length`, 0 as `Index_length`, 0 as `Data_free` FROM sqlite_master WHERE type='table' ORDER BY name" + ); + + $tables = $this->strip_sqlite_system_tables( $stmt->fetchAll( $this->pdo_fetch_mode ) ); + foreach ( $tables as $table ) { + $table_name = $table->Name; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $stmt = $this->execute_sqlite_query( "SELECT COUNT(1) as `Rows` FROM $table_name" ); + $rows = $stmt->fetchall( $this->pdo_fetch_mode ); + $table->Rows = $rows[0]->Rows; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + } + + $this->set_results_from_fetched_data( + $this->strip_sqlite_system_tables( $tables ) + ); + + return; + + case 'TABLES LIKE': + $table_expression = $this->rewriter->skip(); + $stmt = $this->execute_sqlite_query( + "SELECT `name` as `Tables_in_db` FROM `sqlite_master` WHERE `type`='table' AND `name` LIKE :param;", + array( + ':param' => $table_expression->value, + ) + ); + $this->set_results_from_fetched_data( + $stmt->fetchAll( $this->pdo_fetch_mode ) + ); + return; + + default: + switch ( $what1 ) { + case 'TABLES': + $stmt = $this->execute_sqlite_query( + "SELECT name FROM sqlite_master WHERE type='table'" + ); + $this->set_results_from_fetched_data( + $stmt->fetchAll( $this->pdo_fetch_mode ) + ); + return; + + case 'VARIABLE': + case 'VARIABLES': + $this->results = true; + return; + + default: + throw new Exception( 'Unknown show type: ' . $what ); + } + } + } + + /** + * Consumes data types from the query. + * + * @throws Exception If the data type cannot be translated. + * + * @return array The data types. + */ + private function skip_mysql_data_type() { + $type = $this->rewriter->skip(); + if ( ! $type->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_DATA_TYPE + ) ) { + throw new Exception( 'Data type expected in MySQL query, unknown token received: ' . $type->value ); + } + + $mysql_data_type = strtolower( $type->value ); + if ( ! isset( $this->field_types_translation[ $mysql_data_type ] ) ) { + throw new Exception( 'MySQL field type cannot be translated to SQLite: ' . $mysql_data_type ); + } + + $sqlite_data_type = $this->field_types_translation[ $mysql_data_type ]; + + // Skip the length, e.g. (10) in VARCHAR(10). + $paren_maybe = $this->rewriter->peek(); + if ( $paren_maybe && '(' === $paren_maybe->token ) { + $mysql_data_type .= $this->rewriter->skip()->token; + $mysql_data_type .= $this->rewriter->skip()->token; + $mysql_data_type .= $this->rewriter->skip()->token; + } + + // Skip the int keyword. + $int_maybe = $this->rewriter->peek(); + if ( $int_maybe && $int_maybe->matches( + WP_SQLite_Token::TYPE_KEYWORD, + null, + array( 'UNSIGNED' ) + ) + ) { + $mysql_data_type .= ' ' . $this->rewriter->skip()->token; + } + return array( + $sqlite_data_type, + $mysql_data_type, + ); + } + + /** + * Updates the data type cache. + * + * @param string $table The table name. + * @param string $column_or_index The column or index name. + * @param string $mysql_data_type The MySQL data type. + * + * @return void + */ + private function update_data_type_cache( $table, $column_or_index, $mysql_data_type ) { + $this->execute_sqlite_query( + 'INSERT INTO ' . self::DATA_TYPES_CACHE_TABLE . ' (`table`, `column_or_index`, `mysql_type`) + VALUES (:table, :column, :datatype) + ON CONFLICT(`table`, `column_or_index`) DO UPDATE SET `mysql_type` = :datatype + ', + array( + ':table' => $table, + ':column' => $column_or_index, + ':datatype' => $mysql_data_type, + ) + ); + } + + /** + * Gets the cached MySQL data type. + * + * @param string $table The table name. + * @param string $column_or_index The column or index name. + * + * @return string The MySQL data type. + */ + private function get_cached_mysql_data_type( $table, $column_or_index ) { + $stmt = $this->execute_sqlite_query( + 'SELECT d.`mysql_type` FROM ' . self::DATA_TYPES_CACHE_TABLE . ' d + WHERE `table`=:table + AND `column_or_index` = :index', + array( + ':table' => $table, + ':index' => $column_or_index, + ) + ); + $mysql_type = $stmt->fetchColumn( 0 ); + if ( str_ends_with( $mysql_type, ' KEY' ) ) { + $mysql_type = substr( $mysql_type, 0, strlen( $mysql_type ) - strlen( ' KEY' ) ); + } + return $mysql_type; + } + + /** + * Normalizes a column name. + * + * @param string $column_name The column name. + * + * @return string The normalized column name. + */ + private function normalize_column_name( $column_name ) { + return trim( $column_name, '`\'"' ); + } + + /** + * Normalizes an index type. + * + * @param string $index_type The index type. + * + * @return string|null The normalized index type, or null if the index type is not supported. + */ + private function normalize_mysql_index_type( $index_type ) { + $index_type = strtoupper( $index_type ); + $index_type = preg_replace( '/INDEX$/', 'KEY', $index_type ); + $index_type = preg_replace( '/ KEY$/', '', $index_type ); + if ( + 'KEY' === $index_type + || 'PRIMARY' === $index_type + || 'UNIQUE' === $index_type + || 'FULLTEXT' === $index_type + || 'SPATIAL' === $index_type + ) { + return $index_type; + } + return null; + } + + /** + * Converts an index type to a SQLite index type. + * + * @param string|null $normalized_mysql_index_type The normalized index type. + * + * @return string|null The SQLite index type, or null if the index type is not supported. + */ + private function mysql_index_type_to_sqlite_type( $normalized_mysql_index_type ) { + if ( null === $normalized_mysql_index_type ) { + return null; + } + if ( 'PRIMARY' === $normalized_mysql_index_type ) { + return 'PRIMARY KEY'; + } + if ( 'UNIQUE' === $normalized_mysql_index_type ) { + return 'UNIQUE INDEX'; + } + return 'INDEX'; + } + + /** + * Executes a CHECK statement. + */ + private function execute_check() { + $this->rewriter->skip(); // CHECK. + $this->rewriter->skip(); // TABLE. + $table_name = $this->rewriter->consume()->value; // Τable_name. + + $tables = + $this->execute_sqlite_query( + "SELECT name as `table_name` FROM sqlite_master WHERE type='table' AND name = :table_name ORDER BY name", + array( $table_name ) + )->fetchAll(); + + if ( is_array( $tables ) && 1 === count( $tables ) && $table_name === $tables[0]['table_name'] ) { + + $this->set_results_from_fetched_data( + array( + (object) array( + 'Table' => $table_name, + 'Op' => 'check', + 'Msg_type' => 'status', + 'Msg_text' => 'OK', + ), + ) + ); + } else { + + $this->set_results_from_fetched_data( + array( + (object) array( + 'Table' => $table_name, + 'Op' => 'check', + 'Msg_type' => 'Error', + 'Msg_text' => "Table '$table_name' doesn't exist", + ), + (object) array( + 'Table' => $table_name, + 'Op' => 'check', + 'Msg_type' => 'status', + 'Msg_text' => 'Operation failed', + ), + ) + ); + } + } + + /** + * Handle an OPTIMIZE / REPAIR / ANALYZE TABLE statement, by using VACUUM just once, at shutdown. + * + * @param string $query_type The query type. + */ + private function execute_optimize( $query_type ) { + // OPTIMIZE TABLE tablename. + $this->rewriter->skip(); + $this->rewriter->skip(); + $table_name = $this->rewriter->skip()->value; + $status = ''; + + if ( ! $this->vacuum_requested ) { + $this->vacuum_requested = true; + if ( function_exists( 'add_action' ) ) { + $status = "SQLite does not support $query_type, doing VACUUM instead"; + add_action( + 'shutdown', + function () { + $this->execute_sqlite_query( 'VACUUM' ); + } + ); + } else { + /* add_action isn't available in the unit test environment, and we're deep in a transaction. */ + $status = "SQLite unit testing does not support $query_type."; + } + } + $resultset = array( + (object) array( + 'Table' => $table_name, + 'Op' => strtolower( $query_type ), + 'Msg_type' => 'note', + 'Msg_text' => $status, + ), + (object) array( + 'Table' => $table_name, + 'Op' => strtolower( $query_type ), + 'Msg_type' => 'status', + 'Msg_text' => 'OK', + ), + ); + + $this->set_results_from_fetched_data( $resultset ); + } + + /** + * Error handler. + * + * @param Exception $err Exception object. + * + * @return bool Always false. + */ + private function handle_error( Exception $err ) { + $message = $err->getMessage(); + $this->set_error( __LINE__, __FUNCTION__, $message ); + $this->return_value = false; + return false; + } + + /** + * Method to format the error messages and put out to the file. + * + * When $wpdb::suppress_errors is set to true or $wpdb::show_errors is set to false, + * the error messages are ignored. + * + * @param string $line Where the error occurred. + * @param string $function Indicate the function name where the error occurred. + * @param string $message The message. + * + * @return boolean|void + */ + private function set_error( $line, $function, $message ) { + $this->errors[] = array( + 'line' => $line, + 'function' => $function, + ); + $this->error_messages[] = $message; + $this->is_error = true; + } + + /** + * PDO has no explicit close() method. + * + * This is because PHP may choose to reuse the same + * connection for the next request. The PHP manual + * states the PDO object can only be unset: + * + * https://www.php.net/manual/en/pdo.connections.php#114822 + */ + public function close() { + $this->pdo = null; + } + + /** + * Method to return error messages. + * + * @throws Exception If error is found. + * + * @return string + */ + public function get_error_message() { + if ( count( $this->error_messages ) === 0 ) { + $this->is_error = false; + $this->error_messages = array(); + return ''; + } + + if ( false === $this->is_error ) { + return ''; + } + + $output = '
 
' . PHP_EOL; + $output .= '
' . PHP_EOL; + $output .= '

MySQL query:

' . PHP_EOL; + $output .= '

' . $this->mysql_query . '

' . PHP_EOL; + $output .= '

Queries made or created this session were:

' . PHP_EOL; + $output .= '
    ' . PHP_EOL; + foreach ( $this->executed_sqlite_queries as $q ) { + $message = "Executing: {$q['sql']} | " . ( $q['params'] ? 'parameters: ' . implode( ', ', $q['params'] ) : '(no parameters)' ); + + $output .= '
  1. ' . htmlspecialchars( $message ) . '
  2. ' . PHP_EOL; + } + $output .= '
' . PHP_EOL; + $output .= '
' . PHP_EOL; + foreach ( $this->error_messages as $num => $m ) { + $output .= '
' . PHP_EOL; + $output .= sprintf( + 'Error occurred at line %1$d in Function %2$s. Error message was: %3$s.', + (int) $this->errors[ $num ]['line'], + '' . htmlspecialchars( $this->errors[ $num ]['function'] ) . '', + $m + ) . PHP_EOL; + $output .= '
' . PHP_EOL; + } + + try { + throw new Exception(); + } catch ( Exception $e ) { + $output .= '

Backtrace:

' . PHP_EOL; + $output .= '
' . $e->getTraceAsString() . '
' . PHP_EOL; + } + + return $output; + } + + /** + * Executes a query in SQLite. + * + * @param mixed $sql The query to execute. + * @param mixed $params The parameters to bind to the query. + * @throws PDOException If the query could not be executed. + * @return object { + * The result of the query. + * + * @type PDOStatement $stmt The executed statement + * @type * $result The value returned by $stmt. + * } + */ + public function execute_sqlite_query( $sql, $params = array() ) { + $this->executed_sqlite_queries[] = array( + 'sql' => $sql, + 'params' => $params, + ); + + $stmt = $this->pdo->prepare( $sql ); + if ( false === $stmt || null === $stmt ) { + $this->last_exec_returned = null; + $info = $this->pdo->errorInfo(); + $this->last_sqlite_error = $info[0] . ' ' . $info[2]; + throw new PDOException( implode( ' ', array( 'Error:', $info[0], $info[2], 'SQLite:', $sql ) ), $info[1] ); + } + $returned = $stmt->execute( $params ); + $this->last_exec_returned = $returned; + if ( ! $returned ) { + $info = $stmt->errorInfo(); + $this->last_sqlite_error = $info[0] . ' ' . $info[2]; + throw new PDOException( implode( ' ', array( 'Error:', $info[0], $info[2], 'SQLite:', $sql ) ), $info[1] ); + } + + return $stmt; + } + + /** + * Method to set the results from the fetched data. + * + * @param array $data The data to set. + */ + private function set_results_from_fetched_data( $data ) { + if ( null === $this->results ) { + $this->results = $data; + } + if ( is_array( $this->results ) ) { + $this->num_rows = count( $this->results ); + $this->last_select_found_rows = count( $this->results ); + } + $this->return_value = $this->results; + } + + /** + * Method to set the results from the affected rows. + * + * @param int|null $override Override the affected rows. + */ + private function set_result_from_affected_rows( $override = null ) { + /* + * SELECT CHANGES() is a workaround for the fact that + * $stmt->rowCount() returns "0" (zero) with the + * SQLite driver at all times. + * Source: https://www.php.net/manual/en/pdostatement.rowcount.php + */ + if ( null === $override ) { + $this->affected_rows = (int) $this->execute_sqlite_query( 'select changes()' )->fetch()[0]; + } else { + $this->affected_rows = $override; + } + $this->return_value = $this->affected_rows; + $this->num_rows = $this->affected_rows; + $this->results = $this->affected_rows; + } + + /** + * Method to clear previous data. + */ + private function flush() { + $this->mysql_query = ''; + $this->results = null; + $this->last_exec_returned = null; + $this->last_insert_id = null; + $this->affected_rows = null; + $this->column_data = array(); + $this->num_rows = null; + $this->return_value = null; + $this->error_messages = array(); + $this->is_error = false; + $this->executed_sqlite_queries = array(); + $this->like_expression_nesting = 0; + $this->like_escape_count = 0; + $this->is_information_schema_query = false; + $this->has_group_by = false; + } + + /** + * Begin a new transaction or nested transaction. + * + * @return boolean + */ + public function begin_transaction() { + $success = false; + try { + if ( 0 === $this->transaction_level ) { + $this->execute_sqlite_query( 'BEGIN' ); + } else { + $this->execute_sqlite_query( 'SAVEPOINT LEVEL' . $this->transaction_level ); + } + $success = $this->last_exec_returned; + } finally { + if ( $success ) { + ++$this->transaction_level; + } + } + return $success; + } + + /** + * Commit the current transaction or nested transaction. + * + * @return boolean True on success, false on failure. + */ + public function commit() { + if ( 0 === $this->transaction_level ) { + return false; + } + + --$this->transaction_level; + if ( 0 === $this->transaction_level ) { + $this->execute_sqlite_query( 'COMMIT' ); + } else { + $this->execute_sqlite_query( 'RELEASE SAVEPOINT LEVEL' . $this->transaction_level ); + } + return $this->last_exec_returned; + } + + /** + * Rollback the current transaction or nested transaction. + * + * @return boolean True on success, false on failure. + */ + public function rollback() { + if ( 0 === $this->transaction_level ) { + return false; + } + + --$this->transaction_level; + if ( 0 === $this->transaction_level ) { + $this->execute_sqlite_query( 'ROLLBACK' ); + } else { + $this->execute_sqlite_query( 'ROLLBACK TO SAVEPOINT LEVEL' . $this->transaction_level ); + } + return $this->last_exec_returned; + } +} diff --git a/modules/database/sqlite/wp-includes/sqlite/db.php b/modules/database/sqlite/wp-includes/sqlite/db.php index 4480b9155a..04242628dd 100644 --- a/modules/database/sqlite/wp-includes/sqlite/db.php +++ b/modules/database/sqlite/wp-includes/sqlite/db.php @@ -9,8 +9,8 @@ // Require the constants file. require_once dirname( dirname( __DIR__ ) ) . '/constants.php'; -// Bail early if DATABASE_TYPE is not defined as sqlite. -if ( ! defined( 'DATABASE_TYPE' ) || 'sqlite' !== DATABASE_TYPE ) { +// Bail early if DB_ENGINE is not defined as sqlite. +if ( ! defined( 'DB_ENGINE' ) || 'sqlite' !== DB_ENGINE ) { return; } @@ -42,13 +42,12 @@ ); } -require_once __DIR__ . '/class-perflab-sqlite-pdo-user-defined-functions.php'; -require_once __DIR__ . '/class-perflab-sqlite-pdo-engine.php'; -require_once __DIR__ . '/class-perflab-sqlite-object-array.php'; -require_once __DIR__ . '/class-perflab-sqlite-db.php'; -require_once __DIR__ . '/class-perflab-sqlite-pdo-driver.php'; -require_once __DIR__ . '/class-perflab-sqlite-create-query.php'; -require_once __DIR__ . '/class-perflab-sqlite-alter-query.php'; +require_once __DIR__ . '/class-wp-sqlite-lexer.php'; +require_once __DIR__ . '/class-wp-sqlite-query-rewriter.php'; +require_once __DIR__ . '/class-wp-sqlite-translator.php'; +require_once __DIR__ . '/class-wp-sqlite-token.php'; +require_once __DIR__ . '/class-wp-sqlite-pdo-user-defined-functions.php'; +require_once __DIR__ . '/class-wp-sqlite-db.php'; require_once __DIR__ . '/install-functions.php'; -$GLOBALS['wpdb'] = new Perflab_SQLite_DB(); +$GLOBALS['wpdb'] = new WP_SQLite_DB(); diff --git a/modules/database/sqlite/wp-includes/sqlite/install-functions.php b/modules/database/sqlite/wp-includes/sqlite/install-functions.php index e7442ffab4..f541e3794f 100644 --- a/modules/database/sqlite/wp-includes/sqlite/install-functions.php +++ b/modules/database/sqlite/wp-includes/sqlite/install-functions.php @@ -14,14 +14,14 @@ * @since 1.8.0 * * @return boolean + * + * @throws PDOException If the database connection fails. */ -function perflab_sqlite_make_db_sqlite() { +function sqlite_make_db_sqlite() { include_once ABSPATH . 'wp-admin/includes/schema.php'; - $index_array = array(); $table_schemas = wp_get_db_schema(); $queries = explode( ';', $table_schemas ); - $query_parser = new Perflab_SQLite_Create_Query(); try { $pdo = new PDO( 'sqlite:' . FQDB, null, null, array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ) ); // phpcs:ignore WordPress.DB.RestrictedClasses } catch ( PDOException $err ) { @@ -31,66 +31,36 @@ function perflab_sqlite_make_db_sqlite() { wp_die( $message, 'Database Error!' ); } + $translator = new WP_SQLite_Translator( $pdo, $GLOBALS['table_prefix'] ); + $query = null; + try { - $pdo->beginTransaction(); + $translator->begin_transaction(); foreach ( $queries as $query ) { $query = trim( $query ); if ( empty( $query ) ) { continue; } - $rewritten_query = $query_parser->rewrite_query( $query ); - if ( is_array( $rewritten_query ) ) { - $table_query = array_shift( $rewritten_query ); - $index_queries = $rewritten_query; - $table_query = trim( $table_query ); - $pdo->exec( $table_query ); - } else { - $rewritten_query = trim( $rewritten_query ); - $pdo->exec( $rewritten_query ); - } - } - $pdo->commit(); - if ( $index_queries ) { - // $query_parser rewrites KEY to INDEX, so we don't need KEY pattern. - $pattern = '/CREATE\\s*(UNIQUE\\s*INDEX|INDEX)\\s*IF\\s*NOT\\s*EXISTS\\s*(\\w+)?\\s*.*/im'; - $pdo->beginTransaction(); - foreach ( $index_queries as $index_query ) { - preg_match( $pattern, $index_query, $match ); - $index_name = trim( $match[2] ); - if ( in_array( $index_name, $index_array, true ) ) { - $r = rand( 0, 50 ); - $replacement = $index_name . "_$r"; - $index_query = str_ireplace( - 'EXISTS ' . $index_name, - 'EXISTS ' . $replacement, - $index_query - ); - } else { - $index_array[] = $index_name; - } - $pdo->exec( $index_query ); + + $result = $translator->query( $query ); + if ( false === $result ) { + throw new PDOException( $translator->get_error_message() ); } - $pdo->commit(); } + $translator->commit(); } catch ( PDOException $err ) { $err_data = $err->errorInfo; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $err_code = $err_data[1]; - if ( 5 == $err_code || 6 == $err_code ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison - // If the database is locked, commit again. - $pdo->commit(); - } else { - $pdo->rollBack(); - $message = sprintf( - 'Error occurred while creating tables or indexes...
Query was: %s
', - var_export( $rewritten_query, true ) - ); - $message .= sprintf( 'Error message is: %s', $err_data[2] ); - wp_die( $message, 'Database Error!' ); - } + $translator->rollback(); + $message = sprintf( + 'Error occurred while creating tables or indexes...
Query was: %s
', + var_export( $query, true ) + ); + $message .= sprintf( 'Error message is: %s', $err_data[2] ); + wp_die( $message, 'Database Error!' ); } - $query_parser = null; - $pdo = null; + $pdo = null; return true; } @@ -126,8 +96,8 @@ function wp_install( $blog_title, $user_name, $user_email, $is_public, $deprecat wp_check_mysql_version(); wp_cache_flush(); - /* SQLite changes: Replace the call to make_db_current_silent() with perflab_sqlite_make_db_sqlite(). */ - perflab_sqlite_make_db_sqlite(); + /* SQLite changes: Replace the call to make_db_current_silent() with sqlite_make_db_sqlite(). */ + sqlite_make_db_sqlite(); // phpcs:ignore PHPCompatibility.Extensions.RemovedExtensions.sqliteRemoved populate_options(); populate_roles();