initial
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
define( 'WP_MYSQL_ON_SQLITE_LOADER_PATH', __FILE__ );
|
||||
|
||||
/**
|
||||
* Load the PDO MySQL-on-SQLite driver and its dependencies.
|
||||
*/
|
||||
require_once __DIR__ . '/php-polyfills.php';
|
||||
require_once __DIR__ . '/version.php';
|
||||
require_once __DIR__ . '/parser/class-wp-parser-grammar.php';
|
||||
require_once __DIR__ . '/parser/class-wp-parser.php';
|
||||
require_once __DIR__ . '/parser/class-wp-parser-node.php';
|
||||
require_once __DIR__ . '/parser/class-wp-parser-token.php';
|
||||
require_once __DIR__ . '/mysql/class-wp-mysql-token.php';
|
||||
|
||||
/*
|
||||
* The MySQL lexer and parser have an optional native (e.g. Rust) implementation.
|
||||
* When the native extension is loaded, it pre-declares WP_MySQL_Native_Lexer /
|
||||
* WP_MySQL_Native_Parser; otherwise we fall back to the pure-PHP classes shipped
|
||||
* here. WP_MySQL_Lexer / WP_MySQL_Parser is the public entrypoint either way.
|
||||
*/
|
||||
if ( class_exists( 'WP_MySQL_Native_Lexer', false ) ) {
|
||||
require_once __DIR__ . '/mysql/native/class-wp-mysql-lexer.php';
|
||||
} else {
|
||||
require_once __DIR__ . '/mysql/class-wp-mysql-lexer.php';
|
||||
}
|
||||
|
||||
if ( class_exists( 'WP_MySQL_Native_Parser', false ) ) {
|
||||
require_once __DIR__ . '/mysql/native/mysql-rust-bridge.php';
|
||||
require_once __DIR__ . '/mysql/native/class-wp-mysql-native-parser-node.php';
|
||||
require_once __DIR__ . '/mysql/native/trait-wp-mysql-native-parser-impl.php';
|
||||
require_once __DIR__ . '/mysql/native/class-wp-mysql-parser.php';
|
||||
} else {
|
||||
require_once __DIR__ . '/mysql/class-wp-mysql-parser.php';
|
||||
}
|
||||
require_once __DIR__ . '/sqlite/class-wp-sqlite-connection.php';
|
||||
require_once __DIR__ . '/sqlite/class-wp-sqlite-configurator.php';
|
||||
require_once __DIR__ . '/sqlite/class-wp-sqlite-driver.php';
|
||||
require_once __DIR__ . '/sqlite/class-wp-sqlite-driver-exception.php';
|
||||
require_once __DIR__ . '/sqlite/class-wp-sqlite-information-schema-builder.php';
|
||||
require_once __DIR__ . '/sqlite/class-wp-sqlite-information-schema-exception.php';
|
||||
require_once __DIR__ . '/sqlite/class-wp-sqlite-information-schema-reconstructor.php';
|
||||
require_once __DIR__ . '/sqlite/class-wp-sqlite-pdo-user-defined-functions.php';
|
||||
require_once __DIR__ . '/sqlite/class-wp-pdo-mysql-on-sqlite.php';
|
||||
require_once __DIR__ . '/sqlite/class-wp-pdo-proxy-statement.php';
|
||||
+3070
File diff suppressed because it is too large
Load Diff
+63
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
class WP_MySQL_Parser extends WP_Parser {
|
||||
/**
|
||||
* The current query AST.
|
||||
*
|
||||
* @var WP_Parser_Node|null
|
||||
*/
|
||||
private $current_ast;
|
||||
|
||||
/**
|
||||
* Reset this parser with a new token stream.
|
||||
*
|
||||
* @param array<WP_Parser_Token> $tokens The parser tokens.
|
||||
*/
|
||||
public function reset_tokens( array $tokens ): void {
|
||||
$this->tokens = $tokens;
|
||||
$this->position = 0;
|
||||
$this->current_ast = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the next query from the input SQL string.
|
||||
*
|
||||
* This method reads tokens until a query is parsed, or the parsing fails.
|
||||
* It returns a boolean indicating whether a query was successfully parsed.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* // Parse all queries in the input SQL string.
|
||||
* $parser = new WP_MySQL_Parser( $sql );
|
||||
* while ( $parser->next_query() ) {
|
||||
* $ast = $parser->get_query_ast();
|
||||
* if ( ! $ast ) {
|
||||
* // The parsing failed.
|
||||
* }
|
||||
* // The query was successfully parsed.
|
||||
* }
|
||||
*
|
||||
* @return bool Whether a query was successfully parsed.
|
||||
*/
|
||||
public function next_query(): bool {
|
||||
if ( $this->position >= count( $this->tokens ) ) {
|
||||
return false;
|
||||
}
|
||||
$this->current_ast = $this->parse();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current query AST.
|
||||
*
|
||||
* When no query has been parsed yet, the parsing failed, or the end of the
|
||||
* input was reached, this method returns null.
|
||||
*
|
||||
* @see WP_MySQL_Parser::next_query() for usage example.
|
||||
*
|
||||
* @return WP_Parser_Node|null The current query AST, or null if no query was parsed.
|
||||
*/
|
||||
public function get_query_ast(): ?WP_Parser_Node {
|
||||
return $this->current_ast;
|
||||
}
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* MySQL token.
|
||||
*
|
||||
* This class represents a MySQL SQL token that is produced by WP_MySQL_Lexer,
|
||||
* and consumed by WP_MySQL_Parser during the parsing process.
|
||||
*/
|
||||
class WP_MySQL_Token extends WP_Parser_Token {
|
||||
/**
|
||||
* Whether the NO_BACKSLASH_ESCAPES SQL mode is enabled.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $sql_mode_no_backslash_escapes_enabled;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param int $id Token type.
|
||||
* @param int $start Byte offset in the input where the token begins.
|
||||
* @param int $length Byte length of the token in the input.
|
||||
* @param string $input Input bytes from which the token was parsed.
|
||||
* @param bool $sql_mode_no_backslash_escapes_enabled Whether the NO_BACKSLASH_ESCAPES SQL mode is enabled.
|
||||
*/
|
||||
public function __construct(
|
||||
int $id,
|
||||
int $start,
|
||||
int $length,
|
||||
string $input,
|
||||
bool $sql_mode_no_backslash_escapes_enabled
|
||||
) {
|
||||
$this->id = $id;
|
||||
$this->start = $start;
|
||||
$this->length = $length;
|
||||
$this->input = $input;
|
||||
|
||||
$this->sql_mode_no_backslash_escapes_enabled = $sql_mode_no_backslash_escapes_enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the token.
|
||||
*
|
||||
* This method is intended to be used only for testing and debugging purposes,
|
||||
* when tokens need to be presented by their names in a human-readable form.
|
||||
* It should not be used in production code, as it's not performance-optimized.
|
||||
*
|
||||
* @return string The token name.
|
||||
*/
|
||||
public function get_name(): string {
|
||||
$name = WP_MySQL_Lexer::get_token_name( $this->id );
|
||||
if ( null === $name ) {
|
||||
$name = 'UNKNOWN';
|
||||
}
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the real unquoted value of the token.
|
||||
*
|
||||
* @return string The token value.
|
||||
*/
|
||||
public function get_value(): string {
|
||||
$value = $this->get_bytes();
|
||||
if (
|
||||
WP_MySQL_Lexer::SINGLE_QUOTED_TEXT === $this->id
|
||||
|| WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT === $this->id
|
||||
|| WP_MySQL_Lexer::BACK_TICK_QUOTED_ID === $this->id
|
||||
) {
|
||||
// Remove bounding quotes.
|
||||
$quote = $value[0];
|
||||
$value = substr( $value, 1, -1 );
|
||||
|
||||
/*
|
||||
* When the NO_BACKSLASH_ESCAPES SQL mode is enabled, we only need to
|
||||
* handle escaped bounding quotes, as the other characters preserve
|
||||
* their literal values.
|
||||
*/
|
||||
if ( $this->sql_mode_no_backslash_escapes_enabled ) {
|
||||
return str_replace( $quote . $quote, $quote, $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescape MySQL escape sequences.
|
||||
*
|
||||
* MySQL string literals use backslash as an escape character, and
|
||||
* the string bounding quotes can also be escaped by being doubled.
|
||||
*
|
||||
* The escaping is done according to the following rules:
|
||||
*
|
||||
* 1. Some special character escape sequences are recognized.
|
||||
* For example, "\n" is a newline character, "\0" is ASCII NULL.
|
||||
* 2. A specific treatment is applied to "\%" and "\_" sequences.
|
||||
* This is due to their special meaning for pattern matching.
|
||||
* 3. Other backslash-prefixed characters resolve to their literal
|
||||
* values. For example, "\x" represents "x", "\\" represents "\".
|
||||
*
|
||||
* Despite looking similar, these rules are different from the C-style
|
||||
* string escaping, so we cannot use "strip(c)slashes()" in this case.
|
||||
*
|
||||
* See: https://dev.mysql.com/doc/refman/8.4/en/string-literals.html
|
||||
*/
|
||||
$backslash = chr( 92 );
|
||||
$replacements = array(
|
||||
/*
|
||||
* MySQL special character escape sequences.
|
||||
*/
|
||||
( $backslash . '0' ) => chr( 0 ), // An ASCII NULL character (\0).
|
||||
( $backslash . "'" ) => chr( 39 ), // A single quote character (').
|
||||
( $backslash . '"' ) => chr( 34 ), // A double quote character (").
|
||||
( $backslash . 'b' ) => chr( 8 ), // A backspace character.
|
||||
( $backslash . 'n' ) => chr( 10 ), // A newline (linefeed) character (\n).
|
||||
( $backslash . 'r' ) => chr( 13 ), // A carriage return character (\r).
|
||||
( $backslash . 't' ) => chr( 9 ), // A tab character (\t).
|
||||
( $backslash . 'Z' ) => chr( 26 ), // An ASCII 26 (Control+Z) character.
|
||||
|
||||
/*
|
||||
* Normalize escaping of "%" and "_" characters.
|
||||
*
|
||||
* MySQL has unusual handling for "\%" and "\_" in all string literals.
|
||||
* While other sequences follow the C-style escaping ("\?" is "?", etc.),
|
||||
* "\%" resolves to "\%" and "\_" resolves to "\_" (unlike in C strings).
|
||||
*
|
||||
* This means that "\%" behaves like "\\%", and "\_" behaves like "\\_".
|
||||
* To preserve this behavior, we need to add a second backslash here.
|
||||
*
|
||||
* From https://dev.mysql.com/doc/refman/8.4/en/string-literals.html:
|
||||
* > The \% and \_ sequences are used to search for literal instances
|
||||
* > of % and _ in pattern-matching contexts where they would otherwise
|
||||
* > be interpreted as wildcard characters. If you use \% or \_ outside
|
||||
* > of pattern-matching contexts, they evaluate to the strings \% and
|
||||
* > \_, not to % and _.
|
||||
*/
|
||||
( $backslash . '%' ) => $backslash . $backslash . '%',
|
||||
( $backslash . '_' ) => $backslash . $backslash . '_',
|
||||
|
||||
/*
|
||||
* Preserve a double backslash as-is, so that the trailing backslash
|
||||
* is not consumed as the beginning of an escape sequence like "\n".
|
||||
*
|
||||
* Resolving "\\" to "\" will be handled in the next step, where all
|
||||
* other backslash-prefixed characters resolve to their literal values.
|
||||
*/
|
||||
( $backslash . $backslash )
|
||||
=> $backslash . $backslash,
|
||||
|
||||
/*
|
||||
* The bounding quotes can also be escaped by being doubled.
|
||||
*/
|
||||
( $quote . $quote ) => $quote,
|
||||
);
|
||||
|
||||
/*
|
||||
* Apply the replacements.
|
||||
*
|
||||
* It is important to use "strtr()" and not "str_replace()", because
|
||||
* "str_replace()" applies replacements one after another, modifying
|
||||
* intermediate changes rather than just the original string:
|
||||
*
|
||||
* - str_replace( [ 'a', 'b' ], [ 'b', 'c' ], 'ab' ); // 'cc' (bad)
|
||||
* - strtr( 'ab', [ 'a' => 'b', 'b' => 'c' ] ); // 'bc' (good)
|
||||
*/
|
||||
$value = strtr( $value, $replacements );
|
||||
|
||||
/*
|
||||
* A backslash with any other character represents the character itself.
|
||||
* That is, \x evaluates to x, \\ evaluates to \, and \🙂 evaluates to 🙂.
|
||||
*/
|
||||
$preg_quoted_backslash = preg_quote( $backslash );
|
||||
$value = preg_replace( "/$preg_quoted_backslash(.)/u", '$1', $value );
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the token representation as a string.
|
||||
*
|
||||
* This method is intended to be used only for testing and debugging purposes,
|
||||
* when tokens need to be presented in a human-readable form. It should not
|
||||
* be used in production code, as it's not performance-optimized.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string {
|
||||
return $this->get_value() . '<' . $this->id . ',' . $this->get_name() . '>';
|
||||
}
|
||||
}
|
||||
+4
File diff suppressed because one or more lines are too long
+3
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
class WP_MySQL_Lexer extends WP_MySQL_Native_Lexer {}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Parser node backed by a native (Rust) AST.
|
||||
*
|
||||
* Constructed by the native MySQL parser extension. Read methods delegate
|
||||
* into the Rust-owned AST so children are never copied into PHP unless a
|
||||
* caller actually walks the tree. On the first mutation (append_child or
|
||||
* merge_fragment), the node materializes its children into the inherited
|
||||
* `$children` array and behaves like a plain WP_Parser_Node from then on.
|
||||
*/
|
||||
class WP_MySQL_Native_Parser_Node extends WP_Parser_Node {
|
||||
private $was_mutated = false;
|
||||
|
||||
public function __construct( $rule_id, $rule_name ) {
|
||||
parent::__construct( $rule_id, $rule_name );
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
if ( function_exists( 'wp_sqlite_mysql_native_ast_release_wrapper' ) ) {
|
||||
wp_sqlite_mysql_native_ast_release_wrapper( $this );
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function append_child( $node ) {
|
||||
$this->materialize_native_children();
|
||||
parent::append_child( $node );
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function merge_fragment( $node ) {
|
||||
$this->materialize_native_children();
|
||||
if ( $node instanceof self ) {
|
||||
$node->materialize_native_children();
|
||||
}
|
||||
parent::merge_fragment( $node );
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function has_child(): bool {
|
||||
if ( $this->was_mutated ) {
|
||||
return parent::has_child();
|
||||
}
|
||||
return wp_sqlite_mysql_native_ast_has_child( $this );
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function has_child_node( ?string $rule_name = null ): bool {
|
||||
if ( $this->was_mutated ) {
|
||||
return parent::has_child_node( $rule_name );
|
||||
}
|
||||
return wp_sqlite_mysql_native_ast_has_child_node( $this, $rule_name );
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function has_child_token( ?int $token_id = null ): bool {
|
||||
if ( $this->was_mutated ) {
|
||||
return parent::has_child_token( $token_id );
|
||||
}
|
||||
return wp_sqlite_mysql_native_ast_has_child_token( $this, $token_id );
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function get_first_child() {
|
||||
if ( $this->was_mutated ) {
|
||||
return parent::get_first_child();
|
||||
}
|
||||
return wp_sqlite_mysql_native_ast_get_first_child( $this );
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function get_first_child_node( ?string $rule_name = null ): ?WP_Parser_Node {
|
||||
if ( $this->was_mutated ) {
|
||||
return parent::get_first_child_node( $rule_name );
|
||||
}
|
||||
return wp_sqlite_mysql_native_ast_get_first_child_node( $this, $rule_name );
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function get_first_child_token( ?int $token_id = null ): ?WP_Parser_Token {
|
||||
if ( $this->was_mutated ) {
|
||||
return parent::get_first_child_token( $token_id );
|
||||
}
|
||||
return wp_sqlite_mysql_native_ast_get_first_child_token( $this, $token_id );
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function get_first_descendant_node( ?string $rule_name = null ): ?WP_Parser_Node {
|
||||
if ( $this->was_mutated ) {
|
||||
return parent::get_first_descendant_node( $rule_name );
|
||||
}
|
||||
return wp_sqlite_mysql_native_ast_get_first_descendant_node( $this, $rule_name );
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function get_first_descendant_token( ?int $token_id = null ): ?WP_Parser_Token {
|
||||
if ( $this->was_mutated ) {
|
||||
return parent::get_first_descendant_token( $token_id );
|
||||
}
|
||||
return wp_sqlite_mysql_native_ast_get_first_descendant_token( $this, $token_id );
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function get_children(): array {
|
||||
if ( $this->was_mutated ) {
|
||||
return parent::get_children();
|
||||
}
|
||||
return wp_sqlite_mysql_native_ast_get_children( $this );
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function get_child_nodes( ?string $rule_name = null ): array {
|
||||
if ( $this->was_mutated ) {
|
||||
return parent::get_child_nodes( $rule_name );
|
||||
}
|
||||
return wp_sqlite_mysql_native_ast_get_child_nodes( $this, $rule_name );
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function get_child_tokens( ?int $token_id = null ): array {
|
||||
if ( $this->was_mutated ) {
|
||||
return parent::get_child_tokens( $token_id );
|
||||
}
|
||||
return wp_sqlite_mysql_native_ast_get_child_tokens( $this, $token_id );
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function get_descendants(): array {
|
||||
if ( $this->was_mutated ) {
|
||||
return parent::get_descendants();
|
||||
}
|
||||
return wp_sqlite_mysql_native_ast_get_descendants( $this );
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function get_descendant_nodes( ?string $rule_name = null ): array {
|
||||
if ( $this->was_mutated ) {
|
||||
return parent::get_descendant_nodes( $rule_name );
|
||||
}
|
||||
return wp_sqlite_mysql_native_ast_get_descendant_nodes( $this, $rule_name );
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function get_descendant_tokens( ?int $token_id = null ): array {
|
||||
if ( $this->was_mutated ) {
|
||||
return parent::get_descendant_tokens( $token_id );
|
||||
}
|
||||
return wp_sqlite_mysql_native_ast_get_descendant_tokens( $this, $token_id );
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function get_start(): int {
|
||||
if ( $this->was_mutated ) {
|
||||
return parent::get_start();
|
||||
}
|
||||
return wp_sqlite_mysql_native_ast_get_start( $this );
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function get_length(): int {
|
||||
if ( $this->was_mutated ) {
|
||||
return parent::get_length();
|
||||
}
|
||||
return wp_sqlite_mysql_native_ast_get_length( $this );
|
||||
}
|
||||
|
||||
private function materialize_native_children(): void {
|
||||
if ( $this->was_mutated ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->children = wp_sqlite_mysql_native_ast_get_children( $this );
|
||||
$this->was_mutated = true;
|
||||
if ( function_exists( 'wp_sqlite_mysql_native_ast_materialize_wrapper' ) ) {
|
||||
wp_sqlite_mysql_native_ast_materialize_wrapper( $this );
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Native-mode public parser entry point.
|
||||
*
|
||||
* Always extends the pure-PHP `WP_Parser` so `$parser instanceof WP_Parser`
|
||||
* keeps working for callers regardless of whether the Rust extension is
|
||||
* loaded. The actual parsing work is delegated to a composed
|
||||
* `WP_MySQL_Native_Parser` instance via the `WP_MySQL_Native_Parser_Impl`
|
||||
* trait — see that file for the per-method delegation.
|
||||
*/
|
||||
class WP_MySQL_Parser extends WP_Parser {
|
||||
use WP_MySQL_Native_Parser_Impl;
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Bridge helpers for the optional Rust MySQL lexer/parser extension.
|
||||
* PHP keeps the grammar object, while Rust owns the exported parser state.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Export grammar internals for the native parser.
|
||||
*
|
||||
* @param WP_Parser_Grammar $grammar Parser grammar.
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function wp_sqlite_mysql_native_export_grammar( WP_Parser_Grammar $grammar ): array {
|
||||
return array(
|
||||
'highest_terminal_id' => $grammar->highest_terminal_id,
|
||||
'rules' => $grammar->rules,
|
||||
'lookahead_is_match_possible' => $grammar->lookahead_is_match_possible,
|
||||
'rule_names' => $grammar->rule_names,
|
||||
'fragment_ids' => $grammar->fragment_ids,
|
||||
);
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Native-mode `WP_MySQL_Parser` implementation, delivered as a trait.
|
||||
*
|
||||
* The class that uses this trait (`WP_MySQL_Parser` in native mode)
|
||||
* extends the pure-PHP `WP_Parser` so callers' `instanceof WP_Parser`
|
||||
* checks keep working, while the actual parsing work is delegated to
|
||||
* the Rust-registered `WP_MySQL_Native_Parser` instance held in
|
||||
* `$this->native`. `WP_Parser`'s state (`$grammar`, `$tokens`,
|
||||
* `$position`) stays inert in native mode — the trait's overrides
|
||||
* never read it.
|
||||
*
|
||||
* Adding a public method here is enough to plumb a new public method
|
||||
* through to the native parser; the using class does not need touching.
|
||||
*/
|
||||
trait WP_MySQL_Native_Parser_Impl {
|
||||
/**
|
||||
* @var WP_MySQL_Native_Parser
|
||||
*/
|
||||
private $native;
|
||||
|
||||
/**
|
||||
* @param WP_Parser_Grammar $grammar
|
||||
* @param array<WP_Parser_Token>|WP_MySQL_Native_Token_Stream $tokens
|
||||
*/
|
||||
public function __construct( WP_Parser_Grammar $grammar, $tokens ) {
|
||||
// WP_Parser's `array $tokens` constructor signature can't accept
|
||||
// the native token stream object; its `$this->tokens` /
|
||||
// `$this->position` state is inert in native mode anyway, so we
|
||||
// pass an empty array to satisfy the parent contract and keep
|
||||
// the actual tokens on the native parser.
|
||||
parent::__construct( $grammar, array() );
|
||||
$this->native = new WP_MySQL_Native_Parser( $grammar, $tokens );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<WP_Parser_Token>|WP_MySQL_Native_Token_Stream $tokens
|
||||
*/
|
||||
public function reset_tokens( $tokens ): void {
|
||||
$this->native->reset_tokens( $tokens );
|
||||
}
|
||||
|
||||
public function next_query(): bool {
|
||||
return $this->native->next_query();
|
||||
}
|
||||
|
||||
public function get_query_ast(): ?WP_Parser_Node {
|
||||
return $this->native->get_query_ast();
|
||||
}
|
||||
|
||||
public function parse() {
|
||||
return $this->native->parse();
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A parser grammar.
|
||||
*
|
||||
* This class represents a parser grammar that can be consumed by WP_Parser.
|
||||
* It loads a compressed grammar from a PHP array, inflates it to an internal
|
||||
* representation, and precomputes a lookup table for quick branch selection.
|
||||
*
|
||||
* @TODO: Add more details about the grammar implementation.
|
||||
*/
|
||||
class WP_Parser_Grammar {
|
||||
/**
|
||||
* ID for a special grammar rule that represents an empty "ε" (epsilon) rule.
|
||||
*
|
||||
* An "ε" rule in a grammar is a rule that matches an empty input of 0 bytes.
|
||||
* It can be used to represent optional grammar productions, and it is helpful
|
||||
* for expanding 0-or-1, 0-or-more, and 1-or-more quantifiers into simple rules.
|
||||
*
|
||||
* @TODO Investigate whether we can prevent possible conflict with a token ID.
|
||||
* The MySQL grammar doesn't define a token with ID "0", but generally
|
||||
* token IDs are not guaranteed to always satisfy this condition.
|
||||
*/
|
||||
const EMPTY_RULE_ID = 0;
|
||||
|
||||
/**
|
||||
* @TODO: Review and document these properties and their visibility.
|
||||
*/
|
||||
public $rules;
|
||||
public $rule_names;
|
||||
public $fragment_ids;
|
||||
public $lookahead_is_match_possible = array();
|
||||
public $lowest_non_terminal_id;
|
||||
public $highest_terminal_id;
|
||||
public $native_grammar;
|
||||
|
||||
public function __construct( array $rules ) {
|
||||
$this->inflate( $rules );
|
||||
}
|
||||
|
||||
public function get_rule_name( $rule_id ) {
|
||||
return $this->rule_names[ $rule_id ];
|
||||
}
|
||||
|
||||
public function get_rule_id( $rule_name ) {
|
||||
return array_search( $rule_name, $this->rule_names, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflate the grammar to an internal representation optimized for parsing.
|
||||
*
|
||||
* The input grammar is a compressed PHP array to minimize the file size.
|
||||
* Every rule and token in the compressed grammar is encoded as an integer.
|
||||
*/
|
||||
private function inflate( $grammar ) {
|
||||
$this->lowest_non_terminal_id = $grammar['rules_offset'];
|
||||
$this->highest_terminal_id = $this->lowest_non_terminal_id - 1;
|
||||
|
||||
foreach ( $grammar['rules_names'] as $rule_index => $rule_name ) {
|
||||
$this->rule_names[ $rule_index + $grammar['rules_offset'] ] = $rule_name;
|
||||
$this->rules[ $rule_index + $grammar['rules_offset'] ] = array();
|
||||
|
||||
/**
|
||||
* Treat all intermediate rules as fragments to inline before returning
|
||||
* the final parse tree to the API consumer.
|
||||
*
|
||||
* The original grammar was too difficult to parse with rules like:
|
||||
*
|
||||
* query ::= EOF | ((simpleStatement | beginWork) ((SEMICOLON_SYMBOL EOF?) | EOF))
|
||||
*
|
||||
* We've factored rule fragments, such as `EOF?`, into separate rules, such as `%EOF_zero_or_one`.
|
||||
* This is super useful for parsing, but it limits the API consumer's ability to
|
||||
* reason about the parse tree.
|
||||
*
|
||||
* Fragments are intermediate rules that are not part of the original grammar.
|
||||
* They are prefixed with a "%" to be distinguished from the original rules.
|
||||
*/
|
||||
if ( '%' === $rule_name[0] ) {
|
||||
$this->fragment_ids[ $rule_index + $grammar['rules_offset'] ] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->rules = array();
|
||||
foreach ( $grammar['grammar'] as $rule_index => $branches ) {
|
||||
$rule_id = $rule_index + $grammar['rules_offset'];
|
||||
$this->rules[ $rule_id ] = $branches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a rule => [token => true] lookup table for each rule
|
||||
* that starts with a terminal OR with another rule that already
|
||||
* has a lookahead mapping.
|
||||
*
|
||||
* This is similar to left-factoring the grammar, even if not quite
|
||||
* the same.
|
||||
*
|
||||
* This enables us to quickly bail out from checking branches that
|
||||
* cannot possibly match the current token. This increased the parser
|
||||
* speed by a whopping 80%!
|
||||
*
|
||||
* @TODO: Explore these possible next steps:
|
||||
*
|
||||
* * Compute a rule => [token => branch[]] list lookup table and only
|
||||
* process the branches that have a chance of matching the current token.
|
||||
* * Actually left-factor the grammar as much as possible. This, however,
|
||||
* could inflate the serialized grammar size.
|
||||
*/
|
||||
// 5 iterations seem to give us all the speed gains we can get from this.
|
||||
for ( $i = 0; $i < 5; $i++ ) {
|
||||
foreach ( $grammar['grammar'] as $rule_index => $branches ) {
|
||||
$rule_id = $rule_index + $grammar['rules_offset'];
|
||||
if ( isset( $this->lookahead_is_match_possible[ $rule_id ] ) ) {
|
||||
continue;
|
||||
}
|
||||
$rule_lookup = array();
|
||||
$first_symbol_can_be_expanded_to_all_terminals = true;
|
||||
foreach ( $branches as $branch ) {
|
||||
$terminals = false;
|
||||
$branch_starts_with_terminal = $branch[0] < $this->lowest_non_terminal_id;
|
||||
if ( $branch_starts_with_terminal ) {
|
||||
$terminals = array( $branch[0] );
|
||||
} elseif ( isset( $this->lookahead_is_match_possible[ $branch[0] ] ) ) {
|
||||
$terminals = array_keys( $this->lookahead_is_match_possible[ $branch[0] ] );
|
||||
}
|
||||
|
||||
if ( false === $terminals ) {
|
||||
$first_symbol_can_be_expanded_to_all_terminals = false;
|
||||
break;
|
||||
}
|
||||
foreach ( $terminals as $terminal ) {
|
||||
$rule_lookup[ $terminal ] = true;
|
||||
}
|
||||
}
|
||||
if ( $first_symbol_can_be_expanded_to_all_terminals ) {
|
||||
$this->lookahead_is_match_possible[ $rule_id ] = $rule_lookup;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+384
@@ -0,0 +1,384 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A node in parse tree.
|
||||
*
|
||||
* This class represents a node in the parse tree that is produced by WP_Parser.
|
||||
* A node corresponds to the related grammar rule that was matched by the parser.
|
||||
* Each node can contain children, consisting of other nodes and grammar tokens.
|
||||
* In this way, a parser node constitutes a recursive structure that represents
|
||||
* a parse (sub)tree at each level of the full grammar tree.
|
||||
*/
|
||||
class WP_Parser_Node {
|
||||
/**
|
||||
* @TODO: Review and document these properties and their visibility.
|
||||
*/
|
||||
public $rule_id;
|
||||
public $rule_name;
|
||||
protected $children = array();
|
||||
|
||||
public function __construct( $rule_id, $rule_name ) {
|
||||
$this->rule_id = $rule_id;
|
||||
$this->rule_name = $rule_name;
|
||||
}
|
||||
|
||||
public function append_child( $node ) {
|
||||
$this->children[] = $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten the matched rule fragments as if their children were direct
|
||||
* descendants of the current rule.
|
||||
*
|
||||
* What are rule fragments?
|
||||
*
|
||||
* When we initially parse the grammar file, it has compound rules such
|
||||
* as this one:
|
||||
*
|
||||
* query ::= EOF | ((simpleStatement | beginWork) ((SEMICOLON_SYMBOL EOF?) | EOF))
|
||||
*
|
||||
* Building a parser that can understand such rules is way more complex than building
|
||||
* a parser that only follows simple rules, so we flatten those compound rules into
|
||||
* simpler ones. The above rule would be flattened to:
|
||||
*
|
||||
* query ::= EOF | %query0
|
||||
* %query0 ::= %%query01 %%query02
|
||||
* %%query01 ::= simpleStatement | beginWork
|
||||
* %%query02 ::= SEMICOLON_SYMBOL EOF_zero_or_one | EOF
|
||||
* EOF_zero_or_one ::= EOF | ε
|
||||
*
|
||||
* This factorization happens in "convert-grammar.php".
|
||||
*
|
||||
* "Fragments" are intermediate artifacts whose names are not in the original grammar.
|
||||
* They are extremely useful for the parser, but the API consumer should never have to
|
||||
* worry about them. Fragment names start with a percent sign ("%").
|
||||
*
|
||||
* The code below inlines every fragment back in its parent rule.
|
||||
*
|
||||
* We could optimize this. The current $match may be discarded later on so any inlining
|
||||
* effort here would be wasted. However, inlining seems cheap and doing it bottom-up here
|
||||
* is **much** easier than reprocessing the parse tree top-down later on.
|
||||
*
|
||||
* The following parse tree:
|
||||
*
|
||||
* [
|
||||
* 'query' => [
|
||||
* [
|
||||
* '%query01' => [
|
||||
* [
|
||||
* 'simpleStatement' => [
|
||||
* MySQLToken(MySQLLexer::WITH_SYMBOL, 'WITH')
|
||||
* ],
|
||||
* '%query02' => [
|
||||
* [
|
||||
* 'simpleStatement' => [
|
||||
* MySQLToken(MySQLLexer::WITH_SYMBOL, 'WITH')
|
||||
* ]
|
||||
* ],
|
||||
* ]
|
||||
* ]
|
||||
* ]
|
||||
* ]
|
||||
* ]
|
||||
*
|
||||
* Would be inlined as:
|
||||
*
|
||||
* [
|
||||
* 'query' => [
|
||||
* [
|
||||
* 'simpleStatement' => [
|
||||
* MySQLToken(MySQLLexer::WITH_SYMBOL, 'WITH')
|
||||
* ]
|
||||
* ],
|
||||
* [
|
||||
* 'simpleStatement' => [
|
||||
* MySQLToken(MySQLLexer::WITH_SYMBOL, 'WITH')
|
||||
* ]
|
||||
* ]
|
||||
* ]
|
||||
* ]
|
||||
*/
|
||||
public function merge_fragment( $node ) {
|
||||
$this->children = array_merge( $this->children, $node->children );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this node has any child nodes or tokens.
|
||||
*
|
||||
* @return bool True if this node has any child nodes or tokens, false otherwise.
|
||||
*/
|
||||
public function has_child(): bool {
|
||||
return count( $this->children ) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this node has any child nodes.
|
||||
*
|
||||
* @param string|null $rule_name Optional. A node rule name to check for.
|
||||
* @return bool True if any child nodes are found, false otherwise.
|
||||
*/
|
||||
public function has_child_node( ?string $rule_name = null ): bool {
|
||||
foreach ( $this->children as $child ) {
|
||||
if (
|
||||
$child instanceof WP_Parser_Node
|
||||
&& ( null === $rule_name || $child->rule_name === $rule_name )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this node has any child tokens.
|
||||
*
|
||||
* @param int|null $token_id Optional. A token ID to check for.
|
||||
* @return bool True if any child tokens are found, false otherwise.
|
||||
*/
|
||||
public function has_child_token( ?int $token_id = null ): bool {
|
||||
foreach ( $this->children as $child ) {
|
||||
if (
|
||||
$child instanceof WP_Parser_Token
|
||||
&& ( null === $token_id || $child->id === $token_id )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first child node or token of this node.
|
||||
*
|
||||
* @return WP_Parser_Node|WP_Parser_Token|null The first child node or token;
|
||||
* null when no children are found.
|
||||
*/
|
||||
public function get_first_child() {
|
||||
return $this->children[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first child node of this node.
|
||||
*
|
||||
* @param string|null $rule_name Optional. A node rule name to check for.
|
||||
* @return WP_Parser_Node|null The first matching child node; null when no children are found.
|
||||
*/
|
||||
public function get_first_child_node( ?string $rule_name = null ): ?WP_Parser_Node {
|
||||
foreach ( $this->children as $child ) {
|
||||
if (
|
||||
$child instanceof WP_Parser_Node
|
||||
&& ( null === $rule_name || $child->rule_name === $rule_name )
|
||||
) {
|
||||
return $child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first child token of this node.
|
||||
*
|
||||
* @param int|null $token_id Optional. A token ID to check for.
|
||||
* @return WP_Parser_Token|null The first matching child token; null when no children are found.
|
||||
*/
|
||||
public function get_first_child_token( ?int $token_id = null ): ?WP_Parser_Token {
|
||||
foreach ( $this->children as $child ) {
|
||||
if (
|
||||
$child instanceof WP_Parser_Token
|
||||
&& ( null === $token_id || $child->id === $token_id )
|
||||
) {
|
||||
return $child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first descendant node of this node.
|
||||
*
|
||||
* The node children are traversed recursively in a depth-first order until
|
||||
* a matching descendant node is found, or the entire subtree is searched.
|
||||
*
|
||||
* @param string|null $rule_name Optional. A node rule name to check for.
|
||||
* @return WP_Parser_Node|null The first matching descendant node; null when no descendants are found.
|
||||
*/
|
||||
public function get_first_descendant_node( ?string $rule_name = null ): ?WP_Parser_Node {
|
||||
for ( $i = 0; $i < count( $this->children ); $i++ ) {
|
||||
$child = $this->children[ $i ];
|
||||
if ( ! $child instanceof WP_Parser_Node ) {
|
||||
continue;
|
||||
}
|
||||
if ( null === $rule_name || $child->rule_name === $rule_name ) {
|
||||
return $child;
|
||||
}
|
||||
$node = $child->get_first_descendant_node( $rule_name );
|
||||
if ( $node ) {
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first descendant token of this node.
|
||||
*
|
||||
* The node children are traversed recursively in a depth-first order until
|
||||
* a matching descendant token is found, or the entire subtree is searched.
|
||||
*
|
||||
* @param int|null $token_id Optional. A token ID to check for.
|
||||
* @return WP_Parser_Token|null The first matching descendant token; null when no descendants are found.
|
||||
*/
|
||||
public function get_first_descendant_token( ?int $token_id = null ): ?WP_Parser_Token {
|
||||
for ( $i = 0; $i < count( $this->children ); $i++ ) {
|
||||
$child = $this->children[ $i ];
|
||||
if ( $child instanceof WP_Parser_Token ) {
|
||||
if ( null === $token_id || $child->id === $token_id ) {
|
||||
return $child;
|
||||
}
|
||||
} else {
|
||||
$token = $child->get_first_descendant_token( $token_id );
|
||||
if ( $token ) {
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all children of this node.
|
||||
*
|
||||
* @return array<WP_Parser_Node|WP_Parser_Token> An array of all child nodes and tokens of this node.
|
||||
*/
|
||||
public function get_children(): array {
|
||||
return $this->children;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all child nodes of this node.
|
||||
*
|
||||
* @param string|null $rule_name Optional. A node rule name to check for.
|
||||
* @return WP_Parser_Node[] An array of all matching child nodes.
|
||||
*/
|
||||
public function get_child_nodes( ?string $rule_name = null ): array {
|
||||
$nodes = array();
|
||||
foreach ( $this->children as $child ) {
|
||||
if (
|
||||
$child instanceof WP_Parser_Node
|
||||
&& ( null === $rule_name || $child->rule_name === $rule_name )
|
||||
) {
|
||||
$nodes[] = $child;
|
||||
}
|
||||
}
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all child tokens of this node.
|
||||
*
|
||||
* @param int|null $token_id Optional. A token ID to check for.
|
||||
* @return WP_Parser_Token[] An array of all matching child tokens.
|
||||
*/
|
||||
public function get_child_tokens( ?int $token_id = null ): array {
|
||||
$tokens = array();
|
||||
foreach ( $this->children as $child ) {
|
||||
if (
|
||||
$child instanceof WP_Parser_Token
|
||||
&& ( null === $token_id || $child->id === $token_id )
|
||||
) {
|
||||
$tokens[] = $child;
|
||||
}
|
||||
}
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all descendants of this node.
|
||||
*
|
||||
* The descendants are collected using a depth-first pre-order NLR traversal.
|
||||
* This produces a natural ordering that corresponds to the original input.
|
||||
*
|
||||
* @return array<WP_Parser_Node|WP_Parser_Token> An array of all descendant nodes and tokens of this node.
|
||||
*/
|
||||
public function get_descendants(): array {
|
||||
$descendants = array();
|
||||
foreach ( $this->children as $child ) {
|
||||
if ( $child instanceof WP_Parser_Node ) {
|
||||
$descendants[] = $child;
|
||||
$descendants = array_merge( $descendants, $child->get_descendants() );
|
||||
} else {
|
||||
$descendants[] = $child;
|
||||
}
|
||||
}
|
||||
return $descendants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all descendant nodes of this node.
|
||||
*
|
||||
* The descendants are collected using a depth-first pre-order NLR traversal.
|
||||
* This produces a natural ordering that corresponds to the original input.
|
||||
* All matching nodes are collected during the traversal.
|
||||
*
|
||||
* @param string|null $rule_name Optional. A node rule name to check for.
|
||||
* @return WP_Parser_Node[] An array of all matching descendant nodes.
|
||||
*/
|
||||
public function get_descendant_nodes( ?string $rule_name = null ): array {
|
||||
$nodes = array();
|
||||
foreach ( $this->children as $child ) {
|
||||
if ( ! $child instanceof WP_Parser_Node ) {
|
||||
continue;
|
||||
}
|
||||
if ( null === $rule_name || $child->rule_name === $rule_name ) {
|
||||
$nodes[] = $child;
|
||||
}
|
||||
$nodes = array_merge( $nodes, $child->get_descendant_nodes( $rule_name ) );
|
||||
}
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all descendant tokens of this node.
|
||||
*
|
||||
* The descendants are collected using a depth-first pre-order NLR traversal.
|
||||
* This produces a natural ordering that corresponds to the original input.
|
||||
* All matching tokens are collected during the traversal.
|
||||
*
|
||||
* @param int|null $token_id Optional. A token ID to check for.
|
||||
* @return WP_Parser_Token[] An array of all matching descendant tokens.
|
||||
*/
|
||||
public function get_descendant_tokens( ?int $token_id = null ): array {
|
||||
$tokens = array();
|
||||
foreach ( $this->children as $child ) {
|
||||
if ( $child instanceof WP_Parser_Token ) {
|
||||
if ( null === $token_id || $child->id === $token_id ) {
|
||||
$tokens[] = $child;
|
||||
}
|
||||
} else {
|
||||
$tokens = array_merge( $tokens, $child->get_descendant_tokens( $token_id ) );
|
||||
}
|
||||
}
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the byte offset in the input string where this node begins.
|
||||
*
|
||||
* @return int The byte offset in the input string where this node begins.
|
||||
*/
|
||||
public function get_start(): int {
|
||||
return $this->get_first_descendant_token()->start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the byte length of this node in the input string.
|
||||
*
|
||||
* @return int The byte length of this node in the input string.
|
||||
*/
|
||||
public function get_length(): int {
|
||||
$tokens = $this->get_descendant_tokens();
|
||||
$first_token = $tokens[0];
|
||||
$last_token = $tokens[ count( $tokens ) - 1 ];
|
||||
return $last_token->start + $last_token->length - $first_token->start;
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A token, representing a leaf in the parse tree.
|
||||
*
|
||||
* This class represents a token that is consumed and recognized by WP_Parser.
|
||||
* In a parse tree, a token represent a leaf, that is, a node without children.
|
||||
* It is a simple generic container for a token ID and value, that can be used
|
||||
* as a base class and extended for specific use cases.
|
||||
*/
|
||||
class WP_Parser_Token {
|
||||
/**
|
||||
* Token ID represented as an integer constant.
|
||||
*
|
||||
* @var int $id
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* Byte offset in the input where the token begins.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $start;
|
||||
|
||||
/**
|
||||
* Byte length of the token in the input.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $length;
|
||||
|
||||
/**
|
||||
* Input bytes from which the token was parsed.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $input;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param int $id Token type.
|
||||
* @param int $start Byte offset in the input where the token begins.
|
||||
* @param int $length Byte length of the token in the input.
|
||||
* @param string $input Input bytes from which the token was parsed.
|
||||
*/
|
||||
public function __construct(
|
||||
int $id,
|
||||
int $start,
|
||||
int $length,
|
||||
string $input
|
||||
) {
|
||||
$this->id = $id;
|
||||
$this->start = $start;
|
||||
$this->length = $length;
|
||||
$this->input = $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw bytes of the token from the input.
|
||||
*
|
||||
* @return string The token bytes.
|
||||
*/
|
||||
public function get_bytes(): string {
|
||||
return substr( $this->input, $this->start, $this->length );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the real unquoted value of the token.
|
||||
*
|
||||
* @return string The token value.
|
||||
*/
|
||||
public function get_value(): string {
|
||||
return $this->get_bytes();
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A recursive descent parser.
|
||||
*
|
||||
* This is a dynamic recursive descent parser that can parse LL grammars.
|
||||
*
|
||||
* @TODO: Add a detailed description and list the properties that a grammar must
|
||||
* satisfy in order to be supported by this parser (e.g., no left recursion).
|
||||
*/
|
||||
class WP_Parser {
|
||||
protected $grammar;
|
||||
protected $tokens;
|
||||
protected $position;
|
||||
|
||||
public function __construct( WP_Parser_Grammar $grammar, array $tokens ) {
|
||||
$this->grammar = $grammar;
|
||||
$this->tokens = $tokens;
|
||||
$this->position = 0;
|
||||
}
|
||||
|
||||
public function parse() {
|
||||
// @TODO: Make the starting rule lookup non-grammar-specific.
|
||||
$query_rule_id = $this->grammar->get_rule_id( 'query' );
|
||||
$ast = $this->parse_recursive( $query_rule_id );
|
||||
return false === $ast ? null : $ast;
|
||||
}
|
||||
|
||||
private function parse_recursive( $rule_id ) {
|
||||
$is_terminal = $rule_id <= $this->grammar->highest_terminal_id;
|
||||
if ( $is_terminal ) {
|
||||
if ( $this->position >= count( $this->tokens ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( WP_Parser_Grammar::EMPTY_RULE_ID === $rule_id ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( $this->tokens[ $this->position ]->id === $rule_id ) {
|
||||
++$this->position;
|
||||
return $this->tokens[ $this->position - 1 ];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$branches = $this->grammar->rules[ $rule_id ];
|
||||
if ( ! count( $branches ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bale out from processing the current branch if none of its rules can
|
||||
// possibly match the current token.
|
||||
if ( isset( $this->grammar->lookahead_is_match_possible[ $rule_id ] ) ) {
|
||||
$token_id = $this->tokens[ $this->position ]->id;
|
||||
if (
|
||||
! isset( $this->grammar->lookahead_is_match_possible[ $rule_id ][ $token_id ] ) &&
|
||||
! isset( $this->grammar->lookahead_is_match_possible[ $rule_id ][ WP_Parser_Grammar::EMPTY_RULE_ID ] )
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$rule_name = $this->grammar->rule_names[ $rule_id ];
|
||||
$starting_position = $this->position;
|
||||
foreach ( $branches as $branch ) {
|
||||
$this->position = $starting_position;
|
||||
$node = new WP_Parser_Node( $rule_id, $rule_name );
|
||||
$branch_matches = true;
|
||||
foreach ( $branch as $subrule_id ) {
|
||||
$subnode = $this->parse_recursive( $subrule_id );
|
||||
if ( false === $subnode ) {
|
||||
$branch_matches = false;
|
||||
break;
|
||||
} elseif ( true === $subnode ) {
|
||||
/*
|
||||
* The subrule was matched without actually matching a token.
|
||||
* This means a special empty "ε" (epsilon) rule was matched.
|
||||
* An "ε" rule in a grammar matches an empty input of 0 bytes.
|
||||
* It is used to represent optional grammar productions.
|
||||
*/
|
||||
continue;
|
||||
} elseif ( is_array( $subnode ) && 0 === count( $subnode ) ) {
|
||||
continue;
|
||||
}
|
||||
if ( is_array( $subnode ) && ! count( $subnode ) ) {
|
||||
continue;
|
||||
}
|
||||
if ( isset( $this->grammar->fragment_ids[ $subrule_id ] ) ) {
|
||||
$node->merge_fragment( $subnode );
|
||||
} else {
|
||||
$node->append_child( $subnode );
|
||||
}
|
||||
}
|
||||
|
||||
// Negative lookahead for INTO after a valid SELECT statement.
|
||||
// If we match a SELECT statement, but there is an INTO keyword after it,
|
||||
// we're in the wrong branch and need to leave matching to a later rule.
|
||||
// @TODO: Extract this to the "WP_MySQL_Parser" class, or add support
|
||||
// for right-associative rules, which could solve this.
|
||||
// See: https://github.com/mysql/mysql-workbench/blob/8.0.38/library/parsers/grammars/MySQLParser.g4#L994
|
||||
// See: https://github.com/antlr/antlr4/issues/488
|
||||
$la = $this->tokens[ $this->position ] ?? null;
|
||||
if ( $la && 'selectStatement' === $rule_name && WP_MySQL_Lexer::INTO_SYMBOL === $la->id ) {
|
||||
$branch_matches = false;
|
||||
}
|
||||
|
||||
if ( true === $branch_matches ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $branch_matches ) {
|
||||
$this->position = $starting_position;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! $node->has_child() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
/**
|
||||
* Polyfills for PHP 8.0 string functions.
|
||||
*
|
||||
* Implementation follows the Symfony polyfill-php80 package.
|
||||
*
|
||||
* @see https://github.com/symfony/polyfill-php80
|
||||
*
|
||||
* @package wp-sqlite-integration
|
||||
*/
|
||||
|
||||
if ( ! function_exists( 'str_starts_with' ) ) {
|
||||
/**
|
||||
* Check if a string starts with a specific substring.
|
||||
*
|
||||
* @param string $haystack The string to search in.
|
||||
* @param string $needle The string to search for.
|
||||
*
|
||||
* @see https://www.php.net/manual/en/function.str-starts-with
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function str_starts_with( string $haystack, string $needle ) {
|
||||
return 0 === strncmp( $haystack, $needle, strlen( $needle ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'str_contains' ) ) {
|
||||
/**
|
||||
* Check if a string contains a specific substring.
|
||||
*
|
||||
* @param string $haystack The string to search in.
|
||||
* @param string $needle The string to search for.
|
||||
*
|
||||
* @see https://www.php.net/manual/en/function.str-contains
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function str_contains( string $haystack, string $needle ) {
|
||||
return '' === $needle || false !== strpos( $haystack, $needle );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'str_ends_with' ) ) {
|
||||
/**
|
||||
* Check if a string ends with a specific substring.
|
||||
*
|
||||
* @param string $haystack The string to search in.
|
||||
* @param string $needle The string to search for.
|
||||
*
|
||||
* @see https://www.php.net/manual/en/function.str-ends-with
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function str_ends_with( string $haystack, string $needle ) {
|
||||
if ( '' === $needle || $needle === $haystack ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( '' === $haystack ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$needle_length = strlen( $needle );
|
||||
|
||||
return $needle_length <= strlen( $haystack ) && 0 === substr_compare( $haystack, $needle, -$needle_length );
|
||||
}
|
||||
}
|
||||
+7180
File diff suppressed because it is too large
Load Diff
+382
@@ -0,0 +1,382 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* The SQLite driver uses PDO. Enable PDO function calls:
|
||||
* phpcs:disable WordPress.DB.RestrictedClasses.mysql__PDO
|
||||
* phpcs:disable WordPress.DB.RestrictedClasses.mysql__PDOStatement
|
||||
*
|
||||
* PDO uses camel case naming, enable non-snake case:
|
||||
* phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
|
||||
* phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
|
||||
*
|
||||
* PDO uses $class as a variable name, enable it:
|
||||
* phpcs:disable Universal.NamingConventions.NoReservedKeywordParameterNames.classFound
|
||||
*
|
||||
* Some PDOStatement methods use $var as a variable name, enable it:
|
||||
* phpcs:disable Universal.NamingConventions.NoReservedKeywordParameterNames.varFound
|
||||
*
|
||||
* We use traits to support different PHP versions with incompatible PDO statement
|
||||
* method signatures. For that, enable multiple object structures in one file:
|
||||
* phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound
|
||||
*/
|
||||
|
||||
/**
|
||||
* Some PDOStatement methods are not compatible across different PHP versions.
|
||||
* To address "Declaration of ... should be compatible with ..." PHP warnings,
|
||||
* we conditionally define traits with different APIs based on the PHP version.
|
||||
*/
|
||||
if ( PHP_VERSION_ID < 80000 ) {
|
||||
trait WP_PDO_Proxy_Statement_PHP_Compat {
|
||||
/**
|
||||
* Set the default fetch mode for this statement.
|
||||
*
|
||||
* @param int $mode The fetch mode to set as the default.
|
||||
* @param mixed $params Additional parameters for the default fetch mode.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public function setFetchMode( $mode, $params = null ): bool {
|
||||
// Do not pass additional arguments when they are NULL to prevent
|
||||
// "fetch mode doesn't allow any extra arguments" error.
|
||||
if ( null === $params ) {
|
||||
return $this->setDefaultFetchMode( $mode );
|
||||
}
|
||||
return $this->setDefaultFetchMode( $mode, $params );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all remaining rows from the result set.
|
||||
*
|
||||
* @param int $mode The fetch mode to use.
|
||||
* @param mixed $class_name With PDO::FETCH_CLASS, the name of the class to instantiate.
|
||||
* @param mixed $constructor_args With PDO::FETCH_CLASS, the parameters to pass to the class constructor.
|
||||
* @return array The result set as an array of rows.
|
||||
*/
|
||||
public function fetchAll( $mode = null, $class_name = null, $constructor_args = null ): array {
|
||||
// Do not pass additional arguments when they are NULL to prevent
|
||||
// "Extraneous additional parameters" error.
|
||||
if ( null === $class_name && null === $constructor_args ) {
|
||||
return $this->fetchAllRows( $mode );
|
||||
}
|
||||
return $this->fetchAllRows( $mode, $class_name, $constructor_args );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trait WP_PDO_Proxy_Statement_PHP_Compat {
|
||||
/**
|
||||
* Set the default fetch mode for this statement.
|
||||
*
|
||||
* @param int $mode The fetch mode to set as the default.
|
||||
* @param mixed $args Additional parameters for the default fetch mode.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function setFetchMode( $mode, ...$args ): bool {
|
||||
return $this->setDefaultFetchMode( $mode, ...$args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all remaining rows from the result set.
|
||||
*
|
||||
* @param int $mode The fetch mode to use.
|
||||
* @param mixed $args Additional parameters for the fetch mode.
|
||||
* @return array The result set as an array of rows.
|
||||
*/
|
||||
public function fetchAll( $mode = PDO::FETCH_DEFAULT, ...$args ): array {
|
||||
return $this->fetchAllRows( $mode, ...$args );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PDOStatement implementation that operates on in-memory data.
|
||||
*
|
||||
* This class implements a complete PDOStatement interface on top of PHP arrays.
|
||||
* It is used for result sets that are composed or transformed in the PHP layer.
|
||||
*
|
||||
* PDO supports the following fetch modes:
|
||||
* - PDO::FETCH_DEFAULT: current default fetch mode (available from PHP 8.0)
|
||||
* - PDO::FETCH_BOTH: default
|
||||
* - PDO::FETCH_NUM: numeric array
|
||||
* - PDO::FETCH_ASSOC: associative array
|
||||
* - PDO::FETCH_NAMED: associative array retaining duplicate columns
|
||||
* - PDO::FETCH_COLUMN: single column value [1 extra arg]
|
||||
* - PDO::FETCH_KEY_PAIR: key-value pair
|
||||
* - PDO::FETCH_OBJ: object (stdClass)
|
||||
* - PDO::FETCH_CLASS: object (custom class) [1-2 extra args]
|
||||
* - PDO::FETCH_INTO: update an exisisting object, can't be used with fetchAll() [1 extra arg]
|
||||
* - PDO::FETCH_LAZY: lazy fetch via PDORow, can't be used with fetchAll()
|
||||
* - PDO::FETCH_BOUND: bind values to PHP variables, can't be used with fetchAll()
|
||||
* - PDO::FETCH_FUNC: custom function, only works with fetchAll(), can't be default [1 extra arg]
|
||||
*/
|
||||
class WP_PDO_Proxy_Statement extends PDOStatement {
|
||||
use WP_PDO_Proxy_Statement_PHP_Compat;
|
||||
|
||||
/**
|
||||
* The original PDO statement.
|
||||
*
|
||||
* @var PDOStatement
|
||||
*/
|
||||
private $statement;
|
||||
|
||||
/**
|
||||
* The number of affected rows.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
private $affected_rows;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param PDOStatement $statement The original PDO statement.
|
||||
* @param int $affected_rows The number of affected rows.
|
||||
*/
|
||||
public function __construct(
|
||||
PDOStatement $statement,
|
||||
?int $affected_rows = null
|
||||
) {
|
||||
$this->statement = $statement;
|
||||
$this->affected_rows = $affected_rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a prepared statement.
|
||||
*
|
||||
* @param mixed $params The values to bind to the parameters of the prepared statement.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public function execute( $params = null ): bool {
|
||||
return $this->statement->execute( $params );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of columns in the result set.
|
||||
*
|
||||
* @return int The number of columns in the result set.
|
||||
*/
|
||||
public function columnCount(): int {
|
||||
return $this->statement->columnCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of rows affected by the statement.
|
||||
*
|
||||
* @return int The number of rows affected by the statement.
|
||||
*/
|
||||
public function rowCount(): int {
|
||||
return $this->affected_rows ?? $this->statement->rowCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the next row from the result set.
|
||||
*
|
||||
* @param int|null $mode The fetch mode. Controls how the row is returned.
|
||||
* Default: PDO::FETCH_DEFAULT (null for PHP < 8.0)
|
||||
* @param int|null $cursorOrientation The cursor orientation. Controls which row is returned.
|
||||
* Default: PDO::FETCH_ORI_NEXT (null for PHP < 8.0)
|
||||
* @param int|null $cursorOffset The cursor offset. Controls which row is returned.
|
||||
* Default: 0 (null for PHP < 8.0)
|
||||
* @return mixed The row data formatted according to the fetch mode;
|
||||
* false if there are no more rows or a failure occurs.
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function fetch(
|
||||
$mode = 0, // PDO::FETCH_DEFAULT (available from PHP 8.0)
|
||||
$cursorOrientation = 0,
|
||||
$cursorOffset = 0
|
||||
) {
|
||||
return $this->statement->fetch( $mode, $cursorOrientation, $cursorOffset );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single column from the next row of a result set.
|
||||
*
|
||||
* @param int $column The index of the column to fetch (0-indexed).
|
||||
* @return mixed The value of the column; false if there are no more rows.
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function fetchColumn( $column = 0 ) {
|
||||
return $this->statement->fetchColumn( $column );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the next row as an object.
|
||||
*
|
||||
* @param string $class The name of the class to instantiate.
|
||||
* @param array $constructorArgs The parameters to pass to the class constructor.
|
||||
* @return object The next row as an object.
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function fetchObject( $class = 'stdClass', $constructorArgs = array() ) {
|
||||
return $this->statement->fetchObject( $class, $constructorArgs );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata for a column in a result set.
|
||||
*
|
||||
* @param int $column The index of the column (0-indexed).
|
||||
* @return array|false The column metadata as an associative array,
|
||||
* or false if the column does not exist.
|
||||
*/
|
||||
public function getColumnMeta( $column ): array {
|
||||
throw new RuntimeException( 'Not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the SQLSTATE associated with the last statement operation.
|
||||
*
|
||||
* @return string|null The SQLSTATE error code (as defined by the ANSI SQL standard),
|
||||
* or null if there is no error.
|
||||
*/
|
||||
public function errorCode(): ?string {
|
||||
throw new RuntimeException( 'Not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch error information associated with the last statement operation.
|
||||
*
|
||||
* @return array The array consists of at least the following fields:
|
||||
* 0: SQLSTATE error code (as defined by the ANSI SQL standard).
|
||||
* 1: Driver-specific error code.
|
||||
* 2: Driver-specific error message.
|
||||
*/
|
||||
public function errorInfo(): array {
|
||||
throw new RuntimeException( 'Not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a statement attribute.
|
||||
*
|
||||
* @param int $attribute The attribute to get.
|
||||
* @return mixed The value of the attribute.
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function getAttribute( $attribute ) {
|
||||
return $this->statement->getAttribute( $attribute );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a statement attribute.
|
||||
*
|
||||
* @param int $attribute The attribute to set.
|
||||
* @param mixed $value The value of the attribute.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public function setAttribute( $attribute, $value ): bool {
|
||||
return $this->statement->setAttribute( $attribute, $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get result set as iterator.
|
||||
*
|
||||
* @return Iterator The iterator for the result set.
|
||||
*/
|
||||
public function getIterator(): Iterator {
|
||||
throw new RuntimeException( 'Not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances to the next rowset in a multi-rowset statement handle.
|
||||
*
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public function nextRowset(): bool {
|
||||
throw new RuntimeException( 'Not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the cursor, enabling the statement to be executed again.
|
||||
*
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public function closeCursor(): bool {
|
||||
throw new RuntimeException( 'Not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a column to a PHP variable.
|
||||
*
|
||||
* @param int|string $column Number of the column (1-indexed) or name of the column in the result set.
|
||||
* @param mixed $var PHP variable to which the column will be bound.
|
||||
* @param int $type Data type of the parameter, specified by the PDO::PARAM_* constants.
|
||||
* @param int $maxLength A hint for pre-allocation.
|
||||
* @param mixed $driverOptions Optional parameters for the driver.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public function bindColumn( $column, &$var, $type = null, $maxLength = null, $driverOptions = null ): bool {
|
||||
throw new RuntimeException( 'Not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a parameter to a PHP variable.
|
||||
*
|
||||
* @param int|string $param Parameter identifier. Either a 1-indexed position of the parameter or a named parameter.
|
||||
* @param mixed $var PHP variable to which the parameter will be bound.
|
||||
* @param int $type Data type of the parameter, specified by the PDO::PARAM_* constants.
|
||||
* @param int $maxLength Length of the data type.
|
||||
* @param mixed $driverOptions Optional parameters for the driver.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public function bindParam( $param, &$var, $type = PDO::PARAM_STR, $maxLength = 0, $driverOptions = null ): bool {
|
||||
throw new RuntimeException( 'Not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a value to a parameter.
|
||||
*
|
||||
* @param int|string $param Parameter identifier. Either a 1-indexed position of the parameter or a named parameter.
|
||||
* @param mixed $value The value to bind to the parameter.
|
||||
* @param int $type Data type of the parameter, specified by the PDO::PARAM_* constants.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public function bindValue( $param, $value, $type = PDO::PARAM_STR ): bool {
|
||||
throw new RuntimeException( 'Not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump information about the statement.
|
||||
*
|
||||
* Dupms the SQL query and parameters information.
|
||||
*
|
||||
* @return bool|null Returns null, or false on failure.
|
||||
*/
|
||||
public function debugDumpParams(): ?bool {
|
||||
throw new RuntimeException( 'Not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all remaining rows from the result set.
|
||||
*
|
||||
* This is used internally by the "WP_PDO_Proxy_Statement_PHP_Compat" trait,
|
||||
* that is defined conditionally based on the current PHP version.
|
||||
*
|
||||
* @param int $mode The fetch mode to use.
|
||||
* @param mixed $args Additional parameters for the fetch mode.
|
||||
* @return array The result set as an array of rows.
|
||||
*/
|
||||
private function fetchAllRows( $mode = null, ...$args ): array {
|
||||
return $this->statement->fetchAll( $mode, ...$args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default fetch mode for this statement.
|
||||
*
|
||||
* This is used internally by the "WP_PDO_Proxy_Statement_PHP_Compat" trait,
|
||||
* that is defined conditionally based on the current PHP version.
|
||||
*
|
||||
* @param int $mode The fetch mode to set as the default.
|
||||
* @param mixed $args Additional parameters for the default fetch mode.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
private function setDefaultFetchMode( $mode, ...$args ): bool {
|
||||
return $this->statement->setFetchMode( $mode, ...$args );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Polyfill ValueError for PHP < 8.0.
|
||||
*/
|
||||
if ( PHP_VERSION_ID < 80000 && ! class_exists( ValueError::class ) ) {
|
||||
class ValueError extends Error {
|
||||
}
|
||||
}
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* SQLite database configurator.
|
||||
*
|
||||
* This class initializes and configures the SQLite database, so that it can be
|
||||
* used by the SQLite driver to translate and emulate MySQL queries in SQLite.
|
||||
*
|
||||
* The configurator ensures that tables required for emulating MySQL behaviors
|
||||
* are created and populated with necessary data. It is also able to partially
|
||||
* repair and update these tables and metadata in case of database corruption.
|
||||
*/
|
||||
class WP_SQLite_Configurator {
|
||||
/**
|
||||
* The SQLite driver instance.
|
||||
*
|
||||
* @var WP_PDO_MySQL_On_SQLite
|
||||
*/
|
||||
private $driver;
|
||||
|
||||
/**
|
||||
* A service for managing MySQL INFORMATION_SCHEMA tables in SQLite.
|
||||
*
|
||||
* @var WP_SQLite_Information_Schema_Builder
|
||||
*/
|
||||
private $schema_builder;
|
||||
|
||||
/**
|
||||
* A service for reconstructing the MySQL INFORMATION_SCHEMA tables in SQLite.
|
||||
*
|
||||
* @var WP_SQLite_Information_Schema_Reconstructor
|
||||
*/
|
||||
private $schema_reconstructor;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param WP_PDO_MySQL_On_SQLite $driver The SQLite driver instance.
|
||||
* @param WP_SQLite_Information_Schema_Builder $schema_builder The information schema builder instance.
|
||||
*/
|
||||
public function __construct(
|
||||
WP_PDO_MySQL_On_SQLite $driver,
|
||||
WP_SQLite_Information_Schema_Builder $schema_builder
|
||||
) {
|
||||
$this->driver = $driver;
|
||||
$this->schema_builder = $schema_builder;
|
||||
$this->schema_reconstructor = new WP_SQLite_Information_Schema_Reconstructor(
|
||||
$driver,
|
||||
$schema_builder
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the SQLite database is configured.
|
||||
*
|
||||
* This method checks if the database is configured for the latest SQLite
|
||||
* driver version, and if it is not, it will configure the database.
|
||||
*/
|
||||
public function ensure_database_configured(): void {
|
||||
$version = SQLITE_DRIVER_VERSION;
|
||||
$db_version = $this->driver->get_saved_driver_version();
|
||||
if ( version_compare( $version, $db_version ) > 0 ) {
|
||||
$this->configure_database();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the SQLite database.
|
||||
*
|
||||
* This method creates tables used for emulating MySQL behaviors in SQLite,
|
||||
* and populates them with necessary data. When it is used with an already
|
||||
* configured database, it will update the configuration as per the current
|
||||
* SQLite driver version and attempt to repair any configuration corruption.
|
||||
*/
|
||||
public function configure_database(): void {
|
||||
// Use an EXCLUSIVE transaction to prevent multiple connections
|
||||
// from attempting to configure the database at the same time.
|
||||
$this->driver->execute_sqlite_query( 'BEGIN EXCLUSIVE TRANSACTION' );
|
||||
try {
|
||||
$this->ensure_global_variables_table();
|
||||
$this->schema_builder->ensure_information_schema_tables();
|
||||
$this->schema_reconstructor->ensure_correct_information_schema();
|
||||
$this->save_current_driver_version();
|
||||
$this->ensure_database_data();
|
||||
} catch ( Throwable $e ) {
|
||||
$this->driver->execute_sqlite_query( 'ROLLBACK' );
|
||||
throw $e;
|
||||
}
|
||||
$this->driver->execute_sqlite_query( 'COMMIT' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the global variables table exists.
|
||||
*
|
||||
* This method configures a database table to store MySQL global variables
|
||||
* and other internal configuration values.
|
||||
*/
|
||||
private function ensure_global_variables_table(): void {
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
'CREATE TABLE IF NOT EXISTS %s (name TEXT PRIMARY KEY, value TEXT)',
|
||||
$this->driver->get_connection()->quote_identifier(
|
||||
WP_PDO_MySQL_On_SQLite::GLOBAL_VARIABLES_TABLE_NAME
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the database data is correctly populated.
|
||||
*
|
||||
* This method ensures that the "INFORMATION_SCHEMA.SCHEMATA" table contains
|
||||
* records for both the "INFORMATION_SCHEMA" database and the user database.
|
||||
* At the moment, only a single user database is supported.
|
||||
*
|
||||
* Additionally, this method ensures that the user database name is stored
|
||||
* correctly in all the information schema tables.
|
||||
*/
|
||||
public function ensure_database_data(): void {
|
||||
// Get all databases from the "SCHEMATA" table.
|
||||
$schemata_table = $this->schema_builder->get_table_name( false, 'schemata' );
|
||||
$databases = $this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
'SELECT SCHEMA_NAME FROM %s',
|
||||
$this->driver->get_connection()->quote_identifier( $schemata_table )
|
||||
)
|
||||
)->fetchAll( PDO::FETCH_COLUMN ); // phpcs:disable WordPress.DB.RestrictedClasses.mysql__PDO
|
||||
|
||||
// Ensure that the "INFORMATION_SCHEMA" database record exists.
|
||||
if ( ! in_array( 'information_schema', $databases, true ) ) {
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
'INSERT INTO %s (SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME) VALUES (?, ?, ?)',
|
||||
$this->driver->get_connection()->quote_identifier( $schemata_table )
|
||||
),
|
||||
// The "INFORMATION_SCHEMA" database stays on "utf8mb3" even in MySQL 8 and 9.
|
||||
array( 'information_schema', 'utf8mb3', 'utf8mb3_general_ci' )
|
||||
);
|
||||
}
|
||||
|
||||
// Get the existing user database name.
|
||||
$existing_user_db_name = null;
|
||||
foreach ( $databases as $database ) {
|
||||
if ( 'information_schema' !== strtolower( $database ) ) {
|
||||
$existing_user_db_name = $database;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that the user database record exists.
|
||||
if ( null === $existing_user_db_name ) {
|
||||
$existing_user_db_name = WP_SQLite_Information_Schema_Builder::SAVED_DATABASE_NAME;
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
'INSERT INTO %s (SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME) VALUES (?, ?, ?)',
|
||||
$this->driver->get_connection()->quote_identifier( $schemata_table )
|
||||
),
|
||||
// @TODO: This should probably be version-dependent.
|
||||
// Before MySQL 8, the default was different.
|
||||
array( $existing_user_db_name, 'utf8mb4', 'utf8mb4_0900_ai_ci' )
|
||||
);
|
||||
}
|
||||
|
||||
// Migrate from older versions without dynamic database names.
|
||||
$saved_database_name = WP_SQLite_Information_Schema_Builder::SAVED_DATABASE_NAME;
|
||||
if ( $saved_database_name !== $existing_user_db_name ) {
|
||||
// INFORMATION_SCHEMA.SCHEMATA
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
"UPDATE %s SET SCHEMA_NAME = ? WHERE SCHEMA_NAME != 'information_schema'",
|
||||
$this->driver->get_connection()->quote_identifier( $schemata_table )
|
||||
),
|
||||
array( $saved_database_name )
|
||||
);
|
||||
|
||||
// INFORMATION_SCHEMA.TABLES
|
||||
$tables_table = $this->schema_builder->get_table_name( false, 'tables' );
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
"UPDATE %s SET TABLE_SCHEMA = ? WHERE TABLE_SCHEMA != 'information_schema'",
|
||||
$this->driver->get_connection()->quote_identifier( $tables_table )
|
||||
),
|
||||
array( $saved_database_name )
|
||||
);
|
||||
|
||||
// INFORMATION_SCHEMA.COLUMNS
|
||||
$columns_table = $this->schema_builder->get_table_name( false, 'columns' );
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
"UPDATE %s SET TABLE_SCHEMA = ? WHERE TABLE_SCHEMA != 'information_schema'",
|
||||
$this->driver->get_connection()->quote_identifier( $columns_table )
|
||||
),
|
||||
array( $saved_database_name )
|
||||
);
|
||||
|
||||
// INFORMATION_SCHEMA.STATISTICS
|
||||
$statistics_table = $this->schema_builder->get_table_name( false, 'statistics' );
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
"UPDATE %s SET TABLE_SCHEMA = ?, INDEX_SCHEMA = ? WHERE TABLE_SCHEMA != 'information_schema'",
|
||||
$this->driver->get_connection()->quote_identifier( $statistics_table )
|
||||
),
|
||||
array( $saved_database_name, $saved_database_name )
|
||||
);
|
||||
|
||||
// INFORMATION_SCHEMA.TABLE_CONSTRAINTS
|
||||
$table_constraints_table = $this->schema_builder->get_table_name( false, 'table_constraints' );
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
"UPDATE %s SET TABLE_SCHEMA = ?, CONSTRAINT_SCHEMA = ? WHERE TABLE_SCHEMA != 'information_schema'",
|
||||
$this->driver->get_connection()->quote_identifier( $table_constraints_table )
|
||||
),
|
||||
array( $saved_database_name, $saved_database_name )
|
||||
);
|
||||
|
||||
// INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS
|
||||
$referential_constraints_table = $this->schema_builder->get_table_name( false, 'referential_constraints' );
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
"UPDATE %s SET CONSTRAINT_SCHEMA = ?, UNIQUE_CONSTRAINT_SCHEMA = ? WHERE CONSTRAINT_SCHEMA != 'information_schema'",
|
||||
$this->driver->get_connection()->quote_identifier( $referential_constraints_table )
|
||||
),
|
||||
array( $saved_database_name, $saved_database_name )
|
||||
);
|
||||
|
||||
// INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
$key_column_usage_table = $this->schema_builder->get_table_name( false, 'key_column_usage' );
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
"UPDATE %s
|
||||
SET
|
||||
TABLE_SCHEMA = ?,
|
||||
CONSTRAINT_SCHEMA = ?,
|
||||
REFERENCED_TABLE_SCHEMA = CASE WHEN REFERENCED_TABLE_SCHEMA IS NULL THEN NULL ELSE ? END
|
||||
WHERE TABLE_SCHEMA != 'information_schema'",
|
||||
$this->driver->get_connection()->quote_identifier( $key_column_usage_table )
|
||||
),
|
||||
array( $saved_database_name, $saved_database_name, $saved_database_name )
|
||||
);
|
||||
|
||||
// INFORMATION_SCHEMA.CHECK_CONSTRAINTS
|
||||
$check_constraints_table = $this->schema_builder->get_table_name( false, 'check_constraints' );
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
"UPDATE %s SET CONSTRAINT_SCHEMA = ? WHERE CONSTRAINT_SCHEMA != 'information_schema'",
|
||||
$this->driver->get_connection()->quote_identifier( $check_constraints_table )
|
||||
),
|
||||
array( $saved_database_name )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current SQLite driver version.
|
||||
*
|
||||
* This method saves the current SQLite driver version to the database.
|
||||
*/
|
||||
private function save_current_driver_version(): void {
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
'INSERT INTO %s (name, value) VALUES (?, ?) ON CONFLICT(name) DO UPDATE SET value = ?',
|
||||
$this->driver->get_connection()->quote_identifier(
|
||||
WP_PDO_MySQL_On_SQLite::GLOBAL_VARIABLES_TABLE_NAME
|
||||
)
|
||||
),
|
||||
array(
|
||||
WP_PDO_MySQL_On_SQLite::DRIVER_VERSION_VARIABLE_NAME,
|
||||
SQLITE_DRIVER_VERSION,
|
||||
SQLITE_DRIVER_VERSION,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
/*
|
||||
* The SQLite connection uses PDO. Enable PDO function calls:
|
||||
* phpcs:disable WordPress.DB.RestrictedClasses.mysql__PDO
|
||||
*/
|
||||
|
||||
/**
|
||||
* SQLite connection.
|
||||
*
|
||||
* This class configures and encapsulates the connection to an SQLite database.
|
||||
* It requires PDO with the SQLite driver, and currently, it is only a simple
|
||||
* wrapper that leaks some of the PDO APIs (returns PDOStatement values, etc.).
|
||||
* In the future, we may abstract it away from PDO and support SQLite3 as well.
|
||||
*/
|
||||
class WP_SQLite_Connection {
|
||||
/**
|
||||
* The default timeout in seconds for SQLite to wait for a writable lock.
|
||||
*/
|
||||
const DEFAULT_SQLITE_TIMEOUT = 10;
|
||||
|
||||
/**
|
||||
* The supported SQLite journal modes.
|
||||
*
|
||||
* See: https://www.sqlite.org/pragma.html#pragma_journal_mode
|
||||
*/
|
||||
const SQLITE_JOURNAL_MODES = array(
|
||||
'DELETE',
|
||||
'TRUNCATE',
|
||||
'PERSIST',
|
||||
'MEMORY',
|
||||
'WAL',
|
||||
'OFF',
|
||||
);
|
||||
|
||||
/**
|
||||
* The PDO connection for SQLite.
|
||||
*
|
||||
* @var PDO
|
||||
*/
|
||||
private $pdo;
|
||||
|
||||
/**
|
||||
* A query logger callback.
|
||||
*
|
||||
* @var callable(string, array): void
|
||||
*/
|
||||
private $query_logger;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Set up an SQLite connection.
|
||||
*
|
||||
* @param array $options {
|
||||
* An array of options.
|
||||
*
|
||||
* @type string|null $path Optional. SQLite database path.
|
||||
* For in-memory database, use ':memory:'.
|
||||
* Must be set when PDO instance is not provided.
|
||||
* @type PDO|null $pdo Optional. PDO instance with SQLite connection.
|
||||
* If not provided, a new PDO instance will be created.
|
||||
* @type int|null $timeout Optional. SQLite timeout in seconds.
|
||||
* The time to wait for a writable lock.
|
||||
* @type string|null $journal_mode Optional. SQLite journal mode.
|
||||
* }
|
||||
*
|
||||
* @throws InvalidArgumentException When some connection options are invalid.
|
||||
* @throws PDOException When the driver initialization fails.
|
||||
*/
|
||||
public function __construct( array $options ) {
|
||||
// Setup PDO connection.
|
||||
if ( isset( $options['pdo'] ) && $options['pdo'] instanceof PDO ) {
|
||||
$this->pdo = $options['pdo'];
|
||||
} else {
|
||||
if ( ! isset( $options['path'] ) || ! is_string( $options['path'] ) ) {
|
||||
throw new InvalidArgumentException( 'Option "path" is required when "connection" is not provided.' );
|
||||
}
|
||||
$pdo_class = PHP_VERSION_ID >= 80400 ? PDO\SQLite::class : PDO::class;
|
||||
$this->pdo = new $pdo_class( 'sqlite:' . $options['path'] );
|
||||
}
|
||||
|
||||
// Throw exceptions on error.
|
||||
$this->pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
|
||||
|
||||
// Configure SQLite timeout.
|
||||
if ( isset( $options['timeout'] ) && is_int( $options['timeout'] ) ) {
|
||||
$timeout = $options['timeout'];
|
||||
} else {
|
||||
$timeout = self::DEFAULT_SQLITE_TIMEOUT;
|
||||
}
|
||||
$this->pdo->setAttribute( PDO::ATTR_TIMEOUT, $timeout );
|
||||
|
||||
// Configure SQLite journal mode.
|
||||
$journal_mode = $options['journal_mode'] ?? null;
|
||||
if ( $journal_mode && in_array( $journal_mode, self::SQLITE_JOURNAL_MODES, true ) ) {
|
||||
$this->query( 'PRAGMA journal_mode = ' . $journal_mode );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a query in SQLite.
|
||||
*
|
||||
* @param string $sql The query to execute.
|
||||
* @param array $params The query parameters.
|
||||
* @throws PDOException When the query execution fails.
|
||||
* @return PDOStatement The PDO statement object.
|
||||
*/
|
||||
public function query( string $sql, array $params = array() ): PDOStatement {
|
||||
if ( $this->query_logger ) {
|
||||
( $this->query_logger )( $sql, $params );
|
||||
}
|
||||
$stmt = $this->pdo->prepare( $sql );
|
||||
$stmt->execute( $params );
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a SQLite query for execution.
|
||||
*
|
||||
* @param string $sql The query to prepare.
|
||||
* @return PDOStatement The prepared statement.
|
||||
* @throws PDOException When the query preparation fails.
|
||||
*/
|
||||
public function prepare( string $sql ): PDOStatement {
|
||||
if ( $this->query_logger ) {
|
||||
( $this->query_logger )( $sql, array() );
|
||||
}
|
||||
return $this->pdo->prepare( $sql );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the last inserted row.
|
||||
*
|
||||
* @return string The ID of the last inserted row.
|
||||
*/
|
||||
public function get_last_insert_id(): string {
|
||||
return $this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Quote a value for use in a query.
|
||||
*
|
||||
* @param mixed $value The value to quote.
|
||||
* @param int $type The type of the value.
|
||||
* @return string The quoted value.
|
||||
*/
|
||||
public function quote( $value, int $type = PDO::PARAM_STR ): string {
|
||||
return $this->pdo->quote( $value, $type );
|
||||
}
|
||||
|
||||
/**
|
||||
* Quote an SQLite identifier.
|
||||
*
|
||||
* Wraps the identifier in backticks and escapes backtick characters within.
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* Quoted identifiers in SQLite are represented by string constants:
|
||||
*
|
||||
* A string constant is formed by enclosing the string in single quotes (').
|
||||
* A single quote within the string can be encoded by putting two single
|
||||
* quotes in a row - as in Pascal. C-style escapes using the backslash
|
||||
* character are not supported because they are not standard SQL.
|
||||
*
|
||||
* See: https://www.sqlite.org/lang_expr.html#literal_values_constants_
|
||||
*
|
||||
* Although sparsely documented, this applies to backtick and double quoted
|
||||
* string constants as well, so only the quote character needs to be escaped.
|
||||
*
|
||||
* For more details, see the grammar for SQLite table and column names:
|
||||
*
|
||||
* - https://github.com/sqlite/sqlite/blob/873fc5dff2a781251f2c9bd2c791a5fac45b7a2b/src/tokenize.c#L395-L419
|
||||
* - https://github.com/sqlite/sqlite/blob/873fc5dff2a781251f2c9bd2c791a5fac45b7a2b/src/parse.y#L321-L338
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* We use backtick quotes instead of the SQL standard double quotes, due to
|
||||
* an SQLite quirk causing double-quoted strings to be accepted as literals:
|
||||
*
|
||||
* This misfeature means that a misspelled double-quoted identifier will
|
||||
* be interpreted as a string literal, rather than generating an error.
|
||||
*
|
||||
* See: https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted
|
||||
*
|
||||
* @param string $unquoted_identifier The unquoted identifier value.
|
||||
* @return string The quoted identifier value.
|
||||
*/
|
||||
public function quote_identifier( string $unquoted_identifier ): string {
|
||||
return '`' . str_replace( '`', '``', $unquoted_identifier ) . '`';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDO object.
|
||||
*
|
||||
* @return PDO
|
||||
*/
|
||||
public function get_pdo(): PDO {
|
||||
return $this->pdo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a logger for the queries.
|
||||
*
|
||||
* @param callable(string, array): void $logger A query logger callback.
|
||||
*/
|
||||
public function set_query_logger( callable $logger ): void {
|
||||
$this->query_logger = $logger;
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
class WP_SQLite_Driver_Exception extends PDOException {
|
||||
/**
|
||||
* The SQLite driver that originated the exception.
|
||||
*
|
||||
* @var WP_PDO_MySQL_On_SQLite
|
||||
*/
|
||||
private $driver;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param WP_PDO_MySQL_On_SQLite $driver The SQLite driver that originated the exception.
|
||||
* @param string $message The exception message.
|
||||
* @param int|string $code The exception code. In PDO, it can be a string with value of SQLSTATE.
|
||||
* @param Throwable|null $previous The previous throwable used for the exception chaining.
|
||||
*/
|
||||
public function __construct(
|
||||
WP_PDO_MySQL_On_SQLite $driver,
|
||||
string $message,
|
||||
$code = 0,
|
||||
?Throwable $previous = null
|
||||
) {
|
||||
parent::__construct( $message, 0, $previous );
|
||||
$this->code = $code;
|
||||
$this->driver = $driver;
|
||||
}
|
||||
|
||||
public function getDriver(): WP_PDO_MySQL_On_SQLite {
|
||||
return $this->driver;
|
||||
}
|
||||
}
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* The SQLite driver uses PDO. Enable PDO function calls:
|
||||
* phpcs:disable WordPress.DB.RestrictedClasses.mysql__PDO
|
||||
*/
|
||||
|
||||
/**
|
||||
* For back compatibility with dependencies that use their own loader scripts
|
||||
* (e.g., WP CLI SQLite Command), ensure the new PDO-based classes are loaded.
|
||||
*/
|
||||
require_once __DIR__ . '/class-wp-pdo-mysql-on-sqlite.php';
|
||||
require_once __DIR__ . '/class-wp-pdo-proxy-statement.php';
|
||||
|
||||
/**
|
||||
* Deprecated: A proxy of the WP_PDO_MySQL_On_SQLite class preserving legacy API.
|
||||
*
|
||||
* This is a temporary class to preserve the legacy API for easier transition
|
||||
* to the new PDO-based API, developed in the "WP_PDO_MySQL_On_SQLite" class.
|
||||
*/
|
||||
class WP_SQLite_Driver {
|
||||
/**
|
||||
* The SQLite engine version.
|
||||
*
|
||||
* This is a mysqli-like property that is needed to avoid a PHP warning in
|
||||
* the WordPress health info. The "WP_Debug_Data::get_wp_database()" method
|
||||
* calls "$wpdb->dbh->client_info" - a mysqli-specific abstraction leak.
|
||||
*
|
||||
* @TODO: This should be fixed in WordPress core.
|
||||
*
|
||||
* See:
|
||||
* https://github.com/WordPress/wordpress-develop/blob/bcdca3f9925f1d3eca7b78d231837c0caf0c8c24/src/wp-admin/includes/class-wp-debug-data.php#L1579
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $client_info;
|
||||
|
||||
/**
|
||||
* The MySQL-on-SQLite driver instance.
|
||||
*
|
||||
* @var WP_PDO_MySQL_On_SQLite
|
||||
*/
|
||||
private $mysql_on_sqlite_driver;
|
||||
|
||||
/**
|
||||
* Results of the last emulated query.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
private $last_result;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Set up an SQLite connection and the MySQL-on-SQLite driver.
|
||||
*
|
||||
* @param WP_SQLite_Connection $connection A SQLite database connection.
|
||||
* @param string $database The database name.
|
||||
*
|
||||
* @throws WP_SQLite_Driver_Exception When the driver initialization fails.
|
||||
*/
|
||||
public function __construct(
|
||||
WP_SQLite_Connection $connection,
|
||||
string $database,
|
||||
int $mysql_version = 80038
|
||||
) {
|
||||
$this->mysql_on_sqlite_driver = new WP_PDO_MySQL_On_SQLite(
|
||||
sprintf( 'mysql-on-sqlite:dbname=%s', $database ),
|
||||
null,
|
||||
null,
|
||||
array(
|
||||
'mysql_version' => $mysql_version,
|
||||
'pdo' => $connection->get_pdo(),
|
||||
)
|
||||
);
|
||||
$this->main_db_name = $database;
|
||||
$this->client_info = $this->mysql_on_sqlite_driver->client_info;
|
||||
|
||||
$connection->get_pdo()->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SQLite connection instance.
|
||||
*
|
||||
* @return WP_SQLite_Connection
|
||||
*/
|
||||
public function get_connection(): WP_SQLite_Connection {
|
||||
return $this->mysql_on_sqlite_driver->get_connection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version of the SQLite engine.
|
||||
*
|
||||
* @return string SQLite engine version as a string.
|
||||
*/
|
||||
public function get_sqlite_version(): string {
|
||||
return $this->mysql_on_sqlite_driver->get_sqlite_version();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SQLite driver version saved in the database.
|
||||
*
|
||||
* The saved driver version corresponds to the latest version of the SQLite
|
||||
* driver that was used to initialize and configure the SQLite database.
|
||||
*
|
||||
* @return string SQLite driver version as a string.
|
||||
* @throws PDOException When the query execution fails.
|
||||
*/
|
||||
public function get_saved_driver_version(): string {
|
||||
return $this->mysql_on_sqlite_driver->get_saved_driver_version();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific SQL mode is active.
|
||||
*
|
||||
* @param string $mode The SQL mode to check.
|
||||
* @return bool True if the SQL mode is active, false otherwise.
|
||||
*/
|
||||
public function is_sql_mode_active( string $mode ): bool {
|
||||
return $this->mysql_on_sqlite_driver->is_sql_mode_active( $mode );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last executed MySQL query.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function get_last_mysql_query(): ?string {
|
||||
return $this->mysql_on_sqlite_driver->get_last_mysql_query();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SQLite queries executed for the last MySQL query.
|
||||
*
|
||||
* @return array{ sql: string, params: array }[]
|
||||
*/
|
||||
public function get_last_sqlite_queries(): array {
|
||||
return $this->mysql_on_sqlite_driver->get_last_sqlite_queries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the auto-increment value generated for the last query.
|
||||
*
|
||||
* @return int|string
|
||||
*/
|
||||
public function get_insert_id() {
|
||||
return $this->mysql_on_sqlite_driver->get_insert_id();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query Full SQL statement string.
|
||||
* @param int $fetch_mode PDO fetch mode. Default is PDO::FETCH_OBJ.
|
||||
* @param array ...$fetch_mode_args Additional fetch mode arguments.
|
||||
*
|
||||
* @return mixed Return value, depending on the query type.
|
||||
*
|
||||
* @throws WP_SQLite_Driver_Exception When the query execution fails.
|
||||
*/
|
||||
public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mode_args ) {
|
||||
$stmt = $this->mysql_on_sqlite_driver->query( $query, $fetch_mode, ...$fetch_mode_args );
|
||||
|
||||
if ( $stmt->columnCount() > 0 ) {
|
||||
$this->last_result = $stmt->fetchAll( $fetch_mode );
|
||||
} else {
|
||||
$this->last_result = $stmt->rowCount();
|
||||
}
|
||||
return $this->last_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenize a MySQL query and initialize a parser.
|
||||
*
|
||||
* @param string $query The MySQL query to parse.
|
||||
* @return WP_MySQL_Parser A parser initialized for the MySQL query.
|
||||
*/
|
||||
public function create_parser( string $query ): WP_MySQL_Parser {
|
||||
return $this->mysql_on_sqlite_driver->create_parser( $query );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get results of the last query.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_query_results() {
|
||||
return $this->last_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get return value of the last query() function call.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_last_return_value() {
|
||||
return $this->last_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of columns returned by the last emulated query.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_last_column_count(): int {
|
||||
return $this->mysql_on_sqlite_driver->get_last_column_count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get column metadata for results of the last emulated query.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_last_column_meta(): array {
|
||||
return $this->mysql_on_sqlite_driver->get_last_column_meta();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a query in SQLite.
|
||||
*
|
||||
* @param string $sql The query to execute.
|
||||
* @param array $params The query parameters.
|
||||
* @throws PDOException When the query execution fails.
|
||||
* @return PDOStatement The PDO statement object.
|
||||
*/
|
||||
public function execute_sqlite_query( string $sql, array $params = array() ): PDOStatement {
|
||||
return $this->mysql_on_sqlite_driver->execute_sqlite_query( $sql, $params );
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin a new transaction or nested transaction.
|
||||
*/
|
||||
public function beginTransaction(): void { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
|
||||
$this->mysql_on_sqlite_driver->begin_transaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* A temporary alias for back compatibility.
|
||||
*
|
||||
* @see self::beginTransaction()
|
||||
*/
|
||||
public function begin_transaction(): void {
|
||||
$this->beginTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit the current transaction or nested transaction.
|
||||
*/
|
||||
public function commit(): void {
|
||||
$this->mysql_on_sqlite_driver->commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback the current transaction or nested transaction.
|
||||
*/
|
||||
public function rollback(): void {
|
||||
$this->mysql_on_sqlite_driver->rollback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy also the private property "$main_db_name", as it is used in tests.
|
||||
*/
|
||||
public function __set( string $name, $value ): void {
|
||||
if ( 'main_db_name' === $name ) {
|
||||
$closure = function ( string $value ) {
|
||||
$this->main_db_name = $value;
|
||||
};
|
||||
$closure->call( $this->mysql_on_sqlite_driver, $value );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy also this private method, as it is used in tests.
|
||||
*/
|
||||
private function quote_mysql_utf8_string_literal( string $utf8_literal ): string {
|
||||
$closure = function ( string $utf8_literal ) {
|
||||
return $this->quote_mysql_utf8_string_literal( $utf8_literal );
|
||||
};
|
||||
return $closure->call( $this->mysql_on_sqlite_driver, $utf8_literal );
|
||||
}
|
||||
}
|
||||
+3210
File diff suppressed because it is too large
Load Diff
+152
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* SQLite information schema exception.
|
||||
*
|
||||
* This class is used to represent errors that may occur when building
|
||||
* the MySQL information schema for emulation in SQLite.
|
||||
*/
|
||||
class WP_SQLite_Information_Schema_Exception extends Exception {
|
||||
// Information schema exception types.
|
||||
const TYPE_DUPLICATE_TABLE_NAME = 'duplicate-table-name';
|
||||
const TYPE_DUPLICATE_COLUMN_NAME = 'duplicate-column-name';
|
||||
const TYPE_DUPLICATE_KEY_NAME = 'duplicate-key-name';
|
||||
const TYPE_KEY_COLUMN_NOT_FOUND = 'key-column-not-found';
|
||||
const TYPE_CONSTRAINT_DOES_NOT_EXIST = 'constraint-does-not-exist';
|
||||
const TYPE_MULTIPLE_CONSTRAINTS_WITH_NAME = 'multiple-constraints-with-name';
|
||||
|
||||
/**
|
||||
* The exception type.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* The data to be passed with the exception.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $data;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $type The exception type.
|
||||
* @param string $message The exception message.
|
||||
* @param array $data The data to be passed with the exception.
|
||||
* @param Throwable|null $previous The previous throwable used for the exception chaining.
|
||||
*/
|
||||
public function __construct(
|
||||
string $type,
|
||||
string $message,
|
||||
array $data = array(),
|
||||
?Throwable $previous = null
|
||||
) {
|
||||
parent::__construct( $message, 0, $previous );
|
||||
$this->type = $type;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of the exception.
|
||||
*
|
||||
* @return string The type of the exception.
|
||||
*/
|
||||
public function get_type(): string {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data associated with the exception.
|
||||
*
|
||||
* @return array The data associated with the exception.
|
||||
*/
|
||||
public function get_data(): array {
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a duplicate table name exception.
|
||||
*
|
||||
* @param string $table_name The name of the affected table.
|
||||
* @return self The exception instance.
|
||||
*/
|
||||
public static function duplicate_table_name( string $table_name ): WP_SQLite_Information_Schema_Exception {
|
||||
return new self(
|
||||
self::TYPE_DUPLICATE_TABLE_NAME,
|
||||
sprintf( "Table '%s' already exists.", $table_name ),
|
||||
array( 'table_name' => $table_name )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a duplicate column name exception.
|
||||
*
|
||||
* @param string $column_name The name of the affected column.
|
||||
* @return self The exception instance.
|
||||
*/
|
||||
public static function duplicate_column_name( string $column_name ): WP_SQLite_Information_Schema_Exception {
|
||||
return new self(
|
||||
self::TYPE_DUPLICATE_COLUMN_NAME,
|
||||
sprintf( "Column '%s' already exists.", $column_name ),
|
||||
array( 'column_name' => $column_name )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a duplicate key name exception.
|
||||
*
|
||||
* @param string $key_name The name of the affected key.
|
||||
* @return self The exception instance.
|
||||
*/
|
||||
public static function duplicate_key_name( string $key_name ): WP_SQLite_Information_Schema_Exception {
|
||||
return new self(
|
||||
self::TYPE_DUPLICATE_KEY_NAME,
|
||||
sprintf( "Key '%s' already exists.", $key_name ),
|
||||
array( 'key_name' => $key_name )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a key column not found exception.
|
||||
*
|
||||
* @param string $column_name The name of the affected column.
|
||||
* @return self The exception instance.
|
||||
*/
|
||||
public static function key_column_not_found( string $column_name ): WP_SQLite_Information_Schema_Exception {
|
||||
return new self(
|
||||
self::TYPE_KEY_COLUMN_NOT_FOUND,
|
||||
sprintf( "Key column '%s' doesn't exist in table.", $column_name ),
|
||||
array( 'column_name' => $column_name )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a constraint does not exist exception.
|
||||
*
|
||||
* @param string $name The name of the affected constraint.
|
||||
* @return self The exception instance.
|
||||
*/
|
||||
public static function constraint_does_not_exist( string $name ): WP_SQLite_Information_Schema_Exception {
|
||||
return new self(
|
||||
self::TYPE_CONSTRAINT_DOES_NOT_EXIST,
|
||||
sprintf( "Constraint '%s' does not exist.", $name ),
|
||||
array( 'name' => $name )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a multiple constraints with name exception.
|
||||
*
|
||||
* @param string $name The name of the affected constraint.
|
||||
* @return self The exception instance.
|
||||
*/
|
||||
public static function multiple_constraints_with_name( string $name ): WP_SQLite_Information_Schema_Exception {
|
||||
return new self(
|
||||
self::TYPE_MULTIPLE_CONSTRAINTS_WITH_NAME,
|
||||
sprintf( "Table has multiple constraints with the name '%s'. Please use constraint specific 'DROP' clause.", $name ),
|
||||
array( 'name' => $name )
|
||||
);
|
||||
}
|
||||
}
|
||||
+802
@@ -0,0 +1,802 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* The SQLite driver uses PDO. Enable PDO function calls:
|
||||
* phpcs:disable WordPress.DB.RestrictedClasses.mysql__PDO
|
||||
*/
|
||||
|
||||
/**
|
||||
* SQLite information schema recconstructor for MySQL.
|
||||
*
|
||||
* This class checks and reconstructs the MySQL INFORMATION_SCHEMA data in SQLite
|
||||
* when it becomes out of sync with the actual SQLite database schema.
|
||||
*
|
||||
* Currently, it reconstructs schema infromation for missing tables, and removes
|
||||
* stale data for tables that no longer exist. When used with WordPress, it uses
|
||||
* the "wp_get_db_schema()" function to reconstruct WordPress table information.
|
||||
*/
|
||||
class WP_SQLite_Information_Schema_Reconstructor {
|
||||
/**
|
||||
* The SQLite driver instance.
|
||||
*
|
||||
* @var WP_PDO_MySQL_On_SQLite
|
||||
*/
|
||||
private $driver;
|
||||
|
||||
/**
|
||||
* An instance of the SQLite connection.
|
||||
*
|
||||
* @var WP_SQLite_Connection
|
||||
*/
|
||||
private $connection;
|
||||
|
||||
/**
|
||||
* A service for managing MySQL INFORMATION_SCHEMA tables in SQLite.
|
||||
*
|
||||
* @var WP_SQLite_Information_Schema_Builder
|
||||
*/
|
||||
private $schema_builder;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param WP_PDO_MySQL_On_SQLite $driver The SQLite driver instance.
|
||||
* @param WP_SQLite_Information_Schema_Builder $schema_builder The information schema builder instance.
|
||||
*/
|
||||
public function __construct(
|
||||
$driver,
|
||||
WP_SQLite_Information_Schema_Builder $schema_builder
|
||||
) {
|
||||
$this->driver = $driver;
|
||||
$this->connection = $driver->get_connection();
|
||||
$this->schema_builder = $schema_builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the MySQL INFORMATION_SCHEMA data in SQLite is correct.
|
||||
*
|
||||
* This method checks if the MySQL INFORMATION_SCHEMA data in SQLite is correct,
|
||||
* and if it is not, it will reconstruct missing data and remove stale values.
|
||||
*/
|
||||
public function ensure_correct_information_schema(): void {
|
||||
$sqlite_tables = $this->get_sqlite_table_names();
|
||||
$information_schema_tables = $this->get_information_schema_table_names();
|
||||
|
||||
// In WordPress, use "wp_get_db_schema()" to reconstruct WordPress tables.
|
||||
$wp_tables = $this->get_wp_create_table_statements();
|
||||
|
||||
// Reconstruct information schema records for tables that don't have them.
|
||||
foreach ( $sqlite_tables as $table ) {
|
||||
if ( ! in_array( $table, $information_schema_tables, true ) ) {
|
||||
if ( isset( $wp_tables[ $table ] ) ) {
|
||||
// WordPress core table (as returned by "wp_get_db_schema()").
|
||||
$ast = $wp_tables[ $table ];
|
||||
} else {
|
||||
// Other table (a WordPress plugin or unrelated to WordPress).
|
||||
$sql = $this->generate_create_table_statement( $table );
|
||||
$ast = $this->driver->create_parser( $sql )->parse();
|
||||
if ( null === $ast ) {
|
||||
throw new WP_SQLite_Driver_Exception( $this->driver, 'Failed to parse the MySQL query.' );
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* First, let's make sure we clean up all related data. This fixes
|
||||
* partial data corruption, such as when a table record is missing,
|
||||
* but some related column, index, or constraint records are stored.
|
||||
*/
|
||||
$this->record_drop_table( $table );
|
||||
|
||||
$this->schema_builder->record_create_table( $ast );
|
||||
}
|
||||
}
|
||||
|
||||
// Remove information schema records for tables that don't exist.
|
||||
foreach ( $information_schema_tables as $table ) {
|
||||
if ( ! in_array( $table, $sqlite_tables, true ) ) {
|
||||
$this->record_drop_table( $table );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a DROP TABLE statement in the information schema.
|
||||
*
|
||||
* This removes a table record from the information schema, as well as all
|
||||
* column, index, and constraint records that are related to the table.
|
||||
*
|
||||
* @param string $table_name The name of the table to drop.
|
||||
*/
|
||||
private function record_drop_table( string $table_name ): void {
|
||||
$sql = sprintf( 'DROP TABLE %s', $this->connection->quote_identifier( $table_name ) ); // TODO: mysql quote
|
||||
$ast = $this->driver->create_parser( $sql )->parse();
|
||||
if ( null === $ast ) {
|
||||
throw new WP_SQLite_Driver_Exception( $this->driver, 'Failed to parse the MySQL query.' );
|
||||
}
|
||||
$this->schema_builder->record_drop_table(
|
||||
$ast->get_first_descendant_node( 'dropStatement' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the names of all existing tables in the SQLite database.
|
||||
*
|
||||
* @return string[] The names of tables in the SQLite database.
|
||||
*/
|
||||
private function get_sqlite_table_names(): array {
|
||||
return $this->driver->execute_sqlite_query(
|
||||
"
|
||||
SELECT name
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table'
|
||||
AND name != ?
|
||||
AND name NOT LIKE ? ESCAPE '\'
|
||||
AND name NOT LIKE ? ESCAPE '\'
|
||||
ORDER BY name
|
||||
",
|
||||
array(
|
||||
'_mysql_data_types_cache',
|
||||
'sqlite\_%',
|
||||
str_replace( '_', '\_', WP_PDO_MySQL_On_SQLite::RESERVED_PREFIX ) . '%',
|
||||
)
|
||||
)->fetchAll( PDO::FETCH_COLUMN );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the names of all tables recorded in the information schema.
|
||||
*
|
||||
* @return string[] The names of tables in the information schema.
|
||||
*/
|
||||
private function get_information_schema_table_names(): array {
|
||||
$tables_table = $this->schema_builder->get_table_name( false, 'tables' );
|
||||
return $this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
'SELECT table_name FROM %s ORDER BY table_name',
|
||||
$this->connection->quote_identifier( $tables_table )
|
||||
)
|
||||
)->fetchAll( PDO::FETCH_COLUMN );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a map of parsed CREATE TABLE statements for WordPress tables.
|
||||
*
|
||||
* When reconstructing the information schema data for WordPress tables, we
|
||||
* can use the "wp_get_db_schema()" function to get accurate CREATE TABLE
|
||||
* statements. This method parses the result of "wp_get_db_schema()" into
|
||||
* an array of parsed CREATE TABLE statements indexed by the table names.
|
||||
*
|
||||
* @return array<string, WP_Parser_Node> The WordPress CREATE TABLE statements.
|
||||
*/
|
||||
private function get_wp_create_table_statements(): array {
|
||||
// Bail out when not in a WordPress environment.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/*
|
||||
* In WP CLI, $wpdb may not be set. In that case, we can't load the schema.
|
||||
* We need to bail out and use the standard non-WordPress-specific behavior.
|
||||
*/
|
||||
global $wpdb;
|
||||
if ( ! isset( $wpdb ) ) {
|
||||
// Outside of WP CLI, let's trigger a warning.
|
||||
if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
|
||||
trigger_error( 'The $wpdb global is not initialized.', E_USER_WARNING );
|
||||
}
|
||||
return array();
|
||||
}
|
||||
|
||||
// Ensure the "wp_get_db_schema()" function is defined.
|
||||
if ( file_exists( ABSPATH . 'wp-admin/includes/schema.php' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/schema.php';
|
||||
}
|
||||
if ( ! function_exists( 'wp_get_db_schema' ) ) {
|
||||
throw new Exception( 'The "wp_get_db_schema()" function was not defined.' );
|
||||
}
|
||||
|
||||
/*
|
||||
* At this point, WPDB may not yet be initialized, as we're configuring
|
||||
* the database connection. Let's only populate the table names using
|
||||
* the "$table_prefix" global so we can get correct table names.
|
||||
*/
|
||||
global $table_prefix;
|
||||
$wpdb->set_prefix( $table_prefix );
|
||||
|
||||
// Get schema for global tables.
|
||||
$schema = wp_get_db_schema( 'global' );
|
||||
|
||||
// For multisite installs, add schema definitions for all sites.
|
||||
if ( is_multisite() ) {
|
||||
/*
|
||||
* We need to use a database query over the "get_sites()" function,
|
||||
* as WPDB may not yet initialized. Moreover, we need to get the IDs
|
||||
* of all existing blogs, independent of any filters and actions that
|
||||
* could possibly alter the results of a "get_sites()" call.
|
||||
*/
|
||||
$blog_ids = $this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
'SELECT blog_id FROM %s',
|
||||
$this->connection->quote_identifier( $wpdb->blogs )
|
||||
)
|
||||
)->fetchAll( PDO::FETCH_COLUMN );
|
||||
foreach ( $blog_ids as $blog_id ) {
|
||||
$schema .= wp_get_db_schema( 'blog', (int) $blog_id );
|
||||
}
|
||||
} else {
|
||||
// For single site installs, add schema for the main site.
|
||||
$schema .= wp_get_db_schema( 'blog' );
|
||||
}
|
||||
|
||||
// Parse the schema.
|
||||
$parser = $this->driver->create_parser( $schema );
|
||||
$wp_tables = array();
|
||||
while ( $parser->next_query() ) {
|
||||
$ast = $parser->get_query_ast();
|
||||
if ( null === $ast ) {
|
||||
throw new WP_SQLite_Driver_Exception( $this->driver, 'Failed to parse the MySQL query.' );
|
||||
}
|
||||
|
||||
$create_node = $ast->get_first_descendant_node( 'createStatement' );
|
||||
if ( $create_node && $create_node->has_child_node( 'createTable' ) ) {
|
||||
$name_node = $create_node->get_first_descendant_node( 'tableName' );
|
||||
$name = $this->unquote_mysql_identifier(
|
||||
substr( $schema, $name_node->get_start(), $name_node->get_length() )
|
||||
);
|
||||
|
||||
$wp_tables[ $name ] = $create_node;
|
||||
}
|
||||
}
|
||||
return $wp_tables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a MySQL CREATE TABLE statement from an SQLite table definition.
|
||||
*
|
||||
* @param string $table_name The name of the table.
|
||||
* @return string The CREATE TABLE statement.
|
||||
*/
|
||||
private function generate_create_table_statement( string $table_name ): string {
|
||||
// Columns.
|
||||
$columns = $this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
'PRAGMA table_xinfo(%s)',
|
||||
$this->connection->quote_identifier( $table_name )
|
||||
)
|
||||
)->fetchAll( PDO::FETCH_ASSOC );
|
||||
|
||||
$definitions = array();
|
||||
$column_types = array();
|
||||
foreach ( $columns as $column ) {
|
||||
$mysql_type = $this->get_cached_mysql_data_type( $table_name, $column['name'] );
|
||||
if ( null === $mysql_type ) {
|
||||
$mysql_type = $this->get_mysql_column_type( $column['type'] );
|
||||
}
|
||||
$definitions[] = $this->generate_column_definition( $table_name, $column );
|
||||
$column_types[ $column['name'] ] = $mysql_type;
|
||||
}
|
||||
|
||||
// Primary key.
|
||||
$pk_columns = array();
|
||||
foreach ( $columns as $column ) {
|
||||
// A position of the column in the primary key, starting from index 1.
|
||||
// A value of 0 means that the column is not part of the primary key.
|
||||
$pk_position = (int) $column['pk'];
|
||||
if ( 0 !== $pk_position ) {
|
||||
$pk_columns[ $pk_position ] = $column['name'];
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the columns by their position in the primary key.
|
||||
ksort( $pk_columns );
|
||||
|
||||
if ( count( $pk_columns ) > 0 ) {
|
||||
$quoted_pk_columns = array();
|
||||
foreach ( $pk_columns as $pk_column ) {
|
||||
$quoted_pk_columns[] = $this->connection->quote_identifier( $pk_column );
|
||||
}
|
||||
$definitions[] = sprintf( 'PRIMARY KEY (%s)', implode( ', ', $quoted_pk_columns ) );
|
||||
}
|
||||
|
||||
// Indexes and keys.
|
||||
$keys = $this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
'PRAGMA index_list(%s)',
|
||||
$this->connection->quote_identifier( $table_name )
|
||||
)
|
||||
)->fetchAll( PDO::FETCH_ASSOC );
|
||||
|
||||
foreach ( $keys as $key ) {
|
||||
// Skip the internal index that SQLite may create for a primary key.
|
||||
// In MySQL, no explicit index needs to be defined for a primary key.
|
||||
if ( 'pk' === $key['origin'] ) {
|
||||
continue;
|
||||
}
|
||||
$definitions[] = $this->generate_key_definition( $table_name, $key, $column_types );
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
"CREATE TABLE %s (\n %s\n)",
|
||||
$this->connection->quote_identifier( $table_name ),
|
||||
implode( ",\n ", $definitions )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a MySQL column definition from an SQLite column information.
|
||||
*
|
||||
* This method generates a MySQL column definition from SQLite column data.
|
||||
*
|
||||
* @param string $table_name The name of the table.
|
||||
* @param array $column_info The SQLite column information.
|
||||
* @return string The MySQL column definition.
|
||||
*/
|
||||
private function generate_column_definition( string $table_name, array $column_info ): string {
|
||||
$definition = array();
|
||||
$definition[] = $this->connection->quote_identifier( $column_info['name'] );
|
||||
|
||||
// Data type.
|
||||
$mysql_type = $this->get_cached_mysql_data_type( $table_name, $column_info['name'] );
|
||||
if ( null === $mysql_type ) {
|
||||
$mysql_type = $this->get_mysql_column_type( $column_info['type'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct some column types based on their default values:
|
||||
* 1. In MySQL, non-datetime columns can't have a timestamp default.
|
||||
* Let's use DATETIME when default is set to CURRENT_TIMESTAMP.
|
||||
* 2. In MySQL, TEXT and BLOB columns can't have a default value.
|
||||
* Let's use VARCHAR(65535) and VARBINARY(65535) when default is set.
|
||||
*/
|
||||
$default = $this->generate_column_default( $mysql_type, $column_info['dflt_value'] );
|
||||
if ( 'CURRENT_TIMESTAMP' === $default ) {
|
||||
$mysql_type = 'datetime';
|
||||
} elseif ( 'text' === $mysql_type && null !== $default ) {
|
||||
$mysql_type = 'varchar(65535)';
|
||||
} elseif ( 'blob' === $mysql_type && null !== $default ) {
|
||||
$mysql_type = 'varbinary(65535)';
|
||||
}
|
||||
|
||||
$definition[] = $mysql_type;
|
||||
|
||||
// NULL/NOT NULL.
|
||||
if ( '1' === $column_info['notnull'] ) {
|
||||
$definition[] = 'NOT NULL';
|
||||
}
|
||||
|
||||
// Auto increment.
|
||||
$is_auto_increment = false;
|
||||
if ( '0' !== $column_info['pk'] ) {
|
||||
$is_auto_increment = $this->driver->execute_sqlite_query(
|
||||
'SELECT 1 FROM sqlite_master WHERE tbl_name = ? AND sql LIKE ?',
|
||||
array( $table_name, '%AUTOINCREMENT%' )
|
||||
)->fetchColumn();
|
||||
|
||||
if ( $is_auto_increment ) {
|
||||
$definition[] = 'AUTO_INCREMENT';
|
||||
}
|
||||
}
|
||||
|
||||
// Default value.
|
||||
if ( null !== $default && ! $is_auto_increment ) {
|
||||
$definition[] = 'DEFAULT ' . $default;
|
||||
}
|
||||
|
||||
return implode( ' ', $definition );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a MySQL key definition from an SQLite key information.
|
||||
*
|
||||
* This method generates a MySQL key definition from SQLite key data.
|
||||
*
|
||||
* @param string $table_name The name of the table.
|
||||
* @param array $key_info The SQLite key information.
|
||||
* @param array $column_types The MySQL data types of the columns.
|
||||
* @return string The MySQL key definition.
|
||||
*/
|
||||
private function generate_key_definition( string $table_name, array $key_info, array $column_types ): string {
|
||||
$definition = array();
|
||||
|
||||
// Key type.
|
||||
$cached_type = $this->get_cached_mysql_data_type( $table_name, $key_info['name'] );
|
||||
if ( 'FULLTEXT' === $cached_type ) {
|
||||
$definition[] = 'FULLTEXT KEY';
|
||||
} elseif ( 'SPATIAL' === $cached_type ) {
|
||||
$definition[] = 'SPATIAL KEY';
|
||||
} elseif ( 'UNIQUE' === $cached_type || '1' === $key_info['unique'] ) {
|
||||
$definition[] = 'UNIQUE KEY';
|
||||
} else {
|
||||
$definition[] = 'KEY';
|
||||
}
|
||||
|
||||
// Key name.
|
||||
$name = $key_info['name'];
|
||||
|
||||
/*
|
||||
* The SQLite driver prefixes index names with "{$table_name}__" to avoid
|
||||
* naming conflicts among tables in SQLite. We need to remove the prefix.
|
||||
*/
|
||||
if ( str_starts_with( $name, "{$table_name}__" ) ) {
|
||||
$name = substr( $name, strlen( "{$table_name}__" ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* SQLite creates automatic internal indexes for primary and unique keys,
|
||||
* naming them in format "sqlite_autoindex_{$table_name}_{$index_id}".
|
||||
* For these internal indexes, we need to skip their name, so that in
|
||||
* the generated MySQL definition, they follow implicit MySQL naming.
|
||||
*/
|
||||
if ( ! str_starts_with( $name, 'sqlite_autoindex_' ) ) {
|
||||
$definition[] = $this->connection->quote_identifier( $name );
|
||||
}
|
||||
|
||||
// Key columns.
|
||||
$key_columns = $this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
'PRAGMA index_info(%s)',
|
||||
$this->connection->quote_identifier( $key_info['name'] )
|
||||
)
|
||||
)->fetchAll( PDO::FETCH_ASSOC );
|
||||
$cols = array();
|
||||
foreach ( $key_columns as $column ) {
|
||||
/*
|
||||
* Extract type and length from column data type definition.
|
||||
*
|
||||
* This is required when the column data type is inferred from the
|
||||
* '_mysql_data_types_cache' table, which stores the data type in
|
||||
* the format "type(length)", such as "varchar(255)".
|
||||
*/
|
||||
$max_prefix_length = 100;
|
||||
$type = strtolower( $column_types[ $column['name'] ] );
|
||||
$parts = explode( '(', $type );
|
||||
$column_type = $parts[0];
|
||||
$column_length = isset( $parts[1] ) ? (int) $parts[1] : null;
|
||||
|
||||
/*
|
||||
* Add an index column prefix length, if needed.
|
||||
*
|
||||
* This is required for "text" and "blob" types for columns inferred
|
||||
* directly from the SQLite schema, and for the following types for
|
||||
* columns inferred from the '_mysql_data_types_cache' table:
|
||||
* char, varchar
|
||||
* text, tinytext, mediumtext, longtext
|
||||
* blob, tinyblob, mediumblob, longblob
|
||||
* varbinary
|
||||
*/
|
||||
if (
|
||||
str_ends_with( $column_type, 'char' )
|
||||
|| str_ends_with( $column_type, 'text' )
|
||||
|| str_ends_with( $column_type, 'blob' )
|
||||
|| str_starts_with( $column_type, 'var' )
|
||||
) {
|
||||
$cols[] = sprintf(
|
||||
'%s(%d)',
|
||||
$this->connection->quote_identifier( $column['name'] ),
|
||||
min( $column_length ?? $max_prefix_length, $max_prefix_length )
|
||||
);
|
||||
} else {
|
||||
$cols[] = $this->connection->quote_identifier( $column['name'] );
|
||||
}
|
||||
}
|
||||
|
||||
$definition[] = '(' . implode( ', ', $cols ) . ')';
|
||||
return implode( ' ', $definition );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a MySQL default value from an SQLite default value.
|
||||
*
|
||||
* @param string $mysql_type The MySQL data type of the column.
|
||||
* @param string|null $default_value The default value of the SQLite column.
|
||||
* @return string|null The default value, or null if the column has no default value.
|
||||
*/
|
||||
private function generate_column_default( string $mysql_type, ?string $default_value ): ?string {
|
||||
if ( null === $default_value || '' === $default_value ) {
|
||||
return null;
|
||||
}
|
||||
$mysql_type = strtolower( $mysql_type );
|
||||
|
||||
if ( str_starts_with( $mysql_type, 'bit' ) ) {
|
||||
// BIT columns are stored as INTEGER in SQLite.
|
||||
return "b'" . decbin( (int) $default_value ) . "'";
|
||||
}
|
||||
|
||||
/*
|
||||
* In MySQL, geometry columns can't have a default value.
|
||||
*
|
||||
* Geometry columns are saved as TEXT in SQLite, and in an older version
|
||||
* of the SQLite driver, TEXT columns were assigned a default value of ''.
|
||||
*/
|
||||
if ( 'geomcollection' === $mysql_type || 'geometrycollection' === $mysql_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* In MySQL, date/time columns can't have a default value of ''.
|
||||
*
|
||||
* Date/time columns are saved as TEXT in SQLite, and in an older version
|
||||
* of the SQLite driver, TEXT columns were assigned a default value of ''.
|
||||
*/
|
||||
if (
|
||||
"''" === $default_value
|
||||
&& in_array( $mysql_type, array( 'datetime', 'date', 'time', 'timestamp', 'year' ), true )
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert SQLite default values to MySQL default values.
|
||||
*
|
||||
* See:
|
||||
* - https://www.sqlite.org/syntax/column-constraint.html
|
||||
* - https://www.sqlite.org/syntax/literal-value.html
|
||||
* - https://www.sqlite.org/lang_expr.html#literal_values_constants_
|
||||
*/
|
||||
|
||||
// Quoted string literal. E.g.: 'abc', "abc", `abc`
|
||||
$first_byte = $default_value[0] ?? null;
|
||||
if ( '"' === $first_byte || "'" === $first_byte || '`' === $first_byte ) {
|
||||
$value = substr( $default_value, 1, -1 );
|
||||
$value = str_replace( $first_byte . $first_byte, $first_byte, $value );
|
||||
return $this->quote_mysql_utf8_string_literal( $value );
|
||||
}
|
||||
|
||||
// Normalize the default value for easier comparison.
|
||||
$uppercase_default_value = strtoupper( $default_value );
|
||||
|
||||
// NULL, TRUE, FALSE.
|
||||
if ( 'NULL' === $uppercase_default_value ) {
|
||||
// DEFAULT NULL is the same as no default value.
|
||||
return null;
|
||||
} elseif ( 'TRUE' === $uppercase_default_value ) {
|
||||
return '1';
|
||||
} elseif ( 'FALSE' === $uppercase_default_value ) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
// Date/time values.
|
||||
if ( 'CURRENT_TIMESTAMP' === $uppercase_default_value ) {
|
||||
return 'CURRENT_TIMESTAMP';
|
||||
} elseif ( 'CURRENT_DATE' === $uppercase_default_value ) {
|
||||
return null; // Not supported in MySQL.
|
||||
} elseif ( 'CURRENT_TIME' === $uppercase_default_value ) {
|
||||
return null; // Not supported in MySQL.
|
||||
}
|
||||
|
||||
// SQLite supports underscores in all numeric literals.
|
||||
$no_underscore_default_value = str_replace( '_', '', $default_value );
|
||||
|
||||
// Numeric literals. E.g.: 123, 1.23, -1.23, 1e3, 1.2e-3
|
||||
if ( is_numeric( $no_underscore_default_value ) ) {
|
||||
return $no_underscore_default_value;
|
||||
}
|
||||
|
||||
// HEX literals (numeric). E.g.: 0x1a2f, 0X1A2F
|
||||
$value = filter_var( $no_underscore_default_value, FILTER_VALIDATE_INT, FILTER_FLAG_ALLOW_HEX );
|
||||
if ( false !== $value ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// BLOB literals (string). E.g.: x'1a2f', X'1A2F'
|
||||
// Checking the prefix is enough as SQLite doesn't allow malformed values.
|
||||
if ( str_starts_with( $uppercase_default_value, "X'" ) ) {
|
||||
// Convert the hex string to ASCII bytes.
|
||||
return "'" . pack( 'H*', substr( $default_value, 2, -1 ) ) . "'";
|
||||
}
|
||||
|
||||
// Unquoted string literal. E.g.: abc
|
||||
return $this->quote_mysql_utf8_string_literal( $default_value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a MySQL column or index data type from legacy data types cache table.
|
||||
*
|
||||
* This method retrieves MySQL column or index data types from a special table
|
||||
* that was used by an old version of the SQLite driver and that is otherwise
|
||||
* no longer needed. This is more precise than direct inference from SQLite.
|
||||
*
|
||||
* For columns, it returns full column type, including prefix length, e.g.:
|
||||
* int(11), bigint(20) unsigned, varchar(255), longtext
|
||||
*
|
||||
* For indexes, it returns one of:
|
||||
* KEY, PRIMARY, UNIQUE, FULLTEXT, SPATIAL
|
||||
*
|
||||
* @param string $table_name The table name.
|
||||
* @param string $column_or_index_name The column or index name.
|
||||
* @return string|null The MySQL definition, or null when not found.
|
||||
*/
|
||||
private function get_cached_mysql_data_type( string $table_name, string $column_or_index_name ): ?string {
|
||||
try {
|
||||
$mysql_type = $this->driver->execute_sqlite_query(
|
||||
'SELECT mysql_type FROM _mysql_data_types_cache
|
||||
WHERE `table` = ? COLLATE NOCASE
|
||||
AND (
|
||||
-- The old SQLite driver stored the MySQL data types in multiple
|
||||
-- formats - lowercase, uppercase, and, sometimes, with backticks.
|
||||
column_or_index = ? COLLATE NOCASE
|
||||
OR column_or_index = ? COLLATE NOCASE
|
||||
)',
|
||||
array( $table_name, $column_or_index_name, "`$column_or_index_name`" )
|
||||
)->fetchColumn();
|
||||
} catch ( PDOException $e ) {
|
||||
if ( str_contains( $e->getMessage(), 'no such table' ) ) {
|
||||
return null;
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
if ( false === $mysql_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the stored type value is a valid MySQL column type.
|
||||
*
|
||||
* Some older versions of the legacy SQLite driver might have stored
|
||||
* invalid MySQL column types in some scenarios:
|
||||
*
|
||||
* 1. Before https://github.com/WordPress/sqlite-database-integration/pull/126,
|
||||
* the legacy SQLite driver incorrectly stored MySQL column types
|
||||
* for columns with multiple type arguments.
|
||||
*
|
||||
* E.g., a column definition like "col_name decimal(26, 8)" would
|
||||
* be stored with invalid type "decimal(26,".
|
||||
*
|
||||
* 2. Before https://github.com/WordPress/sqlite-database-integration/commit/b5a9fbaed4d0d843f792aaa959e3d00f193ff1ee
|
||||
* (see also https://github.com/Automattic/sqlite-database-integration/pull/2),
|
||||
* the legacy SQLite driver incorrectly recognized indexes on columns
|
||||
* with type keywords as additional table column definitions.
|
||||
*
|
||||
* E.g., an index definition like "KEY timestamp (timestamp)" would
|
||||
* be stored as column "KEY" with invalid type "timestamp(timestamp)".
|
||||
*
|
||||
* To address these issues, we need to check whether the stored type looks
|
||||
* like a valid MySQL column type definition.
|
||||
*/
|
||||
$open_par_index = strpos( $mysql_type, '(' );
|
||||
$close_par_index = strpos( $mysql_type, ')' );
|
||||
if ( false !== $open_par_index ) {
|
||||
$end = false !== $close_par_index ? $close_par_index : strlen( $mysql_type );
|
||||
$parts = explode( '(', substr( $mysql_type, 0, $end ) );
|
||||
$type = strtolower( trim( $parts[0] ) );
|
||||
$args = array();
|
||||
foreach ( explode( ',', $parts[1] ) as $arg ) {
|
||||
$args[] = strtolower( trim( $arg ) );
|
||||
}
|
||||
|
||||
// WooCommerce uses decimal(26,8), decimal(19,4), and decimal(3,2)
|
||||
// column types, so we can can fix the invalid column definitions.
|
||||
$looks_like_wc_table = str_contains( $table_name, 'wc_' ) || str_contains( $table_name, 'woocommerce_' );
|
||||
$is_invalid_decimal = 'decimal' === $type && count( $args ) === 2 && '' === $args[1];
|
||||
if ( $looks_like_wc_table && $is_invalid_decimal ) {
|
||||
if ( '26' === $args[0] ) {
|
||||
// Fix "decimal(26,".
|
||||
return 'decimal(26,8)';
|
||||
} elseif ( '19' === $args[0] ) {
|
||||
// Fix "decimal(19,".
|
||||
return 'decimal(19,4)';
|
||||
} elseif ( '3' === $args[0] ) {
|
||||
// Fix "decimal(3,".
|
||||
return 'decimal(3,2)';
|
||||
}
|
||||
}
|
||||
|
||||
// Only numeric arguments are allowed for MySQL column types.
|
||||
// This handles the incorrectly stored index definition case.
|
||||
foreach ( $args as $arg ) {
|
||||
if ( ! is_numeric( $arg ) ) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no closing parenthesis, the type is invalid.
|
||||
if ( false === $close_par_index ) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize index type for backward compatibility. Some older versions
|
||||
// of the SQLite driver stored index types with a " KEY" suffix, e.g.,
|
||||
// "PRIMARY KEY" or "UNIQUE KEY". More recent versions omit the suffix.
|
||||
if ( str_ends_with( $mysql_type, ' KEY' ) ) {
|
||||
$mysql_type = substr( $mysql_type, 0, strlen( $mysql_type ) - strlen( ' KEY' ) );
|
||||
}
|
||||
return $mysql_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a MySQL column type from an SQLite column type.
|
||||
*
|
||||
* This method converts an SQLite column type to a MySQL column type as per
|
||||
* the SQLite column type affinity rules:
|
||||
* https://sqlite.org/datatype3.html#determination_of_column_affinity
|
||||
*
|
||||
* @param string $column_type The SQLite column type.
|
||||
* @return string The MySQL column type.
|
||||
*/
|
||||
private function get_mysql_column_type( string $column_type ): string {
|
||||
$type = strtoupper( $column_type );
|
||||
|
||||
/*
|
||||
* Following the rules of column affinity:
|
||||
* https://sqlite.org/datatype3.html#determination_of_column_affinity
|
||||
*/
|
||||
|
||||
// 1. If the declared type contains the string "INT" then it is assigned
|
||||
// INTEGER affinity.
|
||||
if ( str_contains( $type, 'INT' ) ) {
|
||||
return 'int';
|
||||
}
|
||||
|
||||
// 2. If the declared type of the column contains any of the strings
|
||||
// "CHAR", "CLOB", or "TEXT" then that column has TEXT affinity.
|
||||
if ( str_contains( $type, 'TEXT' ) || str_contains( $type, 'CHAR' ) || str_contains( $type, 'CLOB' ) ) {
|
||||
return 'text';
|
||||
}
|
||||
|
||||
// 3. If the declared type for a column contains the string "BLOB" or
|
||||
// if no type is specified then the column has affinity BLOB.
|
||||
if ( str_contains( $type, 'BLOB' ) || '' === $type ) {
|
||||
return 'blob';
|
||||
}
|
||||
|
||||
// 4. If the declared type for a column contains any of the strings
|
||||
// "REAL", "FLOA", or "DOUB" then the column has REAL affinity.
|
||||
if ( str_contains( $type, 'REAL' ) || str_contains( $type, 'FLOA' ) ) {
|
||||
return 'float';
|
||||
}
|
||||
if ( str_contains( $type, 'DOUB' ) ) {
|
||||
return 'double';
|
||||
}
|
||||
|
||||
/**
|
||||
* 5. Otherwise, the affinity is NUMERIC.
|
||||
*
|
||||
* While SQLite defaults to a NUMERIC column affinity, it's better to use
|
||||
* TEXT in this case, because numeric SQLite columns in non-strict tables
|
||||
* can contain any text data as well, when it is not a well-formed number.
|
||||
*
|
||||
* See: https://sqlite.org/datatype3.html#type_affinity
|
||||
*/
|
||||
return 'text';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a MySQL UTF-8 string literal for output in a CREATE TABLE statement.
|
||||
*
|
||||
* See WP_PDO_MySQL_On_SQLite::quote_mysql_utf8_string_literal().
|
||||
*
|
||||
* TODO: This is a copy of WP_PDO_MySQL_On_SQLite::quote_mysql_utf8_string_literal().
|
||||
* We may consider extracing it to reusable MySQL helpers.
|
||||
*
|
||||
* @param string $utf8_literal The UTF-8 string literal to escape.
|
||||
* @return string The escaped string literal.
|
||||
*/
|
||||
private function quote_mysql_utf8_string_literal( string $utf8_literal ): string {
|
||||
$backslash = chr( 92 );
|
||||
$replacements = array(
|
||||
"'" => "''", // A single quote character (').
|
||||
$backslash => $backslash . $backslash, // A backslash character (\).
|
||||
chr( 0 ) => $backslash . '0', // An ASCII NULL character (\0).
|
||||
chr( 10 ) => $backslash . 'n', // A newline (linefeed) character (\n).
|
||||
chr( 13 ) => $backslash . 'r', // A carriage return character (\r).
|
||||
);
|
||||
return "'" . strtr( $utf8_literal, $replacements ) . "'";
|
||||
}
|
||||
|
||||
/**
|
||||
* Unquote a quoted MySQL identifier.
|
||||
*
|
||||
* Remove bounding quotes and replace escaped quotes with their values.
|
||||
*
|
||||
* @param string $quoted_identifier The quoted identifier value.
|
||||
* @return string The unquoted identifier value.
|
||||
*/
|
||||
private function unquote_mysql_identifier( string $quoted_identifier ): string {
|
||||
$first_byte = $quoted_identifier[0] ?? null;
|
||||
if ( '"' === $first_byte || '`' === $first_byte ) {
|
||||
$unquoted = substr( $quoted_identifier, 1, -1 );
|
||||
return str_replace( $first_byte . $first_byte, $first_byte, $unquoted );
|
||||
}
|
||||
return $quoted_identifier;
|
||||
}
|
||||
}
|
||||
+987
@@ -0,0 +1,987 @@
|
||||
<?php
|
||||
/**
|
||||
* Custom functions for the SQLite implementation.
|
||||
*
|
||||
* @package wp-sqlite-integration
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class defines user defined functions(UDFs) for PDO library.
|
||||
*
|
||||
* These functions replace those used in the SQL statement with the PHP functions.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* <code>
|
||||
* new WP_SQLite_PDO_User_Defined_Functions(ref_to_pdo_obj);
|
||||
* </code>
|
||||
*
|
||||
* This automatically enables ref_to_pdo_obj to replace the function in the SQL statement
|
||||
* to the ones defined here.
|
||||
*/
|
||||
class WP_SQLite_PDO_User_Defined_Functions {
|
||||
|
||||
/**
|
||||
* Registers the user defined functions for SQLite to a PDO instance.
|
||||
* The functions are registered using PDO::sqliteCreateFunction().
|
||||
*
|
||||
* @param PDO|PDO\SQLite $pdo The PDO object.
|
||||
*/
|
||||
public static function register_for( $pdo ): self {
|
||||
$instance = new self();
|
||||
foreach ( $instance->functions as $f => $t ) {
|
||||
if ( $pdo instanceof PDO\SQLite ) {
|
||||
$pdo->createFunction( $f, array( $instance, $t ) );
|
||||
} else {
|
||||
$pdo->sqliteCreateFunction( $f, array( $instance, $t ) );
|
||||
}
|
||||
}
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Array to define MySQL function => function defined with PHP.
|
||||
*
|
||||
* Replaced functions must be public.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $functions = array(
|
||||
'throw' => 'throw',
|
||||
'month' => 'month',
|
||||
'monthnum' => 'month',
|
||||
'year' => 'year',
|
||||
'day' => 'day',
|
||||
'hour' => 'hour',
|
||||
'minute' => 'minute',
|
||||
'second' => 'second',
|
||||
'week' => 'week',
|
||||
'weekday' => 'weekday',
|
||||
'dayofweek' => 'dayofweek',
|
||||
'dayofmonth' => 'dayofmonth',
|
||||
'unix_timestamp' => 'unix_timestamp',
|
||||
'now' => 'now',
|
||||
'md5' => 'md5',
|
||||
'curdate' => 'curdate',
|
||||
'rand' => 'rand',
|
||||
'from_unixtime' => 'from_unixtime',
|
||||
'localtime' => 'now',
|
||||
'localtimestamp' => 'now',
|
||||
'isnull' => 'isnull',
|
||||
'if' => '_if',
|
||||
'regexp' => 'regexp',
|
||||
'field' => 'field',
|
||||
'log' => 'log',
|
||||
'least' => 'least',
|
||||
'greatest' => 'greatest',
|
||||
'get_lock' => 'get_lock',
|
||||
'release_lock' => 'release_lock',
|
||||
'ucase' => 'ucase',
|
||||
'lcase' => 'lcase',
|
||||
'unhex' => 'unhex',
|
||||
'from_base64' => 'from_base64',
|
||||
'to_base64' => 'to_base64',
|
||||
'inet_ntoa' => 'inet_ntoa',
|
||||
'inet_aton' => 'inet_aton',
|
||||
'datediff' => 'datediff',
|
||||
'locate' => 'locate',
|
||||
'utc_date' => 'utc_date',
|
||||
'utc_time' => 'utc_time',
|
||||
'utc_timestamp' => 'utc_timestamp',
|
||||
'version' => 'version',
|
||||
|
||||
// Internal helper functions.
|
||||
'_helper_like_to_glob_pattern' => '_helper_like_to_glob_pattern',
|
||||
);
|
||||
|
||||
/**
|
||||
* First element of the RAND(N) LCG state (the value the output is derived from).
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
private $rand_seed1 = null;
|
||||
|
||||
/**
|
||||
* Second element of the RAND(N) LCG state (the paired value used in the recurrence).
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
private $rand_seed2 = null;
|
||||
|
||||
/**
|
||||
* Last seed value passed to RAND(N) in the current statement.
|
||||
*
|
||||
* Used to detect whether the rand sequence is advancing with the same seed
|
||||
* (e.g. "SELECT RAND(3) FROM t"), or reseeding (starting a new sequence).
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
private $rand_last_seed = null;
|
||||
|
||||
/**
|
||||
* Clear any per-statement state held by the UDFs.
|
||||
*/
|
||||
public function flush(): void {
|
||||
$this->rand_seed1 = null;
|
||||
$this->rand_seed2 = null;
|
||||
$this->rand_last_seed = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to throw an error from SQLite expressions.
|
||||
*
|
||||
* @param string $message The error message.
|
||||
*
|
||||
* @throws Exception The error message.
|
||||
* @return void
|
||||
*/
|
||||
public function throw( $message ): void {
|
||||
throw new Exception( $message );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to return the unix timestamp.
|
||||
*
|
||||
* Used without an argument, it returns PHP time() function (total seconds passed
|
||||
* from '1970-01-01 00:00:00' GMT). Used with the argument, it changes the value
|
||||
* to the timestamp.
|
||||
*
|
||||
* @param string $field Representing the date formatted as '0000-00-00 00:00:00'.
|
||||
*
|
||||
* @return number of unsigned integer
|
||||
*/
|
||||
public function unix_timestamp( $field = null ) {
|
||||
return is_null( $field ) ? time() : strtotime( $field );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL FROM_UNIXTIME() function.
|
||||
*
|
||||
* @param int $field The unix timestamp.
|
||||
* @param string $format Indicate the way of formatting(optional).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function from_unixtime( $field, $format = null ) {
|
||||
// Convert to ISO time.
|
||||
$date = gmdate( 'Y-m-d H:i:s', $field );
|
||||
|
||||
return is_null( $format ) ? $date : $this->dateformat( $date, $format );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL NOW() function.
|
||||
*
|
||||
* @return string representing current time formatted as '0000-00-00 00:00:00'.
|
||||
*/
|
||||
public function now() {
|
||||
return gmdate( 'Y-m-d H:i:s' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL CURDATE() function.
|
||||
*
|
||||
* @return string representing current time formatted as '0000-00-00'.
|
||||
*/
|
||||
public function curdate() {
|
||||
return gmdate( 'Y-m-d' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL MD5() function.
|
||||
*
|
||||
* @param string $field The string to be hashed.
|
||||
*
|
||||
* @return string of the md5 hash value of the argument.
|
||||
*/
|
||||
public function md5( $field ) {
|
||||
return md5( $field );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL's seeded RAND(N) function.
|
||||
*
|
||||
* Implements MySQL's deterministic LCG (Linear Congruential Generator),
|
||||
* producing bit-exact output for a given seed.
|
||||
*
|
||||
* Known divergences from MySQL:
|
||||
*
|
||||
* 1. In MySQL, RAND(N) behaves differently depending on whether the seed
|
||||
* is constant expression or varies per invocation:
|
||||
* - Constant seed (e.g. "SELECT RAND(3) FROM t"):
|
||||
* LCG is initialized once per statement and advanced for each row.
|
||||
* - Non-constant seed (e.g. "SELECT RAND(col) FROM t"):
|
||||
* LCG is initialized for every row with its seed value.
|
||||
*
|
||||
* A SQLite UDF cannot tell whether the seed expression is constant, so
|
||||
* we just compare the seed against its last value. This diverges from
|
||||
* MySQL in rare cases, and we can consider improving it in the future.
|
||||
*
|
||||
* 2. The LCG state is shared across call sites in the same query, so
|
||||
* "SELECT RAND(1), RAND(1)" yields different results here than in MySQL.
|
||||
* This is a rare edge case that we can consider improving in the future.
|
||||
*
|
||||
* Unseeded RAND() never reaches this function. The AST driver translates it
|
||||
* directly to a more efficient SQLite-native expression.
|
||||
*
|
||||
* @param int|float|string|null $seed Seed value.
|
||||
*
|
||||
* @return float A value in [0, 1).
|
||||
*/
|
||||
public function rand( $seed ) {
|
||||
// Requires 64-bit PHP. Seed * 0x10000001 can exceed PHP_INT_MAX on 32-bit.
|
||||
$max_value = 0x3FFFFFFF;
|
||||
|
||||
if ( null === $seed ) {
|
||||
// MySQL treats NULL seed as 0.
|
||||
$seed = 0;
|
||||
} elseif ( ! is_int( $seed ) ) {
|
||||
/*
|
||||
* MySQL rounds float values and numeric strings take the same path.
|
||||
* Reduce the value to a 32-bit range using "fmod" to avoid firing
|
||||
* the "out-of-range float to int" cast deprecation on PHP 8.1+.
|
||||
*/
|
||||
$seed = (int) fmod( round( (float) $seed, 0, PHP_ROUND_HALF_EVEN ), 0x100000000 );
|
||||
}
|
||||
|
||||
// Initialize MySQL's internal 30-bit seeds.
|
||||
if ( $seed !== $this->rand_last_seed ) {
|
||||
/*
|
||||
* MySQL casts to uint32, and the intermediate results wrap at 32-bit
|
||||
* unsigned boundaries. We emulate this with & 0xFFFFFFFF masks.
|
||||
*/
|
||||
$seed_u32 = $seed & 0xFFFFFFFF;
|
||||
$this->rand_seed1 = ( ( $seed_u32 * 0x10001 + 55555555 ) & 0xFFFFFFFF ) % $max_value;
|
||||
$this->rand_seed2 = ( ( $seed_u32 * 0x10000001 ) & 0xFFFFFFFF ) % $max_value;
|
||||
$this->rand_last_seed = $seed;
|
||||
}
|
||||
|
||||
/*
|
||||
* MySQL's LCG recurrence:
|
||||
* seed1 = (seed1 * 3 + seed2) % max_value
|
||||
* seed2 = (seed1 + seed2 + 33) % max_value
|
||||
*
|
||||
* Note that seed1 is updated first and the new value is used for seed2.
|
||||
*/
|
||||
$this->rand_seed1 = ( $this->rand_seed1 * 3 + $this->rand_seed2 ) % $max_value;
|
||||
$this->rand_seed2 = ( $this->rand_seed1 + $this->rand_seed2 + 33 ) % $max_value;
|
||||
|
||||
return (float) $this->rand_seed1 / (float) $max_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL DATEFORMAT() function.
|
||||
*
|
||||
* @param string $date Formatted as '0000-00-00' or datetime as '0000-00-00 00:00:00'.
|
||||
* @param string $format The string format.
|
||||
*
|
||||
* @return string formatted according to $format
|
||||
*/
|
||||
public function dateformat( $date, $format ) {
|
||||
$mysql_php_date_formats = array(
|
||||
'%a' => 'D',
|
||||
'%b' => 'M',
|
||||
'%c' => 'n',
|
||||
'%D' => 'jS',
|
||||
'%d' => 'd',
|
||||
'%e' => 'j',
|
||||
'%H' => 'H',
|
||||
'%h' => 'h',
|
||||
'%I' => 'h',
|
||||
'%i' => 'i',
|
||||
'%j' => 'z',
|
||||
'%k' => 'G',
|
||||
'%l' => 'g',
|
||||
'%M' => 'F',
|
||||
'%m' => 'm',
|
||||
'%p' => 'A',
|
||||
'%r' => 'h:i:s A',
|
||||
'%S' => 's',
|
||||
'%s' => 's',
|
||||
'%T' => 'H:i:s',
|
||||
'%U' => 'W',
|
||||
'%u' => 'W',
|
||||
'%V' => 'W',
|
||||
'%v' => 'W',
|
||||
'%W' => 'l',
|
||||
'%w' => 'w',
|
||||
'%X' => 'Y',
|
||||
'%x' => 'o',
|
||||
'%Y' => 'Y',
|
||||
'%y' => 'y',
|
||||
);
|
||||
|
||||
$time = strtotime( $date );
|
||||
$format = strtr( $format, $mysql_php_date_formats );
|
||||
|
||||
return gmdate( $format, $time );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to extract the month value from the date.
|
||||
*
|
||||
* @param string $field Representing the date formatted as 0000-00-00.
|
||||
*
|
||||
* @return string Representing the number of the month between 1 and 12.
|
||||
*/
|
||||
public function month( $field ) {
|
||||
/*
|
||||
* MySQL returns 0 for MONTH('0000-00-00') and for dates with
|
||||
* zero month parts like '2020-00-15'. PHP's strtotime() can't
|
||||
* parse these, so we extract the month directly from the string.
|
||||
*/
|
||||
if ( preg_match( '/^\d{4}-(\d{2})/', $field, $matches ) ) {
|
||||
return intval( $matches[1] );
|
||||
}
|
||||
/*
|
||||
* From https://www.php.net/manual/en/datetime.format.php:
|
||||
*
|
||||
* n - Numeric representation of a month, without leading zeros.
|
||||
* 1 through 12
|
||||
*/
|
||||
return intval( gmdate( 'n', strtotime( $field ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to extract the year value from the date.
|
||||
*
|
||||
* @param string $field Representing the date formatted as 0000-00-00.
|
||||
*
|
||||
* @return string Representing the number of the year.
|
||||
*/
|
||||
public function year( $field ) {
|
||||
/*
|
||||
* MySQL returns 0 for YEAR('0000-00-00'). PHP's strtotime()
|
||||
* can't parse zero dates, so we extract the year directly.
|
||||
*/
|
||||
if ( preg_match( '/^(\d{4})-\d{2}/', $field, $matches ) ) {
|
||||
return intval( $matches[1] );
|
||||
}
|
||||
/*
|
||||
* From https://www.php.net/manual/en/datetime.format.php:
|
||||
*
|
||||
* Y - A full numeric representation of a year, 4 digits.
|
||||
*/
|
||||
return intval( gmdate( 'Y', strtotime( $field ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to extract the day value from the date.
|
||||
*
|
||||
* @param string $field Representing the date formatted as 0000-00-00.
|
||||
*
|
||||
* @return string Representing the number of the day of the month from 1 and 31.
|
||||
*/
|
||||
public function day( $field ) {
|
||||
/*
|
||||
* MySQL returns 0 for DAY('0000-00-00') and for dates with
|
||||
* zero day parts like '2020-01-00'. PHP's strtotime() can't
|
||||
* parse these, so we extract the day directly from the string.
|
||||
*/
|
||||
if ( preg_match( '/^\d{4}-\d{2}-(\d{2})/', $field, $matches ) ) {
|
||||
return intval( $matches[1] );
|
||||
}
|
||||
/*
|
||||
* From https://www.php.net/manual/en/datetime.format.php:
|
||||
*
|
||||
* j - Day of the month without leading zeros.
|
||||
* 1 to 31.
|
||||
*/
|
||||
return intval( gmdate( 'j', strtotime( $field ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL SECOND() function.
|
||||
*
|
||||
* @see https://www.php.net/manual/en/datetime.format.php
|
||||
*
|
||||
* @param string $field Representing the time formatted as '00:00:00'.
|
||||
*
|
||||
* @return number Unsigned integer
|
||||
*/
|
||||
public function second( $field ) {
|
||||
/*
|
||||
* From https://www.php.net/manual/en/datetime.format.php:
|
||||
*
|
||||
* s - Seconds, with leading zeros (00 to 59)
|
||||
*/
|
||||
return intval( gmdate( 's', strtotime( $field ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL MINUTE() function.
|
||||
*
|
||||
* @param string $field Representing the time formatted as '00:00:00'.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function minute( $field ) {
|
||||
/*
|
||||
* From https://www.php.net/manual/en/datetime.format.php:
|
||||
*
|
||||
* i - Minutes with leading zeros.
|
||||
* 00 to 59.
|
||||
*/
|
||||
return intval( gmdate( 'i', strtotime( $field ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL HOUR() function.
|
||||
*
|
||||
* Returns the hour for time, in 24-hour format, from 0 to 23.
|
||||
* Importantly, midnight is 0, not 24.
|
||||
*
|
||||
* @param string $time Representing the time formatted, like '14:08:12'.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function hour( $time ) {
|
||||
/*
|
||||
* From https://www.php.net/manual/en/datetime.format.php:
|
||||
*
|
||||
* H 24-hour format of an hour with leading zeros.
|
||||
* 00 through 23.
|
||||
*/
|
||||
return intval( gmdate( 'H', strtotime( $time ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Covers MySQL WEEK() function.
|
||||
*
|
||||
* Always assumes $mode = 1.
|
||||
*
|
||||
* @TODO: Support other modes.
|
||||
*
|
||||
* From https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_week:
|
||||
*
|
||||
* > Returns the week number for date. The two-argument form of WEEK()
|
||||
* > enables you to specify whether the week starts on Sunday or Monday
|
||||
* > and whether the return value should be in the range from 0 to 53
|
||||
* > or from 1 to 53. If the mode argument is omitted, the value of the
|
||||
* > default_week_format system variable is used.
|
||||
* >
|
||||
* > The following table describes how the mode argument works:
|
||||
* >
|
||||
* > Mode First day of week Range Week 1 is the first week …
|
||||
* > 0 Sunday 0-53 with a Sunday in this year
|
||||
* > 1 Monday 0-53 with 4 or more days this year
|
||||
* > 2 Sunday 1-53 with a Sunday in this year
|
||||
* > 3 Monday 1-53 with 4 or more days this year
|
||||
* > 4 Sunday 0-53 with 4 or more days this year
|
||||
* > 5 Monday 0-53 with a Monday in this year
|
||||
* > 6 Sunday 1-53 with 4 or more days this year
|
||||
* > 7 Monday 1-53 with a Monday in this year
|
||||
*
|
||||
* @param string $field Representing the date.
|
||||
* @param int $mode The mode argument.
|
||||
*/
|
||||
public function week( $field, $mode ) {
|
||||
/*
|
||||
* From https://www.php.net/manual/en/datetime.format.php:
|
||||
*
|
||||
* W - ISO-8601 week number of year, weeks starting on Monday.
|
||||
* Example: 42 (the 42nd week in the year)
|
||||
*
|
||||
* Week 1 is the first week with a Thursday in it.
|
||||
*/
|
||||
return intval( gmdate( 'W', strtotime( $field ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates WEEKDAY() function in MySQL.
|
||||
*
|
||||
* Returns the day of the week as an integer.
|
||||
* The days of the week are numbered 0 to 6:
|
||||
* * 0 for Monday
|
||||
* * 1 for Tuesday
|
||||
* * 2 for Wednesday
|
||||
* * 3 for Thursday
|
||||
* * 4 for Friday
|
||||
* * 5 for Saturday
|
||||
* * 6 for Sunday
|
||||
*
|
||||
* @param string $field Representing the date.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function weekday( $field ) {
|
||||
/*
|
||||
* date('N') returns 1 (for Monday) through 7 (for Sunday)
|
||||
* That's one more than MySQL.
|
||||
* Let's subtract one to make it compatible.
|
||||
*/
|
||||
return intval( gmdate( 'N', strtotime( $field ) ) ) - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL DAYOFMONTH() function.
|
||||
*
|
||||
* @see https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_dayofmonth
|
||||
*
|
||||
* @param string $field Representing the date.
|
||||
*
|
||||
* @return int Returns the day of the month for date as a number in the range 1 to 31.
|
||||
*/
|
||||
public function dayofmonth( $field ) {
|
||||
return intval( gmdate( 'j', strtotime( $field ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL DAYOFWEEK() function.
|
||||
*
|
||||
* > Returns the weekday index for date (1 = Sunday, 2 = Monday, …, 7 = Saturday).
|
||||
* > These index values correspond to the ODBC standard. Returns NULL if date is NULL.
|
||||
*
|
||||
* @param string $field Representing the date.
|
||||
*
|
||||
* @return int Returns the weekday index for date (1 = Sunday, 2 = Monday, …, 7 = Saturday).
|
||||
*/
|
||||
public function dayofweek( $field ) {
|
||||
/**
|
||||
* From https://www.php.net/manual/en/datetime.format.php:
|
||||
*
|
||||
* `w` – Numeric representation of the day of the week
|
||||
* 0 (for Sunday) through 6 (for Saturday)
|
||||
*/
|
||||
return intval( gmdate( 'w', strtotime( $field ) ) ) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL DATE() function.
|
||||
*
|
||||
* @see https://www.php.net/manual/en/datetime.format.php
|
||||
*
|
||||
* @param string $date formatted as unix time.
|
||||
*
|
||||
* @return string formatted as '0000-00-00'.
|
||||
*/
|
||||
public function date( $date ) {
|
||||
return gmdate( 'Y-m-d', strtotime( $date ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL ISNULL() function.
|
||||
*
|
||||
* This function returns true if the argument is null, and true if not.
|
||||
*
|
||||
* @param mixed $field The field to be tested.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isnull( $field ) {
|
||||
return is_null( $field );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL IF() function.
|
||||
*
|
||||
* As 'IF' is a reserved word for PHP, function name must be changed.
|
||||
*
|
||||
* @param mixed $expression The statement to be evaluated as true or false.
|
||||
* @param mixed $truthy Statement or value returned if $expression is true.
|
||||
* @param mixed $falsy Statement or value returned if $expression is false.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function _if( $expression, $truthy, $falsy ) {
|
||||
return ( true === $expression ) ? $truthy : $falsy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL REGEXP() function.
|
||||
*
|
||||
* @param string $pattern Regular expression to match.
|
||||
* @param string $field Haystack.
|
||||
*
|
||||
* @return integer 1 if matched, 0 if not matched.
|
||||
*/
|
||||
public function regexp( $pattern, $field ) {
|
||||
/*
|
||||
* If the original query says REGEXP BINARY
|
||||
* the comparison is byte-by-byte and letter casing now
|
||||
* matters since lower- and upper-case letters have different
|
||||
* byte codes.
|
||||
*
|
||||
* The REGEXP function can't be easily made to accept two
|
||||
* parameters, so we'll have to use a hack to get around this.
|
||||
*
|
||||
* If the first character of the pattern is a null byte, we'll
|
||||
* remove it and make the comparison case-sensitive. This should
|
||||
* be reasonably safe since PHP does not allow null bytes in
|
||||
* regular expressions anyway.
|
||||
*/
|
||||
if ( "\x00" === $pattern[0] ) {
|
||||
$pattern = substr( $pattern, 1 );
|
||||
$flags = '';
|
||||
} else {
|
||||
// Otherwise, the search is case-insensitive.
|
||||
$flags = 'i';
|
||||
}
|
||||
$pattern = str_replace( '/', '\/', $pattern );
|
||||
$pattern = '/' . $pattern . '/' . $flags;
|
||||
|
||||
return preg_match( $pattern, $field );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL FIELD() function.
|
||||
*
|
||||
* This function gets the list argument and compares the first item to all the others.
|
||||
* If the same value is found, it returns the position of that value. If not, it
|
||||
* returns 0.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function field() {
|
||||
$num_args = func_num_args();
|
||||
if ( $num_args < 2 || is_null( func_get_arg( 0 ) ) ) {
|
||||
return 0;
|
||||
}
|
||||
$arg_list = func_get_args();
|
||||
$search_string = strtolower( array_shift( $arg_list ) );
|
||||
|
||||
for ( $i = 0; $i < $num_args - 1; $i++ ) {
|
||||
if ( strtolower( $arg_list[ $i ] ) === $search_string ) {
|
||||
return $i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL LOG() function.
|
||||
*
|
||||
* Used with one argument, it returns the natural logarithm of X.
|
||||
* <code>
|
||||
* LOG(X)
|
||||
* </code>
|
||||
* Used with two arguments, it returns the natural logarithm of X base B.
|
||||
* <code>
|
||||
* LOG(B, X)
|
||||
* </code>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* The version of the SQLite driver.
|
||||
*
|
||||
* This constant needs to be updated on plugin release!
|
||||
*/
|
||||
define( 'SQLITE_DRIVER_VERSION', '3.0.0-rc.4' );
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
class WP_SQLite_Crosscheck_DB_ extends WP_SQLite_DB {
|
||||
|
||||
public function __construct( string $dbname ) {
|
||||
parent::__construct( $dbname );
|
||||
$GLOBALS['sqlite'] = $this;
|
||||
$GLOBALS['mysql'] = new wpdb(
|
||||
DB_USER,
|
||||
DB_PASSWORD,
|
||||
DB_NAME,
|
||||
DB_HOST
|
||||
);
|
||||
// $this->resetDatabases();
|
||||
}
|
||||
|
||||
private function resetDatabases() {
|
||||
if ( file_exists( FQDB ) ) {
|
||||
unlink( FQDB );
|
||||
}
|
||||
$GLOBALS['mysql']->query( 'DROP DATABASE IF EXISTS ' . DB_NAME );
|
||||
$GLOBALS['mysql']->query( 'CREATE DATABASE ' . DB_NAME );
|
||||
$GLOBALS['mysql']->query( 'USE ' . DB_NAME );
|
||||
}
|
||||
|
||||
public function query( $query ) {
|
||||
/**
|
||||
* In MySQL, AUTO_INCREMENT columns don't reuse IDs assigned in rollback transactions
|
||||
* In SQLite, AUTOINCREMENT columns do reuse IDs assigned in rollback transactions
|
||||
*
|
||||
* Let's store the current AUTOINCREMENT value for each table, and restore it afterwards.
|
||||
*/
|
||||
if ( preg_match( '/^\s*rollback/i', $query ) ) {
|
||||
$autoincrements = array();
|
||||
$tables = $GLOBALS['@pdo']->query( "SELECT name as `table` FROM sqlite_master WHERE type='table' ORDER BY name" )->fetchAll();
|
||||
foreach ( $tables as $table ) {
|
||||
$table = $table['table'];
|
||||
$autoincrement = $GLOBALS['@pdo']->query( "SELECT seq FROM sqlite_sequence WHERE name = '$table'" )->fetchColumn();
|
||||
$autoincrements[ $table ] = $autoincrement ?: 1;
|
||||
}
|
||||
}
|
||||
$sqlite_retval = parent::query( $query );
|
||||
if ( preg_match( '/^\s*rollback/i', $query ) ) {
|
||||
foreach ( $autoincrements as $table => $autoincrement ) {
|
||||
$GLOBALS['@pdo']->query( "UPDATE sqlite_sequence SET seq = $autoincrement WHERE name = '$table'" );
|
||||
}
|
||||
}
|
||||
$this->crosscheck( $query, $sqlite_retval );
|
||||
return $sqlite_retval;
|
||||
}
|
||||
|
||||
private function crosscheck( $query, $sqlite_retval ) {
|
||||
// echo $query."\n\n";
|
||||
// Be lenient on cross-checking some query types
|
||||
if ( preg_match( '/^\s*SET storage_engine/i', $query ) ) {
|
||||
return;
|
||||
}
|
||||
$this->show_errors = false;
|
||||
$this->suppress_errors = true;
|
||||
$GLOBALS['mysql']->show_errors = false;
|
||||
$GLOBALS['mysql']->suppress_errors = true;
|
||||
|
||||
ob_start();
|
||||
$mysql_retval = $GLOBALS['mysql']->query( $query );
|
||||
ob_end_clean();
|
||||
|
||||
$tests = array(
|
||||
array( 'retval', $mysql_retval, $sqlite_retval ),
|
||||
array( 'num_rows', $GLOBALS['mysql']->num_rows, $GLOBALS['sqlite']->num_rows ),
|
||||
array( 'insert_id', $GLOBALS['mysql']->insert_id, $GLOBALS['sqlite']->insert_id ),
|
||||
array( 'rows_affected', $GLOBALS['mysql']->rows_affected, $GLOBALS['sqlite']->rows_affected ),
|
||||
);
|
||||
|
||||
foreach ( $tests as $test ) {
|
||||
list($factor, $mysql, $sqlite) = $test;
|
||||
if ( $mysql !== $sqlite ) {
|
||||
if ( 'insert_id' === $factor ) {
|
||||
// On multi-inserts MySQL returns the first inserted ID
|
||||
// while SQLite returns the last one. The cached insert_id
|
||||
// value stays the same for a number of subsequent queries.
|
||||
// Let's forgive this for now.
|
||||
continue;
|
||||
}
|
||||
if ( 'rows_affected' === $factor && $mysql_retval === $mysql ) {
|
||||
// SQLite doesn't provide the rowcount() functionality
|
||||
continue;
|
||||
}
|
||||
if ( 'retval' === $factor && $GLOBALS['mysql']->rows_affected === $mysql ) {
|
||||
// SQLite doesn't provide the rowcount() functionality
|
||||
continue;
|
||||
}
|
||||
echo "======================================================\n";
|
||||
echo "======== *** $factor *** differed for query ========= \n";
|
||||
echo "======================================================\n";
|
||||
echo "MySQL query: \n";
|
||||
echo "$query\n\n";
|
||||
|
||||
echo "SQLite queries: \n";
|
||||
foreach ( $this->dbh->last_translation->queries as $query ) {
|
||||
echo $query->sql . "\n";
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
$this->report_factor(
|
||||
'error',
|
||||
$GLOBALS['mysql']->last_error,
|
||||
$GLOBALS['sqlite']->last_error
|
||||
);
|
||||
foreach ( $tests as $test ) {
|
||||
$this->report_factor(
|
||||
$test[0],
|
||||
$test[1],
|
||||
$test[2]
|
||||
);
|
||||
}
|
||||
// throw new Exception();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function report_factor( $factor, $mysql, $sqlite ) {
|
||||
echo "$factor: \n";
|
||||
echo ' MySQL: ' . var_export( $mysql, true ) . "\n";
|
||||
echo ' SQLite: ' . var_export( $sqlite, true ) . "\n\n";
|
||||
}
|
||||
}
|
||||
+675
@@ -0,0 +1,675 @@
|
||||
<?php
|
||||
/**
|
||||
* Extend and replace the wpdb class.
|
||||
*
|
||||
* @package wp-sqlite-integration
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class extends wpdb and replaces it.
|
||||
*
|
||||
* It also rewrites some methods that use mysql specific functions.
|
||||
*/
|
||||
class WP_SQLite_DB extends wpdb {
|
||||
|
||||
/**
|
||||
* Database Handle
|
||||
*
|
||||
* @var WP_SQLite_Driver
|
||||
*/
|
||||
protected $dbh;
|
||||
|
||||
/**
|
||||
* Backward compatibility, see wpdb::$allow_unsafe_unquoted_parameters.
|
||||
*
|
||||
* This property is mirroring "wpdb::$allow_unsafe_unquoted_parameters",
|
||||
* because some tests are accessing it externally using PHP reflection.
|
||||
*
|
||||
* @var
|
||||
*/
|
||||
private $allow_unsafe_unquoted_parameters = true;
|
||||
|
||||
/**
|
||||
* Connects to the SQLite database.
|
||||
*
|
||||
* Unlike for MySQL, no credentials and host are needed.
|
||||
*
|
||||
* @param string $dbname Database name.
|
||||
*/
|
||||
public function __construct( $dbname ) {
|
||||
/**
|
||||
* We need to initialize the "$wpdb" global early, so that the SQLite
|
||||
* driver can configure the database. The call stack goes like this:
|
||||
*
|
||||
* 1. The "parent::__construct()" call executes "$this->db_connect()".
|
||||
* 2. The database connection call initializes the SQLite driver.
|
||||
* 3. The SQLite driver initializes and runs "WP_SQLite_Configurator".
|
||||
* 4. The configurator uses "WP_SQLite_Information_Schema_Reconstructor",
|
||||
* which requires "wp-admin/includes/schema.php" when in WordPress.
|
||||
* 5. The "wp-admin/includes/schema.php" requires the "$wpdb" global,
|
||||
* which creates a circular dependency.
|
||||
*/
|
||||
$GLOBALS['wpdb'] = $this;
|
||||
|
||||
parent::__construct( '', '', $dbname, '' );
|
||||
$this->charset = 'utf8mb4';
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to set character set for the database.
|
||||
*
|
||||
* This overrides wpdb::set_charset(), only to dummy out the MySQL function.
|
||||
*
|
||||
* @see wpdb::set_charset()
|
||||
*
|
||||
* @param resource $dbh The resource given by mysql_connect.
|
||||
* @param string $charset Optional. The character set. Default null.
|
||||
* @param string $collate Optional. The collation. Default null.
|
||||
*/
|
||||
public function set_charset( $dbh, $charset = null, $collate = null ) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the character set for the database.
|
||||
* Hardcoded to utf8mb4 for now.
|
||||
*
|
||||
* @param string $table The table name.
|
||||
* @param string $column The column name.
|
||||
*
|
||||
* @return string The character set.
|
||||
*/
|
||||
public function get_col_charset( $table, $column ) {
|
||||
// Hardcoded for now.
|
||||
return 'utf8mb4';
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the current SQL mode, and ensures its WordPress compatibility.
|
||||
*
|
||||
* If no modes are passed, it will ensure the current MySQL server modes are compatible.
|
||||
*
|
||||
* This overrides wpdb::set_sql_mode() while closely mirroring its implementation.
|
||||
*
|
||||
* @param array $modes Optional. A list of SQL modes to set. Default empty array.
|
||||
*/
|
||||
public function set_sql_mode( $modes = array() ) {
|
||||
if ( empty( $modes ) ) {
|
||||
$result = $this->dbh->query( 'SELECT @@SESSION.sql_mode' );
|
||||
if ( ! isset( $result[0] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$modes_str = $result[0]->{'@@SESSION.sql_mode'};
|
||||
if ( empty( $modes_str ) ) {
|
||||
return;
|
||||
}
|
||||
$modes = explode( ',', $modes_str );
|
||||
}
|
||||
|
||||
$modes = array_change_key_case( $modes, CASE_UPPER );
|
||||
|
||||
/**
|
||||
* Filters the list of incompatible SQL modes to exclude.
|
||||
*
|
||||
* @since 3.9.0
|
||||
*
|
||||
* @param array $incompatible_modes An array of incompatible modes.
|
||||
*/
|
||||
$incompatible_modes = (array) apply_filters( 'incompatible_sql_modes', $this->incompatible_modes );
|
||||
|
||||
foreach ( $modes as $i => $mode ) {
|
||||
if ( in_array( $mode, $incompatible_modes, true ) ) {
|
||||
unset( $modes[ $i ] );
|
||||
}
|
||||
}
|
||||
$modes_str = implode( ',', $modes );
|
||||
|
||||
$this->dbh->query( "SET SESSION sql_mode='$modes_str'" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the current database connection.
|
||||
* Noop in SQLite.
|
||||
*
|
||||
* @return bool True to indicate the connection was successfully closed.
|
||||
*/
|
||||
public function close() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to select the database connection.
|
||||
*
|
||||
* This overrides wpdb::select(), only to dummy out the MySQL function.
|
||||
*
|
||||
* @see wpdb::select()
|
||||
*
|
||||
* @param string $db MySQL database name. Not used.
|
||||
* @param resource|null $dbh Optional link identifier.
|
||||
*/
|
||||
public function select( $db, $dbh = null ) {
|
||||
$this->ready = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to escape characters.
|
||||
*
|
||||
* This overrides wpdb::_real_escape() to avoid using mysql_real_escape_string().
|
||||
*
|
||||
* @see wpdb::_real_escape()
|
||||
*
|
||||
* @param string $data The string to escape.
|
||||
*
|
||||
* @return string escaped
|
||||
*/
|
||||
public function _real_escape( $data ) {
|
||||
if ( ! is_scalar( $data ) ) {
|
||||
return '';
|
||||
}
|
||||
$escaped = addslashes( $data );
|
||||
return $this->add_placeholder_escape( $escaped );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints SQL/DB error.
|
||||
*
|
||||
* This overrides wpdb::print_error() while closely mirroring its implementation.
|
||||
*
|
||||
* @global array $EZSQL_ERROR Stores error information of query and error string.
|
||||
*
|
||||
* @param string $str The error to display.
|
||||
* @return void|false Void if the showing of errors is enabled, false if disabled.
|
||||
*/
|
||||
public function print_error( $str = '' ) {
|
||||
global $EZSQL_ERROR;
|
||||
|
||||
if ( ! $str ) {
|
||||
$str = $this->last_error;
|
||||
}
|
||||
|
||||
$EZSQL_ERROR[] = array(
|
||||
'query' => $this->last_query,
|
||||
'error_str' => $str,
|
||||
);
|
||||
|
||||
if ( $this->suppress_errors ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$caller = $this->get_caller();
|
||||
if ( $caller ) {
|
||||
// Not translated, as this will only appear in the error log.
|
||||
$error_str = sprintf( 'WordPress database error %1$s for query %2$s made by %3$s', $str, $this->last_query, $caller );
|
||||
} else {
|
||||
$error_str = sprintf( 'WordPress database error %1$s for query %2$s', $str, $this->last_query );
|
||||
}
|
||||
|
||||
error_log( $error_str );
|
||||
|
||||
// Are we showing errors?
|
||||
if ( ! $this->show_errors ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
wp_load_translations_early();
|
||||
|
||||
// If there is an error then take note of it.
|
||||
if ( is_multisite() ) {
|
||||
$msg = sprintf(
|
||||
"%s [%s]\n%s\n",
|
||||
__( 'WordPress database error:' ),
|
||||
$str,
|
||||
$this->last_query
|
||||
);
|
||||
|
||||
if ( defined( 'ERRORLOGFILE' ) ) {
|
||||
error_log( $msg, 3, ERRORLOGFILE );
|
||||
}
|
||||
if ( defined( 'DIEONDBERROR' ) ) {
|
||||
wp_die( $msg );
|
||||
}
|
||||
} else {
|
||||
$str = htmlspecialchars( $str, ENT_QUOTES );
|
||||
$query = htmlspecialchars( $this->last_query, ENT_QUOTES );
|
||||
|
||||
printf(
|
||||
'<div id="error"><p class="wpdberror"><strong>%s</strong> [%s]<br /><code>%s</code></p></div>',
|
||||
__( 'WordPress database error:' ),
|
||||
$str,
|
||||
$query
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to flush cached data.
|
||||
*
|
||||
* This overrides wpdb::flush(). This is not necessarily overridden, because
|
||||
* $result will never be resource.
|
||||
*
|
||||
* @see wpdb::flush
|
||||
*/
|
||||
public function flush() {
|
||||
$this->last_result = array();
|
||||
$this->col_info = null;
|
||||
$this->last_query = null;
|
||||
$this->rows_affected = 0;
|
||||
$this->num_rows = 0;
|
||||
$this->last_error = '';
|
||||
$this->result = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to do the database connection.
|
||||
*
|
||||
* This overrides wpdb::db_connect() to avoid using MySQL function.
|
||||
*
|
||||
* @see wpdb::db_connect()
|
||||
*
|
||||
* @param bool $allow_bail Not used.
|
||||
* @return void
|
||||
*/
|
||||
public function db_connect( $allow_bail = true ) {
|
||||
if ( $this->dbh ) {
|
||||
return;
|
||||
}
|
||||
$this->init_charset();
|
||||
|
||||
$pdo = null;
|
||||
if ( isset( $GLOBALS['@pdo'] ) ) {
|
||||
$pdo = $GLOBALS['@pdo'];
|
||||
}
|
||||
|
||||
// Migrate the database file from a legacy path, if it exists.
|
||||
if ( ! defined( 'DB_FILE' ) && ! file_exists( FQDB ) ) {
|
||||
$old_db_path = FQDBDIR . '.ht.sqlite.php';
|
||||
|
||||
if ( file_exists( $old_db_path ) ) {
|
||||
if ( ! rename( $old_db_path, FQDB ) ) {
|
||||
wp_die( 'Failed to rename database file.', 'Error!' );
|
||||
}
|
||||
|
||||
foreach ( array( '-wal', '-shm', '-journal' ) as $suffix ) {
|
||||
if ( file_exists( $old_db_path . $suffix ) ) {
|
||||
if ( ! rename( $old_db_path . $suffix, FQDB . $suffix ) ) {
|
||||
wp_die( 'Failed to rename database file.', 'Error!' );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( null === $this->dbname || '' === $this->dbname ) {
|
||||
$this->bail(
|
||||
'The database name was not set. The SQLite driver requires a database name to be set to emulate MySQL information schema tables.',
|
||||
'db_connect_fail'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->ensure_database_directory( FQDB );
|
||||
|
||||
try {
|
||||
$connection = new WP_SQLite_Connection(
|
||||
array(
|
||||
'pdo' => $pdo,
|
||||
'path' => FQDB,
|
||||
'journal_mode' => defined( 'SQLITE_JOURNAL_MODE' ) ? SQLITE_JOURNAL_MODE : null,
|
||||
)
|
||||
);
|
||||
$this->dbh = new WP_SQLite_Driver( $connection, $this->dbname );
|
||||
$GLOBALS['@pdo'] = $this->dbh->get_connection()->get_pdo();
|
||||
} catch ( Throwable $e ) {
|
||||
$this->last_error = $this->format_error_message( $e );
|
||||
}
|
||||
if ( $this->last_error ) {
|
||||
return false;
|
||||
}
|
||||
$this->ready = true;
|
||||
$this->set_sql_mode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to dummy out wpdb::check_connection()
|
||||
*
|
||||
* @param bool $allow_bail Not used.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function check_connection( $allow_bail = true ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a SQL query for safe execution.
|
||||
*
|
||||
* See "wpdb::prepare()". This override only fixes a WPDB test issue.
|
||||
*
|
||||
* @param string $query Query statement with `sprintf()`-like placeholders.
|
||||
* @param array|mixed $args The array of variables or the first variable to substitute.
|
||||
* @param mixed ...$args Further variables to substitute when using individual arguments.
|
||||
* @return string|void Sanitized query string, if there is a query to prepare.
|
||||
*/
|
||||
public function prepare( $query, ...$args ) {
|
||||
/*
|
||||
* Sync "$allow_unsafe_unquoted_parameters" with the WPDB parent property.
|
||||
* This is only needed because some WPDB tests are accessing the private
|
||||
* property externally via PHP reflection. This should be fixed WP tests.
|
||||
*/
|
||||
$wpdb_allow_unsafe_unquoted_parameters = $this->__get( 'allow_unsafe_unquoted_parameters' );
|
||||
if ( $wpdb_allow_unsafe_unquoted_parameters !== $this->allow_unsafe_unquoted_parameters ) {
|
||||
$property = new ReflectionProperty( 'wpdb', 'allow_unsafe_unquoted_parameters' );
|
||||
$property->setAccessible( true );
|
||||
$property->setValue( $this, $this->allow_unsafe_unquoted_parameters );
|
||||
$property->setAccessible( false );
|
||||
}
|
||||
|
||||
return parent::prepare( $query, ...$args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a database query.
|
||||
*
|
||||
* This overrides wpdb::query() while closely mirroring its implementation.
|
||||
*
|
||||
* @see wpdb::query()
|
||||
*
|
||||
* @param string $query Database query.
|
||||
*
|
||||
* @param string $query Database query.
|
||||
* @return int|bool Boolean true for CREATE, ALTER, TRUNCATE and DROP queries. Number of rows
|
||||
* affected/selected for all other queries. Boolean false on error.
|
||||
*/
|
||||
public function query( $query ) {
|
||||
// Query Monitor integration:
|
||||
$query_monitor_active = defined( 'SQLITE_QUERY_MONITOR_LOADED' ) && SQLITE_QUERY_MONITOR_LOADED;
|
||||
if ( $query_monitor_active && $this->show_errors ) {
|
||||
$this->hide_errors();
|
||||
}
|
||||
|
||||
if ( ! $this->ready ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$query = apply_filters( 'query', $query );
|
||||
|
||||
if ( ! $query ) {
|
||||
$this->insert_id = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->flush();
|
||||
|
||||
// Log how the function was called.
|
||||
$this->func_call = "\$db->query(\"$query\")";
|
||||
|
||||
// Keep track of the last query for debug.
|
||||
$this->last_query = $query;
|
||||
|
||||
// Save the query count before running another query.
|
||||
$last_query_count = count( $this->queries ?? array() );
|
||||
|
||||
/*
|
||||
* @TODO: WPDB uses "$this->check_current_query" to check table/column
|
||||
* charset and strip all invalid characters from the query.
|
||||
* This is an involved process that we can bypass for SQLite,
|
||||
* if we simply strip all invalid UTF-8 characters from the query.
|
||||
*
|
||||
* To do so, mb_convert_encoding can be used with an optional
|
||||
* fallback to a htmlspecialchars method. E.g.:
|
||||
* https://github.com/nette/utils/blob/be534713c227aeef57ce1883fc17bc9f9e29eca2/src/Utils/Strings.php#L42
|
||||
*/
|
||||
$this->_do_query( $query );
|
||||
|
||||
if ( $this->last_error ) {
|
||||
// Clear insert_id on a subsequent failed insert.
|
||||
if ( $this->insert_id && preg_match( '/^\s*(insert|replace)\s/i', $query ) ) {
|
||||
$this->insert_id = 0;
|
||||
}
|
||||
|
||||
$this->print_error();
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( preg_match( '/^\s*(create|alter|truncate|drop)\s/i', $query ) ) {
|
||||
$return_val = true;
|
||||
} elseif ( preg_match( '/^\s*(insert|delete|update|replace)\s/i', $query ) ) {
|
||||
$this->rows_affected = $this->dbh->get_last_return_value();
|
||||
|
||||
// Take note of the insert_id.
|
||||
if ( preg_match( '/^\s*(insert|replace)\s/i', $query ) ) {
|
||||
$this->insert_id = $this->dbh->get_insert_id();
|
||||
}
|
||||
|
||||
// Return number of rows affected.
|
||||
$return_val = $this->rows_affected;
|
||||
} else {
|
||||
$num_rows = 0;
|
||||
|
||||
if ( is_array( $this->result ) ) {
|
||||
$this->last_result = $this->result;
|
||||
$num_rows = count( $this->result );
|
||||
}
|
||||
|
||||
// Log and return the number of rows selected.
|
||||
$this->num_rows = $num_rows;
|
||||
$return_val = $num_rows;
|
||||
}
|
||||
|
||||
// Query monitor integration:
|
||||
if ( $query_monitor_active && class_exists( 'QM_Backtrace' ) ) {
|
||||
if ( did_action( 'qm/cease' ) ) {
|
||||
$this->queries = array();
|
||||
}
|
||||
|
||||
$i = $last_query_count;
|
||||
if ( ! isset( $this->queries[ $i ] ) ) {
|
||||
return $return_val;
|
||||
}
|
||||
|
||||
$this->queries[ $i ]['trace'] = new QM_Backtrace();
|
||||
if ( ! isset( $this->queries[ $i ][3] ) ) {
|
||||
$this->queries[ $i ][3] = $this->time_start;
|
||||
}
|
||||
|
||||
if ( $this->last_error && ! $this->suppress_errors ) {
|
||||
$this->queries[ $i ]['result'] = new WP_Error( 'qmdb', $this->last_error );
|
||||
} else {
|
||||
$this->queries[ $i ]['result'] = (int) $return_val;
|
||||
}
|
||||
|
||||
// Add SQLite query data.
|
||||
$this->queries[ $i ]['sqlite_queries'] = $this->dbh->get_last_sqlite_queries();
|
||||
}
|
||||
return $return_val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal function to perform the SQLite query call.
|
||||
*
|
||||
* This closely mirrors wpdb::_do_query().
|
||||
*
|
||||
* @see wpdb::_do_query()
|
||||
*
|
||||
* @param string $query The query to run.
|
||||
*/
|
||||
private function _do_query( $query ) {
|
||||
if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
|
||||
$this->timer_start();
|
||||
}
|
||||
|
||||
try {
|
||||
$this->result = $this->dbh->query( $query );
|
||||
} catch ( Throwable $e ) {
|
||||
$this->last_error = $this->format_error_message( $e );
|
||||
}
|
||||
|
||||
++$this->num_queries;
|
||||
|
||||
if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
|
||||
$this->log_query(
|
||||
$query,
|
||||
$this->timer_stop(),
|
||||
$this->get_caller(),
|
||||
$this->time_start,
|
||||
array()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to set the class variable $col_info.
|
||||
*
|
||||
* This overrides wpdb::load_col_info(), which uses a mysql function.
|
||||
*
|
||||
* @see wpdb::load_col_info()
|
||||
*/
|
||||
protected function load_col_info() {
|
||||
if ( $this->col_info ) {
|
||||
return;
|
||||
}
|
||||
$this->col_info = array();
|
||||
foreach ( $this->dbh->get_last_column_meta() as $column ) {
|
||||
$this->col_info[] = (object) array(
|
||||
'name' => $column['name'],
|
||||
'orgname' => $column['mysqli:orgname'],
|
||||
'table' => $column['table'],
|
||||
'orgtable' => $column['mysqli:orgtable'],
|
||||
'def' => '', // Unused, always ''.
|
||||
'db' => $column['mysqli:db'],
|
||||
'catalog' => 'def', // Unused, always 'def'.
|
||||
'max_length' => 0, // As of PHP 8.1, this is always 0.
|
||||
'length' => $column['len'],
|
||||
'charsetnr' => $column['mysqli:charsetnr'],
|
||||
'flags' => $column['mysqli:flags'],
|
||||
'type' => $column['mysqli:type'],
|
||||
'decimals' => $column['precision'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to return what the database can do.
|
||||
*
|
||||
* This overrides wpdb::has_cap() to avoid using MySQL functions.
|
||||
* SQLite supports subqueries, but not support collation, group_concat and set_charset.
|
||||
*
|
||||
* @see wpdb::has_cap()
|
||||
*
|
||||
* @param string $db_cap The feature to check for. Accepts 'collation',
|
||||
* 'group_concat', 'subqueries', 'set_charset',
|
||||
* 'utf8mb4', or 'utf8mb4_520'.
|
||||
*
|
||||
* @return bool Whether the database feature is supported, false otherwise.
|
||||
*/
|
||||
public function has_cap( $db_cap ) {
|
||||
return 'subqueries' === strtolower( $db_cap );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to return database version number.
|
||||
*
|
||||
* This overrides wpdb::db_version() to avoid using MySQL function.
|
||||
* It returns mysql version number, but it means nothing for SQLite.
|
||||
* So it return the newest mysql version.
|
||||
*
|
||||
* @see wpdb::db_version()
|
||||
*/
|
||||
public function db_version() {
|
||||
return '8.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the version of the SQLite engine.
|
||||
*
|
||||
* @return string SQLite engine version as a string.
|
||||
*/
|
||||
public function db_server_info() {
|
||||
return $this->dbh->get_sqlite_version();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the SQLite database directory exists and is writable.
|
||||
* Create .htaccess and index.php files to prevent direct access.
|
||||
*
|
||||
* @param string $database_path The path to the SQLite database file.
|
||||
*/
|
||||
private function ensure_database_directory( string $database_path ) {
|
||||
$dir = dirname( $database_path );
|
||||
|
||||
// Set the umask to 0000 to apply permissions exactly as specified.
|
||||
// A non-zero umask affects new file and directory permissions.
|
||||
$umask = umask( 0 );
|
||||
|
||||
// Ensure database directory.
|
||||
if ( ! is_dir( $dir ) ) {
|
||||
if ( ! @mkdir( $dir, 0700, true ) ) {
|
||||
wp_die( sprintf( 'Failed to create database directory: %s', $dir ), 'Error!' );
|
||||
}
|
||||
}
|
||||
if ( ! is_writable( $dir ) ) {
|
||||
wp_die( sprintf( 'Database directory is not writable: %s', $dir ), 'Error!' );
|
||||
}
|
||||
|
||||
// Ensure .htaccess file to prevent direct access.
|
||||
$path = $dir . DIRECTORY_SEPARATOR . '.htaccess';
|
||||
if ( ! is_file( $path ) ) {
|
||||
$result = file_put_contents( $path, 'DENY FROM ALL', LOCK_EX );
|
||||
if ( false === $result ) {
|
||||
wp_die( sprintf( 'Failed to create file: %s', $path ), 'Error!' );
|
||||
}
|
||||
chmod( $path, 0600 );
|
||||
}
|
||||
|
||||
// Ensure index.php file to prevent direct access.
|
||||
$path = $dir . DIRECTORY_SEPARATOR . 'index.php';
|
||||
if ( ! is_file( $path ) ) {
|
||||
$result = file_put_contents( $path, '<?php // Silence is gold. ?>', LOCK_EX );
|
||||
if ( false === $result ) {
|
||||
wp_die( sprintf( 'Failed to create file: %s', $path ), 'Error!' );
|
||||
}
|
||||
chmod( $path, 0600 );
|
||||
}
|
||||
|
||||
// Restore the original umask value.
|
||||
umask( $umask );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format SQLite driver error message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function format_error_message( Throwable $e ) {
|
||||
$output = '<div style="clear:both"> </div>' . PHP_EOL;
|
||||
|
||||
// Queries.
|
||||
if ( $e instanceof WP_SQLite_Driver_Exception ) {
|
||||
$driver = $e->getDriver();
|
||||
|
||||
$output .= '<div class="queries" style="clear:both;margin-bottom:2px;border:red dotted thin;">' . PHP_EOL;
|
||||
$output .= '<p>MySQL query:</p>' . PHP_EOL;
|
||||
$output .= '<p>' . $driver->get_last_mysql_query() . '</p>' . PHP_EOL;
|
||||
$output .= '<p>Queries made or created this session were:</p>' . PHP_EOL;
|
||||
$output .= '<ol>' . PHP_EOL;
|
||||
foreach ( $driver->get_last_sqlite_queries() as $q ) {
|
||||
$message = "Executing: {$q['sql']} | " . ( $q['params'] ? 'parameters: ' . implode( ', ', $q['params'] ) : '(no parameters)' );
|
||||
$output .= '<li>' . htmlspecialchars( $message ) . '</li>' . PHP_EOL;
|
||||
}
|
||||
$output .= '</ol>' . PHP_EOL;
|
||||
$output .= '</div>' . PHP_EOL;
|
||||
}
|
||||
|
||||
// Message.
|
||||
$output .= '<div style="clear:both;margin-bottom:2px;border:red dotted thin;" class="error_message" style="border-bottom:dotted blue thin;">' . PHP_EOL;
|
||||
$output .= $e->getMessage() . PHP_EOL;
|
||||
$output .= '</div>' . PHP_EOL;
|
||||
|
||||
// Backtrace.
|
||||
$output .= '<p>Backtrace:</p>' . PHP_EOL;
|
||||
$output .= '<pre>' . $e->getTraceAsString() . '</pre>' . PHP_EOL;
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/**
|
||||
* Main integration file.
|
||||
*
|
||||
* @package wp-sqlite-integration
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Load the "SQLITE_DRIVER_VERSION" constant.
|
||||
*/
|
||||
require_once __DIR__ . '/../database/version.php';
|
||||
|
||||
// Require the constants file.
|
||||
require_once __DIR__ . '/../../constants.php';
|
||||
|
||||
// Bail early if DB_ENGINE is not defined as sqlite.
|
||||
if ( ! defined( 'DB_ENGINE' ) || 'sqlite' !== DB_ENGINE ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! extension_loaded( 'pdo' ) ) {
|
||||
wp_die(
|
||||
new WP_Error(
|
||||
'pdo_not_loaded',
|
||||
sprintf(
|
||||
'<h1>%1$s</h1><p>%2$s</p>',
|
||||
'PHP PDO Extension is not loaded',
|
||||
'Your PHP installation appears to be missing the PDO extension which is required for this version of WordPress and the type of database you have specified.'
|
||||
)
|
||||
),
|
||||
'PHP PDO Extension is not loaded.'
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! extension_loaded( 'pdo_sqlite' ) ) {
|
||||
wp_die(
|
||||
new WP_Error(
|
||||
'pdo_driver_not_loaded',
|
||||
sprintf(
|
||||
'<h1>%1$s</h1><p>%2$s</p>',
|
||||
'PDO Driver for SQLite is missing',
|
||||
'Your PHP installation appears not to have the right PDO drivers loaded. These are required for this version of WordPress and the type of database you have specified.'
|
||||
)
|
||||
),
|
||||
'PDO Driver for SQLite is missing.'
|
||||
);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../database/load.php';
|
||||
require_once __DIR__ . '/class-wp-sqlite-db.php';
|
||||
require_once __DIR__ . '/install-functions.php';
|
||||
|
||||
$db_name = defined( 'DB_NAME' ) ? DB_NAME : '';
|
||||
|
||||
/*
|
||||
* Debug: Cross-check with MySQL.
|
||||
* This is for debugging purpose only and requires files
|
||||
* that are present in the GitHub repository
|
||||
* but not the plugin published on WordPress.org.
|
||||
*/
|
||||
$crosscheck_tests_file_path = __DIR__ . '/class-wp-sqlite-crosscheck-db.php';
|
||||
if ( defined( 'SQLITE_DEBUG_CROSSCHECK' ) && SQLITE_DEBUG_CROSSCHECK && file_exists( $crosscheck_tests_file_path ) ) {
|
||||
require_once $crosscheck_tests_file_path;
|
||||
$GLOBALS['wpdb'] = new WP_SQLite_Crosscheck_DB( $db_name );
|
||||
} else {
|
||||
$GLOBALS['wpdb'] = new WP_SQLite_DB( $db_name );
|
||||
|
||||
// Boot the Query Monitor plugin if it is active.
|
||||
require_once __DIR__ . '/../../integrations/query-monitor/boot.php';
|
||||
}
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
/**
|
||||
* Main integration file.
|
||||
*
|
||||
* @package wp-sqlite-integration
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Function to create tables according to the schemas of WordPress.
|
||||
*
|
||||
* This is executed only once while installation.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @return boolean
|
||||
*
|
||||
* @throws PDOException If the database connection fails.
|
||||
*/
|
||||
function sqlite_make_db_sqlite() {
|
||||
global $wpdb;
|
||||
|
||||
include_once ABSPATH . 'wp-admin/includes/schema.php';
|
||||
|
||||
$table_schemas = wp_get_db_schema();
|
||||
$queries = explode( ';', $table_schemas );
|
||||
try {
|
||||
$pdo_class = PHP_VERSION_ID >= 80400 ? PDO\SQLite::class : PDO::class; // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
|
||||
$pdo = new $pdo_class( 'sqlite:' . FQDB, null, null, array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ) ); // phpcs:ignore WordPress.DB.RestrictedClasses
|
||||
} catch ( PDOException $err ) {
|
||||
$err_data = $err->errorInfo; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
||||
$message = 'Database connection error!<br />';
|
||||
$message .= sprintf( 'Error message is: %s', $err_data[2] );
|
||||
wp_die( $message, 'Database Error!' );
|
||||
}
|
||||
|
||||
$translator = new WP_SQLite_Driver(
|
||||
new WP_SQLite_Connection( array( 'pdo' => $pdo ) ),
|
||||
$wpdb->dbname
|
||||
);
|
||||
$query = null;
|
||||
|
||||
try {
|
||||
$translator->begin_transaction();
|
||||
foreach ( $queries as $query ) {
|
||||
$query = trim( $query );
|
||||
if ( empty( $query ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$translator->query( $query );
|
||||
}
|
||||
$translator->commit();
|
||||
} catch ( PDOException $err ) {
|
||||
$translator->rollback();
|
||||
$message = sprintf(
|
||||
'Error occurred while creating tables or indexes...<br />Query was: %s<br />',
|
||||
var_export( $query, true )
|
||||
);
|
||||
$message .= sprintf( 'Error message is: %s', $err->getMessage() );
|
||||
wp_die( $message, 'Database Error!' );
|
||||
}
|
||||
|
||||
/*
|
||||
* Debug: Cross-check with MySQL.
|
||||
* This is for debugging purpose only and requires files
|
||||
* that are present in the GitHub repository
|
||||
* but not the plugin published on WordPress.org.
|
||||
*/
|
||||
if ( defined( 'SQLITE_DEBUG_CROSSCHECK' ) && SQLITE_DEBUG_CROSSCHECK ) {
|
||||
$host = DB_HOST;
|
||||
$port = 3306;
|
||||
if ( str_contains( $host, ':' ) ) {
|
||||
$host_parts = explode( ':', $host );
|
||||
$host = $host_parts[0];
|
||||
$port = $host_parts[1];
|
||||
}
|
||||
$dsn = 'mysql:host=' . $host . '; port=' . $port . '; dbname=' . DB_NAME;
|
||||
$pdo_class = PHP_VERSION_ID >= 80400 ? PDO\MySQL::class : PDO::class; // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
|
||||
$pdo_mysql = new $pdo_class( $dsn, DB_USER, DB_PASSWORD, array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ) ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
|
||||
$pdo_mysql->query( 'SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";' );
|
||||
$pdo_mysql->query( 'SET time_zone = "+00:00";' );
|
||||
foreach ( $queries as $query ) {
|
||||
$query = trim( $query );
|
||||
if ( empty( $query ) ) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$pdo_mysql->beginTransaction();
|
||||
$pdo_mysql->query( $query );
|
||||
} catch ( PDOException $err ) {
|
||||
$err_data = $err->errorInfo; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
||||
$err_code = $err_data[1];
|
||||
// phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
|
||||
if ( 5 == $err_code || 6 == $err_code ) {
|
||||
// If the database is locked, commit again.
|
||||
$pdo_mysql->commit();
|
||||
} else {
|
||||
$pdo_mysql->rollBack();
|
||||
$message = sprintf(
|
||||
'Error occurred while creating tables or indexes...<br />Query was: %s<br />',
|
||||
var_export( $query, true )
|
||||
);
|
||||
$message .= sprintf( 'Error message is: %s', $err_data[2] );
|
||||
wp_die( $message, 'Database Error!' );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pdo = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'wp_install' ) ) {
|
||||
/**
|
||||
* Installs the site.
|
||||
*
|
||||
* Runs the required functions to set up and populate the database,
|
||||
* including primary admin user and initial options.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param string $blog_title Site title.
|
||||
* @param string $user_name User's username.
|
||||
* @param string $user_email User's email.
|
||||
* @param bool $is_public Whether the site is public.
|
||||
* @param string $deprecated Optional. Not used.
|
||||
* @param string $user_password Optional. User's chosen password. Default empty (random password).
|
||||
* @param string $language Optional. Language chosen. Default empty.
|
||||
* @return array {
|
||||
* Data for the newly installed site.
|
||||
*
|
||||
* @type string $url The URL of the site.
|
||||
* @type int $user_id The ID of the site owner.
|
||||
* @type string $password The password of the site owner, if their user account didn't already exist.
|
||||
* @type string $password_message The explanatory message regarding the password.
|
||||
* }
|
||||
*/
|
||||
function wp_install( $blog_title, $user_name, $user_email, $is_public, $deprecated = '', $user_password = '', $language = '' ) {
|
||||
if ( ! empty( $deprecated ) ) {
|
||||
_deprecated_argument( __FUNCTION__, '2.6.0' );
|
||||
}
|
||||
|
||||
wp_check_mysql_version();
|
||||
wp_cache_flush();
|
||||
/* SQLite changes: Replace the call to make_db_current_silent() with sqlite_make_db_sqlite(). */
|
||||
sqlite_make_db_sqlite(); // phpcs:ignore PHPCompatibility.Extensions.RemovedExtensions.sqliteRemoved
|
||||
populate_options();
|
||||
populate_roles();
|
||||
|
||||
update_option( 'blogname', $blog_title );
|
||||
update_option( 'admin_email', $user_email );
|
||||
update_option( 'blog_public', $is_public );
|
||||
|
||||
// Freshness of site - in the future, this could get more specific about actions taken, perhaps.
|
||||
update_option( 'fresh_site', 1 );
|
||||
|
||||
if ( $language ) {
|
||||
update_option( 'WPLANG', $language );
|
||||
}
|
||||
|
||||
$guessurl = wp_guess_url();
|
||||
|
||||
update_option( 'siteurl', $guessurl );
|
||||
|
||||
// If not a public site, don't ping.
|
||||
if ( ! $is_public ) {
|
||||
update_option( 'default_pingback_flag', 0 );
|
||||
}
|
||||
|
||||
/*
|
||||
* Create default user. If the user already exists, the user tables are
|
||||
* being shared among sites. Just set the role in that case.
|
||||
*/
|
||||
$user_id = username_exists( $user_name );
|
||||
$user_password = trim( $user_password );
|
||||
$email_password = false;
|
||||
$user_created = false;
|
||||
|
||||
if ( ! $user_id && empty( $user_password ) ) {
|
||||
$user_password = wp_generate_password( 12, false );
|
||||
$message = __( '<strong><em>Note that password</em></strong> carefully! It is a <em>random</em> password that was generated just for you.', 'sqlite-database-integration' );
|
||||
$user_id = wp_create_user( $user_name, $user_password, $user_email );
|
||||
update_user_meta( $user_id, 'default_password_nag', true );
|
||||
$email_password = true;
|
||||
$user_created = true;
|
||||
} elseif ( ! $user_id ) {
|
||||
// Password has been provided.
|
||||
$message = '<em>' . __( 'Your chosen password.', 'sqlite-database-integration' ) . '</em>';
|
||||
$user_id = wp_create_user( $user_name, $user_password, $user_email );
|
||||
$user_created = true;
|
||||
} else {
|
||||
$message = __( 'User already exists. Password inherited.', 'sqlite-database-integration' );
|
||||
}
|
||||
|
||||
$user = new WP_User( $user_id );
|
||||
$user->set_role( 'administrator' );
|
||||
|
||||
if ( $user_created ) {
|
||||
$user->user_url = $guessurl;
|
||||
wp_update_user( $user );
|
||||
}
|
||||
|
||||
wp_install_defaults( $user_id );
|
||||
|
||||
wp_install_maybe_enable_pretty_permalinks();
|
||||
|
||||
flush_rewrite_rules();
|
||||
|
||||
wp_new_blog_notification( $blog_title, $guessurl, $user_id, ( $email_password ? $user_password : __( 'The password you chose during installation.', 'sqlite-database-integration' ) ) );
|
||||
|
||||
wp_cache_flush();
|
||||
|
||||
/**
|
||||
* Fires after a site is fully installed.
|
||||
*
|
||||
* @since 3.9.0
|
||||
*
|
||||
* @param WP_User $user The site owner.
|
||||
*/
|
||||
do_action( 'wp_install', $user );
|
||||
|
||||
return array(
|
||||
'url' => $guessurl,
|
||||
'user_id' => $user_id,
|
||||
'password' => $user_password,
|
||||
'password_message' => $message,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user