From 1651902a971a44bb211db92ae3bcbf2f179623ef Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Fri, 2 Sep 2022 14:42:20 +0300 Subject: [PATCH 01/36] copy & tweak the SQLite implementation --- src/wp-includes/load.php | 4 + .../sqlite/class-wp-pdo-engine.php | 1463 +++++++++++++++++ .../sqlite/class-wp-pdo-sqlite-driver.php | 791 +++++++++ ...s-wp-pdo-sqlite-user-defined-functions.php | 794 +++++++++ .../sqlite/class-wp-sqlite-alter-query.php | 609 +++++++ .../sqlite/class-wp-sqlite-create-query.php | 488 ++++++ src/wp-includes/sqlite/class-wp-sqlite-db.php | 315 ++++ .../sqlite/class-wp-sqlite-object-array.php | 26 + src/wp-includes/sqlite/db.php | 289 ++++ 9 files changed, 4779 insertions(+) create mode 100644 src/wp-includes/sqlite/class-wp-pdo-engine.php create mode 100644 src/wp-includes/sqlite/class-wp-pdo-sqlite-driver.php create mode 100644 src/wp-includes/sqlite/class-wp-pdo-sqlite-user-defined-functions.php create mode 100644 src/wp-includes/sqlite/class-wp-sqlite-alter-query.php create mode 100644 src/wp-includes/sqlite/class-wp-sqlite-create-query.php create mode 100644 src/wp-includes/sqlite/class-wp-sqlite-db.php create mode 100644 src/wp-includes/sqlite/class-wp-sqlite-object-array.php create mode 100644 src/wp-includes/sqlite/db.php diff --git a/src/wp-includes/load.php b/src/wp-includes/load.php index 520902cdd64ba..fe1e837060015 100644 --- a/src/wp-includes/load.php +++ b/src/wp-includes/load.php @@ -667,6 +667,10 @@ function require_wp_db() { require_once ABSPATH . WPINC . '/class-wpdb.php'; + if ( defined( 'USE_SQLITE' ) && USE_SQLITE ) { + require_once ABSPATH . WPINC . '/sqlite/db.php'; + } + if ( file_exists( WP_CONTENT_DIR . '/db.php' ) ) { require_once WP_CONTENT_DIR . '/db.php'; } diff --git a/src/wp-includes/sqlite/class-wp-pdo-engine.php b/src/wp-includes/sqlite/class-wp-pdo-engine.php new file mode 100644 index 0000000000000..c3b033607bcdf --- /dev/null +++ b/src/wp-includes/sqlite/class-wp-pdo-engine.php @@ -0,0 +1,1463 @@ +prepare_directory(); + } + $dsn = 'sqlite:' . FQDB; + if ( isset( $GLOBALS['@pdo'] ) ) { + $this->pdo = $GLOBALS['@pdo']; + } else { + $locked = false; + $status = 0; + do { + try { + $this->pdo = new PDO( $dsn, null, null, array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ) ); + new WP_PDO_SQLite_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 = 'Database initialization error!
' . + 'Code: ' . $status . + ( isset( $err_message ) ? '
Error Message: ' . $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'] ) + ); + file_put_contents( FQDBDIR . 'mem_debug.txt', $message, FILE_APPEND ); + + return true; + } + if ( stripos( $max, 'M' ) !== false ) { + $max = (int) $max * 1024 * 1024; + } + $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 + ); + file_put_contents( FQDBDIR . 'mem_debug.txt', $message, FILE_APPEND ); + } + } + + //$this->pdo = null; + 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( $this->get_sqlite_version(), '3.7.11', '>=' ) ) { + $this->can_insert_multiple_rows = true; + } + $statement = $this->pdo->query( 'PRAGMA foreign_keys' ); + if ( $statement->fetchColumn( 0 ) == '0' ) { + $this->pdo->query( 'PRAGMA foreign_keys = ON' ); + } + } + + /** + * This method makes database direcotry 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.'; + wp_die( $message, '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 ); + $message = 'Unable to create a file in the directory! Please check your server settings.'; + 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.'; + 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 + * @param array $fetch_mode_args + * + * @return mixed according to the query type + * @see PDO::query() + */ + public function query( $statement, $mode = PDO::ATTR_DEFAULT_FETCH_MODE, ...$fetch_mode_args ) { + $this->flush(); + + $this->queries[] = "Raw query:\n$statement"; + $res = $this->determine_query_type( $statement ); + if ( ! $res && defined( 'PDO_DEBUG' ) && PDO_DEBUG ) { + $bailoutString = sprintf( + __( + '

Unknown query type

Sorry, we cannot determine the type of query that is requested.

The query is %s

', + 'sqlite-integration' + ), + $statement + ); + $this->set_error( __LINE__, __FUNCTION__, $bailoutString ); + } + 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; + //foreach ($this->found_rows_result[0] as $key => $value) { + //$_column['FOUND_ROWS()'] = $value; + //} + $column[] = new WP_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 ); + } else { + $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; + $pattern = '/^\\s*(DROP\\s*INDEX\\s*.*?)\\s*ON\\s*(.*)/im'; + if ( preg_match( $pattern, $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 ); + } else { + // Error + } + break; + } + if ( defined( 'PDO_DEBUG' ) && PDO_DEBUG === true ) { + file_put_contents( FQDBDIR . 'debug.txt', $this->get_debug_info(), FILE_APPEND ); + } + + 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 ) ) { + $data['primary_key'] = 1; + } elseif ( in_array( $key, $unique_key ) ) { + $data['unique_key'] = 1; + } else { + $data['multiple_key'] = 1; + } + $this->column_data[] = new WP_SQLite_Object_Array( $data ); + $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 ''; + } + $output = '
 
'; + if ( false === $this->is_error ) { + //return $output; + return ''; + } + $output .= "
Queries made or created this session were
\r\n\t
    \r\n"; + foreach ( $this->queries as $q ) { + $output .= "\t\t
  1. " . $q . "
  2. \r\n"; + } + $output .= "\t
