diff --git a/.github/workflows/phpunit-tests-run.yml b/.github/workflows/phpunit-tests-run.yml index e492d4c4..1ed1bdc4 100644 --- a/.github/workflows/phpunit-tests-run.yml +++ b/.github/workflows/phpunit-tests-run.yml @@ -12,11 +12,6 @@ on: description: 'The version of PHP to use, in the format of X.Y' required: true type: 'string' - phpunit-config: - description: 'The PHPUnit configuration file to use' - required: false - type: 'string' - default: 'phpunit.xml.dist' sqlite: description: 'SQLite version to install (e.g., 3.24.0). Leave empty for latest version.' required: false @@ -24,7 +19,6 @@ on: default: 'latest' env: LOCAL_PHP: ${{ inputs.php }}-fpm - PHPUNIT_CONFIG: ${{ inputs.phpunit-config }} jobs: phpunit-tests: @@ -88,6 +82,3 @@ jobs: - name: Run PHPUnit tests run: php ./vendor/bin/phpunit -c ./phpunit.xml.dist working-directory: packages/mysql-on-sqlite - - - name: Run PHPUnit tests for the legacy driver - run: php ./vendor/bin/phpunit -c ./phpunit.xml.dist diff --git a/.github/workflows/phpunit-tests.yml b/.github/workflows/phpunit-tests.yml index 9bfa33c7..b67233fb 100644 --- a/.github/workflows/phpunit-tests.yml +++ b/.github/workflows/phpunit-tests.yml @@ -43,4 +43,3 @@ jobs: os: ${{ matrix.os }} php: ${{ matrix.php }} sqlite: ${{ matrix.sqlite || 'latest' }} - phpunit-config: ${{ 'phpunit.xml.dist' }} diff --git a/AGENTS.md b/AGENTS.md index a0574315..eb9f2d08 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,15 +28,6 @@ The codebase is pure PHP with zero dependencies. It supports PHP 7.2 through 8.5 MySQL syntax from version 5.7 onward, and requires SQLite 3.37.0 or newer (with legacy mode down to 3.27.0). -### New and old driver -At the moment, the project includes two MySQL-on-SQLite driver implementations: -1. A new, AST-based MySQL-on-SQLite driver (`class-wp-pdo-mysql-on-sqlite.php`). -2. A legacy, token-based MySQL-to-SQLite translator (`class-wp-sqlite-translator.php`). - -This state is temporary. The new driver will fully replace the legacy one. New features -must always be implemented in the new driver. The legacy driver can receive small fixes. -The new driver is under a `WP_SQLITE_AST_DRIVER` feature flag, but it is widely used. - ## Commands The codebase is written in PHP and Composer is used to manage the project. The following commands are useful for development and testing: @@ -54,10 +45,7 @@ composer run test # Run unit tests composer run test tests/SomeTest.php # Run specific unit test file composer run test -- --filter testName # Run specific unit test class/method -# SQLite Database Integration plugin tests -composer run test # Run unit tests -composer run test tests/SomeTest.php # Run specific unit test file -composer run test -- --filter testName # Run specific unit test class/method +# SQLite Database Integration plugin E2E tests composer run test-e2e # Run E2E tests (Playwright via WP env) # WordPress tests diff --git a/README.md b/README.md index d6374c6e..301575d5 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,7 @@ composer run test # Run unit tests composer run test tests/SomeTest.php # Run specific unit test file composer run test -- --filter testName # Run specific unit test class/method -# SQLite Database Integration plugin tests -composer run test # Run unit tests -composer run test tests/SomeTest.php # Run specific unit test file -composer run test -- --filter testName # Run specific unit test class/method +# SQLite Database Integration plugin E2E tests composer run test-e2e # Run E2E tests (Playwright via WP env) # WordPress tests diff --git a/composer.json b/composer.json index 6b301995..689b44ed 100644 --- a/composer.json +++ b/composer.json @@ -18,9 +18,7 @@ "squizlabs/php_codesniffer": "^3.7", "wp-coding-standards/wpcs": "^3.1", "phpcompatibility/phpcompatibility-wp": "*", - "php-parallel-lint/php-parallel-lint": "^1.3", - "yoast/phpunit-polyfills": "2.0.0", - "phpunit/phpunit": "8.5.52" + "php-parallel-lint/php-parallel-lint": "^1.3" }, "config": { "allow-plugins": { @@ -42,9 +40,6 @@ "prepare-release": [ "./bin/prepare-release.sh" ], - "test": [ - "phpunit" - ], "test-e2e": [ "@wp-test-ensure-env @no_additional_args", "rm -f tests/e2e/package.json tests/e2e/node_modules @no_additional_args", diff --git a/packages/mysql-proxy/bin/wp-mysql-proxy.php b/packages/mysql-proxy/bin/wp-mysql-proxy.php index d23984a6..dfb652d7 100644 --- a/packages/mysql-proxy/bin/wp-mysql-proxy.php +++ b/packages/mysql-proxy/bin/wp-mysql-proxy.php @@ -6,8 +6,6 @@ require_once __DIR__ . '/../vendor/autoload.php'; -define( 'WP_SQLITE_AST_DRIVER', true ); - // Process CLI arguments: $shortopts = 'h:d:p:l:'; $longopts = array( 'help', 'database:', 'port:', 'log-level:' ); diff --git a/packages/plugin-sqlite-database-integration/admin-page.php b/packages/plugin-sqlite-database-integration/admin-page.php index cd8613cc..82815626 100644 --- a/packages/plugin-sqlite-database-integration/admin-page.php +++ b/packages/plugin-sqlite-database-integration/admin-page.php @@ -155,8 +155,7 @@ function sqlite_plugin_adminbar_item( $admin_bar ) { global $wpdb; if ( defined( 'SQLITE_DB_DROPIN_VERSION' ) && defined( 'DB_ENGINE' ) && 'sqlite' === DB_ENGINE ) { - $suffix = defined( 'WP_SQLITE_AST_DRIVER' ) && WP_SQLITE_AST_DRIVER ? ' (AST)' : ''; - $title = '' . __( 'Database: SQLite', 'sqlite-database-integration' ) . $suffix . ''; + $title = '' . __( 'Database: SQLite', 'sqlite-database-integration' ) . ''; } elseif ( stripos( $wpdb->db_server_info(), 'maria' ) !== false ) { $title = '' . __( 'Database: MariaDB', 'sqlite-database-integration' ) . ''; } else { diff --git a/packages/plugin-sqlite-database-integration/constants.php b/packages/plugin-sqlite-database-integration/constants.php index 998df4ee..15e6772a 100644 --- a/packages/plugin-sqlite-database-integration/constants.php +++ b/packages/plugin-sqlite-database-integration/constants.php @@ -51,8 +51,3 @@ define( 'FQDB', FQDBDIR . '.ht.sqlite' ); } } - -// Allow enabling the SQLite AST driver via environment variable. -if ( ! defined( 'WP_SQLITE_AST_DRIVER' ) && isset( $_ENV['WP_SQLITE_AST_DRIVER'] ) && 'true' === $_ENV['WP_SQLITE_AST_DRIVER'] ) { - define( 'WP_SQLITE_AST_DRIVER', true ); -} diff --git a/packages/plugin-sqlite-database-integration/readme.txt b/packages/plugin-sqlite-database-integration/readme.txt index e047881c..317e29e2 100644 --- a/packages/plugin-sqlite-database-integration/readme.txt +++ b/packages/plugin-sqlite-database-integration/readme.txt @@ -15,8 +15,6 @@ SQLite integration plugin by the WordPress Team. The SQLite plugin is a community, feature plugin. The intent is to allow testing an SQLite integration with WordPress and gather feedback, with the goal of eventually landing it in WordPress core. -This feature plugin includes code from the PHPMyAdmin project (specifically parts of the PHPMyAdmin/sql-parser library), licensed under the GPL v2 or later. More info on the PHPMyAdmin/sql-parser library can be found on [GitHub](https://github.com/phpmyadmin/sql-parser). - == Frequently Asked Questions == = What is the purpose of this plugin? = diff --git a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-db.php b/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-db.php index 465f5b5b..072030e7 100644 --- a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-db.php +++ b/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-db.php @@ -16,7 +16,7 @@ class WP_SQLite_DB extends wpdb { /** * Database Handle * - * @var WP_SQLite_Translator + * @var WP_SQLite_Driver */ protected $dbh; @@ -94,10 +94,6 @@ public function get_col_charset( $table, $column ) { * @param array $modes Optional. A list of SQL modes to set. Default empty array. */ public function set_sql_mode( $modes = array() ) { - if ( ! $this->dbh instanceof WP_SQLite_Driver ) { - return; - } - if ( empty( $modes ) ) { $result = $this->dbh->query( 'SELECT @@SESSION.sql_mode' ); if ( ! isset( $result[0] ) ) { @@ -175,28 +171,6 @@ public function _real_escape( $data ) { return $this->add_placeholder_escape( $escaped ); } - /** - * Method to dummy out wpdb::esc_like() function. - * - * WordPress 4.0.0 introduced esc_like() function that adds backslashes to %, - * underscore and backslash, which is not interpreted as escape character - * by SQLite. So we override it and dummy out this function. - * - * @param string $text The raw text to be escaped. The input typed by the user should have no - * extra or deleted slashes. - * - * @return string Text in the form of a LIKE phrase. The output is not SQL safe. Call $wpdb::prepare() - * or real_escape next. - */ - public function esc_like( $text ) { - // The new driver adds "ESCAPE '\\'" to every LIKE expression by default. - // We only need to overload this function to a no-op for the old driver. - if ( $this->dbh instanceof WP_SQLite_Driver ) { - return parent::esc_like( $text ); - } - return $text; - } - /** * Prints SQL/DB error. * @@ -326,34 +300,28 @@ public function db_connect( $allow_bail = true ) { } } - if ( defined( 'WP_SQLITE_AST_DRIVER' ) && WP_SQLITE_AST_DRIVER ) { - 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; - } + 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 ); - } - } else { - $this->dbh = new WP_SQLite_Translator( $pdo ); - $this->last_error = $this->dbh->get_error_message(); - $GLOBALS['@pdo'] = $this->dbh->get_pdo(); + $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; @@ -467,11 +435,7 @@ public function query( $query ) { 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 ) ) { - if ( $this->dbh instanceof WP_SQLite_Driver ) { - $this->rows_affected = $this->dbh->get_last_return_value(); - } else { - $this->rows_affected = $this->dbh->get_affected_rows(); - } + $this->rows_affected = $this->dbh->get_last_return_value(); // Take note of the insert_id. if ( preg_match( '/^\s*(insert|replace)\s/i', $query ) ) { @@ -516,11 +480,7 @@ public function query( $query ) { } // Add SQLite query data. - if ( $this->dbh instanceof WP_SQLite_Driver ) { - $this->queries[ $i ]['sqlite_queries'] = $this->dbh->get_last_sqlite_queries(); - } else { - $this->queries[ $i ]['sqlite_queries'] = $this->dbh->executed_sqlite_queries; - } + $this->queries[ $i ]['sqlite_queries'] = $this->dbh->get_last_sqlite_queries(); } return $return_val; } @@ -545,10 +505,6 @@ private function _do_query( $query ) { $this->last_error = $this->format_error_message( $e ); } - if ( $this->dbh instanceof WP_SQLite_Translator ) { - $this->last_error = $this->dbh->get_error_message(); - } - ++$this->num_queries; if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { @@ -573,27 +529,23 @@ protected function load_col_info() { if ( $this->col_info ) { return; } - if ( $this->dbh instanceof WP_SQLite_Driver ) { - $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'], - ); - } - } else { - $this->col_info = $this->dbh->get_columns(); + $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'], + ); } } diff --git a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-lexer.php b/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-lexer.php deleted file mode 100644 index 8619cb33..00000000 --- a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-lexer.php +++ /dev/null @@ -1,2575 +0,0 @@ - - */ - public static $operators = array( - - /* - * Some operators (*, =) may have ambiguous flags, because they depend on - * the context they are being used in. - * For example: 1. SELECT * FROM table; # SQL specific (wildcard) - * SELECT 2 * 3; # arithmetic - * 2. SELECT * FROM table WHERE foo = 'bar'; - * SET @i = 0; - */ - - // @see WP_SQLite_Token::FLAG_OPERATOR_ARITHMETIC - '%' => 1, - '*' => 1, - '+' => 1, - '-' => 1, - '/' => 1, - - // @see WP_SQLite_Token::FLAG_OPERATOR_LOGICAL - '!' => 2, - '!=' => 2, - '&&' => 2, - '<' => 2, - '<=' => 2, - '<=>' => 2, - '<>' => 2, - '=' => 2, - '>' => 2, - '>=' => 2, - '||' => 2, - - // @see WP_SQLite_Token::FLAG_OPERATOR_BITWISE - '&' => 4, - '<<' => 4, - '>>' => 4, - '^' => 4, - '|' => 4, - '~' => 4, - - // @see WP_SQLite_Token::FLAG_OPERATOR_ASSIGNMENT - ':=' => 8, - - // @see WP_SQLite_Token::FLAG_OPERATOR_SQL - '(' => 16, - ')' => 16, - '.' => 16, - ',' => 16, - ';' => 16, - ); - - /** - * List of keywords. - * - * The value associated to each keyword represents its flags. - * - * @see WP_SQLite_Token::FLAG_KEYWORD_RESERVED - * WP_SQLite_Token::FLAG_KEYWORD_COMPOSED - * WP_SQLite_Token::FLAG_KEYWORD_DATA_TYPE - * ∂WP_SQLite_Token::FLAG_KEYWORD_KEY - * WP_SQLite_Token::FLAG_KEYWORD_FUNCTION - * - * @var array - */ - public static $keywords = array( - 'AT' => 1, - 'DO' => 1, - 'IO' => 1, - 'NO' => 1, - 'XA' => 1, - 'ANY' => 1, - 'CPU' => 1, - 'END' => 1, - 'IPC' => 1, - 'NDB' => 1, - 'NEW' => 1, - 'ONE' => 1, - 'ROW' => 1, - 'XID' => 1, - 'BOOL' => 1, - 'BYTE' => 1, - 'CODE' => 1, - 'CUBE' => 1, - 'DATA' => 1, - 'DISK' => 1, - 'ENDS' => 1, - 'FAST' => 1, - 'FILE' => 1, - 'FULL' => 1, - 'HASH' => 1, - 'HELP' => 1, - 'HOST' => 1, - 'LAST' => 1, - 'LESS' => 1, - 'LIST' => 1, - 'LOGS' => 1, - 'MODE' => 1, - 'NAME' => 1, - 'NEXT' => 1, - 'NONE' => 1, - 'ONLY' => 1, - 'OPEN' => 1, - 'PAGE' => 1, - 'PORT' => 1, - 'PREV' => 1, - 'ROWS' => 1, - 'SLOW' => 1, - 'SOME' => 1, - 'STOP' => 1, - 'THAN' => 1, - 'TYPE' => 1, - 'VIEW' => 1, - 'WAIT' => 1, - 'WORK' => 1, - 'X509' => 1, - 'AFTER' => 1, - 'BEGIN' => 1, - 'BLOCK' => 1, - 'BTREE' => 1, - 'CACHE' => 1, - 'CHAIN' => 1, - 'CLOSE' => 1, - 'ERROR' => 1, - 'EVENT' => 1, - 'EVERY' => 1, - 'FIRST' => 1, - 'FIXED' => 1, - 'FLUSH' => 1, - 'FOUND' => 1, - 'HOSTS' => 1, - 'LEVEL' => 1, - 'LOCAL' => 1, - 'LOCKS' => 1, - 'MERGE' => 1, - 'MUTEX' => 1, - 'NAMES' => 1, - 'NCHAR' => 1, - 'NEVER' => 1, - 'OWNER' => 1, - 'PHASE' => 1, - 'PROXY' => 1, - 'QUERY' => 1, - 'QUICK' => 1, - 'RELAY' => 1, - 'RESET' => 1, - 'RTREE' => 1, - 'SHARE' => 1, - 'SLAVE' => 1, - 'START' => 1, - 'SUPER' => 1, - 'SWAPS' => 1, - 'TYPES' => 1, - 'UNTIL' => 1, - 'VALUE' => 1, - 'ACTION' => 1, - 'ALWAYS' => 1, - 'BACKUP' => 1, - 'BINLOG' => 1, - 'CIPHER' => 1, - 'CLIENT' => 1, - 'COMMIT' => 1, - 'ENABLE' => 1, - 'ENGINE' => 1, - 'ERRORS' => 1, - 'ESCAPE' => 1, - 'EVENTS' => 1, - 'EXPIRE' => 1, - 'EXPORT' => 1, - 'FAULTS' => 1, - 'FIELDS' => 1, - 'FILTER' => 1, - 'GLOBAL' => 1, - 'GRANTS' => 1, - 'IMPORT' => 1, - 'ISSUER' => 1, - 'LEAVES' => 1, - 'MASTER' => 1, - 'MEDIUM' => 1, - 'MEMORY' => 1, - 'MODIFY' => 1, - 'NUMBER' => 1, - 'OFFSET' => 1, - 'PARSER' => 1, - 'PLUGIN' => 1, - 'RELOAD' => 1, - 'REMOVE' => 1, - 'REPAIR' => 1, - 'RESUME' => 1, - 'ROLLUP' => 1, - 'SERVER' => 1, - 'SIGNED' => 1, - 'SIMPLE' => 1, - 'SOCKET' => 1, - 'SONAME' => 1, - 'SOUNDS' => 1, - 'SOURCE' => 1, - 'STARTS' => 1, - 'STATUS' => 1, - 'STRING' => 1, - 'TABLES' => 1, - 'ACCOUNT' => 1, - 'ANALYSE' => 1, - 'CHANGED' => 1, - 'CHANNEL' => 1, - 'COLUMNS' => 1, - 'COMMENT' => 1, - 'COMPACT' => 1, - 'CONTEXT' => 1, - 'CURRENT' => 1, - 'DEFINER' => 1, - 'DISABLE' => 1, - 'DISCARD' => 1, - 'DYNAMIC' => 1, - 'ENGINES' => 1, - 'EXECUTE' => 1, - 'FOLLOWS' => 1, - 'GENERAL' => 1, - 'HANDLER' => 1, - 'INDEXES' => 1, - 'INSTALL' => 1, - 'INVOKER' => 1, - 'LOGFILE' => 1, - 'MIGRATE' => 1, - 'NO_WAIT' => 1, - 'OPTIONS' => 1, - 'PARTIAL' => 1, - 'PERSIST' => 1, - 'PLUGINS' => 1, - 'PREPARE' => 1, - 'PROFILE' => 1, - 'REBUILD' => 1, - 'RECOVER' => 1, - 'RESTORE' => 1, - 'RETURNS' => 1, - 'ROUTINE' => 1, - 'SESSION' => 1, - 'STACKED' => 1, - 'STORAGE' => 1, - 'SUBJECT' => 1, - 'SUSPEND' => 1, - 'UNICODE' => 1, - 'UNKNOWN' => 1, - 'UPGRADE' => 1, - 'USE_FRM' => 1, - 'WITHOUT' => 1, - 'WRAPPER' => 1, - 'CASCADED' => 1, - 'CHECKSUM' => 1, - 'DATAFILE' => 1, - 'DUMPFILE' => 1, - 'EXCHANGE' => 1, - 'EXTENDED' => 1, - 'FUNCTION' => 1, - 'LANGUAGE' => 1, - 'MAX_ROWS' => 1, - 'MAX_SIZE' => 1, - 'MIN_ROWS' => 1, - 'NATIONAL' => 1, - 'NVARCHAR' => 1, - 'PRECEDES' => 1, - 'PRESERVE' => 1, - 'PROFILES' => 1, - 'REDOFILE' => 1, - 'RELAYLOG' => 1, - 'ROLLBACK' => 1, - 'SCHEDULE' => 1, - 'SECURITY' => 1, - 'SEQUENCE' => 1, - 'SHUTDOWN' => 1, - 'SNAPSHOT' => 1, - 'SWITCHES' => 1, - 'TRIGGERS' => 1, - 'UNDOFILE' => 1, - 'WARNINGS' => 1, - 'AGGREGATE' => 1, - 'ALGORITHM' => 1, - 'COMMITTED' => 1, - 'DIRECTORY' => 1, - 'DUPLICATE' => 1, - 'EXPANSION' => 1, - 'INVISIBLE' => 1, - 'IO_THREAD' => 1, - 'ISOLATION' => 1, - 'NODEGROUP' => 1, - 'PACK_KEYS' => 1, - 'READ_ONLY' => 1, - 'REDUNDANT' => 1, - 'SAVEPOINT' => 1, - 'SQL_CACHE' => 1, - 'TEMPORARY' => 1, - 'TEMPTABLE' => 1, - 'UNDEFINED' => 1, - 'UNINSTALL' => 1, - 'VARIABLES' => 1, - 'COMPLETION' => 1, - 'COMPRESSED' => 1, - 'CONCURRENT' => 1, - 'CONNECTION' => 1, - 'CONSISTENT' => 1, - 'DEALLOCATE' => 1, - 'IDENTIFIED' => 1, - 'MASTER_SSL' => 1, - 'NDBCLUSTER' => 1, - 'PARTITIONS' => 1, - 'PERSISTENT' => 1, - 'PLUGIN_DIR' => 1, - 'PRIVILEGES' => 1, - 'REORGANIZE' => 1, - 'REPEATABLE' => 1, - 'ROW_FORMAT' => 1, - 'SQL_THREAD' => 1, - 'TABLESPACE' => 1, - 'TABLE_NAME' => 1, - 'VALIDATION' => 1, - 'COLUMN_NAME' => 1, - 'COMPRESSION' => 1, - 'CURSOR_NAME' => 1, - 'DIAGNOSTICS' => 1, - 'EXTENT_SIZE' => 1, - 'MASTER_HOST' => 1, - 'MASTER_PORT' => 1, - 'MASTER_USER' => 1, - 'MYSQL_ERRNO' => 1, - 'NONBLOCKING' => 1, - 'PROCESSLIST' => 1, - 'REPLICATION' => 1, - 'SCHEMA_NAME' => 1, - 'SQL_TSI_DAY' => 1, - 'TRANSACTION' => 1, - 'UNCOMMITTED' => 1, - 'CATALOG_NAME' => 1, - 'CLASS_ORIGIN' => 1, - 'DEFAULT_AUTH' => 1, - 'DES_KEY_FILE' => 1, - 'INITIAL_SIZE' => 1, - 'MASTER_DELAY' => 1, - 'MESSAGE_TEXT' => 1, - 'PARTITIONING' => 1, - 'PERSIST_ONLY' => 1, - 'RELAY_THREAD' => 1, - 'SERIALIZABLE' => 1, - 'SQL_NO_CACHE' => 1, - 'SQL_TSI_HOUR' => 1, - 'SQL_TSI_WEEK' => 1, - 'SQL_TSI_YEAR' => 1, - 'SUBPARTITION' => 1, - 'COLUMN_FORMAT' => 1, - 'INSERT_METHOD' => 1, - 'MASTER_SSL_CA' => 1, - 'RELAY_LOG_POS' => 1, - 'SQL_TSI_MONTH' => 1, - 'SUBPARTITIONS' => 1, - 'AUTO_INCREMENT' => 1, - 'AVG_ROW_LENGTH' => 1, - 'KEY_BLOCK_SIZE' => 1, - 'MASTER_LOG_POS' => 1, - 'MASTER_SSL_CRL' => 1, - 'MASTER_SSL_KEY' => 1, - 'RELAY_LOG_FILE' => 1, - 'SQL_TSI_MINUTE' => 1, - 'SQL_TSI_SECOND' => 1, - 'TABLE_CHECKSUM' => 1, - 'USER_RESOURCES' => 1, - 'AUTOEXTEND_SIZE' => 1, - 'CONSTRAINT_NAME' => 1, - 'DELAY_KEY_WRITE' => 1, - 'FILE_BLOCK_SIZE' => 1, - 'MASTER_LOG_FILE' => 1, - 'MASTER_PASSWORD' => 1, - 'MASTER_SSL_CERT' => 1, - 'PARSE_GCOL_EXPR' => 1, - 'REPLICATE_DO_DB' => 1, - 'SQL_AFTER_GTIDS' => 1, - 'SQL_TSI_QUARTER' => 1, - 'SUBCLASS_ORIGIN' => 1, - 'MASTER_SERVER_ID' => 1, - 'REDO_BUFFER_SIZE' => 1, - 'SQL_BEFORE_GTIDS' => 1, - 'STATS_PERSISTENT' => 1, - 'UNDO_BUFFER_SIZE' => 1, - 'CONSTRAINT_SCHEMA' => 1, - 'GROUP_REPLICATION' => 1, - 'IGNORE_SERVER_IDS' => 1, - 'MASTER_SSL_CAPATH' => 1, - 'MASTER_SSL_CIPHER' => 1, - 'RETURNED_SQLSTATE' => 1, - 'SQL_BUFFER_RESULT' => 1, - 'STATS_AUTO_RECALC' => 1, - 'CONSTRAINT_CATALOG' => 1, - 'MASTER_RETRY_COUNT' => 1, - 'MASTER_SSL_CRLPATH' => 1, - 'MAX_STATEMENT_TIME' => 1, - 'REPLICATE_DO_TABLE' => 1, - 'SQL_AFTER_MTS_GAPS' => 1, - 'STATS_SAMPLE_PAGES' => 1, - 'REPLICATE_IGNORE_DB' => 1, - 'MASTER_AUTO_POSITION' => 1, - 'MASTER_CONNECT_RETRY' => 1, - 'MAX_QUERIES_PER_HOUR' => 1, - 'MAX_UPDATES_PER_HOUR' => 1, - 'MAX_USER_CONNECTIONS' => 1, - 'REPLICATE_REWRITE_DB' => 1, - 'REPLICATE_IGNORE_TABLE' => 1, - 'MASTER_HEARTBEAT_PERIOD' => 1, - 'REPLICATE_WILD_DO_TABLE' => 1, - 'MAX_CONNECTIONS_PER_HOUR' => 1, - 'REPLICATE_WILD_IGNORE_TABLE' => 1, - - 'AS' => 3, - 'BY' => 3, - 'IS' => 3, - 'ON' => 3, - 'OR' => 3, - 'TO' => 3, - 'ADD' => 3, - 'ALL' => 3, - 'AND' => 3, - 'ASC' => 3, - 'DEC' => 3, - 'DIV' => 3, - 'FOR' => 3, - 'GET' => 3, - 'NOT' => 3, - 'OUT' => 3, - 'SQL' => 3, - 'SSL' => 3, - 'USE' => 3, - 'XOR' => 3, - 'BOTH' => 3, - 'CALL' => 3, - 'CASE' => 3, - 'DESC' => 3, - 'DROP' => 3, - 'DUAL' => 3, - 'EACH' => 3, - 'ELSE' => 3, - 'EXIT' => 3, - 'FROM' => 3, - 'INT1' => 3, - 'INT2' => 3, - 'INT3' => 3, - 'INT4' => 3, - 'INT8' => 3, - 'INTO' => 3, - 'JOIN' => 3, - 'KEYS' => 3, - 'KILL' => 3, - 'LIKE' => 3, - 'LOAD' => 3, - 'LOCK' => 3, - 'LONG' => 3, - 'LOOP' => 3, - 'NULL' => 3, - 'OVER' => 3, - 'READ' => 3, - 'SHOW' => 3, - 'THEN' => 3, - 'TRUE' => 3, - 'UNDO' => 3, - 'WHEN' => 3, - 'WITH' => 3, - 'ALTER' => 3, - 'CHECK' => 3, - 'CROSS' => 3, - 'FALSE' => 3, - 'FETCH' => 3, - 'FORCE' => 3, - 'GRANT' => 3, - 'GROUP' => 3, - 'INNER' => 3, - 'INOUT' => 3, - 'LEAVE' => 3, - 'LIMIT' => 3, - 'LINES' => 3, - 'ORDER' => 3, - 'OUTER' => 3, - 'PURGE' => 3, - 'RANGE' => 3, - 'READS' => 3, - 'RLIKE' => 3, - 'TABLE' => 3, - 'UNION' => 3, - 'USAGE' => 3, - 'USING' => 3, - 'WHERE' => 3, - 'WHILE' => 3, - 'WRITE' => 3, - 'BEFORE' => 3, - 'CHANGE' => 3, - 'COLUMN' => 3, - 'CREATE' => 3, - 'CURSOR' => 3, - 'DELETE' => 3, - 'ELSEIF' => 3, - 'EXCEPT' => 3, - 'FLOAT4' => 3, - 'FLOAT8' => 3, - 'HAVING' => 3, - 'IGNORE' => 3, - 'INFILE' => 3, - 'LINEAR' => 3, - 'OPTION' => 3, - 'REGEXP' => 3, - 'RENAME' => 3, - 'RETURN' => 3, - 'REVOKE' => 3, - 'SELECT' => 3, - 'SIGNAL' => 3, - 'STORED' => 3, - 'UNLOCK' => 3, - 'UPDATE' => 3, - 'ANALYZE' => 3, - 'BETWEEN' => 3, - 'CASCADE' => 3, - 'COLLATE' => 3, - 'DECLARE' => 3, - 'DELAYED' => 3, - 'ESCAPED' => 3, - 'EXPLAIN' => 3, - 'FOREIGN' => 3, - 'ITERATE' => 3, - 'LEADING' => 3, - 'NATURAL' => 3, - 'OUTFILE' => 3, - 'PRIMARY' => 3, - 'RELEASE' => 3, - 'REQUIRE' => 3, - 'SCHEMAS' => 3, - 'TRIGGER' => 3, - 'VARYING' => 3, - 'VIRTUAL' => 3, - 'CONTINUE' => 3, - 'DAY_HOUR' => 3, - 'DESCRIBE' => 3, - 'DISTINCT' => 3, - 'ENCLOSED' => 3, - 'MAXVALUE' => 3, - 'MODIFIES' => 3, - 'OPTIMIZE' => 3, - 'RESIGNAL' => 3, - 'RESTRICT' => 3, - 'SPECIFIC' => 3, - 'SQLSTATE' => 3, - 'STARTING' => 3, - 'TRAILING' => 3, - 'UNSIGNED' => 3, - 'ZEROFILL' => 3, - 'CONDITION' => 3, - 'DATABASES' => 3, - 'GENERATED' => 3, - 'INTERSECT' => 3, - 'MIDDLEINT' => 3, - 'PARTITION' => 3, - 'PRECISION' => 3, - 'PROCEDURE' => 3, - 'RECURSIVE' => 3, - 'SENSITIVE' => 3, - 'SEPARATOR' => 3, - 'ACCESSIBLE' => 3, - 'ASENSITIVE' => 3, - 'CONSTRAINT' => 3, - 'DAY_MINUTE' => 3, - 'DAY_SECOND' => 3, - 'OPTIONALLY' => 3, - 'READ_WRITE' => 3, - 'REFERENCES' => 3, - 'SQLWARNING' => 3, - 'TERMINATED' => 3, - 'YEAR_MONTH' => 3, - 'DISTINCTROW' => 3, - 'HOUR_MINUTE' => 3, - 'HOUR_SECOND' => 3, - 'INSENSITIVE' => 3, - 'MASTER_BIND' => 3, - 'LOW_PRIORITY' => 3, - 'SQLEXCEPTION' => 3, - 'VARCHARACTER' => 3, - 'DETERMINISTIC' => 3, - 'HIGH_PRIORITY' => 3, - 'MINUTE_SECOND' => 3, - 'STRAIGHT_JOIN' => 3, - 'IO_AFTER_GTIDS' => 3, - 'SQL_BIG_RESULT' => 3, - 'DAY_MICROSECOND' => 3, - 'IO_BEFORE_GTIDS' => 3, - 'OPTIMIZER_COSTS' => 3, - 'HOUR_MICROSECOND' => 3, - 'SQL_SMALL_RESULT' => 3, - 'MINUTE_MICROSECOND' => 3, - 'NO_WRITE_TO_BINLOG' => 3, - 'SECOND_MICROSECOND' => 3, - 'SQL_CALC_FOUND_ROWS' => 3, - 'MASTER_SSL_VERIFY_SERVER_CERT' => 3, - - 'NO SQL' => 7, - 'GROUP BY' => 7, - 'NOT NULL' => 7, - 'ORDER BY' => 7, - 'SET NULL' => 7, - 'AND CHAIN' => 7, - 'FULL JOIN' => 7, - 'IF EXISTS' => 7, - 'LEFT JOIN' => 7, - 'LESS THAN' => 7, - 'LOAD DATA' => 7, - 'NO ACTION' => 7, - 'ON DELETE' => 7, - 'ON UPDATE' => 7, - 'UNION ALL' => 7, - 'CROSS JOIN' => 7, - 'ESCAPED BY' => 7, - 'FOR UPDATE' => 7, - 'INNER JOIN' => 7, - 'LINEAR KEY' => 7, - 'NO RELEASE' => 7, - 'OR REPLACE' => 7, - 'RIGHT JOIN' => 7, - 'ENCLOSED BY' => 7, - 'LINEAR HASH' => 7, - 'ON SCHEDULE' => 7, - 'STARTING BY' => 7, - 'AND NO CHAIN' => 7, - 'CONTAINS SQL' => 7, - 'FOR EACH ROW' => 7, - 'NATURAL JOIN' => 7, - 'PARTITION BY' => 7, - 'SET PASSWORD' => 7, - 'SQL SECURITY' => 7, - 'CHARACTER SET' => 7, - 'IF NOT EXISTS' => 7, - 'TERMINATED BY' => 7, - 'DATA DIRECTORY' => 7, - 'READS SQL DATA' => 7, - 'UNION DISTINCT' => 7, - 'DEFAULT CHARSET' => 7, - 'DEFAULT COLLATE' => 7, - 'FULL OUTER JOIN' => 7, - 'INDEX DIRECTORY' => 7, - 'LEFT OUTER JOIN' => 7, - 'SUBPARTITION BY' => 7, - 'DISABLE ON SLAVE' => 7, - 'GENERATED ALWAYS' => 7, - 'RIGHT OUTER JOIN' => 7, - 'MODIFIES SQL DATA' => 7, - 'NATURAL LEFT JOIN' => 7, - 'START TRANSACTION' => 7, - 'LOCK IN SHARE MODE' => 7, - 'NATURAL RIGHT JOIN' => 7, - 'SELECT TRANSACTION' => 7, - 'DEFAULT CHARACTER SET' => 7, - 'ON COMPLETION PRESERVE' => 7, - 'NATURAL LEFT OUTER JOIN' => 7, - 'NATURAL RIGHT OUTER JOIN' => 7, - 'WITH CONSISTENT SNAPSHOT' => 7, - 'ON COMPLETION NOT PRESERVE' => 7, - - 'BIT' => 9, - 'XML' => 9, - 'ENUM' => 9, - 'JSON' => 9, - 'TEXT' => 9, - 'ARRAY' => 9, - 'SERIAL' => 9, - 'BOOLEAN' => 9, - 'DATETIME' => 9, - 'GEOMETRY' => 9, - 'MULTISET' => 9, - 'MULTILINEPOINT' => 9, - 'MULTILINEPOLYGON' => 9, - - 'INT' => 11, - 'SET' => 11, - 'BLOB' => 11, - 'REAL' => 11, - 'FLOAT' => 11, - 'BIGINT' => 11, - 'DOUBLE' => 11, - 'DECIMAL' => 11, - 'INTEGER' => 11, - 'NUMERIC' => 11, - 'TINYINT' => 11, - 'VARCHAR' => 11, - 'LONGBLOB' => 11, - 'LONGTEXT' => 11, - 'SMALLINT' => 11, - 'TINYBLOB' => 11, - 'TINYTEXT' => 11, - 'CHARACTER' => 11, - 'MEDIUMINT' => 11, - 'VARBINARY' => 11, - 'MEDIUMBLOB' => 11, - 'MEDIUMTEXT' => 11, - - 'BINARY VARYING' => 15, - - 'KEY' => 19, - 'INDEX' => 19, - 'UNIQUE' => 19, - 'SPATIAL' => 19, - 'FULLTEXT' => 19, - - 'INDEX KEY' => 23, - 'UNIQUE KEY' => 23, - 'FOREIGN KEY' => 23, - 'PRIMARY KEY' => 23, - 'SPATIAL KEY' => 23, - 'FULLTEXT KEY' => 23, - 'UNIQUE INDEX' => 23, - 'SPATIAL INDEX' => 23, - 'FULLTEXT INDEX' => 23, - - 'X' => 33, - 'Y' => 33, - 'LN' => 33, - 'PI' => 33, - 'ABS' => 33, - 'AVG' => 33, - 'BIN' => 33, - 'COS' => 33, - 'COT' => 33, - 'DAY' => 33, - 'ELT' => 33, - 'EXP' => 33, - 'HEX' => 33, - 'LOG' => 33, - 'MAX' => 33, - 'MD5' => 33, - 'MID' => 33, - 'MIN' => 33, - 'NOW' => 33, - 'OCT' => 33, - 'ORD' => 33, - 'POW' => 33, - 'SHA' => 33, - 'SIN' => 33, - 'STD' => 33, - 'SUM' => 33, - 'TAN' => 33, - 'ACOS' => 33, - 'AREA' => 33, - 'ASIN' => 33, - 'ATAN' => 33, - 'CAST' => 33, - 'CEIL' => 33, - 'CONV' => 33, - 'HOUR' => 33, - 'LOG2' => 33, - 'LPAD' => 33, - 'RAND' => 33, - 'RPAD' => 33, - 'SHA1' => 33, - 'SHA2' => 33, - 'SIGN' => 33, - 'SQRT' => 33, - 'SRID' => 33, - 'ST_X' => 33, - 'ST_Y' => 33, - 'TRIM' => 33, - 'USER' => 33, - 'UUID' => 33, - 'WEEK' => 33, - 'ASCII' => 33, - 'ASWKB' => 33, - 'ASWKT' => 33, - 'ATAN2' => 33, - 'COUNT' => 33, - 'CRC32' => 33, - 'FIELD' => 33, - 'FLOOR' => 33, - 'INSTR' => 33, - 'LCASE' => 33, - 'LEAST' => 33, - 'LOG10' => 33, - 'LOWER' => 33, - 'LTRIM' => 33, - 'MONTH' => 33, - 'POWER' => 33, - 'QUOTE' => 33, - 'ROUND' => 33, - 'RTRIM' => 33, - 'SLEEP' => 33, - 'SPACE' => 33, - 'UCASE' => 33, - 'UNHEX' => 33, - 'UPPER' => 33, - 'ASTEXT' => 33, - 'BIT_OR' => 33, - 'BUFFER' => 33, - 'CONCAT' => 33, - 'DECODE' => 33, - 'ENCODE' => 33, - 'EQUALS' => 33, - 'FORMAT' => 33, - 'IFNULL' => 33, - 'ISNULL' => 33, - 'LENGTH' => 33, - 'LOCATE' => 33, - 'MINUTE' => 33, - 'NULLIF' => 33, - 'POINTN' => 33, - 'SECOND' => 33, - 'STDDEV' => 33, - 'STRCMP' => 33, - 'SUBSTR' => 33, - 'WITHIN' => 33, - 'ADDDATE' => 33, - 'ADDTIME' => 33, - 'AGAINST' => 33, - 'BIT_AND' => 33, - 'BIT_XOR' => 33, - 'CEILING' => 33, - 'CHARSET' => 33, - 'CROSSES' => 33, - 'CURDATE' => 33, - 'CURTIME' => 33, - 'DAYNAME' => 33, - 'DEGREES' => 33, - 'ENCRYPT' => 33, - 'EXTRACT' => 33, - 'GLENGTH' => 33, - 'ISEMPTY' => 33, - 'IS_IPV4' => 33, - 'IS_IPV6' => 33, - 'IS_UUID' => 33, - 'QUARTER' => 33, - 'RADIANS' => 33, - 'REVERSE' => 33, - 'SOUNDEX' => 33, - 'ST_AREA' => 33, - 'ST_SRID' => 33, - 'SUBDATE' => 33, - 'SUBTIME' => 33, - 'SYSDATE' => 33, - 'TOUCHES' => 33, - 'TO_DAYS' => 33, - 'VAR_POP' => 33, - 'VERSION' => 33, - 'WEEKDAY' => 33, - 'ASBINARY' => 33, - 'CENTROID' => 33, - 'COALESCE' => 33, - 'COMPRESS' => 33, - 'CONTAINS' => 33, - 'DATEDIFF' => 33, - 'DATE_ADD' => 33, - 'DATE_SUB' => 33, - 'DISJOINT' => 33, - 'DISTANCE' => 33, - 'ENDPOINT' => 33, - 'ENVELOPE' => 33, - 'GET_LOCK' => 33, - 'GREATEST' => 33, - 'ISCLOSED' => 33, - 'ISSIMPLE' => 33, - 'JSON_SET' => 33, - 'MAKEDATE' => 33, - 'MAKETIME' => 33, - 'MAKE_SET' => 33, - 'MBREQUAL' => 33, - 'OVERLAPS' => 33, - 'PASSWORD' => 33, - 'POSITION' => 33, - 'ST_ASWKB' => 33, - 'ST_ASWKT' => 33, - 'ST_UNION' => 33, - 'TIMEDIFF' => 33, - 'TRUNCATE' => 33, - 'VARIANCE' => 33, - 'VAR_SAMP' => 33, - 'YEARWEEK' => 33, - 'ANY_VALUE' => 33, - 'BENCHMARK' => 33, - 'BIT_COUNT' => 33, - 'COLLATION' => 33, - 'CONCAT_WS' => 33, - 'DAYOFWEEK' => 33, - 'DAYOFYEAR' => 33, - 'DIMENSION' => 33, - 'FROM_DAYS' => 33, - 'GEOMETRYN' => 33, - 'INET_ATON' => 33, - 'INET_NTOA' => 33, - 'JSON_KEYS' => 33, - 'JSON_TYPE' => 33, - 'LOAD_FILE' => 33, - 'MBRCOVERS' => 33, - 'MBREQUALS' => 33, - 'MBRWITHIN' => 33, - 'MONTHNAME' => 33, - 'NUMPOINTS' => 33, - 'ROW_COUNT' => 33, - 'ST_ASTEXT' => 33, - 'ST_BUFFER' => 33, - 'ST_EQUALS' => 33, - 'ST_LENGTH' => 33, - 'ST_POINTN' => 33, - 'ST_WITHIN' => 33, - 'SUBSTRING' => 33, - 'TO_BASE64' => 33, - 'UPDATEXML' => 33, - 'BIT_LENGTH' => 33, - 'CONVERT_TZ' => 33, - 'CONVEXHULL' => 33, - 'DAYOFMONTH' => 33, - 'EXPORT_SET' => 33, - 'FOUND_ROWS' => 33, - 'GET_FORMAT' => 33, - 'INET6_ATON' => 33, - 'INET6_NTOA' => 33, - 'INTERSECTS' => 33, - 'JSON_ARRAY' => 33, - 'JSON_DEPTH' => 33, - 'JSON_MERGE' => 33, - 'JSON_QUOTE' => 33, - 'JSON_VALID' => 33, - 'MBRTOUCHES' => 33, - 'NAME_CONST' => 33, - 'PERIOD_ADD' => 33, - 'STARTPOINT' => 33, - 'STDDEV_POP' => 33, - 'ST_CROSSES' => 33, - 'ST_GEOHASH' => 33, - 'ST_ISEMPTY' => 33, - 'ST_ISVALID' => 33, - 'ST_TOUCHES' => 33, - 'TO_SECONDS' => 33, - 'UNCOMPRESS' => 33, - 'UUID_SHORT' => 33, - 'WEEKOFYEAR' => 33, - 'AES_DECRYPT' => 33, - 'AES_ENCRYPT' => 33, - 'BIN_TO_UUID' => 33, - 'CHAR_LENGTH' => 33, - 'DATE_FORMAT' => 33, - 'DES_DECRYPT' => 33, - 'DES_ENCRYPT' => 33, - 'FIND_IN_SET' => 33, - 'FROM_BASE64' => 33, - 'GEOMFROMWKB' => 33, - 'GTID_SUBSET' => 33, - 'JSON_INSERT' => 33, - 'JSON_LENGTH' => 33, - 'JSON_OBJECT' => 33, - 'JSON_PRETTY' => 33, - 'JSON_REMOVE' => 33, - 'JSON_SEARCH' => 33, - 'LINEFROMWKB' => 33, - 'MBRCONTAINS' => 33, - 'MBRDISJOINT' => 33, - 'MBROVERLAPS' => 33, - 'MICROSECOND' => 33, - 'PERIOD_DIFF' => 33, - 'POLYFROMWKB' => 33, - 'SEC_TO_TIME' => 33, - 'STDDEV_SAMP' => 33, - 'STR_TO_DATE' => 33, - 'ST_ASBINARY' => 33, - 'ST_CENTROID' => 33, - 'ST_CONTAINS' => 33, - 'ST_DISJOINT' => 33, - 'ST_DISTANCE' => 33, - 'ST_ENDPOINT' => 33, - 'ST_ENVELOPE' => 33, - 'ST_ISCLOSED' => 33, - 'ST_ISSIMPLE' => 33, - 'ST_OVERLAPS' => 33, - 'ST_SIMPLIFY' => 33, - 'ST_VALIDATE' => 33, - 'SYSTEM_USER' => 33, - 'TIME_FORMAT' => 33, - 'TIME_TO_SEC' => 33, - 'UUID_TO_BIN' => 33, - 'COERCIBILITY' => 33, - 'EXTERIORRING' => 33, - 'EXTRACTVALUE' => 33, - 'GEOMETRYTYPE' => 33, - 'GEOMFROMTEXT' => 33, - 'GROUP_CONCAT' => 33, - 'IS_FREE_LOCK' => 33, - 'IS_USED_LOCK' => 33, - 'JSON_EXTRACT' => 33, - 'JSON_REPLACE' => 33, - 'JSON_UNQUOTE' => 33, - 'LINEFROMTEXT' => 33, - 'MBRCOVEREDBY' => 33, - 'MLINEFROMWKB' => 33, - 'MPOLYFROMWKB' => 33, - 'OCTET_LENGTH' => 33, - 'OLD_PASSWORD' => 33, - 'POINTFROMWKB' => 33, - 'POLYFROMTEXT' => 33, - 'RANDOM_BYTES' => 33, - 'RELEASE_LOCK' => 33, - 'SESSION_USER' => 33, - 'ST_ASGEOJSON' => 33, - 'ST_DIMENSION' => 33, - 'ST_GEOMETRYN' => 33, - 'ST_NUMPOINTS' => 33, - 'TIMESTAMPADD' => 33, - 'CONNECTION_ID' => 33, - 'FROM_UNIXTIME' => 33, - 'GTID_SUBTRACT' => 33, - 'INTERIORRINGN' => 33, - 'JSON_CONTAINS' => 33, - 'MBRINTERSECTS' => 33, - 'MLINEFROMTEXT' => 33, - 'MPOINTFROMWKB' => 33, - 'MPOLYFROMTEXT' => 33, - 'NUMGEOMETRIES' => 33, - 'POINTFROMTEXT' => 33, - 'ST_CONVEXHULL' => 33, - 'ST_DIFFERENCE' => 33, - 'ST_INTERSECTS' => 33, - 'ST_STARTPOINT' => 33, - 'TIMESTAMPDIFF' => 33, - 'WEIGHT_STRING' => 33, - 'IS_IPV4_COMPAT' => 33, - 'IS_IPV4_MAPPED' => 33, - 'LAST_INSERT_ID' => 33, - 'MPOINTFROMTEXT' => 33, - 'POLYGONFROMWKB' => 33, - 'ST_GEOMFROMWKB' => 33, - 'ST_LINEFROMWKB' => 33, - 'ST_POLYFROMWKB' => 33, - 'UNIX_TIMESTAMP' => 33, - 'GEOMCOLLFROMWKB' => 33, - 'MASTER_POS_WAIT' => 33, - 'POLYGONFROMTEXT' => 33, - 'ST_EXTERIORRING' => 33, - 'ST_GEOMETRYTYPE' => 33, - 'ST_GEOMFROMTEXT' => 33, - 'ST_INTERSECTION' => 33, - 'ST_LINEFROMTEXT' => 33, - 'ST_MAKEENVELOPE' => 33, - 'ST_MLINEFROMWKB' => 33, - 'ST_MPOLYFROMWKB' => 33, - 'ST_POINTFROMWKB' => 33, - 'ST_POLYFROMTEXT' => 33, - 'SUBSTRING_INDEX' => 33, - 'CHARACTER_LENGTH' => 33, - 'GEOMCOLLFROMTEXT' => 33, - 'GEOMETRYFROMTEXT' => 33, - 'JSON_MERGE_PATCH' => 33, - 'NUMINTERIORRINGS' => 33, - 'ST_INTERIORRINGN' => 33, - 'ST_MLINEFROMTEXT' => 33, - 'ST_MPOINTFROMWKB' => 33, - 'ST_MPOLYFROMTEXT' => 33, - 'ST_NUMGEOMETRIES' => 33, - 'ST_POINTFROMTEXT' => 33, - 'ST_SYMDIFFERENCE' => 33, - 'JSON_ARRAY_APPEND' => 33, - 'JSON_ARRAY_INSERT' => 33, - 'JSON_STORAGE_FREE' => 33, - 'JSON_STORAGE_SIZE' => 33, - 'LINESTRINGFROMWKB' => 33, - 'MULTIPOINTFROMWKB' => 33, - 'RELEASE_ALL_LOCKS' => 33, - 'ST_LATFROMGEOHASH' => 33, - 'ST_MPOINTFROMTEXT' => 33, - 'ST_POLYGONFROMWKB' => 33, - 'JSON_CONTAINS_PATH' => 33, - 'MULTIPOINTFROMTEXT' => 33, - 'ST_BUFFER_STRATEGY' => 33, - 'ST_DISTANCE_SPHERE' => 33, - 'ST_GEOMCOLLFROMTXT' => 33, - 'ST_GEOMCOLLFROMWKB' => 33, - 'ST_GEOMFROMGEOJSON' => 33, - 'ST_LONGFROMGEOHASH' => 33, - 'ST_POLYGONFROMTEXT' => 33, - 'JSON_MERGE_PRESERVE' => 33, - 'MULTIPOLYGONFROMWKB' => 33, - 'ST_GEOMCOLLFROMTEXT' => 33, - 'ST_GEOMETRYFROMTEXT' => 33, - 'ST_NUMINTERIORRINGS' => 33, - 'ST_POINTFROMGEOHASH' => 33, - 'UNCOMPRESSED_LENGTH' => 33, - 'MULTIPOLYGONFROMTEXT' => 33, - 'ST_LINESTRINGFROMWKB' => 33, - 'ST_MULTIPOINTFROMWKB' => 33, - 'ST_MULTIPOINTFROMTEXT' => 33, - 'MULTILINESTRINGFROMWKB' => 33, - 'ST_MULTIPOLYGONFROMWKB' => 33, - 'MULTILINESTRINGFROMTEXT' => 33, - 'ST_MULTIPOLYGONFROMTEXT' => 33, - 'GEOMETRYCOLLECTIONFROMWKB' => 33, - 'ST_MULTILINESTRINGFROMWKB' => 33, - 'GEOMETRYCOLLECTIONFROMTEXT' => 33, - 'ST_MULTILINESTRINGFROMTEXT' => 33, - 'VALIDATE_PASSWORD_STRENGTH' => 33, - 'WAIT_FOR_EXECUTED_GTID_SET' => 33, - 'ST_GEOMETRYCOLLECTIONFROMWKB' => 33, - 'ST_GEOMETRYCOLLECTIONFROMTEXT' => 33, - 'WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS' => 33, - - 'IF' => 35, - 'IN' => 35, - 'MOD' => 35, - 'LEFT' => 35, - 'MATCH' => 35, - 'RIGHT' => 35, - 'EXISTS' => 35, - 'INSERT' => 35, - 'REPEAT' => 35, - 'SCHEMA' => 35, - 'VALUES' => 35, - 'CONVERT' => 35, - 'DEFAULT' => 35, - 'REPLACE' => 35, - 'DATABASE' => 35, - 'UTC_DATE' => 35, - 'UTC_TIME' => 35, - 'LOCALTIME' => 35, - 'CURRENT_DATE' => 35, - 'CURRENT_TIME' => 35, - 'CURRENT_USER' => 35, - 'UTC_TIMESTAMP' => 35, - 'LOCALTIMESTAMP' => 35, - 'CURRENT_TIMESTAMP' => 35, - - 'NOT IN' => 39, - - 'DATE' => 41, - 'TIME' => 41, - 'YEAR' => 41, - 'POINT' => 41, - 'POLYGON' => 41, - 'TIMESTAMP' => 41, - 'LINESTRING' => 41, - 'MULTIPOINT' => 41, - 'MULTIPOLYGON' => 41, - 'MULTILINESTRING' => 41, - 'GEOMETRYCOLLECTION' => 41, - - 'CHAR' => 43, - 'BINARY' => 43, - 'INTERVAL' => 43, - ); - - /** - * All data type options. - * - * @var array> - */ - public static $data_type_options = array( - 'BINARY' => 1, - 'CHARACTER SET' => array( - 2, - 'var', - ), - 'CHARSET' => array( - 2, - 'var', - ), - 'COLLATE' => array( - 3, - 'var', - ), - 'UNSIGNED' => 4, - 'ZEROFILL' => 5, - ); - - /** - * All field options. - * - * @var array>> - */ - public static $field_options = array( - - /* - * Tells the `OptionsArray` to not sort the options. - * See the note below. - */ - '_UNSORTED' => true, - - 'NOT NULL' => 1, - 'NULL' => 1, - 'DEFAULT' => array( - 2, - 'expr', - array( 'breakOnAlias' => true ), - ), - - // Following are not according to grammar, but MySQL happily accepts these at any location. - 'CHARSET' => array( - 2, - 'var', - ), - 'COLLATE' => array( - 3, - 'var', - ), - 'AUTO_INCREMENT' => 3, - 'PRIMARY' => 4, - 'PRIMARY KEY' => 4, - 'UNIQUE' => 4, - 'UNIQUE KEY' => 4, - 'COMMENT' => array( - 5, - 'var', - ), - 'COLUMN_FORMAT' => array( - 6, - 'var', - ), - 'ON UPDATE' => array( - 7, - 'expr', - ), - - // Generated columns options. - 'GENERATED ALWAYS' => 8, - 'AS' => array( - 9, - 'expr', - array( 'parenthesesDelimited' => true ), - ), - 'VIRTUAL' => 10, - 'PERSISTENT' => 11, - 'STORED' => 11, - 'CHECK' => array( - 12, - 'expr', - array( 'parenthesesDelimited' => true ), - ), - 'INVISIBLE' => 13, - 'ENFORCED' => 14, - 'NOT' => 15, - 'COMPRESSED' => 16, - - /* - * Common entries. - * - * NOTE: Some of the common options are not in the same order which - * causes troubles when checking if the options are in the right order. - * I should find a way to define multiple sets of options and make the - * parser select the right set. - * - * 'UNIQUE' => 4, - * 'UNIQUE KEY' => 4, - * 'COMMENT' => [5, 'var'], - * 'NOT NULL' => 1, - * 'NULL' => 1, - * 'PRIMARY' => 4, - * 'PRIMARY KEY' => 4, - */ - ); - - /** - * Quotes mode. - * - * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_ansi_quotes - * @link https://mariadb.com/kb/en/sql-mode/#ansi_quotes - */ - const SQL_MODE_ANSI_QUOTES = 2; - - /** - * The array of tokens. - * - * @var stdClass[] - */ - public $tokens = array(); - - /** - * The count of tokens. - * - * @var int - */ - public $tokens_count = 0; - - /** - * The index of the next token to be returned. - * - * @var int - */ - public $tokens_index = 0; - - /** - * The object constructor. - * - * @param string $str The query to be lexed. - * @param string $delimiter The delimiter to be used. - */ - public function __construct( $str, $delimiter = null ) { - $this->str = $str; - // `strlen` is used instead of `mb_strlen` because the lexer needs to parse each byte of the input. - $this->string_length = strlen( $str ); - - // Setting the delimiter. - $this->set_delimiter( ! empty( $delimiter ) ? $delimiter : static::$default_delimiter ); - - $this->lex(); - } - - /** - * Sets the delimiter. - * - * @param string $delimiter The new delimiter. - * - * @return void - */ - public function set_delimiter( $delimiter ) { - $this->delimiter = $delimiter; - $this->delimiter_length = strlen( $delimiter ); - } - - /** - * Parses the string and extracts lexemes. - * - * @return void - */ - public function lex() { - /* - * TODO: Sometimes, static::parse* functions make unnecessary calls to - * is* functions. For a better performance, some rules can be deduced - * from context. - * For example, in `parse_bool` there is no need to compare the token - * every time with `true` and `false`. The first step would be to - * compare with 'true' only and just after that add another letter from - * context and compare again with `false`. - * Another example is `parse_comment`. - */ - - /** - * Last processed token. - * - * @var WP_SQLite_Token - */ - $last_token = null; - - for ( $this->last = 0, $last_idx = 0; $this->last < $this->string_length; $last_idx = ++$this->last ) { - /** - * The new token. - * - * @var WP_SQLite_Token - */ - $token = null; - - foreach ( self::PARSER_METHODS as $method ) { - $token = $this->$method(); - - if ( $token ) { - break; - } - } - - if ( null === $token ) { - $token = new WP_SQLite_Token( $this->str[ $this->last ] ); - $this->error( 'Unexpected character.', $this->str[ $this->last ], $this->last ); - } elseif ( - null !== $last_token - && WP_SQLite_Token::TYPE_SYMBOL === $token->type - && $token->flags & WP_SQLite_Token::FLAG_SYMBOL_VARIABLE - && ( - WP_SQLite_Token::TYPE_STRING === $last_token->type - || ( - WP_SQLite_Token::TYPE_SYMBOL === $last_token->type - && $last_token->flags & WP_SQLite_Token::FLAG_SYMBOL_BACKTICK - ) - ) - ) { - // Handles ```... FROM 'user'@'%' ...```. - $last_token->token .= $token->token; - $last_token->type = WP_SQLite_Token::TYPE_SYMBOL; - $last_token->flags = WP_SQLite_Token::FLAG_SYMBOL_USER; - $last_token->value .= '@' . $token->value; - continue; - } elseif ( - null !== $last_token - && WP_SQLite_Token::TYPE_KEYWORD === $token->type - && WP_SQLite_Token::TYPE_OPERATOR === $last_token->type - && '.' === $last_token->value - ) { - // Handles ```... tbl.FROM ...```. In this case, FROM is not a reserved word. - $token->type = WP_SQLite_Token::TYPE_NONE; - $token->flags = 0; - $token->value = $token->token; - } - - $token->position = $last_idx; - - $this->tokens[ $this->tokens_count++ ] = $token; - - // Handling delimiters. - if ( WP_SQLite_Token::TYPE_NONE === $token->type && 'DELIMITER' === $token->value ) { - if ( $this->last + 1 >= $this->string_length ) { - $this->error( 'Expected whitespace(s) before delimiter.', '', $this->last + 1 ); - continue; - } - - /* - * Skipping last R (from `delimiteR`) and whitespaces between - * the keyword `DELIMITER` and the actual delimiter. - */ - $pos = ++$this->last; - $token = $this->parse_whitespace(); - - if ( null !== $token ) { - $token->position = $pos; - $this->tokens[ $this->tokens_count++ ] = $token; - } - - // Preparing the token that holds the new delimiter. - if ( $this->last + 1 >= $this->string_length ) { - $this->error( 'Expected delimiter.', '', $this->last + 1 ); - continue; - } - - $pos = $this->last + 1; - - // Parsing the delimiter. - $this->delimiter = null; - $delimiter_length = 0; - while ( - ++$this->last < $this->string_length - && ! static::is_whitespace( $this->str[ $this->last ] ) - && $delimiter_length < 15 - ) { - $this->delimiter .= $this->str[ $this->last ]; - ++$delimiter_length; - } - - if ( empty( $this->delimiter ) ) { - $this->error( 'Expected delimiter.', '', $this->last ); - $this->delimiter = ';'; - } - - --$this->last; - - // Saving the delimiter and its token. - $this->delimiter_length = strlen( $this->delimiter ); - $token = new WP_SQLite_Token( $this->delimiter, WP_SQLite_Token::TYPE_DELIMITER ); - $token->position = $pos; - $this->tokens[ $this->tokens_count++ ] = $token; - } - - $last_token = $token; - } - - // Adding a final delimiter to mark the ending. - $this->tokens[ $this->tokens_count++ ] = new WP_SQLite_Token( null, WP_SQLite_Token::TYPE_DELIMITER ); - - $this->solve_ambiguity_on_star_operator(); - $this->solve_ambiguity_on_function_keywords(); - } - - /** - * Resolves the ambiguity when dealing with the "*" operator. - * - * In SQL statements, the "*" operator can be an arithmetic operator (like in 2*3) or an SQL wildcard (like in - * SELECT a.* FROM ...). To solve this ambiguity, the solution is to find the next token, excluding whitespaces and - * comments, right after the "*" position. The "*" is for sure an SQL wildcard if the next token found is any of: - * - "FROM" (the FROM keyword like in "SELECT * FROM..."); - * - "USING" (the USING keyword like in "DELETE table_name.* USING..."); - * - "," (a comma separator like in "SELECT *, field FROM..."); - * - ")" (a closing parenthesis like in "COUNT(*)"). - * This methods will change the flag of the "*" tokens when any of those condition above is true. Otherwise, the - * default flag (arithmetic) will be kept. - * - * @return void - */ - private function solve_ambiguity_on_star_operator() { - $i_bak = $this->tokens_index; - while ( true ) { - $star_token = $this->tokens_get_next_of_type_and_value( WP_SQLite_Token::TYPE_OPERATOR, '*' ); - if ( null === $star_token ) { - break; - } - // tokens_get_next() already gets rid of whitespaces and comments. - $next = $this->tokens_get_next(); - - if ( null === $next ) { - continue; - } - - if ( - ( WP_SQLite_Token::TYPE_KEYWORD !== $next->type || ! in_array( $next->value, array( 'FROM', 'USING' ), true ) ) - && ( WP_SQLite_Token::TYPE_OPERATOR !== $next->type || ! in_array( $next->value, array( ',', ')' ), true ) ) - ) { - continue; - } - - $star_token->flags = WP_SQLite_Token::FLAG_OPERATOR_SQL; - } - - $this->tokens_index = $i_bak; - } - - /** - * Resolves the ambiguity when dealing with the functions keywords. - * - * In SQL statements, the function keywords might be used as table names or columns names. - * To solve this ambiguity, the solution is to find the next token, excluding whitespaces and - * comments, right after the function keyword position. The function keyword is for sure used - * as column name or table name if the next token found is any of: - * - * - "FROM" (the FROM keyword like in "SELECT Country x, AverageSalary avg FROM..."); - * - "WHERE" (the WHERE keyword like in "DELETE FROM emp x WHERE x.salary = 20"); - * - "SET" (the SET keyword like in "UPDATE Country x, City y set x.Name=x.Name"); - * - "," (a comma separator like 'x,' in "UPDATE Country x, City y set x.Name=x.Name"); - * - "." (a dot separator like in "x.asset_id FROM (SELECT evt.asset_id FROM evt)". - * - "NULL" (when used as a table alias like in "avg.col FROM (SELECT ev.col FROM ev) avg"). - * - * This method will change the flag of the function keyword tokens when any of those - * condition above is true. Otherwise, the - * default flag (function keyword) will be kept. - * - * @return void - */ - private function solve_ambiguity_on_function_keywords() { - $i_bak = $this->tokens_index; - $keyword_function = WP_SQLite_Token::TYPE_KEYWORD | WP_SQLite_Token::FLAG_KEYWORD_FUNCTION; - while ( true ) { - $keyword_token = $this->tokens_get_next_of_type_and_flag( WP_SQLite_Token::TYPE_KEYWORD, $keyword_function ); - if ( null === $keyword_token ) { - break; - } - $next = $this->tokens_get_next(); - if ( - ( WP_SQLite_Token::TYPE_KEYWORD !== $next->type - || ! in_array( $next->value, self::KEYWORD_NAME_INDICATORS, true ) - ) - && ( WP_SQLite_Token::TYPE_OPERATOR !== $next->type - || ! in_array( $next->value, self::OPERATOR_NAME_INDICATORS, true ) - ) - && ( null !== $next->value ) - ) { - continue; - } - - $keyword_token->type = WP_SQLite_Token::TYPE_NONE; - $keyword_token->flags = WP_SQLite_Token::TYPE_NONE; - $keyword_token->keyword = $keyword_token->value; - } - - $this->tokens_index = $i_bak; - } - - /** - * Creates a new error log. - * - * @param string $msg The error message. - * @param string $str The character that produced the error. - * @param int $pos The position of the character. - * @param int $code The code of the error. - * - * @throws Exception The error log. - * @return void - */ - public function error( $msg, $str = '', $pos = 0, $code = 0 ) { - throw new Exception( - print_r( - array( - 'query' => $this->str, - 'message' => $msg, - 'str' => $str, - 'position' => $pos, - 'code' => $code, - ), - true - ) - ); - } - - /** - * Parses a keyword. - * - * @return WP_SQLite_Token|null - */ - public function parse_keyword() { - $token = ''; - - /** - * Value to be returned. - * - * @var WP_SQLite_Token - */ - $ret = null; - - // The value of `$this->last` where `$token` ends in `$this->str`. - $i_end = $this->last; - - // Whether last parsed character is a whitespace. - $last_space = false; - - for ( $j = 1; $j < static::KEYWORD_MAX_LENGTH && $this->last < $this->string_length; ++$j, ++$this->last ) { - $last_space = false; - // Composed keywords shouldn't have more than one whitespace between keywords. - if ( static::is_whitespace( $this->str[ $this->last ] ) ) { - if ( $last_space ) { - --$j; // The size of the keyword didn't increase. - continue; - } - - $last_space = true; - } - - $token .= $this->str[ $this->last ]; - $flags = static::is_keyword( $token ); - - if ( ( $this->last + 1 !== $this->string_length && ! static::is_separator( $this->str[ $this->last + 1 ] ) ) || ! $flags ) { - continue; - } - - $ret = new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_KEYWORD, $flags ); - $i_end = $this->last; - - /* - * We don't break so we find longest keyword. - * For example, `OR` and `ORDER` have a common prefix `OR`. - * If we stopped at `OR`, the parsing would be invalid. - */ - } - - $this->last = $i_end; - - return $ret; - } - - /** - * Parses a label. - * - * @return WP_SQLite_Token|null - */ - public function parse_label() { - $token = ''; - - /** - * Value to be returned. - * - * @var WP_SQLite_Token - */ - $ret = null; - - // The value of `$this->last` where `$token` ends in `$this->str`. - $i_end = $this->last; - for ( $j = 1; $j < static::LABEL_MAX_LENGTH && $this->last < $this->string_length; ++$j, ++$this->last ) { - if ( ':' === $this->str[ $this->last ] && $j > 1 ) { - // End of label. - $token .= $this->str[ $this->last ]; - $ret = new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_LABEL ); - $i_end = $this->last; - break; - } - - if ( static::is_whitespace( $this->str[ $this->last ] ) && $j > 1 ) { - /* - * Whitespace between label and `:`. - * The size of the keyword didn't increase. - */ - --$j; - } elseif ( static::is_separator( $this->str[ $this->last ] ) ) { - // Any other separator. - break; - } - - $token .= $this->str[ $this->last ]; - } - - $this->last = $i_end; - - return $ret; - } - - /** - * Parses an operator. - * - * @return WP_SQLite_Token|null - */ - public function parse_operator() { - $token = ''; - - /** - * Value to be returned. - * - * @var WP_SQLite_Token - */ - $ret = null; - - // The value of `$this->last` where `$token` ends in `$this->str`. - $i_end = $this->last; - - for ( $j = 1; $j < static::OPERATOR_MAX_LENGTH && $this->last < $this->string_length; ++$j, ++$this->last ) { - $token .= $this->str[ $this->last ]; - $flags = static::is_operator( $token ); - - if ( ! $flags ) { - continue; - } - - $ret = new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_OPERATOR, $flags ); - $i_end = $this->last; - } - - $this->last = $i_end; - - return $ret; - } - - /** - * Parses a whitespace. - * - * @return WP_SQLite_Token|null - */ - public function parse_whitespace() { - $token = $this->str[ $this->last ]; - - if ( ! static::is_whitespace( $token ) ) { - return null; - } - - while ( ++$this->last < $this->string_length && static::is_whitespace( $this->str[ $this->last ] ) ) { - $token .= $this->str[ $this->last ]; - } - - --$this->last; - - return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_WHITESPACE ); - } - - /** - * Parses a comment. - * - * @return WP_SQLite_Token|null - */ - public function parse_comment() { - $i_bak = $this->last; - $token = $this->str[ $this->last ]; - - // Bash style comments (#comment\n). - if ( static::is_comment( $token ) ) { - while ( ++$this->last < $this->string_length && "\n" !== $this->str[ $this->last ] ) { - $token .= $this->str[ $this->last ]; - } - - // Include trailing \n as whitespace token. - if ( $this->last < $this->string_length ) { - --$this->last; - } - - return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_COMMENT, WP_SQLite_Token::FLAG_COMMENT_BASH ); - } - - // C style comments (/*comment*\/). - if ( ++$this->last < $this->string_length ) { - $token .= $this->str[ $this->last ]; - if ( static::is_comment( $token ) ) { - // There might be a conflict with "*" operator here, when string is "*/*". - // This can occurs in the following statements: - // - "SELECT */* comment */ FROM ..." - // - "SELECT 2*/* comment */3 AS `six`;". - $next = $this->last + 1; - if ( ( $next < $this->string_length ) && '*' === $this->str[ $next ] ) { - // Conflict in "*/*": first "*" was not for ending a comment. - // Stop here and let other parsing method define the true behavior of that first star. - $this->last = $i_bak; - - return null; - } - - $flags = WP_SQLite_Token::FLAG_COMMENT_C; - - // This comment already ended. It may be a part of a previous MySQL specific command. - if ( '*/' === $token ) { - return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_COMMENT, $flags ); - } - - // Checking if this is a MySQL-specific command. - if ( $this->last + 1 < $this->string_length && '!' === $this->str[ $this->last + 1 ] ) { - $flags |= WP_SQLite_Token::FLAG_COMMENT_MYSQL_CMD; - $token .= $this->str[ ++$this->last ]; - - while ( - ++$this->last < $this->string_length - && $this->str[ $this->last ] >= '0' - && $this->str[ $this->last ] <= '9' - ) { - $token .= $this->str[ $this->last ]; - } - - --$this->last; - - // We split this comment and parse only its beginning here. - return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_COMMENT, $flags ); - } - - // Parsing the comment. - while ( - ++$this->last < $this->string_length - && ( '*' !== $this->str[ $this->last - 1 ] || '/' !== $this->str[ $this->last ] ) - ) { - $token .= $this->str[ $this->last ]; - } - - // Adding the ending. - if ( $this->last < $this->string_length ) { - $token .= $this->str[ $this->last ]; - } - - return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_COMMENT, $flags ); - } - } - - // SQL style comments (-- comment\n). - if ( ++$this->last < $this->string_length ) { - $token .= $this->str[ $this->last ]; - $end = false; - } else { - --$this->last; - $end = true; - } - - if ( static::is_comment( $token, $end ) ) { - // Checking if this comment did not end already (```--\n```). - if ( "\n" !== $this->str[ $this->last ] ) { - while ( ++$this->last < $this->string_length && "\n" !== $this->str[ $this->last ] ) { - $token .= $this->str[ $this->last ]; - } - } - - // Include trailing \n as whitespace token. - if ( $this->last < $this->string_length ) { - --$this->last; - } - - return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_COMMENT, WP_SQLite_Token::FLAG_COMMENT_SQL ); - } - - $this->last = $i_bak; - - return null; - } - - /** - * Parses a boolean. - * - * @return WP_SQLite_Token|null - */ - public function parse_bool() { - if ( $this->last + 3 >= $this->string_length ) { - // At least `min(strlen('TRUE'), strlen('FALSE'))` characters are required. - return null; - } - - $i_bak = $this->last; - $token = $this->str[ $this->last ] . $this->str[ ++$this->last ] - . $this->str[ ++$this->last ] . $this->str[ ++$this->last ]; // _TRUE_ or _FALS_e. - - if ( static::is_bool( $token ) ) { - return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_BOOL ); - } - - if ( ++$this->last < $this->string_length ) { - $token .= $this->str[ $this->last ]; // fals_E_. - if ( static::is_bool( $token ) ) { - return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_BOOL, 1 ); - } - } - - $this->last = $i_bak; - - return null; - } - - /** - * Parses a number. - * - * @return WP_SQLite_Token|null - */ - public function parse_number() { - /* - * A rudimentary state machine is being used to parse numbers due to - * the various forms of their notation. - * - * Below are the states of the machines and the conditions to change - * the state. - * - * 1 --------------------[ + or - ]-------------------> 1 - * 1 -------------------[ 0x or 0X ]------------------> 2 - * 1 --------------------[ 0 to 9 ]-------------------> 3 - * 1 -----------------------[ . ]---------------------> 4 - * 1 -----------------------[ b ]---------------------> 7 - * - * 2 --------------------[ 0 to F ]-------------------> 2 - * - * 3 --------------------[ 0 to 9 ]-------------------> 3 - * 3 -----------------------[ . ]---------------------> 4 - * 3 --------------------[ e or E ]-------------------> 5 - * - * 4 --------------------[ 0 to 9 ]-------------------> 4 - * 4 --------------------[ e or E ]-------------------> 5 - * - * 5 ---------------[ + or - or 0 to 9 ]--------------> 6 - * - * 7 -----------------------[ ' ]---------------------> 8 - * - * 8 --------------------[ 0 or 1 ]-------------------> 8 - * 8 -----------------------[ ' ]---------------------> 9 - * - * State 1 may be reached by negative numbers. - * State 2 is reached only by hex numbers. - * State 4 is reached only by float numbers. - * State 5 is reached only by numbers in approximate form. - * State 7 is reached only by numbers in bit representation. - * - * Valid final states are: 2, 3, 4 and 6. Any parsing that finished in a - * state other than these is invalid. - * Also, negative states are invalid states. - */ - $i_bak = $this->last; - $token = ''; - $flags = 0; - $state = 1; - for ( ; $this->last < $this->string_length; ++$this->last ) { - if ( 1 === $state ) { - if ( '-' === $this->str[ $this->last ] ) { - $flags |= WP_SQLite_Token::FLAG_NUMBER_NEGATIVE; - } elseif ( - $this->last + 1 < $this->string_length - && '0' === $this->str[ $this->last ] - && 'x' === $this->str[ $this->last + 1 ] - ) { - $token .= $this->str[ $this->last++ ]; - $state = 2; - } elseif ( $this->str[ $this->last ] >= '0' && $this->str[ $this->last ] <= '9' ) { - $state = 3; - } elseif ( '.' === $this->str[ $this->last ] ) { - $state = 4; - } elseif ( 'b' === $this->str[ $this->last ] ) { - $state = 7; - } elseif ( '+' !== $this->str[ $this->last ] ) { - // `+` is a valid character in a number. - break; - } - } elseif ( 2 === $state ) { - $flags |= WP_SQLite_Token::FLAG_NUMBER_HEX; - if ( - ! ( - ( $this->str[ $this->last ] >= '0' && $this->str[ $this->last ] <= '9' ) - || ( $this->str[ $this->last ] >= 'A' && $this->str[ $this->last ] <= 'F' ) - || ( $this->str[ $this->last ] >= 'a' && $this->str[ $this->last ] <= 'f' ) - ) - ) { - break; - } - } elseif ( 3 === $state ) { - if ( '.' === $this->str[ $this->last ] ) { - $state = 4; - } elseif ( 'e' === $this->str[ $this->last ] || 'E' === $this->str[ $this->last ] ) { - $state = 5; - } elseif ( - ( $this->str[ $this->last ] >= 'a' && $this->str[ $this->last ] <= 'z' ) - || ( $this->str[ $this->last ] >= 'A' && $this->str[ $this->last ] <= 'Z' ) - ) { - // A number can't be directly followed by a letter. - $state = -$state; - } elseif ( $this->str[ $this->last ] < '0' || $this->str[ $this->last ] > '9' ) { - // Just digits and `.`, `e` and `E` are valid characters. - break; - } - } elseif ( 4 === $state ) { - $flags |= WP_SQLite_Token::FLAG_NUMBER_FLOAT; - if ( 'e' === $this->str[ $this->last ] || 'E' === $this->str[ $this->last ] ) { - $state = 5; - } elseif ( - ( $this->str[ $this->last ] >= 'a' && $this->str[ $this->last ] <= 'z' ) - || ( $this->str[ $this->last ] >= 'A' && $this->str[ $this->last ] <= 'Z' ) - ) { - // A number can't be directly followed by a letter. - $state = -$state; - } elseif ( $this->str[ $this->last ] < '0' || $this->str[ $this->last ] > '9' ) { - // Just digits, `e` and `E` are valid characters. - break; - } - } elseif ( 5 === $state ) { - $flags |= WP_SQLite_Token::FLAG_NUMBER_APPROXIMATE; - if ( - '+' === $this->str[ $this->last ] || '-' === $this->str[ $this->last ] - || ( $this->str[ $this->last ] >= '0' && $this->str[ $this->last ] <= '9' ) - ) { - $state = 6; - } elseif ( - ( $this->str[ $this->last ] >= 'a' && $this->str[ $this->last ] <= 'z' ) - || ( $this->str[ $this->last ] >= 'A' && $this->str[ $this->last ] <= 'Z' ) - ) { - // A number can't be directly followed by a letter. - $state = -$state; - } else { - break; - } - } elseif ( 6 === $state ) { - if ( $this->str[ $this->last ] < '0' || $this->str[ $this->last ] > '9' ) { - // Just digits are valid characters. - break; - } - } elseif ( 7 === $state ) { - $flags |= WP_SQLite_Token::FLAG_NUMBER_BINARY; - if ( '\'' !== $this->str[ $this->last ] ) { - break; - } - - $state = 8; - } elseif ( 8 === $state ) { - if ( '\'' === $this->str[ $this->last ] ) { - $state = 9; - } elseif ( '0' !== $this->str[ $this->last ] && '1' !== $this->str[ $this->last ] ) { - break; - } - } elseif ( 9 === $state ) { - break; - } - - $token .= $this->str[ $this->last ]; - } - - if ( 2 === $state || 3 === $state || ( '.' !== $token && 4 === $state ) || 6 === $state || 9 === $state ) { - --$this->last; - - return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_NUMBER, $flags ); - } - - $this->last = $i_bak; - - return null; - } - - /** - * Parses a string. - * - * @param string $quote Additional starting symbol. - * - * @return WP_SQLite_Token|null - */ - public function parse_string( $quote = '' ) { - $token = $this->str[ $this->last ]; - $flags = static::is_string( $token ); - - if ( ! $flags && $token !== $quote ) { - return null; - } - - $quote = $token; - - while ( ++$this->last < $this->string_length ) { - if ( - $this->last + 1 < $this->string_length - && ( - ( $this->str[ $this->last ] === $quote && $this->str[ $this->last + 1 ] === $quote ) - || ( '\\' === $this->str[ $this->last ] && '`' !== $quote ) - ) - ) { - $token .= $this->str[ $this->last ] . $this->str[ ++$this->last ]; - } else { - if ( $this->str[ $this->last ] === $quote ) { - break; - } - - $token .= $this->str[ $this->last ]; - } - } - - if ( $this->last >= $this->string_length || $this->str[ $this->last ] !== $quote ) { - $this->error( - sprintf( - 'Ending quote %1$s was expected.', - $quote - ), - '', - $this->last - ); - } else { - $token .= $this->str[ $this->last ]; - } - - return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_STRING, $flags ); - } - - /** - * Parses a symbol. - * - * @return WP_SQLite_Token|null - */ - public function parse_symbol() { - $token = $this->str[ $this->last ]; - $flags = static::is_symbol( $token ); - - if ( ! $flags ) { - return null; - } - - if ( $flags & WP_SQLite_Token::FLAG_SYMBOL_VARIABLE ) { - if ( $this->last + 1 < $this->string_length && '@' === $this->str[ ++$this->last ] ) { - // This is a system variable (e.g. `@@hostname`). - $token .= $this->str[ $this->last++ ]; - $flags |= WP_SQLite_Token::FLAG_SYMBOL_SYSTEM; - } - } elseif ( $flags & WP_SQLite_Token::FLAG_SYMBOL_PARAMETER ) { - if ( '?' !== $token && $this->last + 1 < $this->string_length ) { - ++$this->last; - } - } else { - $token = ''; - } - - $str = null; - - if ( $this->last < $this->string_length ) { - $str = $this->parse_string( '`' ); - - if ( null === $str ) { - $str = $this->parse_unknown(); - - if ( null === $str && ! ( $flags & WP_SQLite_Token::FLAG_SYMBOL_PARAMETER ) ) { - $this->error( 'Variable name was expected.', $this->str[ $this->last ], $this->last ); - } - } - } - - if ( null !== $str ) { - $token .= $str->token; - } - - return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_SYMBOL, $flags ); - } - - /** - * Parses unknown parts of the query. - * - * @return WP_SQLite_Token|null - */ - public function parse_unknown() { - $token = $this->str[ $this->last ]; - if ( static::is_separator( $token ) ) { - return null; - } - - while ( ++$this->last < $this->string_length && ! static::is_separator( $this->str[ $this->last ] ) ) { - $token .= $this->str[ $this->last ]; - - // Test if end of token equals the current delimiter. If so, remove it from the token. - if ( str_ends_with( $token, $this->delimiter ) ) { - $token = substr( $token, 0, -$this->delimiter_length ); - $this->last -= $this->delimiter_length - 1; - break; - } - } - - --$this->last; - - return new WP_SQLite_Token( $token ); - } - - /** - * Parses the delimiter of the query. - * - * @return WP_SQLite_Token|null - */ - public function parse_delimiter() { - $index = 0; - - while ( $index < $this->delimiter_length && $this->last + $index < $this->string_length ) { - if ( $this->delimiter[ $index ] !== $this->str[ $this->last + $index ] ) { - return null; - } - - ++$index; - } - - $this->last += $this->delimiter_length - 1; - - return new WP_SQLite_Token( $this->delimiter, WP_SQLite_Token::TYPE_DELIMITER ); - } - - /** - * Checks if the given string is a keyword. - * - * @param string $str String to be checked. - * @param bool $is_reserved Checks if the keyword is reserved. - * - * @return int|null - */ - public static function is_keyword( $str, $is_reserved = false ) { - $str = strtoupper( $str ); - - if ( isset( static::$keywords[ $str ] ) ) { - if ( $is_reserved && ! ( static::$keywords[ $str ] & WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) ) { - return null; - } - - return static::$keywords[ $str ]; - } - - return null; - } - - /** - * Checks if the given string is an operator. - * - * @param string $str String to be checked. - * - * @return int|null The appropriate flag for the operator. - */ - public static function is_operator( $str ) { - if ( ! isset( static::$operators[ $str ] ) ) { - return null; - } - - return static::$operators[ $str ]; - } - - /** - * Checks if the given character is a whitespace. - * - * @param string $str String to be checked. - * - * @return bool - */ - public static function is_whitespace( $str ) { - return ( ' ' === $str ) || ( "\r" === $str ) || ( "\n" === $str ) || ( "\t" === $str ); - } - - /** - * Checks if the given string is the beginning of a whitespace. - * - * @param string $str String to be checked. - * @param mixed $end Whether this is the end of the string. - * - * @return int|null The appropriate flag for the comment type. - */ - public static function is_comment( $str, $end = false ) { - $string_length = strlen( $str ); - if ( 0 === $string_length ) { - return null; - } - - // If comment is Bash style (#). - if ( '#' === $str[0] ) { - return WP_SQLite_Token::FLAG_COMMENT_BASH; - } - - // If comment is opening C style (/*), warning, it could be a MySQL command (/*!). - if ( ( $string_length > 1 ) && ( '/' === $str[0] ) && ( '*' === $str[1] ) ) { - return ( $string_length > 2 ) && ( '!' === $str[2] ) ? - WP_SQLite_Token::FLAG_COMMENT_MYSQL_CMD : WP_SQLite_Token::FLAG_COMMENT_C; - } - - // If comment is closing C style (*/), warning, it could conflicts with wildcard and a real opening C style. - // It would looks like the following valid SQL statement: "SELECT */* comment */ FROM...". - if ( ( $string_length > 1 ) && ( '*' === $str[0] ) && ( '/' === $str[1] ) ) { - return WP_SQLite_Token::FLAG_COMMENT_C; - } - - // If comment is SQL style (--\s?). - if ( ( $string_length > 2 ) && ( '-' === $str[0] ) && ( '-' === $str[1] ) && static::is_whitespace( $str[2] ) ) { - return WP_SQLite_Token::FLAG_COMMENT_SQL; - } - - if ( ( 2 === $string_length ) && $end && ( '-' === $str[0] ) && ( '-' === $str[1] ) ) { - return WP_SQLite_Token::FLAG_COMMENT_SQL; - } - - return null; - } - - /** - * Checks if the given string is a boolean value. - * This actually checks only for `TRUE` and `FALSE` because `1` or `0` are - * numbers and are parsed by specific methods. - * - * @param string $str String to be checked. - * - * @return bool - */ - public static function is_bool( $str ) { - $str = strtoupper( $str ); - - return ( 'TRUE' === $str ) || ( 'FALSE' === $str ); - } - - /** - * Checks if the given character can be a part of a number. - * - * @param string $str String to be checked. - * - * @return bool - */ - public static function is_number( $str ) { - return ( $str >= '0' ) && ( $str <= '9' ) || ( '.' === $str ) - || ( '-' === $str ) || ( '+' === $str ) || ( 'e' === $str ) || ( 'E' === $str ); - } - - /** - * Checks if the given character is the beginning of a symbol. A symbol - * can be either a variable or a field name. - * - * @param string $str String to be checked. - * - * @return int|null The appropriate flag for the symbol type. - */ - public static function is_symbol( $str ) { - if ( 0 === strlen( $str ) ) { - return null; - } - - if ( '@' === $str[0] ) { - return WP_SQLite_Token::FLAG_SYMBOL_VARIABLE; - } - - if ( '`' === $str[0] ) { - return WP_SQLite_Token::FLAG_SYMBOL_BACKTICK; - } - - if ( ':' === $str[0] || '?' === $str[0] ) { - return WP_SQLite_Token::FLAG_SYMBOL_PARAMETER; - } - - return null; - } - - /** - * Checks if the given character is the beginning of a string. - * - * @param string $str String to be checked. - * - * @return int|null The appropriate flag for the string type. - */ - public static function is_string( $str ) { - if ( strlen( $str ) === 0 ) { - return null; - } - - if ( '\'' === $str[0] ) { - return WP_SQLite_Token::FLAG_STRING_SINGLE_QUOTES; - } - - if ( '"' === $str[0] ) { - return WP_SQLite_Token::FLAG_STRING_DOUBLE_QUOTES; - } - - return null; - } - - /** - * Checks if the given character can be a separator for two lexeme. - * - * @param string $str String to be checked. - * - * @return bool - */ - public static function is_separator( $str ) { - /* - * NOTES: Only non alphanumeric ASCII characters may be separators. - * `~` is the last printable ASCII character. - */ - return ( $str <= '~' ) - && ( '_' !== $str ) - && ( '$' !== $str ) - && ( ( $str < '0' ) || ( $str > '9' ) ) - && ( ( $str < 'a' ) || ( $str > 'z' ) ) - && ( ( $str < 'A' ) || ( $str > 'Z' ) ); - } - - /** - * Constructor. - * - * @param stdClass[] $tokens The initial array of tokens. - */ - public function tokens( array $tokens = array() ) { - $this->tokens = $tokens; - $this->tokens_count = count( $tokens ); - } - - /** - * Gets the next token. - * - * @param int $type The type of the token. - * @param int $flag The flag of the token. - */ - public function tokens_get_next_of_type_and_flag( $type, $flag ) { - for ( ; $this->tokens_index < $this->tokens_count; ++$this->tokens_index ) { - if ( ( $this->tokens[ $this->tokens_index ]->type === $type ) && ( $this->tokens[ $this->tokens_index ]->flags === $flag ) ) { - return $this->tokens[ $this->tokens_index++ ]; - } - } - - return null; - } - - /** - * Gets the next token. - * - * @param int $type The type of the token. - * @param string $value The value of the token. - * - * @return stdClass|null - */ - public function tokens_get_next_of_type_and_value( $type, $value ) { - for ( ; $this->tokens_index < $this->tokens_count; ++$this->tokens_index ) { - if ( ( $this->tokens[ $this->tokens_index ]->type === $type ) && ( $this->tokens[ $this->tokens_index ]->value === $value ) ) { - return $this->tokens[ $this->tokens_index++ ]; - } - } - - return null; - } - - /** - * Gets the next token. Skips any irrelevant token (whitespaces and - * comments). - * - * @return stdClass|null - */ - public function tokens_get_next() { - for ( ; $this->tokens_index < $this->tokens_count; ++$this->tokens_index ) { - if ( - ( WP_SQLite_Token::TYPE_WHITESPACE !== $this->tokens[ $this->tokens_index ]->type ) - && ( WP_SQLite_Token::TYPE_COMMENT !== $this->tokens[ $this->tokens_index ]->type ) - ) { - return $this->tokens[ $this->tokens_index++ ]; - } - } - - return null; - } -} diff --git a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php b/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php deleted file mode 100644 index b72d787f..00000000 --- a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php +++ /dev/null @@ -1,899 +0,0 @@ - - * new WP_SQLite_PDO_User_Defined_Functions(ref_to_pdo_obj); - * - * - * This automatically enables ref_to_pdo_obj to replace the function in the SQL statement - * to the ones defined here. - */ -class WP_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', - ); - - /** - * 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 RAND() function. - * - * SQLite does have a random generator, but it is called RANDOM() and returns random - * number between -9223372036854775808 and +9223372036854775807. So we substitute it - * with PHP random generator. - * - * This function uses mt_rand() which is four times faster than rand() and returns - * the random number between 0 and 1. - * - * @return int - */ - public function rand() { - return mt_rand( 0, 1 ); - } - - /** - * Method to emulate MySQL 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. - * - * LOG(X) - * - * Used with two arguments, it returns the natural logarithm of X base B. - * - * LOG(B, X) - * - * In this case, it returns the value of log(X) / log(B). - * - * Used without an argument, it returns false. This returned value will be - * rewritten to 0, because SQLite doesn't understand true/false value. - * - * @return double|null - */ - public function log() { - $num_args = func_num_args(); - if ( 1 === $num_args ) { - $arg1 = func_get_arg( 0 ); - - return log( $arg1 ); - } - if ( 2 === $num_args ) { - $arg1 = func_get_arg( 0 ); - $arg2 = func_get_arg( 1 ); - - return log( $arg1 ) / log( $arg2 ); - } - return null; - } - - /** - * Method to emulate MySQL LEAST() function. - * - * This function rewrites the function name to SQLite compatible function name. - * - * @return mixed - */ - public function least() { - $arg_list = func_get_args(); - - return min( $arg_list ); - } - - /** - * Method to emulate MySQL GREATEST() function. - * - * This function rewrites the function name to SQLite compatible function name. - * - * @return mixed - */ - public function greatest() { - $arg_list = func_get_args(); - - return max( $arg_list ); - } - - /** - * Method to dummy out MySQL GET_LOCK() function. - * - * This function is meaningless in SQLite, so we do nothing. - * - * @param string $name Not used. - * @param integer $timeout Not used. - * - * @return string - */ - public function get_lock( $name, $timeout ) { - return '1=1'; - } - - /** - * Method to dummy out MySQL RELEASE_LOCK() function. - * - * This function is meaningless in SQLite, so we do nothing. - * - * @param string $name Not used. - * - * @return string - */ - public function release_lock( $name ) { - return '1=1'; - } - - /** - * Method to emulate MySQL UCASE() function. - * - * This is MySQL alias for upper() function. This function rewrites it - * to SQLite compatible name upper(). - * - * @param string $content String to be converted to uppercase. - * - * @return string SQLite compatible function name. - */ - public function ucase( $content ) { - return "upper($content)"; - } - - /** - * Method to emulate MySQL LCASE() function. - * - * This is MySQL alias for lower() function. This function rewrites it - * to SQLite compatible name lower(). - * - * @param string $content String to be converted to lowercase. - * - * @return string SQLite compatible function name. - */ - public function lcase( $content ) { - return "lower($content)"; - } - - /** - * Method to emulate MySQL UNHEX() function. - * - * For a string argument str, UNHEX(str) interprets each pair of characters - * in the argument as a hexadecimal number and converts it to the byte represented - * by the number. The return value is a binary string. - * - * @param string $number Number to be unhexed. - * - * @return string Binary string - */ - public function unhex( $number ) { - return pack( 'H*', $number ); - } - - /** - * Method to emulate MySQL FROM_BASE64() function. - * - * Takes a base64-encoded string and returns the decoded result as a binary - * string. Returns NULL if the argument is NULL or is not a valid base64 string. - * - * @param string|null $str The base64-encoded string. - * - * @return string|null Decoded binary string, or NULL. - */ - public function from_base64( $str ) { - if ( null === $str ) { - return null; - } - $decoded = base64_decode( $str, true ); - if ( false === $decoded ) { - return null; - } - return $decoded; - } - - /** - * Method to emulate MySQL TO_BASE64() function. - * - * Takes a string and returns a base64-encoded result. - * Returns NULL if the argument is NULL. - * - * @param string|null $str The string to encode. - * - * @return string|null Base64-encoded string, or NULL. - */ - public function to_base64( $str ) { - if ( null === $str ) { - return null; - } - return base64_encode( $str ); - } - - /** - * Method to emulate MySQL INET_NTOA() function. - * - * This function gets 4 or 8 bytes integer and turn it into the network address. - * - * @param integer $num Long integer. - * - * @return string - */ - public function inet_ntoa( $num ) { - return long2ip( $num ); - } - - /** - * Method to emulate MySQL INET_ATON() function. - * - * This function gets the network address and turns it into integer. - * - * @param string $addr Network address. - * - * @return int long integer - */ - public function inet_aton( $addr ) { - return absint( ip2long( $addr ) ); - } - - /** - * Method to emulate MySQL DATEDIFF() function. - * - * This function compares two dates value and returns the difference. - * - * @param string $start Start date. - * @param string $end End date. - * - * @return string - */ - public function datediff( $start, $end ) { - $start_date = new DateTime( $start ); - $end_date = new DateTime( $end ); - $interval = $end_date->diff( $start_date, false ); - - return $interval->format( '%r%a' ); - } - - /** - * Method to emulate MySQL LOCATE() function. - * - * This function returns the position if $substr is found in $str. If not, - * it returns 0. If mbstring extension is loaded, mb_strpos() function is - * used. - * - * @param string $substr Needle. - * @param string $str Haystack. - * @param integer $pos Position. - * - * @return integer - */ - public function locate( $substr, $str, $pos = 0 ) { - if ( ! extension_loaded( 'mbstring' ) ) { - $val = strpos( $str, $substr, $pos ); - if ( false !== $val ) { - return $val + 1; - } - return 0; - } - $val = mb_strpos( $str, $substr, $pos ); - if ( false !== $val ) { - return $val + 1; - } - return 0; - } - - /** - * Method to return GMT date in the string format. - * - * @return string formatted GMT date 'dddd-mm-dd' - */ - public function utc_date() { - return gmdate( 'Y-m-d', time() ); - } - - /** - * Method to return GMT time in the string format. - * - * @return string formatted GMT time '00:00:00' - */ - public function utc_time() { - return gmdate( 'H:i:s', time() ); - } - - /** - * Method to return GMT time stamp in the string format. - * - * @return string formatted GMT timestamp 'yyyy-mm-dd 00:00:00' - */ - public function utc_timestamp() { - return gmdate( 'Y-m-d H:i:s', time() ); - } - - /** - * Method to return MySQL version. - * - * This function only returns the current newest version number of MySQL, - * because it is meaningless for SQLite database. - * - * @return string representing the version number: major_version.minor_version - */ - public function version() { - return '5.5'; - } - - /** - * A helper to covert LIKE pattern to a GLOB pattern for "LIKE BINARY" support. - - * @TODO: Some of the MySQL string specifics described below are likely to - * affect also other patterns than just "LIKE BINARY". We should - * consider applying some of the conversions more broadly. - * - * @param string $pattern - * @return string - */ - public function _helper_like_to_glob_pattern( $pattern ) { - if ( null === $pattern ) { - return null; - } - - /* - * 1. Escape characters that have special meaning in GLOB patterns. - * - * We need to: - * 1. Escape "]" as "[]]" to avoid interpreting "[...]" as a character class. - * 2. Escape "*" as "[*]" (must be after 1 to avoid being escaped). - * 3. Escape "?" as "[?]" (must be after 1 to avoid being escaped). - */ - $pattern = str_replace( ']', '[]]', $pattern ); - $pattern = str_replace( '*', '[*]', $pattern ); - $pattern = str_replace( '?', '[?]', $pattern ); - - /* - * 2. Convert LIKE wildcards to GLOB wildcards ("%" -> "*", "_" -> "?"). - * - * We need to convert them only when they don't follow any backslashes, - * or when they follow an even number of backslashes (as "\\" is "\"). - */ - $pattern = preg_replace( '/(^|[^\\\\](?:\\\\{2})*)%/', '$1*', $pattern ); - $pattern = preg_replace( '/(^|[^\\\\](?:\\\\{2})*)_/', '$1?', $pattern ); - - /* - * 3. Unescape LIKE escape sequences. - * - * While in MySQL LIKE patterns, a backslash is usually used to escape - * special characters ("%", "_", and "\"), it works with all characters. - * - * That is: - * SELECT '\\x' prints '\x', but LIKE '\\x' is equivalent to LIKE 'x'. - * - * This is true also for multi-byte characters: - * SELECT '\\©' prints '\©', but LIKE '\\©' is equivalent to LIKE '©'. - * - * However, the multi-byte behavior is likely to depend on the charset. - * For now, we'll assume UTF-8 and thus the "u" modifier for the regex. - */ - $pattern = preg_replace( '/\\\\(.)/u', '$1', $pattern ); - - return $pattern; - } -} diff --git a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php b/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php deleted file mode 100644 index edb32886..00000000 --- a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php +++ /dev/null @@ -1,343 +0,0 @@ -input_tokens = $input_tokens; - $this->max = count( $input_tokens ); - } - - /** - * Returns the updated query. - * - * @return string - */ - public function get_updated_query() { - $query = ''; - foreach ( $this->output_tokens as $token ) { - $query .= $token->token; - } - return $query; - } - - /** - * Add a token to the output. - * - * @param WP_SQLite_Token $token Token object. - */ - public function add( $token ) { - if ( $token ) { - $this->output_tokens[] = $token; - } - } - - /** - * Add multiple tokens to the output. - * - * @param WP_SQLite_Token[] $tokens Array of token objects. - */ - public function add_many( $tokens ) { - $this->output_tokens = array_merge( $this->output_tokens, $tokens ); - } - - /** - * Replaces all tokens. - * - * @param WP_SQLite_Token[] $tokens Array of token objects. - */ - public function replace_all( $tokens ) { - $this->output_tokens = $tokens; - } - - /** - * Peek at the next tokens and return one that matches the given criteria. - * - * @param array $query Optional. Search query. - * [ - * 'type' => string|null, // Token type. - * 'flags' => int|null, // Token flags. - * 'values' => string|null, // Token values. - * ]. - * - * @return WP_SQLite_Token - */ - public function peek( $query = array() ) { - $type = isset( $query['type'] ) ? $query['type'] : null; - $flags = isset( $query['flags'] ) ? $query['flags'] : null; - $values = isset( $query['value'] ) - ? ( is_array( $query['value'] ) ? $query['value'] : array( $query['value'] ) ) - : null; - - $i = $this->index; - while ( ++$i < $this->max ) { - if ( $this->input_tokens[ $i ]->matches( $type, $flags, $values ) ) { - return $this->input_tokens[ $i ]; - } - } - } - - /** - * Move forward and return the next tokens that match the given criteria. - * - * @param int $nth The nth token to return. - * - * @return WP_SQLite_Token - */ - public function peek_nth( $nth ) { - $found = 0; - for ( $i = $this->index + 1;$i < $this->max;$i++ ) { - $token = $this->input_tokens[ $i ]; - if ( ! $token->is_semantically_void() ) { - ++$found; - } - if ( $found === $nth ) { - return $this->input_tokens[ $i ]; - } - } - } - - /** - * Consume all the tokens. - * - * @param array $query Search query. - * - * @return void - */ - public function consume_all( $query = array() ) { - while ( $this->consume( $query ) ) { - // Do nothing. - } - } - - /** - * Consume the next tokens and return one that matches the given criteria. - * - * @param array $query Search query. - * [ - * 'type' => null, // Optional. Token type. - * 'flags' => null, // Optional. Token flags. - * 'values' => null, // Optional. Token values. - * ]. - * - * @return WP_SQLite_Token|null - */ - public function consume( $query = array() ) { - $tokens = $this->move_forward( $query ); - $this->output_tokens = array_merge( $this->output_tokens, $tokens ); - return $this->token; - } - - /** - * Drop the last consumed token and return it. - * - * @return WP_SQLite_Token|null - */ - public function drop_last() { - return array_pop( $this->output_tokens ); - } - - /** - * Skip over the next tokens and return one that matches the given criteria. - * - * @param array $query Search query. - * [ - * 'type' => null, // Optional. Token type. - * 'flags' => null, // Optional. Token flags. - * 'values' => null, // Optional. Token values. - * ]. - * - * @return WP_SQLite_Token|null - */ - public function skip( $query = array() ) { - $this->skip_and_return_all( $query ); - return $this->token; - } - - /** - * Skip over the next tokens until one matches the given criteria, - * and return all the skipped tokens. - * - * @param array $query Search query. - * [ - * 'type' => null, // Optional. Token type. - * 'flags' => null, // Optional. Token flags. - * 'values' => null, // Optional. Token values. - * ]. - * - * @return WP_SQLite_Token[] - */ - public function skip_and_return_all( $query = array() ) { - $tokens = $this->move_forward( $query ); - - /* - * When skipping over whitespaces, make sure to consume - * at least one to avoid SQL syntax errors. - */ - foreach ( $tokens as $token ) { - if ( $token->matches( WP_SQLite_Token::TYPE_WHITESPACE ) ) { - $this->add( $token ); - break; - } - } - - return $tokens; - } - - /** - * Returns the next tokens that match the given criteria. - * - * @param array $query Search query. - * [ - * 'type' => string|null, // Optional. Token type. - * 'flags' => int|null, // Optional. Token flags. - * 'values' => string|null, // Optional. Token values. - * ]. - * - * @return array - */ - private function move_forward( $query = array() ) { - $type = isset( $query['type'] ) ? $query['type'] : null; - $flags = isset( $query['flags'] ) ? $query['flags'] : null; - $values = isset( $query['value'] ) - ? ( is_array( $query['value'] ) ? $query['value'] : array( $query['value'] ) ) - : null; - $depth = isset( $query['depth'] ) ? $query['depth'] : null; - - $buffered = array(); - while ( true ) { - if ( ++$this->index >= $this->max ) { - $this->token = null; - $this->call_stack = array(); - break; - } - $this->token = $this->input_tokens[ $this->index ]; - $this->update_call_stack(); - $buffered[] = $this->token; - if ( - ( null === $depth || $this->depth === $depth ) - && $this->token->matches( $type, $flags, $values ) - ) { - break; - } - } - - return $buffered; - } - - /** - * Returns the last call stack element. - * - * @return array|null - */ - public function last_call_stack_element() { - return count( $this->call_stack ) ? $this->call_stack[ count( $this->call_stack ) - 1 ] : null; - } - - /** - * Updates the call stack. - * - * @return void - */ - private function update_call_stack() { - if ( $this->token->flags & WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ) { - $this->last_function_call = $this->token->value; - } - if ( WP_SQLite_Token::TYPE_OPERATOR === $this->token->type ) { - switch ( $this->token->value ) { - case '(': - if ( $this->last_function_call ) { - array_push( - $this->call_stack, - array( - 'function' => $this->last_function_call, - 'depth' => $this->depth, - ) - ); - $this->last_function_call = null; - } - ++$this->depth; - break; - - case ')': - --$this->depth; - $call_parent = $this->last_call_stack_element(); - if ( - $call_parent && - $call_parent['depth'] === $this->depth - ) { - array_pop( $this->call_stack ); - } - break; - } - } - } -} diff --git a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-token.php b/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-token.php deleted file mode 100644 index fbafd9e9..00000000 --- a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-token.php +++ /dev/null @@ -1,327 +0,0 @@ -, !==, etc. - * Bitwise operators: &, |, ^, etc. - * Assignment operators: =, +=, -=, etc. - * SQL specific operators: . (e.g. .. WHERE database.table ..), - * * (e.g. SELECT * FROM ..) - */ - const TYPE_OPERATOR = 2; - - /** - * Spaces, tabs, new lines, etc. - */ - const TYPE_WHITESPACE = 3; - - /** - * Any type of legal comment. - * - * Bash (#), C (/* *\/) or SQL (--) comments: - * - * -- SQL-comment - * - * #Bash-like comment - * - * /*C-like comment*\/ - * - * or: - * - * /*C-like - * comment*\/ - * - * Backslashes were added to respect PHP's comments syntax. - */ - const TYPE_COMMENT = 4; - - /** - * Boolean values: true or false. - */ - const TYPE_BOOL = 5; - - /** - * Numbers: 4, 0x8, 15.16, 23e42, etc. - */ - const TYPE_NUMBER = 6; - - /** - * Literal strings: 'string', "test". - * Some of these strings are actually symbols. - */ - const TYPE_STRING = 7; - - /** - * Database, table names, variables, etc. - * For example: ```SELECT `foo`, `bar` FROM `database`.`table`;```. - */ - const TYPE_SYMBOL = 8; - - /** - * Delimits an unknown string. - * For example: ```SELECT * FROM test;```, `test` is a delimiter. - */ - const TYPE_DELIMITER = 9; - - /** - * Labels in LOOP statement, ITERATE statement etc. - * For example (only for begin label): - * begin_label: BEGIN [statement_list] END [end_label] - * begin_label: LOOP [statement_list] END LOOP [end_label] - * begin_label: REPEAT [statement_list] ... END REPEAT [end_label] - * begin_label: WHILE ... DO [statement_list] END WHILE [end_label]. - */ - const TYPE_LABEL = 10; - - // Flags that describe the tokens in more detail. - // All keywords must have flag 1 so `Context::isKeyword` method doesn't - // require strict comparison. - const FLAG_KEYWORD_RESERVED = 2; - const FLAG_KEYWORD_COMPOSED = 4; - const FLAG_KEYWORD_DATA_TYPE = 8; - const FLAG_KEYWORD_KEY = 16; - const FLAG_KEYWORD_FUNCTION = 32; - - // Numbers related flags. - const FLAG_NUMBER_HEX = 1; - const FLAG_NUMBER_FLOAT = 2; - const FLAG_NUMBER_APPROXIMATE = 4; - const FLAG_NUMBER_NEGATIVE = 8; - const FLAG_NUMBER_BINARY = 16; - - // Strings related flags. - const FLAG_STRING_SINGLE_QUOTES = 1; - const FLAG_STRING_DOUBLE_QUOTES = 2; - - // Comments related flags. - const FLAG_COMMENT_BASH = 1; - const FLAG_COMMENT_C = 2; - const FLAG_COMMENT_SQL = 4; - const FLAG_COMMENT_MYSQL_CMD = 8; - - // Operators related flags. - const FLAG_OPERATOR_ARITHMETIC = 1; - const FLAG_OPERATOR_LOGICAL = 2; - const FLAG_OPERATOR_BITWISE = 4; - const FLAG_OPERATOR_ASSIGNMENT = 8; - const FLAG_OPERATOR_SQL = 16; - - // Symbols related flags. - const FLAG_SYMBOL_VARIABLE = 1; - const FLAG_SYMBOL_BACKTICK = 2; - const FLAG_SYMBOL_USER = 4; - const FLAG_SYMBOL_SYSTEM = 8; - const FLAG_SYMBOL_PARAMETER = 16; - - /** - * The token it its raw string representation. - * - * @var string - */ - public $token; - - /** - * The value this token contains (i.e. token after some evaluation). - * - * @var mixed - */ - public $value; - - /** - * The keyword value this token contains, always uppercase. - * - * @var mixed|string|null - */ - public $keyword = null; - - /** - * The type of this token. - * - * @var int - */ - public $type; - - /** - * The flags of this token. - * - * @var int - */ - public $flags; - - /** - * The position in the initial string where this token started. - * - * The position is counted in chars, not bytes, so you should - * use mb_* functions to properly handle utf-8 multibyte chars. - * - * @var int|null - */ - public $position; - - /** - * Constructor. - * - * @param string $token The value of the token. - * @param int $type The type of the token. - * @param int $flags The flags of the token. - */ - public function __construct( $token, $type = 0, $flags = 0 ) { - $this->token = $token; - $this->type = $type; - $this->flags = $flags; - $this->value = $this->extract(); - } - - /** - * Check if the token matches the given parameters. - * - * @param int|null $type The type of the token. - * @param int|null $flags The flags of the token. - * @param array|null $values The values of the token. - * - * @return bool - */ - public function matches( $type = null, $flags = null, $values = null ) { - if ( null === $type && null === $flags && ( null === $values || array() === $values ) ) { - return ! $this->is_semantically_void(); - } - - return ( - ( null === $type || $this->type === $type ) - && ( null === $flags || ( $this->flags & $flags ) ) - && ( null === $values || in_array( strtoupper( $this->value ?? '' ), $values, true ) ) - ); - } - - /** - * Check if the token is semantically void (i.e. whitespace or comment). - * - * @return bool - */ - public function is_semantically_void() { - return $this->matches( self::TYPE_WHITESPACE ) || $this->matches( self::TYPE_COMMENT ); - } - - /** - * Does little processing to the token to extract a value. - * - * If no processing can be done it will return the initial string. - * - * @return mixed - */ - private function extract() { - switch ( $this->type ) { - case self::TYPE_KEYWORD: - $this->keyword = strtoupper( $this->token ?? '' ); - if ( ! ( $this->flags & self::FLAG_KEYWORD_RESERVED ) ) { - /* - * Unreserved keywords should stay the way they are - * because they might represent field names. - */ - return $this->token; - } - - return $this->keyword; - - case self::TYPE_WHITESPACE: - return ' '; - - case self::TYPE_BOOL: - return strtoupper( $this->token ?? '' ) === 'TRUE'; - - case self::TYPE_NUMBER: - $ret = str_replace( '--', '', $this->token ); // e.g. ---42 === -42. - if ( $this->flags & self::FLAG_NUMBER_HEX ) { - $ret = str_replace( array( '-', '+' ), '', $this->token ); - if ( $this->flags & self::FLAG_NUMBER_NEGATIVE ) { - $ret = -hexdec( $ret ); - } else { - $ret = hexdec( $ret ); - } - } elseif ( ( $this->flags & self::FLAG_NUMBER_APPROXIMATE ) || ( $this->flags & self::FLAG_NUMBER_FLOAT ) ) { - $ret = (float) $ret; - } elseif ( ! ( $this->flags & self::FLAG_NUMBER_BINARY ) ) { - $ret = (int) $ret; - } - - return $ret; - - case self::TYPE_STRING: - // Trims quotes. - $str = $this->token; - $str = mb_substr( $str, 1, -1, 'UTF-8' ); - - // Removes surrounding quotes. - $quote = $this->token[0]; - $str = str_replace( $quote . $quote, $quote, $str ); - - /* - * Finally unescapes the string. - * - * `stripcslashes` replaces escape sequences with their - * representation. - */ - $str = stripcslashes( $str ); - - return $str; - - case self::TYPE_SYMBOL: - $str = $this->token; - if ( isset( $str[0] ) && ( '@' === $str[0] ) ) { - /* - * `mb_strlen($str)` must be used instead of `null` because - * in PHP 5.3- the `null` parameter isn't handled correctly. - */ - $str = mb_substr( - $str, - ! empty( $str[1] ) && ( '@' === $str[1] ) ? 2 : 1, - mb_strlen( $str ), - 'UTF-8' - ); - } - - if ( isset( $str[0] ) && ( ':' === $str[0] ) ) { - $str = mb_substr( $str, 1, mb_strlen( $str ), 'UTF-8' ); - } - - if ( isset( $str[0] ) && ( ( '`' === $str[0] ) || ( '"' === $str[0] ) || ( '\'' === $str[0] ) ) ) { - $quote = $str[0]; - $str = str_replace( $quote . $quote, $quote, $str ); - $str = mb_substr( $str, 1, -1, 'UTF-8' ); - } - - return $str; - } - - return $this->token; - } -} diff --git a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-translator.php b/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-translator.php deleted file mode 100644 index 3a536d18..00000000 --- a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-translator.php +++ /dev/null @@ -1,4543 +0,0 @@ - 'integer', - 'bool' => 'integer', - 'boolean' => 'integer', - 'tinyint' => 'integer', - 'smallint' => 'integer', - 'mediumint' => 'integer', - 'int' => 'integer', - 'integer' => 'integer', - 'bigint' => 'integer', - 'float' => 'real', - 'double' => 'real', - 'decimal' => 'real', - 'dec' => 'real', - 'enum' => 'text', - 'numeric' => 'real', - 'fixed' => 'real', - 'date' => 'text', - 'datetime' => 'text', - 'timestamp' => 'text', - 'time' => 'text', - 'year' => 'text', - 'char' => 'text', - 'varchar' => 'text', - 'binary' => 'integer', - 'varbinary' => 'blob', - 'tinyblob' => 'blob', - 'tinytext' => 'text', - 'blob' => 'blob', - 'text' => 'text', - 'mediumblob' => 'blob', - 'mediumtext' => 'text', - 'longblob' => 'blob', - 'longtext' => 'text', - 'geomcollection' => 'text', - 'geometrycollection' => 'text', - ); - - /** - * The MySQL to SQLite date formats translation. - * - * Maps MySQL formats to SQLite strftime() formats. - * - * For MySQL formats, see: - * * https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_date-format - * - * For SQLite formats, see: - * * https://www.sqlite.org/lang_datefunc.html - * * https://strftime.org/ - * - * @var array - */ - private $mysql_date_format_to_sqlite_strftime = array( - '%a' => '%D', - '%b' => '%M', - '%c' => '%n', - '%D' => '%jS', - '%d' => '%d', - '%e' => '%j', - '%H' => '%H', - '%h' => '%h', - '%I' => '%h', - '%i' => '%M', - '%j' => '%z', - '%k' => '%G', - '%l' => '%g', - '%M' => '%F', - '%m' => '%m', - '%p' => '%A', - '%r' => '%h:%i:%s %A', - '%S' => '%s', - '%s' => '%s', - '%T' => '%H:%i:%s', - '%U' => '%W', - '%u' => '%W', - '%V' => '%W', - '%v' => '%W', - '%W' => '%l', - '%w' => '%w', - '%X' => '%Y', - '%x' => '%o', - '%Y' => '%Y', - '%y' => '%y', - ); - - /** - * Number of rows found by the last SELECT query. - * - * @var int - */ - private $last_select_found_rows; - - /** - * Number of rows found by the last SQL_CALC_FOUND_ROW query. - * - * @var int integer - */ - private $last_sql_calc_found_rows = null; - - /** - * The query rewriter. - * - * @var WP_SQLite_Query_Rewriter - */ - private $rewriter; - - /** - * Last executed MySQL query. - * - * @var string - */ - public $mysql_query; - - /** - * A list of executed SQLite queries. - * - * @var array - */ - public $executed_sqlite_queries = array(); - - /** - * The affected table name. - * - * @var array - */ - private $table_name = array(); - - /** - * The type of the executed query (SELECT, INSERT, etc). - * - * @var array - */ - private $query_type = array(); - - /** - * The columns to insert. - * - * @var array - */ - private $insert_columns = array(); - - /** - * Class variable to store the result of the query. - * - * @access private - * - * @var array reference to the PHP object - */ - private $results = null; - - /** - * Class variable to check if there is an error. - * - * @var boolean - */ - public $is_error = false; - - /** - * Class variable to store the file name and function to cause error. - * - * @access private - * - * @var array - */ - private $errors; - - /** - * Class variable to store the error messages. - * - * @access private - * - * @var array - */ - private $error_messages = array(); - - /** - * Class variable to store the affected row id. - * - * @var int integer - * @access private - */ - private $last_insert_id; - - /** - * Class variable to store the number of rows affected. - * - * @var int integer - */ - private $affected_rows; - - /** - * Class variable to store the queried column info. - * - * @var array - */ - private $column_data; - - /** - * Variable to emulate MySQL affected row. - * - * @var integer - */ - private $num_rows; - - /** - * Return value from query(). - * - * Each query has its own return value. - * - * @var mixed - */ - private $return_value; - - /** - * Variable to keep track of nested transactions level. - * - * @var int - */ - private $transaction_level = 0; - - /** - * Value returned by the last exec(). - * - * @var mixed - */ - private $last_exec_returned; - - /** - * The PDO fetch mode passed to query(). - * - * @var mixed - */ - private $pdo_fetch_mode; - - /** - * The last reserved keyword seen in an SQL query. - * - * @var mixed - */ - private $last_reserved_keyword; - - /** - * True if a VACUUM operation should be done on shutdown, - * to handle OPTIMIZE TABLE and similar operations. - * - * @var bool - */ - private $vacuum_requested = false; - - /** - * True if the present query is metadata - * - * @var bool - */ - private $is_information_schema_query = false; - - /** - * True if a GROUP BY clause is detected. - * - * @var bool - */ - private $has_group_by = false; - - /** - * 0 if no LIKE is in progress, otherwise counts nested parentheses. - * - * @todo A generic stack of expression would scale better. There's already a call_stack in WP_SQLite_Query_Rewriter. - * @var int - */ - private $like_expression_nesting = 0; - - /** - * 0 if no LIKE is in progress, otherwise counts nested parentheses. - * - * @var int - */ - private $like_escape_count = 0; - - /** - * Associative array with list of system (non-WordPress) tables. - * - * @var array [tablename => tablename] - */ - private $sqlite_system_tables = array(); - - /** - * The last error message from SQLite. - * - * @var string - */ - private $last_sqlite_error; - - /** - * Constructor. - * - * Create PDO object, set user defined functions and initialize other settings. - * Don't use parent::__construct() because this class does not only returns - * PDO instance but many others jobs. - * - * @param PDO $pdo The PDO object. - */ - public function __construct( $pdo = null ) { - if ( ! $pdo ) { - if ( ! is_file( FQDB ) ) { - $this->prepare_directory(); - } - - $locked = false; - $status = 0; - $err_message = ''; - do { - try { - $options = array( - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_STRINGIFY_FETCHES => true, - PDO::ATTR_TIMEOUT => 5, - ); - - $dsn = 'sqlite:' . FQDB; - $pdo_class = PHP_VERSION_ID >= 80400 ? PDO\SQLite::class : PDO::class; - $pdo = new $pdo_class( $dsn, null, null, $options ); - } catch ( PDOException $ex ) { - $status = $ex->getCode(); - if ( self::SQLITE_BUSY === $status || self::SQLITE_LOCKED === $status ) { - $locked = true; - } else { - $err_message = $ex->getMessage(); - } - } - } while ( $locked ); - - if ( $status > 0 ) { - $message = sprintf( - '

