- * 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.
- *
- * @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 Not used.
- * @param integer $timeout Not used.
- *
- * @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 Not used.
- *
- * @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 $content String to be converted to uppercase.
- *
- * @return string SQLite compatible function name.
- */
- 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 $content String to be converted to lowercase.
- *
- * @return string SQLite compatible function name.
- */
- 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 FROM_BASE64() function.
- *
- * Takes a base64-encoded string and returns the decoded result as a binary
- * string. Returns NULL if the argument is NULL or is not a valid base64 string.
- *
- * @param string|null $str The base64-encoded string.
- *
- * @return string|null Decoded binary string, or NULL.
- */
- public function from_base64( $str ) {
- if ( null === $str ) {
- return null;
- }
- $decoded = base64_decode( $str, true );
- if ( false === $decoded ) {
- return null;
- }
- return $decoded;
- }
-
- /**
- * Method to emulate MySQL TO_BASE64() function.
- *
- * Takes a string and returns a base64-encoded result.
- * Returns NULL if the argument is NULL.
- *
- * @param string|null $str The string to encode.
- *
- * @return string|null Base64-encoded string, or NULL.
- */
- public function to_base64( $str ) {
- if ( null === $str ) {
- return null;
- }
- return base64_encode( $str );
- }
-
- /**
- * Method to emulate MySQL INET_NTOA() function.
- *
- * This function gets 4 or 8 bytes integer and turn it into the network address.
- *
- * @param integer $num 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 $addr Network address.
- *
- * @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 Start date.
- * @param string $end End date.
- *
- * @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 $substr Needle.
- * @param string $str Haystack.
- * @param integer $pos 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.
- *
- * @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.
- *
- * @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.
- *
- * @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.
- *
- * @return string representing the version number: major_version.minor_version
- */
- public function version() {
- return '5.5';
- }
-
- /**
- * A helper to covert LIKE pattern to a GLOB pattern for "LIKE BINARY" support.
-
- * @TODO: Some of the MySQL string specifics described below are likely to
- * affect also other patterns than just "LIKE BINARY". We should
- * consider applying some of the conversions more broadly.
- *
- * @param string $pattern
- * @return string
- */
- public function _helper_like_to_glob_pattern( $pattern ) {
- if ( null === $pattern ) {
- return null;
- }
-
- /*
- * 1. Escape characters that have special meaning in GLOB patterns.
- *
- * We need to:
- * 1. Escape "]" as "[]]" to avoid interpreting "[...]" as a character class.
- * 2. Escape "*" as "[*]" (must be after 1 to avoid being escaped).
- * 3. Escape "?" as "[?]" (must be after 1 to avoid being escaped).
- */
- $pattern = str_replace( ']', '[]]', $pattern );
- $pattern = str_replace( '*', '[*]', $pattern );
- $pattern = str_replace( '?', '[?]', $pattern );
-
- /*
- * 2. Convert LIKE wildcards to GLOB wildcards ("%" -> "*", "_" -> "?").
- *
- * We need to convert them only when they don't follow any backslashes,
- * or when they follow an even number of backslashes (as "\\" is "\").
- */
- $pattern = preg_replace( '/(^|[^\\\\](?:\\\\{2})*)%/', '$1*', $pattern );
- $pattern = preg_replace( '/(^|[^\\\\](?:\\\\{2})*)_/', '$1?', $pattern );
-
- /*
- * 3. Unescape LIKE escape sequences.
- *
- * While in MySQL LIKE patterns, a backslash is usually used to escape
- * special characters ("%", "_", and "\"), it works with all characters.
- *
- * That is:
- * SELECT '\\x' prints '\x', but LIKE '\\x' is equivalent to LIKE 'x'.
- *
- * This is true also for multi-byte characters:
- * SELECT '\\©' prints '\©', but LIKE '\\©' is equivalent to LIKE '©'.
- *
- * However, the multi-byte behavior is likely to depend on the charset.
- * For now, we'll assume UTF-8 and thus the "u" modifier for the regex.
- */
- $pattern = preg_replace( '/\\\\(.)/u', '$1', $pattern );
-
- return $pattern;
- }
-}
diff --git a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php b/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php
deleted file mode 100644
index edb32886..00000000
--- a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php
+++ /dev/null
@@ -1,343 +0,0 @@
-input_tokens = $input_tokens;
- $this->max = count( $input_tokens );
- }
-
- /**
- * Returns the updated query.
- *
- * @return string
- */
- public function get_updated_query() {
- $query = '';
- foreach ( $this->output_tokens as $token ) {
- $query .= $token->token;
- }
- return $query;
- }
-
- /**
- * Add a token to the output.
- *
- * @param WP_SQLite_Token $token Token object.
- */
- public function add( $token ) {
- if ( $token ) {
- $this->output_tokens[] = $token;
- }
- }
-
- /**
- * Add multiple tokens to the output.
- *
- * @param WP_SQLite_Token[] $tokens Array of token objects.
- */
- public function add_many( $tokens ) {
- $this->output_tokens = array_merge( $this->output_tokens, $tokens );
- }
-
- /**
- * Replaces all tokens.
- *
- * @param WP_SQLite_Token[] $tokens Array of token objects.
- */
- public function replace_all( $tokens ) {
- $this->output_tokens = $tokens;
- }
-
- /**
- * Peek at the next tokens and return one that matches the given criteria.
- *
- * @param array $query Optional. Search query.
- * [
- * 'type' => string|null, // Token type.
- * 'flags' => int|null, // Token flags.
- * 'values' => string|null, // Token values.
- * ].
- *
- * @return WP_SQLite_Token
- */
- public function peek( $query = array() ) {
- $type = isset( $query['type'] ) ? $query['type'] : null;
- $flags = isset( $query['flags'] ) ? $query['flags'] : null;
- $values = isset( $query['value'] )
- ? ( is_array( $query['value'] ) ? $query['value'] : array( $query['value'] ) )
- : null;
-
- $i = $this->index;
- while ( ++$i < $this->max ) {
- if ( $this->input_tokens[ $i ]->matches( $type, $flags, $values ) ) {
- return $this->input_tokens[ $i ];
- }
- }
- }
-
- /**
- * Move forward and return the next tokens that match the given criteria.
- *
- * @param int $nth The nth token to return.
- *
- * @return WP_SQLite_Token
- */
- public function peek_nth( $nth ) {
- $found = 0;
- for ( $i = $this->index + 1;$i < $this->max;$i++ ) {
- $token = $this->input_tokens[ $i ];
- if ( ! $token->is_semantically_void() ) {
- ++$found;
- }
- if ( $found === $nth ) {
- return $this->input_tokens[ $i ];
- }
- }
- }
-
- /**
- * Consume all the tokens.
- *
- * @param array $query Search query.
- *
- * @return void
- */
- public function consume_all( $query = array() ) {
- while ( $this->consume( $query ) ) {
- // Do nothing.
- }
- }
-
- /**
- * Consume the next tokens and return one that matches the given criteria.
- *
- * @param array $query Search query.
- * [
- * 'type' => null, // Optional. Token type.
- * 'flags' => null, // Optional. Token flags.
- * 'values' => null, // Optional. Token values.
- * ].
- *
- * @return WP_SQLite_Token|null
- */
- public function consume( $query = array() ) {
- $tokens = $this->move_forward( $query );
- $this->output_tokens = array_merge( $this->output_tokens, $tokens );
- return $this->token;
- }
-
- /**
- * Drop the last consumed token and return it.
- *
- * @return WP_SQLite_Token|null
- */
- public function drop_last() {
- return array_pop( $this->output_tokens );
- }
-
- /**
- * Skip over the next tokens and return one that matches the given criteria.
- *
- * @param array $query Search query.
- * [
- * 'type' => null, // Optional. Token type.
- * 'flags' => null, // Optional. Token flags.
- * 'values' => null, // Optional. Token values.
- * ].
- *
- * @return WP_SQLite_Token|null
- */
- public function skip( $query = array() ) {
- $this->skip_and_return_all( $query );
- return $this->token;
- }
-
- /**
- * Skip over the next tokens until one matches the given criteria,
- * and return all the skipped tokens.
- *
- * @param array $query Search query.
- * [
- * 'type' => null, // Optional. Token type.
- * 'flags' => null, // Optional. Token flags.
- * 'values' => null, // Optional. Token values.
- * ].
- *
- * @return WP_SQLite_Token[]
- */
- public function skip_and_return_all( $query = array() ) {
- $tokens = $this->move_forward( $query );
-
- /*
- * When skipping over whitespaces, make sure to consume
- * at least one to avoid SQL syntax errors.
- */
- foreach ( $tokens as $token ) {
- if ( $token->matches( WP_SQLite_Token::TYPE_WHITESPACE ) ) {
- $this->add( $token );
- break;
- }
- }
-
- return $tokens;
- }
-
- /**
- * Returns the next tokens that match the given criteria.
- *
- * @param array $query Search query.
- * [
- * 'type' => string|null, // Optional. Token type.
- * 'flags' => int|null, // Optional. Token flags.
- * 'values' => string|null, // Optional. Token values.
- * ].
- *
- * @return array
- */
- private function move_forward( $query = array() ) {
- $type = isset( $query['type'] ) ? $query['type'] : null;
- $flags = isset( $query['flags'] ) ? $query['flags'] : null;
- $values = isset( $query['value'] )
- ? ( is_array( $query['value'] ) ? $query['value'] : array( $query['value'] ) )
- : null;
- $depth = isset( $query['depth'] ) ? $query['depth'] : null;
-
- $buffered = array();
- while ( true ) {
- if ( ++$this->index >= $this->max ) {
- $this->token = null;
- $this->call_stack = array();
- break;
- }
- $this->token = $this->input_tokens[ $this->index ];
- $this->update_call_stack();
- $buffered[] = $this->token;
- if (
- ( null === $depth || $this->depth === $depth )
- && $this->token->matches( $type, $flags, $values )
- ) {
- break;
- }
- }
-
- return $buffered;
- }
-
- /**
- * Returns the last call stack element.
- *
- * @return array|null
- */
- public function last_call_stack_element() {
- return count( $this->call_stack ) ? $this->call_stack[ count( $this->call_stack ) - 1 ] : null;
- }
-
- /**
- * Updates the call stack.
- *
- * @return void
- */
- private function update_call_stack() {
- if ( $this->token->flags & WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ) {
- $this->last_function_call = $this->token->value;
- }
- if ( WP_SQLite_Token::TYPE_OPERATOR === $this->token->type ) {
- switch ( $this->token->value ) {
- case '(':
- if ( $this->last_function_call ) {
- array_push(
- $this->call_stack,
- array(
- 'function' => $this->last_function_call,
- 'depth' => $this->depth,
- )
- );
- $this->last_function_call = null;
- }
- ++$this->depth;
- break;
-
- case ')':
- --$this->depth;
- $call_parent = $this->last_call_stack_element();
- if (
- $call_parent &&
- $call_parent['depth'] === $this->depth
- ) {
- array_pop( $this->call_stack );
- }
- break;
- }
- }
- }
-}
diff --git a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-token.php b/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-token.php
deleted file mode 100644
index fbafd9e9..00000000
--- a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-token.php
+++ /dev/null
@@ -1,327 +0,0 @@
-, !==, etc.
- * Bitwise operators: &, |, ^, etc.
- * Assignment operators: =, +=, -=, etc.
- * SQL specific operators: . (e.g. .. WHERE database.table ..),
- * * (e.g. SELECT * FROM ..)
- */
- const TYPE_OPERATOR = 2;
-
- /**
- * Spaces, tabs, new lines, etc.
- */
- const TYPE_WHITESPACE = 3;
-
- /**
- * Any type of legal comment.
- *
- * Bash (#), C (/* *\/) or SQL (--) comments:
- *
- * -- SQL-comment
- *
- * #Bash-like comment
- *
- * /*C-like comment*\/
- *
- * or:
- *
- * /*C-like
- * comment*\/
- *
- * Backslashes were added to respect PHP's comments syntax.
- */
- const TYPE_COMMENT = 4;
-
- /**
- * Boolean values: true or false.
- */
- const TYPE_BOOL = 5;
-
- /**
- * Numbers: 4, 0x8, 15.16, 23e42, etc.
- */
- const TYPE_NUMBER = 6;
-
- /**
- * Literal strings: 'string', "test".
- * Some of these strings are actually symbols.
- */
- const TYPE_STRING = 7;
-
- /**
- * Database, table names, variables, etc.
- * For example: ```SELECT `foo`, `bar` FROM `database`.`table`;```.
- */
- const TYPE_SYMBOL = 8;
-
- /**
- * Delimits an unknown string.
- * For example: ```SELECT * FROM test;```, `test` is a delimiter.
- */
- const TYPE_DELIMITER = 9;
-
- /**
- * Labels in LOOP statement, ITERATE statement etc.
- * For example (only for begin label):
- * begin_label: BEGIN [statement_list] END [end_label]
- * begin_label: LOOP [statement_list] END LOOP [end_label]
- * begin_label: REPEAT [statement_list] ... END REPEAT [end_label]
- * begin_label: WHILE ... DO [statement_list] END WHILE [end_label].
- */
- const TYPE_LABEL = 10;
-
- // Flags that describe the tokens in more detail.
- // All keywords must have flag 1 so `Context::isKeyword` method doesn't
- // require strict comparison.
- const FLAG_KEYWORD_RESERVED = 2;
- const FLAG_KEYWORD_COMPOSED = 4;
- const FLAG_KEYWORD_DATA_TYPE = 8;
- const FLAG_KEYWORD_KEY = 16;
- const FLAG_KEYWORD_FUNCTION = 32;
-
- // Numbers related flags.
- const FLAG_NUMBER_HEX = 1;
- const FLAG_NUMBER_FLOAT = 2;
- const FLAG_NUMBER_APPROXIMATE = 4;
- const FLAG_NUMBER_NEGATIVE = 8;
- const FLAG_NUMBER_BINARY = 16;
-
- // Strings related flags.
- const FLAG_STRING_SINGLE_QUOTES = 1;
- const FLAG_STRING_DOUBLE_QUOTES = 2;
-
- // Comments related flags.
- const FLAG_COMMENT_BASH = 1;
- const FLAG_COMMENT_C = 2;
- const FLAG_COMMENT_SQL = 4;
- const FLAG_COMMENT_MYSQL_CMD = 8;
-
- // Operators related flags.
- const FLAG_OPERATOR_ARITHMETIC = 1;
- const FLAG_OPERATOR_LOGICAL = 2;
- const FLAG_OPERATOR_BITWISE = 4;
- const FLAG_OPERATOR_ASSIGNMENT = 8;
- const FLAG_OPERATOR_SQL = 16;
-
- // Symbols related flags.
- const FLAG_SYMBOL_VARIABLE = 1;
- const FLAG_SYMBOL_BACKTICK = 2;
- const FLAG_SYMBOL_USER = 4;
- const FLAG_SYMBOL_SYSTEM = 8;
- const FLAG_SYMBOL_PARAMETER = 16;
-
- /**
- * The token it its raw string representation.
- *
- * @var string
- */
- public $token;
-
- /**
- * The value this token contains (i.e. token after some evaluation).
- *
- * @var mixed
- */
- public $value;
-
- /**
- * The keyword value this token contains, always uppercase.
- *
- * @var mixed|string|null
- */
- public $keyword = null;
-
- /**
- * 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->value = $this->extract();
- }
-
- /**
- * Check if the token matches the given parameters.
- *
- * @param int|null $type The type of the token.
- * @param int|null $flags The flags of the token.
- * @param array|null $values The values of the token.
- *
- * @return bool
- */
- public function matches( $type = null, $flags = null, $values = null ) {
- if ( null === $type && null === $flags && ( null === $values || array() === $values ) ) {
- return ! $this->is_semantically_void();
- }
-
- return (
- ( null === $type || $this->type === $type )
- && ( null === $flags || ( $this->flags & $flags ) )
- && ( null === $values || in_array( strtoupper( $this->value ?? '' ), $values, true ) )
- );
- }
-
- /**
- * Check if the token is semantically void (i.e. whitespace or comment).
- *
- * @return bool
- */
- public function is_semantically_void() {
- return $this->matches( self::TYPE_WHITESPACE ) || $this->matches( self::TYPE_COMMENT );
- }
-
- /**
- * Does little processing to the token to extract a value.
- *
- * If no processing can be done it will return the initial string.
- *
- * @return mixed
- */
- private function extract() {
- switch ( $this->type ) {
- case self::TYPE_KEYWORD:
- $this->keyword = strtoupper( $this->token ?? '' );
- if ( ! ( $this->flags & self::FLAG_KEYWORD_RESERVED ) ) {
- /*
- * Unreserved keywords should stay the way they are
- * because they might represent field names.
- */
- return $this->token;
- }
-
- return $this->keyword;
-
- case self::TYPE_WHITESPACE:
- return ' ';
-
- case self::TYPE_BOOL:
- return strtoupper( $this->token ?? '' ) === 'TRUE';
-
- case self::TYPE_NUMBER:
- $ret = str_replace( '--', '', $this->token ); // e.g. ---42 === -42.
- if ( $this->flags & self::FLAG_NUMBER_HEX ) {
- $ret = str_replace( array( '-', '+' ), '', $this->token );
- if ( $this->flags & self::FLAG_NUMBER_NEGATIVE ) {
- $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/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-translator.php b/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-translator.php
deleted file mode 100644
index 3a536d18..00000000
--- a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-translator.php
+++ /dev/null
@@ -1,4543 +0,0 @@
- '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',
- 'enum' => 'text',
- 'numeric' => 'real',
- 'fixed' => 'real',
- 'date' => 'text',
- 'datetime' => 'text',
- 'timestamp' => 'text',
- 'time' => 'text',
- 'year' => 'text',
- 'char' => 'text',
- 'varchar' => 'text',
- 'binary' => 'integer',
- 'varbinary' => 'blob',
- 'tinyblob' => 'blob',
- 'tinytext' => 'text',
- 'blob' => 'blob',
- 'text' => 'text',
- 'mediumblob' => 'blob',
- 'mediumtext' => 'text',
- 'longblob' => 'blob',
- 'longtext' => 'text',
- 'geomcollection' => 'text',
- 'geometrycollection' => 'text',
- );
-
- /**
- * The MySQL to SQLite date formats translation.
- *
- * Maps MySQL formats to SQLite strftime() formats.
- *
- * For MySQL formats, see:
- * * https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_date-format
- *
- * For SQLite formats, see:
- * * https://www.sqlite.org/lang_datefunc.html
- * * https://strftime.org/
- *
- * @var array
- */
- private $mysql_date_format_to_sqlite_strftime = array(
- '%a' => '%D',
- '%b' => '%M',
- '%c' => '%n',
- '%D' => '%jS',
- '%d' => '%d',
- '%e' => '%j',
- '%H' => '%H',
- '%h' => '%h',
- '%I' => '%h',
- '%i' => '%M',
- '%j' => '%z',
- '%k' => '%G',
- '%l' => '%g',
- '%M' => '%F',
- '%m' => '%m',
- '%p' => '%A',
- '%r' => '%h:%i:%s %A',
- '%S' => '%s',
- '%s' => '%s',
- '%T' => '%H:%i:%s',
- '%U' => '%W',
- '%u' => '%W',
- '%V' => '%W',
- '%v' => '%W',
- '%W' => '%l',
- '%w' => '%w',
- '%X' => '%Y',
- '%x' => '%o',
- '%Y' => '%Y',
- '%y' => '%y',
- );
-
- /**
- * Number of rows found by the last SELECT query.
- *
- * @var int
- */
- private $last_select_found_rows;
-
- /**
- * Number of rows found by the last SQL_CALC_FOUND_ROW query.
- *
- * @var int integer
- */
- private $last_sql_calc_found_rows = null;
-
- /**
- * The query rewriter.
- *
- * @var WP_SQLite_Query_Rewriter
- */
- private $rewriter;
-
- /**
- * Last executed MySQL query.
- *
- * @var string
- */
- public $mysql_query;
-
- /**
- * A list of executed SQLite queries.
- *
- * @var array
- */
- public $executed_sqlite_queries = array();
-
- /**
- * The 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.
- *
- * @var array
- */
- private $insert_columns = array();
-
- /**
- * Class variable to store the result of the query.
- *
- * @access private
- *
- * @var array reference to the PHP object
- */
- private $results = null;
-
- /**
- * Class variable to check if there is an error.
- *
- * @var boolean
- */
- public $is_error = false;
-
- /**
- * Class variable to store the file name and function to cause error.
- *
- * @access private
- *
- * @var array
- */
- private $errors;
-
- /**
- * Class variable to store the error messages.
- *
- * @access private
- *
- * @var array
- */
- private $error_messages = array();
-
- /**
- * Class variable to store the affected row id.
- *
- * @var int integer
- * @access private
- */
- private $last_insert_id;
-
- /**
- * Class variable to store the number of rows affected.
- *
- * @var int integer
- */
- private $affected_rows;
-
- /**
- * Class variable to store the queried column info.
- *
- * @var array
- */
- private $column_data;
-
- /**
- * Variable to emulate MySQL affected row.
- *
- * @var integer
- */
- private $num_rows;
-
- /**
- * Return value from query().
- *
- * Each query has its own return value.
- *
- * @var mixed
- */
- private $return_value;
-
- /**
- * Variable to keep track of nested transactions level.
- *
- * @var int
- */
- private $transaction_level = 0;
-
- /**
- * Value returned by the last exec().
- *
- * @var mixed
- */
- private $last_exec_returned;
-
- /**
- * The PDO fetch mode passed to query().
- *
- * @var mixed
- */
- private $pdo_fetch_mode;
-
- /**
- * The last reserved keyword seen in an SQL query.
- *
- * @var mixed
- */
- private $last_reserved_keyword;
-
- /**
- * True if a VACUUM operation should be done on shutdown,
- * to handle OPTIMIZE TABLE and similar operations.
- *
- * @var bool
- */
- private $vacuum_requested = false;
-
- /**
- * True if the present query is metadata
- *
- * @var bool
- */
- private $is_information_schema_query = false;
-
- /**
- * True if a GROUP BY clause is detected.
- *
- * @var bool
- */
- private $has_group_by = false;
-
- /**
- * 0 if no LIKE is in progress, otherwise counts nested parentheses.
- *
- * @todo A generic stack of expression would scale better. There's already a call_stack in WP_SQLite_Query_Rewriter.
- * @var int
- */
- private $like_expression_nesting = 0;
-
- /**
- * 0 if no LIKE is in progress, otherwise counts nested parentheses.
- *
- * @var int
- */
- private $like_escape_count = 0;
-
- /**
- * Associative array with list of system (non-WordPress) tables.
- *
- * @var array [tablename => tablename]
- */
- private $sqlite_system_tables = array();
-
- /**
- * The last error message from SQLite.
- *
- * @var string
- */
- private $last_sqlite_error;
-
- /**
- * Constructor.
- *
- * Create PDO object, set user defined functions and initialize other settings.
- * Don't use parent::__construct() because this class does not only returns
- * PDO instance but many others jobs.
- *
- * @param PDO $pdo The PDO object.
- */
- public function __construct( $pdo = null ) {
- if ( ! $pdo ) {
- if ( ! is_file( FQDB ) ) {
- $this->prepare_directory();
- }
-
- $locked = false;
- $status = 0;
- $err_message = '';
- do {
- try {
- $options = array(
- PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
- PDO::ATTR_STRINGIFY_FETCHES => true,
- PDO::ATTR_TIMEOUT => 5,
- );
-
- $dsn = 'sqlite:' . FQDB;
- $pdo_class = PHP_VERSION_ID >= 80400 ? PDO\SQLite::class : PDO::class;
- $pdo = new $pdo_class( $dsn, null, null, $options );
- } catch ( PDOException $ex ) {
- $status = $ex->getCode();
- if ( self::SQLITE_BUSY === $status || self::SQLITE_LOCKED === $status ) {
- $locked = true;
- } else {
- $err_message = $ex->getMessage();
- }
- }
- } while ( $locked );
-
- if ( $status > 0 ) {
- $message = sprintf(
- '%s
%s
%s
', - 'Database initialization error!', - "Code: $status", - "Error Message: $err_message" - ); - $this->is_error = true; - $this->error_messages[] = $message; - return; - } - } - - WP_SQLite_PDO_User_Defined_Functions::register_for( $pdo ); - - // MySQL data comes across stringified by default. - $pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO - $pdo->query( WP_SQLite_Translator::CREATE_DATA_TYPES_CACHE_TABLE ); - - /* - * A list of system tables lets us emulate information_schema - * queries without returning extra tables. - */ - $this->sqlite_system_tables ['sqlite_sequence'] = 'sqlite_sequence'; - $this->sqlite_system_tables [ self::DATA_TYPES_CACHE_TABLE ] = self::DATA_TYPES_CACHE_TABLE; - - $this->pdo = $pdo; - - // Fixes a warning in the site-health screen. - $this->client_info = $this->get_sqlite_version(); - - register_shutdown_function( array( $this, '__destruct' ) ); - - // WordPress happens to use no foreign keys. - $statement = $this->pdo->query( 'PRAGMA foreign_keys' ); - // 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";' ); - - $valid_journal_modes = array( 'DELETE', 'TRUNCATE', 'PERSIST', 'MEMORY', 'WAL', 'OFF' ); - if ( defined( 'SQLITE_JOURNAL_MODE' ) && in_array( SQLITE_JOURNAL_MODE, $valid_journal_modes, true ) ) { - $this->pdo->query( 'PRAGMA journal_mode = ' . SQLITE_JOURNAL_MODE ); - } - } - - /** - * 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. - */ - public function __destruct() { - if ( defined( 'SQLITE_MEM_DEBUG' ) && SQLITE_MEM_DEBUG ) { - $max = ini_get( 'memory_limit' ); - if ( is_null( $max ) ) { - $message = sprintf( - '[%s] Memory_limit is not set in php.ini file.', - gmdate( 'Y-m-d H:i:s', $_SERVER['REQUEST_TIME'] ) - ); - error_log( $message ); - return; - } - if ( stripos( $max, 'M' ) !== false ) { - $max = (int) $max * MB_IN_BYTES; - } - $peak = memory_get_peak_usage( true ); - $used = round( (int) $peak / (int) $max * 100, 2 ); - if ( $used > 90 ) { - $message = sprintf( - "[%s] Memory peak usage warning: %s %% used. (max: %sM, now: %sM)\n", - gmdate( 'Y-m-d H:i:s', $_SERVER['REQUEST_TIME'] ), - $used, - $max, - $peak - ); - error_log( $message ); - } - } - } - - /** - * Get the PDO object. - * - * @return PDO - */ - public function get_pdo() { - return $this->pdo; - } - - /** - * Get the version of the SQLite engine. - * - * @return string SQLite engine version as a string. - */ - public function get_sqlite_version(): string { - return $this->pdo->query( 'SELECT SQLITE_VERSION()' )->fetchColumn(); - } - - /** - * Method to return inserted row id. - */ - public function get_insert_id() { - return $this->last_insert_id; - } - - /** - * Method to return the number of rows affected. - */ - public function get_affected_rows() { - return $this->affected_rows; - } - - /** - * This method makes database directory and .htaccess file. - * - * It is executed only once when the installation begins. - */ - private function prepare_directory() { - global $wpdb; - $u = umask( 0000 ); - if ( ! is_dir( FQDBDIR ) ) { - if ( ! @mkdir( FQDBDIR, 0704, true ) ) { - umask( $u ); - wp_die( 'Unable to create the required directory! Please check your server settings.', 'Error!' ); - } - } - if ( ! is_writable( FQDBDIR ) ) { - umask( $u ); - $message = 'Unable to create a file in the directory! Please check your server settings.'; - wp_die( $message, 'Error!' ); - } - if ( ! is_file( FQDBDIR . '.htaccess' ) ) { - $fh = fopen( FQDBDIR . '.htaccess', 'w' ); - if ( ! $fh ) { - umask( $u ); - echo 'Unable to create a file in the directory! Please check your server settings.'; - - return false; - } - fwrite( $fh, 'DENY FROM ALL' ); - fclose( $fh ); - } - if ( ! is_file( FQDBDIR . 'index.php' ) ) { - $fh = fopen( FQDBDIR . 'index.php', 'w' ); - if ( ! $fh ) { - umask( $u ); - echo 'Unable to create a file in the directory! Please check your server settings.'; - - return false; - } - fwrite( $fh, '' ); - fclose( $fh ); - } - umask( $u ); - - return true; - } - - /** - * Method to execute query(). - * - * Divide the query types into seven different ones. That is to say: - * - * 1. SELECT SQL_CALC_FOUND_ROWS - * 2. INSERT - * 3. CREATE TABLE(INDEX) - * 4. ALTER TABLE - * 5. SHOW VARIABLES - * 6. DROP INDEX - * 7. THE OTHERS - * - * #1 is just a tricky play. See the private function handle_sql_count() in query.class.php. - * From #2 through #5 call different functions respectively. - * #6 call the ALTER TABLE query. - * #7 is a normal process: sequentially call prepare_query() and execute_query(). - * - * #1 process has been changed since version 1.5.1. - * - * @param string $statement Full SQL statement string. - * @param int $mode Not used. - * @param array ...$fetch_mode_args Not used. - * - * @see PDO::query() - * - * @throws Exception If the query could not run. - * @throws PDOException If the translated query could not run. - * - * @return mixed according to the query type - */ - public function query( $statement, $mode = PDO::FETCH_OBJ, ...$fetch_mode_args ) { // phpcs:ignore WordPress.DB.RestrictedClasses - $this->flush(); - if ( function_exists( 'apply_filters' ) ) { - /** - * Filters queries before they are translated and run. - * - * Return a non-null value to cause query() to return early with that result. - * Use this filter to intercept queries that don't work correctly in SQLite. - * - * From within the filter you can do - * function filter_sql ($result, $translator, $statement, $mode, $fetch_mode_args) { - * if ( intercepting this query ) { - * return $translator->execute_sqlite_query( $statement ); - * } - * return $result; - * } - * - * @param null|array $result Default null to continue with the query. - * @param object $translator The translator object. You can call $translator->execute_sqlite_query(). - * @param string $statement The statement passed. - * @param int $mode Fetch mode: PDO::FETCH_OBJ, PDO::FETCH_CLASS, etc. - * @param array $fetch_mode_args Variable arguments passed to query. - * - * @returns null|array Null to proceed, or an array containing a resultset. - * @since 2.1.0 - */ - $pre = apply_filters( 'pre_query_sqlite_db', null, $this, $statement, $mode, $fetch_mode_args ); - if ( null !== $pre ) { - return $pre; - } - } - $this->pdo_fetch_mode = $mode; - $this->mysql_query = $statement; - if ( - preg_match( '/^\s*START TRANSACTION/i', $statement ) - || preg_match( '/^\s*BEGIN/i', $statement ) - ) { - return $this->begin_transaction(); - } - if ( preg_match( '/^\s*COMMIT/i', $statement ) ) { - return $this->commit(); - } - if ( preg_match( '/^\s*ROLLBACK/i', $statement ) ) { - return $this->rollback(); - } - - try { - // Perform all the queries in a nested transaction. - $this->begin_transaction(); - - do { - $error = null; - try { - $this->execute_mysql_query( - $statement - ); - } catch ( PDOException $error ) { - if ( $error->getCode() !== self::SQLITE_BUSY ) { - throw $error; - } - } - } while ( $error ); - - if ( function_exists( 'do_action' ) ) { - /** - * 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. - $this->rollback(); - if ( defined( 'PDO_DEBUG' ) && PDO_DEBUG === true ) { - throw $err; - } - return $this->handle_error( $err ); - } - } - - /** - * Method to return the queried column names. - * - * These data are meaningless for SQLite. So they are dummy emulating - * MySQL columns data. - * - * @return array|null of the object - */ - public function get_columns() { - if ( ! empty( $this->results ) ) { - $primary_key = array( - 'meta_id', - 'comment_ID', - 'link_ID', - 'option_id', - 'blog_id', - 'option_name', - 'ID', - 'term_id', - 'object_id', - 'term_taxonomy_id', - 'umeta_id', - 'id', - ); - $unique_key = array( 'term_id', 'taxonomy', 'slug' ); - $data = array( - 'name' => '', // Column name. - 'table' => '', // Table name. - 'max_length' => 0, // Max length of the column. - 'not_null' => 1, // 1 if not null. - 'primary_key' => 0, // 1 if column has primary key. - 'unique_key' => 0, // 1 if column has unique key. - 'multiple_key' => 0, // 1 if column doesn't have unique key. - 'numeric' => 0, // 1 if column has numeric value. - 'blob' => 0, // 1 if column is blob. - 'type' => '', // Type of the column. - 'int' => 0, // 1 if column is int integer. - 'zerofill' => 0, // 1 if column is zero-filled. - ); - $table_name = ''; - $sql = ''; - $query = end( $this->executed_sqlite_queries ); - if ( $query ) { - $sql = $query['sql']; - } - if ( preg_match( '/\s*FROM\s*(.*)?\s*/i', $sql, $match ) ) { - $table_name = trim( $match[1] ); - } - foreach ( $this->results[0] as $key => $value ) { - $data['name'] = $key; - $data['table'] = $table_name; - if ( in_array( $key, $primary_key, true ) ) { - $data['primary_key'] = 1; - } elseif ( in_array( $key, $unique_key, true ) ) { - $data['unique_key'] = 1; - } else { - $data['multiple_key'] = 1; - } - $this->column_data[] = json_decode( json_encode( $data ) ); - - // Reset data for next iteration. - $data['name'] = ''; - $data['table'] = ''; - $data['primary_key'] = 0; - $data['unique_key'] = 0; - $data['multiple_key'] = 0; - } - - return $this->column_data; - } - return null; - } - - /** - * Method to return the queried result data. - * - * @return mixed - */ - public function get_query_results() { - return $this->results; - } - - /** - * Method to return the number of rows from the queried result. - */ - public function get_num_rows() { - return $this->num_rows; - } - - /** - * Method to return the queried results according to the query types. - * - * @return mixed - */ - public function get_return_value() { - return $this->return_value; - } - - /** - * Executes a MySQL query in SQLite. - * - * @param string $query The query. - * - * @throws Exception If the query is not supported. - */ - private function execute_mysql_query( $query ) { - $tokens = ( new WP_SQLite_Lexer( $query ) )->tokens; - - // SQLite does not support CURRENT_TIMESTAMP() calls with parentheses. - // Since CURRENT_TIMESTAMP() can appear in most types of SQL queries, - // let's remove the parentheses globally before further processing. - foreach ( $tokens as $i => $token ) { - if ( WP_SQLite_Token::TYPE_KEYWORD === $token->type && 'CURRENT_TIMESTAMP' === $token->keyword ) { - $paren_open = $tokens[ $i + 1 ] ?? null; - $paren_close = $tokens[ $i + 2 ] ?? null; - if ( WP_SQLite_Token::TYPE_OPERATOR === $paren_open->type && '(' === $paren_open->value - && WP_SQLite_Token::TYPE_OPERATOR === $paren_close->type && ')' === $paren_close->value ) { - unset( $tokens[ $i + 1 ], $tokens[ $i + 2 ] ); - } - } - } - $tokens = array_values( $tokens ); - - $this->rewriter = new WP_SQLite_Query_Rewriter( $tokens ); - $this->query_type = $this->rewriter->peek()->value; - - switch ( $this->query_type ) { - case 'ALTER': - $this->execute_alter(); - break; - - case 'CREATE': - $this->execute_create(); - break; - - case 'SELECT': - $this->execute_select(); - break; - - case 'INSERT': - case 'REPLACE': - $this->execute_insert_or_replace(); - break; - - case 'UPDATE': - $this->execute_update(); - break; - - case 'DELETE': - $this->execute_delete(); - break; - - case 'CALL': - case 'SET': - /* - * It would be lovely to support at least SET autocommit, - * but I don't think that is even possible with SQLite. - */ - $this->results = 0; - break; - - case 'TRUNCATE': - $this->execute_truncate(); - break; - - case 'BEGIN': - case 'START TRANSACTION': - $this->results = $this->begin_transaction(); - break; - - case 'COMMIT': - $this->results = $this->commit(); - break; - - case 'ROLLBACK': - $this->results = $this->rollback(); - break; - - case 'DROP': - $this->execute_drop(); - break; - - case 'SHOW': - $this->execute_show(); - break; - - case 'DESCRIBE': - $this->execute_describe(); - break; - - case 'CHECK': - $this->execute_check(); - break; - - case 'OPTIMIZE': - case 'REPAIR': - case 'ANALYZE': - $this->execute_optimize( $this->query_type ); - break; - - default: - throw new Exception( 'Unknown query type: ' . $this->query_type ); - } - } - - /** - * Executes a MySQL CREATE TABLE query in SQLite. - * - * @throws Exception If the query is not supported. - */ - private function execute_create_table() { - $table = $this->parse_create_table(); - - $definitions = array(); - $on_updates = 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 ); - if ( $field->on_update ) { - $on_updates[ $field->name ] = $field->on_update; - } - - $this->update_data_type_cache( - $table->name, - $field->name, - $field->mysql_data_type - ); - } - - if ( count( $table->primary_key ) > 1 ) { - $definitions[] = 'PRIMARY KEY (' . implode( ', ', array_map( array( $this, 'quote_identifier' ), $table->primary_key ) ) . ')'; - } - - $create_query = ( - $table->create_table . - $this->quote_identifier( $table->name ) . ' (' . "\n" . - implode( ",\n", $definitions ) . - ')' - ); - - $if_not_exists = preg_match( '/\bIF\s+NOT\s+EXISTS\b/i', $create_query ) ? 'IF NOT EXISTS' : ''; - - $this->execute_sqlite_query( $create_query ); - $this->results = $this->last_exec_returned; - $this->return_value = $this->results; - - foreach ( $table->constraints as $constraint ) { - $index_type = $this->mysql_index_type_to_sqlite_type( $constraint->value ); - $unique = ''; - if ( 'UNIQUE INDEX' === $index_type ) { - $unique = 'UNIQUE '; - } - $index_name = $this->generate_index_name( $table->name, $constraint->name ); - $this->execute_sqlite_query( - 'CREATE ' . $unique . 'INDEX ' . $if_not_exists . ' ' . $this->quote_identifier( $index_name ) . ' ON ' . $this->quote_identifier( $table->name ) . ' (' . implode( ', ', array_map( array( $this, 'quote_identifier' ), $constraint->columns ) ) . ')' - ); - $this->update_data_type_cache( - $table->name, - $index_name, - $constraint->value - ); - } - - foreach ( $on_updates as $column => $on_update ) { - $this->add_column_on_update_current_timestamp( $table->name, $column ); - } - } - - /** - * 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 - * alongside checking for the reserved keyword, we'll also - * check whether the second non-whitespace token is a data type. - * - * By checking for the reserved keyword, we can be sure that - * we're not parsing a constraint as a field when the - * constraint symbol matches a data type. - */ - $current_token = $this->rewriter->peek(); - $second_token = $this->rewriter->peek_nth( 2 ); - - if ( - $second_token->matches( - WP_SQLite_Token::TYPE_KEYWORD, - WP_SQLite_Token::FLAG_KEYWORD_DATA_TYPE - ) && ! $current_token->matches( - WP_SQLite_Token::TYPE_KEYWORD, - WP_SQLite_Token::FLAG_KEYWORD_RESERVED - ) - ) { - $result->fields[] = $this->parse_mysql_create_table_field(); - } else { - $result->constraints[] = $this->parse_mysql_create_table_constraint(); - } - - /* - * If we're back at the initial depth, we're done. - * Also, MySQL supports a trailing comma – if we see one, - * then we're also done. - */ - } while ( - $token - && $this->rewriter->depth >= $declarations_depth - && $this->rewriter->peek()->token !== ')' - ); - - // Merge all the definitions of the primary key. - foreach ( $result->constraints as $k => $constraint ) { - if ( 'PRIMARY' === $constraint->value ) { - $result->primary_key = array_merge( - $result->primary_key, - $constraint->columns - ); - unset( $result->constraints[ $k ] ); - } - } - - // Inline primary key in a field definition. - foreach ( $result->fields as $k => $field ) { - if ( $field->primary_key ) { - $result->primary_key[] = $field->name; - } elseif ( in_array( $field->name, $result->primary_key, true ) ) { - $field->primary_key = true; - } - } - - // Remove duplicates. - $result->primary_key = array_unique( $result->primary_key ); - - return $result; - } - - /** - * Parses a CREATE TABLE query. - * - * @throws Exception If the query is not supported. - * - * @return stdClass - */ - private function parse_mysql_create_table_field() { - $result = new stdClass(); - $result->name = ''; - $result->sqlite_data_type = ''; - $result->not_null = false; - $result->default = false; - $result->auto_increment = false; - $result->primary_key = false; - $result->on_update = 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, PRIMARY KEY, DEFAULT, 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' ) - ) ) { - // Consume the next token (could be a value, opening paren, etc.) - $default_token = $this->rewriter->consume(); - $result->default = $default_token->token; - - // Check if the default value is wrapped in parentheses (for function calls like (now())) - if ( $default_token->matches( WP_SQLite_Token::TYPE_OPERATOR, null, array( '(' ) ) ) { - // Track parenthesis depth to consume the complete expression - $paren_depth = 1; - $default_value = '('; - - while ( $paren_depth > 0 && ( $next_token = $this->rewriter->consume() ) ) { - $default_value .= $next_token->token; - - if ( $next_token->matches( WP_SQLite_Token::TYPE_OPERATOR, null, array( '(' ) ) ) { - ++$paren_depth; - } elseif ( $next_token->matches( WP_SQLite_Token::TYPE_OPERATOR, null, array( ')' ) ) ) { - --$paren_depth; - } - } - - $result->default = $default_value; - } - continue; - } - - if ( - $token->matches( - WP_SQLite_Token::TYPE_KEYWORD, - WP_SQLite_Token::FLAG_KEYWORD_RESERVED, - array( 'ON UPDATE' ) - ) && $this->rewriter->peek()->matches( - WP_SQLite_Token::TYPE_KEYWORD, - WP_SQLite_Token::FLAG_KEYWORD_RESERVED, - array( 'CURRENT_TIMESTAMP' ) - ) - ) { - $this->rewriter->skip(); - $result->on_update = true; - 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 = $this->quote_identifier( $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'; - } - /** - * 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'; - } elseif ( false !== $field->default ) { - $definition .= ' DEFAULT ' . $field->default; - } 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 ) { - $definition .= ' DEFAULT \'\''; - } elseif ( in_array( $field->sqlite_data_type, array( 'integer', 'real' ), true ) ) { - $definition .= ' DEFAULT 0'; - } - } - - /* - * 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. - - $name = $this->rewriter->peek(); - if ( '(' !== $name->token && '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 ); - - if ( empty( $result->name ) ) { - $result->name = implode( '_', $result->columns ); - } - } - - do { - $token = $this->rewriter->skip(); - } while ( ! $this->is_create_table_field_terminator( $token, $definition_depth ) ); - - return $result; - } - - /** - * Checks if the current token is the terminator of a CREATE TABLE field. - * - * @param WP_SQLite_Token $token The current token. - * @param int $definition_depth The initial depth. - * @param int|null $current_depth The current depth. - * - * @return bool - */ - private function is_create_table_field_terminator( $token, $definition_depth, $current_depth = null ) { - if ( null === $current_depth ) { - $current_depth = $this->rewriter->depth; - } - return ( - // Reached the end of the query. - null === $token - - // The field-terminating ",". - || ( - $current_depth === $definition_depth && - WP_SQLite_Token::TYPE_OPERATOR === $token->type && - ',' === $token->value - ) - - // The definitions-terminating ")". - || $current_depth === $definition_depth - 1 - - // The query-terminating ";". - || ( - WP_SQLite_Token::TYPE_DELIMITER === $token->type && - ';' === $token->value - ) - ); - } - - /** - * Executes a DELETE statement. - * - * @throws Exception If the table could not be found. - */ - private function execute_delete() { - $this->rewriter->consume(); // DELETE. - - // Process expressions and extract bound parameters. - $params = array(); - while ( true ) { - $token = $this->rewriter->peek(); - if ( ! $token ) { - break; - } - - $this->remember_last_reserved_keyword( $token ); - - if ( - $this->extract_bound_parameter( $token, $params ) - || $this->translate_expression( $token ) - ) { - continue; - } - - $this->rewriter->consume(); - } - $this->rewriter->consume_all(); - - $updated_query = $this->rewriter->get_updated_query(); - - // Perform DELETE-specific translations. - - // Naive rewriting of DELETE JOIN query. - // @TODO: Actually rewrite the query instead of using a hardcoded workaround. - if ( str_contains( $updated_query, ' JOIN ' ) ) { - $table_prefix = isset( $GLOBALS['table_prefix'] ) ? $GLOBALS['table_prefix'] : 'wp_'; - $quoted_table = $this->quote_identifier( $table_prefix . 'options' ); - $this->execute_sqlite_query( - "DELETE FROM $quoted_table WHERE option_id IN (SELECT MIN(option_id) FROM $quoted_table GROUP BY option_name HAVING COUNT(*) > 1)" - ); - $this->set_result_from_affected_rows(); - return; - } - - $rewriter = new WP_SQLite_Query_Rewriter( $this->rewriter->output_tokens ); - - $comma = $rewriter->peek( - array( - 'type' => WP_SQLite_Token::TYPE_OPERATOR, - 'value' => ',', - ) - ); - $from = $rewriter->peek( - array( - 'type' => WP_SQLite_Token::TYPE_KEYWORD, - 'value' => 'FROM', - ) - ); - // The DELETE query targets a single table if there's no comma before the FROM. - if ( ! $comma || ! $from || $comma->position >= $from->position ) { - $this->execute_sqlite_query( - $updated_query, - $params - ); - $this->set_result_from_affected_rows(); - return; - } - - // The DELETE query targets multiple tables – rewrite it into a - // SELECT to fetch the IDs of the rows to delete, then delete them - // using a separate DELETE query. - - $this->table_name = $this->normalize_column_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() ) { - $this->table_name = $this->normalize_column_name( $rewriter->input_tokens[ $i ]->value ); - break; - } - } - if ( ! $this->table_name ) { - throw new Exception( 'Could not find table name for dual delete query.' ); - } - - /* - * Now, let's figure out the primary key name. - * This assumes that all listed table names are the same. - */ - $q = $this->execute_sqlite_query( 'SELECT l.name FROM pragma_table_info(' . $this->pdo->quote( $this->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( $this->quote_identifier( $pk_name ), WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ), - new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), - new WP_SQLite_Token( 'AS', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), - new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), - new WP_SQLite_Token( 'id_' . $alias_nb, WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ), - ) - ); - ++$alias_nb; - } - } - $rewriter->consume_all(); - - // Select the IDs to delete. - $select = $rewriter->get_updated_query(); - $stmt = $this->execute_sqlite_query( $select ); - $stmt->execute( $params ); - $rows = $stmt->fetchAll(); - $ids_to_delete = array(); - foreach ( $rows as $id ) { - $ids_to_delete[] = $id['id_0']; - $ids_to_delete[] = $id['id_1']; - } - - $quoted_table = $this->quote_identifier( $this->table_name ); - $quoted_pk = $this->quote_identifier( $pk_name ); - if ( count( $ids_to_delete ) ) { - $placeholders = implode( ',', array_fill( 0, count( $ids_to_delete ), '?' ) ); - $stmt = $this->execute_sqlite_query( "DELETE FROM {$quoted_table} WHERE {$quoted_pk} IN ({$placeholders})" ); - $stmt->execute( $ids_to_delete ); - } else { - $this->execute_sqlite_query( "DELETE FROM {$quoted_table} WHERE 0=1" ); - } - $this->set_result_from_affected_rows( - count( $ids_to_delete ) - ); - } - - /** - * Executes a SELECT statement. - */ - private function execute_select() { - $this->rewriter->consume(); // SELECT. - - $params = array(); - $table_name = null; - $has_sql_calc_found_rows = false; - - // Consume and record the table name. - while ( true ) { - $token = $this->rewriter->peek(); - if ( ! $token ) { - break; - } - - $this->remember_last_reserved_keyword( $token ); - - if ( ! $table_name ) { - $this->table_name = $this->peek_table_name( $token ); - $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; - } - - if ( $this->skip_index_hint() ) { - continue; - } - - $this->rewriter->consume(); - } - $this->rewriter->consume_all(); - - $updated_query = $this->rewriter->get_updated_query(); - - if ( $table_name && str_starts_with( strtolower( $table_name ), 'information_schema' ) ) { - $this->is_information_schema_query = true; - - $database_name = $this->pdo->quote( defined( 'DB_NAME' ) ? DB_NAME : '' ); - $updated_query = preg_replace( - '/' . $table_name . '\.tables/i', - /** - * TODO: Return real values for hardcoded column values. - */ - "(SELECT - 'def' as TABLE_CATALOG, - $database_name as TABLE_SCHEMA, - name as TABLE_NAME, - CASE type - WHEN 'table' THEN 'BASE TABLE' - WHEN 'view' THEN 'VIEW' - ELSE type - END as TABLE_TYPE, - 'InnoDB' as ENGINE, - 10 as VERSION, - 'Dynamic' as ROW_FORMAT, - 0 as TABLE_ROWS, - 0 as AVG_ROW_LENGTH, - 0 as DATA_LENGTH, - 0 as MAX_DATA_LENGTH, - 0 as INDEX_LENGTH, - 0 as DATA_FREE, - NULL as AUTO_INCREMENT, - NULL as CREATE_TIME, - NULL as UPDATE_TIME, - NULL as CHECK_TIME, - 'utf8mb4_general_ci' as TABLE_COLLATION, - NULL as CHECKSUM, - '' as CREATE_OPTIONS, - '' as TABLE_COMMENT - FROM sqlite_master - WHERE type IN ('table', 'view'))", - $updated_query - ); - } elseif ( - // 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, - * return a dummy select. This check must be done after the query - * has been rewritten to use parameters to avoid false positives - * on queries such as `SELECT * FROM table WHERE field='CONVERT('`. - */ - $updated_query = 'SELECT 1=0'; - $params = array(); - } elseif ( $has_sql_calc_found_rows ) { - // Emulate SQL_CALC_FOUND_ROWS for now. - $query = $updated_query; - // We make the data for next SELECT FOUND_ROWS() statement. - $unlimited_query = preg_replace( '/\\bLIMIT\\s\d+(?:\s*,\s*\d+)?$/imsx', '', $query ); - $stmt = $this->execute_sqlite_query( $unlimited_query ); - $stmt->execute( $params ); - $this->last_sql_calc_found_rows = count( $stmt->fetchAll() ); - } - - // Emulate FOUND_ROWS() by counting the rows in the result set. - if ( strpos( $updated_query, 'FOUND_ROWS(' ) !== false ) { - $last_found_rows = ( $this->last_sql_calc_found_rows ? $this->last_sql_calc_found_rows : 0 ) . ''; - $updated_query = "SELECT {$last_found_rows} AS `FOUND_ROWS()`"; - } - - $stmt = $this->execute_sqlite_query( $updated_query, $params ); - if ( $this->is_information_schema_query ) { - $this->set_results_from_fetched_data( - $this->strip_sqlite_system_tables( - $stmt->fetchAll( $this->pdo_fetch_mode ) - ) - ); - } else { - $this->set_results_from_fetched_data( - $stmt->fetchAll( $this->pdo_fetch_mode ) - ); - } - } - - /** - * 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. - */ - private function execute_truncate() { - $this->rewriter->skip(); // TRUNCATE. - 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 ) ); - $this->rewriter->consume_all(); - $this->execute_sqlite_query( $this->rewriter->get_updated_query() ); - $this->results = true; - $this->return_value = true; - } - - /** - * Executes a DESCRIBE statement. - * - * @throws PDOException When the table is not found. - */ - private function execute_describe() { - $this->rewriter->skip(); - $this->table_name = $this->normalize_column_name( $this->rewriter->consume()->value ); - $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`, - ( - CASE `notnull` - WHEN 0 THEN 'YES' - WHEN 1 THEN 'NO' - END - ) as `Null`, - COALESCE( - 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(" . $this->pdo->quote( $table_name ) . ') p - LEFT JOIN ' . self::DATA_TYPES_CACHE_TABLE . ' d - ON d.`table` = ' . $this->pdo->quote( $table_name ) . ' - AND d.`column_or_index` = p.`name` - ; - ' - ) - ->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(); // 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 && - ! $token->matches( - WP_SQLite_Token::TYPE_KEYWORD, - WP_SQLite_Token::FLAG_KEYWORD_RESERVED - ) - ) { - $this->table_name = $this->normalize_column_name( $token->value ); - } - - $this->remember_last_reserved_keyword( $token ); - - if ( - $this->extract_bound_parameter( $token, $params ) - || $this->translate_expression( $token ) - ) { - continue; - } - - $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(); - $this->execute_sqlite_query( $updated_query, $params ); - $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->quote_identifier( $this->table_name ), WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ), - new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), - ) - ); - } - - /** - * Executes a INSERT or REPLACE statement. - */ - private function execute_insert_or_replace() { - $params = array(); - $is_in_duplicate_section = false; - - $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. - $this->table_name = $this->normalize_column_name( $this->rewriter->consume()->value ); // Table name. - - /* - * A list of columns is given if the opening parenthesis - * is earlier than the VALUES keyword. - */ - $paren = $this->rewriter->peek( - array( - 'type' => WP_SQLite_Token::TYPE_OPERATOR, - 'value' => '(', - ) - ); - $values = $this->rewriter->peek( - array( - 'type' => WP_SQLite_Token::TYPE_KEYWORD, - 'value' => 'VALUES', - ) - ); - if ( $paren && $values && $paren->position <= $values->position ) { - $this->rewriter->consume( - array( - 'type' => WP_SQLite_Token::TYPE_OPERATOR, - 'value' => '(', - ) - ); - while ( true ) { - $token = $this->rewriter->consume(); - if ( $token->matches( WP_SQLite_Token::TYPE_OPERATOR, null, array( ')' ) ) ) { - break; - } - if ( ! $token->matches( WP_SQLite_Token::TYPE_OPERATOR ) ) { - $this->insert_columns[] = $token->value; - } - } - } - - while ( true ) { - $token = $this->rewriter->peek(); - if ( ! $token ) { - break; - } - - $this->remember_last_reserved_keyword( $token ); - - if ( - ( $is_in_duplicate_section && $this->translate_values_function( $token ) ) - || $this->extract_bound_parameter( $token, $params ) - || $this->translate_expression( $token ) - ) { - continue; - } - - if ( $token->matches( - WP_SQLite_Token::TYPE_KEYWORD, - null, - array( 'DUPLICATE' ) - ) - ) { - $is_in_duplicate_section = true; - $this->translate_on_duplicate_key( $this->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; - } - - if ( function_exists( 'apply_filters' ) ) { - $this->last_insert_id = apply_filters( 'sqlite_last_insert_id', $this->last_insert_id, $this->table_name ); - } - } - - /** - * Preprocesses a string literal. - * - * @param string $value The string literal. - * - * @return string The preprocessed string literal. - */ - private function preprocess_string_literal( $value ) { - /* - * The code below converts the date format to one preferred by SQLite. - * - * MySQL accepts ISO 8601 date strings: 'YYYY-MM-DDTHH:MM:SSZ' - * SQLite prefers a slightly different format: 'YYYY-MM-DD HH:MM:SS' - * - * SQLite date and time functions can understand the ISO 8601 notation, but - * lookups don't. To keep the lookups working, we need to store all dates - * in UTC without the "T" and "Z" characters. - * - * Caveat: It will adjust every string that matches the pattern, not just dates. - * - * In theory, we could only adjust semantic dates, e.g. the data inserted - * to a date column or compared against a date column. - * - * In practice, this is hard because dates are just text – SQLite has no separate - * datetime field. We'd need to cache the MySQL data type from the original - * CREATE TABLE query and then keep refreshing the cache after each ALTER TABLE query. - * - * That's a lot of complexity that's perhaps not worth it. Let's just convert - * everything for now. The regexp assumes "Z" is always at the end of the string, - * which is true in the unit test suite, but there could also be a timezone offset - * like "+00:00" or "+01:00". We could add support for that later if needed. - */ - if ( 1 === preg_match( '/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})Z$/', $value, $matches ) ) { - $value = $matches[1] . ' ' . $matches[2]; - } - - /* - * Mimic MySQL's behavior and truncate invalid dates. - * - * "2020-12-41 14:15:27" becomes "0000-00-00 00:00:00" - * - * WARNING: We have no idea whether the truncated value should - * be treated as a date in the first place. - * In SQLite dates are just strings. This could be a perfectly - * valid string that just happens to contain a date-like value. - * - * At the same time, WordPress seems to rely on MySQL's behavior - * and even tests for it in Tests_Post_wpInsertPost::test_insert_empty_post_date. - * Let's truncate the dates for now. - * - * In the future, let's update WordPress to do its own date validation - * and stop relying on this MySQL feature, - */ - if ( 1 === preg_match( '/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})$/', $value, $matches ) ) { - /* - * Calling strtotime("0000-00-00 00:00:00") in 32-bit environments triggers - * an "out of integer range" warning – let's avoid that call for the popular - * case of "zero" dates. - */ - if ( '0000-00-00 00:00:00' !== $value && false === strtotime( $value ) ) { - $value = '0000-00-00 00:00:00'; - } - } - return $value; - } - - /** - * Preprocesses a LIKE expression. - * - * @param WP_SQLite_Token $token The token to preprocess. - * @return string - */ - private function preprocess_like_expr( &$token ) { - /* - * This code handles escaped wildcards in LIKE clauses. - * If we are within a LIKE experession, we look for \_ and \%, the - * escaped LIKE wildcards, the ones where we want a literal, not a - * wildcard match. We change the \ escape for an ASCII \x1a (SUB) character, - * so the \ characters won't get munged. - * These \_ and \% escape sequences are in the token name, because - * the lexer has already done stripcslashes on the value. - */ - if ( $this->like_expression_nesting > 0 ) { - /* Remove the quotes around the name. */ - $unescaped_value = mb_substr( $token->token, 1, -1, 'UTF-8' ); - if ( str_contains( $unescaped_value, '\_' ) || str_contains( $unescaped_value, '\%' ) ) { - ++$this->like_escape_count; - return str_replace( - array( '\_', '\%' ), - array( self::LIKE_ESCAPE_CHAR . '_', self::LIKE_ESCAPE_CHAR . '%' ), - $unescaped_value - ); - } - } - return $token->value; - } - /** - * Translate CAST() function when we want to cast to BINARY. - * - * @param WP_SQLite_Token $token The token to translate. - * - * @return bool - */ - private function translate_cast_as_binary( $token ) { - if ( ! $token->matches( - WP_SQLite_Token::TYPE_KEYWORD, - WP_SQLite_Token::FLAG_KEYWORD_DATA_TYPE, - array( 'BINARY' ) - ) - ) { - return false; - } - - $call_parent = $this->rewriter->last_call_stack_element(); - if ( - ! $call_parent - || 'CAST' !== $call_parent['function'] - ) { - return false; - } - - // Rewrite AS BINARY to AS BLOB inside CAST() calls. - $this->rewriter->skip(); - $this->rewriter->add( new WP_SQLite_Token( 'BLOB', $token->type, $token->flags ) ); - return true; - } - - /** - * Translates an expression in an SQL statement if the token is the start of an expression. - * - * @param WP_SQLite_Token $token The first token of an expression. - * - * @return bool True if the expression was translated successfully, false otherwise. - */ - private function translate_expression( $token ) { - return ( - $this->skip_from_dual( $token ) - || $this->translate_concat_function( $token ) - || $this->translate_concat_comma_to_pipes( $token ) - || $this->translate_function_aliases( $token ) - || $this->translate_cast_as_binary( $token ) - || $this->translate_date_add_sub( $token ) - || $this->translate_date_format( $token ) - || $this->translate_interval( $token ) - || $this->translate_regexp_functions( $token ) - || $this->capture_group_by( $token ) - || $this->translate_ungrouped_having( $token ) - || $this->translate_like_binary( $token ) - || $this->translate_like_escape( $token ) - || $this->translate_left_function( $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->normalize_column_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 ) - || 'AS' === $this->last_reserved_keyword - ) { - return false; - } - - $param_name = ':param' . count( $params ); - $value = $this->preprocess_like_expr( $token ); - $value = $this->preprocess_string_literal( $value ); - $params[ $param_name ] = $value; - $this->rewriter->skip(); - $this->rewriter->add( new WP_SQLite_Token( $param_name, WP_SQLite_Token::TYPE_STRING, WP_SQLite_Token::FLAG_STRING_SINGLE_QUOTES ) ); - $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); - return true; - } - - /** - * Translate CONCAT() function. - * - * @param WP_SQLite_Token $token The token to translate. - * - * @return bool - */ - private function translate_concat_function( $token ) { - if ( - ! $token->matches( - WP_SQLite_Token::TYPE_KEYWORD, - WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, - array( 'CONCAT' ) - ) - ) { - return false; - } - - /* - * Skip the CONCAT function but leave the parentheses. - * There is another code block below that replaces the - * , operators between the CONCAT arguments with ||. - */ - $this->rewriter->skip(); - return true; - } - - /** - * Translate CONCAT() function arguments. - * - * @param WP_SQLite_Token $token The token to translate. - * - * @return bool - */ - private function translate_concat_comma_to_pipes( $token ) { - if ( ! $token->matches( - WP_SQLite_Token::TYPE_OPERATOR, - WP_SQLite_Token::FLAG_OPERATOR_SQL, - array( ',' ) - ) - ) { - return false; - } - - $call_parent = $this->rewriter->last_call_stack_element(); - if ( - ! $call_parent - || 'CONCAT' !== $call_parent['function'] - ) { - return false; - } - - // Rewrite commas to || in CONCAT() calls. - $this->rewriter->skip(); - $this->rewriter->add( new WP_SQLite_Token( '||', WP_SQLite_Token::TYPE_OPERATOR ) ); - return true; - } - - /** - * Translate DATE_ADD() and DATE_SUB() functions. - * - * @param WP_SQLite_Token $token The token to translate. - * - * @return bool - */ - private function translate_date_add_sub( $token ) { - if ( - ! $token->matches( - WP_SQLite_Token::TYPE_KEYWORD, - WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, - array( 'DATE_ADD', 'DATE_SUB' ) - ) - ) { - return false; - } - - $this->rewriter->skip(); - $this->rewriter->add( new WP_SQLite_Token( 'DATETIME', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ) ); - return true; - } - - /** - * 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. - * - * @param object $token The current token. - * - * @return bool False when no match, true when this function consumes the token. - * - * @todo LENGTH and CHAR_LENGTH aren't always the same in MySQL for utf8 characters. They are in SQLite. - */ - private function translate_function_aliases( $token ) { - if ( ! $token->matches( - WP_SQLite_Token::TYPE_KEYWORD, - WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, - array( 'SUBSTRING', 'CHAR_LENGTH' ) - ) - ) { - return false; - } - switch ( $token->value ) { - case 'SUBSTRING': - $name = 'SUBSTR'; - break; - case 'CHAR_LENGTH': - $name = 'LENGTH'; - break; - default: - $name = $token->value; - break; - } - $this->rewriter->skip(); - $this->rewriter->add( new WP_SQLite_Token( $name, $token->type, $token->flags ) ); - - return true; - } - - /** - * Translate VALUES() function. - * - * @param WP_SQLite_Token $token The token to translate. - * - * @return bool - */ - private function translate_values_function( $token ) { - if ( - ! $token->matches( - WP_SQLite_Token::TYPE_KEYWORD, - WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, - array( 'VALUES' ) - ) - ) { - return false; - } - - /* - * Rewrite: VALUES(`option_name`) - * to: excluded.option_name - */ - $this->rewriter->skip(); - $this->rewriter->add( new WP_SQLite_Token( 'excluded', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ) ); - $this->rewriter->add( new WP_SQLite_Token( '.', WP_SQLite_Token::TYPE_OPERATOR ) ); - - $this->rewriter->skip(); // Skip the opening `(`. - // Consume the column name. - $this->rewriter->consume( - array( - 'type' => WP_SQLite_Token::TYPE_OPERATOR, - 'value' => ')', - ) - ); - // Drop the consumed ')' token. - $this->rewriter->drop_last(); - return true; - } - - /** - * Translate DATE_FORMAT() function. - * - * @param WP_SQLite_Token $token The token to translate. - * - * @throws Exception If the token is not a DATE_FORMAT() function. - * - * @return bool - */ - private function translate_date_format( $token ) { - if ( - ! $token->matches( - WP_SQLite_Token::TYPE_KEYWORD, - WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, - array( 'DATE_FORMAT' ) - ) - ) { - return false; - } - - // Rewrite DATE_FORMAT( `post_date`, '%Y-%m-%d' ) to STRFTIME( '%Y-%m-%d', `post_date` ). - - // Skip the DATE_FORMAT function name. - $this->rewriter->skip(); - // Skip the opening `(`. - $this->rewriter->skip(); - - // Skip the first argument so we can read the second one. - $first_arg = $this->rewriter->skip_and_return_all( - array( - 'type' => WP_SQLite_Token::TYPE_OPERATOR, - 'value' => ',', - ) - ); - - // Make sure we actually found the comma. - $comma = array_pop( $first_arg ); - if ( ',' !== $comma->value ) { - throw new Exception( 'Could not parse the DATE_FORMAT() call' ); - } - - // Skip the second argument but capture the token. - $format = $this->rewriter->skip()->value; - $new_format = strtr( $format, $this->mysql_date_format_to_sqlite_strftime ); - if ( ! $new_format ) { - throw new Exception( "Could not translate a DATE_FORMAT() format to STRFTIME format ($format)" ); - } - - /* - * MySQL supports comparing strings and floats, e.g. - * - * > SELECT '00.42' = 0.4200 - * 1 - * - * SQLite does not support that. At the same time, - * WordPress likes to filter dates by comparing numeric - * outputs of DATE_FORMAT() to floats, e.g.: - * - * -- Filter by hour and minutes - * DATE_FORMAT( - * STR_TO_DATE('2014-10-21 00:42:29', '%Y-%m-%d %H:%i:%s'), - * '%H.%i' - * ) = 0.4200; - * - * Let's cast the STRFTIME() output to a float if - * the date format is typically used for string - * to float comparisons. - * - * In the future, let's update WordPress to avoid comparing - * strings and floats. - */ - $cast_to_float = '%H.%i' === $format; - if ( $cast_to_float ) { - $this->rewriter->add( new WP_SQLite_Token( 'CAST', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ) ); - $this->rewriter->add( new WP_SQLite_Token( '(', WP_SQLite_Token::TYPE_OPERATOR ) ); - } - - $this->rewriter->add( new WP_SQLite_Token( 'STRFTIME', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ) ); - $this->rewriter->add( new WP_SQLite_Token( '(', WP_SQLite_Token::TYPE_OPERATOR ) ); - $this->rewriter->add( new WP_SQLite_Token( $this->pdo->quote( $new_format ), WP_SQLite_Token::TYPE_STRING ) ); - $this->rewriter->add( new WP_SQLite_Token( ',', WP_SQLite_Token::TYPE_OPERATOR ) ); - - // Add the buffered tokens back to the stream. - $this->rewriter->add_many( $first_arg ); - - // Consume the closing ')'. - $this->rewriter->consume( - array( - 'type' => WP_SQLite_Token::TYPE_OPERATOR, - 'value' => ')', - ) - ); - - if ( $cast_to_float ) { - $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); - $this->rewriter->add( new WP_SQLite_Token( 'as', WP_SQLite_Token::TYPE_OPERATOR ) ); - $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); - $this->rewriter->add( new WP_SQLite_Token( 'FLOAT', WP_SQLite_Token::TYPE_KEYWORD ) ); - $this->rewriter->add( new WP_SQLite_Token( ')', WP_SQLite_Token::TYPE_OPERATOR ) ); - } - - return true; - } - - /** - * Translate INTERVAL keyword with DATE_ADD() and DATE_SUB(). - * - * @param WP_SQLite_Token $token The token to translate. - * - * @return bool - */ - private function translate_interval( $token ) { - if ( - ! $token->matches( - WP_SQLite_Token::TYPE_KEYWORD, - null, - array( 'INTERVAL' ) - ) - ) { - return false; - } - // Skip the INTERVAL keyword from the output stream. - $this->rewriter->skip(); - - $num = $this->rewriter->skip()->value; - $unit = $this->rewriter->skip()->value; - - /* - * In MySQL, we say: - * DATE_ADD(d, INTERVAL 1 YEAR) - * DATE_SUB(d, INTERVAL 1 YEAR) - * - * In SQLite, we say: - * DATE(d, '+1 YEAR') - * DATE(d, '-1 YEAR') - * - * The sign of the interval is determined by the date_* function - * that is closest in the call stack. - * - * Let's find it. - */ - $interval_op = '+'; // Default to adding. - for ( $j = count( $this->rewriter->call_stack ) - 1; $j >= 0; $j-- ) { - $call = $this->rewriter->call_stack[ $j ]; - if ( 'DATE_ADD' === $call['function'] ) { - $interval_op = '+'; - break; - } - if ( 'DATE_SUB' === $call['function'] ) { - $interval_op = '-'; - break; - } - } - - $this->rewriter->add( new WP_SQLite_Token( $this->pdo->quote( "{$interval_op}$num $unit" ), WP_SQLite_Token::TYPE_STRING ) ); - return true; - } - - /** - * Translate REGEXP and RLIKE keywords. - * - * @param WP_SQLite_Token $token The token to translate. - * - * @return bool - */ - private function translate_regexp_functions( $token ) { - if ( - ! $token->matches( - WP_SQLite_Token::TYPE_KEYWORD, - null, - array( 'REGEXP', 'RLIKE' ) - ) - ) { - return false; - } - $this->rewriter->skip(); - $this->rewriter->add( new WP_SQLite_Token( 'REGEXP', WP_SQLite_Token::TYPE_KEYWORD ) ); - - $next = $this->rewriter->peek(); - - /* - * If the query says REGEXP BINARY, the comparison is byte-by-byte - * and letter casing matters – lowercase and uppercase letters are - * represented using different byte codes. - * - * The REGEXP function can't be easily made to accept two - * parameters, so we'll have to use a hack to get around this. - * - * If the first character of the pattern is a null byte, we'll - * remove it and make the comparison case-sensitive. This should - * be reasonably safe since PHP does not allow null bytes in - * regular expressions anyway. - */ - if ( $next->matches( WP_SQLite_Token::TYPE_KEYWORD, null, array( 'BINARY' ) ) ) { - // Skip the "BINARY" keyword. - $this->rewriter->skip(); - // Prepend a null byte to the pattern. - $this->rewriter->add_many( - array( - new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), - new WP_SQLite_Token( 'char', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ), - new WP_SQLite_Token( '(', WP_SQLite_Token::TYPE_OPERATOR ), - new WP_SQLite_Token( '0', WP_SQLite_Token::TYPE_NUMBER ), - new WP_SQLite_Token( ')', WP_SQLite_Token::TYPE_OPERATOR ), - new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), - new WP_SQLite_Token( '||', WP_SQLite_Token::TYPE_OPERATOR ), - new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), - ) - ); - } - return true; - } - /** - * Translate LIKE BINARY to SQLite equivalent using GLOB. - * - * @param WP_SQLite_Token $token The token to translate. - * - * @return bool - */ - private function translate_like_binary( $token ): bool { - if ( ! $token->matches( WP_SQLite_Token::TYPE_KEYWORD, null, array( 'LIKE' ) ) ) { - return false; - } - - $next = $this->rewriter->peek_nth( 2 ); - if ( ! $next || ! $next->matches( WP_SQLite_Token::TYPE_KEYWORD, null, array( 'BINARY' ) ) ) { - return false; - } - - $this->rewriter->skip(); // Skip 'LIKE' - $this->rewriter->skip(); // Skip 'BINARY' - - $pattern_token = $this->rewriter->peek(); - $this->rewriter->skip(); // Skip the pattern token - - $this->rewriter->add( new WP_SQLite_Token( 'GLOB', WP_SQLite_Token::TYPE_KEYWORD ) ); - $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); - - $escaped_pattern = $this->escape_like_to_glob( $pattern_token->value ); - $this->rewriter->add( new WP_SQLite_Token( $escaped_pattern, WP_SQLite_Token::TYPE_STRING ) ); - $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); - - return true; - } - - /** - * Escape LIKE pattern to GLOB pattern. - * - * @param string $pattern The LIKE pattern. - * @return string The escaped GLOB pattern. - */ - private function escape_like_to_glob( $pattern ) { - $pattern = str_replace( '%', '*', $pattern ); - $pattern = str_replace( '_', '?', $pattern ); - return $this->pdo->quote( $pattern ); - } - - /** - * 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 BY' ) - ) - ) { - return false; - } - - $this->has_group_by = true; - - return false; - } - - /** - * Translate HAVING without GROUP BY to GROUP BY 1 HAVING. - * - * @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; - } - - // GROUP BY is missing, add "GROUP BY 1" before the HAVING clause. - $having = $this->rewriter->skip(); - $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_DELIMITER ) ); - $this->rewriter->add( new WP_SQLite_Token( 'GROUP BY', WP_SQLite_Token::TYPE_KEYWORD ) ); - $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_DELIMITER ) ); - $this->rewriter->add( new WP_SQLite_Token( '1', WP_SQLite_Token::TYPE_NUMBER ) ); - $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_DELIMITER ) ); - $this->rewriter->add( $having ); - - 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; - } - - /** - * 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 ) { - /** - * By default, we assume the table name is in the result set, - * so we allow empty table names to pass through. - * Otherwise, if an information_schema table uses a custom name - * for the name/table_name column, the table would be removed. - */ - $table_name = ''; - $table = (array) $table; - if ( isset( $table['Name'] ) ) { - $table_name = $table['Name']; - } elseif ( isset( $table['table_name'] ) ) { - $table_name = $table['table_name']; - } elseif ( isset( $table['TABLE_NAME'] ) ) { - $table_name = $table['TABLE_NAME']; - } - return '' === $table_name || ! array_key_exists( $table_name, $this->sqlite_system_tables ); - }, - ARRAY_FILTER_USE_BOTH - ) - ); - } - - /** - * Translate the ON DUPLICATE KEY UPDATE clause. - * - * @param string $table_name The table name. - * - * @return void - */ - private function translate_on_duplicate_key( $table_name ) { - /* - * Rewrite: - * ON DUPLICATE KEY UPDATE `option_name` = VALUES(`option_name`) - * to: - * ON CONFLICT(ip) DO UPDATE SET option_name = excluded.option_name - */ - - // Find the conflicting column. - $pk_columns = array(); - foreach ( $this->get_primary_keys( $table_name ) as $row ) { - $pk_columns[] = $row['name']; - } - - $unique_columns = array(); - foreach ( $this->get_keys( $table_name, true ) as $row ) { - foreach ( $row['columns'] as $column ) { - $unique_columns[] = $column['name']; - } - } - - // Guess the conflict column based on the query details. - - // 1. Listed INSERT columns that are either PK or UNIQUE. - $conflict_columns = array_intersect( - $this->insert_columns, - array_merge( $pk_columns, $unique_columns ) - ); - // 2. Composite Primary Key columns. - if ( ! $conflict_columns && count( $pk_columns ) > 1 ) { - $conflict_columns = $pk_columns; - } - // 3. The first unique column. - if ( ! $conflict_columns && count( $unique_columns ) > 0 ) { - $conflict_columns = array( $unique_columns[0] ); - } - // 4. Regular Primary Key column. - if ( ! $conflict_columns ) { - $conflict_columns = $pk_columns; - } - - /* - * If we still haven't found any conflict column, we - * can't rewrite the ON DUPLICATE KEY statement. - * Let's default to a regular INSERT to mimic MySQL - * which would still insert the row without throwing - * an error. - */ - if ( ! $conflict_columns ) { - // Drop the consumed "ON". - $this->rewriter->drop_last(); - // Skip over "DUPLICATE", "KEY", and "UPDATE". - $this->rewriter->skip(); - $this->rewriter->skip(); - $this->rewriter->skip(); - while ( $this->rewriter->skip() ) { - // Skip over the rest of the query. - } - return; - } - - // Skip over "DUPLICATE", "KEY", and "UPDATE". - $this->rewriter->skip(); - $this->rewriter->skip(); - $this->rewriter->skip(); - - // Add the CONFLICT keyword. - $this->rewriter->add( new WP_SQLite_Token( 'CONFLICT', WP_SQLite_Token::TYPE_KEYWORD ) ); - - // Add "(MySQL query:
' . PHP_EOL; - $output .= '' . $this->mysql_query . '
' . PHP_EOL; - $output .= 'Queries made or created this session were:
' . PHP_EOL; - $output .= 'Backtrace:
' . PHP_EOL; - $output .= '' . $e->getTraceAsString() . '' . PHP_EOL; - } - - return $output; - } - - /** - * Executes a query in SQLite. - * - * @param mixed $sql The query to execute. - * @param mixed $params The parameters to bind to the query. - * @throws PDOException If the query could not be executed. - * @return object { - * The result of the query. - * - * @type PDOStatement $stmt The executed statement - * @type * $result The value returned by $stmt. - * } - */ - public function execute_sqlite_query( $sql, $params = array() ) { - $this->executed_sqlite_queries[] = array( - 'sql' => $sql, - 'params' => $params, - ); - - $stmt = $this->pdo->prepare( $sql ); - if ( false === $stmt || null === $stmt ) { - $this->last_exec_returned = null; - $info = $this->pdo->errorInfo(); - $this->last_sqlite_error = $info[0] . ' ' . $info[2]; - throw new PDOException( implode( ' ', array( 'Error:', $info[0], $info[2], 'SQLite:', $sql ) ), $info[1] ); - } - $returned = $stmt->execute( $params ); - $this->last_exec_returned = $returned; - if ( ! $returned ) { - $info = $stmt->errorInfo(); - $this->last_sqlite_error = $info[0] . ' ' . $info[2]; - throw new PDOException( implode( ' ', array( 'Error:', $info[0], $info[2], 'SQLite:', $sql ) ), $info[1] ); - } - - return $stmt; - } - - /** - * Method to set the results from the fetched data. - * - * @param array $data The data to set. - */ - private function set_results_from_fetched_data( $data ) { - if ( null === $this->results ) { - $this->results = $data; - } - if ( is_array( $this->results ) ) { - $this->num_rows = count( $this->results ); - $this->last_select_found_rows = count( $this->results ); - } - $this->return_value = $this->results; - } - - /** - * Method to set the results from the affected rows. - * - * @param int|null $override Override the affected rows. - */ - private function set_result_from_affected_rows( $override = null ) { - /* - * SELECT CHANGES() is a workaround for the fact that - * $stmt->rowCount() returns "0" (zero) with the - * SQLite driver at all times. - * Source: https://www.php.net/manual/en/pdostatement.rowcount.php - */ - if ( null === $override ) { - $this->affected_rows = (int) $this->execute_sqlite_query( 'select changes()' )->fetch()[0]; - } else { - $this->affected_rows = $override; - } - $this->return_value = $this->affected_rows; - $this->num_rows = $this->affected_rows; - $this->results = $this->affected_rows; - } - - /** - * Method to clear previous data. - */ - private function flush() { - $this->mysql_query = ''; - $this->results = null; - $this->last_exec_returned = null; - $this->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; - $this->error_messages = array(); - $this->is_error = false; - $this->executed_sqlite_queries = array(); - $this->like_expression_nesting = 0; - $this->like_escape_count = 0; - $this->is_information_schema_query = false; - $this->has_group_by = false; - } - - /** - * Begin a new transaction or nested transaction. - * - * @return boolean - */ - public function begin_transaction() { - $success = false; - try { - if ( 0 === $this->transaction_level ) { - $this->execute_sqlite_query( 'BEGIN' ); - } else { - $this->execute_sqlite_query( 'SAVEPOINT LEVEL' . $this->transaction_level ); - } - $success = $this->last_exec_returned; - } finally { - if ( $success ) { - ++$this->transaction_level; - if ( function_exists( 'do_action' ) ) { - /** - * 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; - } - - /** - * Commit the current transaction or nested transaction. - * - * @return boolean True on success, false on failure. - */ - public function commit() { - if ( 0 === $this->transaction_level ) { - return false; - } - - --$this->transaction_level; - if ( 0 === $this->transaction_level ) { - $this->execute_sqlite_query( 'COMMIT' ); - } else { - $this->execute_sqlite_query( 'RELEASE SAVEPOINT LEVEL' . $this->transaction_level ); - } - - if ( function_exists( 'do_action' ) ) { - do_action( 'sqlite_transaction_query_executed', 'COMMIT', (bool) $this->last_exec_returned, $this->transaction_level ); - } - return $this->last_exec_returned; - } - - /** - * Rollback the current transaction or nested transaction. - * - * @return boolean True on success, false on failure. - */ - public function rollback() { - if ( 0 === $this->transaction_level ) { - return false; - } - - --$this->transaction_level; - if ( 0 === $this->transaction_level ) { - $this->execute_sqlite_query( 'ROLLBACK' ); - } else { - $this->execute_sqlite_query( 'ROLLBACK TO SAVEPOINT LEVEL' . $this->transaction_level ); - } - if ( function_exists( 'do_action' ) ) { - do_action( 'sqlite_transaction_query_executed', 'ROLLBACK', (bool) $this->last_exec_returned, $this->transaction_level ); - } - return $this->last_exec_returned; - } - - /** - * Create an index name consisting of table name and original index name. - * This is to avoid duplicate index names in SQLite. - * - * @param $table - * @param $original_index_name - * - * @return string - */ - private function generate_index_name( $table, $original_index_name ) { - // Strip the occurrences of 2 or more consecutive underscores from the table name - // to allow easier splitting on __ later. - return preg_replace( '/_{2,}/', '_', $table ) . '__' . $original_index_name; - } - - /** - * @param string $table - * @param string $column - */ - private function add_column_on_update_current_timestamp( $table, $column ) { - $trigger_name = $this->get_column_on_update_current_timestamp_trigger_name( $table, $column ); - - // The trigger wouldn't work for virtual and "WITHOUT ROWID" tables, - // but currently that can't happen as we're not creating such tables. - // See: https://www.sqlite.org/rowidtable.html - $quoted_trigger = $this->quote_identifier( $trigger_name ); - $quoted_table = $this->quote_identifier( $table ); - $quoted_column = $this->quote_identifier( $column ); - $this->execute_sqlite_query( - "CREATE TRIGGER $quoted_trigger - AFTER UPDATE ON $quoted_table - FOR EACH ROW - BEGIN - UPDATE $quoted_table SET $quoted_column = CURRENT_TIMESTAMP WHERE rowid = NEW.rowid; - END" - ); - } - - /** - * @param string $table - * @param string $column - * @return string - */ - private function get_column_on_update_current_timestamp_trigger_name( $table, $column ) { - return "__{$table}_{$column}_on_update__"; - } -} diff --git a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/db.php b/packages/plugin-sqlite-database-integration/wp-includes/sqlite/db.php index dc4d14f8..03313515 100644 --- a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/db.php +++ b/packages/plugin-sqlite-database-integration/wp-includes/sqlite/db.php @@ -47,33 +47,11 @@ ); } -if ( defined( 'WP_SQLITE_AST_DRIVER' ) && WP_SQLITE_AST_DRIVER ) { - require_once __DIR__ . '/../database/load.php'; -} else { - require_once __DIR__ . '/php-polyfills.php'; - require_once __DIR__ . '/class-wp-sqlite-lexer.php'; - require_once __DIR__ . '/class-wp-sqlite-query-rewriter.php'; - require_once __DIR__ . '/class-wp-sqlite-translator.php'; - require_once __DIR__ . '/class-wp-sqlite-token.php'; - require_once __DIR__ . '/class-wp-sqlite-pdo-user-defined-functions.php'; -} +require_once __DIR__ . '/../database/load.php'; require_once __DIR__ . '/class-wp-sqlite-db.php'; require_once __DIR__ . '/install-functions.php'; -/** - * The DB_NAME constant is required by the new SQLite driver. - * - * There are some existing projects in which the DB_NAME constant is missing in - * wp-config.php. To enable easier early adoption and testing of the new SQLite - * driver, let's allow using a default database name when DB_NAME is not set. - * - * TODO: For version 3.0, enforce the DB_NAME constant and remove the fallback. - */ -if ( defined( 'DB_NAME' ) && '' !== DB_NAME ) { - $db_name = DB_NAME; -} else { - $db_name = apply_filters( 'wp_sqlite_default_db_name', 'database_name_here' ); -} +$db_name = defined( 'DB_NAME' ) ? DB_NAME : ''; /* * Debug: Cross-check with MySQL. diff --git a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/install-functions.php b/packages/plugin-sqlite-database-integration/wp-includes/sqlite/install-functions.php index bffe1425..5d73e39e 100644 --- a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/install-functions.php +++ b/packages/plugin-sqlite-database-integration/wp-includes/sqlite/install-functions.php @@ -34,15 +34,11 @@ function sqlite_make_db_sqlite() { wp_die( $message, 'Database Error!' ); } - if ( defined( 'WP_SQLITE_AST_DRIVER' ) && WP_SQLITE_AST_DRIVER ) { - $translator = new WP_SQLite_Driver( - new WP_SQLite_Connection( array( 'pdo' => $pdo ) ), - $wpdb->dbname - ); - } else { - $translator = new WP_SQLite_Translator( $pdo ); - } - $query = null; + $translator = new WP_SQLite_Driver( + new WP_SQLite_Connection( array( 'pdo' => $pdo ) ), + $wpdb->dbname + ); + $query = null; try { $translator->begin_transaction(); @@ -52,21 +48,16 @@ function sqlite_make_db_sqlite() { continue; } - $result = $translator->query( $query ); - if ( false === $result ) { - throw new PDOException( $translator->get_error_message() ); - } + $translator->query( $query ); } $translator->commit(); } catch ( PDOException $err ) { - $err_data = $err->errorInfo; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $err_code = $err_data[1]; $translator->rollback(); $message = sprintf( 'Error occurred while creating tables or indexes...