\r\n
"; + foreach ( $this->error_messages as $num => $m ) { + $output .= "
Error occurred at line {$this->errors[$num]['line']} in Function {$this->errors[$num]['function']}.
Error message was: $m
"; + } + + 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 programatically. + * Needs to be fixed some other way. + * + * @param string $query_type + * + * @return object reference to apropreate driver + */ + private function prepare_engine( $query_type = null ) { + if ( stripos( $query_type, 'create' ) !== false ) { + $engine = new WP_SQLite_Create_Query(); + } elseif ( stripos( $query_type, 'alter' ) !== false ) { + $engine = new WP_SQLite_Alter_Query(); + } else { + $engine = new WP_PDO_SQLite_Driver(); + } + + return $engine; + } + + /** + * 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 of PDO statement + * + * @return boolean + */ + 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 ); + } 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 ); + } + if ( $reason > 0 ) { + $err_message = sprintf( 'Error while executing query! Error message was: %s', $message ); + $this->set_error( __LINE__, __FUNCTION__, $err_message ); + + return false; + } else { + $this->_results = $statement->fetchAll( PDO::FETCH_OBJ ); + } + //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': + //case "foundrows": + $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 + } else { + 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 + * + * @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( "'", '"' ) ) ) { + $param = substr( $param, 0, -1 );//end + } + if ( in_array( $param[0], array( "'", '"' ) ) ) { + $param = substr( $param, 1 ); //start + } + //$this->extracted_variables[] = $param; + $key = ':param_' . $this->param_num++; + $this->extracted_variables[] = $param; + //return the placeholder + //return ' ? '; + 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 + * + * @return boolean|string + */ + 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 + */ + 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 + */ + 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 + * + * @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 = ''; + //$queries = explode(";", $this->rewritten_query); + try { + $this->beginTransaction(); + 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 ); + } + $this->commit(); + } catch ( PDOException $err ) { + $reason = $err->getCode(); + $message = $err->getMessage(); + if ( 5 == $reason || 6 == $reason ) { + $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 + * + * @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 ) { + $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 + * + * @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 WP_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 return dummy data. + * + * @param string + * + * @return bool + */ + private function show_status_workaround( $query ) { + $pattern = '/^SHOW\\s*TABLE\\s*STATUS\\s*LIKE\\s*(.*?)$/im'; + if ( preg_match( $pattern, $query, $match ) ) { + $table_name = str_replace( "'", '', $match[1] ); + } else { + $table_name = ''; + } + $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 WP_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 + */ + private function process_results( $engine ) { + if ( in_array( $this->query_type, array( 'describe', 'desc', 'showcolumns' ) ) ) { + $this->convert_to_columns_object(); + } elseif ( 'showindex' === $this->query_type ) { + $this->convert_to_index_object(); + } elseif ( in_array( $this->query_type, array( 'check', 'analyze' ) ) ) { + $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 to indicate the function name where the error occurred. + * @param string $message + * + * @return boolean + */ + 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 ) { + return false; + } + if ( ! $wpdb->show_errors ) { + return false; + } + file_put_contents( FQDBDIR . 'debug.txt', "Line $line, Function: $function, Message: $message \n", FILE_APPEND ); + } + + /** + * 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 WP_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 ) { + $_columns['Field'] = $row->name; + $_columns['Type'] = $row->type; + $_columns['Null'] = $row->notnull ? 'NO' : 'YES'; + $_columns['Key'] = $row->pk ? 'PRI' : ''; + $_columns['Default'] = $row->dflt_value; + $_results[] = new WP_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 ( count( $this->_results ) == 0 ) { + 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 WP_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 = preg_replace( "/[\';]/", '', $value ); + $value = trim( $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() { + $results = array(); + $_columns = array( + 'Table' => '', + 'Op' => 'analyze', + 'Msg_type' => 'status', + 'Msg_text' => 'Table is already up to date', + ); + if ( 'check' === $this->query_type ) { + $_columns = array( + 'Table' => '', + 'Op' => 'check', + 'Msg_type' => 'status', + 'Msg_text' => 'OK', + ); + } + $_results[] = new WP_SQLite_Object_Array( $_columns ); + $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 ); + + 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() + */ + public function commit() { + $this->pdo->commit(); + $this->has_active_transaction = false; + } + + /** + * Method to call PDO::rollBack(). + * + * @see PDO::rollBack() + */ + public function rollBack() { + $this->pdo->rollBack(); + $this->has_active_transaction = false; + } +} diff --git a/src/wp-includes/sqlite/class-wp-pdo-sqlite-driver.php b/src/wp-includes/sqlite/class-wp-pdo-sqlite-driver.php new file mode 100644 index 0000000000000..f905f17584c58 --- /dev/null +++ b/src/wp-includes/sqlite/class-wp-pdo-sqlite-driver.php @@ -0,0 +1,791 @@ +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; + } else { + $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 ); + //$unlimited_query = preg_replace('/\\bGROUP\\s*BY\\s*.*/imsx', '', $unlimited_query); + // we no longer use SELECT COUNT query + //$unlimited_query = $this->_transform_to_count($unlimited_query); + $_wpdb = new WP_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 WP_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 WP_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 WP_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 WP_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... + * + * @return void + * @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. + * + * @return void + * @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) + * + * @return void + * @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 + * + * @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 handl 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. + * + * @return void + * @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 WP_SQLite_DB(); + $indexes = $_wpdb->get_results( "SHOW INDEX FROM {$table_name}" ); + if ( ! empty( $indexes ) ) { + foreach ( $indexes as $index ) { + if ( 0 == $index->Non_unique ) { + $unique_keys_for_cond[] = $index->Column_name; + if ( strpos( $index->Column_name, ',' ) !== false ) { + $unique_keys_for_check = array_merge( + $unique_keys_for_check, + explode( ',', $index->Column_name ) + ); + } else { + $unique_keys_for_check[] = $index->Column_name; + } + } + } + $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 ] ) ) { + $condition .= $col . '=' . $ins_data_assoc[ $col ] . ' OR '; + } else { + continue; + } + } + } + $condition = rtrim( $condition, ' OR ' ); + $test_query = "SELECT * FROM {$table_name} WHERE {$condition}"; + $results = $_wpdb->query( $test_query ); + $_wpdb = null; + if ( 0 == $results ) { + $this->_query = "INSERT INTO $table_name $insert_data"; + + return; + } else { + $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 ) ) { + $where_array[] = $key . '=' . $val; + } + } + $update_strings = ''; + foreach ( $update_array_assoc as $key => $val ) { + if ( in_array( $key, $unique_keys_for_check ) ) { + $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 ) ) { + $query = str_replace( $match[0], '', $this->_query ); + $_wpdb = new WP_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. + * + * wp-admin/includes/upgrade.php contains '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 WP_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/src/wp-includes/sqlite/class-wp-pdo-sqlite-user-defined-functions.php b/src/wp-includes/sqlite/class-wp-pdo-sqlite-user-defined-functions.php new file mode 100644 index 0000000000000..9d24f451e83fd --- /dev/null +++ b/src/wp-includes/sqlite/class-wp-pdo-sqlite-user-defined-functions.php @@ -0,0 +1,794 @@ + + * new WP_PDO_SQLite_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 WP_PDO_SQLite_User_Defined_Functions { + + /** + * The class constructor + * + * Initializes the use defined functions to PDO object with PDO::sqliteCreateFunction(). + * + * @param PDO $pdo + */ + public function __construct( $pdo ) { + if ( ! $pdo ) { + wp_die( 'Database is not initialized.', 'Database Error' ); + } + foreach ( $this->functions as $f => $t ) { + $pdo->sqliteCreateFunction( $f, array( $this, $t ) ); + } + } + + /** + * array to define MySQL function => function defined with PHP. + * + * Replaced functions must be public. + * + * @var array + */ + private $functions = array( + 'month' => 'month', + 'year' => 'year', + 'day' => 'day', + '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', + 'field' => 'field', + 'log' => 'log', + 'least' => 'least', + 'greatest' => 'greatest', + 'get_lock' => 'get_lock', + 'release_lock' => 'release_lock', + 'ucase' => 'ucase', + 'lcase' => 'lcase', + 'inet_ntoa' => 'inet_ntoa', + 'inet_aton' => 'inet_aton', + 'datediff' => 'datediff', + 'locate' => 'locate', + 'utc_date' => 'utc_date', + 'utc_time' => 'utc_time', + 'utc_timestamp' => 'utc_timestamp', + 'version' => 'version', + ); + + /** + * Method to extract the month value from the date. + * + * @param string 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 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 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. + * + * Used without an argument, it returns PHP time() function (total seconds passed + * from '1970-01-01 00:00:00' GMT). Used with the argument, it changes the value + * to the timestamp. + * + * @param string representing the date formatted as '0000-00-00 00:00:00'. + * + * @return number of unsigned integer + */ + public function unix_timestamp( $field = null ) { + return is_null( $field ) ? time() : strtotime( $field ); + } + + /** + * Method to emulate MySQL SECOND() function. + * + * @param string 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 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 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 of unix timestamp + * @param string to indicate the way of formatting(optional) + * + * @return string formatted as '0000-00-00 00:00:00'. + */ + public function from_unixtime( $field, $format = null ) { + //convert to ISO time + $date = gmdate( 'Y-m-d H:i:s', $field ); + + return is_null( $format ) ? $date : $this->dateformat( $date, $format ); + } + + /** + * Method to emulate MySQL NOW() function. + * + * @return string representing current time formatted as '0000-00-00 00:00:00'. + */ + public function now() { + return gmdate( 'Y-m-d H:i:s' ); + } + + /** + * Method to emulate MySQL CURDATE() function. + * + * @return string representing current time formatted as '0000-00-00'. + */ + public function curdate() { + return gmdate( 'Y-m-d' ); + } + + /** + * Method to emulate MySQL CHAR_LENGTH() function. + * + * @param string + * + * @return int unsigned integer for the length of the argument. + */ + public function char_length( $field ) { + return strlen( $field ); + } + + /** + * Method to emulate MySQL MD5() function. + * + * @param string + * + * @return string of the md5 hash value of the argument. + */ + public function md5( $field ) { + return md5( $field ); + } + + /** + * Method to emulate MySQL RAND() function. + * + * SQLite does have a random generator, but it is called RANDOM() and returns random + * number between -9223372036854775808 and +9223372036854775807. So we substitute it + * with PHP random generator. + * + * This function uses mt_rand() which is four times faster than rand() and returns + * the random number between 0 and 1. + * + * @return int + */ + 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 + * @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. + * + * @param string date formatted as '0000-00-00' or datetime as '0000-00-00 00:00:00'. + * @param string $format + * + * @return string formatted according to $format + */ + public function dateformat( $date, $format ) { + $mysql_php_date_formats = array( + '%a' => 'D', + '%b' => 'M', + '%c' => 'n', + '%D' => 'jS', + '%d' => 'd', + '%e' => 'j', + '%H' => 'H', + '%h' => 'h', + '%I' => 'h', + '%i' => 'i', + '%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', + ); + $t = strtotime( $date ); + $format = strtr( $format, $mysql_php_date_formats ); + $output = gmdate( $format, $t ); + + return $output; + } + + /** + * 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(). + * + * @param string $date representing the start date. + * @param string $interval representing the expression of the time to add. + * + * @return string date formatted as '0000-00-00 00:00:00'. + * @throws Exception + */ + 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' ); + + case 'now()': + $date_object = new DateTime( $this->now() ); + $date_object->add( new DateInterval( $interval ) ); + return $date_object->format( 'Y-m-d H:i:s' ); + + default: + $date_object = new DateTime( $date ); + $date_object->add( new DateInterval( $interval ) ); + return $date_object->format( 'Y-m-d H:i:s' ); + } + } + + /** + * Method to emulate MySQL DATE_SUB() 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(). + * + * @param string $date representing the start date. + * @param string $interval representing the expression of the time to subtract. + * + * @return string date formatted as '0000-00-00 00:00:00'. + * @throws Exception + */ + 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' ); + + default: + $date_object = new DateTime( $date ); + $date_object->sub( new DateInterval( $interval ) ); + return $date_object->format( 'Y-m-d H:i:s' ); + } + } + + /** + * Method to calculate the interval time between two dates value. + * + * @access private + * + * @param string $interval white space separated expression. + * + * @return string representing the time to add or substract. + */ + 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'; + + case 'day_hour': + $days = intval( $_parts[0] ); + $hours = intval( $_parts[1] ); + return 'P' . $days . 'D' . 'T' . $hours . 'H'; + + case 'year_month': + list($years, $months) = explode( '-', $_parts[0] ); + return 'P' . $years . 'Y' . $months . 'M'; + } + } + + /** + * Method to emulate MySQL DATE() function. + * + * @param string $date formatted as unix time. + * + * @return string formatted as '0000-00-00'. + */ + public function date( $date ) { + return gmdate( 'Y-m-d', strtotime( $date ) ); + } + + /** + * Method to emulate MySQL ISNULL() function. + * + * This function returns true if the argument is null, and true if not. + * + * @param various types $field + * + * @return boolean + */ + public function isnull( $field ) { + return is_null( $field ); + } + + /** + * Method to emulate MySQL IF() function. + * + * As 'IF' is a reserved word for PHP, function name must be changed. + * + * @param unknonw $expression the statement to be evaluated as true or false. + * @param unknown $true statement or value returned if $expression is true. + * @param unknown $false statement or value returned if $expression is false. + * + * @return unknown + */ + public function _if( $expression, $true, $false ) { + return ( true === $expression ) ? $true : $false; + } + + /** + * Method to emulate MySQL REGEXP() function. + * + * @param string $field haystack + * @param string $pattern : regular expression to match. + * + * @return integer 1 if matched, 0 if not matched. + */ + public function regexp( $field, $pattern ) { + $pattern = str_replace( '/', '\/', $pattern ); + $pattern = '/' . $pattern . '/i'; + + 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. + * + * @param string + * + * @return NULL if the argument is null | string conatenated 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. + * + * This function gets the list argument and compares the first item to all the others. + * If the same value is found, it returns the position of that value. If not, it + * returns 0. + * + * @param int...|float... variable number of string, integer or double + * + * @return int unsigned integer + */ + public function field() { + global $wpdb; + $num_args = func_num_args(); + if ( $num_args < 2 or 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 ) ) { + return 0; + } + for ( $i = 0; $i < $num_args - 1; $i++ ) { + if ( strtolower( $arg_list[ $i ] ) === $search_string ) { + return $i + 1; + } + } + + return 0; + } + + /** + * Method to emulate MySQL LOG() function. + * + * Used with one argument, it returns the natural logarithm of X. + * + * LOG(X) + * + * Used with two arguments, it returns the natural logarithm of X base B. + * + * LOG(B, X) + * + * In this case, it returns the value of log(X) / log(B). + * + * Used without an argument, it returns false. This returned value will be + * rewritten to 0, because SQLite doesn't understand true/false value. + * + * @param integer representing the base of the logarithm, which is optional. + * @param double value to turn into logarithm. + * + * @return double | NULL + */ + public function log() { + $num_args = func_num_args(); + if ( 1 == $num_args ) { + $arg1 = func_get_arg( 0 ); + + return log( $arg1 ); + } + if ( 2 == $num_args ) { + $arg1 = func_get_arg( 0 ); + $arg2 = func_get_arg( 1 ); + + return log( $arg1 ) / log( $arg2 ); + } + return null; + } + + /** + * Method to emulate MySQL LEAST() function. + * + * This function rewrites the function name to SQLite compatible function name. + * + * @return mixed + */ + public function least() { + $arg_list = func_get_args(); + + return "min($arg_list)"; + } + + /** + * Method to emulate MySQL GREATEST() function. + * + * This function rewrites the function name to SQLite compatible function name. + * + * @return mixed + */ + public function greatest() { + $arg_list = func_get_args(); + + return "max($arg_list)"; + } + + /** + * Method to dummy out MySQL GET_LOCK() function. + * + * This function is meaningless in SQLite, so we do nothing. + * + * @param string $name + * @param integer $timeout + * + * @return string + */ + public function get_lock( $name, $timeout ) { + return '1=1'; + } + + /** + * Method to dummy out MySQL RELEASE_LOCK() function. + * + * This function is meaningless in SQLite, so we do nothing. + * + * @param string $name + * + * @return string + */ + public function release_lock( $name ) { + return '1=1'; + } + + /** + * Method to emulate MySQL UCASE() function. + * + * This is MySQL alias for upper() function. This function rewrites it + * to SQLite compatible name upper(). + * + * @param string + * + * @return string SQLite compatible function name. + */ + public function ucase( $string ) { + return "upper($string)"; + } + + /** + * Method to emulate MySQL LCASE() function. + * + * + * This is MySQL alias for lower() function. This function rewrites it + * to SQLite compatible name lower(). + * + * @param string + * + * @return string SQLite compatible function name. + */ + public function lcase( $string ) { + return "lower($string)"; + } + + /** + * Method to emulate MySQL INET_NTOA() function. + * + * This function gets 4 or 8 bytes integer and turn it into the network address. + * + * @param unsigned long integer + * + * @return string + */ + public function inet_ntoa( $num ) { + return long2ip( $num ); + } + + /** + * Method to emulate MySQL INET_ATON() function. + * + * This function gets the network address and turns it into integer. + * + * @param string + * + * @return int long integer + */ + public function inet_aton( $addr ) { + return absint( ip2long( $addr ) ); + } + + /** + * Method to emulate MySQL DATEDIFF() function. + * + * This function compares two dates value and returns the difference. + * + * @param string start + * @param string end + * + * @return string + */ + public function datediff( $start, $end ) { + $start_date = new DateTime( $start ); + $end_date = new DateTime( $end ); + $interval = $end_date->diff( $start_date, false ); + + return $interval->format( '%r%a' ); + } + + /** + * Method to emulate MySQL LOCATE() function. + * + * This function returns the position if $substr is found in $str. If not, + * it returns 0. If mbstring extension is loaded, mb_strpos() function is + * used. + * + * @param string needle + * @param string haystack + * @param integer position + * + * @return integer + */ + public function locate( $substr, $str, $pos = 0 ) { + if ( ! extension_loaded( 'mbstring' ) ) { + $val = strpos( $str, $substr, $pos ); + if ( false !== $val ) { + return $val + 1; + } + return 0; + } + $val = mb_strpos( $str, $substr, $pos ); + if ( false !== $val ) { + return $val + 1; + } + return 0; + } + + /** + * Method to return GMT date in the string format. + * + * @param none + * + * @return string formatted GMT date 'dddd-mm-dd' + */ + public function utc_date() { + return gmdate( 'Y-m-d', time() ); + } + + /** + * Method to return GMT time in the string format. + * + * @param none + * + * @return string formatted GMT time '00:00:00' + */ + public function utc_time() { + return gmdate( 'H:i:s', time() ); + } + + /** + * Method to return GMT time stamp in the string format. + * + * @param none + * + * @return string formatted GMT timestamp 'yyyy-mm-dd 00:00:00' + */ + public function utc_timestamp() { + return gmdate( 'Y-m-d H:i:s', time() ); + } + + /** + * Method to return MySQL version. + * + * This function only returns the current newest version number of MySQL, + * because it is meaningless for SQLite database. + * + * @param none + * + * @return string representing the version number: major_version.minor_version + */ + public function version() { + //global $required_mysql_version; + //return $required_mysql_version; + return '5.5'; + } +} diff --git a/src/wp-includes/sqlite/class-wp-sqlite-alter-query.php b/src/wp-includes/sqlite/class-wp-sqlite-alter-query.php new file mode 100644 index 0000000000000..b32df47f23e62 --- /dev/null +++ b/src/wp-includes/sqlite/class-wp-sqlite-alter-query.php @@ -0,0 +1,609 @@ +command_tokenizer( $single_command ); + if ( ! empty( $command_tokens ) ) { + $tokens = array_merge( $tmp_tokens, $command_tokens ); + } else { + $this->_query = 'SELECT 1=1'; + + return $this->_query; + } + $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 ) ); + } + } else { + $this->_query = 'SELECT 1=1'; + } + + return $this->_query; + } + + /** + * Function to analyze ALTER TABLE command and sets the data to an array. + * + * @param string $command + * + * @return boolean|array + * @access private + */ + private function command_tokenizer( $command ) { + $tokens = array(); + if ( preg_match( + '/^(ADD|DROP|RENAME|MODIFY|CHANGE|ALTER)\\s*(\\w+)?\\s*(\\w+(\(.+\)|))?\\s*/ims', + $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' ) ) ) { + break; + } elseif ( 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' ) ) ) { + $tokens['command'] = $match_1 . ' ' . $match_2; + if ( '' == $match_3 ) { + $tokens['index_name'] = str_replace( array( '(', ')' ), '', $the_rest ); + } else { + $tokens['index_name'] = $match_3; + } + $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' ) ) ) { + $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': + 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['default_command'] = 'DROP DEFAULT'; + } + } 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 ); + } else { + $tokens['default_command'] = 'DROP DEFAULT'; + } + } + break; + default: + break; + } + + return $tokens; + } + } + + /** + * Function to handle single command. + * + * @access private + * + * @param array of string $queries + * + * @return string + */ + private function handle_single_command( $queries ) { + $tokenized_query = $queries; + if ( stripos( $tokenized_query['command'], 'add column' ) !== false ) { + $column_def = $this->convert_field_types( $tokenized_query['column_name'], $tokenized_query['column_def'] ); + $query = "ALTER TABLE {$tokenized_query['table_name']} ADD COLUMN {$tokenized_query['column_name']} $column_def"; + } elseif ( stripos( $tokenized_query['command'], 'rename' ) !== false ) { + $query = "ALTER TABLE {$tokenized_query['table_name']} RENAME TO {$tokenized_query['column_name']}"; + } elseif ( stripos( $tokenized_query['command'], 'add index' ) !== false ) { + $unique = isset( $tokenized_query['unique'] ) ? 'UNIQUE' : ''; + $query = "CREATE $unique INDEX IF NOT EXISTS {$tokenized_query['index_name']} ON {$tokenized_query['table_name']} {$tokenized_query['column_name']}"; + } elseif ( stripos( $tokenized_query['command'], 'drop index' ) !== false ) { + $query = "DROP INDEX IF EXISTS {$tokenized_query['index_name']}"; + } else { + $query = 'SELECT 1=1'; + } + + return $query; + } + + /** + * Function to handle ADD PRIMARY KEY. + * + * @access private + * + * @param array of string $queries + * + * @return array of string + */ + private function handle_add_primary_key( $queries ) { + $tokenized_query = $queries; + $tbl_name = $tokenized_query['table_name']; + $temp_table = 'temp_' . $tokenized_query['table_name']; + $_wpdb = new WP_SQLite_DB(); + $query_obj = $_wpdb->get_results( "SELECT sql FROM sqlite_master WHERE tbl_name='$tbl_name'" ); + $_wpdb = null; + for ( $i = 0; $i < count( $query_obj ); $i++ ) { + $index_queries[ $i ] = $query_obj[ $i ]->sql; + } + $table_query = array_shift( $index_queries ); + $table_query = str_replace( $tokenized_query['table_name'], $temp_table, $table_query ); + $table_query = rtrim( $table_query, ')' ); + $table_query = ", PRIMARY KEY {$tokenized_query['column_name']}"; + $query[] = $table_query; + $query[] = "INSERT INTO $temp_table SELECT * FROM {$tokenized_query['table_name']}"; + $query[] = "DROP TABLE IF EXISTS {$tokenized_query['table_name']}"; + $query[] = "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 of string $queries + * + * @return array of string + */ + private function handle_drop_primary_key( $queries ) { + $tokenized_query = $queries; + $temp_table = 'temp_' . $tokenized_query['table_name']; + $_wpdb = new WP_SQLite_DB(); + $query_obj = $_wpdb->get_results( "SELECT sql FROM sqlite_master WHERE tbl_name='{$tokenized_query['table_name']}'" ); + $_wpdb = null; + 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[] = $table_query; + $query[] = "INSERT INTO $temp_table SELECT * FROM {$tokenized_query['table_name']}"; + $query[] = "DROP TABLE IF EXISTS {$tokenized_query['table_name']}"; + $query[] = "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 of string $queries + * + * @return string|array of string + */ + 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'] ); + $_wpdb = new WP_SQLite_DB(); + $query_obj = $_wpdb->get_results( "SELECT sql FROM sqlite_master WHERE tbl_name='{$tokenized_query['table_name']}'" ); + $_wpdb = null; + 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'; + } elseif ( preg_match( "/{$tokenized_query['column_name']}\\s*{$column_def}\\s*[,)]/i", $create_query ) ) { + return 'SELECT 1=1'; + } + $create_query = preg_replace( "/{$tokenized_query['table_name']}/i", $temp_table, $create_query ); + if ( preg_match( "/\\b{$tokenized_query['column_name']}\\s*.*(?=,)/ims", $create_query ) ) { + $create_query = preg_replace( + "/\\b{$tokenized_query['column_name']}\\s*.*(?=,)/ims", + "{$tokenized_query['column_name']} {$column_def}", + $create_query + ); + } elseif ( preg_match( "/\\b{$tokenized_query['column_name']}\\s*.*(?=\))/ims", $create_query ) ) { + $create_query = preg_replace( + "/\\b{$tokenized_query['column_name']}\\s*.*(?=\))/ims", + "{$tokenized_query['column_name']} {$column_def}", + $create_query + ); + } + $query[] = $create_query; + $query[] = "INSERT INTO $temp_table SELECT * FROM {$tokenized_query['table_name']}"; + $query[] = "DROP TABLE IF EXISTS {$tokenized_query['table_name']}"; + $query[] = "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 of string $queries + * + * @return string|array of string + */ + private function handle_change_command( $queries ) { + $col_check = false; + $old_fields = ''; + $tokenized_query = $queries; + $temp_table = 'temp_' . $tokenized_query['table_name']; + if ( isset( $tokenized_query['new_column'] ) ) { + $column_name = $tokenized_query['new_column']; + } else { + $column_name = $tokenized_query['old_column']; + } + $column_def = $this->convert_field_types( $column_name, $tokenized_query['column_def'] ); + $_wpdb = new WP_SQLite_DB(); + $col_obj = $_wpdb->get_results( "SHOW COLUMNS FROM {$tokenized_query['table_name']}" ); + foreach ( $col_obj as $col ) { + if ( stripos( $col->Field, $tokenized_query['old_column'] ) !== false ) { + $col_check = true; + } + $old_fields .= $col->Field . ','; + } + if ( false == $col_check ) { + $_wpdb = null; + + return 'SELECT 1=1'; + } + $old_fields = rtrim( $old_fields, ',' ); + $new_fields = str_ireplace( $tokenized_query['old_column'], $column_name, $old_fields ); + $query_obj = $_wpdb->get_results( "SELECT sql FROM sqlite_master WHERE tbl_name='{$tokenized_query['table_name']}'" ); + $_wpdb = null; + 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 ); + if ( preg_match( "/\\b{$tokenized_query['old_column']}\\s*(.+?)(?=,)/ims", $create_query, $match ) ) { + if ( stripos( trim( $match[1] ), $column_def ) !== false ) { + return 'SELECT 1=1'; + } else { + $create_query = preg_replace( + "/\\b{$tokenized_query['old_column']}\\s*.+?(?=,)/ims", + "{$column_name} {$column_def}", + $create_query, + 1 + ); + } + } elseif ( preg_match( "/\\b{$tokenized_query['old_column']}\\s*(.+?)(?=\))/ims", $create_query, $match ) ) { + if ( stripos( trim( $match[1] ), $column_def ) !== false ) { + return 'SELECT 1=1'; + } else { + $create_query = preg_replace( + "/\\b{$tokenized_query['old_column']}\\s*.*(?=\))/ims", + "{$column_name} {$column_def}", + $create_query, + 1 + ); + } + } + $query[] = $create_query; + $query[] = "INSERT INTO $temp_table ($new_fields) SELECT $old_fields FROM {$tokenized_query['table_name']}"; + $query[] = "DROP TABLE IF EXISTS {$tokenized_query['table_name']}"; + $query[] = "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 of string $queries + * + * @return string|array of string + */ + private function handle_alter_command( $queries ) { + $tokenized_query = $queries; + $temp_table = 'temp_' . $tokenized_query['table_name']; + 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; + } else { + $def_value = null; + } + $_wpdb = new WP_SQLite_DB(); + $query_obj = $_wpdb->get_results( "SELECT sql FROM sqlite_master WHERE tbl_name='{$tokenized_query['table_name']}'" ); + $_wpdb = null; + 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'; + } + if ( preg_match( + "/\\s*({$tokenized_query['column_name']})\\s*(.*)?(DEFAULT\\s*.*)[,)]/im", + $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] ); + $pattern = "/$col_name\\s*$col_def_esc\\s*$old_default/im"; + if ( is_null( $def_value ) ) { + $replacement = $col_name . ' ' . $checked_col_def; + } else { + $replacement = $col_name . ' ' . $checked_col_def . ' ' . $def_value; + } + $create_query = preg_replace( $pattern, $replacement, $create_query ); + $create_query = str_ireplace( $tokenized_query['table_name'], $temp_table, $create_query ); + } elseif ( preg_match( "/\\s*({$tokenized_query['column_name']})\\s*(.*)?[,)]/im", $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 ); + $pattern = "/$col_name\\s*$col_def_esc/im"; + if ( is_null( $def_value ) ) { + $replacement = $col_name . ' ' . $checked_col_def; + } else { + $replacement = $col_name . ' ' . $checked_col_def . ' ' . $def_value; + } + $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[] = $create_query; + $query[] = "INSERT INTO $temp_table SELECT * FROM {$tokenized_query['table_name']}"; + $query[] = "DROP TABLE IF EXISTS {$tokenized_query['table_name']}"; + $query[] = "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 + * @param string $col_def + * + * @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 ); + // colDef 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/src/wp-includes/sqlite/class-wp-sqlite-create-query.php b/src/wp-includes/sqlite/class-wp-sqlite-create-query.php new file mode 100644 index 0000000000000..b2a91a6e20b57 --- /dev/null +++ b/src/wp-includes/sqlite/class-wp-sqlite-create-query.php @@ -0,0 +1,488 @@ +_query = $query; + $this->_errors [] = ''; + if ( preg_match( '/^CREATE\\s*(UNIQUE|FULLTEXT|)\\s*INDEX/ims', $this->_query, $match ) ) { + // we manipulate CREATE INDEX query in WP_PDO_Engine.class.php + // FULLTEXT index creation is simply ignored. + if ( isset( $match[1] ) && stripos( $match[1], 'fulltext' ) !== false ) { + return 'SELECT 1=1'; + } else { + return $this->_query; + } + } elseif ( 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. + * + * @param array $matches an array of matches from the Regex + * + * @access private + * @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 WP_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 $matches + * + * @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 WP_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 + * + * @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/src/wp-includes/sqlite/class-wp-sqlite-db.php b/src/wp-includes/sqlite/class-wp-sqlite-db.php new file mode 100644 index 0000000000000..57342f3cdbe13 --- /dev/null +++ b/src/wp-includes/sqlite/class-wp-sqlite-db.php @@ -0,0 +1,315 @@ +ready = true; + } + + /** + * Method to escape characters. + * + * This overrides wpdb::_real_escape() to avoid using mysql_real_escape_string(). + * + * @see wpdb::_real_escape() + * + * @param string $string to escape + * + * @return string escaped + */ + function _real_escape( $string ) { + return addslashes( $string ); + } + + /** + * Method to dummy out wpdb::esc_like() function. + * + * WordPress 4.0.0 introduced esc_like() function that adds backslashes to %, + * underscore and backslash, which is not interpreted as escape character + * by SQLite. So we override it and dummy out this function. + * + * @param string $text The raw text to be escaped. The input typed by the user should have no + * extra or deleted slashes. + * + * @return string Text in the form of a LIKE phrase. The output is not SQL safe. Call $wpdb::prepare() + * or real_escape next. + */ + public function esc_like( $text ) { + return $text; + } + + /** + * Method to put out the error message. + * + * This overrides wpdb::print_error(), for we can't use the parent class method. + * + * @see wpdb::print_error() + * + * @global array $EZSQL_ERROR Stores error information of query and error string + * + * @param string $str The error to display + * + * @return bool False if the showing of errors is disabled. + */ + public function print_error( $str = '' ) { + global $EZSQL_ERROR; + + if ( ! $str ) { + $err = $this->dbh->get_error_message() ? $this->dbh->get_error_message() : ''; + $str = empty( $err ) ? '' : $err[2]; + } + $EZSQL_ERROR[] = array( + 'query' => $this->last_query, + 'error_str' => $str, + ); + + if ( $this->suppress_errors ) { + return false; + } + + wp_load_translations_early(); + + $caller = $this->get_caller(); + if ( $caller ) { + $error_str = sprintf( + __( 'WordPress database error %1$s for query %2$s made by %3$s' ), + $str, + $this->last_query, + $caller + ); + } else { + $error_str = sprintf( __( 'WordPress database error %1$s for query %2$s' ), $str, $this->last_query ); + } + + error_log( $error_str ); + + if ( ! $this->show_errors ) { + return false; + } + + if ( is_multisite() ) { + $msg = "WordPress database error: [$str]\n{$this->last_query}\n"; + if ( defined( 'ERRORLOGFILE' ) ) { + error_log( $msg, 3, ERRORLOGFILE ); + } + if ( defined( 'DIEONDBERROR' ) ) { + wp_die( $msg ); + } + } else { + $str = htmlspecialchars( $str, ENT_QUOTES ); + $query = htmlspecialchars( $this->last_query, ENT_QUOTES ); + + print "
+

WordPress database error: [$str]
+ $query

+
"; + } + } + + /** + * Method to flush cached data. + * + * This overrides wpdb::flush(). This is not necessarily overridden, because + * $result will never be resource. + * + * @see wpdb::flush + */ + public function flush() { + $this->last_result = array(); + $this->col_info = null; + $this->last_query = null; + $this->rows_affected = 0; + $this->num_rows = 0; + $this->last_error = ''; + $this->result = null; + } + + /** + * Method to do the database connection. + * + * This overrides wpdb::db_connect() to avoid using MySQL function. + * + * @see wpdb::db_connect() + * + * @param bool $allow_bail + */ + public function db_connect( $allow_bail = true ) { + $this->init_charset(); + $this->dbh = new WP_PDO_Engine(); + $this->ready = true; + } + + /** + * Method to dummy out wpdb::check_connection() + * + * @param bool $allow_bail + * + * @return bool + */ + public function check_connection( $allow_bail = true ) { + return true; + } + + /** + * Method to execute the query. + * + * This overrides wpdb::query(). In fact, this method does all the database + * access jobs. + * + * @see wpdb::query() + * + * @param string $query Database query + * + * @return int|false Number of rows affected/selected or false on error + */ + public function query( $query ) { + if ( ! $this->ready ) { + return false; + } + + $query = apply_filters( 'query', $query ); + + $return_val = 0; + $this->flush(); + + $this->func_call = "\$db->query(\"$query\")"; + + $this->last_query = $query; + + if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { + $this->timer_start(); + } + + $this->result = $this->dbh->query( $query ); + $this->num_queries++; + + if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { + $this->queries[] = array( $query, $this->timer_stop(), $this->get_caller() ); + } + + $this->last_error = $this->dbh->get_error_message(); + if ( $this->last_error ) { + if ( defined( 'WP_INSTALLING' ) && WP_INSTALLING ) { + //$this->suppress_errors(); + } else { + $this->print_error( $this->last_error ); + + return false; + } + } + + if ( preg_match( '/^\\s*(create|alter|truncate|drop|optimize)\\s*/i', $query ) ) { + return $this->dbh->get_return_value(); + } + if ( preg_match( '/^\\s*(insert|delete|update|replace)\s/i', $query ) ) { + $this->rows_affected = $this->dbh->get_affected_rows(); + if ( preg_match( '/^\s*(insert|replace)\s/i', $query ) ) { + $this->insert_id = $this->dbh->get_insert_id(); + } + return $this->rows_affected; + } + $this->last_result = $this->dbh->get_query_results(); + $this->num_rows = $this->dbh->get_num_rows(); + return $this->num_rows; + } + + /** + * Method to set the class variable $col_info. + * + * This overrides wpdb::load_col_info(), which uses a mysql function. + * + * @see wpdb::load_col_info() + * @access protected + */ + protected function load_col_info() { + if ( $this->col_info ) { + return; + } + $this->col_info = $this->dbh->get_columns(); + } + + /** + * Method to return what the database can do. + * + * This overrides wpdb::has_cap() to avoid using MySQL functions. + * SQLite supports subqueries, but not support collation, group_concat and set_charset. + * + * @see wpdb::has_cap() + * + * @param string $db_cap The feature to check for. Accepts 'collation', + * 'group_concat', 'subqueries', 'set_charset', + * 'utf8mb4', or 'utf8mb4_520'. + * + * @return int|false Whether the database feature is supported, false otherwise. + */ + public function has_cap( $db_cap ) { + return 'subqueries' === strtolower( $db_cap ); + } + + /** + * Method to return database version number. + * + * This overrides wpdb::db_version() to avoid using MySQL function. + * It returns mysql version number, but it means nothing for SQLite. + * So it return the newest mysql version. + * + * @see wpdb::db_version() + */ + public function db_version() { + return '5.5'; + } +} diff --git a/src/wp-includes/sqlite/class-wp-sqlite-object-array.php b/src/wp-includes/sqlite/class-wp-sqlite-object-array.php new file mode 100644 index 0000000000000..ee04443ab2faf --- /dev/null +++ b/src/wp-includes/sqlite/class-wp-sqlite-object-array.php @@ -0,0 +1,26 @@ + $value ) { + if ( is_array( $value ) ) { + if ( ! $node ) { + $node =& $this; + } + $node->$key = new stdClass(); + self::__construct( $value, $node->$key ); + } else { + if ( ! $node ) { + $node =& $this; + } + $node->$key = $value; + } + } + } +} diff --git a/src/wp-includes/sqlite/db.php b/src/wp-includes/sqlite/db.php new file mode 100644 index 0000000000000..e59cd801e7b98 --- /dev/null +++ b/src/wp-includes/sqlite/db.php @@ -0,0 +1,289 @@ + + + +WordPress › Error + + + + +

WordPress

+

$message

+

$data

+ + + +HTML + ); +} + +if ( ! extension_loaded( 'pdo' ) ) { + pdo_log_error( + 'PHP PDO Extension is not loaded.', + 'Your PHP installation appears to be missing the PDO extension which is required for this version of WordPress.' + ); +} + +if ( ! extension_loaded( 'pdo_sqlite' ) ) { + pdo_log_error( + 'PDO Driver for SQLite is missing.', + 'Your PHP installation appears not to have the right PDO drivers loaded. These are required for this version of WordPress and the type of database you have specified.' + ); +} + +/** + * Notice: + * Your scripts have the permission to create directories or files on your server. + * If you write in your wp-config.php like below, we take these definitions. + * define('DB_DIR', '/full_path_to_the_database_directory/'); + * define('DB_FILE', 'database_file_name'); + */ + +/** + * FQDBDIR is a directory where the sqlite database file is placed. + * If DB_DIR is defined, it is used as FQDBDIR. + */ +if ( defined( 'DB_DIR' ) ) { + define( 'FQDBDIR', trailingslashit( DB_DIR ) ); +} elseif ( defined( 'WP_CONTENT_DIR' ) ) { + define( 'FQDBDIR', WP_CONTENT_DIR . '/database/' ); +} else { + define( 'FQDBDIR', ABSPATH . 'wp-content/database/' ); +} + +/** + * FQDB is a database file name. If DB_FILE is defined, it is used + * as FQDB. + */ +if ( defined( 'DB_FILE' ) ) { + define( 'FQDB', FQDBDIR . DB_FILE ); +} else { + define( 'FQDB', FQDBDIR . '.ht.sqlite' ); +} + +require_once ABSPATH . WPINC . '/sqlite/class-wp-pdo-sqlite-user-defined-functions.php'; +require_once ABSPATH . WPINC . '/sqlite/class-wp-pdo-engine.php'; +require_once ABSPATH . WPINC . '/sqlite/class-wp-sqlite-object-array.php'; +require_once ABSPATH . WPINC . '/sqlite/class-wp-sqlite-db.php'; +require_once ABSPATH . WPINC . '/sqlite/class-wp-pdo-sqlite-driver.php'; +require_once ABSPATH . WPINC . '/sqlite/class-wp-sqlite-create-query.php'; +require_once ABSPATH . WPINC . '/sqlite/class-wp-sqlite-alter-query.php'; + +/** + * Function to create tables according to the schemas of WordPress. + * + * This is executed only once while installation. + * + * @return boolean + */ +function 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 WP_SQLite_Create_Query(); + try { + $pdo = new PDO( 'sqlite:' . FQDB, null, null, array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ) ); + } catch ( PDOException $err ) { + $err_data = $err->errorInfo; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $message = 'Database connection error!
'; + $message .= sprintf( 'Error message is: %s', $err_data[2] ); + wp_die( $message, 'Database Error!' ); + } + + try { + $pdo->beginTransaction(); + 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 ); + //foreach($rewritten_query as $single_query) { + // $single_query = trim($single_query); + // $pdo->exec($single_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 ) ) { + $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 ); + } + $pdo->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 ) { + // if the database is locked, commit again + $pdo->commit(); + } else { + $pdo->rollBack(); + $message = sprintf( + 'Error occured 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!' ); + } + } + + $query_parser = null; + $pdo = null; + + return true; +} + +/** + * Installs the site. + * + * Runs the required functions to set up and populate the database, + * including primary admin user and initial options. + * + * @since 2.1.0 + * + * @param string $blog_title Site title. + * @param string $user_name User's username. + * @param string $user_email User's email. + * @param bool $public Whether site is public. + * @param string $deprecated Optional. Not used. + * @param string $user_password Optional. User's chosen password. Default empty (random password). + * @param string $language Optional. Language chosen. Default empty. + * + * @return array Array keys 'url', 'user_id', 'password', and 'password_message'. + */ +function wp_install( $blog_title, $user_name, $user_email, $public, $deprecated = '', $user_password = '', $language = '' ) { + if ( ! empty( $deprecated ) ) { + _deprecated_argument( __FUNCTION__, '2.6.0' ); + } + + wp_check_mysql_version(); + wp_cache_flush(); + /* begin wp-sqlite-db changes */ + // make_db_current_silent(); + make_db_sqlite(); + /* end wp-sqlite-db changes */ + populate_options(); + populate_roles(); + + update_option( 'blogname', $blog_title ); + update_option( 'admin_email', $user_email ); + update_option( 'blog_public', $public ); + + // Freshness of site - in the future, this could get more specific about actions taken, perhaps. + update_option( 'fresh_site', 1 ); + + if ( $language ) { + update_option( 'WPLANG', $language ); + } + + $guessurl = wp_guess_url(); + + update_option( 'siteurl', $guessurl ); + + // If not a public blog, don't ping. + if ( ! $public ) { + update_option( 'default_pingback_flag', 0 ); + } + + /* + * Create default user. If the user already exists, the user tables are + * being shared among sites. Just set the role in that case. + */ + $user_id = username_exists( $user_name ); + $user_password = trim( $user_password ); + $email_password = false; + if ( ! $user_id && empty( $user_password ) ) { + $user_password = wp_generate_password( 12, false ); + $message = __( 'Note that password carefully! It is a random password that was generated just for you.' ); + $user_id = wp_create_user( $user_name, $user_password, $user_email ); + update_user_option( $user_id, 'default_password_nag', true, true ); + $email_password = true; + } elseif ( ! $user_id ) { + // Password has been provided + $message = '' . __( 'Your chosen password.' ) . ''; + $user_id = wp_create_user( $user_name, $user_password, $user_email ); + } else { + $message = __( 'User already exists. Password inherited.' ); + } + + $user = new WP_User( $user_id ); + $user->set_role( 'administrator' ); + + wp_install_defaults( $user_id ); + + wp_install_maybe_enable_pretty_permalinks(); + + flush_rewrite_rules(); + + wp_new_blog_notification( $blog_title, $guessurl, $user_id, ( $email_password ? $user_password : __( 'The password you chose during installation.' ) ) ); + + wp_cache_flush(); + + /** + * Fires after a site is fully installed. + * + * @since 3.9.0 + * + * @param WP_User $user The site owner. + */ + do_action( 'wp_install', $user ); + + return array( + 'url' => $guessurl, + 'user_id' => $user_id, + 'password' => $user_password, + 'password_message' => $message, + ); +} + +$GLOBALS['wpdb'] = new WP_SQLite_DB(); From d738213902ca7df8581fcb9a4c939edf582e2cbd Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Mon, 5 Sep 2022 09:34:43 +0300 Subject: [PATCH 02/36] CS fixes --- .../sqlite/class-wp-pdo-engine.php | 22 +++-- .../sqlite/class-wp-pdo-sqlite-driver.php | 87 +++++++++---------- ...s-wp-pdo-sqlite-user-defined-functions.php | 6 +- .../sqlite/class-wp-sqlite-alter-query.php | 12 +-- src/wp-includes/sqlite/class-wp-sqlite-db.php | 2 + src/wp-includes/sqlite/db.php | 2 +- 6 files changed, 65 insertions(+), 66 deletions(-) diff --git a/src/wp-includes/sqlite/class-wp-pdo-engine.php b/src/wp-includes/sqlite/class-wp-pdo-engine.php index c3b033607bcdf..7c9d39ba000b8 100644 --- a/src/wp-includes/sqlite/class-wp-pdo-engine.php +++ b/src/wp-includes/sqlite/class-wp-pdo-engine.php @@ -344,14 +344,12 @@ public function query( $statement, $mode = PDO::ATTR_DEFAULT_FETCH_MODE, ...$fet $this->queries[] = "Raw query:\n$statement"; $res = $this->determine_query_type( $statement ); if ( ! $res && defined( 'PDO_DEBUG' ) && PDO_DEBUG ) { - $bailoutString = sprintf( - __( - '

Unknown query type

Sorry, we cannot determine the type of query that is requested.

The query is %s

', - 'sqlite-integration' - ), + $bailout_string = sprintf( + /* translators: %s: SQL statement */ + '

' . __( 'Unknown query type' ) . '

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

', $statement ); - $this->set_error( __LINE__, __FUNCTION__, $bailoutString ); + $this->set_error( __LINE__, __FUNCTION__, $bailout_string ); } switch ( strtolower( $this->query_type ) ) { case 'set': @@ -829,10 +827,10 @@ private function replace_variables_with_placeholders( $matches ) { $param = trim( $param ); //remove the quotes at the end and the beginning - if ( in_array( $param[ strlen( $param ) - 1 ], array( "'", '"' ) ) ) { + if ( in_array( $param[ strlen( $param ) - 1 ], array( "'", '"' ), true ) ) { $param = substr( $param, 0, -1 );//end } - if ( in_array( $param[0], array( "'", '"' ) ) ) { + if ( in_array( $param[0], array( "'", '"' ), true ) ) { $param = substr( $param, 1 ); //start } //$this->extracted_variables[] = $param; @@ -1103,7 +1101,7 @@ private function execute_alter_query( $query ) { $this->rollBack(); } } - if ( '' != $re_query ) { + if ( '' !== $re_query ) { $this->query( $re_query ); } if ( $reason > 0 ) { @@ -1199,11 +1197,11 @@ private function show_status_workaround( $query ) { * @param string $engine */ private function process_results( $engine ) { - if ( in_array( $this->query_type, array( 'describe', 'desc', 'showcolumns' ) ) ) { + 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' ) ) ) { + } elseif ( in_array( $this->query_type, array( 'check', 'analyze' ), true ) ) { $this->convert_result_check_or_analyze(); } else { $this->results = $this->_results; @@ -1316,7 +1314,7 @@ private function convert_to_index_object() { 'Index_type' => '', // BTREE, FULLTEXT, HASH, RTREE 'Comment' => '', ); - if ( count( $this->_results ) == 0 ) { + if ( 0 === count( $this->_results ) ) { echo $this->get_error_message(); } else { foreach ( $this->_results as $row ) { diff --git a/src/wp-includes/sqlite/class-wp-pdo-sqlite-driver.php b/src/wp-includes/sqlite/class-wp-pdo-sqlite-driver.php index f905f17584c58..ea3e39853878c 100644 --- a/src/wp-includes/sqlite/class-wp-pdo-sqlite-driver.php +++ b/src/wp-includes/sqlite/class-wp-pdo-sqlite-driver.php @@ -600,7 +600,7 @@ private function execute_duplicate_key_update() { $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 ) { + 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 '; @@ -623,54 +623,53 @@ private function execute_duplicate_key_update() { $_wpdb = null; if ( 0 == $results ) { $this->_query = "INSERT INTO $table_name $insert_data"; - return; - } else { - $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; + } + + $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; } - 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 ]; - } + } + $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 ) ) { - $where_array[] = $key . '=' . $val; - } + } + 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 ) ) { - $where_array[] = $key . '=' . $val; - } else { - $update_strings .= $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; } + $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; } } } @@ -728,7 +727,7 @@ private function handle_orderby_field() { $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 ) ) { + if ( $tbl_name && in_array( $tbl_name, $wpdb->tables, true ) ) { $query = str_replace( $match[0], '', $this->_query ); $_wpdb = new WP_SQLite_DB(); $results = $_wpdb->get_results( $query ); diff --git a/src/wp-includes/sqlite/class-wp-pdo-sqlite-user-defined-functions.php b/src/wp-includes/sqlite/class-wp-pdo-sqlite-user-defined-functions.php index 9d24f451e83fd..06186e6c4eaab 100644 --- a/src/wp-includes/sqlite/class-wp-pdo-sqlite-user-defined-functions.php +++ b/src/wp-includes/sqlite/class-wp-pdo-sqlite-user-defined-functions.php @@ -540,7 +540,7 @@ public function field() { $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 ) ) { + if ( $str_to_check && in_array( trim( $str_to_check ), $wpdb->tables, true ) ) { return 0; } for ( $i = 0; $i < $num_args - 1; $i++ ) { @@ -575,12 +575,12 @@ public function field() { */ public function log() { $num_args = func_num_args(); - if ( 1 == $num_args ) { + if ( 1 === $num_args ) { $arg1 = func_get_arg( 0 ); return log( $arg1 ); } - if ( 2 == $num_args ) { + if ( 2 === $num_args ) { $arg1 = func_get_arg( 0 ); $arg2 = func_get_arg( 1 ); diff --git a/src/wp-includes/sqlite/class-wp-sqlite-alter-query.php b/src/wp-includes/sqlite/class-wp-sqlite-alter-query.php index b32df47f23e62..b4658c2f0e2db 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-alter-query.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-alter-query.php @@ -74,7 +74,7 @@ public function rewrite_query( $query, $query_type ) { } else { $this->_query = $tmp_query; } - if ( '' != $re_command ) { + if ( '' !== $re_command ) { $this->_query = array_merge( $this->_query, array( 'recursion' => $re_command ) ); } } else { @@ -105,7 +105,7 @@ private function command_tokenizer( $command ) { $match_3 = isset( $match[3] ) ? trim( $match[3] ) : ''; switch ( strtolower( $match_1 ) ) { case 'add': - if ( in_array( strtolower( $match_2 ), array( 'fulltext', 'constraint', 'foreign' ) ) ) { + if ( in_array( strtolower( $match_2 ), array( 'fulltext', 'constraint', 'foreign' ), true ) ) { break; } elseif ( stripos( 'column', $match_2 ) !== false ) { $tokens['command'] = $match_1 . ' ' . $match_2; @@ -125,9 +125,9 @@ private function command_tokenizer( $command ) { $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' ) ) ) { + } elseif ( in_array( strtolower( $match_2 ), array( 'index', 'key' ), true ) ) { $tokens['command'] = $match_1 . ' ' . $match_2; - if ( '' == $match_3 ) { + if ( '' === $match_3 ) { $tokens['index_name'] = str_replace( array( '(', ')' ), '', $the_rest ); } else { $tokens['index_name'] = $match_3; @@ -145,7 +145,7 @@ private function command_tokenizer( $command ) { $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' ) ) ) { + } 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 ) { @@ -411,7 +411,7 @@ private function handle_change_command( $queries ) { } $old_fields .= $col->Field . ','; } - if ( false == $col_check ) { + if ( false === $col_check ) { $_wpdb = null; return 'SELECT 1=1'; diff --git a/src/wp-includes/sqlite/class-wp-sqlite-db.php b/src/wp-includes/sqlite/class-wp-sqlite-db.php index 57342f3cdbe13..b1b72521e0bae 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-db.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-db.php @@ -126,12 +126,14 @@ public function print_error( $str = '' ) { $caller = $this->get_caller(); if ( $caller ) { $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' ), $str, $this->last_query, $caller ); } else { + /* translators: 1: Database error message, 2: SQL query. */ $error_str = sprintf( __( 'WordPress database error %1$s for query %2$s' ), $str, $this->last_query ); } diff --git a/src/wp-includes/sqlite/db.php b/src/wp-includes/sqlite/db.php index e59cd801e7b98..3144c0676a912 100644 --- a/src/wp-includes/sqlite/db.php +++ b/src/wp-includes/sqlite/db.php @@ -145,7 +145,7 @@ function make_db_sqlite() { foreach ( $index_queries as $index_query ) { preg_match( $pattern, $index_query, $match ); $index_name = trim( $match[2] ); - if ( in_array( $index_name, $index_array ) ) { + if ( in_array( $index_name, $index_array, true ) ) { $r = rand( 0, 50 ); $replacement = $index_name . "_$r"; $index_query = str_ireplace( From ba59589cc042d98f306dcb274f0442823427b303 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Mon, 5 Sep 2022 10:07:31 +0300 Subject: [PATCH 03/36] more CS fixes --- .../sqlite/class-wp-pdo-engine.php | 20 +++++++++---------- .../sqlite/class-wp-pdo-sqlite-driver.php | 2 +- src/wp-includes/sqlite/db.php | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/sqlite/class-wp-pdo-engine.php b/src/wp-includes/sqlite/class-wp-pdo-engine.php index 7c9d39ba000b8..3c761430e5ac9 100644 --- a/src/wp-includes/sqlite/class-wp-pdo-engine.php +++ b/src/wp-includes/sqlite/class-wp-pdo-engine.php @@ -180,7 +180,7 @@ function __construct() { $GLOBALS['@pdo'] = $this->pdo; } catch ( PDOException $ex ) { $status = $ex->getCode(); - if ( 5 == $status || 6 == $status ) { + if ( 5 == $status || 6 == $status ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison $locked = true; } else { $err_message = $ex->getMessage(); @@ -256,7 +256,7 @@ private function init() { $this->can_insert_multiple_rows = true; } $statement = $this->pdo->query( 'PRAGMA foreign_keys' ); - if ( $statement->fetchColumn( 0 ) == '0' ) { + if ( $statement->fetchColumn( 0 ) == '0' ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison $this->pdo->query( 'PRAGMA foreign_keys = ON' ); } } @@ -480,7 +480,7 @@ public function get_columns() { '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 + 'zerofill' => 0, // 1 if column is zero-filled ); $table_name = ''; if ( preg_match( '/\s*FROM\s*(.*)?\s*/i', $this->rewritten_query, $match ) ) { @@ -489,9 +489,9 @@ public function get_columns() { foreach ( $this->results[0] as $key => $value ) { $data['name'] = $key; $data['table'] = $table_name; - if ( in_array( $key, $primary_key ) ) { + if ( in_array( $key, $primary_key, true ) ) { $data['primary_key'] = 1; - } elseif ( in_array( $key, $unique_key ) ) { + } elseif ( in_array( $key, $unique_key, true ) ) { $data['unique_key'] = 1; } else { $data['multiple_key'] = 1; @@ -642,7 +642,7 @@ private function prepare_query() { $reason = $err->getCode(); $message = $err->getMessage(); } - } while ( 5 == $reason || 6 == $reason ); + } while ( 5 == $reason || 6 == $reason ); // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison if ( $reason > 0 ) { $err_message = sprintf( 'Problem preparing the PDO SQL Statement. Error was: %s', $message ); @@ -690,7 +690,7 @@ private function execute_query( $statement ) { $message = $err->getMessage(); } } - } while ( 5 == $reason || 6 == $reason ); + } while ( 5 == $reason || 6 == $reason ); // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison } else { $this->queries[] = 'Executing: (no parameters)'; do { @@ -712,7 +712,7 @@ private function execute_query( $statement ) { $message = $err->getMessage(); } } - } while ( 5 == $reason || 6 == $reason ); + } 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 ); @@ -1041,7 +1041,7 @@ private function execute_create_query( $query ) { } catch ( PDOException $err ) { $reason = $err->getCode(); $message = $err->getMessage(); - if ( 5 == $reason || 6 == $reason ) { + if ( 5 == $reason || 6 == $reason ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison $this->commit(); } else { $this->rollBack(); @@ -1094,7 +1094,7 @@ private function execute_alter_query( $query ) { } catch ( PDOException $err ) { $reason = $err->getCode(); $message = $err->getMessage(); - if ( 5 == $reason || 6 == $reason ) { + if ( 5 == $reason || 6 == $reason ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison $this->commit(); usleep( 10000 ); } else { diff --git a/src/wp-includes/sqlite/class-wp-pdo-sqlite-driver.php b/src/wp-includes/sqlite/class-wp-pdo-sqlite-driver.php index ea3e39853878c..5e3fa0222d979 100644 --- a/src/wp-includes/sqlite/class-wp-pdo-sqlite-driver.php +++ b/src/wp-includes/sqlite/class-wp-pdo-sqlite-driver.php @@ -564,7 +564,7 @@ private function execute_duplicate_key_update() { $indexes = $_wpdb->get_results( "SHOW INDEX FROM {$table_name}" ); if ( ! empty( $indexes ) ) { foreach ( $indexes as $index ) { - if ( 0 == $index->Non_unique ) { + if ( 0 === $index->Non_unique ) { $unique_keys_for_cond[] = $index->Column_name; if ( strpos( $index->Column_name, ',' ) !== false ) { $unique_keys_for_check = array_merge( diff --git a/src/wp-includes/sqlite/db.php b/src/wp-includes/sqlite/db.php index 3144c0676a912..e6017768114fb 100644 --- a/src/wp-includes/sqlite/db.php +++ b/src/wp-includes/sqlite/db.php @@ -163,7 +163,7 @@ function make_db_sqlite() { } 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 ) { + if ( 5 == $err_code || 6 == $err_code ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison // if the database is locked, commit again $pdo->commit(); } else { From 84c1827fa6bc6bce6dc49ed31232905eddc899c2 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Mon, 5 Sep 2022 10:28:22 +0300 Subject: [PATCH 04/36] some coding improvements --- .../sqlite/class-wp-pdo-engine.php | 23 ++++---- .../sqlite/class-wp-pdo-sqlite-driver.php | 10 ++-- .../sqlite/class-wp-sqlite-alter-query.php | 59 ++++++++----------- .../sqlite/class-wp-sqlite-create-query.php | 12 ++-- src/wp-includes/sqlite/class-wp-sqlite-db.php | 2 +- 5 files changed, 45 insertions(+), 61 deletions(-) diff --git a/src/wp-includes/sqlite/class-wp-pdo-engine.php b/src/wp-includes/sqlite/class-wp-pdo-engine.php index 3c761430e5ac9..071c1c780f896 100644 --- a/src/wp-includes/sqlite/class-wp-pdo-engine.php +++ b/src/wp-includes/sqlite/class-wp-pdo-engine.php @@ -717,11 +717,10 @@ private function execute_query( $statement ) { if ( $reason > 0 ) { $err_message = sprintf( 'Error while executing query! Error message was: %s', $message ); $this->set_error( __LINE__, __FUNCTION__, $err_message ); - return false; - } else { - $this->_results = $statement->fetchAll( PDO::FETCH_OBJ ); } + $this->_results = $statement->fetchAll( PDO::FETCH_OBJ ); + //generate the results that $wpdb will want to see switch ( $this->query_type ) { case 'insert': @@ -791,14 +790,13 @@ private function extract_variables() { 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 - } else { - ini_set( 'pcre.backtrack_limit', $limit ); - $query = preg_replace_callback( - $pattern, - array( $this, 'replace_variables_with_placeholders' ), - $this->rewritten_query - ); } + 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 ) ); @@ -1157,11 +1155,10 @@ private function show_variables_workaround( $query ) { * @return bool */ private function show_status_workaround( $query ) { - $pattern = '/^SHOW\\s*TABLE\\s*STATUS\\s*LIKE\\s*(.*?)$/im'; + $pattern = '/^SHOW\\s*TABLE\\s*STATUS\\s*LIKE\\s*(.*?)$/im'; + $table_name = ''; if ( preg_match( $pattern, $query, $match ) ) { $table_name = str_replace( "'", '', $match[1] ); - } else { - $table_name = ''; } $dummy_data = array( 'Name' => $table_name, diff --git a/src/wp-includes/sqlite/class-wp-pdo-sqlite-driver.php b/src/wp-includes/sqlite/class-wp-pdo-sqlite-driver.php index 5e3fa0222d979..7e4ca2fd03a1c 100644 --- a/src/wp-includes/sqlite/class-wp-pdo-sqlite-driver.php +++ b/src/wp-includes/sqlite/class-wp-pdo-sqlite-driver.php @@ -152,10 +152,9 @@ public function rewrite_query( $query, $query_type ) { default: if ( defined( WP_DEBUG ) && WP_DEBUG ) { break; - } else { - $this->return_true(); - break; } + $this->return_true(); + break; } return $this->_query; @@ -610,11 +609,10 @@ private function execute_duplicate_key_update() { } } else { $col = trim( $unique_key ); - if ( isset( $ins_data_assoc[ $col ] ) ) { - $condition .= $col . '=' . $ins_data_assoc[ $col ] . ' OR '; - } else { + if ( ! isset( $ins_data_assoc[ $col ] ) ) { continue; } + $condition .= $col . '=' . $ins_data_assoc[ $col ] . ' OR '; } } $condition = rtrim( $condition, ' OR ' ); diff --git a/src/wp-includes/sqlite/class-wp-sqlite-alter-query.php b/src/wp-includes/sqlite/class-wp-sqlite-alter-query.php index b4658c2f0e2db..2e3ab05012062 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-alter-query.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-alter-query.php @@ -126,11 +126,10 @@ private function command_tokenizer( $command ) { $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['command'] = $match_1 . ' ' . $match_2; + $tokens['index_name'] = $match_3; if ( '' === $match_3 ) { $tokens['index_name'] = str_replace( array( '(', ')' ), '', $the_rest ); - } else { - $tokens['index_name'] = $match_3; } $tokens['column_name'] = trim( $the_rest ); } else { @@ -202,6 +201,7 @@ private function command_tokenizer( $command ) { } 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; @@ -210,8 +210,6 @@ private function command_tokenizer( $command ) { $tokens['default_command'] = 'SET DEFAULT'; $default_value = str_ireplace( 'set default', '', $the_rest ); $tokens['default_value'] = trim( $default_value ); - } else { - $tokens['default_command'] = 'DROP DEFAULT'; } } else { $tokens['command'] = $match_1 . ' COLUMN'; @@ -220,8 +218,6 @@ private function command_tokenizer( $command ) { $tokens['default_command'] = 'SET DEFAULT'; $default_value = str_ireplace( 'default', '', $the_rest ); $tokens['default_value'] = trim( $default_value ); - } else { - $tokens['default_command'] = 'DROP DEFAULT'; } } break; @@ -244,6 +240,7 @@ private function command_tokenizer( $command ) { */ private function handle_single_command( $queries ) { $tokenized_query = $queries; + $query = 'SELECT 1=1'; if ( stripos( $tokenized_query['command'], 'add column' ) !== false ) { $column_def = $this->convert_field_types( $tokenized_query['column_name'], $tokenized_query['column_def'] ); $query = "ALTER TABLE {$tokenized_query['table_name']} ADD COLUMN {$tokenized_query['column_name']} $column_def"; @@ -254,8 +251,6 @@ private function handle_single_command( $queries ) { $query = "CREATE $unique INDEX IF NOT EXISTS {$tokenized_query['index_name']} ON {$tokenized_query['table_name']} {$tokenized_query['column_name']}"; } elseif ( stripos( $tokenized_query['command'], 'drop index' ) !== false ) { $query = "DROP INDEX IF EXISTS {$tokenized_query['index_name']}"; - } else { - $query = 'SELECT 1=1'; } return $query; @@ -397,10 +392,9 @@ private function handle_change_command( $queries ) { $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']; - } else { - $column_name = $tokenized_query['old_column']; } $column_def = $this->convert_field_types( $column_name, $tokenized_query['column_def'] ); $_wpdb = new WP_SQLite_DB(); @@ -428,25 +422,23 @@ private function handle_change_command( $queries ) { if ( preg_match( "/\\b{$tokenized_query['old_column']}\\s*(.+?)(?=,)/ims", $create_query, $match ) ) { if ( stripos( trim( $match[1] ), $column_def ) !== false ) { return 'SELECT 1=1'; - } else { - $create_query = preg_replace( - "/\\b{$tokenized_query['old_column']}\\s*.+?(?=,)/ims", - "{$column_name} {$column_def}", - $create_query, - 1 - ); } + $create_query = preg_replace( + "/\\b{$tokenized_query['old_column']}\\s*.+?(?=,)/ims", + "{$column_name} {$column_def}", + $create_query, + 1 + ); } elseif ( preg_match( "/\\b{$tokenized_query['old_column']}\\s*(.+?)(?=\))/ims", $create_query, $match ) ) { if ( stripos( trim( $match[1] ), $column_def ) !== false ) { return 'SELECT 1=1'; - } else { - $create_query = preg_replace( - "/\\b{$tokenized_query['old_column']}\\s*.*(?=\))/ims", - "{$column_name} {$column_def}", - $create_query, - 1 - ); } + $create_query = preg_replace( + "/\\b{$tokenized_query['old_column']}\\s*.*(?=\))/ims", + "{$column_name} {$column_def}", + $create_query, + 1 + ); } $query[] = $create_query; $query[] = "INSERT INTO $temp_table ($new_fields) SELECT $old_fields FROM {$tokenized_query['table_name']}"; @@ -471,11 +463,10 @@ private function handle_change_command( $queries ) { 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; - } else { - $def_value = null; } $_wpdb = new WP_SQLite_DB(); $query_obj = $_wpdb->get_results( "SELECT sql FROM sqlite_master WHERE tbl_name='{$tokenized_query['table_name']}'" ); @@ -498,10 +489,9 @@ private function handle_alter_command( $queries ) { $checked_col_def = $this->convert_field_types( $col_name, $col_def ); $old_default = trim( $match[3] ); $pattern = "/$col_name\\s*$col_def_esc\\s*$old_default/im"; - if ( is_null( $def_value ) ) { - $replacement = $col_name . ' ' . $checked_col_def; - } else { - $replacement = $col_name . ' ' . $checked_col_def . ' ' . $def_value; + $replacement = $col_name . ' ' . $checked_col_def; + if ( ! is_null( $def_value ) ) { + $replacement .= ' ' . $def_value; } $create_query = preg_replace( $pattern, $replacement, $create_query ); $create_query = str_ireplace( $tokenized_query['table_name'], $temp_table, $create_query ); @@ -511,10 +501,9 @@ private function handle_alter_command( $queries ) { $col_def_esc = str_replace( array( '(', ')' ), array( '\(', '\)' ), $col_def ); $checked_col_def = $this->convert_field_types( $col_name, $col_def ); $pattern = "/$col_name\\s*$col_def_esc/im"; - if ( is_null( $def_value ) ) { - $replacement = $col_name . ' ' . $checked_col_def; - } else { - $replacement = $col_name . ' ' . $checked_col_def . ' ' . $def_value; + $replacement = $col_name . ' ' . $checked_col_def; + if ( ! is_null( $def_value ) ) { + $replacement .= ' ' . $def_value; } $create_query = preg_replace( $pattern, $replacement, $create_query ); $create_query = str_ireplace( $tokenized_query['table_name'], $temp_table, $create_query ); diff --git a/src/wp-includes/sqlite/class-wp-sqlite-create-query.php b/src/wp-includes/sqlite/class-wp-sqlite-create-query.php index b2a91a6e20b57..a4c485dc54425 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-create-query.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-create-query.php @@ -57,10 +57,10 @@ public function rewrite_query( $query ) { // FULLTEXT index creation is simply ignored. if ( isset( $match[1] ) && stripos( $match[1], 'fulltext' ) !== false ) { return 'SELECT 1=1'; - } else { - return $this->_query; } - } elseif ( preg_match( '/^CREATE\\s*(TEMP|TEMPORARY|)\\s*TRIGGER\\s*/im', $this->_query ) ) { + 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; @@ -156,7 +156,7 @@ private function rewrite_field_types() { } $pattern = "/\\b(?_query ) ) { - ; + // ; } else { $this->_query = preg_replace( $pattern, " $r ", $this->_query ); } @@ -290,7 +290,7 @@ private function _rewrite_unique_key( $matches ) { $_wpdb = null; if ( $results ) { foreach ( $results as $result ) { - if ( $result->name == $index_name ) { + if ( $result->name === $index_name ) { $r = rand( 0, 50 ); $index_name = $index_name . "_$r"; break; @@ -378,7 +378,7 @@ private function _rewrite_key( $matches ) { $_wpdb = null; if ( $results ) { foreach ( $results as $result ) { - if ( $result->name == $index_name ) { + if ( $result->name === $index_name ) { $r = rand( 0, 50 ); $index_name = $index_name . "_$r"; break; diff --git a/src/wp-includes/sqlite/class-wp-sqlite-db.php b/src/wp-includes/sqlite/class-wp-sqlite-db.php index b1b72521e0bae..d729c9188eb81 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-db.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-db.php @@ -246,7 +246,7 @@ public function query( $query ) { $this->last_error = $this->dbh->get_error_message(); if ( $this->last_error ) { if ( defined( 'WP_INSTALLING' ) && WP_INSTALLING ) { - //$this->suppress_errors(); + // $this->suppress_errors(); } else { $this->print_error( $this->last_error ); From 761850778574c1b81e5d662f34c8b283803c343a Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Wed, 7 Sep 2022 13:36:32 +0300 Subject: [PATCH 05/36] change constant to DATABASE_TYPE --- src/wp-includes/load.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/load.php b/src/wp-includes/load.php index fe1e837060015..fe5e4b7357d83 100644 --- a/src/wp-includes/load.php +++ b/src/wp-includes/load.php @@ -667,7 +667,7 @@ function require_wp_db() { require_once ABSPATH . WPINC . '/class-wpdb.php'; - if ( defined( 'USE_SQLITE' ) && USE_SQLITE ) { + if ( defined( 'DATABASE_TYPE' ) && 'sqlite' === DATABASE_TYPE ) { require_once ABSPATH . WPINC . '/sqlite/db.php'; } From 4af4ab249ae271eb4459af84693f625eb56228ea Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Fri, 9 Sep 2022 10:11:20 +0300 Subject: [PATCH 06/36] update system status info --- src/wp-admin/includes/class-wp-debug-data.php | 128 +++++++++++------- 1 file changed, 77 insertions(+), 51 deletions(-) diff --git a/src/wp-admin/includes/class-wp-debug-data.php b/src/wp-admin/includes/class-wp-debug-data.php index ed6a9e75a47b3..2b452b04ddb03 100644 --- a/src/wp-admin/includes/class-wp-debug-data.php +++ b/src/wp-admin/includes/class-wp-debug-data.php @@ -863,51 +863,75 @@ public static function debug_data() { 'value' => wp_date( 'c', $_SERVER['REQUEST_TIME'] ), ); - // Populate the database debug fields. - if ( is_object( $wpdb->dbh ) ) { - // mysqli or PDO. - $extension = get_class( $wpdb->dbh ); - } else { - // Unknown sql extension. - $extension = null; + $database_type = defined( 'DATABASE_TYPE' ) && 'sqlite' === DATABASE_TYPE ? 'sqlite' : 'mysql'; + $extension = null; + if ( 'mysql' === $database_type ) { + // Populate the database debug fields. + if ( is_object( $wpdb->dbh ) ) { + // mysqli or PDO. + $extension = get_class( $wpdb->dbh ); + } } $server = $wpdb->get_var( 'SELECT VERSION()' ); $client_version = $wpdb->dbh->client_info; - $info['wp-database']['fields']['extension'] = array( - 'label' => __( 'Extension' ), - 'value' => $extension, + $info['wp-database']['fields']['database_type'] = array( + 'label' => __( 'Database type' ), + 'value' => 'sqlite' === $database_type ? 'SQLite' : 'MySQL/MariaDB', ); - $info['wp-database']['fields']['server_version'] = array( - 'label' => __( 'Server version' ), - 'value' => $server, - ); + if ( 'mysql' === $database_type ) { + $info['wp-database']['fields']['extension'] = array( + 'label' => __( 'Extension' ), + 'value' => $extension, + ); - $info['wp-database']['fields']['client_version'] = array( - 'label' => __( 'Client version' ), - 'value' => $client_version, - ); + $info['wp-database']['fields']['server_version'] = array( + 'label' => __( 'Server version' ), + 'value' => $server, + ); - $info['wp-database']['fields']['database_user'] = array( - 'label' => __( 'Database username' ), - 'value' => $wpdb->dbuser, - 'private' => true, - ); + $info['wp-database']['fields']['client_version'] = array( + 'label' => __( 'Client version' ), + 'value' => $client_version, + ); - $info['wp-database']['fields']['database_host'] = array( - 'label' => __( 'Database host' ), - 'value' => $wpdb->dbhost, - 'private' => true, - ); + $info['wp-database']['fields']['database_user'] = array( + 'label' => __( 'Database username' ), + 'value' => $wpdb->dbuser, + 'private' => true, + ); - $info['wp-database']['fields']['database_name'] = array( - 'label' => __( 'Database name' ), - 'value' => $wpdb->dbname, - 'private' => true, - ); + $info['wp-database']['fields']['database_host'] = array( + 'label' => __( 'Database host' ), + 'value' => $wpdb->dbhost, + 'private' => true, + ); + + $info['wp-database']['fields']['database_name'] = array( + 'label' => __( 'Database name' ), + 'value' => $wpdb->dbname, + 'private' => true, + ); + } elseif ( 'sqlite' === $database_type ) { + $info['wp-database']['fields']['database_version'] = array( + 'label' => __( 'Database version' ), + 'value' => class_exists( 'SQLite3' ) ? SQLite3::version()['versionString'] : null, + ); + + $info['wp-database']['fields']['database_file'] = array( + 'label' => __( 'Database file' ), + 'value' => FQDB, + 'private' => true, + ); + + $info['wp-database']['fields']['database_size'] = array( + 'label' => __( 'Database size' ), + 'value' => size_format( filesize( FQDB ) ), + ); + } $info['wp-database']['fields']['database_prefix'] = array( 'label' => __( 'Table prefix' ), @@ -915,27 +939,29 @@ public static function debug_data() { 'private' => true, ); - $info['wp-database']['fields']['database_charset'] = array( - 'label' => __( 'Database charset' ), - 'value' => $wpdb->charset, - 'private' => true, - ); + if ( 'mysql' === $database_type ) { + $info['wp-database']['fields']['database_charset'] = array( + 'label' => __( 'Database charset' ), + 'value' => $wpdb->charset, + 'private' => true, + ); - $info['wp-database']['fields']['database_collate'] = array( - 'label' => __( 'Database collation' ), - 'value' => $wpdb->collate, - 'private' => true, - ); + $info['wp-database']['fields']['database_collate'] = array( + 'label' => __( 'Database collation' ), + 'value' => $wpdb->collate, + 'private' => true, + ); - $info['wp-database']['fields']['max_allowed_packet'] = array( - 'label' => __( 'Max allowed packet size' ), - 'value' => self::get_mysql_var( 'max_allowed_packet' ), - ); + $info['wp-database']['fields']['max_allowed_packet'] = array( + 'label' => __( 'Max allowed packet size' ), + 'value' => self::get_mysql_var( 'max_allowed_packet' ), + ); - $info['wp-database']['fields']['max_connections'] = array( - 'label' => __( 'Max connections number' ), - 'value' => self::get_mysql_var( 'max_connections' ), - ); + $info['wp-database']['fields']['max_connections'] = array( + 'label' => __( 'Max connections number' ), + 'value' => self::get_mysql_var( 'max_connections' ), + ); + } // List must use plugins if there are any. $mu_plugins = get_mu_plugins(); From faf217e76d9de7338a7b60694f8196d6fad4dfdc Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Fri, 9 Sep 2022 10:16:14 +0300 Subject: [PATCH 07/36] add debug info for constant --- src/wp-admin/includes/class-wp-debug-data.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/wp-admin/includes/class-wp-debug-data.php b/src/wp-admin/includes/class-wp-debug-data.php index 2b452b04ddb03..167db2db28524 100644 --- a/src/wp-admin/includes/class-wp-debug-data.php +++ b/src/wp-admin/includes/class-wp-debug-data.php @@ -278,6 +278,11 @@ public static function debug_data() { 'label' => 'WP_MAX_MEMORY_LIMIT', 'value' => WP_MAX_MEMORY_LIMIT, ), + 'DATABASE_TYPE' => array( + 'label' => 'DATABASE_TYPE', + 'value' => ( defined( 'DATABASE_TYPE' ) ? DATABASE_TYPE : __( 'Undefined' ) ), + 'debug' => ( defined( 'DATABASE_TYPE' ) ? DATABASE_TYPE : 'undefined' ), + ), 'WP_DEBUG' => array( 'label' => 'WP_DEBUG', 'value' => WP_DEBUG ? __( 'Enabled' ) : __( 'Disabled' ), From 95280954440cc8b9f7accf1d494e53ff112ef6b7 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Fri, 9 Sep 2022 10:18:25 +0300 Subject: [PATCH 08/36] change wording --- src/wp-admin/includes/class-wp-debug-data.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/class-wp-debug-data.php b/src/wp-admin/includes/class-wp-debug-data.php index 167db2db28524..b93e811d32cb8 100644 --- a/src/wp-admin/includes/class-wp-debug-data.php +++ b/src/wp-admin/includes/class-wp-debug-data.php @@ -922,7 +922,7 @@ public static function debug_data() { ); } elseif ( 'sqlite' === $database_type ) { $info['wp-database']['fields']['database_version'] = array( - 'label' => __( 'Database version' ), + 'label' => __( 'SQLite version' ), 'value' => class_exists( 'SQLite3' ) ? SQLite3::version()['versionString'] : null, ); From 24a8d0aa175facff6482f72ff3ac02f80db868b2 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Fri, 9 Sep 2022 12:17:14 +0300 Subject: [PATCH 09/36] debug tweak --- src/wp-includes/sqlite/class-wp-pdo-engine.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/sqlite/class-wp-pdo-engine.php b/src/wp-includes/sqlite/class-wp-pdo-engine.php index 071c1c780f896..b0e1f51c29388 100644 --- a/src/wp-includes/sqlite/class-wp-pdo-engine.php +++ b/src/wp-includes/sqlite/class-wp-pdo-engine.php @@ -422,8 +422,9 @@ public function query( $statement, $mode = PDO::ATTR_DEFAULT_FETCH_MODE, ...$fet } break; } - if ( defined( 'PDO_DEBUG' ) && PDO_DEBUG === true ) { - file_put_contents( FQDBDIR . 'debug.txt', $this->get_debug_info(), FILE_APPEND ); + $debug_string = $this->get_debug_info(); + if ( defined( 'PDO_DEBUG' ) && PDO_DEBUG === true && $debug_string ) { + error_log( $debug_string ); } return $this->return_value; @@ -1231,7 +1232,7 @@ private function set_error( $line, $function, $message ) { if ( ! $wpdb->show_errors ) { return false; } - file_put_contents( FQDBDIR . 'debug.txt', "Line $line, Function: $function, Message: $message \n", FILE_APPEND ); + error_log( "Line $line, Function: $function, Message: $message" ); } /** From 3cc5da5ce84212e342315ce36241bfdc7f829b96 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Tue, 13 Sep 2022 14:54:18 +0300 Subject: [PATCH 10/36] minor coding tweaks --- .../sqlite/class-wp-pdo-engine.php | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/src/wp-includes/sqlite/class-wp-pdo-engine.php b/src/wp-includes/sqlite/class-wp-pdo-engine.php index b0e1f51c29388..b7b50e45b833b 100644 --- a/src/wp-includes/sqlite/class-wp-pdo-engine.php +++ b/src/wp-includes/sqlite/class-wp-pdo-engine.php @@ -355,6 +355,7 @@ public function query( $statement, $mode = PDO::ATTR_DEFAULT_FETCH_MODE, ...$fet case 'set': $this->return_value = false; break; + case 'foundrows': $_column = array( 'FOUND_ROWS()' => '' ); $column = array(); @@ -369,6 +370,7 @@ public function query( $statement, $mode = PDO::ATTR_DEFAULT_FETCH_MODE, ...$fet $this->found_rows_result = null; } break; + case 'insert': if ( $this->can_insert_multiple_rows ) { $this->execute_insert_query_new( $statement ); @@ -376,6 +378,7 @@ public function query( $statement, $mode = PDO::ATTR_DEFAULT_FETCH_MODE, ...$fet $this->execute_insert_query( $statement ); } break; + case 'create': $this->return_value = $this->execute_create_query( $statement ); break; @@ -616,14 +619,12 @@ private function flush() { */ private function prepare_engine( $query_type = null ) { if ( stripos( $query_type, 'create' ) !== false ) { - $engine = new WP_SQLite_Create_Query(); - } elseif ( stripos( $query_type, 'alter' ) !== false ) { - $engine = new WP_SQLite_Alter_Query(); - } else { - $engine = new WP_PDO_SQLite_Driver(); + return new WP_SQLite_Create_Query(); } - - return $engine; + if ( stripos( $query_type, 'alter' ) !== false ) { + return new WP_SQLite_Alter_Query(); + } + return new WP_PDO_SQLite_Driver(); } /** @@ -771,7 +772,6 @@ private function execute_query( $statement ) { private function extract_variables() { if ( 'create' === $this->query_type ) { $this->prepared_query = $this->rewritten_query; - return; } @@ -997,13 +997,16 @@ private function parse_multiple_inserts( $values ) { $part .= $token; } break; + case "'": $literal = ! $literal; $part .= $token; break; + default: $part .= $token; break; + } } if ( ! empty( $part ) ) { @@ -1226,10 +1229,7 @@ private function set_error( $line, $function, $message ) { ); $this->error_messages[] = $message; $this->is_error = true; - if ( $wpdb->suppress_errors ) { - return false; - } - if ( ! $wpdb->show_errors ) { + if ( $wpdb->suppress_errors || ! $wpdb->show_errors ) { return false; } error_log( "Line $line, Function: $function, Message: $message" ); @@ -1338,6 +1338,7 @@ private function convert_to_index_object() { $_columns['Column_name'] = $col_name; } break; + case 'index': $_columns['Non_unique'] = 1; if ( stripos( $row->sql, 'unique' ) !== false ) { @@ -1349,6 +1350,7 @@ private function convert_to_index_object() { } $_columns['Key_name'] = $row->name; break; + } $_columns['Table'] = $row->tbl_name; $_columns['Collation'] = null; @@ -1364,8 +1366,7 @@ private function convert_to_index_object() { preg_match( '/WHERE\\s*(.*)$/im', $this->queries[0], $match ); list($key, $value) = explode( '=', $match[1] ); $key = trim( $key ); - $value = preg_replace( "/[\';]/", '', $value ); - $value = trim( $value ); + $value = trim( preg_replace( "/[\';]/", '', $value ) ); foreach ( $_results as $result ) { if ( ! empty( $result->$key ) && is_scalar( $result->$key ) && stripos( $value, $result->$key ) !== false ) { unset( $_results ); @@ -1375,6 +1376,7 @@ private function convert_to_index_object() { } } } + $this->results = $_results; } @@ -1384,22 +1386,15 @@ private function convert_to_index_object() { * @access private */ private function convert_result_check_or_analyze() { - $results = array(); - $_columns = array( - 'Table' => '', - 'Op' => 'analyze', - 'Msg_type' => 'status', - 'Msg_text' => 'Table is already up to date', - ); - if ( 'check' === $this->query_type ) { - $_columns = array( + $is_check = 'check' === $this->query_type; + $_results[] = new WP_SQLite_Object_Array( + array( 'Table' => '', - 'Op' => 'check', + 'Op' => $is_check ? 'check' : 'analyze', 'Msg_type' => 'status', - 'Msg_text' => 'OK', - ); - } - $_results[] = new WP_SQLite_Object_Array( $_columns ); + 'Msg_text' => $is_check ? 'OK' : __( 'Table is already up to date' ), + ) + ); $this->results = $_results; } From e71dba7ed36a60f012f81d613d5cd2a13300645c Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Tue, 13 Sep 2022 15:07:10 +0300 Subject: [PATCH 11/36] site-health improvements --- src/wp-admin/includes/class-wp-site-health.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index da6c81e985219..e9d06eb63527e 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -15,6 +15,7 @@ class WP_Site_Health { private $is_recommended_mysql_version; public $is_mariadb = false; + public $is_sqlite = false; private $mysql_server_version = ''; private $mysql_required_version = '5.5'; private $mysql_recommended_version = '8.0'; @@ -208,6 +209,7 @@ private function prepare_sql_data() { global $wpdb; $mysql_server_type = $wpdb->db_server_info(); + $this->is_sqlite = defined( 'DATABASE_TYPE' ) && 'sqlite' === DATABASE_TYPE ? 'sqlite' : 'mysql'; $this->mysql_server_version = $wpdb->get_var( 'SELECT VERSION()' ); @@ -1230,7 +1232,7 @@ public function get_test_sql_server() { $db_dropin = file_exists( WP_CONTENT_DIR . '/db.php' ); - if ( ! $this->is_recommended_mysql_version ) { + if ( ! $this->is_sqlite && ! $this->is_recommended_mysql_version ) { $result['status'] = 'recommended'; $result['label'] = __( 'Outdated SQL server' ); @@ -1246,7 +1248,7 @@ public function get_test_sql_server() { ); } - if ( ! $this->is_acceptable_mysql_version ) { + if ( ! $this->is_sqlite && ! $this->is_acceptable_mysql_version ) { $result['status'] = 'critical'; $result['label'] = __( 'Severely outdated SQL server' ); @@ -1310,6 +1312,10 @@ public function get_test_utf8mb4_support() { 'test' => 'utf8mb4_support', ); + if ( $this->is_sqlite ) { + return $result; + } + if ( ! $this->is_mariadb ) { if ( version_compare( $this->mysql_server_version, '5.5.3', '<' ) ) { $result['status'] = 'recommended'; From b5b14d9ebdc0a04f491931391c4304c7afc65b1a Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Tue, 13 Sep 2022 15:12:01 +0300 Subject: [PATCH 12/36] Fix PHP Warning by adding a db_server_info() method --- src/wp-includes/sqlite/class-wp-sqlite-db.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/wp-includes/sqlite/class-wp-sqlite-db.php b/src/wp-includes/sqlite/class-wp-sqlite-db.php index d729c9188eb81..daad39aee101a 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-db.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-db.php @@ -314,4 +314,15 @@ public function has_cap( $db_cap ) { public function db_version() { return '5.5'; } + + /** + * Retrieves full database server information. + * + * @since 5.5.0 + * + * @return string|false Server info on success, false on failure. + */ + public function db_server_info() { + return SQLite3::version()['versionString']; + } } From 1d186049a506c0216feb39af667d16263f2d24b1 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Thu, 23 Feb 2023 10:56:31 +0200 Subject: [PATCH 13/36] Update implementation --- src/wp-admin/includes/upgrade.php | 64 +- .../sqlite/class-wp-pdo-engine.php | 1454 -------- .../sqlite/class-wp-pdo-sqlite-driver.php | 788 ----- .../sqlite/class-wp-sqlite-alter-query.php | 598 ---- .../sqlite/class-wp-sqlite-create-query.php | 488 --- src/wp-includes/sqlite/class-wp-sqlite-db.php | 130 +- .../sqlite/class-wp-sqlite-lexer.php | 2582 ++++++++++++++ .../sqlite/class-wp-sqlite-object-array.php | 26 - ...-wp-sqlite-pdo-user-defined-functions.php} | 560 ++- .../sqlite/class-wp-sqlite-query-rewriter.php | 343 ++ .../sqlite/class-wp-sqlite-token.php | 330 ++ .../sqlite/class-wp-sqlite-translator.php | 3035 +++++++++++++++++ src/wp-includes/sqlite/db.php | 306 +- 13 files changed, 6734 insertions(+), 3970 deletions(-) delete mode 100644 src/wp-includes/sqlite/class-wp-pdo-engine.php delete mode 100644 src/wp-includes/sqlite/class-wp-pdo-sqlite-driver.php delete mode 100644 src/wp-includes/sqlite/class-wp-sqlite-alter-query.php delete mode 100644 src/wp-includes/sqlite/class-wp-sqlite-create-query.php create mode 100644 src/wp-includes/sqlite/class-wp-sqlite-lexer.php delete mode 100644 src/wp-includes/sqlite/class-wp-sqlite-object-array.php rename src/wp-includes/sqlite/{class-wp-pdo-sqlite-user-defined-functions.php => class-wp-sqlite-pdo-user-defined-functions.php} (57%) create mode 100644 src/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php create mode 100644 src/wp-includes/sqlite/class-wp-sqlite-token.php create mode 100644 src/wp-includes/sqlite/class-wp-sqlite-translator.php diff --git a/src/wp-admin/includes/upgrade.php b/src/wp-admin/includes/upgrade.php index cb3939108b856..bd904067ee034 100644 --- a/src/wp-admin/includes/upgrade.php +++ b/src/wp-admin/includes/upgrade.php @@ -51,7 +51,12 @@ function wp_install( $blog_title, $user_name, $user_email, $is_public, $deprecat wp_check_mysql_version(); wp_cache_flush(); - make_db_current_silent(); + + if ( defined( 'DATABASE_TYPE' ) && 'sqlite' === DATABASE_TYPE ) { + sqlite_make_db_sqlite(); + } else { + make_db_current_silent(); + } populate_options(); populate_roles(); @@ -3264,6 +3269,63 @@ function make_db_current_silent( $tables = 'all' ) { dbDelta( $tables ); } + +/** + * Function to create tables according to the schemas of WordPress. + * + * This is executed only once while installation. + * + * @since 1.0.0 + * + * @return boolean + */ +function sqlite_make_db_sqlite() { + include_once ABSPATH . 'wp-admin/includes/schema.php'; + + $table_schemas = wp_get_db_schema(); + $queries = explode( ';', $table_schemas ); + try { + $pdo = new PDO( 'sqlite:' . FQDB, null, null, array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ) ); // phpcs:ignore WordPress.DB.RestrictedClasses + } catch ( PDOException $err ) { + $err_data = $err->errorInfo; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + wp_die( $err_data[2], 'Database Error!' ); + } + + $translator = new WP_SQLite_Translator( $pdo, $GLOBALS['table_prefix'] ); + $query = null; + + try { + $pdo->beginTransaction(); + foreach ( $queries as $query ) { + $query = trim( $query ); + if ( empty( $query ) ) { + continue; + } + + $translation = $translator->translate( $query ); + foreach ( $translation->queries as $query ) { + $stmt = $pdo->prepare( $query->sql ); + $stmt->execute( $query->params ); + } + } + $pdo->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(); + wp_die( $err_data[2], 'Database Error!' ); + } + } + + $pdo = null; + + return true; +} + /** * Creates a site theme from an existing theme. * diff --git a/src/wp-includes/sqlite/class-wp-pdo-engine.php b/src/wp-includes/sqlite/class-wp-pdo-engine.php deleted file mode 100644 index b7b50e45b833b..0000000000000 --- a/src/wp-includes/sqlite/class-wp-pdo-engine.php +++ /dev/null @@ -1,1454 +0,0 @@ -prepare_directory(); - } - $dsn = 'sqlite:' . FQDB; - if ( isset( $GLOBALS['@pdo'] ) ) { - $this->pdo = $GLOBALS['@pdo']; - } else { - $locked = false; - $status = 0; - do { - try { - $this->pdo = new PDO( $dsn, null, null, array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ) ); - new WP_PDO_SQLite_User_Defined_Functions( $this->pdo ); - $GLOBALS['@pdo'] = $this->pdo; - } catch ( PDOException $ex ) { - $status = $ex->getCode(); - if ( 5 == $status || 6 == $status ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison - $locked = true; - } else { - $err_message = $ex->getMessage(); - } - } - } while ( $locked ); - if ( $status > 0 ) { - $message = 'Database initialization error!
' . - 'Code: ' . $status . - ( isset( $err_message ) ? '
Error Message: ' . $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'] ) - ); - file_put_contents( FQDBDIR . 'mem_debug.txt', $message, FILE_APPEND ); - - return true; - } - if ( stripos( $max, 'M' ) !== false ) { - $max = (int) $max * 1024 * 1024; - } - $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 - ); - file_put_contents( FQDBDIR . 'mem_debug.txt', $message, FILE_APPEND ); - } - } - - //$this->pdo = null; - 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( $this->get_sqlite_version(), '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 direcotry 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.'; - wp_die( $message, '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 ); - $message = 'Unable to create a file in the directory! Please check your server settings.'; - 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.'; - 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 - * @param array $fetch_mode_args - * - * @return mixed according to the query type - * @see PDO::query() - */ - public function query( $statement, $mode = PDO::ATTR_DEFAULT_FETCH_MODE, ...$fetch_mode_args ) { - $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' ) . '

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