%s

%s

%s

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

MySQL query:

' . PHP_EOL; - $output .= '

' . $this->mysql_query . '

' . PHP_EOL; - $output .= '

Queries made or created this session were:

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

Backtrace:

' . PHP_EOL; - $output .= '
' . $e->getTraceAsString() . '
' . PHP_EOL; - } - - return $output; - } - - /** - * Executes a query in SQLite. - * - * @param mixed $sql The query to execute. - * @param mixed $params The parameters to bind to the query. - * @throws PDOException If the query could not be executed. - * @return object { - * The result of the query. - * - * @type PDOStatement $stmt The executed statement - * @type * $result The value returned by $stmt. - * } - */ - public function execute_sqlite_query( $sql, $params = array() ) { - $this->executed_sqlite_queries[] = array( - 'sql' => $sql, - 'params' => $params, - ); - - $stmt = $this->pdo->prepare( $sql ); - if ( false === $stmt || null === $stmt ) { - $this->last_exec_returned = null; - $info = $this->pdo->errorInfo(); - $this->last_sqlite_error = $info[0] . ' ' . $info[2]; - throw new PDOException( implode( ' ', array( 'Error:', $info[0], $info[2], 'SQLite:', $sql ) ), $info[1] ); - } - $returned = $stmt->execute( $params ); - $this->last_exec_returned = $returned; - if ( ! $returned ) { - $info = $stmt->errorInfo(); - $this->last_sqlite_error = $info[0] . ' ' . $info[2]; - throw new PDOException( implode( ' ', array( 'Error:', $info[0], $info[2], 'SQLite:', $sql ) ), $info[1] ); - } - - return $stmt; - } - - /** - * Method to set the results from the fetched data. - * - * @param array $data The data to set. - */ - private function set_results_from_fetched_data( $data ) { - if ( null === $this->results ) { - $this->results = $data; - } - if ( is_array( $this->results ) ) { - $this->num_rows = count( $this->results ); - $this->last_select_found_rows = count( $this->results ); - } - $this->return_value = $this->results; - } - - /** - * Method to set the results from the affected rows. - * - * @param int|null $override Override the affected rows. - */ - private function set_result_from_affected_rows( $override = null ) { - /* - * SELECT CHANGES() is a workaround for the fact that - * $stmt->rowCount() returns "0" (zero) with the - * SQLite driver at all times. - * Source: https://www.php.net/manual/en/pdostatement.rowcount.php - */ - if ( null === $override ) { - $this->affected_rows = (int) $this->execute_sqlite_query( 'select changes()' )->fetch()[0]; - } else { - $this->affected_rows = $override; - } - $this->return_value = $this->affected_rows; - $this->num_rows = $this->affected_rows; - $this->results = $this->affected_rows; - } - - /** - * Method to clear previous data. - */ - private function flush() { - $this->mysql_query = ''; - $this->results = null; - $this->last_exec_returned = null; - $this->table_name = null; - $this->last_insert_id = null; - $this->affected_rows = null; - $this->insert_columns = array(); - $this->column_data = array(); - $this->num_rows = null; - $this->return_value = null; - $this->error_messages = array(); - $this->is_error = false; - $this->executed_sqlite_queries = array(); - $this->like_expression_nesting = 0; - $this->like_escape_count = 0; - $this->is_information_schema_query = false; - $this->has_group_by = false; - } - - /** - * Begin a new transaction or nested transaction. - * - * @return boolean - */ - public function begin_transaction() { - $success = false; - try { - if ( 0 === $this->transaction_level ) { - $this->execute_sqlite_query( 'BEGIN' ); - } else { - $this->execute_sqlite_query( 'SAVEPOINT LEVEL' . $this->transaction_level ); - } - $success = $this->last_exec_returned; - } finally { - if ( $success ) { - ++$this->transaction_level; - if ( function_exists( 'do_action' ) ) { - /** - * Notifies that a transaction-related query has been translated and executed. - * - * @param string $command The SQL statement (one of "START TRANSACTION", "COMMIT", "ROLLBACK"). - * @param bool $success Whether the SQL statement was successful or not. - * @param int $nesting_level The nesting level of the transaction. - * - * @since 0.1.0 - */ - do_action( 'sqlite_transaction_query_executed', 'START TRANSACTION', (bool) $this->last_exec_returned, $this->transaction_level - 1 ); - } - } - } - return $success; - } - - /** - * Commit the current transaction or nested transaction. - * - * @return boolean True on success, false on failure. - */ - public function commit() { - if ( 0 === $this->transaction_level ) { - return false; - } - - --$this->transaction_level; - if ( 0 === $this->transaction_level ) { - $this->execute_sqlite_query( 'COMMIT' ); - } else { - $this->execute_sqlite_query( 'RELEASE SAVEPOINT LEVEL' . $this->transaction_level ); - } - - if ( function_exists( 'do_action' ) ) { - do_action( 'sqlite_transaction_query_executed', 'COMMIT', (bool) $this->last_exec_returned, $this->transaction_level ); - } - return $this->last_exec_returned; - } - - /** - * Rollback the current transaction or nested transaction. - * - * @return boolean True on success, false on failure. - */ - public function rollback() { - if ( 0 === $this->transaction_level ) { - return false; - } - - --$this->transaction_level; - if ( 0 === $this->transaction_level ) { - $this->execute_sqlite_query( 'ROLLBACK' ); - } else { - $this->execute_sqlite_query( 'ROLLBACK TO SAVEPOINT LEVEL' . $this->transaction_level ); - } - if ( function_exists( 'do_action' ) ) { - do_action( 'sqlite_transaction_query_executed', 'ROLLBACK', (bool) $this->last_exec_returned, $this->transaction_level ); - } - return $this->last_exec_returned; - } - - /** - * Create an index name consisting of table name and original index name. - * This is to avoid duplicate index names in SQLite. - * - * @param $table - * @param $original_index_name - * - * @return string - */ - private function generate_index_name( $table, $original_index_name ) { - // Strip the occurrences of 2 or more consecutive underscores from the table name - // to allow easier splitting on __ later. - return preg_replace( '/_{2,}/', '_', $table ) . '__' . $original_index_name; - } - - /** - * @param string $table - * @param string $column - */ - private function add_column_on_update_current_timestamp( $table, $column ) { - $trigger_name = $this->get_column_on_update_current_timestamp_trigger_name( $table, $column ); - - // The trigger wouldn't work for virtual and "WITHOUT ROWID" tables, - // but currently that can't happen as we're not creating such tables. - // See: https://www.sqlite.org/rowidtable.html - $quoted_trigger = $this->quote_identifier( $trigger_name ); - $quoted_table = $this->quote_identifier( $table ); - $quoted_column = $this->quote_identifier( $column ); - $this->execute_sqlite_query( - "CREATE TRIGGER $quoted_trigger - AFTER UPDATE ON $quoted_table - FOR EACH ROW - BEGIN - UPDATE $quoted_table SET $quoted_column = CURRENT_TIMESTAMP WHERE rowid = NEW.rowid; - END" - ); - } - - /** - * @param string $table - * @param string $column - * @return string - */ - private function get_column_on_update_current_timestamp_trigger_name( $table, $column ) { - return "__{$table}_{$column}_on_update__"; - } -} diff --git a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/db.php b/packages/plugin-sqlite-database-integration/wp-includes/sqlite/db.php index dc4d14f8..03313515 100644 --- a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/db.php +++ b/packages/plugin-sqlite-database-integration/wp-includes/sqlite/db.php @@ -47,33 +47,11 @@ ); } -if ( defined( 'WP_SQLITE_AST_DRIVER' ) && WP_SQLITE_AST_DRIVER ) { - require_once __DIR__ . '/../database/load.php'; -} else { - require_once __DIR__ . '/php-polyfills.php'; - require_once __DIR__ . '/class-wp-sqlite-lexer.php'; - require_once __DIR__ . '/class-wp-sqlite-query-rewriter.php'; - require_once __DIR__ . '/class-wp-sqlite-translator.php'; - require_once __DIR__ . '/class-wp-sqlite-token.php'; - require_once __DIR__ . '/class-wp-sqlite-pdo-user-defined-functions.php'; -} +require_once __DIR__ . '/../database/load.php'; require_once __DIR__ . '/class-wp-sqlite-db.php'; require_once __DIR__ . '/install-functions.php'; -/** - * The DB_NAME constant is required by the new SQLite driver. - * - * There are some existing projects in which the DB_NAME constant is missing in - * wp-config.php. To enable easier early adoption and testing of the new SQLite - * driver, let's allow using a default database name when DB_NAME is not set. - * - * TODO: For version 3.0, enforce the DB_NAME constant and remove the fallback. - */ -if ( defined( 'DB_NAME' ) && '' !== DB_NAME ) { - $db_name = DB_NAME; -} else { - $db_name = apply_filters( 'wp_sqlite_default_db_name', 'database_name_here' ); -} +$db_name = defined( 'DB_NAME' ) ? DB_NAME : ''; /* * Debug: Cross-check with MySQL. diff --git a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/install-functions.php b/packages/plugin-sqlite-database-integration/wp-includes/sqlite/install-functions.php index bffe1425..5d73e39e 100644 --- a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/install-functions.php +++ b/packages/plugin-sqlite-database-integration/wp-includes/sqlite/install-functions.php @@ -34,15 +34,11 @@ function sqlite_make_db_sqlite() { wp_die( $message, 'Database Error!' ); } - if ( defined( 'WP_SQLITE_AST_DRIVER' ) && WP_SQLITE_AST_DRIVER ) { - $translator = new WP_SQLite_Driver( - new WP_SQLite_Connection( array( 'pdo' => $pdo ) ), - $wpdb->dbname - ); - } else { - $translator = new WP_SQLite_Translator( $pdo ); - } - $query = null; + $translator = new WP_SQLite_Driver( + new WP_SQLite_Connection( array( 'pdo' => $pdo ) ), + $wpdb->dbname + ); + $query = null; try { $translator->begin_transaction(); @@ -52,21 +48,16 @@ function sqlite_make_db_sqlite() { continue; } - $result = $translator->query( $query ); - if ( false === $result ) { - throw new PDOException( $translator->get_error_message() ); - } + $translator->query( $query ); } $translator->commit(); } catch ( PDOException $err ) { - $err_data = $err->errorInfo; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $err_code = $err_data[1]; $translator->rollback(); $message = sprintf( 'Error occurred while creating tables or indexes...
Query was: %s
', var_export( $query, true ) ); - $message .= sprintf( 'Error message is: %s', $err_data[2] ); + $message .= sprintf( 'Error message is: %s', $err->getMessage() ); wp_die( $message, 'Database Error!' ); } diff --git a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/php-polyfills.php b/packages/plugin-sqlite-database-integration/wp-includes/sqlite/php-polyfills.php deleted file mode 100644 index b3ab8ed6..00000000 --- a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/php-polyfills.php +++ /dev/null @@ -1,68 +0,0 @@ - - /packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-translator.php /packages/mysql-on-sqlite/src/sqlite/*\.php /tests/* diff --git a/phpunit.xml.dist b/phpunit.xml.dist deleted file mode 100644 index 134b752a..00000000 --- a/phpunit.xml.dist +++ /dev/null @@ -1,23 +0,0 @@ - - - - - tests/ - tests/e2e/ - packages/ - wordpress/ - - - diff --git a/tests/WP_SQLite_Metadata_Tests.php b/tests/WP_SQLite_Metadata_Tests.php deleted file mode 100644 index b9c51b69..00000000 --- a/tests/WP_SQLite_Metadata_Tests.php +++ /dev/null @@ -1,368 +0,0 @@ -= 80400 ? PDO\SQLite::class : PDO::class; - $this->sqlite = new $pdo_class( 'sqlite::memory:' ); - $this->engine = new WP_SQLite_Translator( $this->sqlite ); - - $translator = $this->engine; - - try { - $translator->begin_transaction(); - foreach ( $queries as $query ) { - $query = trim( $query ); - if ( empty( $query ) ) { - continue; - } - - $result = $translator->execute_sqlite_query( $query ); - if ( false === $result ) { - throw new PDOException( $translator->get_error_message() ); - } - } - $translator->commit(); - } catch ( PDOException $err ) { - $err_data = - $err->errorInfo; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $err_code = $err_data[1]; - $translator->rollback(); - $message = sprintf( - 'Error occurred while creating tables or indexes...
Query was: %s
', - var_export( $query, true ) - ); - $message .= sprintf( 'Error message is: %s', $err_data[2] ); - wp_die( $message, 'Database Error!' ); - } - } - - public function testCountTables() { - $this->assertQuery( "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'wpdata'" ); - - $actual = $this->engine->get_query_results(); - $count = array_values( get_object_vars( $actual[0] ) )[0]; - self::assertIsNumeric( $count ); - } - - public function testInformationSchemaTables() { - $result = $this->assertQuery( "SELECT * FROM information_schema.tables WHERE TABLE_NAME = 'wp_options'" ); - $this->assertEquals( - array( - 'TABLE_CATALOG' => 'def', - 'TABLE_SCHEMA' => '', - 'TABLE_NAME' => 'wp_options', - 'TABLE_TYPE' => 'BASE TABLE', - 'ENGINE' => 'InnoDB', - 'ROW_FORMAT' => 'Dynamic', - 'TABLE_COLLATION' => 'utf8mb4_general_ci', - 'AUTO_INCREMENT' => null, - 'CREATE_TIME' => null, - 'UPDATE_TIME' => null, - 'CHECK_TIME' => null, - 'TABLE_ROWS' => '0', - 'AVG_ROW_LENGTH' => '0', - 'DATA_LENGTH' => '0', - 'MAX_DATA_LENGTH' => '0', - 'INDEX_LENGTH' => '0', - 'DATA_FREE' => '0', - 'CHECKSUM' => null, - 'CREATE_OPTIONS' => '', - 'VERSION' => '10', - 'TABLE_COMMENT' => '', - ), - (array) $result[0] - ); - - $result = $this->assertQuery( - "SELECT - table_name as 'name', - engine AS 'engine', - CAST( data_length / 1024 / 1024 AS UNSIGNED ) AS 'data' - FROM INFORMATION_SCHEMA.TABLES - WHERE TABLE_NAME = 'wp_posts' - ORDER BY name ASC;" - ); - - $this->assertEquals( - array( - 'name' => 'wp_posts', - 'engine' => 'InnoDB', - 'data' => '0', - ), - (array) $result[0] - ); - } - - public function testInformationSchemaQueryHidesSqliteSystemTables() { - /** - * By default, system tables are not returned. - */ - $result = $this->assertQuery( "SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'sqlite_sequence'" ); - $this->assertEquals( 0, count( $result ) ); - - /** - * If we use a custom name for the table_name column, system tables are returned. - */ - $result = $this->assertQuery( "SELECT TABLE_NAME as custom_name FROM INFORMATION_SCHEMA.TABLES WHERE custom_name = 'sqlite_sequence'" ); - $this->assertEquals( 1, count( $result ) ); - } - - private function assertQuery( $sql, $error_substring = null ) { - $retval = $this->engine->query( $sql ); - if ( null === $error_substring ) { - $this->assertEquals( - '', - $this->engine->get_error_message() - ); - $this->assertNotFalse( - $retval - ); - } else { - $this->assertStringContainsStringIgnoringCase( $error_substring, $this->engine->get_error_message() ); - } - - return $retval; - } - - public function testCheckTable() { - - /* a good table */ - $table_name = 'wp_options'; - $expected_result = array( - (object) array( - 'Table' => $table_name, - 'Op' => 'check', - 'Msg_type' => 'status', - 'Msg_text' => 'OK', - ), - ); - - $this->assertQuery( - "CHECK TABLE $table_name;" - ); - - $this->assertEquals( - $expected_result, - $this->engine->get_query_results() - ); - - /* a different good table */ - $table_name = 'wp_postmeta'; - $expected_result = array( - (object) array( - 'Table' => $table_name, - 'Op' => 'check', - 'Msg_type' => 'status', - 'Msg_text' => 'OK', - ), - ); - - $this->assertQuery( - "CHECK TABLE $table_name;" - ); - $this->assertEquals( - $expected_result, - $this->engine->get_query_results() - ); - - /* a bogus, missing, table */ - $table_name = 'wp_sqlite_rocks'; - $expected_result = array( - (object) array( - 'Table' => $table_name, - 'Op' => 'check', - 'Msg_type' => 'Error', - 'Msg_text' => "Table '$table_name' doesn't exist", - ), - (object) array( - 'Table' => $table_name, - 'Op' => 'check', - 'Msg_type' => 'status', - 'Msg_text' => 'Operation failed', - ), - ); - - $this->assertQuery( - "CHECK TABLE $table_name;" - ); - - $this->assertEquals( - $expected_result, - $this->engine->get_query_results() - ); - } - - public function testOptimizeTable() { - - /* a good table */ - $table_name = 'wp_options'; - - $this->assertQuery( - "OPTIMIZE TABLE $table_name;" - ); - - $actual = $this->engine->get_query_results(); - - array_map( - function ( $row ) { - $this->assertIsObject( $row ); - $row = (array) $row; - $this->assertIsString( $row['Table'] ); - $this->assertIsString( $row['Op'] ); - $this->assertIsString( $row['Msg_type'] ); - $this->assertIsString( $row['Msg_text'] ); - }, - $actual - ); - - $ok = array_filter( - $actual, - function ( $row ) { - $row = (array) $row; - - return strtolower( $row['Msg_type'] ) === 'status' && strtolower( $row['Msg_text'] ) === 'ok'; - } - ); - $this->assertIsArray( $ok ); - $this->assertGreaterThan( 0, count( $ok ) ); - } - - public function testRepairTable() { - - /* a good table */ - $table_name = 'wp_options'; - - $this->assertQuery( - "REPAIR TABLE $table_name;" - ); - - $actual = $this->engine->get_query_results(); - - array_map( - function ( $r ) { - $this->assertIsObject( $r ); - $row = $r; - $row = (array) $row; - $this->assertIsString( $row['Table'] ); - $this->assertIsString( $row['Op'] ); - $this->assertIsString( $row['Msg_type'] ); - $this->assertIsString( $row['Msg_text'] ); - }, - $actual - ); - - $ok = array_filter( - $actual, - function ( $row ) { - return strtolower( $row->Msg_type ) === 'status' && strtolower( $row->Msg_text ) === 'ok'; - } - ); - $this->assertIsArray( $ok ); - $this->assertGreaterThan( 0, count( $ok ) ); - } - - // this tests for successful rejection of a bad query - - public function testShowTableStatus() { - - $this->assertQuery( - "INSERT INTO wp_comments ( comment_author, comment_content ) VALUES ( 'PhpUnit', 'Testing' )" - ); - - $this->assertQuery( - "INSERT INTO wp_comments ( comment_author, comment_content ) VALUES ( 'PhpUnit0', 'Testing0' ), ( 'PhpUnit1', 'Testing1' ), ( 'PhpUnit2', 'Testing2' )" - ); - - $this->assertTableEmpty( 'wp_comments', false ); - - $this->assertQuery( - 'SHOW TABLE STATUS FROM wp;' - ); - - $actual = $this->engine->get_query_results(); - - $this->assertIsArray( $actual ); - $this->assertGreaterThanOrEqual( - 1, - count( $actual ) - ); - $this->assertIsObject( $actual[0] ); - - $rows = array_values( - array_filter( - $actual, - function ( $row ) { - $this->assertIsObject( $row ); - $this->assertIsString( $row->Name ); - $this->assertIsNumeric( $row->Rows ); - - return str_ends_with( $row->Name, 'comments' ); - } - ) - ); - $this->assertEquals( 'wp_comments', $rows[0]->Name ); - $this->assertEquals( 4, $rows[0]->Rows ); - } - - private function assertTableEmpty( $table_name, $empty_var ) { - - $this->assertQuery( - "SELECT COUNT(*) num FROM $table_name" - ); - - $actual = $this->engine->get_query_results(); - if ( $empty_var ) { - $this->assertEquals( 0, $actual[0]->num, "$table_name is not empty" ); - } else { - $this->assertGreaterThan( 0, $actual[0]->num, "$table_name is empty" ); - } - } - - public function testTruncateTable() { - - $this->assertQuery( - "INSERT INTO wp_comments ( comment_author, comment_content ) VALUES ( 'PhpUnit', 'Testing' )" - ); - - $this->assertQuery( - "INSERT INTO wp_comments ( comment_author, comment_content ) VALUES ( 'PhpUnit0', 'Testing0' ), ( 'PhpUnit1', 'Testing1' ), ( 'PhpUnit2', 'Testing2' )" - ); - - $this->assertTableEmpty( 'wp_comments', false ); - - $this->assertQuery( - 'TRUNCATE TABLE wp_comments;' - ); - $actual = $this->engine->get_query_results(); - $this->assertEquals( - true, - $actual - ); - $this->assertTableEmpty( 'wp_comments', true ); - } - - public function testBogusQuery() { - - $this->assertQuery( - 'SELECT 1, BOGUS(1) FROM bogus;', - 'no such table: bogus' - ); - $actual = $this->engine->get_query_results(); - $this->assertEquals( - null, - $actual - ); - } -} diff --git a/tests/WP_SQLite_PDO_User_Defined_Functions_Tests.php b/tests/WP_SQLite_PDO_User_Defined_Functions_Tests.php deleted file mode 100644 index 34740194..00000000 --- a/tests/WP_SQLite_PDO_User_Defined_Functions_Tests.php +++ /dev/null @@ -1,28 +0,0 @@ -= 80400 ? PDO\SQLite::class : PDO::class; - $pdo = new $pdo_class( 'sqlite::memory:' ); - $fns = WP_SQLite_PDO_User_Defined_Functions::register_for( $pdo ); - - $this->assertEquals( - $expected, - $fns->field( ...$args ) - ); - } - - public static function dataProviderForTestFieldFunction() { - return array( - array( 1, array( 'a', 'a' ) ), - array( 2, array( 'User 0000019', 'User 0000018', 'User 0000019', 'User 0000020' ) ), - ); - } -} diff --git a/tests/WP_SQLite_Query_RewriterTests.php b/tests/WP_SQLite_Query_RewriterTests.php deleted file mode 100644 index 57531269..00000000 --- a/tests/WP_SQLite_Query_RewriterTests.php +++ /dev/null @@ -1,81 +0,0 @@ -assertEquals( - 'int', - $r->consume( - array( - 'type' => WP_SQLite_Token::TYPE_KEYWORD, - 'flags' => WP_SQLite_Token::FLAG_KEYWORD_DATA_TYPE, - ) - )->value - ); - $this->assertEquals( - 'DATE_ADD', - $r->consume( - array( - 'type' => WP_SQLite_Token::TYPE_KEYWORD, - 'flags' => WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, - ) - )->value - ); - } - public function testSkip() { - $r = new WP_SQLite_Query_Rewriter( - array( - new WP_SQLite_Token( 'DO', WP_SQLite_Token::TYPE_KEYWORD ), - new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), - new WP_SQLite_Token( 'UPDATE', WP_SQLite_Token::TYPE_KEYWORD ), - new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), - new WP_SQLite_Token( 'SET', WP_SQLite_Token::TYPE_KEYWORD ), - new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), - ) - ); - $this->assertEquals( - 'DO', - $r->skip()->value - ); - $this->assertEquals( - 'UPDATE', - $r->skip()->value - ); - } - - public function skip_over() { - $r = new WP_SQLite_Query_Rewriter( - array( - new WP_SQLite_Token( 'DO', WP_SQLite_Token::TYPE_KEYWORD ), - new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), - new WP_SQLite_Token( 'UPDATE', WP_SQLite_Token::TYPE_KEYWORD ), - new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), - new WP_SQLite_Token( 'SET', WP_SQLite_Token::TYPE_KEYWORD ), - new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), - ) - ); - $buffer = $r->skip_over( - array( - 'values' => array( 'UPDATE' ), - ) - ); - $this->assertCount( 3, $buffer ); - $this->assertEquals( 'DO', $buffer[0]->value ); - $this->assertEquals( ' ', $buffer[1]->value ); - $this->assertEquals( 'UPDATE', $buffer[2]->value ); - } -} diff --git a/tests/WP_SQLite_Query_Tests.php b/tests/WP_SQLite_Query_Tests.php deleted file mode 100644 index 00997631..00000000 --- a/tests/WP_SQLite_Query_Tests.php +++ /dev/null @@ -1,573 +0,0 @@ -= 80400 ? PDO\SQLite::class : PDO::class; - $this->sqlite = new $pdo_class( 'sqlite::memory:' ); - $this->engine = new WP_SQLite_Translator( $this->sqlite ); - - $translator = $this->engine; - - try { - $translator->begin_transaction(); - foreach ( $queries as $query ) { - $query = trim( $query ); - if ( empty( $query ) ) { - continue; - } - - $result = $translator->execute_sqlite_query( $query ); - if ( false === $result ) { - throw new PDOException( $translator->get_error_message() ); - } - } - $translator->commit(); - } catch ( PDOException $err ) { - $err_data = - $err->errorInfo; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $err_code = $err_data[1]; - $translator->rollback(); - $message = sprintf( - 'Error occurred while creating tables or indexes...
Query was: %s
', - var_export( $query, true ) - ); - $message .= sprintf( 'Error message is: %s', $err_data[2] ); - wp_die( $message, 'Database Error!' ); - } - - /* Mock up some metadata rows. When meta_key starts with _, the custom field isn't visible to the editor. */ - for ( $i = 1; $i <= 40; $i++ ) { - $k1 = 'visible_meta_key_' . str_pad( $i, 2, '0', STR_PAD_LEFT ); - $k2 = '_invisible_meta_key_%_percent' . str_pad( $i, 2, '0', STR_PAD_LEFT ); - $this->assertQuery( - "INSERT INTO wp_postmeta (post_id, meta_key, meta_value) VALUES (1, '$k1', '$k1-value');" - ); - $this->assertQuery( - "INSERT INTO wp_postmeta (post_id, meta_key, meta_value) VALUES (1, '$k2', '$k2-value');" - ); - } - - /* Mock up some transients for testing. Site transients. Two expired, one in the future. */ - $time = - 90; - foreach ( array( 'tag1', 'tag2', 'tag3' ) as $tag ) { - $tv = '_site_transient_' . $tag; - $tt = '_site_transient_timeout_' . $tag; - $this->assertQuery( - "INSERT INTO wp_options (option_name, option_value, autoload) VALUES ('$tv', '$tag', 'no');" - ); - $this->assertQuery( - "INSERT INTO wp_options (option_name, option_value, autoload) VALUES ('$tt', UNIX_TIMESTAMP() + $time, 'no');" - ); - $time += 60; - } - /* Ordinary transients. */ - $time = - 90; - foreach ( array( 'tag4', 'tag5', 'tag6' ) as $tag ) { - $tv = '_transient_' . $tag; - $tt = '_transient_timeout_' . $tag; - $this->assertQuery( - "INSERT INTO wp_options (option_name, option_value, autoload) VALUES ('$tv', '$tag', 'no');" - ); - $this->assertQuery( - "INSERT INTO wp_options (option_name, option_value, autoload) VALUES ('$tt', UNIX_TIMESTAMP() + $time, 'no');" - ); - $time += 60; - } - } - - public function testGreatestLeast() { - $q = <<<'QUERY' -SELECT GREATEST('a', 'b') letter; -QUERY; - - $result = $this->assertQuery( $q ); - $actual = $this->engine->get_query_results(); - $this->assertEquals( 1, count( $actual ) ); - $this->assertEquals( 'b', $actual[0]->letter ); - - $q = <<<'QUERY' -SELECT LEAST('a', 'b') letter; -QUERY; - - $result = $this->assertQuery( $q ); - $actual = $this->engine->get_query_results(); - $this->assertEquals( 1, count( $actual ) ); - $this->assertEquals( 'a', $actual[0]->letter ); - - $q = <<<'QUERY' -SELECT GREATEST(2, 1.5) num; -QUERY; - - $result = $this->assertQuery( $q ); - $actual = $this->engine->get_query_results(); - $this->assertEquals( 1, count( $actual ) ); - $this->assertEquals( 2, $actual[0]->num ); - - $q = <<<'QUERY' -SELECT LEAST(2, 1.5, 1.0) num; -QUERY; - - $result = $this->assertQuery( $q ); - $actual = $this->engine->get_query_results(); - $this->assertEquals( 1, count( $actual ) ); - $this->assertEquals( 1, $actual[0]->num ); - } - - public function testLikeEscapingSimpleNoSemicolon() { - $q = <<<'QUERY' -SELECT DISTINCT meta_key FROM wp_postmeta WHERE meta_key LIKE '\_%' -QUERY; - - $result = $this->assertQuery( $q ); - - $actual = $this->engine->get_query_results(); - $this->assertEquals( 40, count( $actual ) ); - } - - public function testLikeEscapingPercent() { - $q = <<<'QUERY' -SELECT DISTINCT meta_key FROM wp_postmeta WHERE meta_key LIKE '%\_\%\_percent%' -QUERY; - - $result = $this->assertQuery( $q ); - - $actual = $this->engine->get_query_results(); - $this->assertEquals( 40, count( $actual ) ); - } - - public function testLikeEscapingSimpleSemicolon() { - $q = <<<'QUERY' -SELECT DISTINCT meta_key FROM wp_postmeta WHERE meta_key LIKE '\_%'; -QUERY; - - $result = $this->assertQuery( $q ); - - $actual = $this->engine->get_query_results(); - $this->assertEquals( 40, count( $actual ) ); - } - - public function testLikeEscapingBasic() { - $q = <<<'QUERY' -SELECT DISTINCT meta_key FROM wp_postmeta WHERE meta_key NOT BETWEEN '_' AND '_z' AND meta_key NOT LIKE '\_%' ORDER BY meta_key LIMIT 30 -QUERY; - - $result = $this->assertQuery( $q ); - - $actual = $this->engine->get_query_results(); - $this->assertEquals( 30, count( $actual ) ); - $last = $actual[ count( $actual ) - 1 ]->meta_key; - $this->assertEquals( 'visible_meta_key_30', $last ); - } - - public function testLikeEscapingParenAfterLike() { - $q = <<<'QUERY' - SELECT DISTINCT meta_key - FROM wp_postmeta - WHERE (meta_key != 'hello' AND meta_key NOT LIKE '\_%') AND meta_id > 0 -QUERY; - - $this->assertQuery( $q ); - - $actual = $this->engine->get_query_results(); - $this->assertEquals( 40, count( $actual ) ); - $last = $actual[ count( $actual ) - 1 ]->meta_key; - $this->assertEquals( 'visible_meta_key_40', $last ); - } - - public function testLikeEscapingWithConcatFunction() { - $q = <<<'QUERY' -SELECT DISTINCT meta_key FROM wp_postmeta WHERE meta_key NOT BETWEEN '_' AND '_z' AND meta_key NOT LIKE CONCAT('\_', '%') ORDER BY meta_key LIMIT 30 -QUERY; - - $result = $this->assertQuery( $q ); - - $actual = $this->engine->get_query_results(); - $this->assertEquals( 30, count( $actual ) ); - $last = $actual[ count( $actual ) - 1 ]->meta_key; - $this->assertEquals( 'visible_meta_key_30', $last ); - } - - // https://github.com/WordPress/sqlite-database-integration/issues/19 - - public function testHavingWithoutGroupBy() { - - $q = <<<'QUERY' -SELECT DISTINCT meta_key FROM wp_postmeta WHERE meta_key NOT BETWEEN '_' AND '_z' HAVING meta_key NOT LIKE '\_%' ORDER BY meta_key LIMIT 30 -QUERY; - - $result = $this->assertQuery( $q ); - - $actual = $this->engine->get_query_results(); - $this->assertEquals( 30, count( $actual ) ); - $last = $actual[ count( $actual ) - 1 ]->meta_key; - $this->assertEquals( 'visible_meta_key_30', $last ); - - $q = <<<'QUERY' -SELECT DISTINCT meta_key FROM wp_postmeta WHERE meta_key NOT BETWEEN '_' AND '_z' HAVING meta_key NOT LIKE CONCAT('\_', '%') ORDER BY meta_key LIMIT 30 -QUERY; - - $result = $this->assertQuery( $q ); - - $actual = $this->engine->get_query_results(); - $this->assertEquals( 30, count( $actual ) ); - $last = $actual[ count( $actual ) - 1 ]->meta_key; - $this->assertEquals( 'visible_meta_key_30', $last ); - } - - public function testCharLengthSimple() { - $query = <<<'QUERY' -SELECT * FROM wp_options WHERE LENGTH(option_name) != CHAR_LENGTH(option_name) -QUERY; - - $this->assertQuery( $query ); - $actual = $this->engine->get_query_results(); - $this->assertEquals( 0, count( $actual ) ); - } - - public function testSubstringSimple() { - $query = <<<'QUERY' -SELECT SUBSTR(option_name, 1) ss1, SUBSTRING(option_name, 1) sstr1, - SUBSTR(option_name, -2) es1, SUBSTRING(option_name, -2) estr1 -FROM wp_options -WHERE SUBSTR(option_name, -2) != SUBSTRING(option_name, -2) - OR SUBSTR(option_name, 1) != SUBSTRING(option_name, 1) -QUERY; - - $this->assertQuery( $query ); - $actual = $this->engine->get_query_results(); - $this->assertEquals( 0, count( $actual ) ); - } - - public function testCharLengthComplex() { - $query = <<<'QUERY' -SELECT option_name, - CHAR_LENGTH( - CASE WHEN option_name LIKE '\_site\_transient\_%' - THEN '_site_transient_' - WHEN option_name LIKE '\_transient\_%' - THEN '_transient_' - ELSE '' END - ) prefix_length, - - SUBSTR(option_name, CHAR_LENGTH( - CASE WHEN option_name LIKE '\_site\_transient\_%' - THEN '_site_transient_' - WHEN option_name LIKE '\_transient\_%' - THEN '_transient_' - ELSE '' END - ) + 1) suffix -FROM wp_options -WHERE option_name LIKE '\_%transient\_%' -AND option_name NOT LIKE '%\_transient\_timeout\_%' -QUERY; - - $this->assertQuery( $query ); - $actual = $this->engine->get_query_results(); - $this->assertEquals( 6, count( $actual ) ); - foreach ( $actual as $row ) { - self::assertTrue( str_ends_with( $row->option_name, '_' . $row->suffix ) ); - } - } - - public function testAllTransients() { - $this->assertQuery( - "SELECT * FROM wp_options WHERE option_name LIKE '\_%transient\_%'" - ); - $actual = $this->engine->get_query_results(); - $this->assertEquals( 12, count( $actual ) ); - } - - public function testExpiredTransients() { - $query = <<<'QUERY' -SELECT a.option_id, a.option_name, a.option_value as option_content, a.autoload, b.option_value as option_timeout, - UNIX_TIMESTAMP() - b.option_value as age, - CONCAT ( - CASE WHEN a.option_name LIKE '\_site\_transient\_%' - THEN '_site_transient_timeout_' - ELSE '_transient_timeout_' - END, - SUBSTRING(a.option_name, CHAR_LENGTH( - CASE WHEN a.option_name LIKE '\_site\_transient\_%' - THEN '_site_transient_' - ELSE '_transient_' - END - ) + 1)) AS timeout_name - - - FROM wp_options a LEFT JOIN wp_options b ON b.option_name = - CONCAT( - CASE WHEN a.option_name LIKE '\_site\_transient\_%' - THEN '_site_transient_timeout_' - ELSE '_transient_timeout_' - END - , - SUBSTRING(a.option_name, CHAR_LENGTH( - CASE WHEN a.option_name LIKE '\_site\_transient\_%' - THEN '_site_transient_' - ELSE '_transient_' - END - ) + 1) - ) - WHERE (a.option_name LIKE '\_transient\_%' OR a.option_name LIKE '\_site\_transient\_%') - AND a.option_name NOT LIKE '%\_transient\_timeout\_%' - AND b.option_value < UNIX_TIMESTAMP() -QUERY; - - $this->assertQuery( $query ); - $actual = $this->engine->get_query_results(); - $this->assertEquals( 4, count( $actual ) ); - foreach ( $actual as $row ) { - self::assertLessThan( time(), $row->option_timeout ); - } - } - - public function testDeleteExpiredNonSiteTransients() { - - $now = time(); - - /* option.php: delete_expired_transients is the source of this query. */ - $query = <<<'QUERY' -DELETE a, b FROM wp_options a, wp_options b -WHERE a.option_name LIKE '\_transient\_%' -AND a.option_name NOT LIKE '\_transient\_timeout_%' -AND b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) ) -AND b.option_value < UNIX_TIMESTAMP() -QUERY; - $this->assertQuery( $query ); - - /* are the expired transients gone? */ - $query = <<<'QUERY' -SELECT a.option_id, a.option_name, a.option_value as option_content, - a.autoload, b.option_value as option_timeout, - UNIX_TIMESTAMP() - b.option_value as age, - CONCAT ( - CASE WHEN a.option_name LIKE '\_site\_transient\_%' - THEN '_site_transient_timeout_' - ELSE '_transient_timeout_' - END, - SUBSTRING(a.option_name, CHAR_LENGTH( - CASE WHEN a.option_name LIKE '\_site\_transient\_%' - THEN '_site_transient_' - ELSE '_transient_' - END - ) + 1)) AS timeout_name - - - FROM wp_options a LEFT JOIN wp_options b ON b.option_name = - CONCAT( - CASE WHEN a.option_name LIKE '\_site\_transient\_%' - THEN '_site_transient_timeout_' - ELSE '_transient_timeout_' - END - , - SUBSTRING(a.option_name, CHAR_LENGTH( - CASE WHEN a.option_name LIKE '\_site\_transient\_%' - THEN '_site_transient_' - ELSE '_transient_' - END - ) + 1) - ) - WHERE (a.option_name LIKE '\_transient\_%' OR a.option_name LIKE '\_site\_transient\_%') - AND a.option_name NOT LIKE '%\_transient\_timeout\_%' -QUERY; - - $this->assertQuery( $query ); - $actual = $this->engine->get_query_results(); - $count_unexpired = 0; - foreach ( $actual as $row ) { - if ( str_starts_with( $row->option_name, '_transient' ) ) { - ++$count_unexpired; - $this->assertGreaterThan( $now, $row->option_timeout ); - } - } - $this->assertEquals( 1, $count_unexpired ); - } - - public function testUserCountsByRole() { - /* commas appear after the LIKE term sometimes, as here. */ - $query = <<<'QUERY' -SELECT COUNT(NULLIF(`meta_value` LIKE '%\"administrator\"%', false)), - COUNT(NULLIF(`meta_value` LIKE '%\"editor\"%', false)), - COUNT(NULLIF(`meta_value` LIKE '%\"author\"%', false)), - COUNT(NULLIF(`meta_value` LIKE '%\"contributor\"%', false)), - COUNT(NULLIF(`meta_value` LIKE '%\"subscriber\"%', false)), - COUNT(NULLIF(`meta_value` = 'a:0:{}', false)), - COUNT(*) -FROM wp_usermeta -INNER JOIN wp_users ON user_id = ID -WHERE meta_key = 'wp_capabilities' - -QUERY; - $this->assertQuery( $query ); - } - - public function testTranscendental() { - $this->markTestIncomplete( 'For some reason sqlite\'s transcendental functions are missing.' ); - $this->assertQuery( 'SELECT 2.0, SQRT(2.0) sqr, SIN(0.5) s;' ); - } - - public function testRecoverSerialized() { - - $obj = array( - 'this' => 'that', - 'that' => array( 'the', 'other', 'thing' ), - 'two' => 2, - 2 => 'two', - 'pi' => pi(), - 'moo' => "Mrs O'Leary's cow!", - ); - $option_name = 'serialized_option'; - $option_value = serialize( $obj ); - $option_value_escaped = $this->engine->get_pdo()->quote( $option_value ); - /* Note well: this is heredoc not nowdoc */ - $insert = <<assertQuery( $insert ); - $get = <<assertQuery( $get ); - - $actual = $this->engine->get_query_results(); - $retrieved_name = $actual[0]->option_name; - $retrieved_string = $actual[0]->option_value; - $this->assertEquals( $option_value, $retrieved_string ); - $unserialized = unserialize( $retrieved_string ); - $this->assertEquals( $obj, $unserialized ); - - ++$obj ['two']; - $obj ['pi'] *= 2; - $option_value = serialize( $obj ); - $option_value_escaped = $this->engine->get_pdo()->quote( $option_value ); - /* Note well: this is heredoc not nowdoc */ - $insert = <<assertQuery( $insert ); - $get = <<assertQuery( $get ); - - $actual = $this->engine->get_query_results(); - $retrieved_string = $actual[0]->option_value; - $this->assertEquals( $option_value, $retrieved_string ); - $unserialized = unserialize( $retrieved_string ); - $this->assertEquals( $obj, $unserialized ); - } - - public function testOnDuplicateKey() { - $this->assertQuery( - 'CREATE TABLE `test` ( - `id` INT PRIMARY KEY, - `text` VARCHAR(255), - );' - ); - // The order is deliberate to test that the query works with the keys in any order. - $this->assertQuery( - 'INSERT INTO test (`text`, `id`) - VALUES ("test", 1) - ON DUPLICATE KEY UPDATE `text` = "test1"' - ); - } - public function testOnDuplicateKeyWithUnnamedKeys() { - $this->assertQuery( - 'CREATE TABLE `test` ( - `id` INT, - `name` VARCHAR(255), - `other` VARCHAR(255), - PRIMARY KEY (id), - UNIQUE KEY (name) - );' - ); - // The order is deliberate to test that the query works with the keys in any order. - $this->assertQuery( - 'INSERT INTO test (`name`, other) - VALUES ("name", "test") - ON DUPLICATE KEY UPDATE `other` = values(other)' - ); - } - - public function testOnCreateTableIfNotExistsWithIndexAdded() { - $this->assertQuery( - 'CREATE TABLE IF not EXISTS `test` ( - `id` INT, - `name` VARCHAR(255), - `other` VARCHAR(255), - PRIMARY KEY (id), - UNIQUE KEY (name) - );' - ); - $this->assertQuery( - 'CREATE TABLE if NOT ExisTS `test` ( - `id` INT, - `name` VARCHAR(255), - `other` VARCHAR(255), - PRIMARY KEY (id), - UNIQUE KEY (name) - );' - ); - } - - public function testShowColumns() { - - $query = 'SHOW COLUMNS FROM wp_posts'; - $this->assertQuery( $query ); - - $actual = $this->engine->get_query_results(); - foreach ( $actual as $row ) { - $this->assertIsObject( $row ); - $this->assertTrue( property_exists( $row, 'Field' ) ); - $this->assertTrue( property_exists( $row, 'Type' ) ); - $this->assertTrue( property_exists( $row, 'Null' ) ); - $this->assertTrue( ( 'NO' === $row->Null ) || ( 'YES' === $row->Null ) ); - $this->assertTrue( property_exists( $row, 'Key' ) ); - $this->assertTrue( property_exists( $row, 'Default' ) ); - } - } - - private function assertQuery( $sql ) { - $retval = $this->engine->query( $sql ); - $this->assertEquals( - '', - $this->engine->get_error_message() - ); - $this->assertNotFalse( - $retval - ); - - return $retval; - } -} diff --git a/tests/WP_SQLite_Translator_Tests.php b/tests/WP_SQLite_Translator_Tests.php deleted file mode 100644 index a3a1dc02..00000000 --- a/tests/WP_SQLite_Translator_Tests.php +++ /dev/null @@ -1,3707 +0,0 @@ -= 80400 ? PDO\SQLite::class : PDO::class; - $this->sqlite = new $pdo_class( 'sqlite::memory:' ); - $this->engine = new WP_SQLite_Translator( $this->sqlite ); - - // Skip all old driver tests when running on legacy SQLite version. - // The old driver is to be removed in favor of the new AST driver, - // so this is just a temporary measure to pass all CI combinations. - $is_legacy_sqlite = version_compare( $this->engine->get_sqlite_version(), '3.37.0', '<' ); - if ( $is_legacy_sqlite ) { - $this->markTestSkipped( "The old SQLite driver doesn't pass some test on legacy SQLite versions" ); - return; - } - - $this->engine->query( - "CREATE TABLE _options ( - ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, - option_name TEXT NOT NULL default '', - option_value TEXT NOT NULL default '' - );" - ); - $this->engine->query( - "CREATE TABLE _dates ( - ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, - option_name TEXT NOT NULL default '', - option_value DATE NOT NULL - );" - ); - } - - private function assertQuery( $sql, $error_substring = null ) { - $retval = $this->engine->query( $sql ); - if ( null === $error_substring ) { - $this->assertEquals( - '', - $this->engine->get_error_message() - ); - $this->assertNotFalse( - $retval - ); - } else { - $this->assertStringContainsStringIgnoringCase( $error_substring, $this->engine->get_error_message() ); - } - - return $retval; - } - - public function testRegexp() { - $this->assertQuery( - "INSERT INTO _options (option_name, option_value) VALUES ('rss_0123456789abcdef0123456789abcdef', '1');" - ); - $this->assertQuery( - "INSERT INTO _options (option_name, option_value) VALUES ('transient', '1');" - ); - - $this->assertQuery( "DELETE FROM _options WHERE option_name REGEXP '^rss_.+$'" ); - $this->assertQuery( 'SELECT * FROM _options' ); - $this->assertCount( 1, $this->engine->get_query_results() ); - } - - /** - * @dataProvider regexpOperators - */ - public function testRegexps( $operator, $regexp, $expected_result ) { - $this->assertQuery( - "INSERT INTO _options (option_name) VALUES ('rss_123'), ('RSS_123'), ('transient');" - ); - - $this->assertQuery( "SELECT ID, option_name FROM _options WHERE option_name $operator '$regexp' ORDER BY id LIMIT 1" ); - $this->assertEquals( - array( $expected_result ), - $this->engine->get_query_results() - ); - } - - public static function regexpOperators() { - $lowercase_rss = (object) array( - 'ID' => '1', - 'option_name' => 'rss_123', - ); - $uppercase_rss = (object) array( - 'ID' => '2', - 'option_name' => 'RSS_123', - ); - $lowercase_transient = (object) array( - 'ID' => '3', - 'option_name' => 'transient', - ); - return array( - array( 'REGEXP', '^RSS_.+$', $lowercase_rss ), - array( 'RLIKE', '^RSS_.+$', $lowercase_rss ), - array( 'REGEXP BINARY', '^RSS_.+$', $uppercase_rss ), - array( 'RLIKE BINARY', '^RSS_.+$', $uppercase_rss ), - array( 'NOT REGEXP', '^RSS_.+$', $lowercase_transient ), - array( 'NOT RLIKE', '^RSS_.+$', $lowercase_transient ), - array( 'NOT REGEXP BINARY', '^RSS_.+$', $lowercase_rss ), - array( 'NOT RLIKE BINARY', '^RSS_.+$', $lowercase_rss ), - ); - } - - public function testInsertDateNow() { - $this->assertQuery( - "INSERT INTO _dates (option_name, option_value) VALUES ('first', now());" - ); - - $this->assertQuery( 'SELECT YEAR(option_value) as y FROM _dates' ); - - $results = $this->engine->get_query_results(); - $this->assertCount( 1, $results ); - $this->assertEquals( gmdate( 'Y' ), $results[0]->y ); - } - - public function testUpdateWithLimit() { - $this->assertQuery( - "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 00:00:45');" - ); - $this->assertQuery( - "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2003-05-28 00:00:45');" - ); - - $this->assertQuery( - "UPDATE _dates SET option_value = '2001-05-27 10:08:48' WHERE option_name = 'first' ORDER BY option_name LIMIT 1;" - ); - - $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first';" ); - $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second';" ); - - $this->assertEquals( '2001-05-27 10:08:48', $result1[0]->option_value ); - $this->assertEquals( '2003-05-28 00:00:45', $result2[0]->option_value ); - - $this->assertQuery( - "UPDATE _dates SET option_value = '2001-05-27 10:08:49' WHERE option_name = 'first';" - ); - $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first';" ); - $this->assertEquals( '2001-05-27 10:08:49', $result1[0]->option_value ); - - $this->assertQuery( - "UPDATE _dates SET option_value = '2001-05-12 10:00:40' WHERE option_name in ( SELECT option_name from _dates );" - ); - $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first';" ); - $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second';" ); - $this->assertEquals( '2001-05-12 10:00:40', $result1[0]->option_value ); - $this->assertEquals( '2001-05-12 10:00:40', $result2[0]->option_value ); - } - - public function testUpdateWithLimitNoEndToken() { - $this->assertQuery( - "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 00:00:45')" - ); - $this->assertQuery( - "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2003-05-28 00:00:45')" - ); - - $this->assertQuery( - "UPDATE _dates SET option_value = '2001-05-27 10:08:48' WHERE option_name = 'first' ORDER BY option_name LIMIT 1" - ); - $results = $this->engine->get_query_results(); - - $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" ); - $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second'" ); - - $this->assertEquals( '2001-05-27 10:08:48', $result1[0]->option_value ); - $this->assertEquals( '2003-05-28 00:00:45', $result2[0]->option_value ); - - $this->assertQuery( - "UPDATE _dates SET option_value = '2001-05-27 10:08:49' WHERE option_name = 'first'" - ); - $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" ); - $this->assertEquals( '2001-05-27 10:08:49', $result1[0]->option_value ); - - $this->assertQuery( - "UPDATE _dates SET option_value = '2001-05-12 10:00:40' WHERE option_name in ( SELECT option_name from _dates )" - ); - $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" ); - $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second'" ); - $this->assertEquals( '2001-05-12 10:00:40', $result1[0]->option_value ); - $this->assertEquals( '2001-05-12 10:00:40', $result2[0]->option_value ); - } - - public function testUpdateWithoutWhereButWithSubSelect() { - $this->assertQuery( - "INSERT INTO _options (option_name, option_value) VALUES ('User 0000019', 'second');" - ); - $this->assertQuery( - "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48');" - ); - $this->assertQuery( - "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2003-05-27 10:08:48');" - ); - $return = $this->assertQuery( - "UPDATE _dates SET option_value = (SELECT option_value from _options WHERE option_name = 'User 0000019')" - ); - $this->assertSame( 2, $return, 'UPDATE query did not return 2 when two row were changed' ); - - $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" ); - $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second'" ); - $this->assertEquals( 'second', $result1[0]->option_value ); - $this->assertEquals( 'second', $result2[0]->option_value ); - } - - public function testUpdateWithoutWhereButWithLimit() { - $this->assertQuery( - "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48');" - ); - $this->assertQuery( - "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2003-05-27 10:08:48');" - ); - $return = $this->assertQuery( - "UPDATE _dates SET option_value = 'second' LIMIT 1" - ); - $this->assertSame( 1, $return, 'UPDATE query did not return 2 when two row were changed' ); - - $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" ); - $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second'" ); - $this->assertEquals( 'second', $result1[0]->option_value ); - $this->assertEquals( '2003-05-27 10:08:48', $result2[0]->option_value ); - } - - public function testCastAsBinary() { - $this->assertQuery( - // Use a confusing alias to make sure it replaces only the correct token - "SELECT CAST('ABC' AS BINARY) as binary;" - ); - $results = $this->engine->get_query_results(); - $this->assertCount( 1, $results ); - $this->assertEquals( 'ABC', $results[0]->binary ); - } - - public function testSelectFromDual() { - $result = $this->assertQuery( - 'SELECT 1 as output FROM DUAL' - ); - $this->assertEquals( 1, $result[0]->output ); - } - - public function testShowCreateTableNotFound() { - $this->assertQuery( - 'SHOW CREATE TABLE _no_such_table;' - ); - $results = $this->engine->get_query_results(); - $this->assertCount( 0, $results ); - } - - public function testShowCreateTable1() { - $this->assertQuery( - "CREATE TABLE _tmp_table ( - ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL, - option_name VARCHAR(255) default '', - option_value TEXT NOT NULL, - UNIQUE KEY option_name (option_name(100)), - KEY composite (option_name(100), option_value(100)) - );" - ); - - $this->assertQuery( - 'SHOW CREATE TABLE _tmp_table;' - ); - $results = $this->engine->get_query_results(); - # TODO: Should we fix mismatch with original `option_value` text NOT NULL,` without default? - $this->assertEquals( - "CREATE TABLE `_tmp_table` ( - `ID` bigint NOT NULL AUTO_INCREMENT, - `option_name` varchar(255) DEFAULT '', - `option_value` text NOT NULL DEFAULT '', - PRIMARY KEY (`ID`), - KEY `composite` (`option_name`(100), `option_value`(100)), - UNIQUE KEY `option_name` (`option_name`(100)) -);", - $results[0]->{'Create Table'} - ); - } - - public function testShowCreateTableWithEmptyDatetimeDefault() { - $this->assertQuery( - "CREATE TABLE _tmp_table ( - ID BIGINT PRIMARY KEY AUTO_INCREMENT, - timestamp1 datetime NOT NULL, - timestamp2 date NOT NULL, - timestamp3 time NOT NULL, - timestamp4 timestamp NOT NULL, - timestamp5 year NOT NULL, - notempty1 datetime DEFAULT '1999-12-12 12:12:12', - notempty2 date DEFAULT '1999-12-12', - notempty3 time DEFAULT '12:12:12', - notempty4 year DEFAULT '2024', - notempty5 timestamp DEFAULT '1734539165', - );" - ); - - $this->assertQuery( - 'SHOW CREATE TABLE _tmp_table;' - ); - $results = $this->engine->get_query_results(); - - $this->assertEquals( - "CREATE TABLE `_tmp_table` ( - `ID` bigint AUTO_INCREMENT, - `timestamp1` datetime NOT NULL, - `timestamp2` date NOT NULL, - `timestamp3` time NOT NULL, - `timestamp4` timestamp NOT NULL, - `timestamp5` year NOT NULL, - `notempty1` datetime DEFAULT '1999-12-12 12:12:12', - `notempty2` date DEFAULT '1999-12-12', - `notempty3` time DEFAULT '12:12:12', - `notempty4` year DEFAULT '2024', - `notempty5` timestamp DEFAULT '1734539165', - PRIMARY KEY (`ID`) -);", - $results[0]->{'Create Table'} - ); - } - - public function testShowCreateTableQuoted() { - $this->assertQuery( - "CREATE TABLE _tmp_table ( - ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL, - option_name VARCHAR(255) default '', - option_value TEXT NOT NULL, - UNIQUE KEY option_name (option_name(100)), - KEY composite (option_name, option_value(100)) - );" - ); - - $this->assertQuery( - 'SHOW CREATE TABLE `_tmp_table`;' - ); - $results = $this->engine->get_query_results(); - # TODO: Should we fix mismatch with original `option_value` text NOT NULL,` without default? - $this->assertEquals( - "CREATE TABLE `_tmp_table` ( - `ID` bigint NOT NULL AUTO_INCREMENT, - `option_name` varchar(255) DEFAULT '', - `option_value` text NOT NULL DEFAULT '', - PRIMARY KEY (`ID`), - KEY `composite` (`option_name`(100), `option_value`(100)), - UNIQUE KEY `option_name` (`option_name`(100)) -);", - $results[0]->{'Create Table'} - ); - } - - public function testShowCreateTableSimpleTable() { - $this->assertQuery( - 'CREATE TABLE _tmp_table ( - ID BIGINT NOT NULL - );' - ); - - $this->assertQuery( - 'SHOW CREATE TABLE _tmp_table;' - ); - $results = $this->engine->get_query_results(); - $this->assertEquals( - 'CREATE TABLE `_tmp_table` ( - `ID` bigint NOT NULL DEFAULT 0 -);', - $results[0]->{'Create Table'} - ); - } - - public function testShowCreateTableWithAlterAndCreateIndex() { - $this->assertQuery( - "CREATE TABLE _tmp_table ( - ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL, - option_name VARCHAR(255) default '', - option_value TEXT NOT NULL - );" - ); - - $this->assertQuery( - 'ALTER TABLE _tmp_table CHANGE COLUMN option_name option_name SMALLINT NOT NULL default 14' - ); - - $this->assertQuery( - 'ALTER TABLE _tmp_table ADD INDEX option_name (option_name);' - ); - - $this->assertQuery( - 'SHOW CREATE TABLE _tmp_table;' - ); - $results = $this->engine->get_query_results(); - $this->assertEquals( - 'CREATE TABLE `_tmp_table` ( - `ID` bigint NOT NULL AUTO_INCREMENT, - `option_name` smallint NOT NULL DEFAULT 14, - `option_value` text NOT NULL DEFAULT \'\', - PRIMARY KEY (`ID`), - KEY `option_name` (`option_name`) -);', - $results[0]->{'Create Table'} - ); - } - - public function testCreateTablseWithIdenticalIndexNames() { - $this->assertQuery( - "CREATE TABLE _tmp_table_a ( - ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL, - option_name VARCHAR(255) default '', - option_value TEXT NOT NULL, - KEY `option_name` (`option_name`(100)), - KEY `double__underscores` (`option_name`(100), `ID`) - );" - ); - - $this->assertQuery( - "CREATE TABLE _tmp_table_b ( - ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL, - option_name VARCHAR(255) default '', - option_value TEXT NOT NULL, - KEY `option_name` (`option_name`(100)), - KEY `double__underscores` (`option_name`(100), `ID`) - );" - ); - } - - public function testShowCreateTablePreservesDoubleUnderscoreKeyNames() { - $this->assertQuery( - "CREATE TABLE _tmp__table ( - ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL, - option_name VARCHAR(255) default '', - option_value TEXT NOT NULL, - KEY `option_name` (`option_name`(100)), - KEY `double__underscores` (`option_name`(100), `ID`) - );" - ); - - $this->assertQuery( - 'SHOW CREATE TABLE _tmp__table;' - ); - $results = $this->engine->get_query_results(); - $this->assertEquals( - 'CREATE TABLE `_tmp__table` ( - `ID` bigint NOT NULL AUTO_INCREMENT, - `option_name` varchar(255) DEFAULT \'\', - `option_value` text NOT NULL DEFAULT \'\', - PRIMARY KEY (`ID`), - KEY `double__underscores` (`option_name`(100), `ID`), - KEY `option_name` (`option_name`(100)) -);', - $results[0]->{'Create Table'} - ); - } - - public function testShowCreateTableLimitsKeyLengths() { - $this->assertQuery( - 'CREATE TABLE _tmp__table ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, - `order_id` bigint(20) unsigned DEFAULT NULL, - `meta_key` varchar(20) DEFAULT NULL, - `meta_value` text DEFAULT NULL, - `meta_data` mediumblob DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `meta_key_value` (`meta_key`(20),`meta_value`(82)), - KEY `order_id_meta_key_meta_value` (`order_id`,`meta_key`(100),`meta_value`(82)), - KEY `order_id_meta_key_meta_data` (`order_id`,`meta_key`(100),`meta_data`(100)) - );' - ); - - $this->assertQuery( - 'SHOW CREATE TABLE _tmp__table;' - ); - $results = $this->engine->get_query_results(); - $this->assertEquals( - 'CREATE TABLE `_tmp__table` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, - `order_id` bigint(20) unsigned DEFAULT NULL, - `meta_key` varchar(20) DEFAULT NULL, - `meta_value` text DEFAULT NULL, - `meta_data` mediumblob DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `order_id_meta_key_meta_data` (`order_id`, `meta_key`(20), `meta_data`(100)), - KEY `order_id_meta_key_meta_value` (`order_id`, `meta_key`(20), `meta_value`(100)), - KEY `meta_key_value` (`meta_key`(20), `meta_value`(100)) -);', - $results[0]->{'Create Table'} - ); - } - - public function testShowCreateTableWithPrimaryKeyColumnsReverseOrdered() { - $this->assertQuery( - 'CREATE TABLE `_tmp_table` ( - `ID_A` BIGINT NOT NULL, - `ID_B` BIGINT NOT NULL, - `ID_C` BIGINT NOT NULL, - PRIMARY KEY (`ID_B`, `ID_A`, `ID_C`) - );' - ); - - $this->assertQuery( - 'SHOW CREATE TABLE _tmp_table;' - ); - $results = $this->engine->get_query_results(); - $this->assertEquals( - 'CREATE TABLE `_tmp_table` ( - `ID_A` bigint NOT NULL DEFAULT 0, - `ID_B` bigint NOT NULL DEFAULT 0, - `ID_C` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`ID_B`, `ID_A`, `ID_C`) -);', - $results[0]->{'Create Table'} - ); - } - - public function testShowCreateTableWithColumnKeys() { - $this->assertQuery( - "CREATE TABLE _tmp_table ( - `ID` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL, - `option_name` varchar(255) DEFAULT '', - `option_value` text NOT NULL DEFAULT '', - KEY _tmp_table__composite (option_name, option_value), - UNIQUE KEY _tmp_table__option_name (option_name) );" - ); - } - - public function testShowCreateTableWithCorrectDefaultValues() { - $this->assertQuery( - "CREATE TABLE _tmp__table ( - ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL, - default_empty_string VARCHAR(255) default '', - null_no_default VARCHAR(255), - );" - ); - - $this->assertQuery( - 'SHOW CREATE TABLE _tmp__table;' - ); - $results = $this->engine->get_query_results(); - $this->assertEquals( - 'CREATE TABLE `_tmp__table` ( - `ID` bigint NOT NULL AUTO_INCREMENT, - `default_empty_string` varchar(255) DEFAULT \'\', - `null_no_default` varchar(255), - PRIMARY KEY (`ID`) -);', - $results[0]->{'Create Table'} - ); - } - - public function testSelectIndexHintForce() { - $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('first');" ); - $result = $this->assertQuery( - 'SELECT 1 as output FROM _options FORCE INDEX (PRIMARY, post_parent) WHERE 1=1' - ); - $this->assertEquals( 1, $result[0]->output ); - } - - public function testSelectIndexHintUseGroup() { - $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('first');" ); - $result = $this->assertQuery( - 'SELECT 1 as output FROM _options USE KEY FOR GROUP BY (PRIMARY, post_parent) WHERE 1=1' - ); - $this->assertEquals( 1, $result[0]->output ); - } - - public function testLeftFunction1Char() { - $result = $this->assertQuery( - 'SELECT LEFT("abc", 1) as output' - ); - $this->assertEquals( 'a', $result[0]->output ); - } - - public function testLeftFunction5Chars() { - $result = $this->assertQuery( - 'SELECT LEFT("Lorem ipsum", 5) as output' - ); - $this->assertEquals( 'Lorem', $result[0]->output ); - } - - public function testLeftFunctionNullString() { - $result = $this->assertQuery( - 'SELECT LEFT(NULL, 5) as output' - ); - $this->assertEquals( null, $result[0]->output ); - } - - public function testLeftFunctionNullLength() { - $result = $this->assertQuery( - 'SELECT LEFT("Test", NULL) as output' - ); - $this->assertEquals( null, $result[0]->output ); - } - - public function testInsertSelectFromDual() { - $result = $this->assertQuery( - 'INSERT INTO _options (option_name, option_value) SELECT "A", "b" FROM DUAL WHERE ( SELECT NULL FROM DUAL ) IS NULL' - ); - $this->assertEquals( 1, $result ); - } - - public function testCreateTemporaryTable() { - $this->assertQuery( - "CREATE TEMPORARY TABLE _tmp_table ( - ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, - option_name TEXT NOT NULL default '', - option_value TEXT NOT NULL default '' - );" - ); - $this->assertQuery( - 'DROP TEMPORARY TABLE _tmp_table;' - ); - } - - public function testShowTablesLike() { - $this->assertQuery( - "CREATE TABLE _tmp_table ( - ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, - option_name TEXT NOT NULL default '', - option_value TEXT NOT NULL default '' - );" - ); - $this->assertQuery( - "CREATE TABLE _tmp_table_2 ( - ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, - option_name TEXT NOT NULL default '', - option_value TEXT NOT NULL default '' - );" - ); - - $this->assertQuery( - "SHOW TABLES LIKE '_tmp_table';" - ); - $this->assertEquals( - array( - (object) array( - 'Tables_in_db' => '_tmp_table', - ), - ), - $this->engine->get_query_results() - ); - } - - public function testShowTableStatusFrom() { - // Created in setUp() function - $this->assertQuery( 'DROP TABLE _options' ); - $this->assertQuery( 'DROP TABLE _dates' ); - - $this->assertQuery( - "CREATE TABLE _tmp_table ( - ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, - option_name TEXT NOT NULL default '', - option_value TEXT NOT NULL default '' - );" - ); - - $this->assertQuery( - "SHOW TABLE STATUS FROM 'mydb';" - ); - - $this->assertCount( - 1, - $this->engine->get_query_results() - ); - } - - public function testShowTableStatusIn() { - // Created in setUp() function - $this->assertQuery( 'DROP TABLE _options' ); - $this->assertQuery( 'DROP TABLE _dates' ); - - $this->assertQuery( - "CREATE TABLE _tmp_table ( - ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, - option_name TEXT NOT NULL default '', - option_value TEXT NOT NULL default '' - );" - ); - - $this->assertQuery( - "SHOW TABLE STATUS IN 'mydb';" - ); - - $this->assertCount( - 1, - $this->engine->get_query_results() - ); - } - - public function testShowTableStatusInTwoTables() { - // Created in setUp() function - $this->assertQuery( 'DROP TABLE _options' ); - $this->assertQuery( 'DROP TABLE _dates' ); - - $this->assertQuery( - "CREATE TABLE _tmp_table ( - ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, - option_name TEXT NOT NULL default '', - option_value TEXT NOT NULL default '' - );" - ); - - $this->assertQuery( - "CREATE TABLE _tmp_table2 ( - ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, - option_name TEXT NOT NULL default '', - option_value TEXT NOT NULL default '' - );" - ); - $this->assertQuery( - "SHOW TABLE STATUS IN 'mydb';" - ); - - $this->assertCount( - 2, - $this->engine->get_query_results() - ); - } - - public function testShowTableStatusLike() { - // Created in setUp() function - $this->assertQuery( 'DROP TABLE _options' ); - $this->assertQuery( 'DROP TABLE _dates' ); - - $this->assertQuery( - "CREATE TABLE _tmp_table1 ( - ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, - option_name TEXT NOT NULL default '', - option_value TEXT NOT NULL default '' - );" - ); - - $this->assertQuery( - "CREATE TABLE _tmp_table2 ( - ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, - option_name TEXT NOT NULL default '', - option_value TEXT NOT NULL default '' - );" - ); - - $this->assertQuery( - "SHOW TABLE STATUS LIKE '_tmp_table%';" - ); - $this->assertCount( - 2, - $this->engine->get_query_results() - ); - $this->assertEquals( - '_tmp_table1', - $this->engine->get_query_results()[0]->Name - ); - } - - public function testCreateTable() { - $result = $this->assertQuery( - "CREATE TABLE wptests_users ( - ID bigint(20) unsigned NOT NULL auto_increment, - user_login varchar(60) NOT NULL default '', - user_pass varchar(255) NOT NULL default '', - user_nicename varchar(50) NOT NULL default '', - user_email varchar(100) NOT NULL default '', - user_url varchar(100) NOT NULL default '', - user_registered datetime NOT NULL default '0000-00-00 00:00:00', - user_activation_key varchar(255) NOT NULL default '', - user_status int(11) NOT NULL default '0', - display_name varchar(250) NOT NULL default '', - PRIMARY KEY (ID), - KEY user_login_key (user_login), - KEY user_nicename (user_nicename), - KEY user_email (user_email) - ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci" - ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - - $this->assertQuery( 'DESCRIBE wptests_users;' ); - $results = $this->engine->get_query_results(); - $this->assertEquals( - array( - (object) array( - 'Field' => 'ID', - 'Type' => 'bigint(20) unsigned', - 'Null' => 'NO', - 'Key' => 'PRI', - 'Default' => '0', - 'Extra' => '', - ), - (object) array( - 'Field' => 'user_login', - 'Type' => 'varchar(60)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - (object) array( - 'Field' => 'user_pass', - 'Type' => 'varchar(255)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - (object) array( - 'Field' => 'user_nicename', - 'Type' => 'varchar(50)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - (object) array( - 'Field' => 'user_email', - 'Type' => 'varchar(100)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - (object) array( - 'Field' => 'user_url', - 'Type' => 'varchar(100)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - (object) array( - 'Field' => 'user_registered', - 'Type' => 'datetime', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '0000-00-00 00:00:00', - 'Extra' => '', - ), - (object) array( - 'Field' => 'user_activation_key', - 'Type' => 'varchar(255)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - (object) array( - 'Field' => 'user_status', - 'Type' => 'int(11)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '0', - 'Extra' => '', - ), - (object) array( - 'Field' => 'display_name', - 'Type' => 'varchar(250)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - ), - $results - ); - } - - public function testCreateTableWithTrailingComma() { - $result = $this->assertQuery( - 'CREATE TABLE wptests_users ( - ID bigint(20) unsigned NOT NULL auto_increment, - PRIMARY KEY (ID), - ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci' - ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - } - - public function testCreateTableSpatialIndex() { - $result = $this->assertQuery( - 'CREATE TABLE wptests_users ( - ID bigint(20) unsigned NOT NULL auto_increment, - UNIQUE KEY (ID), - )' - ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - } - - public function testCreateTableWithMultiValueColumnTypeModifiers() { - $result = $this->assertQuery( - "CREATE TABLE wptests_users ( - ID bigint(20) unsigned NOT NULL auto_increment, - decimal_column DECIMAL(10,2) NOT NULL DEFAULT 0, - float_column FLOAT(10,2) NOT NULL DEFAULT 0, - enum_column ENUM('a', 'b', 'c') NOT NULL DEFAULT 'a', - PRIMARY KEY (ID), - )" - ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - - $this->assertQuery( 'DESCRIBE wptests_users;' ); - $results = $this->engine->get_query_results(); - $this->assertEquals( - array( - (object) array( - 'Field' => 'ID', - 'Type' => 'bigint(20) unsigned', - 'Null' => 'NO', - 'Key' => 'PRI', - 'Default' => '0', - 'Extra' => '', - ), - (object) array( - 'Field' => 'decimal_column', - 'Type' => 'decimal(10,2)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => 0, - 'Extra' => '', - ), - (object) array( - 'Field' => 'float_column', - 'Type' => 'float(10,2)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => 0, - 'Extra' => '', - ), - (object) array( - 'Field' => 'enum_column', - 'Type' => "enum('a','b','c')", - 'Null' => 'NO', - 'Key' => '', - 'Default' => 'a', - 'Extra' => '', - ), - ), - $results - ); - } - - public function testAlterTableAddAndDropColumn() { - $result = $this->assertQuery( - "CREATE TABLE _tmp_table ( - name varchar(20) NOT NULL default '' - );" - ); - - $result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD COLUMN `column` int;' ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - - $this->assertQuery( 'DESCRIBE _tmp_table;' ); - $results = $this->engine->get_query_results(); - $this->assertEquals( - array( - (object) array( - 'Field' => 'name', - 'Type' => 'varchar(20)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - (object) array( - 'Field' => 'column', - 'Type' => 'int', - 'Null' => 'YES', - 'Key' => '', - 'Default' => null, - 'Extra' => '', - ), - ), - $results - ); - - $result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD `column2` int;' ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - - $this->assertQuery( 'DESCRIBE _tmp_table;' ); - $results = $this->engine->get_query_results(); - $this->assertEquals( - array( - (object) array( - 'Field' => 'name', - 'Type' => 'varchar(20)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - (object) array( - 'Field' => 'column', - 'Type' => 'int', - 'Null' => 'YES', - 'Key' => '', - 'Default' => null, - 'Extra' => '', - ), - (object) array( - 'Field' => 'column2', - 'Type' => 'int', - 'Null' => 'YES', - 'Key' => '', - 'Default' => null, - 'Extra' => '', - ), - ), - $results - ); - - $result = $this->assertQuery( 'ALTER TABLE _tmp_table DROP COLUMN `column`;' ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - - $this->assertQuery( 'DESCRIBE _tmp_table;' ); - $results = $this->engine->get_query_results(); - $this->assertEquals( - array( - (object) array( - 'Field' => 'name', - 'Type' => 'varchar(20)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - (object) array( - 'Field' => 'column2', - 'Type' => 'int', - 'Null' => 'YES', - 'Key' => '', - 'Default' => null, - 'Extra' => '', - ), - ), - $results - ); - - $result = $this->assertQuery( 'ALTER TABLE _tmp_table DROP `column2`;' ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - - $this->assertQuery( 'DESCRIBE _tmp_table;' ); - $results = $this->engine->get_query_results(); - $this->assertEquals( - array( - (object) array( - 'Field' => 'name', - 'Type' => 'varchar(20)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - ), - $results - ); - } - - public function testAlterTableAddNotNullVarcharColumn() { - $result = $this->assertQuery( - "CREATE TABLE _tmp_table ( - name varchar(20) NOT NULL default '' - );" - ); - - $result = $this->assertQuery( "ALTER TABLE _tmp_table ADD COLUMN `column` VARCHAR(20) NOT NULL DEFAULT 'foo';" ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - - $this->assertQuery( 'DESCRIBE _tmp_table;' ); - $results = $this->engine->get_query_results(); - $this->assertEquals( - array( - (object) array( - 'Field' => 'name', - 'Type' => 'varchar(20)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - (object) array( - 'Field' => 'column', - 'Type' => 'varchar(20)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => 'foo', - 'Extra' => '', - ), - ), - $results - ); - } - - public function testColumnWithOnUpdate() { - // CREATE TABLE with ON UPDATE - $this->assertQuery( - 'CREATE TABLE _tmp_table ( - id int(11) NOT NULL, - created_at timestamp NULL ON UPDATE CURRENT_TIMESTAMP - );' - ); - $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); - $this->assertEquals( - array( - (object) array( - 'Field' => 'id', - 'Type' => 'int(11)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '0', - 'Extra' => '', - ), - (object) array( - 'Field' => 'created_at', - 'Type' => 'timestamp', - 'Null' => 'YES', - 'Key' => '', - 'Default' => null, - 'Extra' => '', - ), - ), - $results - ); - - // ADD COLUMN with ON UPDATE - $this->assertQuery( - 'ALTER TABLE _tmp_table ADD COLUMN updated_at timestamp NULL ON UPDATE CURRENT_TIMESTAMP' - ); - $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); - $this->assertEquals( - array( - (object) array( - 'Field' => 'id', - 'Type' => 'int(11)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '0', - 'Extra' => '', - ), - (object) array( - 'Field' => 'created_at', - 'Type' => 'timestamp', - 'Null' => 'YES', - 'Key' => '', - 'Default' => null, - 'Extra' => '', - ), - (object) array( - 'Field' => 'updated_at', - 'Type' => 'timestamp', - 'Null' => 'YES', - 'Key' => '', - 'Default' => null, - 'Extra' => '', - ), - ), - $results - ); - - // assert ON UPDATE triggers - $results = $this->assertQuery( "SELECT * FROM sqlite_master WHERE type = 'trigger'" ); - $this->assertEquals( - array( - (object) array( - 'type' => 'trigger', - 'name' => '___tmp_table_created_at_on_update__', - 'tbl_name' => '_tmp_table', - 'rootpage' => '0', - 'sql' => "CREATE TRIGGER `___tmp_table_created_at_on_update__`\n\t\t\tAFTER UPDATE ON `_tmp_table`\n\t\t\tFOR EACH ROW\n\t\t\tBEGIN\n\t\t\t UPDATE `_tmp_table` SET `created_at` = CURRENT_TIMESTAMP WHERE rowid = NEW.rowid;\n\t\t\tEND", - ), - (object) array( - 'type' => 'trigger', - 'name' => '___tmp_table_updated_at_on_update__', - 'tbl_name' => '_tmp_table', - 'rootpage' => '0', - 'sql' => "CREATE TRIGGER `___tmp_table_updated_at_on_update__`\n\t\t\tAFTER UPDATE ON `_tmp_table`\n\t\t\tFOR EACH ROW\n\t\t\tBEGIN\n\t\t\t UPDATE `_tmp_table` SET `updated_at` = CURRENT_TIMESTAMP WHERE rowid = NEW.rowid;\n\t\t\tEND", - ), - ), - $results - ); - - // on INSERT, no timestamps are expected - $this->assertQuery( 'INSERT INTO _tmp_table (id) VALUES (1)' ); - $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 1' ); - $this->assertNull( $result[0]->created_at ); - $this->assertNull( $result[0]->updated_at ); - - // on UPDATE, we expect timestamps in form YYYY-MM-DD HH:MM:SS - $this->assertQuery( 'UPDATE _tmp_table SET id = 2 WHERE id = 1' ); - $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 2' ); - $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->created_at ); - $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->updated_at ); - - // drop ON UPDATE - $this->assertQuery( - 'ALTER TABLE _tmp_table - CHANGE created_at created_at timestamp NULL, - CHANGE COLUMN updated_at updated_at timestamp NULL' - ); - $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); - $this->assertEquals( - array( - (object) array( - 'Field' => 'id', - 'Type' => 'int(11)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '0', - 'Extra' => '', - ), - (object) array( - 'Field' => 'created_at', - 'Type' => 'timestamp', - 'Null' => 'YES', - 'Key' => '', - 'Default' => null, - 'Extra' => '', - ), - (object) array( - 'Field' => 'updated_at', - 'Type' => 'timestamp', - 'Null' => 'YES', - 'Key' => '', - 'Default' => null, - 'Extra' => '', - ), - ), - $results - ); - - // assert ON UPDATE triggers are removed - $results = $this->assertQuery( "SELECT * FROM sqlite_master WHERE type = 'trigger'" ); - $this->assertEquals( array(), $results ); - - // now, no timestamps are expected - $this->assertQuery( 'INSERT INTO _tmp_table (id) VALUES (10)' ); - $this->assertQuery( 'UPDATE _tmp_table SET id = 11 WHERE id = 10' ); - $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 11' ); - $this->assertNull( $result[0]->created_at ); - $this->assertNull( $result[0]->updated_at ); - } - - public function testDataTypeKeywordsAsKeyNames() { - // CREATE TABLE with a data type as a key name - $this->assertQuery( - 'CREATE TABLE `_tmp_table` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, - `timestamp` datetime NOT NULL, - PRIMARY KEY (`id`), - KEY `timestamp` (`timestamp`), - );' - ); - $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); - $this->assertEquals( - array( - (object) array( - 'Field' => 'id', - 'Type' => 'bigint(20) unsigned', - 'Null' => 'NO', - 'Key' => 'PRI', - 'Default' => '0', - 'Extra' => '', - ), - (object) array( - 'Field' => 'timestamp', - 'Type' => 'datetime', - 'Null' => 'NO', - 'Key' => '', - 'Default' => null, - 'Extra' => '', - ), - ), - $results - ); - } - - - public function testReservedKeywordsAsFieldNames() { - // CREATE TABLE with a reserved keyword as a field name - $this->assertQuery( - 'CREATE TABLE `_tmp_table` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, - `INDEX` timestamp, - PRIMARY KEY (`id`) - );' - ); - $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); - $this->assertEquals( - array( - (object) array( - 'Field' => 'id', - 'Type' => 'bigint(20) unsigned', - 'Null' => 'NO', - 'Key' => 'PRI', - 'Default' => '0', - 'Extra' => '', - ), - (object) array( - 'Field' => 'INDEX', - 'Type' => 'timestamp', - 'Null' => 'YES', - 'Key' => '', - 'Default' => null, - 'Extra' => '', - ), - ), - $results - ); - } - - public function testColumnWithOnUpdateAndNoIdField() { - // CREATE TABLE with ON UPDATE - $this->assertQuery( - 'CREATE TABLE _tmp_table ( - name varchar(20) NOT NULL, - created_at timestamp NULL ON UPDATE CURRENT_TIMESTAMP - );' - ); - - // on INSERT, no timestamps are expected - $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('aaa')" ); - $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name = 'aaa'" ); - $this->assertNull( $result[0]->created_at ); - - // on UPDATE, we expect timestamps in form YYYY-MM-DD HH:MM:SS - $this->assertQuery( "UPDATE _tmp_table SET name = 'bbb' WHERE name = 'aaa'" ); - $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name = 'bbb'" ); - $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->created_at ); - } - - public function testColumnWithOnUpdateAndAutoincrementPrimaryKey() { - // CREATE TABLE with ON UPDATE, AUTO_INCREMENT, and PRIMARY KEY - $this->assertQuery( - 'CREATE TABLE _tmp_table ( - id int(11) NOT NULL AUTO_INCREMENT, - created_at timestamp NULL ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (id) - );' - ); - - // on INSERT, no timestamps are expected - $this->assertQuery( 'INSERT INTO _tmp_table (id) VALUES (1)' ); - $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 1' ); - $this->assertNull( $result[0]->created_at ); - - // on UPDATE, we expect timestamps in form YYYY-MM-DD HH:MM:SS - $this->assertQuery( 'UPDATE _tmp_table SET id = 2 WHERE id = 1' ); - $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 2' ); - $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->created_at ); - } - - public function testChangeColumnWithOnUpdate() { - // CREATE TABLE with ON UPDATE - $this->assertQuery( - 'CREATE TABLE _tmp_table ( - id int(11) NOT NULL, - created_at timestamp NULL - );' - ); - $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); - $this->assertEquals( - array( - (object) array( - 'Field' => 'id', - 'Type' => 'int(11)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '0', - 'Extra' => '', - ), - (object) array( - 'Field' => 'created_at', - 'Type' => 'timestamp', - 'Null' => 'YES', - 'Key' => '', - 'Default' => null, - 'Extra' => '', - ), - ), - $results - ); - - // no ON UPDATE is set - $this->assertQuery( 'INSERT INTO _tmp_table (id) VALUES (1)' ); - $this->assertQuery( 'UPDATE _tmp_table SET id = 1 WHERE id = 1' ); - $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 1' ); - $this->assertNull( $result[0]->created_at ); - - // CHANGE COLUMN to add ON UPDATE - $this->assertQuery( - 'ALTER TABLE _tmp_table CHANGE COLUMN created_at created_at timestamp NULL ON UPDATE CURRENT_TIMESTAMP' - ); - $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); - $this->assertEquals( - array( - (object) array( - 'Field' => 'id', - 'Type' => 'int(11)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '0', - 'Extra' => '', - ), - (object) array( - 'Field' => 'created_at', - 'Type' => 'timestamp', - 'Null' => 'YES', - 'Key' => '', - 'Default' => null, - 'Extra' => '', - ), - ), - $results - ); - - // now, ON UPDATE SHOULD BE SET - $this->assertQuery( 'UPDATE _tmp_table SET id = 1 WHERE id = 1' ); - $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 1' ); - $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->created_at ); - - // change column to remove ON UPDATE - $this->assertQuery( - 'ALTER TABLE _tmp_table CHANGE COLUMN created_at created_at timestamp NULL' - ); - $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); - $this->assertEquals( - array( - (object) array( - 'Field' => 'id', - 'Type' => 'int(11)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '0', - 'Extra' => '', - ), - (object) array( - 'Field' => 'created_at', - 'Type' => 'timestamp', - 'Null' => 'YES', - 'Key' => '', - 'Default' => null, - 'Extra' => '', - ), - ), - $results - ); - - // now, no timestamp is expected - $this->assertQuery( 'INSERT INTO _tmp_table (id) VALUES (2)' ); - $this->assertQuery( 'UPDATE _tmp_table SET id = 2 WHERE id = 2' ); - $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 2' ); - $this->assertNull( $result[0]->created_at ); - } - - public function testAlterTableWithColumnFirstAndAfter() { - $this->assertQuery( - "CREATE TABLE _tmp_table ( - id int(11) NOT NULL, - name varchar(20) NOT NULL default '' - );" - ); - - // ADD COLUMN with FIRST - $this->assertQuery( - "ALTER TABLE _tmp_table ADD COLUMN new_first_column VARCHAR(255) NOT NULL DEFAULT '' FIRST" - ); - $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); - $this->assertEquals( - array( - (object) array( - 'Field' => 'id', - 'Type' => 'int(11)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '0', - 'Extra' => '', - ), - (object) array( - 'Field' => 'name', - 'Type' => 'varchar(20)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => null, - 'Extra' => '', - ), - (object) array( - 'Field' => 'new_first_column', - 'Type' => 'varchar(255)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - ), - $results - ); - - // ADD COLUMN with AFTER - $this->assertQuery( - "ALTER TABLE _tmp_table ADD COLUMN new_column VARCHAR(255) NOT NULL DEFAULT '' AFTER id" - ); - $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); - $this->assertEquals( - array( - (object) array( - 'Field' => 'id', - 'Type' => 'int(11)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '0', - 'Extra' => '', - ), - (object) array( - 'Field' => 'name', - 'Type' => 'varchar(20)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => null, - 'Extra' => '', - ), - (object) array( - 'Field' => 'new_first_column', - 'Type' => 'varchar(255)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - (object) array( - 'Field' => 'new_column', - 'Type' => 'varchar(255)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - ), - $results - ); - - // CHANGE with FIRST - $this->assertQuery( - "ALTER TABLE _tmp_table CHANGE id id int(11) NOT NULL DEFAULT '0' FIRST" - ); - $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); - $this->assertEquals( - array( - (object) array( - 'Field' => 'id', - 'Type' => 'int(11)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '0', - 'Extra' => '', - ), - (object) array( - 'Field' => 'name', - 'Type' => 'varchar(20)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => null, - 'Extra' => '', - ), - (object) array( - 'Field' => 'new_first_column', - 'Type' => 'varchar(255)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - (object) array( - 'Field' => 'new_column', - 'Type' => 'varchar(255)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - ), - $results - ); - - // CHANGE with AFTER - $this->assertQuery( - "ALTER TABLE _tmp_table CHANGE id id int(11) NOT NULL DEFAULT '0' AFTER name" - ); - $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); - $this->assertEquals( - array( - (object) array( - 'Field' => 'id', - 'Type' => 'int(11)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '0', - 'Extra' => '', - ), - (object) array( - 'Field' => 'name', - 'Type' => 'varchar(20)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => null, - 'Extra' => '', - ), - (object) array( - 'Field' => 'new_first_column', - 'Type' => 'varchar(255)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - (object) array( - 'Field' => 'new_column', - 'Type' => 'varchar(255)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - ), - $results - ); - } - - public function testAlterTableWithMultiColumnFirstAndAfter() { - $this->assertQuery( - 'CREATE TABLE _tmp_table ( - id int(11) NOT NULL - );' - ); - - // ADD COLUMN - $this->assertQuery( - 'ALTER TABLE _tmp_table - ADD COLUMN new1 varchar(255) NOT NULL, - ADD COLUMN new2 varchar(255) NOT NULL FIRST, - ADD COLUMN new3 varchar(255) NOT NULL AFTER new1' - ); - $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); - $this->assertEquals( - array( - (object) array( - 'Field' => 'id', - 'Type' => 'int(11)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '0', - 'Extra' => '', - ), - (object) array( - 'Field' => 'new1', - 'Type' => 'varchar(255)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => null, - 'Extra' => '', - ), - (object) array( - 'Field' => 'new2', - 'Type' => 'varchar(255)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - (object) array( - 'Field' => 'new3', - 'Type' => 'varchar(255)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - ), - $results - ); - - // CHANGE - $this->assertQuery( - 'ALTER TABLE _tmp_table - CHANGE new1 new1 int(11) NOT NULL FIRST, - CHANGE new2 new2 int(11) NOT NULL, - CHANGE new3 new3 int(11) NOT NULL AFTER new2' - ); - $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); - $this->assertEquals( - array( - (object) array( - 'Field' => 'id', - 'Type' => 'int(11)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '0', - 'Extra' => '', - ), - (object) array( - 'Field' => 'new1', - 'Type' => 'int(11)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => null, - 'Extra' => '', - ), - (object) array( - 'Field' => 'new2', - 'Type' => 'int(11)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - (object) array( - 'Field' => 'new3', - 'Type' => 'int(11)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - ), - $results - ); - } - - public function testAlterTableAddIndex() { - $result = $this->assertQuery( - "CREATE TABLE _tmp_table ( - name varchar(20) NOT NULL default '' - );" - ); - - $result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD INDEX name (name);' ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - - $this->assertQuery( 'SHOW INDEX FROM _tmp_table;' ); - $results = $this->engine->get_query_results(); - $this->assertEquals( - array( - (object) array( - 'Table' => '_tmp_table', - 'Non_unique' => '1', - 'Key_name' => 'name', - 'Seq_in_index' => '0', - 'Column_name' => 'name', - 'Collation' => 'A', - 'Cardinality' => '0', - 'Sub_part' => null, - 'Packed' => null, - 'Null' => '', - 'Index_type' => 'BTREE', - 'Comment' => '', - 'Index_comment' => '', - ), - ), - $results - ); - } - - public function testAlterTableAddUniqueIndex() { - $result = $this->assertQuery( - "CREATE TABLE _tmp_table ( - name varchar(20) NOT NULL default '' - );" - ); - - $result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD UNIQUE INDEX name (name(20));' ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - - $this->assertQuery( 'SHOW INDEX FROM _tmp_table;' ); - $results = $this->engine->get_query_results(); - $this->assertEquals( - array( - (object) array( - 'Table' => '_tmp_table', - 'Non_unique' => '0', - 'Key_name' => 'name', - 'Seq_in_index' => '0', - 'Column_name' => 'name', - 'Collation' => 'A', - 'Cardinality' => '0', - 'Sub_part' => null, - 'Packed' => null, - 'Null' => '', - 'Index_type' => 'BTREE', - 'Comment' => '', - 'Index_comment' => '', - ), - ), - $results - ); - } - - public function testAlterTableAddFulltextIndex() { - $result = $this->assertQuery( - "CREATE TABLE _tmp_table ( - name varchar(20) NOT NULL default '' - );" - ); - - $result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD FULLTEXT INDEX name (name);' ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - - $this->assertQuery( 'SHOW INDEX FROM _tmp_table;' ); - $results = $this->engine->get_query_results(); - $this->assertEquals( - array( - (object) array( - 'Table' => '_tmp_table', - 'Non_unique' => '1', - 'Key_name' => 'name', - 'Seq_in_index' => '0', - 'Column_name' => 'name', - 'Collation' => 'A', - 'Cardinality' => '0', - 'Sub_part' => null, - 'Packed' => null, - 'Null' => '', - 'Index_type' => 'FULLTEXT', - 'Comment' => '', - 'Index_comment' => '', - ), - ), - $results - ); - } - - public function testAlterTableModifyColumn() { - $this->assertQuery( - "CREATE TABLE _tmp_table ( - ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, - name varchar(20) NOT NULL default '', - lastname varchar(20) NOT NULL default '', - KEY composite (name, lastname), - UNIQUE KEY name (name) - );" - ); - // Insert a record - $result = $this->assertQuery( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (1, 'Johnny', 'Appleseed');" ); - $this->assertEquals( 1, $result ); - - // Primary key violation: - $result = $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (1, 'Mike', 'Pearseed');" ); - $this->assertEquals( false, $result ); - - // Unique constraint violation: - $result = $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (2, 'Johnny', 'Appleseed');" ); - $this->assertEquals( false, $result ); - - // Rename the "name" field to "firstname": - $result = $this->engine->query( "ALTER TABLE _tmp_table CHANGE column name firstname varchar(50) NOT NULL default 'mark';" ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - - // Confirm the original data is still there: - $result = $this->engine->query( 'SELECT * FROM _tmp_table;' ); - $this->assertCount( 1, $result ); - $this->assertEquals( 1, $result[0]->ID ); - $this->assertEquals( 'Johnny', $result[0]->firstname ); - $this->assertEquals( 'Appleseed', $result[0]->lastname ); - - // Confirm the primary key is intact: - $result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname) VALUES (1, 'Mike', 'Pearseed');" ); - $this->assertEquals( false, $result ); - - // Confirm the unique key is intact: - $result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname) VALUES (2, 'Johnny', 'Appleseed');" ); - $this->assertEquals( false, $result ); - - // Confirm the autoincrement still works: - $result = $this->engine->query( "INSERT INTO _tmp_table (firstname, lastname) VALUES ('John', 'Doe');" ); - $this->assertEquals( true, $result ); - $result = $this->engine->query( "SELECT * FROM _tmp_table WHERE firstname='John';" ); - $this->assertCount( 1, $result ); - $this->assertEquals( 2, $result[0]->ID ); - } - - - public function testAlterTableModifyColumnWithSkippedColumnKeyword() { - $this->assertQuery( - "CREATE TABLE _tmp_table ( - ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, - name varchar(20) NOT NULL default '', - lastname varchar(20) NOT NULL default '', - KEY composite (name, lastname), - UNIQUE KEY name (name) - );" - ); - // Insert a record - $result = $this->assertQuery( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (1, 'Johnny', 'Appleseed');" ); - $this->assertEquals( 1, $result ); - - // Primary key violation: - $result = $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (1, 'Mike', 'Pearseed');" ); - $this->assertEquals( false, $result ); - - // Unique constraint violation: - $result = $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (2, 'Johnny', 'Appleseed');" ); - $this->assertEquals( false, $result ); - - // Rename the "name" field to "firstname": - $result = $this->engine->query( "ALTER TABLE _tmp_table CHANGE name firstname varchar(50) NOT NULL default 'mark';" ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - - // Confirm the original data is still there: - $result = $this->engine->query( 'SELECT * FROM _tmp_table;' ); - $this->assertCount( 1, $result ); - $this->assertEquals( 1, $result[0]->ID ); - $this->assertEquals( 'Johnny', $result[0]->firstname ); - $this->assertEquals( 'Appleseed', $result[0]->lastname ); - - // Confirm the primary key is intact: - $result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname) VALUES (1, 'Mike', 'Pearseed');" ); - $this->assertEquals( false, $result ); - - // Confirm the unique key is intact: - $result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname) VALUES (2, 'Johnny', 'Appleseed');" ); - $this->assertEquals( false, $result ); - - // Confirm the autoincrement still works: - $result = $this->engine->query( "INSERT INTO _tmp_table (firstname, lastname) VALUES ('John', 'Doe');" ); - $this->assertEquals( true, $result ); - $result = $this->engine->query( "SELECT * FROM _tmp_table WHERE firstname='John';" ); - $this->assertCount( 1, $result ); - $this->assertEquals( 2, $result[0]->ID ); - } - - public function testAlterTableModifyColumnWithHyphens() { - $result = $this->assertQuery( - 'CREATE TABLE wptests_dbdelta_test2 ( - `foo-bar` varchar(255) DEFAULT NULL - )' - ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - - $result = $this->assertQuery( - 'ALTER TABLE wptests_dbdelta_test2 CHANGE COLUMN `foo-bar` `foo-bar` text DEFAULT NULL' - ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - - $result = $this->assertQuery( 'DESCRIBE wptests_dbdelta_test2;' ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertNotFalse( $result ); - $this->assertEquals( - array( - (object) array( - 'Field' => 'foo-bar', - 'Type' => 'text', - 'Null' => 'YES', - 'Key' => '', - 'Default' => 'NULL', - 'Extra' => '', - ), - ), - $result - ); - } - - public function testAlterTableModifyColumnComplexChange() { - $result = $this->assertQuery( - "CREATE TABLE _tmp_table ( - ID INTEGER NOT NULL, - name varchar(20) NOT NULL default '', - lastname varchar(20) default '', - date_as_string varchar(20) default '', - PRIMARY KEY (ID, name) - );" - ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - - // Add a unique index - $result = $this->assertQuery( - 'ALTER TABLE _tmp_table ADD UNIQUE INDEX "test_unique_composite" (name, lastname);' - ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - - // Add a regular index - $result = $this->assertQuery( - 'ALTER TABLE _tmp_table ADD INDEX "test_regular" (lastname);' - ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - - // Confirm the table is well-behaved so far: - - // Insert a few records - $result = $this->assertQuery( - " - INSERT INTO _tmp_table (ID, name, lastname, date_as_string) - VALUES - (1, 'Johnny', 'Appleseed', '2002-01-01 12:53:13'), - (2, 'Mike', 'Foo', '2003-01-01 12:53:13'), - (3, 'Kate', 'Bar', '2004-01-01 12:53:13'), - (4, 'Anna', 'Pear', '2005-01-01 12:53:13') - ;" - ); - $this->assertEquals( 4, $result ); - - // Primary key violation: - $result = $this->engine->query( "INSERT INTO _tmp_table (ID, name) VALUES (1, 'Johnny');" ); - $this->assertEquals( false, $result ); - - // Unique constraint violation: - $result = $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (5, 'Kate', 'Bar');" ); - $this->assertEquals( false, $result ); - - // No constraint violation: - $result = $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (5, 'Joanna', 'Bar');" ); - $this->assertEquals( 1, $result ); - - // Now – let's change a few columns: - $result = $this->engine->query( 'ALTER TABLE _tmp_table CHANGE COLUMN name firstname varchar(20)' ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - - $result = $this->engine->query( 'ALTER TABLE _tmp_table CHANGE COLUMN date_as_string datetime datetime NOT NULL' ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - - // Finally, let's confirm our data is intact and the table is still well-behaved: - $result = $this->engine->query( 'SELECT * FROM _tmp_table ORDER BY ID;' ); - $this->assertCount( 5, $result ); - $this->assertEquals( 1, $result[0]->ID ); - $this->assertEquals( 'Johnny', $result[0]->firstname ); - $this->assertEquals( 'Appleseed', $result[0]->lastname ); - $this->assertEquals( '2002-01-01 12:53:13', $result[0]->datetime ); - - // Primary key violation: - $result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, datetime) VALUES (1, 'Johnny', '2010-01-01 12:53:13');" ); - $this->assertEquals( false, $result ); - - // Unique constraint violation: - $result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname, datetime) VALUES (6, 'Kate', 'Bar', '2010-01-01 12:53:13');" ); - $this->assertEquals( false, $result ); - - // No constraint violation: - $result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname, datetime) VALUES (6, 'Sophie', 'Bar', '2010-01-01 12:53:13');" ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - } - - public function testCaseInsensitiveUniqueIndex() { - $result = $this->engine->query( - "CREATE TABLE _tmp_table ( - ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, - name varchar(20) NOT NULL default '', - lastname varchar(20) NOT NULL default '', - KEY name (name), - UNIQUE KEY uname (name), - UNIQUE KEY last (lastname) - );" - ); - $this->assertEquals( 1, $result ); - - $result1 = $this->engine->query( "INSERT INTO _tmp_table (name, lastname) VALUES ('first', 'last');" ); - $this->assertEquals( 1, $result1 ); - - $result1 = $this->engine->query( 'SELECT COUNT(*) num FROM _tmp_table;' ); - $this->assertEquals( 1, $result1[0]->num ); - - // Unique keys should be case-insensitive: - $result2 = $this->assertQuery( - "INSERT INTO _tmp_table (name, lastname) VALUES ('FIRST', 'LAST' );", - 'UNIQUE constraint failed' - ); - - $this->assertEquals( false, $result2 ); - - $result1 = $this->engine->query( 'SELECT COUNT(*) num FROM _tmp_table;' ); - $this->assertEquals( 1, $result1[0]->num ); - - // Unique keys should be case-insensitive: - $result1 = $this->assertQuery( - "INSERT IGNORE INTO _tmp_table (name) VALUES ('FIRST');" - ); - - self::assertEquals( 0, $result1 ); - - $result2 = $this->engine->get_query_results(); - $this->assertEquals( 0, $result2 ); - - $result1 = $this->engine->query( 'SELECT COUNT(*)num FROM _tmp_table;' ); - $this->assertEquals( 1, $result1[0]->num ); - - // Unique keys should be case-insensitive: - $result2 = $this->assertQuery( - "INSERT INTO _tmp_table (name, lastname) VALUES ('FIRSTname', 'LASTname' );" - ); - - $this->assertEquals( 1, $result2 ); - - $result1 = $this->engine->query( 'SELECT COUNT(*) num FROM _tmp_table;' ); - $this->assertEquals( 2, $result1[0]->num ); - } - - public function testOnDuplicateUpdate() { - $this->assertQuery( - "CREATE TABLE _tmp_table ( - ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, - name varchar(20) NOT NULL default '', - UNIQUE KEY myname (name) - );" - ); - - // $result1 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('first');" ); - // $this->assertEquals( '', $this->engine->get_error_message() ); - // $this->assertEquals( 1, $result1 ); - - $result2 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('FIRST') ON DUPLICATE KEY UPDATE `name` = VALUES(`name`);" ); - $this->assertEquals( 1, $result2 ); - - $this->assertQuery( 'SELECT * FROM _tmp_table;' ); - $this->assertCount( 1, $this->engine->get_query_results() ); - $this->assertEquals( - array( - (object) array( - 'name' => 'FIRST', - 'ID' => 1, - ), - ), - $this->engine->get_query_results() - ); - } - - public function testTruncatesInvalidDates() { - $this->assertQuery( "INSERT INTO _dates (option_value) VALUES ('2022-01-01 14:24:12');" ); - $this->assertQuery( "INSERT INTO _dates (option_value) VALUES ('2022-31-01 14:24:12');" ); - - $this->assertQuery( 'SELECT * FROM _dates;' ); - $results = $this->engine->get_query_results(); - $this->assertCount( 2, $results ); - $this->assertEquals( '2022-01-01 14:24:12', $results[0]->option_value ); - $this->assertEquals( '0000-00-00 00:00:00', $results[1]->option_value ); - } - - public function testCaseInsensitiveSelect() { - $this->assertQuery( - "CREATE TABLE _tmp_table ( - ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, - name varchar(20) NOT NULL default '' - );" - ); - $this->assertQuery( - "INSERT INTO _tmp_table (name) VALUES ('first');" - ); - $this->assertQuery( "SELECT name FROM _tmp_table WHERE name = 'FIRST';" ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertCount( 1, $this->engine->get_query_results() ); - $this->assertEquals( - array( - (object) array( - 'name' => 'first', - ), - ), - $this->engine->get_query_results() - ); - } - - public function testSelectBetweenDates() { - $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2016-01-15T00:00:00Z');" ); - $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2016-01-16T00:00:00Z');" ); - $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('third', '2016-01-17T00:00:00Z');" ); - $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('fourth', '2016-01-18T00:00:00Z');" ); - - $this->assertQuery( "SELECT * FROM _dates WHERE option_value BETWEEN '2016-01-15T00:00:00Z' AND '2016-01-17T00:00:00Z' ORDER BY ID;" ); - $results = $this->engine->get_query_results(); - $this->assertCount( 3, $results ); - $this->assertEquals( 'first', $results[0]->option_name ); - $this->assertEquals( 'second', $results[1]->option_name ); - $this->assertEquals( 'third', $results[2]->option_name ); - } - - public function testSelectFilterByDatesGtLt() { - $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2016-01-15T00:00:00Z');" ); - $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2016-01-16T00:00:00Z');" ); - $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('third', '2016-01-17T00:00:00Z');" ); - $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('fourth', '2016-01-18T00:00:00Z');" ); - - $this->assertQuery( - " - SELECT * FROM _dates - WHERE option_value > '2016-01-15 00:00:00' - AND option_value < '2016-01-17 00:00:00' - ORDER BY ID - " - ); - $results = $this->engine->get_query_results(); - $this->assertCount( 1, $results ); - $this->assertEquals( 'second', $results[0]->option_name ); - } - - public function testSelectFilterByDatesZeroHour() { - $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2014-10-21 00:42:29');" ); - $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2014-10-21 01:42:29');" ); - - $this->assertQuery( - ' - SELECT * FROM _dates - WHERE YEAR(option_value) = 2014 - AND MONTHNUM(option_value) = 10 - AND DAY(option_value) = 21 - AND HOUR(option_value) = 0 - AND MINUTE(option_value) = 42 - ' - ); - $results = $this->engine->get_query_results(); - $this->assertCount( 1, $results ); - $this->assertEquals( 'first', $results[0]->option_name ); - } - - public function testCorrectlyInsertsDatesAndStrings() { - $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('2016-01-15T00:00:00Z', '2016-01-15T00:00:00Z');" ); - - $this->assertQuery( 'SELECT * FROM _dates' ); - $results = $this->engine->get_query_results(); - $this->assertCount( 1, $results ); - $this->assertEquals( '2016-01-15 00:00:00', $results[0]->option_value ); - if ( '2016-01-15T00:00:00Z' !== $results[0]->option_name ) { - $this->markTestSkipped( 'A datetime-like string was rewritten to an SQLite format even though it was used as a text and not as a datetime.' ); - } - $this->assertEquals( '2016-01-15T00:00:00Z', $results[0]->option_name ); - } - - public function testTransactionRollback() { - $this->assertQuery( 'BEGIN' ); - $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('first');" ); - $this->assertQuery( 'SELECT * FROM _options;' ); - $this->assertCount( 1, $this->engine->get_query_results() ); - $this->assertQuery( 'ROLLBACK' ); - - $this->assertQuery( 'SELECT * FROM _options;' ); - $this->assertCount( 0, $this->engine->get_query_results() ); - } - - public function testTransactionCommit() { - $this->assertQuery( 'BEGIN' ); - $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('first');" ); - $this->assertQuery( 'SELECT * FROM _options;' ); - $this->assertCount( 1, $this->engine->get_query_results() ); - $this->assertQuery( 'COMMIT' ); - - $this->assertQuery( 'SELECT * FROM _options;' ); - $this->assertCount( 1, $this->engine->get_query_results() ); - } - - public function testStartTransactionCommand() { - $this->assertQuery( 'START TRANSACTION' ); - $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('first');" ); - $this->assertQuery( 'SELECT * FROM _options;' ); - $this->assertCount( 1, $this->engine->get_query_results() ); - $this->assertQuery( 'ROLLBACK' ); - - $this->assertQuery( 'SELECT * FROM _options;' ); - $this->assertCount( 0, $this->engine->get_query_results() ); - } - - public function testNestedTransactionWork() { - $this->assertQuery( 'BEGIN' ); - $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('first');" ); - $this->assertQuery( 'START TRANSACTION' ); - $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('second');" ); - $this->assertQuery( 'START TRANSACTION' ); - $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('third');" ); - $this->assertQuery( 'SELECT * FROM _options;' ); - $this->assertCount( 3, $this->engine->get_query_results() ); - - $this->assertQuery( 'ROLLBACK' ); - $this->assertQuery( 'SELECT * FROM _options;' ); - $this->assertCount( 2, $this->engine->get_query_results() ); - - $this->assertQuery( 'ROLLBACK' ); - $this->assertQuery( 'SELECT * FROM _options;' ); - $this->assertCount( 1, $this->engine->get_query_results() ); - - $this->assertQuery( 'COMMIT' ); - $this->assertQuery( 'SELECT * FROM _options;' ); - $this->assertCount( 1, $this->engine->get_query_results() ); - } - - public function testNestedTransactionWorkComplexModify() { - $this->assertQuery( 'BEGIN' ); - // Create a complex ALTER Table query where the first - // column is added successfully, but the second fails. - // Behind the scenes, this single MySQL query is split - // into multiple SQLite queries – some of them will - // succeed, some will fail. - $success = $this->engine->query( - ' - ALTER TABLE _options - ADD COLUMN test varchar(20), - ADD COLUMN test varchar(20) - ' - ); - $this->assertFalse( $success ); - // Commit the transaction. - $this->assertQuery( 'COMMIT' ); - - // Confirm the entire query failed atomically and no column was - // added to the table. - $this->assertQuery( 'DESCRIBE _options;' ); - $fields = $this->engine->get_query_results(); - - $this->assertEquals( - $fields, - array( - (object) array( - 'Field' => 'ID', - 'Type' => 'integer', - 'Null' => 'NO', - 'Key' => 'PRI', - 'Default' => '0', - 'Extra' => '', - ), - (object) array( - 'Field' => 'option_name', - 'Type' => 'text', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - (object) array( - 'Field' => 'option_value', - 'Type' => 'text', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ), - ) - ); - } - - public function testCount() { - $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('first');" ); - $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('second');" ); - $this->assertQuery( 'SELECT COUNT(*) as count FROM _options;' ); - - $results = $this->engine->get_query_results(); - $this->assertCount( 1, $results ); - $this->assertSame( '2', $results[0]->count ); - } - - public function testUpdateDate() { - $this->assertQuery( - "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48');" - ); - - $this->assertQuery( 'SELECT option_value FROM _dates' ); - - $results = $this->engine->get_query_results(); - $this->assertCount( 1, $results ); - $this->assertEquals( '2003-05-27 10:08:48', $results[0]->option_value ); - - $this->assertQuery( - "UPDATE _dates SET option_value = DATE_SUB(option_value, INTERVAL '2' YEAR);" - ); - - $this->assertQuery( 'SELECT option_value FROM _dates' ); - - $results = $this->engine->get_query_results(); - $this->assertCount( 1, $results ); - $this->assertEquals( '2001-05-27 10:08:48', $results[0]->option_value ); - } - - public function testInsertDateLiteral() { - $this->assertQuery( - "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48');" - ); - - $this->assertQuery( 'SELECT option_value FROM _dates' ); - - $results = $this->engine->get_query_results(); - $this->assertCount( 1, $results ); - $this->assertEquals( '2003-05-27 10:08:48', $results[0]->option_value ); - } - - public function testSelectDate1() { - $this->assertQuery( - "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2000-05-27 10:08:48');" - ); - - $this->assertQuery( - 'SELECT - YEAR( _dates.option_value ) as year, - MONTH( _dates.option_value ) as month, - DAYOFMONTH( _dates.option_value ) as dayofmonth, - MONTHNUM( _dates.option_value ) as monthnum, - WEEKDAY( _dates.option_value ) as weekday, - WEEK( _dates.option_value, 1 ) as week1, - HOUR( _dates.option_value ) as hour, - MINUTE( _dates.option_value ) as minute, - SECOND( _dates.option_value ) as second - FROM _dates' - ); - - $results = $this->engine->get_query_results(); - $this->assertCount( 1, $results ); - $this->assertEquals( '2000', $results[0]->year ); - $this->assertEquals( '5', $results[0]->month ); - $this->assertEquals( '27', $results[0]->dayofmonth ); - $this->assertEquals( '5', $results[0]->weekday ); - $this->assertEquals( '21', $results[0]->week1 ); - $this->assertEquals( '5', $results[0]->monthnum ); - $this->assertEquals( '10', $results[0]->hour ); - $this->assertEquals( '8', $results[0]->minute ); - $this->assertEquals( '48', $results[0]->second ); - } - - public function testSelectDate24HourFormat() { - $this->assertQuery( - " - INSERT INTO _dates (option_name, option_value) - VALUES - ('second', '2003-05-27 14:08:48'), - ('first', '2003-05-27 00:08:48'); - " - ); - - // HOUR(14:08) should yield 14 in the 24 hour format - $this->assertQuery( "SELECT HOUR( _dates.option_value ) as hour FROM _dates WHERE option_name = 'second'" ); - $results = $this->engine->get_query_results(); - $this->assertCount( 1, $results ); - $this->assertEquals( '14', $results[0]->hour ); - - // HOUR(00:08) should yield 0 in the 24 hour format - $this->assertQuery( "SELECT HOUR( _dates.option_value ) as hour FROM _dates WHERE option_name = 'first'" ); - $results = $this->engine->get_query_results(); - $this->assertCount( 1, $results ); - $this->assertEquals( '0', $results[0]->hour ); - - // Lookup by HOUR(00:08) = 0 should yield the right record - $this->assertQuery( - 'SELECT HOUR( _dates.option_value ) as hour FROM _dates - WHERE HOUR(_dates.option_value) = 0 ' - ); - - $results = $this->engine->get_query_results(); - $this->assertCount( 1, $results ); - $this->assertEquals( '0', $results[0]->hour ); - } - - public function testSelectByDateFunctions() { - $this->assertQuery( - " - INSERT INTO _dates (option_name, option_value) - VALUES ('second', '2014-10-21 00:42:29'); - " - ); - - // HOUR(14:08) should yield 14 in the 24 hour format - $this->assertQuery( - ' - SELECT * FROM _dates WHERE - year(option_value) = 2014 - AND monthnum(option_value) = 10 - AND day(option_value) = 21 - AND hour(option_value) = 0 - AND minute(option_value) = 42 - ' - ); - $results = $this->engine->get_query_results(); - $this->assertCount( 1, $results ); - } - - public function testSelectByDateFormat() { - $this->assertQuery( - " - INSERT INTO _dates (option_name, option_value) - VALUES ('second', '2014-10-21 00:42:29'); - " - ); - - // HOUR(14:08) should yield 14 in the 24 hour format - $this->assertQuery( - " - SELECT * FROM _dates WHERE DATE_FORMAT(option_value, '%H.%i') = 0.42 - " - ); - $results = $this->engine->get_query_results(); - $this->assertCount( 1, $results ); - } - - public function testInsertOnDuplicateKey() { - $this->assertQuery( - "CREATE TABLE _tmp_table ( - ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, - name varchar(20) NOT NULL default '', - UNIQUE KEY name (name) - );" - ); - $result1 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('first');" ); - $this->assertEquals( 1, $result1 ); - - $result2 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('FIRST') ON DUPLICATE KEY SET name=VALUES(`name`);" ); - $this->assertEquals( 1, $result2 ); - - $this->assertQuery( 'SELECT COUNT(*) as cnt FROM _tmp_table' ); - $results = $this->engine->get_query_results(); - $this->assertEquals( 1, $results[0]->cnt ); - } - - public function testCreateTableCompositePk() { - $this->assertQuery( - 'CREATE TABLE wptests_term_relationships ( - object_id bigint(20) unsigned NOT NULL default 0, - term_taxonomy_id bigint(20) unsigned NOT NULL default 0, - term_order int(11) NOT NULL default 0, - PRIMARY KEY (object_id,term_taxonomy_id), - KEY term_taxonomy_id (term_taxonomy_id) - ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci' - ); - $result1 = $this->engine->query( 'INSERT INTO wptests_term_relationships VALUES (1,2,1),(1,3,2);' ); - $this->assertEquals( 2, $result1 ); - - $result2 = $this->engine->query( 'INSERT INTO wptests_term_relationships VALUES (1,2,2),(1,3,1);' ); - $this->assertEquals( false, $result2 ); - } - - public function testDescribeAccurate() { - $result = $this->assertQuery( - 'CREATE TABLE wptests_term_relationships ( - object_id bigint(20) unsigned NOT NULL default 0, - term_taxonomy_id bigint(20) unsigned NOT NULL default 0, - term_name varchar(11) NOT NULL default 0, - PRIMARY KEY (object_id,term_taxonomy_id), - KEY term_taxonomy_id (term_taxonomy_id), - KEY compound_key (object_id(20),term_taxonomy_id(20)), - FULLTEXT KEY term_name (term_name) - ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci' - ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertNotFalse( $result ); - - $result = $this->assertQuery( 'DESCRIBE wptests_term_relationships;' ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertNotFalse( $result ); - - $fields = $this->engine->get_query_results(); - - $this->assertEquals( - array( - (object) array( - 'Field' => 'object_id', - 'Type' => 'bigint(20) unsigned', - 'Null' => 'NO', - 'Key' => 'PRI', - 'Default' => '0', - 'Extra' => '', - ), - (object) array( - 'Field' => 'term_taxonomy_id', - 'Type' => 'bigint(20) unsigned', - 'Null' => 'NO', - 'Key' => 'PRI', - 'Default' => '0', - 'Extra' => '', - ), - (object) array( - 'Field' => 'term_name', - 'Type' => 'varchar(11)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '0', - 'Extra' => '', - ), - ), - $fields - ); - } - - public function testAlterTableAddColumnChangesMySQLDataType() { - $result = $this->assertQuery( - 'CREATE TABLE _test ( - object_id bigint(20) unsigned NOT NULL default 0 - )' - ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertNotFalse( $result ); - - $result = $this->assertQuery( "ALTER TABLE `_test` ADD COLUMN object_name varchar(255) NOT NULL DEFAULT 'adb';" ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertNotFalse( $result ); - - $result = $this->assertQuery( 'DESCRIBE _test;' ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertNotFalse( $result ); - $fields = $this->engine->get_query_results(); - - $this->assertEquals( - array( - (object) array( - 'Field' => 'object_id', - 'Type' => 'bigint(20) unsigned', - 'Null' => 'NO', - 'Key' => '', - 'Default' => '0', - 'Extra' => '', - ), - (object) array( - 'Field' => 'object_name', - 'Type' => 'varchar(255)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => 'adb', - 'Extra' => '', - ), - ), - $fields - ); - } - public function testShowGrantsFor() { - $result = $this->assertQuery( 'SHOW GRANTS FOR current_user();' ); - $this->assertEquals( - $result, - array( - (object) array( - 'Grants for root@localhost' => 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, SHUTDOWN, PROCESS, FILE, REFERENCES, INDEX, ALTER, SHOW DATABASES, SUPER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE USER, EVENT, TRIGGER, CREATE TABLESPACE, CREATE ROLE, DROP ROLE ON *.* TO `root`@`localhost` WITH GRANT OPTION', - ), - ) - ); - } - - public function testShowIndex() { - $result = $this->assertQuery( - 'CREATE TABLE wptests_term_relationships ( - object_id bigint(20) unsigned NOT NULL default 0, - term_taxonomy_id bigint(20) unsigned NOT NULL default 0, - term_name varchar(11) NOT NULL default 0, - FULLTEXT KEY term_name_fulltext (term_name), - FULLTEXT INDEX term_name_fulltext2 (`term_name`), - SPATIAL KEY term_name_spatial (term_name), - PRIMARY KEY (object_id,term_taxonomy_id), - KEY term_taxonomy_id (term_taxonomy_id), - KEY compound_key (object_id(20),term_taxonomy_id(20)) - ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci' - ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertNotFalse( $result ); - - $result = $this->assertQuery( 'SHOW INDEX FROM wptests_term_relationships;' ); - $this->assertNotFalse( $result ); - - $this->assertEquals( - array( - (object) array( - 'Table' => 'wptests_term_relationships', - 'Non_unique' => '0', - 'Key_name' => 'PRIMARY', - 'Seq_in_index' => '0', - 'Column_name' => 'object_id', - 'Collation' => 'A', - 'Cardinality' => '0', - 'Sub_part' => null, - 'Packed' => null, - 'Null' => '', - 'Index_type' => 'BTREE', - 'Comment' => '', - 'Index_comment' => '', - ), - (object) array( - 'Table' => 'wptests_term_relationships', - 'Non_unique' => '0', - 'Key_name' => 'PRIMARY', - 'Seq_in_index' => '0', - 'Column_name' => 'term_taxonomy_id', - 'Collation' => 'A', - 'Cardinality' => '0', - 'Sub_part' => null, - 'Packed' => null, - 'Null' => '', - 'Index_type' => 'BTREE', - 'Comment' => '', - 'Index_comment' => '', - ), - (object) array( - 'Table' => 'wptests_term_relationships', - 'Non_unique' => '1', - 'Key_name' => 'compound_key', - 'Seq_in_index' => '0', - 'Column_name' => 'object_id', - 'Collation' => 'A', - 'Cardinality' => '0', - 'Sub_part' => null, - 'Packed' => null, - 'Null' => '', - 'Index_type' => 'BTREE', - 'Comment' => '', - 'Index_comment' => '', - ), - (object) array( - 'Table' => 'wptests_term_relationships', - 'Non_unique' => '1', - 'Key_name' => 'compound_key', - 'Seq_in_index' => '0', - 'Column_name' => 'term_taxonomy_id', - 'Collation' => 'A', - 'Cardinality' => '0', - 'Sub_part' => null, - 'Packed' => null, - 'Null' => '', - 'Index_type' => 'BTREE', - 'Comment' => '', - 'Index_comment' => '', - ), - (object) array( - 'Table' => 'wptests_term_relationships', - 'Non_unique' => '1', - 'Key_name' => 'term_taxonomy_id', - 'Seq_in_index' => '0', - 'Column_name' => 'term_taxonomy_id', - 'Collation' => 'A', - 'Cardinality' => '0', - 'Sub_part' => null, - 'Packed' => null, - 'Null' => '', - 'Index_type' => 'BTREE', - 'Comment' => '', - 'Index_comment' => '', - ), - (object) array( - 'Table' => 'wptests_term_relationships', - 'Non_unique' => '1', - 'Key_name' => 'term_name_spatial', - 'Seq_in_index' => '0', - 'Column_name' => 'term_name', - 'Collation' => 'A', - 'Cardinality' => '0', - 'Sub_part' => null, - 'Packed' => null, - 'Null' => '', - 'Index_type' => 'SPATIAL', - 'Comment' => '', - 'Index_comment' => '', - ), - (object) array( - 'Table' => 'wptests_term_relationships', - 'Non_unique' => '1', - 'Key_name' => 'term_name_fulltext2', - 'Seq_in_index' => '0', - 'Column_name' => 'term_name', - 'Collation' => 'A', - 'Cardinality' => '0', - 'Sub_part' => null, - 'Packed' => null, - 'Null' => '', - 'Index_type' => 'FULLTEXT', - 'Comment' => '', - 'Index_comment' => '', - ), - (object) array( - 'Table' => 'wptests_term_relationships', - 'Non_unique' => '1', - 'Key_name' => 'term_name_fulltext', - 'Seq_in_index' => '0', - 'Column_name' => 'term_name', - 'Collation' => 'A', - 'Cardinality' => '0', - 'Sub_part' => null, - 'Packed' => null, - 'Null' => '', - 'Index_type' => 'FULLTEXT', - 'Comment' => '', - 'Index_comment' => '', - ), - (object) array( - 'Table' => 'wptests_term_relationships', - 'Non_unique' => '0', - 'Key_name' => 'wptests_term_relationships', - 'Seq_in_index' => '0', - 'Column_name' => 'object_id', - 'Collation' => 'A', - 'Cardinality' => '0', - 'Sub_part' => null, - 'Packed' => null, - 'Null' => '', - 'Index_type' => 'BTREE', - 'Comment' => '', - 'Index_comment' => '', - ), - (object) array( - 'Table' => 'wptests_term_relationships', - 'Non_unique' => '0', - 'Key_name' => 'wptests_term_relationships', - 'Seq_in_index' => '0', - 'Column_name' => 'term_taxonomy_id', - 'Collation' => 'A', - 'Cardinality' => '0', - 'Sub_part' => null, - 'Packed' => null, - 'Null' => '', - 'Index_type' => 'BTREE', - 'Comment' => '', - 'Index_comment' => '', - ), - ), - $this->engine->get_query_results() - ); - } - - public function testInsertOnDuplicateKeyCompositePk() { - $result = $this->assertQuery( - 'CREATE TABLE wptests_term_relationships ( - object_id bigint(20) unsigned NOT NULL default 0, - term_taxonomy_id bigint(20) unsigned NOT NULL default 0, - term_order int(11) NOT NULL default 0, - PRIMARY KEY (object_id,term_taxonomy_id), - KEY term_taxonomy_id (term_taxonomy_id) - ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci' - ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertNotFalse( $result ); - - $result1 = $this->assertQuery( 'INSERT INTO wptests_term_relationships VALUES (1,2,1),(1,3,2);' ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 2, $result1 ); - - $result2 = $this->assertQuery( 'INSERT INTO wptests_term_relationships VALUES (1,2,2),(1,3,1) ON DUPLICATE KEY SET term_order = VALUES(term_order);' ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 2, $result2 ); - - $this->assertQuery( 'SELECT COUNT(*) as cnt FROM wptests_term_relationships' ); - $results = $this->engine->get_query_results(); - $this->assertEquals( 2, $results[0]->cnt ); - } - - public function testStringToFloatComparison() { - $this->assertQuery( "SELECT ('00.42' = 0.4200) as cmp;" ); - $results = $this->engine->get_query_results(); - if ( 1 !== $results[0]->cmp ) { - $this->markTestSkipped( 'Comparing a string and a float returns true in MySQL. In SQLite, they\'re different. Skipping. ' ); - } - $this->assertEquals( '1', $results[0]->cmp ); - - $this->assertQuery( "SELECT (0+'00.42' = 0.4200) as cmp;" ); - $results = $this->engine->get_query_results(); - $this->assertEquals( '1', $results[0]->cmp ); - } - - public function testZeroPlusStringToFloatComparison() { - - $this->assertQuery( "SELECT (0+'00.42' = 0.4200) as cmp;" ); - $results = $this->engine->get_query_results(); - $this->assertEquals( '1', $results[0]->cmp ); - - $this->assertQuery( "SELECT 0+'1234abcd' = 1234 as cmp;" ); - $results = $this->engine->get_query_results(); - $this->assertEquals( '1', $results[0]->cmp ); - } - - public function testCalcFoundRows() { - $result = $this->assertQuery( - "CREATE TABLE wptests_dummy ( - ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - user_login TEXT NOT NULL default '' - );" - ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertNotFalse( $result ); - - $result = $this->assertQuery( - "INSERT INTO wptests_dummy (user_login) VALUES ('test');" - ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 1, $result ); - - $result = $this->assertQuery( - 'SELECT SQL_CALC_FOUND_ROWS * FROM wptests_dummy' - ); - $this->assertNotFalse( $result ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( 'test', $result[0]->user_login ); - } - - public function testComplexSelectBasedOnDates() { - $this->assertQuery( - "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48');" - ); - - $this->assertQuery( - 'SELECT SQL_CALC_FOUND_ROWS _dates.ID - FROM _dates - WHERE YEAR( _dates.option_value ) = 2003 AND MONTH( _dates.option_value ) = 5 AND DAYOFMONTH( _dates.option_value ) = 27 - ORDER BY _dates.option_value DESC - LIMIT 0, 10' - ); - - $results = $this->engine->get_query_results(); - $this->assertCount( 1, $results ); - } - - public function testUpdateReturnValue() { - $this->assertQuery( - "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48');" - ); - - $return = $this->assertQuery( - "UPDATE _dates SET option_value = '2001-05-27 10:08:48'" - ); - $this->assertSame( 1, $return, 'UPDATE query did not return 1 when one row was changed' ); - - $return = $this->assertQuery( - "UPDATE _dates SET option_value = '2001-05-27 10:08:48'" - ); - if ( 1 === $return ) { - $this->markTestIncomplete( - 'SQLite UPDATE query returned 1 when no rows were changed. ' . - 'This is a database compatibility issue – MySQL would return 0 ' . - 'in the same scenario.' - ); - } - $this->assertSame( 0, $return, 'UPDATE query did not return 0 when no rows were changed' ); - } - - public function testOrderByField() { - $this->assertQuery( - "INSERT INTO _options (option_name, option_value) VALUES ('User 0000019', 'second');" - ); - $this->assertQuery( - "INSERT INTO _options (option_name, option_value) VALUES ('User 0000020', 'third');" - ); - $this->assertQuery( - "INSERT INTO _options (option_name, option_value) VALUES ('User 0000018', 'first');" - ); - - $this->assertQuery( 'SELECT FIELD(option_name, "User 0000018", "User 0000019", "User 0000020") as sorting_order FROM _options ORDER BY FIELD(option_name, "User 0000018", "User 0000019", "User 0000020")' ); - - $this->assertEquals( - array( - (object) array( - 'sorting_order' => '1', - ), - (object) array( - 'sorting_order' => '2', - ), - (object) array( - 'sorting_order' => '3', - ), - ), - $this->engine->get_query_results() - ); - - $this->assertQuery( 'SELECT option_value FROM _options ORDER BY FIELD(option_name, "User 0000018", "User 0000019", "User 0000020")' ); - - $this->assertEquals( - array( - (object) array( - 'option_value' => 'first', - ), - (object) array( - 'option_value' => 'second', - ), - (object) array( - 'option_value' => 'third', - ), - ), - $this->engine->get_query_results() - ); - } - - public function testFetchedDataIsStringified() { - $this->assertQuery( - "INSERT INTO _options (option_name, option_value) VALUES ('rss_0123456789abcdef0123456789abcdef', '1');" - ); - - $this->assertQuery( 'SELECT ID FROM _options' ); - - $this->assertEquals( - array( - (object) array( - 'ID' => '1', - ), - ), - $this->engine->get_query_results() - ); - } - - public function testCreateTableQuery() { - $this->assertQuery( - <<<'QUERY' - CREATE TABLE IF NOT EXISTS wptests_users ( - ID bigint(20) unsigned NOT NULL auto_increment, - user_login varchar(60) NOT NULL default '', - user_pass varchar(255) NOT NULL default '', - user_nicename varchar(50) NOT NULL default '', - user_email varchar(100) NOT NULL default '', - user_url varchar(100) NOT NULL default '', - user_registered datetime NOT NULL default '0000-00-00 00:00:00', - user_activation_key varchar(255) NOT NULL default '', - user_status int(11) NOT NULL default '0', - display_name varchar(250) NOT NULL default '', - PRIMARY KEY (ID), - KEY user_login_key (user_login), - KEY user_nicename (user_nicename), - KEY user_email (user_email) - ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci -QUERY - ); - $this->assertQuery( - <<<'QUERY' - INSERT INTO wptests_users VALUES (1,'admin','$P$B5ZQZ5ZQZ5ZQZ5ZQZ5ZQZ5ZQZ5ZQZ5','admin','admin@localhost', '', '2019-01-01 00:00:00', '', 0, 'admin'); -QUERY - ); - $rows = $this->assertQuery( 'SELECT * FROM wptests_users' ); - $this->assertCount( 1, $rows ); - - $this->assertQuery( 'SELECT SQL_CALC_FOUND_ROWS * FROM wptests_users' ); - $result = $this->assertQuery( 'SELECT FOUND_ROWS()' ); - $this->assertEquals( - array( - (object) array( - 'FOUND_ROWS()' => '1', - ), - ), - $result - ); - } - - public function testTranslatesComplexDelete() { - $this->sqlite->query( - "CREATE TABLE wptests_dummy ( - ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - user_login TEXT NOT NULL default '', - option_name TEXT NOT NULL default '', - option_value TEXT NOT NULL default '' - );" - ); - $this->sqlite->query( - "INSERT INTO wptests_dummy (user_login, option_name, option_value) VALUES ('admin', '_transient_timeout_test', '1675963960');" - ); - $this->sqlite->query( - "INSERT INTO wptests_dummy (user_login, option_name, option_value) VALUES ('admin', '_transient_test', '1675963960');" - ); - - $result = $this->assertQuery( - "DELETE a, b FROM wptests_dummy a, wptests_dummy b - WHERE a.option_name LIKE '\_transient\_%' - AND a.option_name NOT LIKE '\_transient\_timeout_%' - AND b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) );" - ); - $this->assertEquals( - 2, - $result - ); - } - - public function testTranslatesDoubleAlterTable() { - $result = $this->assertQuery( - 'ALTER TABLE _options - ADD INDEX test_index(option_name(140),option_value(51)), - DROP INDEX test_index, - ADD INDEX test_index2(option_name(140),option_value(51)) - ' - ); - $this->assertEquals( '', $this->engine->get_error_message() ); - $this->assertEquals( - 1, - $result - ); - $result = $this->assertQuery( - 'SHOW INDEX FROM _options' - ); - $this->assertCount( 3, $result ); - $this->assertEquals( 'PRIMARY', $result[0]->Key_name ); - $this->assertEquals( 'test_index2', $result[1]->Key_name ); - $this->assertEquals( 'test_index2', $result[2]->Key_name ); - } - - public function testTranslatesComplexSelect() { - $this->assertQuery( - "CREATE TABLE wptests_postmeta ( - meta_id bigint(20) unsigned NOT NULL auto_increment, - post_id bigint(20) unsigned NOT NULL default '0', - meta_key varchar(255) default NULL, - meta_value longtext, - PRIMARY KEY (meta_id), - KEY post_id (post_id), - KEY meta_key (meta_key(191)) - ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci" - ); - $this->assertQuery( - "CREATE TABLE wptests_posts ( - ID bigint(20) unsigned NOT NULL auto_increment, - post_status varchar(20) NOT NULL default 'open', - post_type varchar(20) NOT NULL default 'post', - post_date varchar(20) NOT NULL default 'post', - PRIMARY KEY (ID) - ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci" - ); - $result = $this->assertQuery( - "SELECT SQL_CALC_FOUND_ROWS wptests_posts.ID - FROM wptests_posts INNER JOIN wptests_postmeta ON ( wptests_posts.ID = wptests_postmeta.post_id ) - WHERE 1=1 - AND ( - NOT EXISTS ( - SELECT 1 FROM wptests_postmeta mt1 - WHERE mt1.post_ID = wptests_postmeta.post_ID - LIMIT 1 - ) - ) - AND ( - (wptests_posts.post_type = 'post' AND (wptests_posts.post_status = 'publish')) - ) - GROUP BY wptests_posts.ID - ORDER BY wptests_posts.post_date DESC - LIMIT 0, 10" - ); - - // No exception is good enough of a test for now - $this->assertTrue( true ); - } - - public function testTranslatesUtf8Insert() { - $this->assertQuery( - "INSERT INTO _options VALUES(1,'ąłółźćę†','ąłółźćę†')" - ); - $this->assertCount( - 1, - $this->assertQuery( 'SELECT * FROM _options' ) - ); - $this->assertQuery( 'DELETE FROM _options' ); - } - - public function testTranslatesRandom() { - $this->assertIsNumeric( - $this->sqlite->query( 'SELECT RAND() AS rand' )->fetchColumn() - ); - - $this->assertIsNumeric( - $this->sqlite->query( 'SELECT RAND(5) AS rand' )->fetchColumn() - ); - } - - public function testTranslatesUtf8SELECT() { - $this->assertQuery( - "INSERT INTO _options VALUES(1,'ąłółźćę†','ąłółźćę†')" - ); - $this->assertCount( - 1, - $this->assertQuery( 'SELECT * FROM _options' ) - ); - - $this->assertQuery( - "SELECT option_name as 'ą' FROM _options WHERE option_name='ąłółźćę†' AND option_value='ąłółźćę†'" - ); - - $this->assertEquals( - array( (object) array( 'ą' => 'ąłółźćę†' ) ), - $this->engine->get_query_results() - ); - - $this->assertQuery( - "SELECT option_name as 'ą' FROM _options WHERE option_name LIKE '%ółźć%'" - ); - - $this->assertEquals( - array( (object) array( 'ą' => 'ąłółźćę†' ) ), - $this->engine->get_query_results() - ); - - $this->assertQuery( 'DELETE FROM _options' ); - } - - public function testTranslateLikeBinaryAndGlob() { - // Create a temporary table for testing - $this->assertQuery( - "CREATE TABLE _tmp_table ( - ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - name varchar(20) NOT NULL default '' - );" - ); - - // Insert data into the table - $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('first');" ); - $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('FIRST');" ); - $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('second');" ); - $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('');" ); - $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('%special%');" ); - $this->assertQuery( 'INSERT INTO _tmp_table (name) VALUES (NULL);' ); - $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('special%chars');" ); - $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('special_chars');" ); - $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('special\\chars');" ); - - // Test case-sensitive LIKE BINARY - $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'first'" ); - $this->assertCount( 1, $result ); - $this->assertEquals( 'first', $result[0]->name ); - - // Test case-sensitive LIKE BINARY with wildcard % - $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'f%'" ); - $this->assertCount( 1, $result ); - $this->assertEquals( 'first', $result[0]->name ); - - // Test case-sensitive LIKE BINARY with wildcard _ - $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'f_rst'" ); - $this->assertCount( 1, $result ); - $this->assertEquals( 'first', $result[0]->name ); - - // Test case-insensitive LIKE - $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE 'FIRST'" ); - $this->assertCount( 2, $result ); // Should match both 'first' and 'FIRST' - - // Test mixed case with LIKE BINARY - $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'First'" ); - $this->assertCount( 0, $result ); - - // Test no matches with LIKE BINARY - $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'third'" ); - $this->assertCount( 0, $result ); - - // Test GLOB equivalent for case-sensitive matching with wildcard - $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name GLOB 'f*'" ); - $this->assertCount( 1, $result ); - $this->assertEquals( 'first', $result[0]->name ); - - // Test GLOB with single character wildcard - $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name GLOB 'f?rst'" ); - $this->assertCount( 1, $result ); - $this->assertEquals( 'first', $result[0]->name ); - - // Test GLOB with no matches - $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name GLOB 'S*'" ); - $this->assertCount( 0, $result ); - - // Test GLOB case sensitivity with LIKE and GLOB - $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name GLOB 'first';" ); - $this->assertCount( 1, $result ); // Should only match 'first' - - $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name GLOB 'FIRST';" ); - $this->assertCount( 1, $result ); // Should only match 'FIRST' - - // Test NULL comparison with LIKE BINARY - $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'first';" ); - $this->assertCount( 1, $result ); - $this->assertEquals( 'first', $result[0]->name ); - - $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE name LIKE BINARY NULL;' ); - $this->assertCount( 0, $result ); // NULL comparison should return no results - - // Test pattern with special characters using LIKE BINARY - $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY '%special%';" ); - $this->assertCount( 4, $result ); - $this->assertEquals( '%special%', $result[0]->name ); - $this->assertEquals( 'special%chars', $result[1]->name ); - $this->assertEquals( 'special_chars', $result[2]->name ); - $this->assertEquals( 'specialchars', $result[3]->name ); - } - - public function testOnConflictReplace() { - $this->assertQuery( - "CREATE TABLE _tmp_table ( - ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - name varchar(20) NOT NULL default 'default-value', - unique_name varchar(20) NOT NULL default 'unique-default-value', - inline_unique_name varchar(20) NOT NULL default 'inline-unique-default-value', - no_default varchar(20) NOT NULL, - UNIQUE KEY unique_name (unique_name) - );" - ); - - $this->assertQuery( - "INSERT INTO _tmp_table VALUES (1, null, null, null, '');" - ); - $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE ID = 1' ); - $this->assertEquals( - array( - (object) array( - 'ID' => '1', - 'name' => 'default-value', - 'unique_name' => 'unique-default-value', - 'inline_unique_name' => 'inline-unique-default-value', - 'no_default' => '', - ), - ), - $result - ); - - $this->assertQuery( - "INSERT INTO _tmp_table VALUES (2, '1', '2', '3', '4');" - ); - $this->assertQuery( - 'UPDATE _tmp_table SET name = null WHERE ID = 2;' - ); - - $result = $this->assertQuery( 'SELECT name FROM _tmp_table WHERE ID = 2' ); - $this->assertEquals( - array( - (object) array( - 'name' => 'default-value', - ), - ), - $result - ); - - // This should fail because of the UNIQUE constraint - $this->assertQuery( - 'UPDATE _tmp_table SET unique_name = NULL WHERE ID = 2;', - 'UNIQUE constraint failed: _tmp_table.unique_name' - ); - - // Inline unique constraint aren't supported currently, so this should pass - $this->assertQuery( - 'UPDATE _tmp_table SET inline_unique_name = NULL WHERE ID = 2;', - '' - ); - - // WPDB allows for NULL values in columns that don't have a default value and a NOT NULL constraint - $this->assertQuery( - 'UPDATE _tmp_table SET no_default = NULL WHERE ID = 2;', - '' - ); - - $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE ID = 2' ); - $this->assertEquals( - array( - (object) array( - 'ID' => '2', - 'name' => 'default-value', - 'unique_name' => '2', - 'inline_unique_name' => 'inline-unique-default-value', - 'no_default' => '', - ), - ), - $result - ); - } - - public function testDefaultNullValue() { - $this->assertQuery( - 'CREATE TABLE _tmp_table ( - name varchar(20) NOT NULL default NULL, - no_default varchar(20) NOT NULL - );' - ); - - $result = $this->assertQuery( - 'DESCRIBE _tmp_table;' - ); - $this->assertEquals( - array( - (object) array( - 'Field' => 'name', - 'Type' => 'varchar(20)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => 'NULL', - 'Extra' => '', - ), - (object) array( - 'Field' => 'no_default', - 'Type' => 'varchar(20)', - 'Null' => 'NO', - 'Key' => '', - 'Default' => null, - 'Extra' => '', - ), - ), - $result - ); - } - - public function testCurrentTimestamp() { - // SELECT - $results = $this->assertQuery( - 'SELECT - current_timestamp AS t1, - CURRENT_TIMESTAMP AS t2, - current_timestamp() AS t3, - CURRENT_TIMESTAMP() AS t4' - ); - $this->assertIsArray( $results ); - $this->assertCount( 1, $results ); - $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $results[0]->t1 ); - $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $results[0]->t2 ); - $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $results[0]->t3 ); - - // INSERT - $this->assertQuery( - "INSERT INTO _dates (option_name, option_value) VALUES ('first', CURRENT_TIMESTAMP())" - ); - $results = $this->assertQuery( 'SELECT option_value AS t FROM _dates' ); - $this->assertCount( 1, $results ); - $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $results[0]->t ); - - // UPDATE - $this->assertQuery( 'UPDATE _dates SET option_value = NULL' ); - $results = $this->assertQuery( 'SELECT option_value AS t FROM _dates' ); - $this->assertCount( 1, $results ); - $this->assertEmpty( $results[0]->t ); - - $this->assertQuery( 'UPDATE _dates SET option_value = CURRENT_TIMESTAMP()' ); - $results = $this->assertQuery( 'SELECT option_value AS t FROM _dates' ); - $this->assertCount( 1, $results ); - $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $results[0]->t ); - - // DELETE - // We can only assert that the query passes. It is not guaranteed that we'll actually - // delete the existing record, as the delete query could fall into a different second. - $this->assertQuery( 'DELETE FROM _dates WHERE option_value = CURRENT_TIMESTAMP()' ); - } - - public function testGroupByHaving() { - $this->assertQuery( - 'CREATE TABLE _tmp_table ( - name varchar(20) - );' - ); - - $this->assertQuery( - "INSERT INTO _tmp_table VALUES ('a'), ('b'), ('b'), ('c'), ('c'), ('c')" - ); - - $result = $this->assertQuery( - 'SELECT name, COUNT(*) as count FROM _tmp_table GROUP BY name HAVING COUNT(*) > 1' - ); - $this->assertEquals( - array( - (object) array( - 'name' => 'b', - 'count' => '2', - ), - (object) array( - 'name' => 'c', - 'count' => '3', - ), - ), - $result - ); - } - - public function testHavingWithoutGroupBy() { - $this->assertQuery( - 'CREATE TABLE _tmp_table ( - name varchar(20) - );' - ); - - $this->assertQuery( - "INSERT INTO _tmp_table VALUES ('a'), ('b'), ('b'), ('c'), ('c'), ('c')" - ); - - // HAVING condition satisfied - $result = $this->assertQuery( - "SELECT 'T' FROM _tmp_table HAVING COUNT(*) > 1" - ); - $this->assertEquals( - array( - (object) array( - ':param0' => 'T', - ), - ), - $result - ); - - // HAVING condition not satisfied - $result = $this->assertQuery( - "SELECT 'T' FROM _tmp_table HAVING COUNT(*) > 100" - ); - $this->assertEquals( - array(), - $result - ); - - // DISTINCT ... HAVING, where only some results meet the HAVING condition - $result = $this->assertQuery( - 'SELECT DISTINCT name FROM _tmp_table HAVING COUNT(*) > 1' - ); - $this->assertEquals( - array( - (object) array( - 'name' => 'b', - ), - (object) array( - 'name' => 'c', - ), - ), - $result - ); - } - - /** - * @dataProvider mysqlVariablesToTest - */ - public function testSelectVariable( $variable_name ) { - // Make sure the query does not error - $this->assertQuery( "SELECT $variable_name;" ); - } - - public static function mysqlVariablesToTest() { - return array( - // NOTE: This list was derived from the variables used by the UpdraftPlus plugin. - // We will start here and plan to expand supported variables over time. - array( '@@character_set_client' ), - array( '@@character_set_results' ), - array( '@@collation_connection' ), - array( '@@GLOBAL.gtid_purged' ), - array( '@@GLOBAL.log_bin' ), - array( '@@GLOBAL.log_bin_trust_function_creators' ), - array( '@@GLOBAL.sql_mode' ), - array( '@@SESSION.max_allowed_packet' ), - array( '@@SESSION.sql_mode' ), - - // Intentionally mix letter casing to help demonstrate case-insensitivity - array( '@@cHarActer_Set_cLient' ), - array( '@@gLoBAL.gTiD_purGed' ), - array( '@@sEssIOn.sqL_moDe' ), - ); - } - - /** - * Test CREATE TABLE with DEFAULT (now()) - GitHub issue #300 - * Tests that DEFAULT with function calls in parentheses works correctly. - */ - public function testCreateTableWithDefaultNowFunction() { - // Test the exact SQL from the issue - $this->assertQuery( - 'CREATE TABLE `test_now_default` ( - `id` int NOT NULL, - `updated` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;' - ); - - // Verify the table was created successfully - $results = $this->assertQuery( 'DESCRIBE test_now_default;' ); - $this->assertCount( 2, $results ); - - // Verify the updated column has the correct properties - $updated_field = $results[1]; - $this->assertEquals( 'updated', $updated_field->Field ); - $this->assertEquals( 'timestamp', $updated_field->Type ); - $this->assertEquals( 'NO', $updated_field->Null ); - - // Insert a row to verify the default value works - $this->assertQuery( 'INSERT INTO test_now_default (id) VALUES (1)' ); - $result = $this->assertQuery( 'SELECT * FROM test_now_default WHERE id = 1' ); - $this->assertCount( 1, $result ); - - // Verify the updated timestamp was set (should match YYYY-MM-DD HH:MM:SS format) - $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->updated ); - - // Test ON UPDATE trigger works - $this->assertQuery( 'UPDATE test_now_default SET id = 2 WHERE id = 1' ); - $result = $this->assertQuery( 'SELECT * FROM test_now_default WHERE id = 2' ); - $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->updated ); - } - - public function testQuoteIdentifierEscapesBackticks() { - // Create a table with a backtick in the column name using double-quote - // quoting (MySQL syntax). The translator must properly escape the - // backtick when generating SQLite DDL with backtick-quoted identifiers. - $this->assertQuery( - 'CREATE TABLE _tmp_backtick_test ( - ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, - "col`name" varchar(50) NOT NULL - );' - ); - - $this->assertQuery( "INSERT INTO _tmp_backtick_test (ID, \"col`name\") VALUES (1, 'value1')" ); - - $result = $this->assertQuery( 'SELECT * FROM _tmp_backtick_test WHERE ID = 1' ); - $this->assertCount( 1, $result ); - $this->assertEquals( 'value1', $result[0]->{'col`name'} ); - - // Verify the column appears in DESCRIBE output. - $description = $this->assertQuery( 'DESCRIBE _tmp_backtick_test' ); - $column_names = array_map( - function ( $row ) { - return $row->Field; - }, - $description - ); - $this->assertContains( 'col`name', $column_names ); - - // Verify SHOW CREATE TABLE produces valid, parseable output. - $create = $this->assertQuery( 'SHOW CREATE TABLE _tmp_backtick_test' ); - $create_sql = $create[0]->{'Create Table'}; - $this->assertStringContainsString( '`col``name`', $create_sql ); - - // Verify autoincrement detection works with backtick-quoted identifiers. - $this->assertStringContainsString( 'AUTO_INCREMENT', $create_sql ); - } - - public function testDoubleQuotedStringsAreParameterized() { - $this->assertQuery( 'INSERT INTO _options (option_name, option_value) VALUES ("dq_name", "dq_value")' ); - - // The double-quoted strings should be bound as parameters, not inlined. - $insert_query = null; - foreach ( $this->engine->executed_sqlite_queries as $q ) { - if ( stripos( $q['sql'], 'INSERT' ) !== false && stripos( $q['sql'], '_options' ) !== false ) { - $insert_query = $q; - break; - } - } - $this->assertNotNull( $insert_query ); - $this->assertNotEmpty( $insert_query['params'], 'Double-quoted strings should be bound as parameters' ); - $this->assertStringNotContainsString( 'dq_name', $insert_query['sql'], 'Value should not appear in SQL' ); - $this->assertStringNotContainsString( 'dq_value', $insert_query['sql'], 'Value should not appear in SQL' ); - $this->assertContains( 'dq_name', $insert_query['params'] ); - $this->assertContains( 'dq_value', $insert_query['params'] ); - - // Verify the data was inserted correctly. - $result = $this->assertQuery( 'SELECT * FROM _options WHERE option_name = "dq_name"' ); - $this->assertCount( 1, $result ); - $this->assertEquals( 'dq_value', $result[0]->option_value ); - } - - public function testDoubleQuotedStringWithBackslashEscapeDoesNotCauseInjection() { - // In MySQL, \" inside double-quoted strings is an escaped double quote. - // The MySQL lexer produces a single token: "admin\" OR 1=1--" - // with value: admin" OR 1=1-- - // - // Without parameterization, passing the raw token to SQLite would be: - // "admin\" OR 1=1--" (SQLite sees "admin\" as identifier + SQL) - // - // With parameterization, the value is safely bound as a parameter. - $this->assertQuery( - 'INSERT INTO _options (option_name, option_value) VALUES ("safe_key", "admin\" OR 1=1--")' - ); - - // Verify the injection payload is not present in the SQL sent to SQLite. - $insert_query = null; - foreach ( $this->engine->executed_sqlite_queries as $q ) { - if ( stripos( $q['sql'], 'INSERT' ) !== false && stripos( $q['sql'], '_options' ) !== false ) { - $insert_query = $q; - break; - } - } - $this->assertNotNull( $insert_query ); - $this->assertStringNotContainsString( 'OR 1=1', $insert_query['sql'], 'Injection payload should not appear in SQL' ); - $this->assertNotEmpty( $insert_query['params'], 'Values should be bound as parameters' ); - - $result = $this->assertQuery( 'SELECT * FROM _options WHERE option_name = "safe_key"' ); - $this->assertCount( 1, $result ); - $this->assertEquals( 'admin" OR 1=1--', $result[0]->option_value ); - } - - public function testDateFormatWithSingleQuotesInFormat() { - $this->assertQuery( - 'CREATE TABLE _tmp_dates ( - ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, - created_at DATETIME NOT NULL - );' - ); - $this->assertQuery( "INSERT INTO _tmp_dates (created_at) VALUES ('2024-01-15 10:30:00')" ); - - // DATE_FORMAT with a format that produces a value — verify it works. - $result = $this->assertQuery( - "SELECT DATE_FORMAT(created_at, '%Y-%m-%d') as formatted FROM _tmp_dates" - ); - $this->assertCount( 1, $result ); - $this->assertEquals( '2024-01-15', $result[0]->formatted ); - } - - public function testIntervalExpression() { - $this->assertQuery( - 'CREATE TABLE _tmp_dates ( - ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, - created_at DATETIME NOT NULL - );' - ); - $this->assertQuery( 'INSERT INTO _tmp_dates (created_at) VALUES (\'2024-01-15 10:30:00\')' ); - - $result = $this->assertQuery( - 'SELECT DATE_ADD(created_at, INTERVAL 1 DAY) as future_date FROM _tmp_dates' - ); - $this->assertCount( 1, $result ); - $this->assertEquals( '2024-01-16 10:30:00', $result[0]->future_date ); - - $result = $this->assertQuery( - 'SELECT DATE_SUB(created_at, INTERVAL 1 DAY) as past_date FROM _tmp_dates' - ); - $this->assertCount( 1, $result ); - $this->assertEquals( '2024-01-14 10:30:00', $result[0]->past_date ); - } - - public function testLikeBinaryWithSingleQuoteInPattern() { - $this->assertQuery( - "CREATE TABLE _tmp_table ( - ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - name varchar(50) NOT NULL default '' - );" - ); - - $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('it''s a test')" ); - $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('no quote here')" ); - - $result = $this->assertQuery( - "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'it''s%'" - ); - $this->assertCount( 1, $result ); - $this->assertEquals( "it's a test", $result[0]->name ); - } -} diff --git a/tests/bootstrap.php b/tests/bootstrap.php deleted file mode 100644 index f2d89b4b..00000000 --- a/tests/bootstrap.php +++ /dev/null @@ -1,92 +0,0 @@ - "$WP_DIR/docker-compose.override.yml" services: wordpress-develop: - environment: - WP_SQLITE_AST_DRIVER: true volumes: - ../packages/plugin-sqlite-database-integration:/var/www/src/wp-content/plugins/sqlite-database-integration - ../packages/mysql-on-sqlite/src:/var/www/src/wp-content/plugins/sqlite-database-integration/wp-includes/database @@ -40,8 +38,6 @@ services: php: # PHP temporarily pinned to 8.3.10, see: https://github.com/WordPress/wordpress-develop/pull/9602 image: wordpressdevelop/php@sha256:c0ba85936a9d1ac2c98bf3da2d62ceb0e5787a6b11e383630df0c5a5bf2534b5 - environment: - WP_SQLITE_AST_DRIVER: true volumes: - ../packages/plugin-sqlite-database-integration:/var/www/src/wp-content/plugins/sqlite-database-integration - ../packages/mysql-on-sqlite/src:/var/www/src/wp-content/plugins/sqlite-database-integration/wp-includes/database @@ -49,8 +45,6 @@ services: cli: # PHP temporarily pinned to 8.3.10, see: https://github.com/WordPress/wordpress-develop/pull/9602 image: wordpressdevelop/cli@sha256:85ad7d7a9c3bd9a8775fc83aea7f7dfc0aad25b2bc4f7d740696b28cd2a0ef89 - environment: - WP_SQLITE_AST_DRIVER: true volumes: - ../packages/plugin-sqlite-database-integration:/var/www/src/wp-content/plugins/sqlite-database-integration - ../packages/mysql-on-sqlite/src:/var/www/src/wp-content/plugins/sqlite-database-integration/wp-includes/database