', - $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; - //foreach ($this->found_rows_result[0] as $key => $value) { - //$_column['FOUND_ROWS()'] = $value; - //} - $column[] = new WP_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 ); - } else { - $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; - $pattern = '/^\\s*(DROP\\s*INDEX\\s*.*?)\\s*ON\\s*(.*)/im'; - if ( preg_match( $pattern, $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 ); - } else { - // Error - } - 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 WP_SQLite_Object_Array( $data ); - $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 ''; - } - $output = '
 
'; - if ( false === $this->is_error ) { - //return $output; - return ''; - } - $output .= "
Queries made or created this session were
\r\n\t
    \r\n"; - foreach ( $this->queries as $q ) { - $output .= "\t\t
  1. " . $q . "
  2. \r\n"; - } - $output .= "\t
\r\n
"; - foreach ( $this->error_messages as $num => $m ) { - $output .= "
Error occurred at line {$this->errors[$num]['line']} in Function {$this->errors[$num]['function']}.
Error message was: $m
"; - } - - 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 programatically. - * Needs to be fixed some other way. - * - * @param string $query_type - * - * @return object reference to apropreate driver - */ - private function prepare_engine( $query_type = null ) { - if ( stripos( $query_type, 'create' ) !== false ) { - return new WP_SQLite_Create_Query(); - } - if ( stripos( $query_type, 'alter' ) !== false ) { - return new WP_SQLite_Alter_Query(); - } - return new WP_PDO_SQLite_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 ); // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison - - 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 of PDO statement - * - * @return boolean - */ - 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 ); - - //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': - //case "foundrows": - $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 - * - * @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 - } - //$this->extracted_variables[] = $param; - $key = ':param_' . $this->param_num++; - $this->extracted_variables[] = $param; - //return the placeholder - //return ' ? '; - 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 - * - * @return boolean|string - */ - 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 - */ - 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 - */ - 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 - * - * @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 = ''; - //$queries = explode(";", $this->rewritten_query); - try { - $this->beginTransaction(); - 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 ); - } - $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 - * - * @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 - * - * @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 WP_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 return dummy data. - * - * @param string - * - * @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 WP_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 - */ - 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 to indicate the function name where the error occurred. - * @param string $message - * - * @return boolean - */ - 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 WP_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 ) { - $_columns['Field'] = $row->name; - $_columns['Type'] = $row->type; - $_columns['Null'] = $row->notnull ? 'NO' : 'YES'; - $_columns['Key'] = $row->pk ? 'PRI' : ''; - $_columns['Default'] = $row->dflt_value; - $_results[] = new WP_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 WP_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 WP_SQLite_Object_Array( - array( - 'Table' => '', - 'Op' => $is_check ? 'check' : 'analyze', - 'Msg_type' => 'status', - 'Msg_text' => $is_check ? 'OK' : __( 'Table is already up to date' ), - ) - ); - $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 ); - - 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() - */ - public function commit() { - $this->pdo->commit(); - $this->has_active_transaction = false; - } - - /** - * Method to call PDO::rollBack(). - * - * @see PDO::rollBack() - */ - public function rollBack() { - $this->pdo->rollBack(); - $this->has_active_transaction = false; - } -} diff --git a/src/wp-includes/sqlite/class-wp-pdo-sqlite-driver.php b/src/wp-includes/sqlite/class-wp-pdo-sqlite-driver.php deleted file mode 100644 index 7e4ca2fd03a1c..0000000000000 --- a/src/wp-includes/sqlite/class-wp-pdo-sqlite-driver.php +++ /dev/null @@ -1,788 +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 ); - //$unlimited_query = preg_replace('/\\bGROUP\\s*BY\\s*.*/imsx', '', $unlimited_query); - // we no longer use SELECT COUNT query - //$unlimited_query = $this->_transform_to_count($unlimited_query); - $_wpdb = new WP_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 WP_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 WP_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 WP_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 WP_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... - * - * @return void - * @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. - * - * @return void - * @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) - * - * @return void - * @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 - * - * @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 handl 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. - * - * @return void - * @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 WP_SQLite_DB(); - $indexes = $_wpdb->get_results( "SHOW INDEX FROM {$table_name}" ); - if ( ! empty( $indexes ) ) { - foreach ( $indexes as $index ) { - if ( 0 === $index->Non_unique ) { - $unique_keys_for_cond[] = $index->Column_name; - if ( strpos( $index->Column_name, ',' ) !== false ) { - $unique_keys_for_check = array_merge( - $unique_keys_for_check, - explode( ',', $index->Column_name ) - ); - } else { - $unique_keys_for_check[] = $index->Column_name; - } - } - } - $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 ) { - $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 WP_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. - * - * wp-admin/includes/upgrade.php contains '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 WP_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/src/wp-includes/sqlite/class-wp-sqlite-alter-query.php b/src/wp-includes/sqlite/class-wp-sqlite-alter-query.php deleted file mode 100644 index 2e3ab05012062..0000000000000 --- a/src/wp-includes/sqlite/class-wp-sqlite-alter-query.php +++ /dev/null @@ -1,598 +0,0 @@ -command_tokenizer( $single_command ); - if ( ! empty( $command_tokens ) ) { - $tokens = array_merge( $tmp_tokens, $command_tokens ); - } else { - $this->_query = 'SELECT 1=1'; - - return $this->_query; - } - $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 ) ); - } - } else { - $this->_query = 'SELECT 1=1'; - } - - return $this->_query; - } - - /** - * Function to analyze ALTER TABLE command and sets the data to an array. - * - * @param string $command - * - * @return boolean|array - * @access private - */ - private function command_tokenizer( $command ) { - $tokens = array(); - if ( preg_match( - '/^(ADD|DROP|RENAME|MODIFY|CHANGE|ALTER)\\s*(\\w+)?\\s*(\\w+(\(.+\)|))?\\s*/ims', - $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; - } elseif ( 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 of string $queries - * - * @return string - */ - private function handle_single_command( $queries ) { - $tokenized_query = $queries; - $query = 'SELECT 1=1'; - if ( stripos( $tokenized_query['command'], 'add column' ) !== false ) { - $column_def = $this->convert_field_types( $tokenized_query['column_name'], $tokenized_query['column_def'] ); - $query = "ALTER TABLE {$tokenized_query['table_name']} ADD COLUMN {$tokenized_query['column_name']} $column_def"; - } elseif ( stripos( $tokenized_query['command'], 'rename' ) !== false ) { - $query = "ALTER TABLE {$tokenized_query['table_name']} RENAME TO {$tokenized_query['column_name']}"; - } elseif ( stripos( $tokenized_query['command'], 'add index' ) !== false ) { - $unique = isset( $tokenized_query['unique'] ) ? 'UNIQUE' : ''; - $query = "CREATE $unique INDEX IF NOT EXISTS {$tokenized_query['index_name']} ON {$tokenized_query['table_name']} {$tokenized_query['column_name']}"; - } elseif ( stripos( $tokenized_query['command'], 'drop index' ) !== false ) { - $query = "DROP INDEX IF EXISTS {$tokenized_query['index_name']}"; - } - - return $query; - } - - /** - * Function to handle ADD PRIMARY KEY. - * - * @access private - * - * @param array of string $queries - * - * @return array of string - */ - private function handle_add_primary_key( $queries ) { - $tokenized_query = $queries; - $tbl_name = $tokenized_query['table_name']; - $temp_table = 'temp_' . $tokenized_query['table_name']; - $_wpdb = new WP_SQLite_DB(); - $query_obj = $_wpdb->get_results( "SELECT sql FROM sqlite_master WHERE tbl_name='$tbl_name'" ); - $_wpdb = null; - for ( $i = 0; $i < count( $query_obj ); $i++ ) { - $index_queries[ $i ] = $query_obj[ $i ]->sql; - } - $table_query = array_shift( $index_queries ); - $table_query = str_replace( $tokenized_query['table_name'], $temp_table, $table_query ); - $table_query = rtrim( $table_query, ')' ); - $table_query = ", PRIMARY KEY {$tokenized_query['column_name']}"; - $query[] = $table_query; - $query[] = "INSERT INTO $temp_table SELECT * FROM {$tokenized_query['table_name']}"; - $query[] = "DROP TABLE IF EXISTS {$tokenized_query['table_name']}"; - $query[] = "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 of string $queries - * - * @return array of string - */ - private function handle_drop_primary_key( $queries ) { - $tokenized_query = $queries; - $temp_table = 'temp_' . $tokenized_query['table_name']; - $_wpdb = new WP_SQLite_DB(); - $query_obj = $_wpdb->get_results( "SELECT sql FROM sqlite_master WHERE tbl_name='{$tokenized_query['table_name']}'" ); - $_wpdb = null; - 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[] = $table_query; - $query[] = "INSERT INTO $temp_table SELECT * FROM {$tokenized_query['table_name']}"; - $query[] = "DROP TABLE IF EXISTS {$tokenized_query['table_name']}"; - $query[] = "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 of string $queries - * - * @return string|array of string - */ - 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'] ); - $_wpdb = new WP_SQLite_DB(); - $query_obj = $_wpdb->get_results( "SELECT sql FROM sqlite_master WHERE tbl_name='{$tokenized_query['table_name']}'" ); - $_wpdb = null; - 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'; - } elseif ( preg_match( "/{$tokenized_query['column_name']}\\s*{$column_def}\\s*[,)]/i", $create_query ) ) { - return 'SELECT 1=1'; - } - $create_query = preg_replace( "/{$tokenized_query['table_name']}/i", $temp_table, $create_query ); - if ( preg_match( "/\\b{$tokenized_query['column_name']}\\s*.*(?=,)/ims", $create_query ) ) { - $create_query = preg_replace( - "/\\b{$tokenized_query['column_name']}\\s*.*(?=,)/ims", - "{$tokenized_query['column_name']} {$column_def}", - $create_query - ); - } elseif ( preg_match( "/\\b{$tokenized_query['column_name']}\\s*.*(?=\))/ims", $create_query ) ) { - $create_query = preg_replace( - "/\\b{$tokenized_query['column_name']}\\s*.*(?=\))/ims", - "{$tokenized_query['column_name']} {$column_def}", - $create_query - ); - } - $query[] = $create_query; - $query[] = "INSERT INTO $temp_table SELECT * FROM {$tokenized_query['table_name']}"; - $query[] = "DROP TABLE IF EXISTS {$tokenized_query['table_name']}"; - $query[] = "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 of string $queries - * - * @return string|array of string - */ - 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'] ); - $_wpdb = new WP_SQLite_DB(); - $col_obj = $_wpdb->get_results( "SHOW COLUMNS FROM {$tokenized_query['table_name']}" ); - foreach ( $col_obj as $col ) { - if ( stripos( $col->Field, $tokenized_query['old_column'] ) !== false ) { - $col_check = true; - } - $old_fields .= $col->Field . ','; - } - if ( false === $col_check ) { - $_wpdb = null; - - return 'SELECT 1=1'; - } - $old_fields = rtrim( $old_fields, ',' ); - $new_fields = str_ireplace( $tokenized_query['old_column'], $column_name, $old_fields ); - $query_obj = $_wpdb->get_results( "SELECT sql FROM sqlite_master WHERE tbl_name='{$tokenized_query['table_name']}'" ); - $_wpdb = null; - 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 ); - if ( preg_match( "/\\b{$tokenized_query['old_column']}\\s*(.+?)(?=,)/ims", $create_query, $match ) ) { - if ( stripos( trim( $match[1] ), $column_def ) !== false ) { - return 'SELECT 1=1'; - } - $create_query = preg_replace( - "/\\b{$tokenized_query['old_column']}\\s*.+?(?=,)/ims", - "{$column_name} {$column_def}", - $create_query, - 1 - ); - } elseif ( preg_match( "/\\b{$tokenized_query['old_column']}\\s*(.+?)(?=\))/ims", $create_query, $match ) ) { - if ( stripos( trim( $match[1] ), $column_def ) !== false ) { - return 'SELECT 1=1'; - } - $create_query = preg_replace( - "/\\b{$tokenized_query['old_column']}\\s*.*(?=\))/ims", - "{$column_name} {$column_def}", - $create_query, - 1 - ); - } - $query[] = $create_query; - $query[] = "INSERT INTO $temp_table ($new_fields) SELECT $old_fields FROM {$tokenized_query['table_name']}"; - $query[] = "DROP TABLE IF EXISTS {$tokenized_query['table_name']}"; - $query[] = "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 of string $queries - * - * @return string|array of string - */ - 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; - } - $_wpdb = new WP_SQLite_DB(); - $query_obj = $_wpdb->get_results( "SELECT sql FROM sqlite_master WHERE tbl_name='{$tokenized_query['table_name']}'" ); - $_wpdb = null; - 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'; - } - if ( preg_match( - "/\\s*({$tokenized_query['column_name']})\\s*(.*)?(DEFAULT\\s*.*)[,)]/im", - $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] ); - $pattern = "/$col_name\\s*$col_def_esc\\s*$old_default/im"; - $replacement = $col_name . ' ' . $checked_col_def; - if ( ! is_null( $def_value ) ) { - $replacement .= ' ' . $def_value; - } - $create_query = preg_replace( $pattern, $replacement, $create_query ); - $create_query = str_ireplace( $tokenized_query['table_name'], $temp_table, $create_query ); - } elseif ( preg_match( "/\\s*({$tokenized_query['column_name']})\\s*(.*)?[,)]/im", $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 ); - $pattern = "/$col_name\\s*$col_def_esc/im"; - $replacement = $col_name . ' ' . $checked_col_def; - if ( ! is_null( $def_value ) ) { - $replacement .= ' ' . $def_value; - } - $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[] = $create_query; - $query[] = "INSERT INTO $temp_table SELECT * FROM {$tokenized_query['table_name']}"; - $query[] = "DROP TABLE IF EXISTS {$tokenized_query['table_name']}"; - $query[] = "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 - * @param string $col_def - * - * @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 ); - // colDef 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/src/wp-includes/sqlite/class-wp-sqlite-create-query.php b/src/wp-includes/sqlite/class-wp-sqlite-create-query.php deleted file mode 100644 index a4c485dc54425..0000000000000 --- a/src/wp-includes/sqlite/class-wp-sqlite-create-query.php +++ /dev/null @@ -1,488 +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 WP_PDO_Engine.class.php - // 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. - * - * @param array $matches an array of matches from the Regex - * - * @access private - * @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 WP_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 $matches - * - * @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 WP_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 - * - * @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/src/wp-includes/sqlite/class-wp-sqlite-db.php b/src/wp-includes/sqlite/class-wp-sqlite-db.php index daad39aee101a..1789cb90e0707 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-db.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-db.php @@ -1,4 +1,10 @@ charset = 'utf8mb4'; } /** @@ -29,13 +39,27 @@ public function __construct() { * * @see wpdb::set_charset() * - * @param resource $dbh The resource given by mysql_connect - * @param string $charset Optional. The character set. Default null. - * @param string $collate Optional. The collation. Default null. + * @param resource $dbh The resource given by mysql_connect. + * @param string $charset Optional. The character set. Default null. + * @param string $collate Optional. The collation. Default null. */ 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() * @@ -46,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. * @@ -53,7 +87,7 @@ public function set_sql_mode( $modes = array() ) { * * @see wpdb::select() * - * @param string $db MySQL database name + * @param string $db MySQL database name. Not used. * @param resource|null $dbh Optional link identifier. */ public function select( $db, $dbh = null ) { @@ -67,12 +101,12 @@ public function select( $db, $dbh = null ) { * * @see wpdb::_real_escape() * - * @param string $string to escape + * @param string $str The string to escape. * * @return string escaped */ - function _real_escape( $string ) { - return addslashes( $string ); + function _real_escape( $str ) { + return addslashes( $str ); } /** @@ -99,11 +133,11 @@ public function esc_like( $text ) { * * @see wpdb::print_error() * - * @global array $EZSQL_ERROR Stores error information of query and error string + * @global array $EZSQL_ERROR Stores error information of query and error string. * - * @param string $str The error to display + * @param string $str The error to display. * - * @return bool False if the showing of errors is disabled. + * @return bool|void False if the showing of errors is disabled. */ public function print_error( $str = '' ) { global $EZSQL_ERROR; @@ -124,18 +158,14 @@ public function print_error( $str = '' ) { wp_load_translations_early(); $caller = $this->get_caller(); - if ( $caller ) { - $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' ), - $str, - $this->last_query, - $caller - ); - } else { - /* translators: 1: Database error message, 2: SQL query. */ - $error_str = sprintf( __( 'WordPress database error %1$s for query %2$s' ), $str, $this->last_query ); - } + $caller = $caller ? $caller : '(unknown)'; + + $error_str = sprintf( + 'WordPress database error %1$s for query %2$s made by %3$s', + $str, + $this->last_query, + $caller + ); error_log( $error_str ); @@ -155,10 +185,11 @@ public function print_error( $str = '' ) { $str = htmlspecialchars( $str, ENT_QUOTES ); $query = htmlspecialchars( $this->last_query, ENT_QUOTES ); - print "
-

WordPress database error: [$str]
- $query

-
"; + printf( + '

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

', + $str, + '' . $query . '' + ); } } @@ -187,18 +218,32 @@ public function flush() { * * @see wpdb::db_connect() * - * @param bool $allow_bail + * @param bool $allow_bail Not used. + * @return void */ public function db_connect( $allow_bail = true ) { + if ( $this->dbh ) { + return; + } $this->init_charset(); - $this->dbh = new WP_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; } /** * Method to dummy out wpdb::check_connection() * - * @param bool $allow_bail + * @param bool $allow_bail Not used. * * @return bool */ @@ -214,7 +259,7 @@ public function check_connection( $allow_bail = true ) { * * @see wpdb::query() * - * @param string $query Database query + * @param string $query Database query. * * @return int|false Number of rows affected/selected or false on error */ @@ -225,11 +270,9 @@ public function query( $query ) { $query = apply_filters( 'query', $query ); - $return_val = 0; $this->flush(); - $this->func_call = "\$db->query(\"$query\")"; - + $this->func_call = "\$db->query(\"$query\")"; $this->last_query = $query; if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { @@ -245,18 +288,14 @@ public function query( $query ) { $this->last_error = $this->dbh->get_error_message(); if ( $this->last_error ) { - if ( defined( 'WP_INSTALLING' ) && WP_INSTALLING ) { - // $this->suppress_errors(); - } else { - $this->print_error( $this->last_error ); - - return false; - } + $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(); } + if ( preg_match( '/^\\s*(insert|delete|update|replace)\s/i', $query ) ) { $this->rows_affected = $this->dbh->get_affected_rows(); if ( preg_match( '/^\s*(insert|replace)\s/i', $query ) ) { @@ -264,6 +303,7 @@ public function query( $query ) { } return $this->rows_affected; } + $this->last_result = $this->dbh->get_query_results(); $this->num_rows = $this->dbh->get_num_rows(); return $this->num_rows; @@ -296,7 +336,7 @@ protected function load_col_info() { * 'group_concat', 'subqueries', 'set_charset', * 'utf8mb4', or 'utf8mb4_520'. * - * @return int|false Whether the database feature is supported, false otherwise. + * @return bool Whether the database feature is supported, false otherwise. */ public function has_cap( $db_cap ) { return 'subqueries' === strtolower( $db_cap ); @@ -318,8 +358,6 @@ public function db_version() { /** * Retrieves full database server information. * - * @since 5.5.0 - * * @return string|false Server info on success, false on failure. */ public function db_server_info() { diff --git a/src/wp-includes/sqlite/class-wp-sqlite-lexer.php b/src/wp-includes/sqlite/class-wp-sqlite-lexer.php new file mode 100644 index 0000000000000..109892f264554 --- /dev/null +++ b/src/wp-includes/sqlite/class-wp-sqlite-lexer.php @@ -0,0 +1,2582 @@ + + */ + 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 + */ + public 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( int $type, int $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/src/wp-includes/sqlite/class-wp-sqlite-object-array.php b/src/wp-includes/sqlite/class-wp-sqlite-object-array.php deleted file mode 100644 index ee04443ab2faf..0000000000000 --- a/src/wp-includes/sqlite/class-wp-sqlite-object-array.php +++ /dev/null @@ -1,26 +0,0 @@ - $value ) { - if ( is_array( $value ) ) { - if ( ! $node ) { - $node =& $this; - } - $node->$key = new stdClass(); - self::__construct( $value, $node->$key ); - } else { - if ( ! $node ) { - $node =& $this; - } - $node->$key = $value; - } - } - } -} diff --git a/src/wp-includes/sqlite/class-wp-pdo-sqlite-user-defined-functions.php b/src/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php similarity index 57% rename from src/wp-includes/sqlite/class-wp-pdo-sqlite-user-defined-functions.php rename to src/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php index 06186e6c4eaab..77728e074a42b 100644 --- a/src/wp-includes/sqlite/class-wp-pdo-sqlite-user-defined-functions.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php @@ -1,4 +1,10 @@ - * new WP_PDO_SQLite_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 WP_PDO_SQLite_User_Defined_Functions { +class WP_SQLite_PDO_User_Defined_Functions { /** * The class constructor * * Initializes the use defined functions to PDO object with PDO::sqliteCreateFunction(). * - * @param PDO $pdo + * @param PDO $pdo The PDO object. */ public function __construct( $pdo ) { if ( ! $pdo ) { @@ -33,7 +39,7 @@ public function __construct( $pdo ) { } /** - * array to define MySQL function => function defined with PHP. + * Array to define MySQL function => function defined with PHP. * * Replaced functions must be public. * @@ -41,31 +47,28 @@ 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', @@ -84,39 +87,6 @@ public function __construct( $pdo ) { 'version' => 'version', ); - /** - * Method to extract the month value from the date. - * - * @param string 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 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 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. * @@ -124,7 +94,7 @@ public function day( $field ) { * from '1970-01-01 00:00:00' GMT). Used with the argument, it changes the value * to the timestamp. * - * @param string representing the date formatted as '0000-00-00 00:00:00'. + * @param string $field Representing the date formatted as '0000-00-00 00:00:00'. * * @return number of unsigned integer */ @@ -132,51 +102,16 @@ public function unix_timestamp( $field = null ) { return is_null( $field ) ? time() : strtotime( $field ); } - /** - * Method to emulate MySQL SECOND() function. - * - * @param string 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 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 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 of unix timestamp - * @param string to 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 + // Convert to ISO time. $date = gmdate( 'Y-m-d H:i:s', $field ); return is_null( $format ) ? $date : $this->dateformat( $date, $format ); @@ -203,7 +138,7 @@ public function curdate() { /** * Method to emulate MySQL CHAR_LENGTH() function. * - * @param string + * @param string $field The string to be measured. * * @return int unsigned integer for the length of the argument. */ @@ -214,7 +149,7 @@ public function char_length( $field ) { /** * Method to emulate MySQL MD5() function. * - * @param string + * @param string $field The string to be hashed. * * @return string of the md5 hash value of the argument. */ @@ -238,27 +173,11 @@ 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 - * @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. * - * @param string date formatted as '0000-00-00' or datetime as '0000-00-00 00:00:00'. - * @param string $format + * @param string $date Formatted as '0000-00-00' or datetime as '0000-00-00 00:00:00'. + * @param string $format The string format. * * @return string formatted according to $format */ @@ -295,155 +214,224 @@ public function dateformat( $date, $format ) { '%Y' => 'Y', '%y' => 'y', ); - $t = strtotime( $date ); - $format = strtr( $format, $mysql_php_date_formats ); - $output = gmdate( $format, $t ); - return $output; + $time = strtotime( $date ); + $format = strtr( $format, $mysql_php_date_formats ); + + return gmdate( $format, $time ); } /** - * 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'. - * @throws Exception + * @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'. - * @throws Exception + * @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'; - } + /** + * 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'. @@ -457,7 +445,7 @@ public function date( $date ) { * * This function returns true if the argument is null, and true if not. * - * @param various types $field + * @param mixed $field The field to be tested. * * @return boolean */ @@ -483,42 +471,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 $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. - * - * @param string - * - * @return NULL if the argument is null | string conatenated 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. * @@ -526,23 +511,16 @@ public function concat() { * If the same value is found, it returns the position of that value. If not, it * returns 0. * - * @param int...|float... variable number of string, integer or double - * - * @return int unsigned integer + * @return int */ public function field() { - global $wpdb; $num_args = func_num_args(); - if ( $num_args < 2 or is_null( func_get_arg( 0 ) ) ) { + 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; @@ -568,10 +546,7 @@ public function field() { * Used without an argument, it returns false. This returned value will be * rewritten to 0, because SQLite doesn't understand true/false value. * - * @param integer representing the base of the logarithm, which is optional. - * @param double value to turn into logarithm. - * - * @return double | NULL + * @return double|null */ public function log() { $num_args = func_num_args(); @@ -620,8 +595,8 @@ public function greatest() { * * This function is meaningless in SQLite, so we do nothing. * - * @param string $name - * @param integer $timeout + * @param string $name Not used. + * @param integer $timeout Not used. * * @return string */ @@ -634,7 +609,7 @@ public function get_lock( $name, $timeout ) { * * This function is meaningless in SQLite, so we do nothing. * - * @param string $name + * @param string $name Not used. * * @return string */ @@ -648,27 +623,26 @@ public function release_lock( $name ) { * This is MySQL alias for upper() function. This function rewrites it * to SQLite compatible name upper(). * - * @param string + * @param string $content String to be converted to uppercase. * * @return string SQLite compatible function name. */ - public function ucase( $string ) { - return "upper($string)"; + public function ucase( $content ) { + return "upper($content)"; } /** * Method to emulate MySQL LCASE() function. * - * * This is MySQL alias for lower() function. This function rewrites it * to SQLite compatible name lower(). * - * @param string + * @param string $content String to be converted to lowercase. * * @return string SQLite compatible function name. */ - public function lcase( $string ) { - return "lower($string)"; + public function lcase( $content ) { + return "lower($content)"; } /** @@ -676,7 +650,7 @@ public function lcase( $string ) { * * This function gets 4 or 8 bytes integer and turn it into the network address. * - * @param unsigned long integer + * @param integer $num Long integer. * * @return string */ @@ -689,7 +663,7 @@ public function inet_ntoa( $num ) { * * This function gets the network address and turns it into integer. * - * @param string + * @param string $addr Network address. * * @return int long integer */ @@ -702,8 +676,8 @@ public function inet_aton( $addr ) { * * This function compares two dates value and returns the difference. * - * @param string start - * @param string end + * @param string $start Start date. + * @param string $end End date. * * @return string */ @@ -722,9 +696,9 @@ public function datediff( $start, $end ) { * it returns 0. If mbstring extension is loaded, mb_strpos() function is * used. * - * @param string needle - * @param string haystack - * @param integer position + * @param string $substr Needle. + * @param string $str Haystack. + * @param integer $pos Position. * * @return integer */ @@ -746,8 +720,6 @@ public function locate( $substr, $str, $pos = 0 ) { /** * Method to return GMT date in the string format. * - * @param none - * * @return string formatted GMT date 'dddd-mm-dd' */ public function utc_date() { @@ -757,8 +729,6 @@ public function utc_date() { /** * Method to return GMT time in the string format. * - * @param none - * * @return string formatted GMT time '00:00:00' */ public function utc_time() { @@ -768,8 +738,6 @@ public function utc_time() { /** * Method to return GMT time stamp in the string format. * - * @param none - * * @return string formatted GMT timestamp 'yyyy-mm-dd 00:00:00' */ public function utc_timestamp() { @@ -782,13 +750,9 @@ public function utc_timestamp() { * This function only returns the current newest version number of MySQL, * because it is meaningless for SQLite database. * - * @param none - * * @return string representing the version number: major_version.minor_version */ public function version() { - //global $required_mysql_version; - //return $required_mysql_version; return '5.5'; } } diff --git a/src/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php b/src/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php new file mode 100644 index 0000000000000..e91dd75650d97 --- /dev/null +++ b/src/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 WP_SQLite_Token|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/src/wp-includes/sqlite/class-wp-sqlite-token.php b/src/wp-includes/sqlite/class-wp-sqlite-token.php new file mode 100644 index 0000000000000..24a4d4c782887 --- /dev/null +++ b/src/wp-includes/sqlite/class-wp-sqlite-token.php @@ -0,0 +1,330 @@ +, !==, etc. + * Bitwise operators: &, |, ^, etc. + * Assignment operators: =, +=, -=, etc. + * SQL specific operators: . (e.g. .. WHERE database.table ..), + * * (e.g. SELECT * FROM ..) + */ + public const TYPE_OPERATOR = 2; + + /** + * Spaces, tabs, new lines, etc. + */ + public 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. + */ + public const TYPE_COMMENT = 4; + + /** + * Boolean values: true or false. + */ + public const TYPE_BOOL = 5; + + /** + * Numbers: 4, 0x8, 15.16, 23e42, etc. + */ + public const TYPE_NUMBER = 6; + + /** + * Literal strings: 'string', "test". + * Some of these strings are actually symbols. + */ + public const TYPE_STRING = 7; + + /** + * Database, table names, variables, etc. + * For example: ```SELECT `foo`, `bar` FROM `database`.`table`;```. + */ + public const TYPE_SYMBOL = 8; + + /** + * Delimits an unknown string. + * For example: ```SELECT * FROM test;```, `test` is a delimiter. + */ + public 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]. + */ + public 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. + public const FLAG_KEYWORD_RESERVED = 2; + public const FLAG_KEYWORD_COMPOSED = 4; + public const FLAG_KEYWORD_DATA_TYPE = 8; + public const FLAG_KEYWORD_KEY = 16; + public const FLAG_KEYWORD_FUNCTION = 32; + + // Numbers related flags. + public const FLAG_NUMBER_HEX = 1; + public const FLAG_NUMBER_FLOAT = 2; + public const FLAG_NUMBER_APPROXIMATE = 4; + public const FLAG_NUMBER_NEGATIVE = 8; + public const FLAG_NUMBER_BINARY = 16; + + // Strings related flags. + public const FLAG_STRING_SINGLE_QUOTES = 1; + public const FLAG_STRING_DOUBLE_QUOTES = 2; + + // Comments related flags. + public const FLAG_COMMENT_BASH = 1; + public const FLAG_COMMENT_C = 2; + public const FLAG_COMMENT_SQL = 4; + public const FLAG_COMMENT_MYSQL_CMD = 8; + + // Operators related flags. + public const FLAG_OPERATOR_ARITHMETIC = 1; + public const FLAG_OPERATOR_LOGICAL = 2; + public const FLAG_OPERATOR_BITWISE = 4; + public const FLAG_OPERATOR_ASSIGNMENT = 8; + public const FLAG_OPERATOR_SQL = 16; + + // Symbols related flags. + public const FLAG_SYMBOL_VARIABLE = 1; + public const FLAG_SYMBOL_BACKTICK = 2; + public const FLAG_SYMBOL_USER = 4; + public const FLAG_SYMBOL_SYSTEM = 8; + public 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, ?array $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/src/wp-includes/sqlite/class-wp-sqlite-translator.php b/src/wp-includes/sqlite/class-wp-sqlite-translator.php new file mode 100644 index 0000000000000..defa722553b42 --- /dev/null +++ b/src/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -0,0 +1,3035 @@ + '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', + ); + + /** + * The last found rows. + * + * @var int|string + */ + private $last_found_rows = 0; + + /** + * The number of rows found by the last SELECT query. + * + * @var int + */ + protected $last_select_found_rows; + + /** + * Class variable which is used for CALC_FOUND_ROW query. + * + * @var unsigned integer + */ + public $found_rows_result = null; + + /** + * Class variable used for query with ORDER BY FIELD() + * + * @var array of the object + */ + public $pre_ordered_results = null; + + /** + * Class variable to store the last query. + * + * @var string + */ + public $last_translation; + + /** + * The query rewriter. + * + * @var WP_SQLite_Query_Rewriter + */ + private $rewriter; + + /** + * Class variable to store the query strings. + * + * @var array + */ + public $queries = array(); + + /** + * The query type. + * + * @var string + */ + private $query_type; + + /** + * Class variable to store the rewritten queries. + * + * @access private + * + * @var array + */ + private $rewritten_query; + + /** + * 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 unsigned integer + * @access private + */ + private $last_insert_id; + + /** + * Class variable to store the number of rows affected. + * + * @var unsigned 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 check if there is an active transaction. + * + * @var boolean + * @access protected + */ + protected $has_active_transaction = false; + + /** + * 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 { + $dsn = 'sqlite:' . FQDB; + $pdo = new PDO( $dsn, null, null, array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ) ); // phpcs:ignore WordPress.DB.RestrictedClasses + new WP_SQLite_PDO_User_Defined_Functions( $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!', + "Code: $status", + "Error Message: $err_message" + ); + $this->is_error = true; + $this->last_error = $message; + + return false; + } + + // MySQL data comes across stringified by default. + $pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); + $pdo->query( WP_SQLite_Translator::CREATE_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' ) ); + $this->init(); + + $this->pdo->query( 'PRAGMA encoding="UTF-8";' ); + + $this->table_prefix = $GLOBALS['table_prefix']; + } + + /** + * 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' ); + } + } + + /** + * 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(); + try { + if ( + preg_match( '/^START TRANSACTION/i', $statement ) + || preg_match( '/^BEGIN/i', $statement ) + ) { + return $this->beginTransaction(); + } + if ( preg_match( '/^COMMIT/i', $statement ) ) { + return $this->commit(); + } + if ( preg_match( '/^ROLLBACK/i', $statement ) ) { + return $this->rollBack(); + } + + do { + $error = null; + try { + $translation = $this->translate( + $statement, + $this->found_rows_result + ); + } catch ( PDOException $error ) { + if ( $error->getCode() !== self::SQLITE_BUSY ) { + return $this->handle_error( $error ); + } + } + } while ( $error ); + + $stmt = null; + $last_retval = null; + foreach ( $translation->queries as $query ) { + $this->queries[] = "Executing: {$query->sql} | " . ( $query->params ? 'parameters: ' . implode( ', ', $query->params ) : '(no parameters)' ); + do { + $error = null; + try { + $stmt = $this->pdo->prepare( $query->sql ); + $last_retval = $stmt->execute( $query->params ); + } catch ( PDOException $error ) { + if ( $error->getCode() !== self::SQLITE_BUSY ) { + throw $error; + } + } + } while ( $error ); + } + + if ( $translation->has_result ) { + $this->results = $translation->result; + } else { + switch ( $translation->mysql_query_type ) { + case 'DESCRIBE': + $this->results = $stmt->fetchAll( $mode ); + if ( ! $this->results ) { + $this->handle_error( new PDOException( 'Table not found' ) ); + return; + } + break; + case 'SELECT': + case 'SHOW': + $this->results = $stmt->fetchAll( $mode ); + break; + case 'TRUNCATE': + $this->results = true; + $this->return_value = true; + return $this->return_value; + case 'SET': + $this->results = 0; + break; + default: + $this->results = $last_retval; + break; + } + } + + if ( $translation->calc_found_rows ) { + $this->found_rows_result = $translation->calc_found_rows; + } + + if ( is_array( $this->results ) ) { + $this->num_rows = count( $this->results ); + $this->last_select_found_rows = count( $this->results ); + } + + switch ( $translation->sqlite_query_type ) { + case 'DELETE': + case 'UPDATE': + case 'INSERT': + case 'REPLACE': + /* + * 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 + */ + $this->affected_rows = (int) $this->pdo->query( 'select changes()' )->fetch()[0]; + $this->return_value = $this->affected_rows; + $this->num_rows = $this->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; + } + break; + default: + $this->return_value = $this->results; + break; + } + + return $this->return_value; + } catch ( Exception $err ) { + if ( defined( 'PDO_DEBUG' ) && PDO_DEBUG === true ) { + throw $err; + } + return $this->handle_error( $err ); + } + } + + /** + * Gets the query object. + * + * @param string $sql The SQL query. + * @param array $params The parameters. + * + * @return stdClass + */ + public static function get_query_object( $sql = '', $params = array() ) { + $sql_obj = new stdClass(); + $sql_obj->sql = trim( $sql ); + $sql_obj->params = $params; + return $sql_obj; + } + + /** + * Gets the translation result. + * + * @param array $queries The queries. + * @param boolean $has_result Whether the query has a result. + * @param mixed $custom_output The result. + * + * @return stdClass + */ + protected function get_translation_result( $queries, $has_result = false, $custom_output = null ) { + $result = new stdClass(); + $result->queries = $queries; + $result->has_result = $has_result; + $result->result = $custom_output; + $result->calc_found_rows = null; + $result->sqlite_query_type = null; + $result->mysql_query_type = null; + $result->rewriter = null; + $result->query_type = null; + + return $result; + } + + /** + * 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[] = 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; + } + + /** + * Translates the query. + * + * @param string $query The query. + * @param int|string $last_found_rows The last found rows. + * + * @throws Exception If the query is not supported. + * + * @return stdClass + */ + public function translate( string $query, $last_found_rows = null ) { + $this->last_found_rows = $last_found_rows; + + $tokens = ( new WP_SQLite_Lexer( $query ) )->tokens; + $this->rewriter = new WP_SQLite_Query_Rewriter( $tokens ); + $this->query_type = $this->rewriter->peek()->value; + + switch ( $this->query_type ) { + case 'ALTER': + $result = $this->translate_alter(); + break; + + case 'CREATE': + $result = $this->translate_create(); + break; + + case 'REPLACE': + case 'SELECT': + case 'INSERT': + case 'UPDATE': + case 'DELETE': + $result = $this->translate_crud(); + 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. + */ + $result = $this->get_translation_result( array( $this->noop() ) ); + break; + + case '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(); + $result = $this->get_translation_result( + array( + WP_SQLite_Translator::get_query_object( $this->rewriter->get_updated_query() ), + ) + ); + break; + + case 'START TRANSACTION': + $result = $this->get_translation_result( + array( + WP_SQLite_Translator::get_query_object( 'BEGIN' ), + ) + ); + break; + + case 'BEGIN': + case 'COMMIT': + case 'ROLLBACK': + $result = $this->get_translation_result( + array( + WP_SQLite_Translator::get_query_object( $query ), + ) + ); + break; + + case 'DROP': + $result = $this->translate_drop(); + break; + + case 'DESCRIBE': + $this->rewriter->skip(); + $table_name = $this->rewriter->consume()->value; + $result = $this->get_translation_result( + array( + WP_SQLite_Translator::get_query_object( + "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` + ; + " + ), + ) + ); + break; + + case 'SHOW': + $result = $this->translate_show(); + break; + + default: + throw new Exception( 'Unknown query type: ' . $this->query_type ); + } + // The query type could have changed – let's grab the new one. + if ( count( $result->queries ) ) { + $last_query = $result->queries[ count( $result->queries ) - 1 ]; + $first_word = preg_match( '/^\s*(\w+)/', $last_query->sql, $matches ) ? $matches[1] : ''; + $result->sqlite_query_type = strtoupper( $first_word ); + } + $result->mysql_query_type = $this->query_type; + return $result; + } + + /** + * Translates the CREATE TABLE query. + * + * @throws Exception If the query is not supported. + * + * @return stdClass + */ + private function translate_create_table() { + $table = $this->parse_create_table(); + + $extra_queries = array(); + $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 ); + $extra_queries[] = $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_table_query = WP_SQLite_Translator::get_query_object( + $table->create_table . + '"' . $table->name . '" (' . "\n" . + implode( ",\n", $definitions ) . + ')' + ); + + foreach ( $table->constraints as $constraint ) { + $index_type = $this->mysql_index_type_to_sqlite_type( $constraint->value ); + $unique = ''; + if ( 'UNIQUE' === $constraint->value ) { + $unique = 'UNIQUE '; + } + $index_name = "{$table->name}__{$constraint->name}"; + $extra_queries[] = WP_SQLite_Translator::get_query_object( + "CREATE $unique INDEX \"$index_name\" ON \"{$table->name}\" (\"" . implode( '", "', $constraint->columns ) . '")' + ); + $extra_queries[] = $this->update_data_type_cache( + $table->name, + $index_name, + $constraint->value, + ); + } + + return $this->get_translation_result( + array_merge( + array( + $create_table_query, + ), + $extra_queries + ) + ); + } + + /** + * 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( $result->name ); + } + + /* + * 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 + ) + ); + } + + + /** + * Translator method. + * + * @throws Exception If the query type is unknown. + * + * @return stdClass + */ + private function translate_crud() { + $query_type = $this->rewriter->consume()->value; + + $params = array(); + $is_in_duplicate_section = false; + $table_name = null; + $has_sql_calc_found_rows = false; + + // Consume the query type. + if ( 'INSERT' === $query_type && '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(); + if ( 'INSERT' === $query_type || 'REPLACE' === $query_type ) { + $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; + } + } + } + } + + $last_reserved_keyword = null; + while ( true ) { + $token = $this->rewriter->peek(); + if ( ! $token ) { + break; + } + + if ( WP_SQLite_Token::TYPE_KEYWORD === $token->type && $token->flags & WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) { + $last_reserved_keyword = $token->value; + if ( 'FROM' === $last_reserved_keyword ) { + $from_table = $this->rewriter->peek_nth( 2 )->value; + if ( 'DUAL' === strtoupper( $from_table ) ) { + // FROM DUAL is a MySQLism that means "no tables". + $this->rewriter->skip(); + $this->rewriter->skip(); + continue; + } elseif ( ! $table_name ) { + $table_name = $from_table; + } + } + } + + if ( 'SQL_CALC_FOUND_ROWS' === $token->value && WP_SQLite_Token::TYPE_KEYWORD === $token->type ) { + $has_sql_calc_found_rows = true; + $this->rewriter->skip(); + continue; + } + + if ( 'AS' !== $last_reserved_keyword && WP_SQLite_Token::TYPE_STRING === $token->type && $token->flags & WP_SQLite_Token::FLAG_STRING_SINGLE_QUOTES ) { + // Rewrite string values to bound parameters. + $param_name = ':param' . count( $params ); + $params[ $param_name ] = $this->preprocess_string_literal( $token->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 ) ); + continue; + } + + if ( WP_SQLite_Token::TYPE_KEYWORD === $token->type ) { + if ( + $this->translate_concat_function( $token ) + || $this->translate_cast_as_binary( $token ) + || $this->translate_date_add_sub( $token ) + || $this->translate_values_function( $token, $is_in_duplicate_section ) + || $this->translate_date_format( $token ) + || $this->translate_interval( $token ) + || $this->translate_regexp_functions( $token ) + ) { + continue; + } + + if ( 'INSERT' === $query_type && 'DUPLICATE' === $token->keyword ) { + $is_in_duplicate_section = true; + $this->translate_on_duplicate_key( $table_name ); + continue; + } + } + + if ( $this->translate_concat_comma_to_pipes( $token ) ) { + continue; + } + $this->rewriter->consume(); + } + $this->rewriter->consume_all(); + + $updated_query = $this->rewriter->get_updated_query(); + $result = $this->get_translation_result( array() ); + + if ( 'SELECT' === $query_type && $table_name && str_starts_with( strtolower( $table_name ), 'information_schema' ) ) { + return $this->translate_information_schema_query( + $updated_query + ); + } + + /* + * 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('`. + */ + if ( + strpos( $updated_query, '@@SESSION.sql_mode' ) !== false + || strpos( $updated_query, 'CONVERT( ' ) !== false + ) { + $updated_query = 'SELECT 1=0'; + $params = array(); + } + + // Emulate SQL_CALC_FOUND_ROWS for now. + if ( $has_sql_calc_found_rows ) { + $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->pdo->prepare( $unlimited_query ); + $stmt->execute( $params ); + $result->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_found_rows ? $this->last_found_rows : 0 ) . ''; + $result->queries[] = WP_SQLite_Translator::get_query_object( + "SELECT {$last_found_rows} AS `FOUND_ROWS()`", + ); + return $result; + } + + /* + * Now that functions are rewritten to SQLite dialect, + * let's translate unsupported delete queries. + */ + if ( 'DELETE' === $query_type ) { + $delete_result = $this->postprocess_double_delete( $params ); + if ( $delete_result ) { + return $delete_result; + } + } + + $result->queries[] = WP_SQLite_Translator::get_query_object( $updated_query, $params ); + return $result; + } + + /** + * 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 ) ) { + if ( false === strtotime( $value ) ) { + $value = '0000-00-00 00:00:00'; + } + } + return $value; + } + + /** + * Postprocesses a double delete query. + * + * @param array $rewritten_params The rewritten parameters. + * + * @throws Exception If the query is not a double delete query. + * + * @return WP_SQLite_Translation_Result|null The translation result or null if the query is not a double delete query. + */ + private function postprocess_double_delete( $rewritten_params ) { + // Naive rewriting of DELETE JOIN query. + // @TODO: Actually rewrite the query instead of using a hardcoded workaround. + $updated_query = $this->rewriter->get_updated_query(); + if ( str_contains( $updated_query, ' JOIN ' ) ) { + return $this->get_translation_result( + array( + WP_SQLite_Translator::get_query_object( + "DELETE FROM {$this->table_prefix}options WHERE option_id IN (SELECT MIN(option_id) FROM {$this->table_prefix}options GROUP BY option_name HAVING COUNT(*) > 1)" + ), + ) + ); + } + + $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', + ) + ); + // It's a dual delete query if the comma comes before the FROM. + if ( ! $comma || ! $from || $comma->position >= $from->position ) { + return; + } + + $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->pdo->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->pdo->prepare( $select ); + $stmt->execute( $rewritten_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" + ); + return $this->get_translation_result( + array( + WP_SQLite_Translator::get_query_object( $query ), + ), + true, + count( $ids_to_delete ) + ); + } + + /** + * Translate an information_schema query. + * + * @param string $query The query to translate. + * + * @return WP_SQLite_Translation_Result + */ + private function translate_information_schema_query( $query ) { + // @TODO: Actually rewrite the columns. + if ( str_contains( $query, 'bytes' ) ) { + // Count rows per table. + $tables = $this->pdo->query( "SELECT name as `table` FROM sqlite_master WHERE type='table' ORDER BY name" )->fetchAll(); + $rows = '(CASE '; + foreach ( $tables as $table ) { + $table_name = $table['table']; + $count = $this->pdo->query( "SELECT COUNT(*) as `count` FROM $table_name" )->fetch(); + $rows .= " WHEN name = '$table_name' THEN {$count['count']} "; + } + $rows .= 'ELSE 0 END) '; + return $this->get_translation_result( + array( + WP_SQLite_Translator::get_query_object( + "SELECT name as `table`, $rows as `rows`, 0 as `bytes` FROM sqlite_master WHERE type='table' ORDER BY name" + ), + ) + ); + } + return $this->get_translation_result( + array( + WP_SQLite_Translator::get_query_object( + "SELECT name, 'myisam' as `engine`, 0 as `data`, 0 as `index` FROM sqlite_master WHERE type='table' ORDER BY name" + ), + ) + ); + } + + /** + * 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 ) ) { + $call_parent = $this->rewriter->last_call_stack_element(); + // Rewrite AS BINARY to AS BLOB inside CAST() calls. + if ( + $call_parent + && 'CAST' === $call_parent['function'] + && 'BINARY' === $token->value + ) { + $this->rewriter->skip(); + $this->rewriter->add( new WP_SQLite_Token( 'BLOB', $token->type, $token->flags ) ); + return true; + } + } + return false; + } + + /** + * Translate CONCAT() function. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_concat_function( $token ) { + /* + * Skip the CONCAT function but leave the parentheses. + * There is another code block below that replaces the + * , operators between the CONCAT arguments with ||. + */ + if ( + 'CONCAT' === $token->keyword + && $token->flags & WP_SQLite_Token::FLAG_KEYWORD_FUNCTION + ) { + $this->rewriter->skip(); + return true; + } + return false; + } + + /** + * Translate CONCAT() function arguments. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_concat_comma_to_pipes( $token ) { + if ( WP_SQLite_Token::TYPE_OPERATOR === $token->type ) { + $call_parent = $this->rewriter->last_call_stack_element(); + // Rewrite commas to || in CONCAT() calls. + if ( + $call_parent + && 'CONCAT' === $call_parent['function'] + && ',' === $token->value + && $token->flags & WP_SQLite_Token::FLAG_OPERATOR_SQL + ) { + $this->rewriter->skip(); + $this->rewriter->add( new WP_SQLite_Token( '||', WP_SQLite_Token::TYPE_OPERATOR ) ); + return true; + } + } + return false; + } + + /** + * 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->flags & WP_SQLite_Token::FLAG_KEYWORD_FUNCTION && ( + 'DATE_ADD' === $token->keyword || + 'DATE_SUB' === $token->keyword + ) + ) { + $this->rewriter->skip(); + $this->rewriter->add( new WP_SQLite_Token( 'DATETIME', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ) ); + return true; + } + return false; + } + + /** + * Translate VALUES() function. + * + * @param WP_SQLite_Token $token The token to translate. + * @param bool $is_in_duplicate_section Whether the VALUES() function is in a duplicate section. + * + * @return bool + */ + private function translate_values_function( $token, $is_in_duplicate_section ) { + if ( + $token->flags & WP_SQLite_Token::FLAG_KEYWORD_FUNCTION && + 'VALUES' === $token->keyword && + $is_in_duplicate_section + ) { + /* + * 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; + } + return false; + } + + /** + * 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->flags & WP_SQLite_Token::FLAG_KEYWORD_FUNCTION && + 'DATE_FORMAT' === $token->keyword + ) { + // 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; + } + return false; + } + + /** + * 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 ( 'INTERVAL' === $token->keyword ) { + // 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; + } + return false; + } + + /** + * Translate REGEXP and RLIKE keywords. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_regexp_functions( $token ) { + if ( 'REGEXP' === $token->keyword || 'RLIKE' === $token->keyword ) { + $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; + } + return false; + } + + /** + * 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->pdo->prepare( '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->pdo->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->pdo->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->pdo->prepare( '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. + * + * @return stdClass + */ + private function translate_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 ); + $queries = array(); + 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 + ) + ); + $queries[] = $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 ) { + if ( count( $queries ) ) { + /* + * Mixing CHANGE COLUMN with other operations would require keeping track of the + * original table schema, and then applying the changes in order. This is not + * currently supported. + * + * Ideally, each ALTER TABLE operation would be flushed before the next one is + * processed, but that's not currently the case. + */ + throw new Exception( + 'Mixing CHANGE COLUMN with other operations in a single ALTER TABLE ' . + 'query is not supported yet.' + ); + } + // 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 ); + $queries[] = $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 ); + $queries[] = WP_SQLite_Translator::get_query_object( + "CREATE TABLE `$cache_table_name` as SELECT * FROM `$table_name`" + ); + + // 4. Drop the old table to free up the indexes names + $queries[] = WP_SQLite_Translator::get_query_object( + "DROP TABLE `$table_name`" + ); + + // 5. Create a new table from the updated schema + $queries[] = WP_SQLite_Translator::get_query_object( + $create_table->get_updated_query() + ); + + // 6. Copy the data from step 3 to the new table + $queries[] = WP_SQLite_Translator::get_query_object( + "INSERT INTO {$table_name} SELECT * FROM $cache_table_name" + ); + + // 7. Drop the old table copy + $queries[] = WP_SQLite_Translator::get_query_object( + "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 + */ + $queries[] = WP_SQLite_Translator::get_query_object( + "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 ), + ) + ); + $queries[] = $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(); + $queries[] = WP_SQLite_Translator::get_query_object( + $this->rewriter->get_updated_query() + ); + } while ( $comma ); + + return $this->get_translation_result( $queries ); + } + + /** + * Translates a CREATE query. + * + * @throws Exception If the query is an unknown create type. + * @return stdClass The translation result. + */ + private function translate_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': + return $this->translate_create_table(); + + case 'PROCEDURE': + case 'DATABASE': + return $this->get_translation_result( array( $this->noop() ) ); + + default: + throw new Exception( 'Unknown create type: ' . $what ); + } + } + + /** + * Translates a DROP query. + * + * @throws Exception If the query is an unknown drop type. + * + * @return stdClass The translation result. + */ + private function translate_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(); + return $this->get_translation_result( array( WP_SQLite_Translator::get_query_object( $this->rewriter->get_updated_query() ) ) ); + + case 'PROCEDURE': + case 'DATABASE': + return $this->get_translation_result( array( $this->noop() ) ); + + default: + throw new Exception( 'Unknown drop type: ' . $what ); + } + } + + /** + * Translates a SHOW query. + * + * @throws Exception If the query is an unknown show type. + * @return stdClass The translation result. + */ + private function translate_show() { + $this->rewriter->skip(); + $what1 = $this->rewriter->consume()->token; + $what2 = $this->rewriter->consume()->token; + $what = $what1 . ' ' . $what2; + switch ( $what ) { + case 'CREATE PROCEDURE': + return $this->get_translation_result( array( $this->noop() ) ); + + case 'FULL COLUMNS': + $this->rewriter->consume(); + $table_name = $this->rewriter->consume()->token; + return $this->get_translation_result( + array( + WP_SQLite_Translator::get_query_object( + "PRAGMA table_info($table_name);" + ), + ) + ); + + case 'COLUMNS FROM': + $table_name = $this->rewriter->consume()->token; + return $this->get_translation_result( + array( + WP_SQLite_Translator::get_query_object( + "PRAGMA table_info(\"$table_name\");" + ), + ) + ); + + 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' => '', + ) + ); + } + return $this->get_translation_result( + array( + WP_SQLite_Translator::get_query_object( + 'SELECT 1=1;' + ), + ), + true, + $results + ); + + case 'TABLES LIKE': + $table_expression = $this->rewriter->skip(); + return $this->get_translation_result( + array( + WP_SQLite_Translator::get_query_object( + "SELECT `name` as `Tables_in_db` FROM `sqlite_master` WHERE `type`='table' AND `name` LIKE :param;", + array( + ':param' => $table_expression->value, + ) + ), + ) + ); + + default: + switch ( $what1 ) { + case 'TABLES': + return $this->get_translation_result( + array( + WP_SQLite_Translator::get_query_object( + "SELECT name FROM sqlite_master WHERE type='table'" + ), + ) + ); + + case 'VARIABLE': + case 'VARIABLES': + return $this->get_translation_result( + array( + $this->noop(), + ) + ); + + default: + throw new Exception( 'Unknown show type: ' . $what ); + } + } + } + + /** + * Returns a dummy `SELECT 1=1` query object. + * + * @return stdClass The dummy query object. + */ + private function noop() { + return WP_SQLite_Translator::get_query_object( + 'SELECT 1 WHERE 1=0;', + array() + ); + } + + /** + * 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 unsigned keyword. + $unsigned_maybe = $this->rewriter->peek(); + if ( $unsigned_maybe && $unsigned_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 stdClass The query object. + */ + private function update_data_type_cache( $table, $column_or_index, $mysql_data_type ) { + return WP_SQLite_Translator::get_query_object( + '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->pdo->prepare( + 'SELECT d.`mysql_type` FROM ' . self::DATA_TYPES_CACHE_TABLE . ' d + WHERE `table`=:table + AND `column_or_index` = :index', + ); + $stmt->execute( + 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'; + } + + /** + * Error handler. + * + * @param Exception $err Exception object. + * + * @return bool Always false. + */ + private function handle_error( Exception $err ) { + $message = $err->getMessage(); + $err_message = sprintf( 'Problem preparing the PDO SQL Statement. Error was: %s. trace: %s', $message, $err->getTraceAsString() ); + $this->set_error( __LINE__, __FUNCTION__, $err_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 .= '

Queries made or created this session were:

' . PHP_EOL; + $output .= '
    ' . PHP_EOL; + foreach ( $this->queries as $q ) { + $output .= '
  1. ' . htmlspecialchars( $q ) . '
  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 .= '
' . htmlspecialchars( $e->getTraceAsString() ) . '
' . PHP_EOL; + } + + return $output; + } + + /** + * Method to clear previous data. + */ + private function flush() { + $this->rewritten_query = ''; + $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->error_messages = array(); + $this->is_error = false; + $this->queries = array(); + $this->param_num = 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() { + if ( $this->has_active_transaction ) { + $this->pdo->commit(); + $this->has_active_transaction = false; + } + } + + /** + * Method to call PDO::rollBack(). + * + * @see PDO::rollBack() + * + * @return void + */ + public function rollBack() { + if ( $this->has_active_transaction ) { + $this->pdo->rollBack(); + $this->has_active_transaction = false; + } + } +} diff --git a/src/wp-includes/sqlite/db.php b/src/wp-includes/sqlite/db.php index e6017768114fb..ecbc9f1239d9f 100644 --- a/src/wp-includes/sqlite/db.php +++ b/src/wp-includes/sqlite/db.php @@ -1,289 +1,53 @@ - - -WordPress › Error - - - - -

WordPress

-

$message

-

$data

- - +// Require the constants file. +require_once dirname( dirname( __DIR__ ) ) . '/constants.php'; -HTML - ); +// Bail early if DATABASE_TYPE is not defined as sqlite. +if ( ! defined( 'DATABASE_TYPE' ) || 'sqlite' !== DATABASE_TYPE ) { + return; } if ( ! extension_loaded( 'pdo' ) ) { - pdo_log_error( - 'PHP PDO Extension is not loaded.', - 'Your PHP installation appears to be missing the PDO extension which is required for this version of WordPress.' + wp_die( + new WP_Error( + 'pdo_not_loaded', + sprintf( + '

%1$s

%2$s

', + 'PHP PDO Extension is not loaded', + 'Your PHP installation appears to be missing the PDO extension which is required for this version of WordPress and the type of database you have specified.' + ) + ), + 'PHP PDO Extension is not loaded.' ); } if ( ! extension_loaded( 'pdo_sqlite' ) ) { - pdo_log_error( - 'PDO Driver for SQLite is missing.', - 'Your PHP installation appears not to have the right PDO drivers loaded. These are required for this version of WordPress and the type of database you have specified.' + wp_die( + new WP_Error( + 'pdo_driver_not_loaded', + sprintf( + '

%1$s

%2$s

', + 'PDO Driver for SQLite is missing', + 'Your PHP installation appears not to have the right PDO drivers loaded. These are required for this version of WordPress and the type of database you have specified.' + ) + ), + 'PDO Driver for SQLite is missing.' ); } -/** - * Notice: - * Your scripts have the permission to create directories or files on your server. - * If you write in your wp-config.php like below, we take these definitions. - * define('DB_DIR', '/full_path_to_the_database_directory/'); - * define('DB_FILE', 'database_file_name'); - */ - -/** - * FQDBDIR is a directory where the sqlite database file is placed. - * If DB_DIR is defined, it is used as FQDBDIR. - */ -if ( defined( 'DB_DIR' ) ) { - define( 'FQDBDIR', trailingslashit( DB_DIR ) ); -} elseif ( defined( 'WP_CONTENT_DIR' ) ) { - define( 'FQDBDIR', WP_CONTENT_DIR . '/database/' ); -} else { - define( 'FQDBDIR', ABSPATH . 'wp-content/database/' ); -} - -/** - * FQDB is a database file name. If DB_FILE is defined, it is used - * as FQDB. - */ -if ( defined( 'DB_FILE' ) ) { - define( 'FQDB', FQDBDIR . DB_FILE ); -} else { - define( 'FQDB', FQDBDIR . '.ht.sqlite' ); -} - -require_once ABSPATH . WPINC . '/sqlite/class-wp-pdo-sqlite-user-defined-functions.php'; -require_once ABSPATH . WPINC . '/sqlite/class-wp-pdo-engine.php'; -require_once ABSPATH . WPINC . '/sqlite/class-wp-sqlite-object-array.php'; -require_once ABSPATH . WPINC . '/sqlite/class-wp-sqlite-db.php'; -require_once ABSPATH . WPINC . '/sqlite/class-wp-pdo-sqlite-driver.php'; -require_once ABSPATH . WPINC . '/sqlite/class-wp-sqlite-create-query.php'; -require_once ABSPATH . WPINC . '/sqlite/class-wp-sqlite-alter-query.php'; - -/** - * Function to create tables according to the schemas of WordPress. - * - * This is executed only once while installation. - * - * @return boolean - */ -function 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 WP_SQLite_Create_Query(); - try { - $pdo = new PDO( 'sqlite:' . FQDB, null, null, array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ) ); - } catch ( PDOException $err ) { - $err_data = $err->errorInfo; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $message = 'Database connection error!
'; - $message .= sprintf( 'Error message is: %s', $err_data[2] ); - wp_die( $message, 'Database Error!' ); - } - - try { - $pdo->beginTransaction(); - 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 ); - //foreach($rewritten_query as $single_query) { - // $single_query = trim($single_query); - // $pdo->exec($single_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 ); - } - $pdo->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 occured 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!' ); - } - } - - $query_parser = null; - $pdo = null; - - return true; -} - -/** - * Installs the site. - * - * Runs the required functions to set up and populate the database, - * including primary admin user and initial options. - * - * @since 2.1.0 - * - * @param string $blog_title Site title. - * @param string $user_name User's username. - * @param string $user_email User's email. - * @param bool $public Whether site is public. - * @param string $deprecated Optional. Not used. - * @param string $user_password Optional. User's chosen password. Default empty (random password). - * @param string $language Optional. Language chosen. Default empty. - * - * @return array Array keys 'url', 'user_id', 'password', and 'password_message'. - */ -function wp_install( $blog_title, $user_name, $user_email, $public, $deprecated = '', $user_password = '', $language = '' ) { - if ( ! empty( $deprecated ) ) { - _deprecated_argument( __FUNCTION__, '2.6.0' ); - } - - wp_check_mysql_version(); - wp_cache_flush(); - /* begin wp-sqlite-db changes */ - // make_db_current_silent(); - make_db_sqlite(); - /* end wp-sqlite-db changes */ - populate_options(); - populate_roles(); - - update_option( 'blogname', $blog_title ); - update_option( 'admin_email', $user_email ); - update_option( 'blog_public', $public ); - - // Freshness of site - in the future, this could get more specific about actions taken, perhaps. - update_option( 'fresh_site', 1 ); - - if ( $language ) { - update_option( 'WPLANG', $language ); - } - - $guessurl = wp_guess_url(); - - update_option( 'siteurl', $guessurl ); - - // If not a public blog, don't ping. - if ( ! $public ) { - update_option( 'default_pingback_flag', 0 ); - } - - /* - * Create default user. If the user already exists, the user tables are - * being shared among sites. Just set the role in that case. - */ - $user_id = username_exists( $user_name ); - $user_password = trim( $user_password ); - $email_password = false; - if ( ! $user_id && empty( $user_password ) ) { - $user_password = wp_generate_password( 12, false ); - $message = __( 'Note that password carefully! It is a random password that was generated just for you.' ); - $user_id = wp_create_user( $user_name, $user_password, $user_email ); - update_user_option( $user_id, 'default_password_nag', true, true ); - $email_password = true; - } elseif ( ! $user_id ) { - // Password has been provided - $message = '' . __( 'Your chosen password.' ) . ''; - $user_id = wp_create_user( $user_name, $user_password, $user_email ); - } else { - $message = __( 'User already exists. Password inherited.' ); - } - - $user = new WP_User( $user_id ); - $user->set_role( 'administrator' ); - - wp_install_defaults( $user_id ); - - wp_install_maybe_enable_pretty_permalinks(); - - flush_rewrite_rules(); - - wp_new_blog_notification( $blog_title, $guessurl, $user_id, ( $email_password ? $user_password : __( 'The password you chose during installation.' ) ) ); - - wp_cache_flush(); - - /** - * Fires after a site is fully installed. - * - * @since 3.9.0 - * - * @param WP_User $user The site owner. - */ - do_action( 'wp_install', $user ); - - return array( - 'url' => $guessurl, - 'user_id' => $user_id, - 'password' => $user_password, - 'password_message' => $message, - ); -} +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 WP_SQLite_DB(); From 7b72595dfaa6ee913a643d209f25adad6ad577e1 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Thu, 23 Feb 2023 11:06:47 +0200 Subject: [PATCH 14/36] backport more changes --- .gitignore | 4 ++++ src/wp-includes/sqlite/db.php | 31 +++++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 0a02b30a1548d..3cb9f8bd820a5 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,7 @@ wp-tests-config.php # Visual regression test diffs tests/visual-regression/specs/__snapshots__ + +src/wp-content/database/.ht.sqlite + +src/wp-content/database/ diff --git a/src/wp-includes/sqlite/db.php b/src/wp-includes/sqlite/db.php index ecbc9f1239d9f..405535184e8bf 100644 --- a/src/wp-includes/sqlite/db.php +++ b/src/wp-includes/sqlite/db.php @@ -6,14 +6,38 @@ * @since 1.0.0 */ -// 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 ) { return; } +/** + * FQDBDIR is a directory where the sqlite database file is placed. + * If DB_DIR is defined, it is used as FQDBDIR. + */ +if ( ! defined( 'FQDBDIR' ) ) { + if ( defined( 'DB_DIR' ) ) { + define( 'FQDBDIR', trailingslashit( DB_DIR ) ); + } elseif ( defined( 'WP_CONTENT_DIR' ) ) { + define( 'FQDBDIR', WP_CONTENT_DIR . '/database/' ); + } else { + define( 'FQDBDIR', ABSPATH . 'wp-content/database/' ); + } +} + +/** + * FQDB is a database file name. If DB_FILE is defined, it is used + * as FQDB. + */ +if ( ! defined( 'FQDB' ) ) { + if ( defined( 'DB_FILE' ) ) { + define( 'FQDB', FQDBDIR . DB_FILE ); + } else { + define( 'FQDB', FQDBDIR . '.ht.sqlite' ); + } +} + + if ( ! extension_loaded( 'pdo' ) ) { wp_die( new WP_Error( @@ -48,6 +72,5 @@ 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 WP_SQLite_DB(); From 886508a6702a9cd3d9bc4a42cbb6da1cf16a18a5 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Thu, 23 Feb 2023 11:09:27 +0200 Subject: [PATCH 15/36] Force SQLite so that tests run in this PR using the new db --- src/wp-load.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/wp-load.php b/src/wp-load.php index d3787d108e17d..a89dac35a581b 100644 --- a/src/wp-load.php +++ b/src/wp-load.php @@ -21,6 +21,9 @@ define( 'ABSPATH', __DIR__ . '/' ); } +// @TODO: Remove this. It's only here so that the PR can be tested. +define( 'DATABASE_TYPE', 'sqlite' ); + /* * The error_reporting() function can be disabled in php.ini. On systems where that is the case, * it's best to add a dummy function to the wp-config.php file, but as this call to the function From 605846af9b9e7d0b1470d5fc4f3c2cc4364e0869 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Thu, 23 Feb 2023 11:51:11 +0200 Subject: [PATCH 16/36] compatibility fixes for PHP < 7 --- .../sqlite/class-wp-sqlite-lexer.php | 8 +- .../sqlite/class-wp-sqlite-token.php | 74 +++++++++---------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/wp-includes/sqlite/class-wp-sqlite-lexer.php b/src/wp-includes/sqlite/class-wp-sqlite-lexer.php index 109892f264554..fa202c37555e1 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-lexer.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-lexer.php @@ -16,19 +16,19 @@ class WP_SQLite_Lexer { /** * The maximum length of a keyword. */ - public const KEYWORD_MAX_LENGTH = 30; + const KEYWORD_MAX_LENGTH = 30; /** * The maximum length of a label. * * Ref: https://dev.mysql.com/doc/refman/5.7/en/statement-labels.html */ - public const LABEL_MAX_LENGTH = 16; + const LABEL_MAX_LENGTH = 16; /** * The maximum length of an operator. */ - public const OPERATOR_MAX_LENGTH = 4; + const OPERATOR_MAX_LENGTH = 4; /** * A list of methods that are used in lexing the SQL query. @@ -1404,7 +1404,7 @@ class WP_SQLite_Lexer { * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_ansi_quotes * @link https://mariadb.com/kb/en/sql-mode/#ansi_quotes */ - public const SQL_MODE_ANSI_QUOTES = 2; + const SQL_MODE_ANSI_QUOTES = 2; /** * The array of tokens. diff --git a/src/wp-includes/sqlite/class-wp-sqlite-token.php b/src/wp-includes/sqlite/class-wp-sqlite-token.php index 24a4d4c782887..2f85b0daa03ac 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-token.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-token.php @@ -22,12 +22,12 @@ class WP_SQLite_Token { * determined because of the ambiguous context. Further analysis might be * required to detect its type. */ - public const TYPE_NONE = 0; + const TYPE_NONE = 0; /** * SQL specific keywords: SELECT, UPDATE, INSERT, etc. */ - public const TYPE_KEYWORD = 1; + const TYPE_KEYWORD = 1; /** * Any type of legal operator. @@ -39,12 +39,12 @@ class WP_SQLite_Token { * SQL specific operators: . (e.g. .. WHERE database.table ..), * * (e.g. SELECT * FROM ..) */ - public const TYPE_OPERATOR = 2; + const TYPE_OPERATOR = 2; /** * Spaces, tabs, new lines, etc. */ - public const TYPE_WHITESPACE = 3; + const TYPE_WHITESPACE = 3; /** * Any type of legal comment. @@ -64,35 +64,35 @@ class WP_SQLite_Token { * * Backslashes were added to respect PHP's comments syntax. */ - public const TYPE_COMMENT = 4; + const TYPE_COMMENT = 4; /** * Boolean values: true or false. */ - public const TYPE_BOOL = 5; + const TYPE_BOOL = 5; /** * Numbers: 4, 0x8, 15.16, 23e42, etc. */ - public const TYPE_NUMBER = 6; + const TYPE_NUMBER = 6; /** * Literal strings: 'string', "test". * Some of these strings are actually symbols. */ - public const TYPE_STRING = 7; + const TYPE_STRING = 7; /** * Database, table names, variables, etc. * For example: ```SELECT `foo`, `bar` FROM `database`.`table`;```. */ - public const TYPE_SYMBOL = 8; + const TYPE_SYMBOL = 8; /** * Delimits an unknown string. * For example: ```SELECT * FROM test;```, `test` is a delimiter. */ - public const TYPE_DELIMITER = 9; + const TYPE_DELIMITER = 9; /** * Labels in LOOP statement, ITERATE statement etc. @@ -102,47 +102,47 @@ class WP_SQLite_Token { * begin_label: REPEAT [statement_list] ... END REPEAT [end_label] * begin_label: WHILE ... DO [statement_list] END WHILE [end_label]. */ - public const TYPE_LABEL = 10; + 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. - public const FLAG_KEYWORD_RESERVED = 2; - public const FLAG_KEYWORD_COMPOSED = 4; - public const FLAG_KEYWORD_DATA_TYPE = 8; - public const FLAG_KEYWORD_KEY = 16; - public const FLAG_KEYWORD_FUNCTION = 32; + 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. - public const FLAG_NUMBER_HEX = 1; - public const FLAG_NUMBER_FLOAT = 2; - public const FLAG_NUMBER_APPROXIMATE = 4; - public const FLAG_NUMBER_NEGATIVE = 8; - public const FLAG_NUMBER_BINARY = 16; + 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. - public const FLAG_STRING_SINGLE_QUOTES = 1; - public const FLAG_STRING_DOUBLE_QUOTES = 2; + const FLAG_STRING_SINGLE_QUOTES = 1; + const FLAG_STRING_DOUBLE_QUOTES = 2; // Comments related flags. - public const FLAG_COMMENT_BASH = 1; - public const FLAG_COMMENT_C = 2; - public const FLAG_COMMENT_SQL = 4; - public const FLAG_COMMENT_MYSQL_CMD = 8; + const FLAG_COMMENT_BASH = 1; + const FLAG_COMMENT_C = 2; + const FLAG_COMMENT_SQL = 4; + const FLAG_COMMENT_MYSQL_CMD = 8; // Operators related flags. - public const FLAG_OPERATOR_ARITHMETIC = 1; - public const FLAG_OPERATOR_LOGICAL = 2; - public const FLAG_OPERATOR_BITWISE = 4; - public const FLAG_OPERATOR_ASSIGNMENT = 8; - public const FLAG_OPERATOR_SQL = 16; + 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. - public const FLAG_SYMBOL_VARIABLE = 1; - public const FLAG_SYMBOL_BACKTICK = 2; - public const FLAG_SYMBOL_USER = 4; - public const FLAG_SYMBOL_SYSTEM = 8; - public const FLAG_SYMBOL_PARAMETER = 16; + 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. From e8f8b7c2c25351d52e36163a24d8c05c07948a2d Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Tue, 28 Feb 2023 14:33:06 +0200 Subject: [PATCH 17/36] backport latest changes from the plugin --- .../sqlite/class-wp-sqlite-query-rewriter.php | 2 +- .../sqlite/class-wp-sqlite-translator.php | 2060 +++++++++-------- 2 files changed, 1064 insertions(+), 998 deletions(-) diff --git a/src/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php b/src/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php index e91dd75650d97..edb328861826b 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php @@ -296,7 +296,7 @@ private function move_forward( $query = array() ) { /** * Returns the last call stack element. * - * @return WP_SQLite_Token|null + * @return array|null */ public function last_call_stack_element() { return count( $this->call_stack ) ? $this->call_stack[ count( $this->call_stack ) - 1 ] : null; diff --git a/src/wp-includes/sqlite/class-wp-sqlite-translator.php b/src/wp-includes/sqlite/class-wp-sqlite-translator.php index defa722553b42..a1cd14f374b33 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -9,8 +9,7 @@ /** * The queries translator class. */ -class WP_SQLite_Translator extends PDO { - +class WP_SQLite_Translator { const SQLITE_BUSY = 5; const SQLITE_LOCKED = 6; @@ -24,13 +23,6 @@ class WP_SQLite_Translator extends PDO { PRIMARY KEY(`table`, `column_or_index`) );'; - /** - * The SQLite database. - * - * @var PDO - */ - private $sqlite; - /** * Class variable to reference to the PDO instance. * @@ -49,13 +41,6 @@ class WP_SQLite_Translator extends PDO { */ public $client_info = ''; - /** - * The table prefix. - * - * @var string - */ - private $table_prefix; - /** * How to translate field types from MySQL to SQLite. * @@ -146,39 +131,18 @@ class WP_SQLite_Translator extends PDO { ); /** - * The last found rows. - * - * @var int|string - */ - private $last_found_rows = 0; - - /** - * The number of rows found by the last SELECT query. + * Number of rows found by the last SELECT query. * * @var int */ - protected $last_select_found_rows; - - /** - * Class variable which is used for CALC_FOUND_ROW query. - * - * @var unsigned integer - */ - public $found_rows_result = null; - - /** - * Class variable used for query with ORDER BY FIELD() - * - * @var array of the object - */ - public $pre_ordered_results = null; + private $last_select_found_rows; /** - * Class variable to store the last query. + * Number of rows found by the last SQL_CALC_FOUND_ROW query. * - * @var string + * @var int integer */ - public $last_translation; + private $last_sql_calc_found_rows = null; /** * The query rewriter. @@ -188,27 +152,18 @@ class WP_SQLite_Translator extends PDO { private $rewriter; /** - * Class variable to store the query strings. - * - * @var array - */ - public $queries = array(); - - /** - * The query type. + * Last executed MySQL query. * * @var string */ - private $query_type; + public $mysql_query; /** - * Class variable to store the rewritten queries. - * - * @access private + * A list of executed SQLite queries. * * @var array */ - private $rewritten_query; + public $executed_sqlite_queries = array(); /** * The columns to insert. @@ -254,7 +209,7 @@ class WP_SQLite_Translator extends PDO { /** * Class variable to store the affected row id. * - * @var unsigned integer + * @var int integer * @access private */ private $last_insert_id; @@ -262,7 +217,7 @@ class WP_SQLite_Translator extends PDO { /** * Class variable to store the number of rows affected. * - * @var unsigned integer + * @var int integer */ private $affected_rows; @@ -290,12 +245,32 @@ class WP_SQLite_Translator extends PDO { private $return_value; /** - * Variable to check if there is an active transaction. + * Variable to keep track of nested transactions level. * - * @var boolean - * @access protected + * @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 */ - protected $has_active_transaction = false; + private $last_reserved_keyword; /** * Constructor. @@ -319,10 +294,9 @@ public function __construct( $pdo = null ) { try { $dsn = 'sqlite:' . FQDB; $pdo = new PDO( $dsn, null, null, array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ) ); // phpcs:ignore WordPress.DB.RestrictedClasses - new WP_SQLite_PDO_User_Defined_Functions( $pdo ); } catch ( PDOException $ex ) { $status = $ex->getCode(); - if ( 5 === $status || 6 === $status ) { + if ( self::SQLITE_BUSY === $status || self::SQLITE_LOCKED === $status ) { $locked = true; } else { $err_message = $ex->getMessage(); @@ -331,33 +305,36 @@ public function __construct( $pdo = null ) { } while ( $locked ); if ( $status > 0 ) { - $message = sprintf( + $message = sprintf( '

%s

%s

%s

', 'Database initialization error!', "Code: $status", "Error Message: $err_message" ); - $this->is_error = true; - $this->last_error = $message; - - return false; + $this->is_error = true; + $this->error_messages[] = $message; + return; } - - // MySQL data comes across stringified by default. - $pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); - $pdo->query( WP_SQLite_Translator::CREATE_DATA_TYPES_CACHE_TABLE ); } + + new WP_SQLite_PDO_User_Defined_Functions( $pdo ); + + // MySQL data comes across stringified by default. + $pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); + $pdo->query( WP_SQLite_Translator::CREATE_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' ) ); - $this->init(); + $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";' ); - - $this->table_prefix = $GLOBALS['table_prefix']; } /** @@ -367,8 +344,6 @@ public function __construct( $pdo = null ) { * 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 ) { @@ -379,7 +354,7 @@ function __destruct() { gmdate( 'Y-m-d H:i:s', $_SERVER['REQUEST_TIME'] ) ); error_log( $message ); - return true; + return; } if ( stripos( $max, 'M' ) !== false ) { $max = (int) $max * MB_IN_BYTES; @@ -397,27 +372,6 @@ function __destruct() { 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' ); - } } /** @@ -522,114 +476,44 @@ private function prepare_directory() { */ public function query( $statement, $mode = PDO::FETCH_OBJ, ...$fetch_mode_args ) { // phpcs:ignore WordPress.DB.RestrictedClasses $this->flush(); + $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 { - if ( - preg_match( '/^START TRANSACTION/i', $statement ) - || preg_match( '/^BEGIN/i', $statement ) - ) { - return $this->beginTransaction(); - } - if ( preg_match( '/^COMMIT/i', $statement ) ) { - return $this->commit(); - } - if ( preg_match( '/^ROLLBACK/i', $statement ) ) { - return $this->rollBack(); - } + // Perform all the queries in a nested transaction. + $this->begin_transaction(); do { $error = null; try { - $translation = $this->translate( - $statement, - $this->found_rows_result + $this->execute_mysql_query( + $statement ); } catch ( PDOException $error ) { if ( $error->getCode() !== self::SQLITE_BUSY ) { - return $this->handle_error( $error ); + throw $error; } } } while ( $error ); - $stmt = null; - $last_retval = null; - foreach ( $translation->queries as $query ) { - $this->queries[] = "Executing: {$query->sql} | " . ( $query->params ? 'parameters: ' . implode( ', ', $query->params ) : '(no parameters)' ); - do { - $error = null; - try { - $stmt = $this->pdo->prepare( $query->sql ); - $last_retval = $stmt->execute( $query->params ); - } catch ( PDOException $error ) { - if ( $error->getCode() !== self::SQLITE_BUSY ) { - throw $error; - } - } - } while ( $error ); - } - - if ( $translation->has_result ) { - $this->results = $translation->result; - } else { - switch ( $translation->mysql_query_type ) { - case 'DESCRIBE': - $this->results = $stmt->fetchAll( $mode ); - if ( ! $this->results ) { - $this->handle_error( new PDOException( 'Table not found' ) ); - return; - } - break; - case 'SELECT': - case 'SHOW': - $this->results = $stmt->fetchAll( $mode ); - break; - case 'TRUNCATE': - $this->results = true; - $this->return_value = true; - return $this->return_value; - case 'SET': - $this->results = 0; - break; - default: - $this->results = $last_retval; - break; - } - } - - if ( $translation->calc_found_rows ) { - $this->found_rows_result = $translation->calc_found_rows; - } - - if ( is_array( $this->results ) ) { - $this->num_rows = count( $this->results ); - $this->last_select_found_rows = count( $this->results ); - } - - switch ( $translation->sqlite_query_type ) { - case 'DELETE': - case 'UPDATE': - case 'INSERT': - case 'REPLACE': - /* - * 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 - */ - $this->affected_rows = (int) $this->pdo->query( 'select changes()' )->fetch()[0]; - $this->return_value = $this->affected_rows; - $this->num_rows = $this->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; - } - break; - default: - $this->return_value = $this->results; - break; - } - + // 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; } @@ -637,51 +521,13 @@ public function query( $statement, $mode = PDO::FETCH_OBJ, ...$fetch_mode_args ) } } - /** - * Gets the query object. - * - * @param string $sql The SQL query. - * @param array $params The parameters. - * - * @return stdClass - */ - public static function get_query_object( $sql = '', $params = array() ) { - $sql_obj = new stdClass(); - $sql_obj->sql = trim( $sql ); - $sql_obj->params = $params; - return $sql_obj; - } - - /** - * Gets the translation result. - * - * @param array $queries The queries. - * @param boolean $has_result Whether the query has a result. - * @param mixed $custom_output The result. - * - * @return stdClass - */ - protected function get_translation_result( $queries, $has_result = false, $custom_output = null ) { - $result = new stdClass(); - $result->queries = $queries; - $result->has_result = $has_result; - $result->result = $custom_output; - $result->calc_found_rows = null; - $result->sqlite_query_type = null; - $result->mysql_query_type = null; - $result->rewriter = null; - $result->query_type = null; - - return $result; - } - /** * 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 + * @return array|null of the object */ public function get_columns() { if ( ! empty( $this->results ) ) { @@ -711,11 +557,16 @@ public function get_columns() { '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. + 'int' => 0, // 1 if column is int integer. 'zerofill' => 0, // 1 if column is zero-filled. ); $table_name = ''; - if ( preg_match( '/\s*FROM\s*(.*)?\s*/i', $this->rewritten_query, $match ) ) { + $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 ) { @@ -769,37 +620,41 @@ public function get_return_value() { } /** - * Translates the query. + * Executes a MySQL query in SQLite. * - * @param string $query The query. - * @param int|string $last_found_rows The last found rows. + * @param string $query The query. * * @throws Exception If the query is not supported. - * - * @return stdClass */ - public function translate( string $query, $last_found_rows = null ) { - $this->last_found_rows = $last_found_rows; - - $tokens = ( new WP_SQLite_Lexer( $query ) )->tokens; - $this->rewriter = new WP_SQLite_Query_Rewriter( $tokens ); - $this->query_type = $this->rewriter->peek()->value; + 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 ( $this->query_type ) { + switch ( $query_type ) { case 'ALTER': - $result = $this->translate_alter(); + $this->execute_alter(); break; case 'CREATE': - $result = $this->translate_create(); + $this->execute_create(); break; - case 'REPLACE': case 'SELECT': + $this->execute_select(); + break; + case 'INSERT': + case 'REPLACE': + $this->execute_insert_or_replace(); + break; + case 'UPDATE': + $this->execute_update(); + break; + case 'DELETE': - $result = $this->translate_crud(); + $this->execute_delete(); break; case 'CALL': @@ -808,119 +663,52 @@ public function translate( string $query, $last_found_rows = null ) { * It would be lovely to support at least SET autocommit, * but I don't think that is even possible with SQLite. */ - $result = $this->get_translation_result( array( $this->noop() ) ); + $this->results = 0; break; case '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(); - $result = $this->get_translation_result( - array( - WP_SQLite_Translator::get_query_object( $this->rewriter->get_updated_query() ), - ) - ); + $this->execute_truncate(); break; + case 'BEGIN': case 'START TRANSACTION': - $result = $this->get_translation_result( - array( - WP_SQLite_Translator::get_query_object( 'BEGIN' ), - ) - ); + $this->results = $this->begin_transaction(); break; - case 'BEGIN': case 'COMMIT': + $this->results = $this->commit(); + break; + case 'ROLLBACK': - $result = $this->get_translation_result( - array( - WP_SQLite_Translator::get_query_object( $query ), - ) - ); + $this->results = $this->rollback(); break; case 'DROP': - $result = $this->translate_drop(); + $this->execute_drop(); break; - case 'DESCRIBE': - $this->rewriter->skip(); - $table_name = $this->rewriter->consume()->value; - $result = $this->get_translation_result( - array( - WP_SQLite_Translator::get_query_object( - "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` - ; - " - ), - ) - ); + case 'SHOW': + $this->execute_show(); break; - case 'SHOW': - $result = $this->translate_show(); + case 'DESCRIBE': + $this->execute_describe(); break; default: - throw new Exception( 'Unknown query type: ' . $this->query_type ); + throw new Exception( 'Unknown query type: ' . $query_type ); } - // The query type could have changed – let's grab the new one. - if ( count( $result->queries ) ) { - $last_query = $result->queries[ count( $result->queries ) - 1 ]; - $first_word = preg_match( '/^\s*(\w+)/', $last_query->sql, $matches ) ? $matches[1] : ''; - $result->sqlite_query_type = strtoupper( $first_word ); - } - $result->mysql_query_type = $this->query_type; - return $result; } /** - * Translates the CREATE TABLE query. + * Executes a MySQL CREATE TABLE query in SQLite. * * @throws Exception If the query is not supported. - * - * @return stdClass */ - private function translate_create_table() { + private function execute_create_table() { $table = $this->parse_create_table(); - $extra_queries = array(); - $definitions = array(); + $definitions = array(); foreach ( $table->fields as $field ) { /* * Do not include the inline PRIMARY KEY definition @@ -933,11 +721,11 @@ private function translate_create_table() { throw new Exception( 'Cannot combine AUTOINCREMENT and multiple primary keys in SQLite' ); } - $definitions[] = $this->make_sqlite_field_definition( $field ); - $extra_queries[] = $this->update_data_type_cache( + $definitions[] = $this->make_sqlite_field_definition( $field ); + $this->update_data_type_cache( $table->name, $field->name, - $field->mysql_data_type, + $field->mysql_data_type ); } @@ -945,38 +733,32 @@ private function translate_create_table() { $definitions[] = 'PRIMARY KEY ("' . implode( '", "', $table->primary_key ) . '")'; } - $create_table_query = WP_SQLite_Translator::get_query_object( + $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' === $constraint->value ) { + if ( 'UNIQUE INDEX' === $index_type ) { $unique = 'UNIQUE '; } - $index_name = "{$table->name}__{$constraint->name}"; - $extra_queries[] = WP_SQLite_Translator::get_query_object( + $index_name = "{$table->name}__{$constraint->name}"; + $this->execute_sqlite_query( "CREATE $unique INDEX \"$index_name\" ON \"{$table->name}\" (\"" . implode( '", "', $constraint->columns ) . '")' ); - $extra_queries[] = $this->update_data_type_cache( + $this->update_data_type_cache( $table->name, $index_name, - $constraint->value, + $constraint->value ); } - - return $this->get_translation_result( - array_merge( - array( - $create_table_query, - ), - $extra_queries - ) - ); } /** @@ -1059,7 +841,7 @@ private function parse_create_table() { ) ) { $result->fields[] = $this->parse_mysql_create_table_field(); } else { - $result->constraints[] = $this->parse_mysql_create_table_constraint( $result->name ); + $result->constraints[] = $this->parse_mysql_create_table_constraint(); } /* @@ -1134,7 +916,7 @@ private function parse_mysql_create_table_field() { if ( $token->matches( WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED, - array( 'NOT NULL' ), + array( 'NOT NULL' ) ) ) { $result->not_null = true; continue; @@ -1143,7 +925,7 @@ private function parse_mysql_create_table_field() { if ( $token->matches( WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED, - array( 'PRIMARY KEY' ), + array( 'PRIMARY KEY' ) ) ) { $result->primary_key = true; continue; @@ -1152,7 +934,7 @@ private function parse_mysql_create_table_field() { if ( $token->matches( WP_SQLite_Token::TYPE_KEYWORD, null, - array( 'AUTO_INCREMENT' ), + array( 'AUTO_INCREMENT' ) ) ) { $result->primary_key = true; $result->auto_increment = true; @@ -1162,7 +944,7 @@ private function parse_mysql_create_table_field() { if ( $token->matches( WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, - array( 'DEFAULT' ), + array( 'DEFAULT' ) ) ) { $result->default = $this->rewriter->consume()->token; continue; @@ -1294,277 +1076,49 @@ private function is_create_table_field_terminator( $token, $definition_depth, $c ); } - /** - * Translator method. - * - * @throws Exception If the query type is unknown. - * - * @return stdClass + * Executes a DELETE statement. */ - private function translate_crud() { - $query_type = $this->rewriter->consume()->value; - - $params = array(); - $is_in_duplicate_section = false; - $table_name = null; - $has_sql_calc_found_rows = false; - - // Consume the query type. - if ( 'INSERT' === $query_type && '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(); - if ( 'INSERT' === $query_type || 'REPLACE' === $query_type ) { - $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; - } - } - } - } + private function execute_delete() { + $this->rewriter->consume(); // DELETE - $last_reserved_keyword = null; + // Process expressions and extract bound parameters + $params = array(); while ( true ) { $token = $this->rewriter->peek(); if ( ! $token ) { break; } - if ( WP_SQLite_Token::TYPE_KEYWORD === $token->type && $token->flags & WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) { - $last_reserved_keyword = $token->value; - if ( 'FROM' === $last_reserved_keyword ) { - $from_table = $this->rewriter->peek_nth( 2 )->value; - if ( 'DUAL' === strtoupper( $from_table ) ) { - // FROM DUAL is a MySQLism that means "no tables". - $this->rewriter->skip(); - $this->rewriter->skip(); - continue; - } elseif ( ! $table_name ) { - $table_name = $from_table; - } - } - } - - if ( 'SQL_CALC_FOUND_ROWS' === $token->value && WP_SQLite_Token::TYPE_KEYWORD === $token->type ) { - $has_sql_calc_found_rows = true; - $this->rewriter->skip(); - continue; - } + $this->remember_last_reserved_keyword( $token ); - if ( 'AS' !== $last_reserved_keyword && WP_SQLite_Token::TYPE_STRING === $token->type && $token->flags & WP_SQLite_Token::FLAG_STRING_SINGLE_QUOTES ) { - // Rewrite string values to bound parameters. - $param_name = ':param' . count( $params ); - $params[ $param_name ] = $this->preprocess_string_literal( $token->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 ) ); + if ( + $this->extract_bound_parameter( $token, $params ) + || $this->translate_expression( $token ) + ) { continue; } - if ( WP_SQLite_Token::TYPE_KEYWORD === $token->type ) { - if ( - $this->translate_concat_function( $token ) - || $this->translate_cast_as_binary( $token ) - || $this->translate_date_add_sub( $token ) - || $this->translate_values_function( $token, $is_in_duplicate_section ) - || $this->translate_date_format( $token ) - || $this->translate_interval( $token ) - || $this->translate_regexp_functions( $token ) - ) { - continue; - } - - if ( 'INSERT' === $query_type && 'DUPLICATE' === $token->keyword ) { - $is_in_duplicate_section = true; - $this->translate_on_duplicate_key( $table_name ); - continue; - } - } - - if ( $this->translate_concat_comma_to_pipes( $token ) ) { - continue; - } $this->rewriter->consume(); } $this->rewriter->consume_all(); $updated_query = $this->rewriter->get_updated_query(); - $result = $this->get_translation_result( array() ); - - if ( 'SELECT' === $query_type && $table_name && str_starts_with( strtolower( $table_name ), 'information_schema' ) ) { - return $this->translate_information_schema_query( - $updated_query - ); - } - - /* - * 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('`. - */ - if ( - strpos( $updated_query, '@@SESSION.sql_mode' ) !== false - || strpos( $updated_query, 'CONVERT( ' ) !== false - ) { - $updated_query = 'SELECT 1=0'; - $params = array(); - } - // Emulate SQL_CALC_FOUND_ROWS for now. - if ( $has_sql_calc_found_rows ) { - $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->pdo->prepare( $unlimited_query ); - $stmt->execute( $params ); - $result->calc_found_rows = count( $stmt->fetchAll() ); - } + // Perform DELETE-specific translations - // Emulate FOUND_ROWS() by counting the rows in the result set. - if ( strpos( $updated_query, 'FOUND_ROWS(' ) !== false ) { - $last_found_rows = ( $this->last_found_rows ? $this->last_found_rows : 0 ) . ''; - $result->queries[] = WP_SQLite_Translator::get_query_object( - "SELECT {$last_found_rows} AS `FOUND_ROWS()`", + // 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)" ); - return $result; + $this->set_result_from_affected_rows(); + return; } - /* - * Now that functions are rewritten to SQLite dialect, - * let's translate unsupported delete queries. - */ - if ( 'DELETE' === $query_type ) { - $delete_result = $this->postprocess_double_delete( $params ); - if ( $delete_result ) { - return $delete_result; - } - } - - $result->queries[] = WP_SQLite_Translator::get_query_object( $updated_query, $params ); - return $result; - } - - /** - * 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 ) ) { - if ( false === strtotime( $value ) ) { - $value = '0000-00-00 00:00:00'; - } - } - return $value; - } - - /** - * Postprocesses a double delete query. - * - * @param array $rewritten_params The rewritten parameters. - * - * @throws Exception If the query is not a double delete query. - * - * @return WP_SQLite_Translation_Result|null The translation result or null if the query is not a double delete query. - */ - private function postprocess_double_delete( $rewritten_params ) { - // Naive rewriting of DELETE JOIN query. - // @TODO: Actually rewrite the query instead of using a hardcoded workaround. - $updated_query = $this->rewriter->get_updated_query(); - if ( str_contains( $updated_query, ' JOIN ' ) ) { - return $this->get_translation_result( - array( - WP_SQLite_Translator::get_query_object( - "DELETE FROM {$this->table_prefix}options WHERE option_id IN (SELECT MIN(option_id) FROM {$this->table_prefix}options GROUP BY option_name HAVING COUNT(*) > 1)" - ), - ) - ); - } - - $rewriter = new WP_SQLite_Query_Rewriter( $this->rewriter->output_tokens ); + $rewriter = new WP_SQLite_Query_Rewriter( $this->rewriter->output_tokens ); $comma = $rewriter->peek( array( @@ -1578,11 +1132,20 @@ private function postprocess_double_delete( $rewritten_params ) { 'value' => 'FROM', ) ); - // It's a dual delete query if the comma comes before the 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 ) ); @@ -1611,7 +1174,7 @@ private function postprocess_double_delete( $rewritten_params ) { * Now, let's figure out the primary key name. * This assumes that all listed table names are the same. */ - $q = $this->pdo->query( 'SELECT l.name FROM pragma_table_info("' . $table_name . '") as l WHERE l.pk = 1;' ); + $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']; /* @@ -1648,8 +1211,8 @@ private function postprocess_double_delete( $rewritten_params ) { // Select the IDs to delete. $select = $rewriter->get_updated_query(); - $stmt = $this->pdo->prepare( $select ); - $stmt->execute( $rewritten_params ); + $stmt = $this->execute_sqlite_query( $select ); + $stmt->execute( $params ); $rows = $stmt->fetchAll(); $ids_to_delete = array(); foreach ( $rows as $id ) { @@ -1662,49 +1225,356 @@ private function postprocess_double_delete( $rewritten_params ) { ? "DELETE FROM {$table_name} WHERE {$pk_name} IN (" . implode( ',', $ids_to_delete ) . ')' : "DELETE FROM {$table_name} WHERE 0=1" ); - return $this->get_translation_result( - array( - WP_SQLite_Translator::get_query_object( $query ), - ), - true, + $this->execute_sqlite_query( $query ); + $this->set_result_from_affected_rows( count( $ids_to_delete ) ); } /** - * Translate an information_schema query. - * - * @param string $query The query to translate. - * - * @return WP_SQLite_Translation_Result + * Executes a SELECT statement. */ - private function translate_information_schema_query( $query ) { - // @TODO: Actually rewrite the columns. - if ( str_contains( $query, 'bytes' ) ) { - // Count rows per table. - $tables = $this->pdo->query( "SELECT name as `table` FROM sqlite_master WHERE type='table' ORDER BY name" )->fetchAll(); - $rows = '(CASE '; - foreach ( $tables as $table ) { - $table_name = $table['table']; - $count = $this->pdo->query( "SELECT COUNT(*) as `count` FROM $table_name" )->fetch(); - $rows .= " WHEN name = '$table_name' THEN {$count['count']} "; + 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; } - $rows .= 'ELSE 0 END) '; - return $this->get_translation_result( + + $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' ) ) { + // @TODO: Actually rewrite the columns. + if ( str_contains( $updated_query, 'bytes' ) ) { + // Count rows per table. + $tables = $this->execute_sqlite_query( "SELECT name as `table` FROM sqlite_master WHERE type='table' ORDER BY name" )->fetchAll(); + $rows = '(CASE '; + foreach ( $tables as $table ) { + $table_name = $table['table']; + $count = $this->execute_sqlite_query( "SELECT COUNT(*) as `count` FROM $table_name" )->fetch(); + $rows .= " WHEN name = '$table_name' THEN {$count['count']} "; + } + $rows .= 'ELSE 0 END) '; + $updated_query = "SELECT name as `table`, $rows as `rows`, 0 as `bytes` FROM sqlite_master WHERE type='table' ORDER BY name"; + } else { + $updated_query = "SELECT name, 'myisam' as `engine`, 0 as `data`, 0 as `index` FROM sqlite_master WHERE type='table' ORDER BY name"; + } + $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 ); + $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. + */ + 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( - WP_SQLite_Translator::get_query_object( - "SELECT name as `table`, $rows as `rows`, 0 as `bytes` FROM sqlite_master WHERE type='table' ORDER BY name" - ), + '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; + } + } } - return $this->get_translation_result( - array( - WP_SQLite_Translator::get_query_object( - "SELECT name, 'myisam' as `engine`, 0 as `data`, 0 as `index` FROM sqlite_master WHERE type='table' ORDER BY name" - ), + + 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 ) ) { + if ( false === strtotime( $value ) ) { + $value = '0000-00-00 00:00:00'; + } + } + return $value; } /** @@ -1715,20 +1585,162 @@ private function translate_information_schema_query( $query ) { * @return bool */ private function translate_cast_as_binary( $token ) { - if ( $token->matches( WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_DATA_TYPE ) ) { - $call_parent = $this->rewriter->last_call_stack_element(); - // Rewrite AS BINARY to AS BLOB inside CAST() calls. - if ( - $call_parent - && 'CAST' === $call_parent['function'] - && 'BINARY' === $token->value - ) { - $this->rewriter->skip(); - $this->rewriter->add( new WP_SQLite_Token( 'BLOB', $token->type, $token->flags ) ); - return true; - } + 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_cast_as_binary( $token ) + || $this->translate_date_add_sub( $token ) + || $this->translate_date_format( $token ) + || $this->translate_interval( $token ) + || $this->translate_regexp_functions( $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; } - return false; + + $param_name = ':param' . count( $params ); + $params[ $param_name ] = $this->preprocess_string_literal( $token->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; } /** @@ -1739,19 +1751,22 @@ private function translate_cast_as_binary( $token ) { * @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 ||. */ - if ( - 'CONCAT' === $token->keyword - && $token->flags & WP_SQLite_Token::FLAG_KEYWORD_FUNCTION - ) { - $this->rewriter->skip(); - return true; - } - return false; + $this->rewriter->skip(); + return true; } /** @@ -1762,21 +1777,27 @@ private function translate_concat_function( $token ) { * @return bool */ private function translate_concat_comma_to_pipes( $token ) { - if ( WP_SQLite_Token::TYPE_OPERATOR === $token->type ) { - $call_parent = $this->rewriter->last_call_stack_element(); - // Rewrite commas to || in CONCAT() calls. - if ( - $call_parent - && 'CONCAT' === $call_parent['function'] - && ',' === $token->value - && $token->flags & WP_SQLite_Token::FLAG_OPERATOR_SQL - ) { - $this->rewriter->skip(); - $this->rewriter->add( new WP_SQLite_Token( '||', WP_SQLite_Token::TYPE_OPERATOR ) ); - return true; - } + if ( ! $token->matches( + WP_SQLite_Token::TYPE_OPERATOR, + WP_SQLite_Token::FLAG_OPERATOR_SQL, + array( ',' ) + ) + ) { + return false; } - 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; } /** @@ -1788,16 +1809,18 @@ private function translate_concat_comma_to_pipes( $token ) { */ private function translate_date_add_sub( $token ) { if ( - $token->flags & WP_SQLite_Token::FLAG_KEYWORD_FUNCTION && ( - 'DATE_ADD' === $token->keyword || - 'DATE_SUB' === $token->keyword + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, + array( 'DATE_ADD', 'DATE_SUB' ) ) ) { - $this->rewriter->skip(); - $this->rewriter->add( new WP_SQLite_Token( 'DATETIME', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ) ); - return true; + return false; } - 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; } /** @@ -1808,33 +1831,36 @@ private function translate_date_add_sub( $token ) { * * @return bool */ - private function translate_values_function( $token, $is_in_duplicate_section ) { + private function translate_values_function( $token ) { if ( - $token->flags & WP_SQLite_Token::FLAG_KEYWORD_FUNCTION && - 'VALUES' === $token->keyword && - $is_in_duplicate_section + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, + array( 'VALUES' ) + ) ) { - /* - * 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; + return false; } - 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; } /** @@ -1848,93 +1874,97 @@ private function translate_values_function( $token, $is_in_duplicate_section ) { */ private function translate_date_format( $token ) { if ( - $token->flags & WP_SQLite_Token::FLAG_KEYWORD_FUNCTION && - 'DATE_FORMAT' === $token->keyword + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, + array( 'DATE_FORMAT' ) + ) ) { - // Rewrite DATE_FORMAT( `post_date`, '%Y-%m-%d' ) to STRFTIME( '%Y-%m-%d', `post_date` ). + return false; + } - // Skip the DATE_FORMAT function name. - $this->rewriter->skip(); - // Skip the opening `(`. - $this->rewriter->skip(); + // Rewrite DATE_FORMAT( `post_date`, '%Y-%m-%d' ) to STRFTIME( '%Y-%m-%d', `post_date` ). - // 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' => ',', - ) - ); + // Skip the DATE_FORMAT function name. + $this->rewriter->skip(); + // Skip the opening `(`. + $this->rewriter->skip(); - // 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 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' => ',', + ) + ); - // 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)" ); - } + // 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' ); + } - /* - * 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 ) ); - } + // 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)" ); + } - $this->rewriter->add( new WP_SQLite_Token( 'STRFTIME', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ) ); + /* + * 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( "'$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 ); + $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 ) ); - // Consume the closing ')'. - $this->rewriter->consume( - array( - 'type' => WP_SQLite_Token::TYPE_OPERATOR, - 'value' => ')', - ) - ); + // Add the buffered tokens back to the stream. + $this->rewriter->add_many( $first_arg ); - 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 ) ); - } + // Consume the closing ')'. + $this->rewriter->consume( + array( + 'type' => WP_SQLite_Token::TYPE_OPERATOR, + 'value' => ')', + ) + ); - return true; + 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 false; + + return true; } /** @@ -1945,44 +1975,50 @@ private function translate_date_format( $token ) { * @return bool */ private function translate_interval( $token ) { - if ( 'INTERVAL' === $token->keyword ) { - // Skip the INTERVAL keyword from the output stream. - $this->rewriter->skip(); + 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; + $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; - } + /* + * 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; } - return false; + + $this->rewriter->add( new WP_SQLite_Token( "'{$interval_op}$num $unit'", WP_SQLite_Token::TYPE_STRING ) ); + return true; } /** @@ -1993,45 +2029,51 @@ private function translate_interval( $token ) { * @return bool */ private function translate_regexp_functions( $token ) { - if ( 'REGEXP' === $token->keyword || 'RLIKE' === $token->keyword ) { - $this->rewriter->skip(); - $this->rewriter->add( new WP_SQLite_Token( 'REGEXP', WP_SQLite_Token::TYPE_KEYWORD ) ); + 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(); + $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; + /* + * 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 false; + return true; } /** @@ -2140,7 +2182,7 @@ private function translate_on_duplicate_key( $table_name ) { * @return array */ private function get_primary_keys( $table_name ) { - $stmt = $this->pdo->prepare( 'SELECT * FROM pragma_table_info(:table_name) as l WHERE l.pk > 0;' ); + $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(); } @@ -2154,12 +2196,12 @@ private function get_primary_keys( $table_name ) { * @return array */ private function get_keys( $table_name, $only_unique = false ) { - $query = $this->pdo->query( 'SELECT * FROM pragma_index_list("' . $table_name . '") as l;' ); + $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->pdo->query( 'SELECT * FROM pragma_index_info("' . $index['name'] . '") as l;' ); + $query = $this->execute_sqlite_query( 'SELECT * FROM pragma_index_info("' . $index['name'] . '") as l;' ); $results[] = array( 'index' => $index, 'columns' => $query->fetchAll(), @@ -2177,7 +2219,7 @@ private function get_keys( $table_name, $only_unique = false ) { * @return string */ private function get_sqlite_create_table( $table_name ) { - $stmt = $this->pdo->prepare( 'SELECT sql FROM sqlite_master WHERE type="table" AND name=:table' ); + $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 ) { @@ -2191,9 +2233,8 @@ private function get_sqlite_create_table( $table_name ) { * * @throws Exception If the subject is not 'table', or we're performing an unknown operation. * - * @return stdClass */ - private function translate_alter() { + private function execute_alter() { $this->rewriter->consume(); $subject = strtolower( $this->rewriter->consume()->token ); if ( 'table' !== $subject ) { @@ -2201,7 +2242,6 @@ private function translate_alter() { } $table_name = $this->normalize_column_name( $this->rewriter->consume()->token ); - $queries = array(); do { /* * This loop may be executed multiple times if there are multiple operations in the ALTER query. @@ -2235,7 +2275,7 @@ private function translate_alter() { WP_SQLite_Token::FLAG_KEYWORD_DATA_TYPE ) ); - $queries[] = $this->update_data_type_cache( + $this->update_data_type_cache( $table_name, $column_name, $mysql_data_type @@ -2243,25 +2283,11 @@ private function translate_alter() { } elseif ( 'DROP' === $op_type && 'COLUMN' === $op_subject ) { $this->rewriter->consume_all(); } elseif ( 'CHANGE' === $op_type && 'COLUMN' === $op_subject ) { - if ( count( $queries ) ) { - /* - * Mixing CHANGE COLUMN with other operations would require keeping track of the - * original table schema, and then applying the changes in order. This is not - * currently supported. - * - * Ideally, each ALTER TABLE operation would be flushed before the next one is - * processed, but that's not currently the case. - */ - throw new Exception( - 'Mixing CHANGE COLUMN with other operations in a single ALTER TABLE ' . - 'query is not supported yet.' - ); - } // 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 ); - $queries[] = $this->update_data_type_cache( + $this->update_data_type_cache( $table_name, $new_field->name, $new_field->mysql_data_type @@ -2343,29 +2369,21 @@ private function translate_alter() { // 3. Copy the data out of the old table $cache_table_name = "_tmp__{$table_name}_" . rand( 10000000, 99999999 ); - $queries[] = WP_SQLite_Translator::get_query_object( + $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 - $queries[] = WP_SQLite_Translator::get_query_object( - "DROP TABLE `$table_name`" - ); + $this->execute_sqlite_query( "DROP TABLE `$table_name`" ); // 5. Create a new table from the updated schema - $queries[] = WP_SQLite_Translator::get_query_object( - $create_table->get_updated_query() - ); + $this->execute_sqlite_query( $create_table->get_updated_query() ); // 6. Copy the data from step 3 to the new table - $queries[] = WP_SQLite_Translator::get_query_object( - "INSERT INTO {$table_name} SELECT * FROM $cache_table_name" - ); + $this->execute_sqlite_query( "INSERT INTO {$table_name} SELECT * FROM $cache_table_name" ); // 7. Drop the old table copy - $queries[] = WP_SQLite_Translator::get_query_object( - "DROP TABLE `$cache_table_name`" - ); + $this->execute_sqlite_query( "DROP TABLE `$cache_table_name`" ); // 8. Restore any indexes that were dropped in step 4 foreach ( $old_indexes as $row ) { @@ -2390,7 +2408,7 @@ private function translate_alter() { * Use IF NOT EXISTS to avoid collisions with indexes that were * a part of the CREATE TABLE statement */ - $queries[] = WP_SQLite_Translator::get_query_object( + $this->execute_sqlite_query( "CREATE $unique INDEX IF NOT EXISTS `{$row['index']['name']}` ON $table_name (" . implode( ', ', $columns ) . ')' ); } @@ -2424,7 +2442,7 @@ private function translate_alter() { new WP_SQLite_Token( '(', WP_SQLite_Token::TYPE_OPERATOR ), ) ); - $queries[] = $this->update_data_type_cache( + $this->update_data_type_cache( $table_name, $sqlite_index_name, $mysql_index_type @@ -2486,21 +2504,21 @@ private function translate_alter() { ) ); $this->rewriter->drop_last(); - $queries[] = WP_SQLite_Translator::get_query_object( + $this->execute_sqlite_query( $this->rewriter->get_updated_query() ); } while ( $comma ); - return $this->get_translation_result( $queries ); + $this->results = 1; + $this->return_value = $this->results; } /** * Translates a CREATE query. * * @throws Exception If the query is an unknown create type. - * @return stdClass The translation result. */ - private function translate_create() { + private function execute_create() { $this->rewriter->consume(); $what = $this->rewriter->consume()->token; @@ -2517,11 +2535,13 @@ private function translate_create() { switch ( $what ) { case 'TABLE': - return $this->translate_create_table(); + $this->execute_create_table(); + break; case 'PROCEDURE': case 'DATABASE': - return $this->get_translation_result( array( $this->noop() ) ); + $this->results = true; + break; default: throw new Exception( 'Unknown create type: ' . $what ); @@ -2532,10 +2552,8 @@ private function translate_create() { * Translates a DROP query. * * @throws Exception If the query is an unknown drop type. - * - * @return stdClass The translation result. */ - private function translate_drop() { + private function execute_drop() { $this->rewriter->consume(); $what = $this->rewriter->consume()->token; @@ -2553,11 +2571,14 @@ private function translate_drop() { switch ( $what ) { case 'TABLE': $this->rewriter->consume_all(); - return $this->get_translation_result( array( WP_SQLite_Translator::get_query_object( $this->rewriter->get_updated_query() ) ) ); + $this->execute_sqlite_query( $this->rewriter->get_updated_query() ); + $this->results = $this->last_exec_returned; + break; case 'PROCEDURE': case 'DATABASE': - return $this->get_translation_result( array( $this->noop() ) ); + $this->results = true; + return; default: throw new Exception( 'Unknown drop type: ' . $what ); @@ -2568,37 +2589,29 @@ private function translate_drop() { * Translates a SHOW query. * * @throws Exception If the query is an unknown show type. - * @return stdClass The translation result. */ - private function translate_show() { + 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': - return $this->get_translation_result( array( $this->noop() ) ); + $this->results = true; + return; case 'FULL COLUMNS': $this->rewriter->consume(); - $table_name = $this->rewriter->consume()->token; - return $this->get_translation_result( - array( - WP_SQLite_Translator::get_query_object( - "PRAGMA table_info($table_name);" - ), - ) - ); - + // Fall through. case 'COLUMNS FROM': $table_name = $this->rewriter->consume()->token; - return $this->get_translation_result( - array( - WP_SQLite_Translator::get_query_object( - "PRAGMA table_info(\"$table_name\");" - ), - ) + $stmt = $this->execute_sqlite_query( + "PRAGMA table_info(\"$table_name\");" ); + $this->set_results_from_fetched_data( + $stmt->fetchAll( $this->pdo_fetch_mode ) + ); + return; case 'INDEX FROM': $table_name = $this->rewriter->consume()->token; @@ -2666,47 +2679,39 @@ private function translate_show() { ) ); } - return $this->get_translation_result( - array( - WP_SQLite_Translator::get_query_object( - 'SELECT 1=1;' - ), - ), - true, + $this->set_results_from_fetched_data( $results ); + return; case 'TABLES LIKE': $table_expression = $this->rewriter->skip(); - return $this->get_translation_result( + $stmt = $this->execute_sqlite_query( + "SELECT `name` as `Tables_in_db` FROM `sqlite_master` WHERE `type`='table' AND `name` LIKE :param;", array( - WP_SQLite_Translator::get_query_object( - "SELECT `name` as `Tables_in_db` FROM `sqlite_master` WHERE `type`='table' AND `name` LIKE :param;", - array( - ':param' => $table_expression->value, - ) - ), + ':param' => $table_expression->value, ) ); + $this->set_results_from_fetched_data( + $stmt->fetchAll( $this->pdo_fetch_mode ) + ); + return; default: switch ( $what1 ) { case 'TABLES': - return $this->get_translation_result( - array( - WP_SQLite_Translator::get_query_object( - "SELECT name FROM sqlite_master WHERE type='table'" - ), - ) + $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': - return $this->get_translation_result( - array( - $this->noop(), - ) - ); + $this->results = true; + return; default: throw new Exception( 'Unknown show type: ' . $what ); @@ -2714,18 +2719,6 @@ private function translate_show() { } } - /** - * Returns a dummy `SELECT 1=1` query object. - * - * @return stdClass The dummy query object. - */ - private function noop() { - return WP_SQLite_Translator::get_query_object( - 'SELECT 1 WHERE 1=0;', - array() - ); - } - /** * Consumes data types from the query. * @@ -2757,9 +2750,9 @@ private function skip_mysql_data_type() { $mysql_data_type .= $this->rewriter->skip()->token; } - // Skip the unsigned keyword. - $unsigned_maybe = $this->rewriter->peek(); - if ( $unsigned_maybe && $unsigned_maybe->matches( + // Skip the int keyword. + $int_maybe = $this->rewriter->peek(); + if ( $int_maybe && $int_maybe->matches( WP_SQLite_Token::TYPE_KEYWORD, null, array( 'UNSIGNED' ) @@ -2780,10 +2773,10 @@ private function skip_mysql_data_type() { * @param string $column_or_index The column or index name. * @param string $mysql_data_type The MySQL data type. * - * @return stdClass The query object. + * @return void */ private function update_data_type_cache( $table, $column_or_index, $mysql_data_type ) { - return WP_SQLite_Translator::get_query_object( + $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 @@ -2805,12 +2798,10 @@ private function update_data_type_cache( $table, $column_or_index, $mysql_data_t * @return string The MySQL data type. */ private function get_cached_mysql_data_type( $table, $column_or_index ) { - $stmt = $this->pdo->prepare( + $stmt = $this->execute_sqlite_query( 'SELECT d.`mysql_type` FROM ' . self::DATA_TYPES_CACHE_TABLE . ' d WHERE `table`=:table AND `column_or_index` = :index', - ); - $stmt->execute( array( ':table' => $table, ':index' => $column_or_index, @@ -2946,10 +2937,14 @@ public function get_error_message() { $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->queries as $q ) { - $output .= '
  1. ' . htmlspecialchars( $q ) . '
  2. ' . PHP_EOL; + foreach ( $this->executed_sqlite_queries as $q ) { + $message = "Executing: {$q['sql']} | " . ( $q['params'] ? 'parameters: ' . implode( ', ', $q['params'] ) : '(no parameters)' ); + + $output .= '
  3. ' . htmlspecialchars( $message ) . '
  4. ' . PHP_EOL; } $output .= '
' . PHP_EOL; $output .= '
' . PHP_EOL; @@ -2974,62 +2969,133 @@ public function get_error_message() { return $output; } + /** + * Executes a query in SQLite – for internal use only. + * + * @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. + * } + */ + private function execute_sqlite_query( $sql, $params = array() ) { + $this->executed_sqlite_queries[] = array( + 'sql' => $sql, + 'params' => $params, + ); + + $stmt = $this->pdo->prepare( $sql ); + $this->last_exec_returned = $stmt->execute( $params ); + return $stmt; + } + + 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; + } + + 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->rewritten_query = ''; - $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->error_messages = array(); - $this->is_error = false; - $this->queries = array(); - $this->param_num = 0; + $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->last_exec_returned = null; } /** - * Method to call PDO::beginTransaction(). + * Begin a new transaction or nested transaction. * - * @see PDO::beginTransaction() * @return boolean */ - public function beginTransaction() { - if ( $this->has_active_transaction ) { - return false; + 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; + } } - $this->has_active_transaction = $this->pdo->beginTransaction(); - return $this->has_active_transaction; + return $success; } /** - * Method to call PDO::commit(). + * Commit the current transaction or nested transaction. * - * @see PDO::commit() - * - * @return void + * @return boolean True on success, false on failure. */ public function commit() { - if ( $this->has_active_transaction ) { - $this->pdo->commit(); - $this->has_active_transaction = false; + 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; } /** - * Method to call PDO::rollBack(). - * - * @see PDO::rollBack() + * Rollback the current transaction or nested transaction. * - * @return void + * @return boolean True on success, false on failure. */ - public function rollBack() { - if ( $this->has_active_transaction ) { - $this->pdo->rollBack(); - $this->has_active_transaction = false; + 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; } } From fa4a2bc30ede3e26818438fe43540afabb06792a Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Wed, 1 Mar 2023 08:49:16 +0200 Subject: [PATCH 18/36] Remove type declarations (PHP Compatibility) --- src/wp-includes/sqlite/class-wp-sqlite-lexer.php | 2 +- src/wp-includes/sqlite/class-wp-sqlite-token.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/sqlite/class-wp-sqlite-lexer.php b/src/wp-includes/sqlite/class-wp-sqlite-lexer.php index fa202c37555e1..5adf9caa5516f 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-lexer.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-lexer.php @@ -2533,7 +2533,7 @@ public function tokens( array $tokens = array(), $count = -1 ) { * @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( int $type, int $flag ) { + 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++ ]; diff --git a/src/wp-includes/sqlite/class-wp-sqlite-token.php b/src/wp-includes/sqlite/class-wp-sqlite-token.php index 2f85b0daa03ac..12b393e29cc25 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-token.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-token.php @@ -213,7 +213,7 @@ public function __construct( $token, $type = 0, $flags = 0 ) { * * @return bool */ - public function matches( $type = null, $flags = null, ?array $values = null ) { + public function matches( $type = null, $flags = null, $values = null ) { if ( null === $type && null === $flags && ( null === $values || array() === $values ) ) { return ! $this->is_semantically_void(); } From 55fd8ff996d789d1d378b19818d75a5f657664ce Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Wed, 1 Mar 2023 08:56:15 +0200 Subject: [PATCH 19/36] overzealous check --- src/wp-admin/includes/upgrade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/upgrade.php b/src/wp-admin/includes/upgrade.php index bd904067ee034..71c4432eca4cd 100644 --- a/src/wp-admin/includes/upgrade.php +++ b/src/wp-admin/includes/upgrade.php @@ -53,7 +53,7 @@ function wp_install( $blog_title, $user_name, $user_email, $is_public, $deprecat wp_cache_flush(); if ( defined( 'DATABASE_TYPE' ) && 'sqlite' === DATABASE_TYPE ) { - sqlite_make_db_sqlite(); + sqlite_make_db_sqlite(); // phpcs:ignore PHPCompatibility.Extensions.RemovedExtensions.sqliteRemoved } else { make_db_current_silent(); } From adedea2f6ab0bf0c09185dbd3132bc10f49cf28e Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Wed, 1 Mar 2023 08:56:37 +0200 Subject: [PATCH 20/36] strict_types not necessary --- src/wp-includes/sqlite/class-wp-sqlite-lexer.php | 2 -- src/wp-includes/sqlite/class-wp-sqlite-token.php | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/wp-includes/sqlite/class-wp-sqlite-lexer.php b/src/wp-includes/sqlite/class-wp-sqlite-lexer.php index 5adf9caa5516f..0f058dde3cb99 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-lexer.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-lexer.php @@ -6,8 +6,6 @@ * @see https://github.com/phpmyadmin/sql-parser */ -declare(strict_types=1); - /** * Performs lexical analysis over a SQL statement and splits it in multiple tokens. */ diff --git a/src/wp-includes/sqlite/class-wp-sqlite-token.php b/src/wp-includes/sqlite/class-wp-sqlite-token.php index 12b393e29cc25..5d1a6c47add40 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-token.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-token.php @@ -6,8 +6,6 @@ * @see https://github.com/phpmyadmin/sql-parser */ -declare(strict_types=1); - /** * Defines a token along with a set of types and flags and utility functions. * From 383acb6389199dd79832809dec096a8dd3ad065c Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Fri, 3 Mar 2023 12:42:33 +0200 Subject: [PATCH 21/36] backport installation function changes --- src/wp-admin/includes/upgrade.php | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/wp-admin/includes/upgrade.php b/src/wp-admin/includes/upgrade.php index 71c4432eca4cd..051fbbddb1b15 100644 --- a/src/wp-admin/includes/upgrade.php +++ b/src/wp-admin/includes/upgrade.php @@ -3295,30 +3295,24 @@ function sqlite_make_db_sqlite() { $query = null; try { - $pdo->beginTransaction(); + $translator->begin_transaction(); foreach ( $queries as $query ) { $query = trim( $query ); if ( empty( $query ) ) { continue; } - $translation = $translator->translate( $query ); - foreach ( $translation->queries as $query ) { - $stmt = $pdo->prepare( $query->sql ); - $stmt->execute( $query->params ); + $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(); - wp_die( $err_data[2], 'Database Error!' ); - } + $translator->rollback(); + wp_die( $err_data[2], 'Database Error!' ); } $pdo = null; From 562520e438c7195f9e7c4d6a49d55c97fc179de3 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Fri, 3 Mar 2023 12:44:35 +0200 Subject: [PATCH 22/36] Proof of concept for db-type selection during the installation --- src/wp-admin/setup-config.php | 108 +++++++++++++++++++++++++--------- wp-config-sample.php | 2 + 2 files changed, 81 insertions(+), 29 deletions(-) diff --git a/src/wp-admin/setup-config.php b/src/wp-admin/setup-config.php index ddcb4943c7ba7..1926617f0d5ed 100644 --- a/src/wp-admin/setup-config.php +++ b/src/wp-admin/setup-config.php @@ -163,7 +163,7 @@ function setup_config_display_header( $body_classes = array() ) { _e( 'Before getting started' ); ?> -

+

  1. @@ -221,17 +221,31 @@ function setup_config_display_header( $body_classes = array() ) {

    - + + + + + + - + - + - + - + @@ -267,6 +281,23 @@ function setup_config_display_header( $body_classes = array() ) {

    +db_connect(); + if ( 'mysql' === $dbtype ) { + /* + * The wpdb constructor bails when WP_SETUP_CONFIG is set, so we must + * fire this manually. We'll fail here if the values are no good. + */ + $wpdb->db_connect(); - if ( ! empty( $wpdb->error ) ) { - wp_die( $wpdb->error->get_error_message() . $tryagain_link ); - } + if ( ! empty( $wpdb->error ) ) { + wp_die( $wpdb->error->get_error_message() . $tryagain_link ); + } - $errors = $wpdb->suppress_errors(); - $wpdb->query( "SELECT $prefix" ); - $wpdb->suppress_errors( $errors ); + $errors = $wpdb->suppress_errors(); + $wpdb->query( "SELECT $prefix" ); + $wpdb->suppress_errors( $errors ); - if ( ! $wpdb->last_error ) { - // MySQL was able to parse the prefix as a value, which we don't want. Bail. - wp_die( __( 'Error: "Table Prefix" is invalid.' ) ); + if ( ! $wpdb->last_error ) { + // MySQL was able to parse the prefix as a value, which we don't want. Bail. + wp_die( __( 'Error: "Table Prefix" is invalid.' ) ); + } } // Generate keys and salts using secure CSPRNG; fallback to API if enabled; further fallback to original wp_generate_password(). @@ -385,6 +434,7 @@ function setup_config_display_header( $body_classes = array() ) { $padding = $match[2]; switch ( $constant ) { + case 'DATABASE_TYPE': case 'DB_NAME': case 'DB_USER': case 'DB_PASSWORD': diff --git a/wp-config-sample.php b/wp-config-sample.php index 6c4ea5f376b73..64bb6239ac77f 100644 --- a/wp-config-sample.php +++ b/wp-config-sample.php @@ -19,6 +19,8 @@ */ // ** Database settings - You can get this info from your web host ** // +define( 'DATABASE_TYPE', 'mysql' ); + /** The name of the database for WordPress */ define( 'DB_NAME', 'database_name_here' ); From f178291951a97a1b144c7275e2ee28ed4c26a047 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Fri, 3 Mar 2023 14:17:03 +0200 Subject: [PATCH 23/36] Use a select input instead of radios --- src/wp-admin/setup-config.php | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/src/wp-admin/setup-config.php b/src/wp-admin/setup-config.php index 1926617f0d5ed..3d09207e63f5e 100644 --- a/src/wp-admin/setup-config.php +++ b/src/wp-admin/setup-config.php @@ -224,14 +224,10 @@ function setup_config_display_header( $body_classes = array() ) { @@ -282,20 +278,14 @@ function setup_config_display_header( $body_classes = array() ) {

    Date: Fri, 3 Mar 2023 14:19:45 +0200 Subject: [PATCH 24/36] remove console.log, it was just for debugging --- src/wp-admin/setup-config.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wp-admin/setup-config.php b/src/wp-admin/setup-config.php index 3d09207e63f5e..58a7406811b2f 100644 --- a/src/wp-admin/setup-config.php +++ b/src/wp-admin/setup-config.php @@ -279,7 +279,6 @@ function setup_config_display_header( $body_classes = array() ) { '; + endif; + ?> + %s

    ', $error_message ); endif; From da0eb68b5f837e39072288a2f2769d5b11ebd880 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Mon, 13 Mar 2023 10:43:02 +0200 Subject: [PATCH 27/36] Rename DATABASE_TYPE to DB_ENGINE for consistency with other constants --- src/wp-admin/includes/class-wp-debug-data.php | 22 +++++++++---------- .../includes/class-wp-site-health.php | 2 +- src/wp-admin/includes/upgrade.php | 2 +- src/wp-admin/setup-config.php | 6 ++--- src/wp-includes/load.php | 2 +- src/wp-includes/sqlite/db.php | 4 ++-- src/wp-load.php | 2 +- wp-config-sample.php | 2 +- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/wp-admin/includes/class-wp-debug-data.php b/src/wp-admin/includes/class-wp-debug-data.php index b93e811d32cb8..2f2083894ae01 100644 --- a/src/wp-admin/includes/class-wp-debug-data.php +++ b/src/wp-admin/includes/class-wp-debug-data.php @@ -278,10 +278,10 @@ public static function debug_data() { 'label' => 'WP_MAX_MEMORY_LIMIT', 'value' => WP_MAX_MEMORY_LIMIT, ), - 'DATABASE_TYPE' => array( - 'label' => 'DATABASE_TYPE', - 'value' => ( defined( 'DATABASE_TYPE' ) ? DATABASE_TYPE : __( 'Undefined' ) ), - 'debug' => ( defined( 'DATABASE_TYPE' ) ? DATABASE_TYPE : 'undefined' ), + 'DB_ENGINE' => array( + 'label' => 'DB_ENGINE', + 'value' => ( defined( 'DB_ENGINE' ) ? DB_ENGINE : __( 'Undefined' ) ), + 'debug' => ( defined( 'DB_ENGINE' ) ? DB_ENGINE : 'undefined' ), ), 'WP_DEBUG' => array( 'label' => 'WP_DEBUG', @@ -868,7 +868,7 @@ public static function debug_data() { 'value' => wp_date( 'c', $_SERVER['REQUEST_TIME'] ), ); - $database_type = defined( 'DATABASE_TYPE' ) && 'sqlite' === DATABASE_TYPE ? 'sqlite' : 'mysql'; + $db_engine = defined( 'DB_ENGINE' ) && 'sqlite' === DB_ENGINE ? 'sqlite' : 'mysql'; $extension = null; if ( 'mysql' === $database_type ) { // Populate the database debug fields. @@ -882,12 +882,12 @@ public static function debug_data() { $client_version = $wpdb->dbh->client_info; - $info['wp-database']['fields']['database_type'] = array( - 'label' => __( 'Database type' ), - 'value' => 'sqlite' === $database_type ? 'SQLite' : 'MySQL/MariaDB', + $info['wp-database']['fields']['db_engine'] = array( + 'label' => __( 'Database engine' ), + 'value' => 'sqlite' === $db_engine ? 'SQLite' : 'MySQL/MariaDB', ); - if ( 'mysql' === $database_type ) { + if ( 'mysql' === $db_engine ) { $info['wp-database']['fields']['extension'] = array( 'label' => __( 'Extension' ), 'value' => $extension, @@ -920,7 +920,7 @@ public static function debug_data() { 'value' => $wpdb->dbname, 'private' => true, ); - } elseif ( 'sqlite' === $database_type ) { + } elseif ( 'sqlite' === $db_engine ) { $info['wp-database']['fields']['database_version'] = array( 'label' => __( 'SQLite version' ), 'value' => class_exists( 'SQLite3' ) ? SQLite3::version()['versionString'] : null, @@ -944,7 +944,7 @@ public static function debug_data() { 'private' => true, ); - if ( 'mysql' === $database_type ) { + if ( 'mysql' === $db_engine ) { $info['wp-database']['fields']['database_charset'] = array( 'label' => __( 'Database charset' ), 'value' => $wpdb->charset, diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index e9d06eb63527e..0a7081e971e7a 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -209,7 +209,7 @@ private function prepare_sql_data() { global $wpdb; $mysql_server_type = $wpdb->db_server_info(); - $this->is_sqlite = defined( 'DATABASE_TYPE' ) && 'sqlite' === DATABASE_TYPE ? 'sqlite' : 'mysql'; + $this->is_sqlite = defined( 'DB_ENGINE' ) && 'sqlite' === DB_ENGINE ? 'sqlite' : 'mysql'; $this->mysql_server_version = $wpdb->get_var( 'SELECT VERSION()' ); diff --git a/src/wp-admin/includes/upgrade.php b/src/wp-admin/includes/upgrade.php index 051fbbddb1b15..d41d21599b0bf 100644 --- a/src/wp-admin/includes/upgrade.php +++ b/src/wp-admin/includes/upgrade.php @@ -52,7 +52,7 @@ function wp_install( $blog_title, $user_name, $user_email, $is_public, $deprecat wp_check_mysql_version(); wp_cache_flush(); - if ( defined( 'DATABASE_TYPE' ) && 'sqlite' === DATABASE_TYPE ) { + if ( defined( 'DB_ENGINE' ) && 'sqlite' === DB_ENGINE ) { sqlite_make_db_sqlite(); // phpcs:ignore PHPCompatibility.Extensions.RemovedExtensions.sqliteRemoved } else { make_db_current_silent(); diff --git a/src/wp-admin/setup-config.php b/src/wp-admin/setup-config.php index 5e491f588ee50..fa5ac5c8cc936 100644 --- a/src/wp-admin/setup-config.php +++ b/src/wp-admin/setup-config.php @@ -352,7 +352,7 @@ function setup_config_display_header( $body_classes = array() ) { * * @ignore */ - define( 'DATABASE_TYPE', $dbtype ); + define( 'DB_ENGINE', $dbtype ); define( 'DB_NAME', $dbname ); define( 'DB_USER', $uname ); define( 'DB_PASSWORD', $pwd ); @@ -363,7 +363,7 @@ function setup_config_display_header( $body_classes = array() ) { * * @ignore */ - define( 'DATABASE_TYPE', $dbtype ); + define( 'DB_ENGINE', $dbtype ); define( 'DB_NAME', '' ); define( 'DB_USER', '' ); define( 'DB_PASSWORD', '' ); @@ -442,7 +442,7 @@ function setup_config_display_header( $body_classes = array() ) { $padding = $match[2]; switch ( $constant ) { - case 'DATABASE_TYPE': + case 'DB_ENGINE': case 'DB_NAME': case 'DB_USER': case 'DB_PASSWORD': diff --git a/src/wp-includes/load.php b/src/wp-includes/load.php index fe5e4b7357d83..2d2448afcbbad 100644 --- a/src/wp-includes/load.php +++ b/src/wp-includes/load.php @@ -667,7 +667,7 @@ function require_wp_db() { require_once ABSPATH . WPINC . '/class-wpdb.php'; - if ( defined( 'DATABASE_TYPE' ) && 'sqlite' === DATABASE_TYPE ) { + if ( defined( 'DB_ENGINE' ) && 'sqlite' === DB_ENGINE ) { require_once ABSPATH . WPINC . '/sqlite/db.php'; } diff --git a/src/wp-includes/sqlite/db.php b/src/wp-includes/sqlite/db.php index 405535184e8bf..55050cd37cd14 100644 --- a/src/wp-includes/sqlite/db.php +++ b/src/wp-includes/sqlite/db.php @@ -6,8 +6,8 @@ * @since 1.0.0 */ -// 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; } diff --git a/src/wp-load.php b/src/wp-load.php index a89dac35a581b..ea5f82b9f7705 100644 --- a/src/wp-load.php +++ b/src/wp-load.php @@ -22,7 +22,7 @@ } // @TODO: Remove this. It's only here so that the PR can be tested. -define( 'DATABASE_TYPE', 'sqlite' ); +define( 'DB_ENGINE', 'sqlite' ); /* * The error_reporting() function can be disabled in php.ini. On systems where that is the case, diff --git a/wp-config-sample.php b/wp-config-sample.php index 64bb6239ac77f..6587dc3a80296 100644 --- a/wp-config-sample.php +++ b/wp-config-sample.php @@ -19,7 +19,7 @@ */ // ** Database settings - You can get this info from your web host ** // -define( 'DATABASE_TYPE', 'mysql' ); +define( 'DB_ENGINE', 'mysql' ); /** The name of the database for WordPress */ define( 'DB_NAME', 'database_name_here' ); From 3f2729f82908eca2fc807c4de7466a8174f48eff Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Tue, 28 Mar 2023 10:43:21 +0300 Subject: [PATCH 28/36] Backport changes Backports changes from https://github.com/WordPress/sqlite-database-integration/pull/25 --- ...s-wp-sqlite-pdo-user-defined-functions.php | 16 +- .../sqlite/class-wp-sqlite-translator.php | 649 ++++++++++++++++-- 2 files changed, 588 insertions(+), 77 deletions(-) diff --git a/src/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php b/src/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php index 77728e074a42b..2b39033ac942b 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php @@ -59,7 +59,6 @@ public function __construct( $pdo ) { 'dayofmonth' => 'dayofmonth', 'unix_timestamp' => 'unix_timestamp', 'now' => 'now', - 'char_length' => 'char_length', 'md5' => 'md5', 'curdate' => 'curdate', 'rand' => 'rand', @@ -135,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. * @@ -574,7 +562,7 @@ public function log() { public function least() { $arg_list = func_get_args(); - return "min($arg_list)"; + return min( $arg_list ); } /** @@ -587,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/src/wp-includes/sqlite/class-wp-sqlite-translator.php b/src/wp-includes/sqlite/class-wp-sqlite-translator.php index a1cd14f374b33..ab01d42bd7c03 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -23,6 +23,11 @@ class WP_SQLite_Translator { PRIMARY KEY(`table`, `column_or_index`) );'; + /** + * We use the ASCII SUB character to escape LIKE literal _ and % + */ + const LIKE_ESCAPE_CHAR = "\x1a"; + /** * Class variable to reference to the PDO instance. * @@ -272,6 +277,50 @@ class WP_SQLite_Translator { */ 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. * @@ -292,8 +341,14 @@ public function __construct( $pdo = null ) { $err_message = ''; do { try { + $options = array( + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_STRINGIFY_FETCHES => true, + PDO::ATTR_TIMEOUT => 5, + ); + $dsn = 'sqlite:' . FQDB; - $pdo = new PDO( $dsn, null, null, array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ) ); // phpcs:ignore WordPress.DB.RestrictedClasses + $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 ) { @@ -320,9 +375,16 @@ public function __construct( $pdo = null ) { new WP_SQLite_PDO_User_Defined_Functions( $pdo ); // MySQL data comes across stringified by default. - $pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); + $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. @@ -330,6 +392,7 @@ public function __construct( $pdo = null ) { 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' ); @@ -476,6 +539,35 @@ private function prepare_directory() { */ 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 ( @@ -695,6 +787,16 @@ private function execute_mysql_query( $query ) { $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 ); } @@ -1078,11 +1180,13 @@ private function is_create_table_field_terminator( $token, $definition_depth, $c /** * Executes a DELETE statement. + * + * @throws Exception If the table could not be found. */ private function execute_delete() { - $this->rewriter->consume(); // DELETE + $this->rewriter->consume(); // DELETE. - // Process expressions and extract bound parameters + // Process expressions and extract bound parameters. $params = array(); while ( true ) { $token = $this->rewriter->peek(); @@ -1105,7 +1209,7 @@ private function execute_delete() { $updated_query = $this->rewriter->get_updated_query(); - // Perform DELETE-specific translations + // Perform DELETE-specific translations. // Naive rewriting of DELETE JOIN query. // @TODO: Actually rewrite the query instead of using a hardcoded workaround. @@ -1221,9 +1325,9 @@ private function execute_delete() { } $query = ( - count( $ids_to_delete ) - ? "DELETE FROM {$table_name} WHERE {$pk_name} IN (" . implode( ',', $ids_to_delete ) . ')' - : "DELETE FROM {$table_name} WHERE 0=1" + 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( @@ -1235,7 +1339,7 @@ private function execute_delete() { * Executes a SELECT statement. */ private function execute_select() { - $this->rewriter->consume(); // SELECT + $this->rewriter->consume(); // SELECT. $params = array(); $table_name = null; @@ -1273,22 +1377,9 @@ private function execute_select() { $updated_query = $this->rewriter->get_updated_query(); if ( $table_name && str_starts_with( strtolower( $table_name ), 'information_schema' ) ) { - // @TODO: Actually rewrite the columns. - if ( str_contains( $updated_query, 'bytes' ) ) { - // Count rows per table. - $tables = $this->execute_sqlite_query( "SELECT name as `table` FROM sqlite_master WHERE type='table' ORDER BY name" )->fetchAll(); - $rows = '(CASE '; - foreach ( $tables as $table ) { - $table_name = $table['table']; - $count = $this->execute_sqlite_query( "SELECT COUNT(*) as `count` FROM $table_name" )->fetch(); - $rows .= " WHEN name = '$table_name' THEN {$count['count']} "; - } - $rows .= 'ELSE 0 END) '; - $updated_query = "SELECT name as `table`, $rows as `rows`, 0 as `bytes` FROM sqlite_master WHERE type='table' ORDER BY name"; - } else { - $updated_query = "SELECT name, 'myisam' as `engine`, 0 as `data`, 0 as `index` FROM sqlite_master WHERE type='table' ORDER BY name"; - } - $params = array(); + $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 @@ -1318,9 +1409,17 @@ private function execute_select() { } $stmt = $this->execute_sqlite_query( $updated_query, $params ); - $this->set_results_from_fetched_data( - $stmt->fetchAll( $this->pdo_fetch_mode ) - ); + 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 ) + ); + } } /** @@ -1340,6 +1439,8 @@ private function execute_truncate() { /** * Executes a DESCRIBE statement. + * + * @throws PDOException When the table is not found. */ private function execute_describe() { $this->rewriter->skip(); @@ -1392,7 +1493,7 @@ private function execute_describe() { * Executes an UPDATE statement. */ private function execute_update() { - $this->rewriter->consume(); // UPDATE + $this->rewriter->consume(); // Update. $params = array(); while ( true ) { @@ -1570,13 +1671,48 @@ private function preprocess_string_literal( $value ) { * 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 ) ) { - if ( false === strtotime( $value ) ) { + /* + * 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. * @@ -1620,11 +1756,15 @@ private function translate_expression( $token ) { $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 ) ); } @@ -1736,7 +1876,9 @@ private function extract_bound_parameter( $token, &$params ) { } $param_name = ':param' . count( $params ); - $params[ $param_name ] = $this->preprocess_string_literal( $token->value ); + $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 ) ); @@ -1760,6 +1902,7 @@ private function translate_concat_function( $token ) { ) { return false; } + /* * Skip the CONCAT function but leave the parentheses. * There is another code block below that replaces the @@ -1823,11 +1966,45 @@ private function translate_date_add_sub( $token ) { 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. - * @param bool $is_in_duplicate_section Whether the VALUES() function is in a duplicate section. * * @return bool */ @@ -2076,6 +2253,185 @@ private function translate_regexp_functions( $token ) { 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. * @@ -2190,7 +2546,7 @@ private function get_primary_keys( $table_name ) { /** * Get the keys for a table. * - * @param string $table_name Table name. + * @param string $table_name Table name. * @param bool $only_unique Only return unique keys. * * @return array @@ -2232,7 +2588,6 @@ private function get_sqlite_create_table( $table_name ) { * 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(); @@ -2608,9 +2963,38 @@ private function execute_show() { $stmt = $this->execute_sqlite_query( "PRAGMA table_info(\"$table_name\");" ); - $this->set_results_from_fetched_data( - $stmt->fetchAll( $this->pdo_fetch_mode ) + /* @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': @@ -2682,6 +3066,28 @@ private function execute_show() { $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': @@ -2837,11 +3243,11 @@ private function normalize_mysql_index_type( $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 + 'KEY' === $index_type + || 'PRIMARY' === $index_type + || 'UNIQUE' === $index_type + || 'FULLTEXT' === $index_type + || 'SPATIAL' === $index_type ) { return $index_type; } @@ -2868,6 +3274,98 @@ private function mysql_index_type_to_sqlite_type( $normalized_mysql_index_type ) 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. * @@ -2876,9 +3374,8 @@ private function mysql_index_type_to_sqlite_type( $normalized_mysql_index_type ) * @return bool Always false. */ private function handle_error( Exception $err ) { - $message = $err->getMessage(); - $err_message = sprintf( 'Problem preparing the PDO SQL Statement. Error was: %s. trace: %s', $message, $err->getTraceAsString() ); - $this->set_error( __LINE__, __FUNCTION__, $err_message ); + $message = $err->getMessage(); + $this->set_error( __LINE__, __FUNCTION__, $message ); $this->return_value = false; return false; } @@ -2936,7 +3433,7 @@ public function get_error_message() { } $output = '
     
    ' . PHP_EOL; - $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; @@ -2949,7 +3446,7 @@ public function get_error_message() { $output .= '' . PHP_EOL; $output .= '
    ' . PHP_EOL; foreach ( $this->error_messages as $num => $m ) { - $output .= '
    ' . PHP_EOL; + $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'], @@ -2963,14 +3460,14 @@ public function get_error_message() { throw new Exception(); } catch ( Exception $e ) { $output .= '

    Backtrace:

    ' . PHP_EOL; - $output .= '
    ' . htmlspecialchars( $e->getTraceAsString() ) . '
    ' . PHP_EOL; + $output .= '
    ' . $e->getTraceAsString() . '
    ' . PHP_EOL; } return $output; } /** - * Executes a query in SQLite – for internal use only. + * Executes a query in SQLite. * * @param mixed $sql The query to execute. * @param mixed $params The parameters to bind to the query. @@ -2982,17 +3479,35 @@ public function get_error_message() { * @type * $result The value returned by $stmt. * } */ - private function execute_sqlite_query( $sql, $params = array() ) { + public function execute_sqlite_query( $sql, $params = array() ) { $this->executed_sqlite_queries[] = array( 'sql' => $sql, 'params' => $params, ); - $stmt = $this->pdo->prepare( $sql ); - $this->last_exec_returned = $stmt->execute( $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; @@ -3004,6 +3519,11 @@ private function set_results_from_fetched_data( $data ) { $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 @@ -3025,18 +3545,21 @@ private function set_result_from_affected_rows( $override = null ) { * 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->last_exec_returned = null; + $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; } /** From b8bdaddd22ecc5ed4598c8a886336398732035d6 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Thu, 20 Jul 2023 11:09:20 +0300 Subject: [PATCH 29/36] minor tweaks --- src/wp-includes/sqlite/class-wp-sqlite-db.php | 3 --- .../sqlite/class-wp-sqlite-translator.php | 13 ++++--------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/sqlite/class-wp-sqlite-db.php b/src/wp-includes/sqlite/class-wp-sqlite-db.php index 1789cb90e0707..b3db03574924b 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-db.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-db.php @@ -16,8 +16,6 @@ class WP_SQLite_DB extends wpdb { /** * Database Handle * - * @access protected - * * @var WP_SQLite_Translator */ protected $dbh; @@ -315,7 +313,6 @@ public function query( $query ) { * This overrides wpdb::load_col_info(), which uses a mysql function. * * @see wpdb::load_col_info() - * @access protected */ protected function load_col_info() { if ( $this->col_info ) { diff --git a/src/wp-includes/sqlite/class-wp-sqlite-translator.php b/src/wp-includes/sqlite/class-wp-sqlite-translator.php index ab01d42bd7c03..4729f070a245c 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -31,8 +31,6 @@ class WP_SQLite_Translator { /** * Class variable to reference to the PDO instance. * - * @access private - * * @var PDO object */ private $pdo; @@ -180,8 +178,6 @@ class WP_SQLite_Translator { /** * Class variable to store the result of the query. * - * @access private - * * @var array reference to the PHP object */ private $results = null; @@ -196,8 +192,6 @@ class WP_SQLite_Translator { /** * Class variable to store the file name and function to cause error. * - * @access private - * * @var array */ private $errors; @@ -205,8 +199,6 @@ class WP_SQLite_Translator { /** * Class variable to store the error messages. * - * @access private - * * @var array */ private $error_messages = array(); @@ -215,7 +207,6 @@ class WP_SQLite_Translator { * Class variable to store the affected row id. * * @var int integer - * @access private */ private $last_insert_id; @@ -2424,6 +2415,10 @@ private function strip_sqlite_system_tables( $tables ) { array_filter( $tables, function ( $table ) { + // Bail early if $table is not an object. + if ( ! is_object( $table ) ) { + return true; + } $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 ); }, From 1828212cdc5e10930e6cd67c8f48894061736352 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Wed, 30 Aug 2023 09:29:56 +0300 Subject: [PATCH 30/36] CS fix after rebase --- src/wp-admin/includes/class-wp-debug-data.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wp-admin/includes/class-wp-debug-data.php b/src/wp-admin/includes/class-wp-debug-data.php index 2f2083894ae01..551a29bcec59c 100644 --- a/src/wp-admin/includes/class-wp-debug-data.php +++ b/src/wp-admin/includes/class-wp-debug-data.php @@ -871,10 +871,10 @@ public static function debug_data() { $db_engine = defined( 'DB_ENGINE' ) && 'sqlite' === DB_ENGINE ? 'sqlite' : 'mysql'; $extension = null; if ( 'mysql' === $database_type ) { - // Populate the database debug fields. - if ( is_object( $wpdb->dbh ) ) { - // mysqli or PDO. - $extension = get_class( $wpdb->dbh ); + // Populate the database debug fields. + if ( is_object( $wpdb->dbh ) ) { + // mysqli or PDO. + $extension = get_class( $wpdb->dbh ); } } From 18790ca7cb3ffe31d7a9b1ff6b883278e893e06c Mon Sep 17 00:00:00 2001 From: Aristeides Stathopoulos Date: Thu, 28 Dec 2023 13:15:31 +0200 Subject: [PATCH 31/36] Update SQLite implementation --- src/wp-includes/sqlite/class-wp-sqlite-db.php | 6 +- ...s-wp-sqlite-pdo-user-defined-functions.php | 14 +- .../sqlite/class-wp-sqlite-translator.php | 197 +++++++++++++----- 3 files changed, 154 insertions(+), 63 deletions(-) diff --git a/src/wp-includes/sqlite/class-wp-sqlite-db.php b/src/wp-includes/sqlite/class-wp-sqlite-db.php index b3db03574924b..ed80e19b51d46 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-db.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-db.php @@ -103,7 +103,7 @@ public function select( $db, $dbh = null ) { * * @return string escaped */ - function _real_escape( $str ) { + public function _real_escape( $str ) { return addslashes( $str ); } @@ -278,7 +278,7 @@ public function query( $query ) { } $this->result = $this->dbh->query( $query ); - $this->num_queries++; + ++$this->num_queries; if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { $this->queries[] = array( $query, $this->timer_stop(), $this->get_caller() ); @@ -349,7 +349,7 @@ public function has_cap( $db_cap ) { * @see wpdb::db_version() */ public function db_version() { - return '5.5'; + return '8.0'; } /** diff --git a/src/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php b/src/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php index 2b39033ac942b..7d4ac5ee78216 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php @@ -221,7 +221,7 @@ 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 + * 1 through 12 */ return intval( gmdate( 'n', strtotime( $field ) ) ); } @@ -446,14 +446,14 @@ public function isnull( $field ) { * * As 'IF' is a reserved word for PHP, function name must be changed. * - * @param unknonw $expression the statement to be evaluated as true or false. - * @param unknown $true statement or value returned if $expression is true. - * @param unknown $false statement or value returned if $expression is false. + * @param mixed $expression The statement to be evaluated as true or false. + * @param mixed $truthy Statement or value returned if $expression is true. + * @param mixed $falsy Statement or value returned if $expression is false. * - * @return unknown + * @return mixed */ - public function _if( $expression, $true, $false ) { - return ( true === $expression ) ? $true : $false; + public function _if( $expression, $truthy, $falsy ) { + return ( true === $expression ) ? $truthy : $falsy; } /** diff --git a/src/wp-includes/sqlite/class-wp-sqlite-translator.php b/src/wp-includes/sqlite/class-wp-sqlite-translator.php index 4729f070a245c..9c784244bb8a8 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -31,6 +31,8 @@ class WP_SQLite_Translator { /** * Class variable to reference to the PDO instance. * + * @access private + * * @var PDO object */ private $pdo; @@ -168,6 +170,20 @@ class WP_SQLite_Translator { */ public $executed_sqlite_queries = array(); + /** + * The affected table name. + * + * @var array + */ + private $table_name = array(); + + /** + * The type of the executed query (SELECT, INSERT, etc). + * + * @var array + */ + private $query_type = array(); + /** * The columns to insert. * @@ -178,6 +194,8 @@ class WP_SQLite_Translator { /** * Class variable to store the result of the query. * + * @access private + * * @var array reference to the PHP object */ private $results = null; @@ -192,6 +210,8 @@ class WP_SQLite_Translator { /** * Class variable to store the file name and function to cause error. * + * @access private + * * @var array */ private $errors; @@ -199,6 +219,8 @@ class WP_SQLite_Translator { /** * Class variable to store the error messages. * + * @access private + * * @var array */ private $error_messages = array(); @@ -207,6 +229,7 @@ class WP_SQLite_Translator { * Class variable to store the affected row id. * * @var int integer + * @access private */ private $last_insert_id; @@ -243,7 +266,7 @@ class WP_SQLite_Translator { /** * Variable to keep track of nested transactions level. * - * @var number + * @var int */ private $transaction_level = 0; @@ -312,6 +335,13 @@ class WP_SQLite_Translator { */ private $sqlite_system_tables = array(); + /** + * The last error message from SQLite. + * + * @var string + */ + private $last_sqlite_error; + /** * Constructor. * @@ -385,7 +415,8 @@ public function __construct( $pdo = null ) { // 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 + // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual + if ( $statement->fetchColumn( 0 ) == '0' ) { $this->pdo->query( 'PRAGMA foreign_keys = ON' ); } $this->pdo->query( 'PRAGMA encoding="UTF-8";' ); @@ -399,7 +430,7 @@ public function __construct( $pdo = null ) { * * This definition is changed since version 1.7. */ - function __destruct() { + public function __destruct() { if ( defined( 'SQLITE_MEM_DEBUG' ) && SQLITE_MEM_DEBUG ) { $max = ini_get( 'memory_limit' ); if ( is_null( $max ) ) { @@ -591,8 +622,31 @@ public function query( $statement, $mode = PDO::FETCH_OBJ, ...$fetch_mode_args ) } } while ( $error ); + /** + * Notifies that a query has been translated and executed. + * + * @param string $query The executed SQL query. + * @param string $query_type The type of the SQL query (e.g. SELECT, INSERT, UPDATE, DELETE). + * @param string $table_name The name of the table affected by the SQL query. + * @param array $insert_columns The columns affected by the INSERT query (if applicable). + * @param int $last_insert_id The ID of the last inserted row (if applicable). + * @param int $affected_rows The number of affected rows (if applicable). + * + * @since 0.1.0 + */ + do_action( + 'sqlite_translated_query_executed', + $this->mysql_query, + $this->query_type, + $this->table_name, + $this->insert_columns, + $this->last_insert_id, + $this->affected_rows + ); + // Commit the nested transaction. $this->commit(); + return $this->return_value; } catch ( Exception $err ) { // Rollback the nested transaction. @@ -710,11 +764,11 @@ public function get_return_value() { * @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; + $tokens = ( new WP_SQLite_Lexer( $query ) )->tokens; + $this->rewriter = new WP_SQLite_Query_Rewriter( $tokens ); + $this->query_type = $this->rewriter->peek()->value; - switch ( $query_type ) { + switch ( $this->query_type ) { case 'ALTER': $this->execute_alter(); break; @@ -785,11 +839,11 @@ private function execute_mysql_query( $query ) { case 'OPTIMIZE': case 'REPAIR': case 'ANALYZE': - $this->execute_optimize( $query_type ); + $this->execute_optimize( $this->query_type ); break; default: - throw new Exception( 'Unknown query type: ' . $query_type ); + throw new Exception( 'Unknown query type: ' . $this->query_type ); } } @@ -1241,7 +1295,7 @@ private function execute_delete() { // SELECT to fetch the IDs of the rows to delete, then delete them // using a separate DELETE query. - $table_name = $rewriter->skip()->value; + $this->table_name = $rewriter->skip()->value; $rewriter->add( new WP_SQLite_Token( 'SELECT', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) ); /* @@ -1257,11 +1311,11 @@ private function execute_delete() { 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; + $this->table_name = $rewriter->input_tokens[ $i ]->value; break; } } - if ( ! $table_name ) { + if ( ! $this->table_name ) { throw new Exception( 'Could not find table name for dual delete query.' ); } @@ -1269,7 +1323,7 @@ private function execute_delete() { * 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;' ); + $q = $this->execute_sqlite_query( 'SELECT l.name FROM pragma_table_info("' . $this->table_name . '") as l WHERE l.pk = 1;' ); $pk_name = $q->fetch()['name']; /* @@ -1317,8 +1371,8 @@ private function execute_delete() { $query = ( count( $ids_to_delete ) - ? "DELETE FROM {$table_name} WHERE {$pk_name} IN (" . implode( ',', $ids_to_delete ) . ')' - : "DELETE FROM {$table_name} WHERE 0=1" + ? "DELETE FROM {$this->table_name} WHERE {$pk_name} IN (" . implode( ',', $ids_to_delete ) . ')' + : "DELETE FROM {$this->table_name} WHERE 0=1" ); $this->execute_sqlite_query( $query ); $this->set_result_from_affected_rows( @@ -1346,7 +1400,8 @@ private function execute_select() { $this->remember_last_reserved_keyword( $token ); if ( ! $table_name ) { - $table_name = $this->peek_table_name( $token ); + $this->table_name = $this->peek_table_name( $token ); + $table_name = $this->peek_table_name( $token ); } if ( $this->skip_sql_calc_found_rows( $token ) ) { @@ -1418,7 +1473,9 @@ private function execute_select() { */ private function execute_truncate() { $this->rewriter->skip(); // TRUNCATE. - $this->rewriter->skip(); // TABLE. + if ( 'TABLE' === strtoupper( $this->rewriter->peek()->value ) ) { + $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 ) ); @@ -1435,8 +1492,8 @@ private function execute_truncate() { */ private function execute_describe() { $this->rewriter->skip(); - $table_name = $this->rewriter->consume()->value; - $stmt = $this->execute_sqlite_query( + $this->table_name = $this->rewriter->consume()->value; + $stmt = $this->execute_sqlite_query( "SELECT `name` as `Field`, ( @@ -1465,9 +1522,9 @@ private function execute_describe() { ELSE 'PRI' END ) as `Key` - FROM pragma_table_info(\"$table_name\") p + FROM pragma_table_info(\"$this->table_name\") p LEFT JOIN " . self::DATA_TYPES_CACHE_TABLE . " d - ON d.`table` = \"$table_name\" + ON d.`table` = \"$this->table_name\" AND d.`column_or_index` = p.`name` ; " @@ -1493,6 +1550,17 @@ private function execute_update() { break; } + // Record the table name. + if ( + ! $this->table_name && + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED + ) + ) { + $this->table_name = $token->value; + } + $this->remember_last_reserved_keyword( $token ); if ( @@ -1517,7 +1585,6 @@ private function execute_update() { private function execute_insert_or_replace() { $params = array(); $is_in_duplicate_section = false; - $table_name = null; $this->rewriter->consume(); // INSERT or REPLACE. @@ -1531,7 +1598,7 @@ private function execute_insert_or_replace() { // Consume and record the table name. $this->insert_columns = array(); $this->rewriter->consume(); // INTO. - $table_name = $this->rewriter->consume()->value; // Table name. + $this->table_name = $this->rewriter->consume()->value; // Table name. /* * A list of columns is given if the opening parenthesis @@ -1590,7 +1657,7 @@ private function execute_insert_or_replace() { ) ) { $is_in_duplicate_section = true; - $this->translate_on_duplicate_key( $table_name ); + $this->translate_on_duplicate_key( $this->table_name ); continue; } @@ -1606,6 +1673,7 @@ private function execute_insert_or_replace() { if ( is_numeric( $this->last_insert_id ) ) { $this->last_insert_id = (int) $this->last_insert_id; } + $this->last_insert_id = apply_filters( 'sqlite_last_insert_id', $this->last_insert_id, $this->table_name ); } /** @@ -1694,7 +1762,7 @@ private function preprocess_like_expr( &$token ) { /* 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 ++; + ++$this->like_escape_count; return str_replace( array( '\_', '\%' ), array( self::LIKE_ESCAPE_CHAR . '_', self::LIKE_ESCAPE_CHAR . '%' ), @@ -2320,14 +2388,14 @@ private function translate_like_escape( $token ) { } else { /* open parenthesis during LIKE parameter, count it. */ if ( $token->matches( WP_SQLite_Token::TYPE_OPERATOR, null, array( '(' ) ) ) { - $this->like_expression_nesting ++; + ++$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 --; + --$this->like_expression_nesting; return false; } @@ -2415,12 +2483,20 @@ private function strip_sqlite_system_tables( $tables ) { array_filter( $tables, function ( $table ) { - // Bail early if $table is not an object. - if ( ! is_object( $table ) ) { - return true; + $table_name = false; + if ( is_array( $table ) ) { + if ( isset( $table['Name'] ) ) { + $table_name = $table['Name']; + } elseif ( isset( $table['table_name'] ) ) { + $table_name = $table['table_name']; + } + } elseif ( is_object( $table ) ) { + $table_name = property_exists( $table, 'Name' ) + ? $table->Name // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + : $table->table_name; } - $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 ); + + return $table_name && ! array_key_exists( $table_name, $this->sqlite_system_tables ); }, ARRAY_FILTER_USE_BOTH ) @@ -2591,7 +2667,7 @@ private function execute_alter() { throw new Exception( 'Unknown subject: ' . $subject ); } - $table_name = $this->normalize_column_name( $this->rewriter->consume()->token ); + $this->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. @@ -2603,13 +2679,13 @@ private function execute_alter() { 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 ), + new WP_SQLite_Token( $this->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; + $is_index_op = (bool) $mysql_index_type; if ( 'ADD' === $op_type && 'COLUMN' === $op_subject ) { $column_name = $this->rewriter->consume()->value; @@ -2626,7 +2702,7 @@ private function execute_alter() { ) ); $this->update_data_type_cache( - $table_name, + $this->table_name, $column_name, $mysql_data_type ); @@ -2638,7 +2714,7 @@ private function execute_alter() { $new_field = $this->parse_mysql_create_table_field(); $alter_terminator = end( $this->rewriter->output_tokens ); $this->update_data_type_cache( - $table_name, + $this->table_name, $new_field->name, $new_field->mysql_data_type ); @@ -2659,8 +2735,8 @@ private function execute_alter() { */ // 1. Get the existing table schema. - $old_schema = $this->get_sqlite_create_table( $table_name ); - $old_indexes = $this->get_keys( $table_name, false ); + $old_schema = $this->get_sqlite_create_table( $this->table_name ); + $old_indexes = $this->get_keys( $this->table_name, false ); // 2. Adjust the column definition. @@ -2718,19 +2794,19 @@ private function execute_alter() { } // 3. Copy the data out of the old table - $cache_table_name = "_tmp__{$table_name}_" . rand( 10000000, 99999999 ); + $cache_table_name = "_tmp__{$this->table_name}_" . rand( 10000000, 99999999 ); $this->execute_sqlite_query( - "CREATE TABLE `$cache_table_name` as SELECT * FROM `$table_name`" + "CREATE TABLE `$cache_table_name` as SELECT * FROM `$this->table_name`" ); // 4. Drop the old table to free up the indexes names - $this->execute_sqlite_query( "DROP TABLE `$table_name`" ); + $this->execute_sqlite_query( "DROP TABLE `$this->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" ); + $this->execute_sqlite_query( "INSERT INTO {$this->table_name} SELECT * FROM $cache_table_name" ); // 7. Drop the old table copy $this->execute_sqlite_query( "DROP TABLE `$cache_table_name`" ); @@ -2759,7 +2835,7 @@ private function execute_alter() { * 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 ) . ')' + "CREATE $unique INDEX IF NOT EXISTS `{$row['index']['name']}` ON $this->table_name (" . implode( ', ', $columns ) . ')' ); } @@ -2776,7 +2852,7 @@ private function execute_alter() { } 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"; + $sqlite_index_name = "{$this->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 ), @@ -2787,13 +2863,13 @@ private function execute_alter() { 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( '"' . $this->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, + $this->table_name, $sqlite_index_name, $mysql_index_type ); @@ -2841,7 +2917,7 @@ private function execute_alter() { 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 ), + new WP_SQLite_Token( "\"{$this->table_name}__$key_name\"", WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ), ) ); } else { @@ -3381,16 +3457,16 @@ private function handle_error( Exception $err ) { * 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. + * @param string $line Where the error occurred. + * @param string $function_name Indicate the function name where the error occurred. + * @param string $message The message. * * @return boolean|void */ - private function set_error( $line, $function, $message ) { + private function set_error( $line, $function_name, $message ) { $this->errors[] = array( 'line' => $line, - 'function' => $function, + 'function' => $function_name, ); $this->error_messages[] = $message; $this->is_error = true; @@ -3543,8 +3619,10 @@ private function flush() { $this->mysql_query = ''; $this->results = null; $this->last_exec_returned = null; + $this->table_name = null; $this->last_insert_id = null; $this->affected_rows = null; + $this->insert_columns = array(); $this->column_data = array(); $this->num_rows = null; $this->return_value = null; @@ -3574,6 +3652,16 @@ public function begin_transaction() { } finally { if ( $success ) { ++$this->transaction_level; + /** + * Notifies that a transaction-related query has been translated and executed. + * + * @param string $command The SQL statement (one of "START TRANSACTION", "COMMIT", "ROLLBACK"). + * @param bool $success Whether the SQL statement was successful or not. + * @param int $nesting_level The nesting level of the transaction. + * + * @since 0.1.0 + */ + do_action( 'sqlite_transaction_query_executed', 'START TRANSACTION', (bool) $this->last_exec_returned, $this->transaction_level - 1 ); } } return $success; @@ -3595,6 +3683,8 @@ public function commit() { } else { $this->execute_sqlite_query( 'RELEASE SAVEPOINT LEVEL' . $this->transaction_level ); } + + do_action( 'sqlite_transaction_query_executed', 'COMMIT', (bool) $this->last_exec_returned, $this->transaction_level ); return $this->last_exec_returned; } @@ -3614,6 +3704,7 @@ public function rollback() { } else { $this->execute_sqlite_query( 'ROLLBACK TO SAVEPOINT LEVEL' . $this->transaction_level ); } + do_action( 'sqlite_transaction_query_executed', 'ROLLBACK', (bool) $this->last_exec_returned, $this->transaction_level ); return $this->last_exec_returned; } } From f21e20ce3c618f4f1ff8a74b7f9544bea45fd509 Mon Sep 17 00:00:00 2001 From: Aristeides Stathopoulos Date: Thu, 28 Dec 2023 13:15:43 +0200 Subject: [PATCH 32/36] Add PHPCS exclusion --- phpcs.xml.dist | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 3defbc290a6b2..1568411dd808d 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -224,7 +224,7 @@ ############################################################################# SELECTIVE EXCLUSIONS Exclude specific files for specific sniffs and/or exclude sub-groups in sniffs. - + These exclusions are listed ordered by alphabetic sniff name. ############################################################################# --> @@ -281,6 +281,10 @@ /tests/phpunit/tests/db/charset\.php + + /src/wp-includes/sqlite/*\.php + + From 5efd2e29dc8f424a0d69f4b92026a2d736c29a5c Mon Sep 17 00:00:00 2001 From: Aristeides Stathopoulos Date: Tue, 9 Jan 2024 12:35:24 +0200 Subject: [PATCH 33/36] fix typo --- src/wp-admin/includes/class-wp-debug-data.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/class-wp-debug-data.php b/src/wp-admin/includes/class-wp-debug-data.php index 551a29bcec59c..c11ee29b86afc 100644 --- a/src/wp-admin/includes/class-wp-debug-data.php +++ b/src/wp-admin/includes/class-wp-debug-data.php @@ -870,7 +870,7 @@ public static function debug_data() { $db_engine = defined( 'DB_ENGINE' ) && 'sqlite' === DB_ENGINE ? 'sqlite' : 'mysql'; $extension = null; - if ( 'mysql' === $database_type ) { + if ( 'mysql' === $db_engine ) { // Populate the database debug fields. if ( is_object( $wpdb->dbh ) ) { // mysqli or PDO. From d367e65bc118c4b62aa42d24506523b23b8d4074 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Thu, 18 Apr 2024 10:08:42 +0300 Subject: [PATCH 34/36] Sync SQLite classes with the plugin --- .../sqlite/class-wp-sqlite-lexer.php | 27 ++-- ...s-wp-sqlite-pdo-user-defined-functions.php | 16 +++ .../sqlite/class-wp-sqlite-token.php | 13 +- .../sqlite/class-wp-sqlite-translator.php | 122 +++++++++++++++++- 4 files changed, 149 insertions(+), 29 deletions(-) diff --git a/src/wp-includes/sqlite/class-wp-sqlite-lexer.php b/src/wp-includes/sqlite/class-wp-sqlite-lexer.php index 0f058dde3cb99..8619cb333b05e 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-lexer.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-lexer.php @@ -1,6 +1,6 @@ $method(); if ( $token ) { @@ -1668,10 +1668,10 @@ private function solve_ambiguity_on_function_keywords() { $next = $this->tokens_get_next(); if ( ( WP_SQLite_Token::TYPE_KEYWORD !== $next->type - || ! in_array( $next->value, $this->keyword_name_indicators, true ) + || ! in_array( $next->value, self::KEYWORD_NAME_INDICATORS, true ) ) && ( WP_SQLite_Token::TYPE_OPERATOR !== $next->type - || ! in_array( $next->value, $this->operator_name_indicators, true ) + || ! in_array( $next->value, self::OPERATOR_NAME_INDICATORS, true ) ) && ( null !== $next->value ) ) { @@ -2068,7 +2068,7 @@ public function parse_number() { } elseif ( $this->last + 1 < $this->string_length && '0' === $this->str[ $this->last ] - && ( 'x' === $this->str[ $this->last + 1 ] || 'X' === $this->str[ $this->last + 1 ] ) + && 'x' === $this->str[ $this->last + 1 ] ) { $token .= $this->str[ $this->last++ ]; $state = 2; @@ -2260,7 +2260,7 @@ public function parse_symbol() { if ( null === $str ) { $str = $this->parse_unknown(); - if ( null === $str ) { + if ( null === $str && ! ( $flags & WP_SQLite_Token::FLAG_SYMBOL_PARAMETER ) ) { $this->error( 'Variable name was expected.', $this->str[ $this->last ], $this->last ); } } @@ -2514,15 +2514,10 @@ public static function is_separator( $str ) { * 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; - } - + public function tokens( array $tokens = array() ) { $this->tokens = $tokens; - $this->tokens_count = -1 === $count ? count( $tokens ) : $count; + $this->tokens_count = count( $tokens ); } /** diff --git a/src/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php b/src/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php index 7d4ac5ee78216..6f0d83dfc1546 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php @@ -76,6 +76,7 @@ public function __construct( $pdo ) { 'release_lock' => 'release_lock', 'ucase' => 'ucase', 'lcase' => 'lcase', + 'unhex' => 'unhex', 'inet_ntoa' => 'inet_ntoa', 'inet_aton' => 'inet_aton', 'datediff' => 'datediff', @@ -633,6 +634,21 @@ public function lcase( $content ) { return "lower($content)"; } + /** + * Method to emulate MySQL UNHEX() function. + * + * For a string argument str, UNHEX(str) interprets each pair of characters + * in the argument as a hexadecimal number and converts it to the byte represented + * by the number. The return value is a binary string. + * + * @param string $number Number to be unhexed. + * + * @return string Binary string + */ + public function unhex( $number ) { + return pack( 'H*', $number ); + } + /** * Method to emulate MySQL INET_NTOA() function. * diff --git a/src/wp-includes/sqlite/class-wp-sqlite-token.php b/src/wp-includes/sqlite/class-wp-sqlite-token.php index 5d1a6c47add40..9976cf341c8ef 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-token.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-token.php @@ -161,7 +161,7 @@ class WP_SQLite_Token { * * @var mixed|string|null */ - public $keyword; + public $keyword = null; /** * The type of this token. @@ -195,11 +195,10 @@ class WP_SQLite_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(); + $this->token = $token; + $this->type = $type; + $this->flags = $flags; + $this->value = $this->extract(); } /** @@ -262,8 +261,8 @@ private function extract() { case self::TYPE_NUMBER: $ret = str_replace( '--', '', $this->token ); // e.g. ---42 === -42. if ( $this->flags & self::FLAG_NUMBER_HEX ) { + $ret = str_replace( array( '-', '+' ), '', $this->token ); if ( $this->flags & self::FLAG_NUMBER_NEGATIVE ) { - $ret = str_replace( '-', '', $this->token ); $ret = -hexdec( $ret ); } else { $ret = hexdec( $ret ); diff --git a/src/wp-includes/sqlite/class-wp-sqlite-translator.php b/src/wp-includes/sqlite/class-wp-sqlite-translator.php index 9c784244bb8a8..9a1fabc621fa7 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -1040,7 +1040,7 @@ private function parse_mysql_create_table_field() { $result->name = ''; $result->sqlite_data_type = ''; $result->not_null = false; - $result->default = null; + $result->default = false; $result->auto_increment = false; $result->primary_key = false; @@ -1054,7 +1054,7 @@ private function parse_mysql_create_table_field() { $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. + // Look for the NOT NULL, PRIMARY KEY, DEFAULT, and AUTO_INCREMENT flags. while ( true ) { $token = $this->rewriter->skip(); if ( ! $token ) { @@ -1123,8 +1123,30 @@ private function make_sqlite_field_definition( $field ) { if ( $field->not_null ) { $definition .= ' NOT NULL'; } - if ( null !== $field->default ) { + /** + * WPDB removes the STRICT_TRANS_TABLES mode from MySQL queries. + * This mode allows the use of `NULL` when NOT NULL is set on a column that falls back to DEFAULT. + * SQLite does not support this behavior, so we need to add the `ON CONFLICT REPLACE` clause to the column definition. + */ + if ($field->not_null) { + $definition .= ' ON CONFLICT REPLACE'; + } + /** + * The value of DEFAULT can be NULL. PHP would print this as an empty string, so we need a special case for it. + */ + if (null === $field->default) { + $definition .= ' DEFAULT NULL'; + } else if (false !== $field->default) { $definition .= ' DEFAULT ' . $field->default; + } else if ($field->not_null) { + /** + * If the column is NOT NULL, we need to provide a default value to match WPDB behavior caused by removing the STRICT_TRANS_TABLES mode. + */ + if ('text' === $field->sqlite_data_type) { + $definition .= ' DEFAULT \'\''; + } else if (in_array($field->sqlite_data_type, array('integer', 'real'), true)) { + $definition .= ' DEFAULT 0'; + } } /* @@ -1824,6 +1846,7 @@ private function translate_expression( $token ) { || $this->capture_group_by( $token ) || $this->translate_ungrouped_having( $token ) || $this->translate_like_escape( $token ) + || $this->translate_left_function( $token ) ); } @@ -2025,6 +2048,41 @@ private function translate_date_add_sub( $token ) { return true; } + /** + * Translate the LEFT() function. + * + * > Returns the leftmost len characters from the string str, or NULL if any argument is NULL. + * + * https://dev.mysql.com/doc/refman/8.3/en/string-functions.html#function_left + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_left_function( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, + array( 'LEFT' ) + ) + ) { + return false; + } + + $this->rewriter->skip(); + $this->rewriter->add( new WP_SQLite_Token( 'SUBSTRING', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ) ); + $this->rewriter->consume( + array( + 'type' => WP_SQLite_Token::TYPE_OPERATOR, + 'value' => ',', + ) + ); + $this->rewriter->add( new WP_SQLite_Token( 1, WP_SQLite_Token::TYPE_NUMBER ) ); + $this->rewriter->add( new WP_SQLite_Token( ',', WP_SQLite_Token::TYPE_OPERATOR ) ); + return true; + } + /** * Convert function aliases. * @@ -3026,6 +3084,16 @@ private function execute_show() { $this->results = true; return; + case 'GRANTS FOR': + $this->set_results_from_fetched_data( + array( + (object) array( + 'Grants for root@localhost' => 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, SHUTDOWN, PROCESS, FILE, REFERENCES, INDEX, ALTER, SHOW DATABASES, SUPER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE USER, EVENT, TRIGGER, CREATE TABLESPACE, CREATE ROLE, DROP ROLE ON *.* TO `root`@`localhost` WITH GRANT OPTION', + ), + ) + ); + return; + case 'FULL COLUMNS': $this->rewriter->consume(); // Fall through. @@ -3141,12 +3209,54 @@ function ( $row ) use ( $name_map ) { return; case 'TABLE STATUS': // FROM `database`. - $this->rewriter->skip(); + // Match the optional [{FROM | IN} db_name] + $database_expression = $this->rewriter->consume(); + if ( $database_expression->token === 'FROM' || $database_expression->token === 'IN' ) { + $this->rewriter->consume(); + $database_expression = $this->rewriter->consume(); + } + + $pattern = '%'; + // [LIKE 'pattern' | WHERE expr] + if($database_expression->token === 'LIKE') { + $pattern = $this->rewriter->consume()->value; + } else if($database_expression->token === 'WHERE') { + // @TODO Support me please. + } else if($database_expression->token !== ';') { + throw new Exception( 'Syntax error: Unexpected token ' . $database_expression->token .' in query '. $this->mysql_query ); + } + $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" - ); + "SELECT + name as `Name`, + 'myisam' as `Engine`, + 10 as `Version`, + 'Fixed' as `Row_format`, + 0 as `Rows`, + 0 as `Avg_row_length`, + 0 as `Data_length`, + 0 as `Max_data_length`, + 0 as `Index_length`, + 0 as `Data_free` , + 0 as `Auto_increment`, + '2024-03-20 15:33:20' as `Create_time`, + '2024-03-20 15:33:20' as `Update_time`, + null as `Check_time`, + null as `Collation`, + null as `Checksum`, + '' as `Create_options`, + '' as `Comment` + FROM sqlite_master + WHERE + type='table' + AND name LIKE :pattern + ORDER BY name", + array( + ':pattern' => $pattern, + ) + ); $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 From 3f6aabebd6358a3016d9cc95e45c76996fc8fd55 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Thu, 18 Apr 2024 10:22:19 +0300 Subject: [PATCH 35/36] CS fixes --- .../sqlite/class-wp-sqlite-translator.php | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/wp-includes/sqlite/class-wp-sqlite-translator.php b/src/wp-includes/sqlite/class-wp-sqlite-translator.php index 9a1fabc621fa7..32aa855e30ea4 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -1128,23 +1128,23 @@ private function make_sqlite_field_definition( $field ) { * This mode allows the use of `NULL` when NOT NULL is set on a column that falls back to DEFAULT. * SQLite does not support this behavior, so we need to add the `ON CONFLICT REPLACE` clause to the column definition. */ - if ($field->not_null) { + if ( $field->not_null ) { $definition .= ' ON CONFLICT REPLACE'; } /** * The value of DEFAULT can be NULL. PHP would print this as an empty string, so we need a special case for it. */ - if (null === $field->default) { + if ( null === $field->default ) { $definition .= ' DEFAULT NULL'; - } else if (false !== $field->default) { + } elseif ( false !== $field->default ) { $definition .= ' DEFAULT ' . $field->default; - } else if ($field->not_null) { + } elseif ( $field->not_null ) { /** * If the column is NOT NULL, we need to provide a default value to match WPDB behavior caused by removing the STRICT_TRANS_TABLES mode. */ - if ('text' === $field->sqlite_data_type) { + if ( 'text' === $field->sqlite_data_type ) { $definition .= ' DEFAULT \'\''; - } else if (in_array($field->sqlite_data_type, array('integer', 'real'), true)) { + } elseif ( in_array( $field->sqlite_data_type, array( 'integer', 'real' ), true ) ) { $definition .= ' DEFAULT 0'; } } @@ -3211,19 +3211,19 @@ function ( $row ) use ( $name_map ) { case 'TABLE STATUS': // FROM `database`. // Match the optional [{FROM | IN} db_name] $database_expression = $this->rewriter->consume(); - if ( $database_expression->token === 'FROM' || $database_expression->token === 'IN' ) { + if ( 'FROM' === $database_expression->token || 'IN' === $database_expression->token ) { $this->rewriter->consume(); $database_expression = $this->rewriter->consume(); } $pattern = '%'; // [LIKE 'pattern' | WHERE expr] - if($database_expression->token === 'LIKE') { + if ( 'LIKE' === $database_expression->token ) { $pattern = $this->rewriter->consume()->value; - } else if($database_expression->token === 'WHERE') { + } elseif ( 'WHERE' === $database_expression->token ) { // @TODO Support me please. - } else if($database_expression->token !== ';') { - throw new Exception( 'Syntax error: Unexpected token ' . $database_expression->token .' in query '. $this->mysql_query ); + } elseif ( ';' !== $database_expression->token ) { + throw new Exception( 'Syntax error: Unexpected token ' . $database_expression->token . ' in query ' . $this->mysql_query ); } $database_expression = $this->rewriter->skip(); @@ -3252,12 +3252,11 @@ function ( $row ) use ( $name_map ) { type='table' AND name LIKE :pattern ORDER BY name", - array( ':pattern' => $pattern, ) ); - $tables = $this->strip_sqlite_system_tables( $stmt->fetchAll( $this->pdo_fetch_mode ) ); + $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" ); From 47342f932b6445b374bc0b975e2ac56ed786d760 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Thu, 20 Jun 2024 14:22:18 +0300 Subject: [PATCH 36/36] Update the SQLite translator class --- .../sqlite/class-wp-sqlite-translator.php | 340 +++++++++++++++--- 1 file changed, 284 insertions(+), 56 deletions(-) diff --git a/src/wp-includes/sqlite/class-wp-sqlite-translator.php b/src/wp-includes/sqlite/class-wp-sqlite-translator.php index 32aa855e30ea4..ac6a170c5e85c 100644 --- a/src/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/src/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -1438,6 +1438,10 @@ private function execute_select() { continue; } + if ( $this->skip_index_hint() ) { + continue; + } + $this->rewriter->consume(); } $this->rewriter->consume_all(); @@ -1449,8 +1453,9 @@ private function execute_select() { $updated_query = $this->get_information_schema_query( $updated_query ); $params = array(); } elseif ( - strpos( $updated_query, '@@SESSION.sql_mode' ) !== false - || strpos( $updated_query, 'CONVERT( ' ) !== false + // Examples: @@SESSION.sql_mode, @@GLOBAL.max_allowed_packet, @@character_set_client + preg_match( '/@@((SESSION|GLOBAL)\s*\.\s*)?\w+\b/i', $updated_query ) === 1 || + strpos( $updated_query, 'CONVERT( ' ) !== false ) { /* * If the query contains a function that is not supported by SQLite, @@ -1490,6 +1495,71 @@ private function execute_select() { } } + /** + * Ignores the FORCE INDEX clause + * + * USE {INDEX|KEY} + * [FOR {JOIN|ORDER BY|GROUP BY}] ([index_list]) + * | {IGNORE|FORCE} {INDEX|KEY} + * [FOR {JOIN|ORDER BY|GROUP BY}] (index_list) + * + * @see https://dev.mysql.com/doc/refman/8.3/en/index-hints.html + * @return bool + */ + private function skip_index_hint() { + $force = $this->rewriter->peek(); + if ( ! $force || ! $force->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'USE', 'FORCE', 'IGNORE' ) + ) ) { + return false; + } + + $index = $this->rewriter->peek_nth( 2 ); + if ( ! $index || ! $index->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'INDEX', 'KEY' ) + ) ) { + return false; + } + + $this->rewriter->skip(); // USE, FORCE, IGNORE. + $this->rewriter->skip(); // INDEX, KEY. + + $maybe_for = $this->rewriter->peek(); + if ( $maybe_for && $maybe_for->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'FOR' ) + ) ) { + $this->rewriter->skip(); // FOR. + + $token = $this->rewriter->peek(); + if ( $token && $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'JOIN', 'ORDER', 'GROUP' ) + ) ) { + $this->rewriter->skip(); // JOIN, ORDER, GROUP. + if ( 'BY' === strtoupper( $this->rewriter->peek()->value ) ) { + $this->rewriter->skip(); // BY. + } + } + } + + // Skip everything until the closing parenthesis. + $this->rewriter->skip( + array( + 'type' => WP_SQLite_Token::TYPE_OPERATOR, + 'value' => ')', + ) + ); + + return true; + } + /** * Executes a TRUNCATE statement. */ @@ -1515,7 +1585,23 @@ private function execute_truncate() { private function execute_describe() { $this->rewriter->skip(); $this->table_name = $this->rewriter->consume()->value; - $stmt = $this->execute_sqlite_query( + $this->set_results_from_fetched_data( + $this->describe( $this->table_name ) + ); + if ( ! $this->results ) { + throw new PDOException( 'Table not found' ); + } + } + + /** + * Executes a SELECT statement. + * + * @param string $table_name The table name. + * + * @return array + */ + private function describe( $table_name ) { + return $this->execute_sqlite_query( "SELECT `name` as `Field`, ( @@ -1524,7 +1610,7 @@ private function execute_describe() { WHEN 1 THEN 'NO' END ) as `Null`, - IFNULL( + COALESCE( d.`mysql_type`, ( CASE `type` @@ -1544,34 +1630,70 @@ private function execute_describe() { ELSE 'PRI' END ) as `Key` - FROM pragma_table_info(\"$this->table_name\") p + FROM pragma_table_info(\"$table_name\") p LEFT JOIN " . self::DATA_TYPES_CACHE_TABLE . " d - ON d.`table` = \"$this->table_name\" + 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' ); - } + ) + ->fetchAll( $this->pdo_fetch_mode ); } /** * Executes an UPDATE statement. + * Supported syntax: + * + * UPDATE [LOW_PRIORITY] [IGNORE] table_reference + * SET assignment_list + * [WHERE where_condition] + * [ORDER BY ...] + * [LIMIT row_count] + * + * @see https://dev.mysql.com/doc/refman/8.0/en/update.html */ private function execute_update() { - $this->rewriter->consume(); // Update. - - $params = array(); + $this->rewriter->consume(); // Consume the UPDATE keyword. + $has_where = false; + $needs_closing_parenthesis = false; + $params = array(); while ( true ) { $token = $this->rewriter->peek(); if ( ! $token ) { break; } + /* + * If the query contains a WHERE clause, + * we need to rewrite the query to use a nested SELECT statement. + * eg: + * - UPDATE table SET column = value WHERE condition LIMIT 1; + * will be rewritten to: + * - UPDATE table SET column = value WHERE rowid IN (SELECT rowid FROM table WHERE condition LIMIT 1); + */ + if ( 0 === $this->rewriter->depth ) { + if ( ( 'LIMIT' === $token->value || 'ORDER' === $token->value ) && ! $has_where ) { + $this->rewriter->add( + new WP_SQLite_Token( 'WHERE', WP_SQLite_Token::TYPE_KEYWORD ) + ); + $needs_closing_parenthesis = true; + $this->preface_where_clause_with_a_subquery(); + } elseif ( 'WHERE' === $token->value ) { + $has_where = true; + $needs_closing_parenthesis = true; + $this->rewriter->consume(); + $this->preface_where_clause_with_a_subquery(); + $this->rewriter->add( + new WP_SQLite_Token( 'WHERE', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) + ); + } + } + + // Ignore the semicolon in case of rewritten query as it breaks the query. + if ( ';' === $this->rewriter->peek()->value && $this->rewriter->peek()->type === WP_SQLite_Token::TYPE_DELIMITER ) { + break; + } + // Record the table name. if ( ! $this->table_name && @@ -1594,6 +1716,12 @@ private function execute_update() { $this->rewriter->consume(); } + + // Wrap up the WHERE clause with the nested SELECT statement. + if ( $needs_closing_parenthesis ) { + $this->rewriter->add( new WP_SQLite_Token( ')', WP_SQLite_Token::TYPE_OPERATOR ) ); + } + $this->rewriter->consume_all(); $updated_query = $this->rewriter->get_updated_query(); @@ -1601,6 +1729,39 @@ private function execute_update() { $this->set_result_from_affected_rows(); } + /** + * Injects `rowid IN (SELECT rowid FROM table WHERE ...` into the WHERE clause at the current + * position in the query. + * + * This is necessary to emulate the behavior of MySQL's UPDATE LIMIT and DELETE LIMIT statement + * as SQLite does not support LIMIT in UPDATE and DELETE statements. + * + * The WHERE clause is wrapped in a subquery that selects the rowid of the rows that match the original + * WHERE clause. + * + * @return void + */ + private function preface_where_clause_with_a_subquery() { + $this->rewriter->add_many( + array( + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'rowid', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'IN', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( '(', WP_SQLite_Token::TYPE_OPERATOR ), + new WP_SQLite_Token( 'SELECT', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'rowid', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'FROM', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( $this->table_name, WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + ) + ); + } + /** * Executes a INSERT or REPLACE statement. */ @@ -2642,9 +2803,10 @@ private function translate_on_duplicate_key( $table_name ) { $this->rewriter->add( new WP_SQLite_Token( '(', WP_SQLite_Token::TYPE_OPERATOR ) ); $max = count( $conflict_columns ); - foreach ( $conflict_columns as $i => $conflict_column ) { + $i = 0; + foreach ( $conflict_columns as $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 ) { + if ( ++$i < $max ) { $this->rewriter->add( new WP_SQLite_Token( ',', WP_SQLite_Token::TYPE_OPERATOR ) ); $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); } @@ -3076,8 +3238,8 @@ private function execute_drop() { */ private function execute_show() { $this->rewriter->skip(); - $what1 = $this->rewriter->consume()->token; - $what2 = $this->rewriter->consume()->token; + $what1 = strtoupper( $this->rewriter->consume()->token ); + $what2 = strtoupper( $this->rewriter->consume()->token ); $what = $what1 . ' ' . $what2; switch ( $what ) { case 'CREATE PROCEDURE': @@ -3099,41 +3261,8 @@ private function execute_show() { // 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 ); + + $this->set_results_from_fetched_data( $this->get_columns_from( $table_name ) ); return; case 'INDEX FROM': @@ -3208,8 +3337,61 @@ function ( $row ) use ( $name_map ) { return; + case 'CREATE TABLE': + // Value is unquoted table name + $table_name = $this->rewriter->consume()->value; + $columns = $this->get_columns_from( $table_name ); + $keys = $this->get_keys( $table_name ); + + if ( empty( $columns ) ) { + $this->set_results_from_fetched_data( + array() + ); + return; + } + + foreach ( $columns as $column ) { + $column = (array) $column; + $definition = ''; + $definition .= '`' . $column['Field'] . '` '; + $definition .= $this->get_cached_mysql_data_type( + $table_name, + $column['Field'] + ) ?? $column['Type']; + $definition .= 'PRI' === $column['Key'] ? ' PRIMARY KEY' : ''; + $definition .= 'PRI' === $column['Key'] && 'INTEGER' === $column['Type'] ? ' AUTO_INCREMENT' : ''; + $definition .= 'NO' === $column['Null'] ? ' NOT NULL' : ''; + $definition .= $column['Default'] ? ' DEFAULT ' . $column['Default'] : ''; + $entries[] = $definition; + } + foreach ( $keys as $key ) { + $key = (array) $key; + $definition = ''; + $definition .= '1' === $key['index']['unique'] ? 'UNIQUE ' : ''; + $definition .= 'KEY '; + $definition .= $key['index']['name']; + $definition .= ' ('; + $definition .= implode( + ', ', + array_column( $key['columns'], 'name' ) + ); + $definition .= ')'; + $entries[] = $definition; + } + $create_table = "CREATE TABLE $table_name (\n\t"; + $create_table .= implode( ",\n\t", $entries ); + $create_table .= "\n);"; + $this->set_results_from_fetched_data( + array( + (object) array( + 'Create Table' => $create_table, + ), + ) + ); + return; + case 'TABLE STATUS': // FROM `database`. - // Match the optional [{FROM | IN} db_name] + // Match the optional [{FROM | IN} db_name]. $database_expression = $this->rewriter->consume(); if ( 'FROM' === $database_expression->token || 'IN' === $database_expression->token ) { $this->rewriter->consume(); @@ -3278,6 +3460,7 @@ function ( $row ) use ( $name_map ) { ':param' => $table_expression->value, ) ); + $this->set_results_from_fetched_data( $stmt->fetchAll( $this->pdo_fetch_mode ) ); @@ -3305,6 +3488,51 @@ function ( $row ) use ( $name_map ) { } } + /** + * Gets the columns from a table. + * + * @param string $table_name The table name. + * + * @return array The columns. + */ + private function get_columns_from( $table_name ) { + $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 + ); + return $columns; + } + /** * Consumes data types from the query. *