diff --git a/.github/workflows/lexer-benchmark.yml b/.github/workflows/lexer-benchmark.yml index 9c41005f0..4b7ad8fa1 100644 --- a/.github/workflows/lexer-benchmark.yml +++ b/.github/workflows/lexer-benchmark.yml @@ -38,6 +38,12 @@ jobs: php-version: '8.4' coverage: none + - name: Install Composer dependencies (mysql-on-sqlite) + uses: ramsey/composer-install@v3 + with: + working-directory: packages/mysql-on-sqlite + composer-options: "--optimize-autoloader" + - name: Benchmark base vs PR env: BASE_SHA: ${{ github.event.pull_request.base.sha }} diff --git a/.github/workflows/mysql-proxy-tests.yml b/.github/workflows/mysql-proxy-tests.yml index 6e6a0afa2..52475b43a 100644 --- a/.github/workflows/mysql-proxy-tests.yml +++ b/.github/workflows/mysql-proxy-tests.yml @@ -35,6 +35,15 @@ jobs: composer-options: "--optimize-autoloader" working-directory: packages/mysql-proxy + # The proxy tests run the SQLite driver, which loads its own Composer + # dependencies (wordpress/mysql-parser). + - name: Install Composer dependencies (mysql-on-sqlite) + uses: ramsey/composer-install@v3 + with: + ignore-cache: "yes" + composer-options: "--optimize-autoloader" + working-directory: packages/mysql-on-sqlite + - name: Run MySQL Proxy tests run: composer run test working-directory: packages/mysql-proxy diff --git a/.github/workflows/phpunit-tests.yml b/.github/workflows/phpunit-tests.yml index 5126e2ea3..d852c6dab 100644 --- a/.github/workflows/phpunit-tests.yml +++ b/.github/workflows/phpunit-tests.yml @@ -7,14 +7,14 @@ on: paths: - '.github/workflows/phpunit-tests.yml' - 'packages/mysql-on-sqlite/**' - - 'packages/php-ext-wp-mysql-parser/**' + - 'packages/mysql-parser/**' - 'composer.json' - 'composer.lock' pull_request: paths: - '.github/workflows/phpunit-tests.yml' - 'packages/mysql-on-sqlite/**' - - 'packages/php-ext-wp-mysql-parser/**' + - 'packages/mysql-parser/**' - 'composer.json' - 'composer.lock' workflow_dispatch: @@ -29,10 +29,7 @@ permissions: {} jobs: test: - # The pure-PHP parser is exercised across the full PHP/SQLite range; the - # native Rust parser extension is exercised on PHP 8.0+ (its minimum). Both - # run the same mysql-on-sqlite suite, just with a different parser engine. - name: PHP ${{ matrix.php }}${{ matrix.extension && ' + ext-wp-mysql-parser' || '' }} / SQLite ${{ matrix.sqlite }} + name: PHP ${{ matrix.php }} / SQLite ${{ matrix.sqlite }} runs-on: ubuntu-latest timeout-minutes: 30 permissions: @@ -41,24 +38,17 @@ jobs: fail-fast: false matrix: include: - # Pure-PHP parser, across the supported PHP versions, each pinned to a - # representative SQLite version spanning the supported range. - - { php: '7.2', sqlite: '3.27.0', extension: false } # minimum with WP_SQLITE_UNSAFE_ENABLE_UNSUPPORTED_VERSIONS - - { php: '7.3', sqlite: '3.31.1', extension: false } # Ubuntu 20.04 LTS - - { php: '7.4', sqlite: '3.34.1', extension: false } # Debian 11 (Bullseye) - - { php: '8.0', sqlite: '3.37.0', extension: false } # minimum supported version (STRICT tables) - - { php: '8.1', sqlite: '3.40.1', extension: false } # Debian 12 (Bookworm) - - { php: '8.2', sqlite: '3.45.1', extension: false } # Ubuntu 24.04 LTS - - { php: '8.3', sqlite: '3.46.1', extension: false } # Debian 13 (Trixie) - - { php: '8.4', sqlite: '3.51.2', extension: false } # First 2026 release - - { php: '8.5', sqlite: 'latest', extension: false } - # Native Rust parser extension (requires PHP 8.0+). - - { php: '8.0', sqlite: '3.37.0', extension: true } - - { php: '8.1', sqlite: '3.40.1', extension: true } - - { php: '8.2', sqlite: '3.45.1', extension: true } - - { php: '8.3', sqlite: '3.46.1', extension: true } - - { php: '8.4', sqlite: '3.51.2', extension: true } - - { php: '8.5', sqlite: 'latest', extension: true } + # All supported PHP versions, each pinned to a representative SQLite + # version spanning the supported range. + - { php: '7.2', sqlite: '3.27.0' } # minimum with WP_SQLITE_UNSAFE_ENABLE_UNSUPPORTED_VERSIONS + - { php: '7.3', sqlite: '3.31.1' } # Ubuntu 20.04 LTS + - { php: '7.4', sqlite: '3.34.1' } # Debian 11 (Bullseye) + - { php: '8.0', sqlite: '3.37.0' } # minimum supported version (STRICT tables) + - { php: '8.1', sqlite: '3.40.1' } # Debian 12 (Bookworm) + - { php: '8.2', sqlite: '3.45.1' } # Ubuntu 24.04 LTS + - { php: '8.3', sqlite: '3.46.1' } # Debian 13 (Trixie) + - { php: '8.4', sqlite: '3.51.2' } # First 2026 release + - { php: '8.5', sqlite: 'latest' } steps: - name: Checkout repository @@ -124,29 +114,6 @@ jobs: exit 1 fi - - name: Set up Rust - if: matrix.extension - uses: dtolnay/rust-toolchain@stable - - - name: Cache Rust build - if: matrix.extension - uses: Swatinem/rust-cache@v2 - with: - workspaces: packages/php-ext-wp-mysql-parser - # Segregate by PHP version: the extension links against the PHP headers - # of the matrix's php-config, so a build cached for one PHP version is - # ABI-incompatible with another (Zend module API mismatch on load). - key: php-${{ matrix.php }} - - - name: Install native build dependencies - if: matrix.extension - run: | - sudo apt-get update - sudo apt-get install -y libclang-dev - echo "PHP_CONFIG=$(command -v php-config)" >> "$GITHUB_ENV" - LIBCLANG_SO="$(find /usr/lib -name 'libclang.so*' | head -n 1)" - echo "LIBCLANG_PATH=$(dirname "$LIBCLANG_SO")" >> "$GITHUB_ENV" - - name: Install Composer dependencies (root) uses: ramsey/composer-install@v3 with: @@ -160,29 +127,6 @@ jobs: ignore-cache: "yes" composer-options: "--optimize-autoloader" - - name: Check Rust formatting - if: ${{ matrix.extension && matrix.php == '8.2' }} - run: cargo fmt --check - working-directory: packages/php-ext-wp-mysql-parser - - - name: Build parser extension - if: matrix.extension - run: cargo build --release - working-directory: packages/php-ext-wp-mysql-parser - - - name: Verify native parser extension - if: matrix.extension - run: php -d extension="$GITHUB_WORKSPACE/packages/php-ext-wp-mysql-parser/target/release/libwp_mysql_parser.so" tests/tools/verify-native-parser-extension.php - working-directory: packages/mysql-on-sqlite - - - name: Run PHPUnit suite with parser extension - if: matrix.extension - env: - WP_SQLITE_REQUIRE_NATIVE_PARSER_EXTENSION: '1' - run: php -d extension="$GITHUB_WORKSPACE/packages/php-ext-wp-mysql-parser/target/release/libwp_mysql_parser.so" ./vendor/bin/phpunit -c ./phpunit.xml.dist - working-directory: packages/mysql-on-sqlite - - name: Run PHPUnit suite - if: ${{ ! matrix.extension }} run: php ./vendor/bin/phpunit -c ./phpunit.xml.dist working-directory: packages/mysql-on-sqlite diff --git a/.github/workflows/wp-tests-phpunit-native-extension-setup.sh b/.github/workflows/wp-tests-phpunit-native-extension-setup.sh deleted file mode 100644 index 943b06c59..000000000 --- a/.github/workflows/wp-tests-phpunit-native-extension-setup.sh +++ /dev/null @@ -1,211 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -WP_DIR="$ROOT_DIR/wordpress" -COMPOSE_OVERRIDE="$WP_DIR/docker-compose.override.yml" -RUNTIME_DIR="$ROOT_DIR/tmp-native-extension" -EXTENSION_SOURCE_VOLUME=" - ../packages/php-ext-wp-mysql-parser:/var/native-parser-extension-src" -EXTENSION_RUNTIME_VOLUME=" - ../tmp-native-extension:/var/native-parser-extension:ro" -EXTENSION_INI_VOLUME=" - ../tmp-native-extension/wp-mysql-parser.ini:/usr/local/etc/php/conf.d/wp-mysql-parser.ini:ro" - -if [ ! -f "$COMPOSE_OVERRIDE" ]; then - echo "Missing $COMPOSE_OVERRIDE. Run composer run wp-setup first." >&2 - exit 1 -fi - -add_volume_to_service() { - local service="$1" - local volume="$2" - - node - "$COMPOSE_OVERRIDE" "$service" "$volume" <<'NODE' -const fs = require( 'fs' ); - -const file = process.argv[2]; -const service = process.argv[3]; -const volume = process.argv[4]; -const lines = fs.readFileSync( file, 'utf8' ).split( '\n' ); - -const serviceIndex = lines.findIndex( line => line === ` ${ service }:` ); -if ( serviceIndex === -1 ) { - throw new Error( `Service ${ service } not found in ${ file }.` ); -} - -let serviceEnd = lines.length; -for ( let i = serviceIndex + 1; i < lines.length; i++ ) { - if ( /^ [A-Za-z0-9_-]+:/.test( lines[i] ) ) { - serviceEnd = i; - break; - } -} - -if ( lines.slice( serviceIndex, serviceEnd ).some( line => line.trim() === volume.trim() ) ) { - process.exit( 0 ); -} - -let volumesIndex = -1; -for ( let i = serviceIndex + 1; i < serviceEnd; i++ ) { - if ( lines[i].trim() === 'volumes:' ) { - volumesIndex = i; - break; - } -} - -if ( volumesIndex === -1 ) { - throw new Error( `Service ${ service } has no volumes list in ${ file }.` ); -} - -let insertAt = volumesIndex + 1; -while ( insertAt < serviceEnd && /^\s{6}- /.test( lines[insertAt] ) ) { - insertAt++; -} - -lines.splice( insertAt, 0, volume ); -fs.writeFileSync( file, lines.join( '\n' ) ); -NODE -} - -add_volume_to_service php "$EXTENSION_SOURCE_VOLUME" -add_volume_to_service cli "$EXTENSION_SOURCE_VOLUME" - -cat > "$WP_DIR/native-build-extension.sh" <<'EOF' -#!/bin/sh -set -eu - -apt-get update -apt-get install -y --no-install-recommends ca-certificates curl build-essential clang libclang-dev pkg-config - -if [ ! -x "$HOME/.cargo/bin/cargo" ]; then - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable -fi - -. "$HOME/.cargo/env" - -PHP_CONFIG="$(command -v php-config)" -export PHP_CONFIG - -LIBCLANG_SO="$(find /usr/lib /usr/local/lib -name 'libclang.so*' 2>/dev/null | head -n 1)" -if [ -z "$LIBCLANG_SO" ]; then - echo "Unable to locate libclang.so after installing libclang-dev." >&2 - exit 1 -fi - -LIBCLANG_PATH="$(dirname "$LIBCLANG_SO")" -export LIBCLANG_PATH - -cd /var/native-parser-extension-src -cargo build --release -EOF - -chmod +x "$WP_DIR/native-build-extension.sh" - -cd "$WP_DIR" -node tools/local-env/scripts/docker.js run --rm php sh /var/www/native-build-extension.sh - -mkdir -p "$RUNTIME_DIR" -cp "$ROOT_DIR/packages/php-ext-wp-mysql-parser/target/release/libwp_mysql_parser.so" "$RUNTIME_DIR/libwp_mysql_parser.so" -printf '%s\n' 'extension=/var/native-parser-extension/libwp_mysql_parser.so' > "$RUNTIME_DIR/wp-mysql-parser.ini" - -add_volume_to_service php "$EXTENSION_RUNTIME_VOLUME" -add_volume_to_service cli "$EXTENSION_RUNTIME_VOLUME" -add_volume_to_service php "$EXTENSION_INI_VOLUME" -add_volume_to_service cli "$EXTENSION_INI_VOLUME" - -cat > "$WP_DIR/native-verify-extension.php" <<'EOF' -hasProperty( 'native' ) ) { - wp_sqlite_native_parser_verification_fail( $message ); - } - - $native_property = $parser_reflection->getProperty( 'native' ); - $native_property->setAccessible( true ); - if ( ! ( $native_property->getValue( $parser ) instanceof WP_MySQL_Native_Parser ) ) { - wp_sqlite_native_parser_verification_fail( $message ); - } -} - -$lexer = new WP_MySQL_Lexer( 'SELECT 1' ); -if ( ! ( $lexer instanceof WP_MySQL_Native_Lexer ) ) { - wp_sqlite_native_parser_verification_fail( 'Native lexer is not available in the WordPress PHP test container.' ); -} - -$tokens = $lexer->native_token_stream(); -$rules = include '/var/www/src/wp-content/plugins/sqlite-database-integration/wp-includes/database/mysql/mysql-grammar.php'; -$grammar = new WP_Parser_Grammar( $rules ); -$parser = new WP_MySQL_Parser( $grammar, $tokens ); -wp_sqlite_assert_native_parser_delegate( $parser, 'WordPress PHP test container did not select the native parser delegate.' ); - -$parser_ast = $parser->parse(); -if ( ! ( $parser_ast instanceof WP_MySQL_Native_Parser_Node ) ) { - wp_sqlite_native_parser_verification_fail( 'Native parser did not produce a native-backed AST in the WordPress PHP test container.' ); -} - -$driver = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname=wp;' ); -$parser = $driver->create_parser( 'SELECT 1' ); -wp_sqlite_assert_native_parser_delegate( $parser, 'WordPress PHP test container SQLite driver did not create a native parser delegate.' ); -$parser->next_query(); -$ast = $parser->get_query_ast(); - -if ( ! ( $ast instanceof WP_MySQL_Native_Parser_Node ) ) { - wp_sqlite_native_parser_verification_fail( 'WordPress PHP test container did not select the native-backed AST.' ); -} - -$reflection = new ReflectionObject( $ast ); -if ( $reflection->hasProperty( 'native_ast' ) || $reflection->hasProperty( 'native_node_index' ) ) { - wp_sqlite_native_parser_verification_fail( 'Native wrapper still stores Rust AST handle properties.' ); -} - -$first = $ast->get_first_child_node(); -if ( ! ( $first instanceof WP_MySQL_Native_Parser_Node ) ) { - wp_sqlite_native_parser_verification_fail( 'Native wrapper did not return a native-backed child node.' ); -} - -if ( $first !== $ast->get_first_child_node() ) { - wp_sqlite_native_parser_verification_fail( 'Native wrapper identity is not stable across reads.' ); -} - -$synthetic = new WP_Parser_Node( 0, 'synthetic' ); -$first->append_child( $synthetic ); -$same_first = $ast->get_first_child_node(); -if ( $same_first !== $first || ! in_array( $synthetic, $same_first->get_children(), true ) ) { - wp_sqlite_native_parser_verification_fail( 'Materialized native wrapper was lost from the parent cache.' ); -} -EOF - -node - "$WP_DIR/tests/phpunit/includes/bootstrap.php" <<'NODE' -const fs = require( 'fs' ); - -const file = process.argv[2]; -const marker = "require_once ABSPATH . 'wp-settings.php';"; -const guard = [ - '/*', - ' * Native parser extension guard. This file is generated by the SQLite integration workflow.', - ' */', - "require_once dirname( __DIR__, 3 ) . '/native-verify-extension.php';", -].join( '\n' ); - -let contents = fs.readFileSync( file, 'utf8' ); - -if ( contents.includes( guard ) ) { - process.exit( 0 ); -} - -if ( ! contents.includes( marker ) ) { - throw new Error( `Unable to find WordPress bootstrap marker in ${ file }.` ); -} - -contents = contents.replace( marker, `${ marker }\n\n${ guard }` ); -fs.writeFileSync( file, contents ); -NODE - -node tools/local-env/scripts/docker.js run --rm php php -m | grep -qx 'wp_mysql_parser' -node tools/local-env/scripts/docker.js run --rm php php /var/www/native-verify-extension.php diff --git a/.github/workflows/wp-tests-phpunit-run.js b/.github/workflows/wp-tests-phpunit-run.js index ae4f5f6a3..0878cd592 100644 --- a/.github/workflows/wp-tests-phpunit-run.js +++ b/.github/workflows/wp-tests-phpunit-run.js @@ -9,8 +9,6 @@ const { execSync } = require( 'child_process' ); const fs = require( 'fs' ); const path = require( 'path' ); -const requiresNativeParserExtension = process.env.WP_SQLITE_REQUIRE_NATIVE_PARSER_EXTENSION === '1'; - const expectedErrors = [ 'Tests_DB_Charset::test_invalid_characters_in_query', 'Tests_DB_Charset::test_set_charset_changes_the_connection_collation', @@ -91,31 +89,10 @@ const expectedFailures = [ ]; console.log( 'Running WordPress PHPUnit tests with expected failures tracking...' ); -if ( requiresNativeParserExtension ) { - console.log( 'Native parser extension is required for this PHPUnit run.' ); -} console.log( 'Expected errors:', expectedErrors ); console.log( 'Expected failures:', expectedFailures ); -function verifyNativeParserExtension() { - const verifier = path.join( __dirname, '..', '..', 'wordpress', 'native-verify-extension.php' ); - if ( ! fs.existsSync( verifier ) ) { - console.error( `Error: Native parser verifier not found at ${ verifier }.` ); - process.exit( 1 ); - } - - execSync( 'composer run wp-test-ensure-env', { stdio: 'inherit' } ); - execSync( - 'cd wordpress && node tools/local-env/scripts/docker.js run --rm php php /var/www/native-verify-extension.php', - { stdio: 'inherit' } - ); -} - try { - if ( requiresNativeParserExtension ) { - verifyNativeParserExtension(); - } - try { execSync( `composer run wp-test-php -- --log-junit=phpunit-results.xml --verbose`, diff --git a/.github/workflows/wp-tests-phpunit.yml b/.github/workflows/wp-tests-phpunit.yml index 810b77b8a..a49cfc043 100644 --- a/.github/workflows/wp-tests-phpunit.yml +++ b/.github/workflows/wp-tests-phpunit.yml @@ -36,37 +36,3 @@ jobs: - name: Stop Docker containers if: always() run: composer run wp-test-clean - - native-parser-test: - name: WordPress PHPUnit Tests / Rust extension - runs-on: ubuntu-latest - timeout-minutes: 40 - permissions: - contents: read # Required to clone the repo. - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Set UID and GID for PHP in WordPress images - run: | - echo "PHP_FPM_UID=$(id -u)" >> $GITHUB_ENV - echo "PHP_FPM_GID=$(id -g)" >> $GITHUB_ENV - - - name: Set up WordPress test environment - run: composer run wp-setup - - - name: Build and load parser extension in WordPress PHP containers - run: bash .github/workflows/wp-tests-phpunit-native-extension-setup.sh - - - name: Run WordPress PHPUnit tests with parser extension - env: - WP_SQLITE_REQUIRE_NATIVE_PARSER_EXTENSION: '1' - run: node .github/workflows/wp-tests-phpunit-run.js - - - name: Stop Docker containers - if: always() - run: composer run wp-test-clean diff --git a/bin/build-sqlite-plugin-zip.sh b/bin/build-sqlite-plugin-zip.sh index fcb80e7ef..7b984bdcf 100755 --- a/bin/build-sqlite-plugin-zip.sh +++ b/bin/build-sqlite-plugin-zip.sh @@ -26,6 +26,26 @@ cp -R "$DIR/packages/plugin-sqlite-database-integration" "$PLUGIN_DIR" rm "$PLUGIN_DIR/wp-includes/database" cp -R "$DIR/packages/mysql-on-sqlite/src" "$PLUGIN_DIR/wp-includes/database" +# Bundle the driver's production Composer dependencies (wordpress/mysql-parser). +# The dependencies are installed into a temporary vendor directory, so that the +# development vendor directory of the package is left untouched, and copied with +# the path-repository symlink resolved into a real copy of the parser package. +COMPOSER_VENDOR_DIR="$BUILD_DIR/driver-vendor" composer install \ + --working-dir "$DIR/packages/mysql-on-sqlite" \ + --no-dev --no-interaction --optimize-autoloader --quiet +cp -RL "$BUILD_DIR/driver-vendor" "$PLUGIN_DIR/wp-includes/vendor" +rm -rf "$BUILD_DIR/driver-vendor" + +# Strip development files from the bundled parser package. +rm -rf "$PLUGIN_DIR/wp-includes/vendor/wordpress/mysql-parser/bin" \ + "$PLUGIN_DIR/wp-includes/vendor/wordpress/mysql-parser/build" \ + "$PLUGIN_DIR/wp-includes/vendor/wordpress/mysql-parser/data" \ + "$PLUGIN_DIR/wp-includes/vendor/wordpress/mysql-parser/tests" \ + "$PLUGIN_DIR/wp-includes/vendor/wordpress/mysql-parser/tools" \ + "$PLUGIN_DIR/wp-includes/vendor/wordpress/mysql-parser/vendor" \ + "$PLUGIN_DIR/wp-includes/vendor/wordpress/mysql-parser/composer.lock" \ + "$PLUGIN_DIR/wp-includes/vendor/wordpress/mysql-parser/phpunit.xml.dist" + # Remove dev-only files. rm -rf "$PLUGIN_DIR/composer.json" rm -rf "$PLUGIN_DIR/vendor" diff --git a/packages/mysql-on-sqlite/composer.json b/packages/mysql-on-sqlite/composer.json index c7ef2b417..1065e0810 100644 --- a/packages/mysql-on-sqlite/composer.json +++ b/packages/mysql-on-sqlite/composer.json @@ -1,14 +1,23 @@ { - "name": "wordpress/mysql-on-sqlite", - "type": "library", + "name": "wordpress/mysql-on-sqlite", + "type": "library", + "require": { + "wordpress/mysql-parser": "@dev" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "repositories": [ + { + "type": "path", + "url": "../mysql-parser" + } + ], "scripts": { "test": "phpunit", "bench-lexer": [ "@php tests/tools/run-lexer-benchmark.php", "@php -d opcache.enable_cli=1 -d opcache.jit_buffer_size=64M -d opcache.jit=tracing tests/tools/run-lexer-benchmark.php" ] - }, - "require-dev": { - "phpunit/phpunit": "^8.5" - } + } } diff --git a/packages/mysql-on-sqlite/src/load.php b/packages/mysql-on-sqlite/src/load.php index 62387a2e7..4fff24fc6 100644 --- a/packages/mysql-on-sqlite/src/load.php +++ b/packages/mysql-on-sqlite/src/load.php @@ -4,35 +4,14 @@ /** * Load the PDO MySQL-on-SQLite driver and its dependencies. + * + * The MySQL lexer and parser are provided by the "wordpress/mysql-parser" + * Composer package and are loaded through the Composer autoloader. */ +require_once __DIR__ . '/../vendor/autoload.php'; + require_once __DIR__ . '/php-polyfills.php'; require_once __DIR__ . '/version.php'; -require_once __DIR__ . '/parser/class-wp-parser-grammar.php'; -require_once __DIR__ . '/parser/class-wp-parser.php'; -require_once __DIR__ . '/parser/class-wp-parser-node.php'; -require_once __DIR__ . '/parser/class-wp-parser-token.php'; -require_once __DIR__ . '/mysql/class-wp-mysql-token.php'; - -/* - * The MySQL lexer and parser have an optional native (e.g. Rust) implementation. - * When the native extension is loaded, it pre-declares WP_MySQL_Native_Lexer / - * WP_MySQL_Native_Parser; otherwise we fall back to the pure-PHP classes shipped - * here. WP_MySQL_Lexer / WP_MySQL_Parser is the public entrypoint either way. - */ -if ( class_exists( 'WP_MySQL_Native_Lexer', false ) ) { - require_once __DIR__ . '/mysql/native/class-wp-mysql-lexer.php'; -} else { - require_once __DIR__ . '/mysql/class-wp-mysql-lexer.php'; -} - -if ( class_exists( 'WP_MySQL_Native_Parser', false ) ) { - require_once __DIR__ . '/mysql/native/mysql-rust-bridge.php'; - require_once __DIR__ . '/mysql/native/class-wp-mysql-native-parser-node.php'; - require_once __DIR__ . '/mysql/native/trait-wp-mysql-native-parser-impl.php'; - require_once __DIR__ . '/mysql/native/class-wp-mysql-parser.php'; -} else { - require_once __DIR__ . '/mysql/class-wp-mysql-parser.php'; -} require_once __DIR__ . '/sqlite/class-wp-sqlite-connection.php'; require_once __DIR__ . '/sqlite/class-wp-sqlite-configurator.php'; require_once __DIR__ . '/sqlite/class-wp-sqlite-driver.php'; diff --git a/packages/mysql-on-sqlite/src/mysql/class-wp-mysql-lexer.php b/packages/mysql-on-sqlite/src/mysql/class-wp-mysql-lexer.php deleted file mode 100644 index d6ee9970e..000000000 --- a/packages/mysql-on-sqlite/src/mysql/class-wp-mysql-lexer.php +++ /dev/null @@ -1,3070 +0,0 @@ -sql_modes". - * The list of the SQL modes is not exhaustive. Only the ones that influence - * the lexer behavior are included in this list. - * - * See: - * https://dev.mysql.com/doc/refman/8.4/en/sql-mode.html - */ - const SQL_MODE_HIGH_NOT_PRECEDENCE = 1; - const SQL_MODE_PIPES_AS_CONCAT = 2; - const SQL_MODE_IGNORE_SPACE = 4; - const SQL_MODE_NO_BACKSLASH_ESCAPES = 8; - - /** - * Character masks for frequently used character classes. - * - * These are intended to be used with "strspn()" and "strcspn()" functions - * for fast character class matching in the SQL payload. - */ - const WHITESPACE_MASK = " \t\n\r\f"; - const DIGIT_MASK = '0123456789'; - const HEX_DIGIT_MASK = '0123456789abcdefABCDEF'; - - /** - * Tokens from the MySQL Workbench "predefined.tokens" list. - * - * This list preserves the token names and IDs from the MySQL Workbench - * "predefined.tokens" list, adding some tokens missing from the list. - * - * See: - * https://github.com/mysql/mysql-workbench/blob/8.0.38/library/parsers/grammars/predefined.tokens - */ - const ACCESSIBLE_SYMBOL = 1; - const ACCOUNT_SYMBOL = 2; - const ACTION_SYMBOL = 3; - const ADD_SYMBOL = 4; - const ADDDATE_SYMBOL = 5; - const AFTER_SYMBOL = 6; - const AGAINST_SYMBOL = 7; - const AGGREGATE_SYMBOL = 8; - const ALGORITHM_SYMBOL = 9; - const ALL_SYMBOL = 10; - const ALTER_SYMBOL = 11; - const ALWAYS_SYMBOL = 12; - const ANALYSE_SYMBOL = 13; - const ANALYZE_SYMBOL = 14; - const AND_SYMBOL = 15; - const ANY_SYMBOL = 16; - const AS_SYMBOL = 17; - const ASC_SYMBOL = 18; - const ASCII_SYMBOL = 19; - const ASENSITIVE_SYMBOL = 20; - const AT_SYMBOL = 21; - const AUTHORS_SYMBOL = 22; - const AUTOEXTEND_SIZE_SYMBOL = 23; - const AUTO_INCREMENT_SYMBOL = 24; - const AVG_ROW_LENGTH_SYMBOL = 25; - const AVG_SYMBOL = 26; - const BACKUP_SYMBOL = 27; - const BEFORE_SYMBOL = 28; - const BEGIN_SYMBOL = 29; - const BETWEEN_SYMBOL = 30; - const BIGINT_SYMBOL = 31; - const BINARY_SYMBOL = 32; - const BINLOG_SYMBOL = 33; - const BIN_NUM_SYMBOL = 34; - const BIT_AND_SYMBOL = 35; - const BIT_OR_SYMBOL = 36; - const BIT_SYMBOL = 37; - const BIT_XOR_SYMBOL = 38; - const BLOB_SYMBOL = 39; - const BLOCK_SYMBOL = 40; - const BOOLEAN_SYMBOL = 41; - const BOOL_SYMBOL = 42; - const BOTH_SYMBOL = 43; - const BTREE_SYMBOL = 44; - const BY_SYMBOL = 45; - const BYTE_SYMBOL = 46; - const CACHE_SYMBOL = 47; - const CALL_SYMBOL = 48; - const CASCADE_SYMBOL = 49; - const CASCADED_SYMBOL = 50; - const CASE_SYMBOL = 51; - const CAST_SYMBOL = 52; - const CATALOG_NAME_SYMBOL = 53; - const CHAIN_SYMBOL = 54; - const CHANGE_SYMBOL = 55; - const CHANGED_SYMBOL = 56; - const CHANNEL_SYMBOL = 57; - const CHARSET_SYMBOL = 58; - const CHARACTER_SYMBOL = 59; - const CHAR_SYMBOL = 60; - const CHECKSUM_SYMBOL = 61; - const CHECK_SYMBOL = 62; - const CIPHER_SYMBOL = 63; - const CLASS_ORIGIN_SYMBOL = 64; - const CLIENT_SYMBOL = 65; - const CLOSE_SYMBOL = 66; - const COALESCE_SYMBOL = 67; - const CODE_SYMBOL = 68; - const COLLATE_SYMBOL = 69; - const COLLATION_SYMBOL = 70; - const COLUMNS_SYMBOL = 71; - const COLUMN_SYMBOL = 72; - const COLUMN_NAME_SYMBOL = 73; - const COLUMN_FORMAT_SYMBOL = 74; - const COMMENT_SYMBOL = 75; - const COMMITTED_SYMBOL = 76; - const COMMIT_SYMBOL = 77; - const COMPACT_SYMBOL = 78; - const COMPLETION_SYMBOL = 79; - const COMPRESSED_SYMBOL = 80; - const COMPRESSION_SYMBOL = 81; - const CONCURRENT_SYMBOL = 82; - const CONDITION_SYMBOL = 83; - const CONNECTION_SYMBOL = 84; - const CONSISTENT_SYMBOL = 85; - const CONSTRAINT_SYMBOL = 86; - const CONSTRAINT_CATALOG_SYMBOL = 87; - const CONSTRAINT_NAME_SYMBOL = 88; - const CONSTRAINT_SCHEMA_SYMBOL = 89; - const CONTAINS_SYMBOL = 90; - const CONTEXT_SYMBOL = 91; - const CONTINUE_SYMBOL = 92; - const CONTRIBUTORS_SYMBOL = 93; - const CONVERT_SYMBOL = 94; - const COUNT_SYMBOL = 95; - const CPU_SYMBOL = 96; - const CREATE_SYMBOL = 97; - const CROSS_SYMBOL = 98; - const CUBE_SYMBOL = 99; - const CURDATE_SYMBOL = 100; - const CURRENT_SYMBOL = 101; - const CURRENT_DATE_SYMBOL = 102; - const CURRENT_TIME_SYMBOL = 103; - const CURRENT_TIMESTAMP_SYMBOL = 104; - const CURRENT_USER_SYMBOL = 105; - const CURSOR_SYMBOL = 106; - const CURSOR_NAME_SYMBOL = 107; - const CURTIME_SYMBOL = 108; - const DATABASE_SYMBOL = 109; - const DATABASES_SYMBOL = 110; - const DATAFILE_SYMBOL = 111; - const DATA_SYMBOL = 112; - const DATETIME_SYMBOL = 113; - const DATE_ADD_SYMBOL = 114; - const DATE_SUB_SYMBOL = 115; - const DATE_SYMBOL = 116; - const DAYOFMONTH_SYMBOL = 117; - const DAY_HOUR_SYMBOL = 118; - const DAY_MICROSECOND_SYMBOL = 119; - const DAY_MINUTE_SYMBOL = 120; - const DAY_SECOND_SYMBOL = 121; - const DAY_SYMBOL = 122; - const DEALLOCATE_SYMBOL = 123; - const DEC_SYMBOL = 124; - const DECIMAL_NUM_SYMBOL = 125; - const DECIMAL_SYMBOL = 126; - const DECLARE_SYMBOL = 127; - const DEFAULT_SYMBOL = 128; - const DEFAULT_AUTH_SYMBOL = 129; - const DEFINER_SYMBOL = 130; - const DELAYED_SYMBOL = 131; - const DELAY_KEY_WRITE_SYMBOL = 132; - const DELETE_SYMBOL = 133; - const DESC_SYMBOL = 134; - const DESCRIBE_SYMBOL = 135; - const DES_KEY_FILE_SYMBOL = 136; - const DETERMINISTIC_SYMBOL = 137; - const DIAGNOSTICS_SYMBOL = 138; - const DIRECTORY_SYMBOL = 139; - const DISABLE_SYMBOL = 140; - const DISCARD_SYMBOL = 141; - const DISK_SYMBOL = 142; - const DISTINCT_SYMBOL = 143; - const DISTINCTROW_SYMBOL = 144; - const DIV_SYMBOL = 145; - const DOUBLE_SYMBOL = 146; - const DO_SYMBOL = 147; - const DROP_SYMBOL = 148; - const DUAL_SYMBOL = 149; - const DUMPFILE_SYMBOL = 150; - const DUPLICATE_SYMBOL = 151; - const DYNAMIC_SYMBOL = 152; - const EACH_SYMBOL = 153; - const ELSE_SYMBOL = 154; - const ELSEIF_SYMBOL = 155; - const ENABLE_SYMBOL = 156; - const ENCLOSED_SYMBOL = 157; - const ENCRYPTION_SYMBOL = 158; - const END_SYMBOL = 159; - const ENDS_SYMBOL = 160; - const END_OF_INPUT_SYMBOL = 161; // defined in "predefined.tokens", but not used - const ENGINES_SYMBOL = 162; - const ENGINE_SYMBOL = 163; - const ENUM_SYMBOL = 164; - const ERROR_SYMBOL = 165; - const ERRORS_SYMBOL = 166; - const ESCAPED_SYMBOL = 167; - const ESCAPE_SYMBOL = 168; - const EVENTS_SYMBOL = 169; - const EVENT_SYMBOL = 170; - const EVERY_SYMBOL = 171; - const EXCHANGE_SYMBOL = 172; - const EXECUTE_SYMBOL = 173; - const EXISTS_SYMBOL = 174; - const EXIT_SYMBOL = 175; - const EXPANSION_SYMBOL = 176; - const EXPIRE_SYMBOL = 177; - const EXPLAIN_SYMBOL = 178; - const EXPORT_SYMBOL = 179; - const EXTENDED_SYMBOL = 180; - const EXTENT_SIZE_SYMBOL = 181; - const EXTRACT_SYMBOL = 182; - const FALSE_SYMBOL = 183; - const FAST_SYMBOL = 184; - const FAULTS_SYMBOL = 185; - const FETCH_SYMBOL = 186; - const FIELDS_SYMBOL = 187; - const FILE_SYMBOL = 188; - const FILE_BLOCK_SIZE_SYMBOL = 189; - const FILTER_SYMBOL = 190; - const FIRST_SYMBOL = 191; - const FIXED_SYMBOL = 192; - const FLOAT4_SYMBOL = 193; - const FLOAT8_SYMBOL = 194; - const FLOAT_SYMBOL = 195; - const FLUSH_SYMBOL = 196; - const FOLLOWS_SYMBOL = 197; - const FORCE_SYMBOL = 198; - const FOREIGN_SYMBOL = 199; - const FOR_SYMBOL = 200; - const FORMAT_SYMBOL = 201; - const FOUND_SYMBOL = 202; - const FROM_SYMBOL = 203; - const FULL_SYMBOL = 204; - const FULLTEXT_SYMBOL = 205; - const FUNCTION_SYMBOL = 206; - const GET_SYMBOL = 207; - const GENERAL_SYMBOL = 208; - const GENERATED_SYMBOL = 209; - const GROUP_REPLICATION_SYMBOL = 210; - const GEOMETRYCOLLECTION_SYMBOL = 211; - const GEOMETRY_SYMBOL = 212; - const GET_FORMAT_SYMBOL = 213; - const GLOBAL_SYMBOL = 214; - const GRANT_SYMBOL = 215; - const GRANTS_SYMBOL = 216; - const GROUP_SYMBOL = 217; - const GROUP_CONCAT_SYMBOL = 218; - const HANDLER_SYMBOL = 219; - const HASH_SYMBOL = 220; - const HAVING_SYMBOL = 221; - const HELP_SYMBOL = 222; - const HIGH_PRIORITY_SYMBOL = 223; - const HOST_SYMBOL = 224; - const HOSTS_SYMBOL = 225; - const HOUR_MICROSECOND_SYMBOL = 226; - const HOUR_MINUTE_SYMBOL = 227; - const HOUR_SECOND_SYMBOL = 228; - const HOUR_SYMBOL = 229; - const IDENTIFIED_SYMBOL = 230; - const IF_SYMBOL = 231; - const IGNORE_SYMBOL = 232; - const IGNORE_SERVER_IDS_SYMBOL = 233; - const IMPORT_SYMBOL = 234; - const INDEXES_SYMBOL = 235; - const INDEX_SYMBOL = 236; - const INFILE_SYMBOL = 237; - const INITIAL_SIZE_SYMBOL = 238; - const INNER_SYMBOL = 239; - const INOUT_SYMBOL = 240; - const INSENSITIVE_SYMBOL = 241; - const INSERT_SYMBOL = 242; - const INSERT_METHOD_SYMBOL = 243; - const INSTANCE_SYMBOL = 244; - const INSTALL_SYMBOL = 245; - const INTEGER_SYMBOL = 246; - const INTERVAL_SYMBOL = 247; - const INTO_SYMBOL = 248; - const INT_SYMBOL = 249; - const INVOKER_SYMBOL = 250; - const IN_SYMBOL = 251; - const IO_AFTER_GTIDS_SYMBOL = 252; - const IO_BEFORE_GTIDS_SYMBOL = 253; - const IO_THREAD_SYMBOL = 254; - const IO_SYMBOL = 255; - const IPC_SYMBOL = 256; - const IS_SYMBOL = 257; - const ISOLATION_SYMBOL = 258; - const ISSUER_SYMBOL = 259; - const ITERATE_SYMBOL = 260; - const JOIN_SYMBOL = 261; - const JSON_SYMBOL = 262; - const KEYS_SYMBOL = 263; - const KEY_BLOCK_SIZE_SYMBOL = 264; - const KEY_SYMBOL = 265; - const KILL_SYMBOL = 266; - const LANGUAGE_SYMBOL = 267; - const LAST_SYMBOL = 268; - const LEADING_SYMBOL = 269; - const LEAVES_SYMBOL = 270; - const LEAVE_SYMBOL = 271; - const LEFT_SYMBOL = 272; - const LESS_SYMBOL = 273; - const LEVEL_SYMBOL = 274; - const LIKE_SYMBOL = 275; - const LIMIT_SYMBOL = 276; - const LINEAR_SYMBOL = 277; - const LINES_SYMBOL = 278; - const LINESTRING_SYMBOL = 279; - const LIST_SYMBOL = 280; - const LOAD_SYMBOL = 281; - const LOCALTIME_SYMBOL = 282; - const LOCALTIMESTAMP_SYMBOL = 283; - const LOCAL_SYMBOL = 284; - const LOCATOR_SYMBOL = 285; - const LOCKS_SYMBOL = 286; - const LOCK_SYMBOL = 287; - const LOGFILE_SYMBOL = 288; - const LOGS_SYMBOL = 289; - const LONGBLOB_SYMBOL = 290; - const LONGTEXT_SYMBOL = 291; - const LONG_NUM_SYMBOL = 292; - const LONG_SYMBOL = 293; - const LOOP_SYMBOL = 294; - const LOW_PRIORITY_SYMBOL = 295; - const MASTER_AUTO_POSITION_SYMBOL = 296; - const MASTER_BIND_SYMBOL = 297; - const MASTER_CONNECT_RETRY_SYMBOL = 298; - const MASTER_DELAY_SYMBOL = 299; - const MASTER_HOST_SYMBOL = 300; - const MASTER_LOG_FILE_SYMBOL = 301; - const MASTER_LOG_POS_SYMBOL = 302; - const MASTER_PASSWORD_SYMBOL = 303; - const MASTER_PORT_SYMBOL = 304; - const MASTER_RETRY_COUNT_SYMBOL = 305; - const MASTER_SERVER_ID_SYMBOL = 306; - const MASTER_SSL_CAPATH_SYMBOL = 307; - const MASTER_SSL_CA_SYMBOL = 308; - const MASTER_SSL_CERT_SYMBOL = 309; - const MASTER_SSL_CIPHER_SYMBOL = 310; - const MASTER_SSL_CRL_SYMBOL = 311; - const MASTER_SSL_CRLPATH_SYMBOL = 312; - const MASTER_SSL_KEY_SYMBOL = 313; - const MASTER_SSL_SYMBOL = 314; - const MASTER_SSL_VERIFY_SERVER_CERT_SYMBOL = 315; - const MASTER_SYMBOL = 316; - const MASTER_TLS_VERSION_SYMBOL = 317; - const MASTER_USER_SYMBOL = 318; - const MASTER_HEARTBEAT_PERIOD_SYMBOL = 319; - const MATCH_SYMBOL = 320; - const MAX_CONNECTIONS_PER_HOUR_SYMBOL = 321; - const MAX_QUERIES_PER_HOUR_SYMBOL = 322; - const MAX_ROWS_SYMBOL = 323; - const MAX_SIZE_SYMBOL = 324; - const MAX_STATEMENT_TIME_SYMBOL = 325; - const MAX_SYMBOL = 326; - const MAX_UPDATES_PER_HOUR_SYMBOL = 327; - const MAX_USER_CONNECTIONS_SYMBOL = 328; - const MAXVALUE_SYMBOL = 329; - const MEDIUMBLOB_SYMBOL = 330; - const MEDIUMINT_SYMBOL = 331; - const MEDIUMTEXT_SYMBOL = 332; - const MEDIUM_SYMBOL = 333; - const MEMORY_SYMBOL = 334; - const MERGE_SYMBOL = 335; - const MESSAGE_TEXT_SYMBOL = 336; - const MICROSECOND_SYMBOL = 337; - const MID_SYMBOL = 338; - const MIDDLEINT_SYMBOL = 339; - const MIGRATE_SYMBOL = 340; - const MINUTE_MICROSECOND_SYMBOL = 341; - const MINUTE_SECOND_SYMBOL = 342; - const MINUTE_SYMBOL = 343; - const MIN_ROWS_SYMBOL = 344; - const MIN_SYMBOL = 345; - const MODE_SYMBOL = 346; - const MODIFIES_SYMBOL = 347; - const MODIFY_SYMBOL = 348; - const MOD_SYMBOL = 349; - const MONTH_SYMBOL = 350; - const MULTILINESTRING_SYMBOL = 351; - const MULTIPOINT_SYMBOL = 352; - const MULTIPOLYGON_SYMBOL = 353; - const MUTEX_SYMBOL = 354; - const MYSQL_ERRNO_SYMBOL = 355; - const NAMES_SYMBOL = 356; - const NAME_SYMBOL = 357; - const NATIONAL_SYMBOL = 358; - const NATURAL_SYMBOL = 359; - const NCHAR_STRING_SYMBOL = 360; - const NCHAR_SYMBOL = 361; - const NDB_SYMBOL = 362; - const NDBCLUSTER_SYMBOL = 363; - const NEG_SYMBOL = 364; - const NEVER_SYMBOL = 365; - const NEW_SYMBOL = 366; - const NEXT_SYMBOL = 367; - const NODEGROUP_SYMBOL = 368; - const NONE_SYMBOL = 369; - const NONBLOCKING_SYMBOL = 370; - const NOT_SYMBOL = 371; - const NOW_SYMBOL = 372; - const NO_SYMBOL = 373; - const NO_WAIT_SYMBOL = 374; - const NO_WRITE_TO_BINLOG_SYMBOL = 375; - const NULL_SYMBOL = 376; - const NUMBER_SYMBOL = 377; - const NUMERIC_SYMBOL = 378; - const NVARCHAR_SYMBOL = 379; - const OFFLINE_SYMBOL = 380; - const OFFSET_SYMBOL = 381; - const OLD_PASSWORD_SYMBOL = 382; - const ON_SYMBOL = 383; - const ONE_SYMBOL = 384; - const ONLINE_SYMBOL = 385; - const ONLY_SYMBOL = 386; - const OPEN_SYMBOL = 387; - const OPTIMIZE_SYMBOL = 388; - const OPTIMIZER_COSTS_SYMBOL = 389; - const OPTIONS_SYMBOL = 390; - const OPTION_SYMBOL = 391; - const OPTIONALLY_SYMBOL = 392; - const ORDER_SYMBOL = 393; - const OR_SYMBOL = 394; - const OUTER_SYMBOL = 395; - const OUTFILE_SYMBOL = 396; - const OUT_SYMBOL = 397; - const OWNER_SYMBOL = 398; - const PACK_KEYS_SYMBOL = 399; - const PAGE_SYMBOL = 400; - const PARSER_SYMBOL = 401; - const PARTIAL_SYMBOL = 402; - const PARTITIONING_SYMBOL = 403; - const PARTITIONS_SYMBOL = 404; - const PARTITION_SYMBOL = 405; - const PASSWORD_SYMBOL = 406; - const PHASE_SYMBOL = 407; - const PLUGINS_SYMBOL = 408; - const PLUGIN_DIR_SYMBOL = 409; - const PLUGIN_SYMBOL = 410; - const POINT_SYMBOL = 411; - const POLYGON_SYMBOL = 412; - const PORT_SYMBOL = 413; - const POSITION_SYMBOL = 414; - const PRECEDES_SYMBOL = 415; - const PRECISION_SYMBOL = 416; - const PREPARE_SYMBOL = 417; - const PRESERVE_SYMBOL = 418; - const PREV_SYMBOL = 419; - const PRIMARY_SYMBOL = 420; - const PRIVILEGES_SYMBOL = 421; - const PROCEDURE_SYMBOL = 422; - const PROCESS_SYMBOL = 423; - const PROCESSLIST_SYMBOL = 424; - const PROFILE_SYMBOL = 425; - const PROFILES_SYMBOL = 426; - const PROXY_SYMBOL = 427; - const PURGE_SYMBOL = 428; - const QUARTER_SYMBOL = 429; - const QUERY_SYMBOL = 430; - const QUICK_SYMBOL = 431; - const RANGE_SYMBOL = 432; - const READS_SYMBOL = 433; - const READ_ONLY_SYMBOL = 434; - const READ_SYMBOL = 435; - const READ_WRITE_SYMBOL = 436; - const REAL_SYMBOL = 437; - const REBUILD_SYMBOL = 438; - const RECOVER_SYMBOL = 439; - const REDOFILE_SYMBOL = 440; - const REDO_BUFFER_SIZE_SYMBOL = 441; - const REDUNDANT_SYMBOL = 442; - const REFERENCES_SYMBOL = 443; - const REGEXP_SYMBOL = 444; - const RELAY_SYMBOL = 445; - const RELAYLOG_SYMBOL = 446; - const RELAY_LOG_FILE_SYMBOL = 447; - const RELAY_LOG_POS_SYMBOL = 448; - const RELAY_THREAD_SYMBOL = 449; - const RELEASE_SYMBOL = 450; - const RELOAD_SYMBOL = 451; - const REMOVE_SYMBOL = 452; - const RENAME_SYMBOL = 453; - const REORGANIZE_SYMBOL = 454; - const REPAIR_SYMBOL = 455; - const REPEATABLE_SYMBOL = 456; - const REPEAT_SYMBOL = 457; - const REPLACE_SYMBOL = 458; - const REPLICATION_SYMBOL = 459; - const REPLICATE_DO_DB_SYMBOL = 460; - const REPLICATE_IGNORE_DB_SYMBOL = 461; - const REPLICATE_DO_TABLE_SYMBOL = 462; - const REPLICATE_IGNORE_TABLE_SYMBOL = 463; - const REPLICATE_WILD_DO_TABLE_SYMBOL = 464; - const REPLICATE_WILD_IGNORE_TABLE_SYMBOL = 465; - const REPLICATE_REWRITE_DB_SYMBOL = 466; - const REQUIRE_SYMBOL = 467; - const RESET_SYMBOL = 468; - const RESIGNAL_SYMBOL = 469; - const RESTORE_SYMBOL = 470; - const RESTRICT_SYMBOL = 471; - const RESUME_SYMBOL = 472; - const RETURNED_SQLSTATE_SYMBOL = 473; - const RETURNS_SYMBOL = 474; - const RETURN_SYMBOL = 475; - const REVERSE_SYMBOL = 476; - const REVOKE_SYMBOL = 477; - const RIGHT_SYMBOL = 478; - const RLIKE_SYMBOL = 479; - const ROLLBACK_SYMBOL = 480; - const ROLLUP_SYMBOL = 481; - const ROTATE_SYMBOL = 482; - const ROUTINE_SYMBOL = 483; - const ROWS_SYMBOL = 484; - const ROW_COUNT_SYMBOL = 485; - const ROW_FORMAT_SYMBOL = 486; - const ROW_SYMBOL = 487; - const RTREE_SYMBOL = 488; - const SAVEPOINT_SYMBOL = 489; - const SCHEDULE_SYMBOL = 490; - const SCHEMA_SYMBOL = 491; - const SCHEMA_NAME_SYMBOL = 492; - const SCHEMAS_SYMBOL = 493; - const SECOND_MICROSECOND_SYMBOL = 494; - const SECOND_SYMBOL = 495; - const SECURITY_SYMBOL = 496; - const SELECT_SYMBOL = 497; - const SENSITIVE_SYMBOL = 498; - const SEPARATOR_SYMBOL = 499; - const SERIALIZABLE_SYMBOL = 500; - const SERIAL_SYMBOL = 501; - const SESSION_SYMBOL = 502; - const SERVER_SYMBOL = 503; - const SERVER_OPTIONS_SYMBOL = 504; - const SESSION_USER_SYMBOL = 505; - const SET_SYMBOL = 506; - const SET_VAR_SYMBOL = 507; - const SHARE_SYMBOL = 508; - const SHOW_SYMBOL = 509; - const SHUTDOWN_SYMBOL = 510; - const SIGNAL_SYMBOL = 511; - const SIGNED_SYMBOL = 512; - const SIMPLE_SYMBOL = 513; - const SLAVE_SYMBOL = 514; - const SLOW_SYMBOL = 515; - const SMALLINT_SYMBOL = 516; - const SNAPSHOT_SYMBOL = 517; - const SOME_SYMBOL = 518; - const SOCKET_SYMBOL = 519; - const SONAME_SYMBOL = 520; - const SOUNDS_SYMBOL = 521; - const SOURCE_SYMBOL = 522; - const SPATIAL_SYMBOL = 523; - const SPECIFIC_SYMBOL = 524; - const SQLEXCEPTION_SYMBOL = 525; - const SQLSTATE_SYMBOL = 526; - const SQLWARNING_SYMBOL = 527; - const SQL_AFTER_GTIDS_SYMBOL = 528; - const SQL_AFTER_MTS_GAPS_SYMBOL = 529; - const SQL_BEFORE_GTIDS_SYMBOL = 530; - const SQL_BIG_RESULT_SYMBOL = 531; - const SQL_BUFFER_RESULT_SYMBOL = 532; - const SQL_CACHE_SYMBOL = 533; - const SQL_CALC_FOUND_ROWS_SYMBOL = 534; - const SQL_NO_CACHE_SYMBOL = 535; - const SQL_SMALL_RESULT_SYMBOL = 536; - const SQL_SYMBOL = 537; - const SQL_THREAD_SYMBOL = 538; - const SSL_SYMBOL = 539; - const STACKED_SYMBOL = 540; - const STARTING_SYMBOL = 541; - const STARTS_SYMBOL = 542; - const START_SYMBOL = 543; - const STATS_AUTO_RECALC_SYMBOL = 544; - const STATS_PERSISTENT_SYMBOL = 545; - const STATS_SAMPLE_PAGES_SYMBOL = 546; - const STATUS_SYMBOL = 547; - const STDDEV_SAMP_SYMBOL = 548; - const STDDEV_SYMBOL = 549; - const STDDEV_POP_SYMBOL = 550; - const STD_SYMBOL = 551; - const STOP_SYMBOL = 552; - const STORAGE_SYMBOL = 553; - const STORED_SYMBOL = 554; - const STRAIGHT_JOIN_SYMBOL = 555; - const STRING_SYMBOL = 556; - const SUBCLASS_ORIGIN_SYMBOL = 557; - const SUBDATE_SYMBOL = 558; - const SUBJECT_SYMBOL = 559; - const SUBPARTITIONS_SYMBOL = 560; - const SUBPARTITION_SYMBOL = 561; - const SUBSTR_SYMBOL = 562; - const SUBSTRING_SYMBOL = 563; - const SUM_SYMBOL = 564; - const SUPER_SYMBOL = 565; - const SUSPEND_SYMBOL = 566; - const SWAPS_SYMBOL = 567; - const SWITCHES_SYMBOL = 568; - const SYSDATE_SYMBOL = 569; - const SYSTEM_USER_SYMBOL = 570; - const TABLES_SYMBOL = 571; - const TABLESPACE_SYMBOL = 572; - const TABLE_REF_PRIORITY_SYMBOL = 573; - const TABLE_SYMBOL = 574; - const TABLE_CHECKSUM_SYMBOL = 575; - const TABLE_NAME_SYMBOL = 576; - const TEMPORARY_SYMBOL = 577; - const TEMPTABLE_SYMBOL = 578; - const TERMINATED_SYMBOL = 579; - const TEXT_SYMBOL = 580; - const THAN_SYMBOL = 581; - const THEN_SYMBOL = 582; - const TIMESTAMP_SYMBOL = 583; - const TIMESTAMP_ADD_SYMBOL = 584; - const TIMESTAMP_DIFF_SYMBOL = 585; - const TIME_SYMBOL = 586; - const TINYBLOB_SYMBOL = 587; - const TINYINT_SYMBOL = 588; - const TINYTEXT_SYMBOL = 589; - const TO_SYMBOL = 590; - const TRAILING_SYMBOL = 591; - const TRANSACTION_SYMBOL = 592; - const TRIGGERS_SYMBOL = 593; - const TRIGGER_SYMBOL = 594; - const TRIM_SYMBOL = 595; - const TRUE_SYMBOL = 596; - const TRUNCATE_SYMBOL = 597; - const TYPES_SYMBOL = 598; - const TYPE_SYMBOL = 599; - const UDF_RETURNS_SYMBOL = 600; - const UNCOMMITTED_SYMBOL = 601; - const UNDEFINED_SYMBOL = 602; - const UNDOFILE_SYMBOL = 603; - const UNDO_BUFFER_SIZE_SYMBOL = 604; - const UNDO_SYMBOL = 605; - const UNICODE_SYMBOL = 606; - const UNINSTALL_SYMBOL = 607; - const UNION_SYMBOL = 608; - const UNIQUE_SYMBOL = 609; - const UNKNOWN_SYMBOL = 610; - const UNLOCK_SYMBOL = 611; - const UNSIGNED_SYMBOL = 612; - const UNTIL_SYMBOL = 613; - const UPDATE_SYMBOL = 614; - const UPGRADE_SYMBOL = 615; - const USAGE_SYMBOL = 616; - const USER_RESOURCES_SYMBOL = 617; - const USER_SYMBOL = 618; - const USE_FRM_SYMBOL = 619; - const USE_SYMBOL = 620; - const USING_SYMBOL = 621; - const UTC_DATE_SYMBOL = 622; - const UTC_TIMESTAMP_SYMBOL = 623; - const UTC_TIME_SYMBOL = 624; - const VALIDATION_SYMBOL = 625; - const VALUES_SYMBOL = 626; - const VALUE_SYMBOL = 627; - const VARBINARY_SYMBOL = 628; - const VARCHAR_SYMBOL = 629; - const VARCHARACTER_SYMBOL = 630; - const VARIABLES_SYMBOL = 631; - const VARIANCE_SYMBOL = 632; - const VARYING_SYMBOL = 633; - const VAR_POP_SYMBOL = 634; - const VAR_SAMP_SYMBOL = 635; - const VIEW_SYMBOL = 636; - const VIRTUAL_SYMBOL = 637; - const WAIT_SYMBOL = 638; - const WARNINGS_SYMBOL = 639; - const WEEK_SYMBOL = 640; - const WEIGHT_STRING_SYMBOL = 641; - const WHEN_SYMBOL = 642; - const WHERE_SYMBOL = 643; - const WHILE_SYMBOL = 644; - const WITH_SYMBOL = 645; - const WITHOUT_SYMBOL = 646; - const WORK_SYMBOL = 647; - const WRAPPER_SYMBOL = 648; - const WRITE_SYMBOL = 649; - const X509_SYMBOL = 650; - const XA_SYMBOL = 651; - const XID_SYMBOL = 652; - const XML_SYMBOL = 653; - const XOR_SYMBOL = 654; - const YEAR_MONTH_SYMBOL = 655; - const YEAR_SYMBOL = 656; - const ZEROFILL_SYMBOL = 657; - const PERSIST_SYMBOL = 658; - const ROLE_SYMBOL = 659; - const ADMIN_SYMBOL = 660; - const INVISIBLE_SYMBOL = 661; - const VISIBLE_SYMBOL = 662; - const EXCEPT_SYMBOL = 663; - const COMPONENT_SYMBOL = 664; - const RECURSIVE_SYMBOL = 665; - const JSON_OBJECTAGG_SYMBOL = 666; - const JSON_ARRAYAGG_SYMBOL = 667; - const OF_SYMBOL = 668; - const SKIP_SYMBOL = 669; - const LOCKED_SYMBOL = 670; - const NOWAIT_SYMBOL = 671; - const GROUPING_SYMBOL = 672; - const PERSIST_ONLY_SYMBOL = 673; - const HISTOGRAM_SYMBOL = 674; - const BUCKETS_SYMBOL = 675; - const REMOTE_SYMBOL = 676; - const CLONE_SYMBOL = 677; - const CUME_DIST_SYMBOL = 678; - const DENSE_RANK_SYMBOL = 679; - const EXCLUDE_SYMBOL = 680; - const FIRST_VALUE_SYMBOL = 681; - const FOLLOWING_SYMBOL = 682; - const GROUPS_SYMBOL = 683; - const LAG_SYMBOL = 684; - const LAST_VALUE_SYMBOL = 685; - const LEAD_SYMBOL = 686; - const NTH_VALUE_SYMBOL = 687; - const NTILE_SYMBOL = 688; - const NULLS_SYMBOL = 689; - const OTHERS_SYMBOL = 690; - const OVER_SYMBOL = 691; - const PERCENT_RANK_SYMBOL = 692; - const PRECEDING_SYMBOL = 693; - const RANK_SYMBOL = 694; - const RESPECT_SYMBOL = 695; - const ROW_NUMBER_SYMBOL = 696; - const TIES_SYMBOL = 697; - const UNBOUNDED_SYMBOL = 698; - const WINDOW_SYMBOL = 699; - const EMPTY_SYMBOL = 700; - const JSON_TABLE_SYMBOL = 701; - const NESTED_SYMBOL = 702; - const ORDINALITY_SYMBOL = 703; - const PATH_SYMBOL = 704; - const HISTORY_SYMBOL = 705; - const REUSE_SYMBOL = 706; - const SRID_SYMBOL = 707; - const THREAD_PRIORITY_SYMBOL = 708; - const RESOURCE_SYMBOL = 709; - const SYSTEM_SYMBOL = 710; - const VCPU_SYMBOL = 711; - const MASTER_PUBLIC_KEY_PATH_SYMBOL = 712; - const GET_MASTER_PUBLIC_KEY_SYMBOL = 713; - const RESTART_SYMBOL = 714; - const DEFINITION_SYMBOL = 715; - const DESCRIPTION_SYMBOL = 716; - const ORGANIZATION_SYMBOL = 717; - const REFERENCE_SYMBOL = 718; - const OPTIONAL_SYMBOL = 719; - const SECONDARY_SYMBOL = 720; - const SECONDARY_ENGINE_SYMBOL = 721; - const SECONDARY_LOAD_SYMBOL = 722; - const SECONDARY_UNLOAD_SYMBOL = 723; - const ACTIVE_SYMBOL = 724; - const INACTIVE_SYMBOL = 725; - const LATERAL_SYMBOL = 726; - const RETAIN_SYMBOL = 727; - const OLD_SYMBOL = 728; - const NETWORK_NAMESPACE_SYMBOL = 729; - const ENFORCED_SYMBOL = 730; - const ARRAY_SYMBOL = 731; - const OJ_SYMBOL = 732; - const MEMBER_SYMBOL = 733; - const RANDOM_SYMBOL = 734; - const MASTER_COMPRESSION_ALGORITHM_SYMBOL = 735; - const MASTER_ZSTD_COMPRESSION_LEVEL_SYMBOL = 736; - const PRIVILEGE_CHECKS_USER_SYMBOL = 737; - const MASTER_TLS_CIPHERSUITES_SYMBOL = 738; - const REQUIRE_ROW_FORMAT_SYMBOL = 739; - const PASSWORD_LOCK_TIME_SYMBOL = 740; - const FAILED_LOGIN_ATTEMPTS_SYMBOL = 741; - const REQUIRE_TABLE_PRIMARY_KEY_CHECK_SYMBOL = 742; - const STREAM_SYMBOL = 743; - const OFF_SYMBOL = 744; - - /** - * Additional tokens, mostly mirroring the MySQL Workbench lexer grammar. - * - * These tokens are defined in the MySQL Workbench "MySQLLexer.g4" grammar. - * - * See: - * https://github.com/mysql/mysql-workbench/blob/8.0.38/library/parsers/grammars/MySQLLexer.g4 - */ - - // Punctuators - const AT_AT_SIGN_SYMBOL = 745; - const AT_SIGN_SYMBOL = 746; - const CLOSE_CURLY_SYMBOL = 747; - const CLOSE_PAR_SYMBOL = 748; - const COLON_SYMBOL = 749; - const COMMA_SYMBOL = 750; - const DOT_SYMBOL = 751; - const OPEN_CURLY_SYMBOL = 752; - const OPEN_PAR_SYMBOL = 753; - const PARAM_MARKER = 754; - const SEMICOLON_SYMBOL = 755; - - // Operators - const ASSIGN_OPERATOR = 756; - const BITWISE_AND_OPERATOR = 757; - const BITWISE_NOT_OPERATOR = 758; - const BITWISE_OR_OPERATOR = 759; - const BITWISE_XOR_OPERATOR = 760; - const CONCAT_PIPES_SYMBOL = 761; - const DIV_OPERATOR = 762; - const EQUAL_OPERATOR = 763; - const GREATER_OR_EQUAL_OPERATOR = 764; - const GREATER_THAN_OPERATOR = 765; - const JSON_SEPARATOR_SYMBOL = 766; - const JSON_UNQUOTED_SEPARATOR_SYMBOL = 767; - const LESS_OR_EQUAL_OPERATOR = 768; - const LESS_THAN_OPERATOR = 769; - const LOGICAL_AND_OPERATOR = 770; - const LOGICAL_NOT_OPERATOR = 771; - const LOGICAL_OR_OPERATOR = 772; - const MINUS_OPERATOR = 773; - const MOD_OPERATOR = 774; - const MULT_OPERATOR = 775; - const NOT_EQUAL_OPERATOR = 776; - const NULL_SAFE_EQUAL_OPERATOR = 777; - const PLUS_OPERATOR = 778; - const SHIFT_LEFT_OPERATOR = 779; - const SHIFT_RIGHT_OPERATOR = 780; - - // Literals - const BACK_TICK_QUOTED_ID = 781; - const BIN_NUMBER = 782; - const DECIMAL_NUMBER = 783; - const DOUBLE_QUOTED_TEXT = 784; - const FLOAT_NUMBER = 785; - const HEX_NUMBER = 786; - const INT_NUMBER = 787; - const LONG_NUMBER = 788; - const NCHAR_TEXT = 789; - const SINGLE_QUOTED_TEXT = 790; - const ULONGLONG_NUMBER = 791; - - // Identifier-like tokens - const AT_TEXT_SUFFIX = 792; - const IDENTIFIER = 793; - const UNDERSCORE_CHARSET = 794; - - // Other tokens - const INT1_SYMBOL = 795; - const INT2_SYMBOL = 796; - const INT3_SYMBOL = 797; - const INT4_SYMBOL = 798; - const INT8_SYMBOL = 799; - const NOT2_SYMBOL = 800; - const NULL2_SYMBOL = 801; - const SQL_TSI_DAY_SYMBOL = 802; - const SQL_TSI_HOUR_SYMBOL = 803; - const SQL_TSI_MICROSECOND_SYMBOL = 804; - const SQL_TSI_MINUTE_SYMBOL = 805; - const SQL_TSI_MONTH_SYMBOL = 806; - const SQL_TSI_QUARTER_SYMBOL = 807; - const SQL_TSI_SECOND_SYMBOL = 808; - const SQL_TSI_WEEK_SYMBOL = 809; - const SQL_TSI_YEAR_SYMBOL = 810; - - /** - * Other tokens, missing in the MySQL Workbench "MySQLLexer.g4" grammar. - * - * These tokens are missing in the "MySQLLexer.g4" grammar, because the MySQL - * Workbench lexer and parser don't cover 100% of the MySQL syntax. - */ - const INTERSECT_SYMBOL = 811; - const ATTRIBUTE_SYMBOL = 812; - const SOURCE_AUTO_POSITION_SYMBOL = 813; - const SOURCE_BIND_SYMBOL = 814; - const SOURCE_COMPRESSION_ALGORITHM_SYMBOL = 815; - const SOURCE_CONNECT_RETRY_SYMBOL = 816; - const SOURCE_CONNECTION_AUTO_FAILOVER_SYMBOL = 817; - const SOURCE_DELAY_SYMBOL = 818; - const SOURCE_HEARTBEAT_PERIOD_SYMBOL = 819; - const SOURCE_HOST_SYMBOL = 820; - const SOURCE_LOG_FILE_SYMBOL = 821; - const SOURCE_LOG_POS_SYMBOL = 822; - const SOURCE_PASSWORD_SYMBOL = 823; - const SOURCE_PORT_SYMBOL = 824; - const SOURCE_PUBLIC_KEY_PATH_SYMBOL = 825; - const SOURCE_RETRY_COUNT_SYMBOL = 826; - const SOURCE_SSL_SYMBOL = 827; - const SOURCE_SSL_CA_SYMBOL = 828; - const SOURCE_SSL_CAPATH_SYMBOL = 829; - const SOURCE_SSL_CERT_SYMBOL = 830; - const SOURCE_SSL_CIPHER_SYMBOL = 831; - const SOURCE_SSL_CRL_SYMBOL = 832; - const SOURCE_SSL_CRLPATH_SYMBOL = 833; - const SOURCE_SSL_KEY_SYMBOL = 834; - const SOURCE_SSL_VERIFY_SERVER_CERT_SYMBOL = 835; - const SOURCE_TLS_CIPHERSUITES_SYMBOL = 836; - const SOURCE_TLS_VERSION_SYMBOL = 837; - const SOURCE_USER_SYMBOL = 838; - const SOURCE_ZSTD_COMPRESSION_LEVEL_SYMBOL = 839; - const GET_SOURCE_PUBLIC_KEY_SYMBOL = 840; - const GTID_ONLY_SYMBOL = 841; - const ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS_SYMBOL = 842; - const ZONE_SYMBOL = 843; - const INNODB_SYMBOL = 844; // From 5.7.11 defined as is_identifier(..., "INNODB") in "sql_yacc.yy". - const TLS_SYMBOL = 845; // Added in 8.0.21. From 8.0.16 defined as is_identifier(..., "TLS") in "sql_yacc.yy". - const REDO_LOG_SYMBOL = 846; // From 8.0.21 defined as is_identifier(..., "REDO_LOG") in "sql_yacc.yy". - const KEYRING_SYMBOL = 847; - const ENGINE_ATTRIBUTE_SYMBOL = 848; - const SECONDARY_ENGINE_ATTRIBUTE_SYMBOL = 849; - const JSON_VALUE_SYMBOL = 850; - const RETURNING_SYMBOL = 851; - const GEOMCOLLECTION_SYMBOL = 852; - - // Comments - const COMMENT = 900; - const MYSQL_COMMENT_START = 901; - const MYSQL_COMMENT_END = 902; - - // Special tokens - const WHITESPACE = 0; - const EOF = -1; - - /** - * A map of SQL keyword string values to their corresponding token types. - * - * This is used for a fast lookup of MySQL keywords during tokenization. - */ - const TOKENS = array( - // Tokens from MySQL 5.7: - 'ACCESSIBLE' => self::ACCESSIBLE_SYMBOL, - 'ACCOUNT' => self::ACCOUNT_SYMBOL, - 'ACTION' => self::ACTION_SYMBOL, - 'ADD' => self::ADD_SYMBOL, - 'ADDDATE' => self::ADDDATE_SYMBOL, - 'AFTER' => self::AFTER_SYMBOL, - 'AGAINST' => self::AGAINST_SYMBOL, - 'AGGREGATE' => self::AGGREGATE_SYMBOL, - 'ALGORITHM' => self::ALGORITHM_SYMBOL, - 'ALL' => self::ALL_SYMBOL, - 'ALTER' => self::ALTER_SYMBOL, - 'ALWAYS' => self::ALWAYS_SYMBOL, - 'ANALYSE' => self::ANALYSE_SYMBOL, - 'ANALYZE' => self::ANALYZE_SYMBOL, - 'AND' => self::AND_SYMBOL, - 'ANY' => self::ANY_SYMBOL, - 'AS' => self::AS_SYMBOL, - 'ASC' => self::ASC_SYMBOL, - 'ASCII' => self::ASCII_SYMBOL, - 'ASENSITIVE' => self::ASENSITIVE_SYMBOL, - 'AT' => self::AT_SYMBOL, - 'ATTRIBUTE' => self::ATTRIBUTE_SYMBOL, - 'AUTHORS' => self::AUTHORS_SYMBOL, - 'AUTO_INCREMENT' => self::AUTO_INCREMENT_SYMBOL, - 'AUTOEXTEND_SIZE' => self::AUTOEXTEND_SIZE_SYMBOL, - 'AVG' => self::AVG_SYMBOL, - 'AVG_ROW_LENGTH' => self::AVG_ROW_LENGTH_SYMBOL, - 'BACKUP' => self::BACKUP_SYMBOL, - 'BEFORE' => self::BEFORE_SYMBOL, - 'BEGIN' => self::BEGIN_SYMBOL, - 'BETWEEN' => self::BETWEEN_SYMBOL, - 'BIGINT' => self::BIGINT_SYMBOL, - 'BIN_NUM' => self::BIN_NUM_SYMBOL, - 'BINARY' => self::BINARY_SYMBOL, - 'BINLOG' => self::BINLOG_SYMBOL, - 'BIT' => self::BIT_SYMBOL, - 'BIT_AND' => self::BIT_AND_SYMBOL, - 'BIT_OR' => self::BIT_OR_SYMBOL, - 'BIT_XOR' => self::BIT_XOR_SYMBOL, - 'BLOB' => self::BLOB_SYMBOL, - 'BLOCK' => self::BLOCK_SYMBOL, - 'BOOL' => self::BOOL_SYMBOL, - 'BOOLEAN' => self::BOOLEAN_SYMBOL, - 'BOTH' => self::BOTH_SYMBOL, - 'BTREE' => self::BTREE_SYMBOL, - 'BY' => self::BY_SYMBOL, - 'BYTE' => self::BYTE_SYMBOL, - 'CACHE' => self::CACHE_SYMBOL, - 'CALL' => self::CALL_SYMBOL, - 'CASCADE' => self::CASCADE_SYMBOL, - 'CASCADED' => self::CASCADED_SYMBOL, - 'CASE' => self::CASE_SYMBOL, - 'CAST' => self::CAST_SYMBOL, - 'CATALOG_NAME' => self::CATALOG_NAME_SYMBOL, - 'CHAIN' => self::CHAIN_SYMBOL, - 'CHANGE' => self::CHANGE_SYMBOL, - 'CHANGED' => self::CHANGED_SYMBOL, - 'CHANNEL' => self::CHANNEL_SYMBOL, - 'CHAR' => self::CHAR_SYMBOL, - 'CHARACTER' => self::CHARACTER_SYMBOL, - 'CHARSET' => self::CHARSET_SYMBOL, - 'CHECK' => self::CHECK_SYMBOL, - 'CHECKSUM' => self::CHECKSUM_SYMBOL, - 'CIPHER' => self::CIPHER_SYMBOL, - 'CLASS_ORIGIN' => self::CLASS_ORIGIN_SYMBOL, - 'CLIENT' => self::CLIENT_SYMBOL, - 'CLOSE' => self::CLOSE_SYMBOL, - 'COALESCE' => self::COALESCE_SYMBOL, - 'CODE' => self::CODE_SYMBOL, - 'COLLATE' => self::COLLATE_SYMBOL, - 'COLLATION' => self::COLLATION_SYMBOL, - 'COLUMN' => self::COLUMN_SYMBOL, - 'COLUMN_FORMAT' => self::COLUMN_FORMAT_SYMBOL, - 'COLUMN_NAME' => self::COLUMN_NAME_SYMBOL, - 'COLUMNS' => self::COLUMNS_SYMBOL, - 'COMMENT' => self::COMMENT_SYMBOL, - 'COMMIT' => self::COMMIT_SYMBOL, - 'COMMITTED' => self::COMMITTED_SYMBOL, - 'COMPACT' => self::COMPACT_SYMBOL, - 'COMPLETION' => self::COMPLETION_SYMBOL, - 'COMPRESSED' => self::COMPRESSED_SYMBOL, - 'COMPRESSION' => self::COMPRESSION_SYMBOL, - 'CONCURRENT' => self::CONCURRENT_SYMBOL, - 'CONDITION' => self::CONDITION_SYMBOL, - 'CONNECTION' => self::CONNECTION_SYMBOL, - 'CONSISTENT' => self::CONSISTENT_SYMBOL, - 'CONSTRAINT' => self::CONSTRAINT_SYMBOL, - 'CONSTRAINT_CATALOG' => self::CONSTRAINT_CATALOG_SYMBOL, - 'CONSTRAINT_NAME' => self::CONSTRAINT_NAME_SYMBOL, - 'CONSTRAINT_SCHEMA' => self::CONSTRAINT_SCHEMA_SYMBOL, - 'CONTAINS' => self::CONTAINS_SYMBOL, - 'CONTEXT' => self::CONTEXT_SYMBOL, - 'CONTINUE' => self::CONTINUE_SYMBOL, - 'CONTRIBUTORS' => self::CONTRIBUTORS_SYMBOL, - 'CONVERT' => self::CONVERT_SYMBOL, - 'COUNT' => self::COUNT_SYMBOL, - 'CPU' => self::CPU_SYMBOL, - 'CREATE' => self::CREATE_SYMBOL, - 'CROSS' => self::CROSS_SYMBOL, - 'CUBE' => self::CUBE_SYMBOL, - 'CURDATE' => self::CURDATE_SYMBOL, - 'CURRENT' => self::CURRENT_SYMBOL, - 'CURRENT_DATE' => self::CURRENT_DATE_SYMBOL, - 'CURRENT_TIME' => self::CURRENT_TIME_SYMBOL, - 'CURRENT_TIMESTAMP' => self::CURRENT_TIMESTAMP_SYMBOL, - 'CURRENT_USER' => self::CURRENT_USER_SYMBOL, - 'CURSOR' => self::CURSOR_SYMBOL, - 'CURSOR_NAME' => self::CURSOR_NAME_SYMBOL, - 'CURTIME' => self::CURTIME_SYMBOL, - 'DATA' => self::DATA_SYMBOL, - 'DATABASE' => self::DATABASE_SYMBOL, - 'DATABASES' => self::DATABASES_SYMBOL, - 'DATAFILE' => self::DATAFILE_SYMBOL, - 'DATE' => self::DATE_SYMBOL, - 'DATE_ADD' => self::DATE_ADD_SYMBOL, - 'DATE_SUB' => self::DATE_SUB_SYMBOL, - 'DATETIME' => self::DATETIME_SYMBOL, - 'DAY' => self::DAY_SYMBOL, - 'DAY_HOUR' => self::DAY_HOUR_SYMBOL, - 'DAY_MICROSECOND' => self::DAY_MICROSECOND_SYMBOL, - 'DAY_MINUTE' => self::DAY_MINUTE_SYMBOL, - 'DAY_SECOND' => self::DAY_SECOND_SYMBOL, - 'DAYOFMONTH' => self::DAYOFMONTH_SYMBOL, - 'DEALLOCATE' => self::DEALLOCATE_SYMBOL, - 'DEC' => self::DEC_SYMBOL, - 'DECIMAL' => self::DECIMAL_SYMBOL, - 'DECIMAL_NUM' => self::DECIMAL_NUM_SYMBOL, - 'DECLARE' => self::DECLARE_SYMBOL, - 'DEFAULT' => self::DEFAULT_SYMBOL, - 'DEFAULT_AUTH' => self::DEFAULT_AUTH_SYMBOL, - 'DEFINER' => self::DEFINER_SYMBOL, - 'DELAY_KEY_WRITE' => self::DELAY_KEY_WRITE_SYMBOL, - 'DELAYED' => self::DELAYED_SYMBOL, - 'DELETE' => self::DELETE_SYMBOL, - 'DES_KEY_FILE' => self::DES_KEY_FILE_SYMBOL, - 'DESC' => self::DESC_SYMBOL, - 'DESCRIBE' => self::DESCRIBE_SYMBOL, - 'DETERMINISTIC' => self::DETERMINISTIC_SYMBOL, - 'DIAGNOSTICS' => self::DIAGNOSTICS_SYMBOL, - 'DIRECTORY' => self::DIRECTORY_SYMBOL, - 'DISABLE' => self::DISABLE_SYMBOL, - 'DISCARD' => self::DISCARD_SYMBOL, - 'DISK' => self::DISK_SYMBOL, - 'DISTINCT' => self::DISTINCT_SYMBOL, - 'DISTINCTROW' => self::DISTINCTROW_SYMBOL, - 'DIV' => self::DIV_SYMBOL, - 'DO' => self::DO_SYMBOL, - 'DOUBLE' => self::DOUBLE_SYMBOL, - 'DROP' => self::DROP_SYMBOL, - 'DUAL' => self::DUAL_SYMBOL, - 'DUMPFILE' => self::DUMPFILE_SYMBOL, - 'DUPLICATE' => self::DUPLICATE_SYMBOL, - 'DYNAMIC' => self::DYNAMIC_SYMBOL, - 'EACH' => self::EACH_SYMBOL, - 'ELSE' => self::ELSE_SYMBOL, - 'ELSEIF' => self::ELSEIF_SYMBOL, - 'ENABLE' => self::ENABLE_SYMBOL, - 'ENCLOSED' => self::ENCLOSED_SYMBOL, - 'ENCRYPTION' => self::ENCRYPTION_SYMBOL, - 'END' => self::END_SYMBOL, - 'END_OF_INPUT' => self::EOF, - 'ENDS' => self::ENDS_SYMBOL, - 'ENGINE' => self::ENGINE_SYMBOL, - 'ENGINES' => self::ENGINES_SYMBOL, - 'ENUM' => self::ENUM_SYMBOL, - 'ERROR' => self::ERROR_SYMBOL, - 'ERRORS' => self::ERRORS_SYMBOL, - 'ESCAPE' => self::ESCAPE_SYMBOL, - 'ESCAPED' => self::ESCAPED_SYMBOL, - 'EVENT' => self::EVENT_SYMBOL, - 'EVENTS' => self::EVENTS_SYMBOL, - 'EVERY' => self::EVERY_SYMBOL, - 'EXCHANGE' => self::EXCHANGE_SYMBOL, - 'EXECUTE' => self::EXECUTE_SYMBOL, - 'EXISTS' => self::EXISTS_SYMBOL, - 'EXIT' => self::EXIT_SYMBOL, - 'EXPANSION' => self::EXPANSION_SYMBOL, - 'EXPIRE' => self::EXPIRE_SYMBOL, - 'EXPLAIN' => self::EXPLAIN_SYMBOL, - 'EXPORT' => self::EXPORT_SYMBOL, - 'EXTENDED' => self::EXTENDED_SYMBOL, - 'EXTENT_SIZE' => self::EXTENT_SIZE_SYMBOL, - 'EXTRACT' => self::EXTRACT_SYMBOL, - 'FALSE' => self::FALSE_SYMBOL, - 'FAST' => self::FAST_SYMBOL, - 'FAULTS' => self::FAULTS_SYMBOL, - 'FETCH' => self::FETCH_SYMBOL, - 'FIELDS' => self::FIELDS_SYMBOL, - 'FILE' => self::FILE_SYMBOL, - 'FILE_BLOCK_SIZE' => self::FILE_BLOCK_SIZE_SYMBOL, - 'FILTER' => self::FILTER_SYMBOL, - 'FIRST' => self::FIRST_SYMBOL, - 'FIXED' => self::FIXED_SYMBOL, - 'FLOAT' => self::FLOAT_SYMBOL, - 'FLOAT4' => self::FLOAT4_SYMBOL, - 'FLOAT8' => self::FLOAT8_SYMBOL, - 'FLUSH' => self::FLUSH_SYMBOL, - 'FOLLOWS' => self::FOLLOWS_SYMBOL, - 'FOR' => self::FOR_SYMBOL, - 'FORCE' => self::FORCE_SYMBOL, - 'FOREIGN' => self::FOREIGN_SYMBOL, - 'FORMAT' => self::FORMAT_SYMBOL, - 'FOUND' => self::FOUND_SYMBOL, - 'FROM' => self::FROM_SYMBOL, - 'FULL' => self::FULL_SYMBOL, - 'FULLTEXT' => self::FULLTEXT_SYMBOL, - 'FUNCTION' => self::FUNCTION_SYMBOL, - 'GENERAL' => self::GENERAL_SYMBOL, - 'GENERATED' => self::GENERATED_SYMBOL, - 'GEOMCOLLECTION' => self::GEOMCOLLECTION_SYMBOL, - 'GEOMETRY' => self::GEOMETRY_SYMBOL, - 'GEOMETRYCOLLECTION' => self::GEOMETRYCOLLECTION_SYMBOL, - 'GET' => self::GET_SYMBOL, - 'GET_FORMAT' => self::GET_FORMAT_SYMBOL, - 'GLOBAL' => self::GLOBAL_SYMBOL, - 'GRANT' => self::GRANT_SYMBOL, - 'GRANTS' => self::GRANTS_SYMBOL, - 'GROUP' => self::GROUP_SYMBOL, - 'GROUP_CONCAT' => self::GROUP_CONCAT_SYMBOL, - 'GROUP_REPLICATION' => self::GROUP_REPLICATION_SYMBOL, - 'HANDLER' => self::HANDLER_SYMBOL, - 'HASH' => self::HASH_SYMBOL, - 'HAVING' => self::HAVING_SYMBOL, - 'HELP' => self::HELP_SYMBOL, - 'HIGH_PRIORITY' => self::HIGH_PRIORITY_SYMBOL, - 'HOST' => self::HOST_SYMBOL, - 'HOSTS' => self::HOSTS_SYMBOL, - 'HOUR' => self::HOUR_SYMBOL, - 'HOUR_MICROSECOND' => self::HOUR_MICROSECOND_SYMBOL, - 'HOUR_MINUTE' => self::HOUR_MINUTE_SYMBOL, - 'HOUR_SECOND' => self::HOUR_SECOND_SYMBOL, - 'IDENTIFIED' => self::IDENTIFIED_SYMBOL, - 'IF' => self::IF_SYMBOL, - 'IGNORE' => self::IGNORE_SYMBOL, - 'IGNORE_SERVER_IDS' => self::IGNORE_SERVER_IDS_SYMBOL, - 'IMPORT' => self::IMPORT_SYMBOL, - 'IN' => self::IN_SYMBOL, - 'INDEX' => self::INDEX_SYMBOL, - 'INDEXES' => self::INDEXES_SYMBOL, - 'INFILE' => self::INFILE_SYMBOL, - 'INITIAL_SIZE' => self::INITIAL_SIZE_SYMBOL, - 'INNER' => self::INNER_SYMBOL, - 'INNODB' => self::INNODB_SYMBOL, - 'INOUT' => self::INOUT_SYMBOL, - 'INSENSITIVE' => self::INSENSITIVE_SYMBOL, - 'INSERT' => self::INSERT_SYMBOL, - 'INSERT_METHOD' => self::INSERT_METHOD_SYMBOL, - 'INSTALL' => self::INSTALL_SYMBOL, - 'INSTANCE' => self::INSTANCE_SYMBOL, - 'INT' => self::INT_SYMBOL, - 'INT1' => self::INT1_SYMBOL, - 'INT2' => self::INT2_SYMBOL, - 'INT3' => self::INT3_SYMBOL, - 'INT4' => self::INT4_SYMBOL, - 'INT8' => self::INT8_SYMBOL, - 'INTEGER' => self::INTEGER_SYMBOL, - 'INTERVAL' => self::INTERVAL_SYMBOL, - 'INTO' => self::INTO_SYMBOL, - 'INVOKER' => self::INVOKER_SYMBOL, - 'IO' => self::IO_SYMBOL, - 'IO_AFTER_GTIDS' => self::IO_AFTER_GTIDS_SYMBOL, - 'IO_BEFORE_GTIDS' => self::IO_BEFORE_GTIDS_SYMBOL, - 'IO_THREAD' => self::IO_THREAD_SYMBOL, - 'IPC' => self::IPC_SYMBOL, - 'IS' => self::IS_SYMBOL, - 'ISOLATION' => self::ISOLATION_SYMBOL, - 'ISSUER' => self::ISSUER_SYMBOL, - 'ITERATE' => self::ITERATE_SYMBOL, - 'JOIN' => self::JOIN_SYMBOL, - 'JSON' => self::JSON_SYMBOL, - 'KEY' => self::KEY_SYMBOL, - 'KEY_BLOCK_SIZE' => self::KEY_BLOCK_SIZE_SYMBOL, - 'KEYS' => self::KEYS_SYMBOL, - 'KILL' => self::KILL_SYMBOL, - 'LANGUAGE' => self::LANGUAGE_SYMBOL, - 'LAST' => self::LAST_SYMBOL, - 'LEADING' => self::LEADING_SYMBOL, - 'LEAVE' => self::LEAVE_SYMBOL, - 'LEAVES' => self::LEAVES_SYMBOL, - 'LEFT' => self::LEFT_SYMBOL, - 'LESS' => self::LESS_SYMBOL, - 'LEVEL' => self::LEVEL_SYMBOL, - 'LIKE' => self::LIKE_SYMBOL, - 'LIMIT' => self::LIMIT_SYMBOL, - 'LINEAR' => self::LINEAR_SYMBOL, - 'LINES' => self::LINES_SYMBOL, - 'LINESTRING' => self::LINESTRING_SYMBOL, - 'LIST' => self::LIST_SYMBOL, - 'LOAD' => self::LOAD_SYMBOL, - 'LOCAL' => self::LOCAL_SYMBOL, - 'LOCALTIME' => self::LOCALTIME_SYMBOL, - 'LOCALTIMESTAMP' => self::LOCALTIMESTAMP_SYMBOL, - 'LOCATOR' => self::LOCATOR_SYMBOL, - 'LOCK' => self::LOCK_SYMBOL, - 'LOCKS' => self::LOCKS_SYMBOL, - 'LOGFILE' => self::LOGFILE_SYMBOL, - 'LOGS' => self::LOGS_SYMBOL, - 'LONG' => self::LONG_SYMBOL, - 'LONG_NUM' => self::LONG_NUM_SYMBOL, - 'LONGBLOB' => self::LONGBLOB_SYMBOL, - 'LONGTEXT' => self::LONGTEXT_SYMBOL, - 'LOOP' => self::LOOP_SYMBOL, - 'LOW_PRIORITY' => self::LOW_PRIORITY_SYMBOL, - 'MASTER' => self::MASTER_SYMBOL, - 'MASTER_AUTO_POSITION' => self::MASTER_AUTO_POSITION_SYMBOL, - 'MASTER_BIND' => self::MASTER_BIND_SYMBOL, - 'MASTER_CONNECT_RETRY' => self::MASTER_CONNECT_RETRY_SYMBOL, - 'MASTER_DELAY' => self::MASTER_DELAY_SYMBOL, - 'MASTER_HEARTBEAT_PERIOD' => self::MASTER_HEARTBEAT_PERIOD_SYMBOL, - 'MASTER_HOST' => self::MASTER_HOST_SYMBOL, - 'MASTER_LOG_FILE' => self::MASTER_LOG_FILE_SYMBOL, - 'MASTER_LOG_POS' => self::MASTER_LOG_POS_SYMBOL, - 'MASTER_PASSWORD' => self::MASTER_PASSWORD_SYMBOL, - 'MASTER_PORT' => self::MASTER_PORT_SYMBOL, - 'MASTER_RETRY_COUNT' => self::MASTER_RETRY_COUNT_SYMBOL, - 'MASTER_SERVER_ID' => self::MASTER_SERVER_ID_SYMBOL, - 'MASTER_SSL' => self::MASTER_SSL_SYMBOL, - 'MASTER_SSL_CA' => self::MASTER_SSL_CA_SYMBOL, - 'MASTER_SSL_CAPATH' => self::MASTER_SSL_CAPATH_SYMBOL, - 'MASTER_SSL_CERT' => self::MASTER_SSL_CERT_SYMBOL, - 'MASTER_SSL_CIPHER' => self::MASTER_SSL_CIPHER_SYMBOL, - 'MASTER_SSL_CRL' => self::MASTER_SSL_CRL_SYMBOL, - 'MASTER_SSL_CRLPATH' => self::MASTER_SSL_CRLPATH_SYMBOL, - 'MASTER_SSL_KEY' => self::MASTER_SSL_KEY_SYMBOL, - 'MASTER_SSL_VERIFY_SERVER_CERT' => self::MASTER_SSL_VERIFY_SERVER_CERT_SYMBOL, - 'MASTER_TLS_VERSION' => self::MASTER_TLS_VERSION_SYMBOL, - 'MASTER_USER' => self::MASTER_USER_SYMBOL, - 'MATCH' => self::MATCH_SYMBOL, - 'MAX' => self::MAX_SYMBOL, - 'MAX_CONNECTIONS_PER_HOUR' => self::MAX_CONNECTIONS_PER_HOUR_SYMBOL, - 'MAX_QUERIES_PER_HOUR' => self::MAX_QUERIES_PER_HOUR_SYMBOL, - 'MAX_ROWS' => self::MAX_ROWS_SYMBOL, - 'MAX_SIZE' => self::MAX_SIZE_SYMBOL, - 'MAX_STATEMENT_TIME' => self::MAX_STATEMENT_TIME_SYMBOL, - 'MAX_UPDATES_PER_HOUR' => self::MAX_UPDATES_PER_HOUR_SYMBOL, - 'MAX_USER_CONNECTIONS' => self::MAX_USER_CONNECTIONS_SYMBOL, - 'MAXVALUE' => self::MAXVALUE_SYMBOL, - 'MEDIUM' => self::MEDIUM_SYMBOL, - 'MEDIUMBLOB' => self::MEDIUMBLOB_SYMBOL, - 'MEDIUMINT' => self::MEDIUMINT_SYMBOL, - 'MEDIUMTEXT' => self::MEDIUMTEXT_SYMBOL, - 'MEMORY' => self::MEMORY_SYMBOL, - 'MERGE' => self::MERGE_SYMBOL, - 'MESSAGE_TEXT' => self::MESSAGE_TEXT_SYMBOL, - 'MICROSECOND' => self::MICROSECOND_SYMBOL, - 'MID' => self::MID_SYMBOL, - 'MIDDLEINT' => self::MIDDLEINT_SYMBOL, - 'MIGRATE' => self::MIGRATE_SYMBOL, - 'MIN' => self::MIN_SYMBOL, - 'MIN_ROWS' => self::MIN_ROWS_SYMBOL, - 'MINUTE' => self::MINUTE_SYMBOL, - 'MINUTE_MICROSECOND' => self::MINUTE_MICROSECOND_SYMBOL, - 'MINUTE_SECOND' => self::MINUTE_SECOND_SYMBOL, - 'MOD' => self::MOD_SYMBOL, - 'MODE' => self::MODE_SYMBOL, - 'MODIFIES' => self::MODIFIES_SYMBOL, - 'MODIFY' => self::MODIFY_SYMBOL, - 'MONTH' => self::MONTH_SYMBOL, - 'MULTILINESTRING' => self::MULTILINESTRING_SYMBOL, - 'MULTIPOINT' => self::MULTIPOINT_SYMBOL, - 'MULTIPOLYGON' => self::MULTIPOLYGON_SYMBOL, - 'MUTEX' => self::MUTEX_SYMBOL, - 'MYSQL_ERRNO' => self::MYSQL_ERRNO_SYMBOL, - 'NAME' => self::NAME_SYMBOL, - 'NAMES' => self::NAMES_SYMBOL, - 'NATIONAL' => self::NATIONAL_SYMBOL, - 'NATURAL' => self::NATURAL_SYMBOL, - 'NCHAR' => self::NCHAR_SYMBOL, - 'NCHAR_STRING' => self::NCHAR_STRING_SYMBOL, - 'NDB' => self::NDB_SYMBOL, - 'NDBCLUSTER' => self::NDBCLUSTER_SYMBOL, - 'NEG' => self::NEG_SYMBOL, - 'NEVER' => self::NEVER_SYMBOL, - 'NEW' => self::NEW_SYMBOL, - 'NEXT' => self::NEXT_SYMBOL, - 'NO' => self::NO_SYMBOL, - 'NO_WAIT' => self::NO_WAIT_SYMBOL, - 'NO_WRITE_TO_BINLOG' => self::NO_WRITE_TO_BINLOG_SYMBOL, - 'NODEGROUP' => self::NODEGROUP_SYMBOL, - 'NONBLOCKING' => self::NONBLOCKING_SYMBOL, - 'NONE' => self::NONE_SYMBOL, - 'NOT' => self::NOT_SYMBOL, - 'NOW' => self::NOW_SYMBOL, - 'NULL' => self::NULL_SYMBOL, - 'NUMBER' => self::NUMBER_SYMBOL, - 'NUMERIC' => self::NUMERIC_SYMBOL, - 'NVARCHAR' => self::NVARCHAR_SYMBOL, - 'OFFLINE' => self::OFFLINE_SYMBOL, - 'OFFSET' => self::OFFSET_SYMBOL, - 'OLD_PASSWORD' => self::OLD_PASSWORD_SYMBOL, - 'ON' => self::ON_SYMBOL, - 'ONE' => self::ONE_SYMBOL, - 'ONLINE' => self::ONLINE_SYMBOL, - 'ONLY' => self::ONLY_SYMBOL, - 'OPEN' => self::OPEN_SYMBOL, - 'OPTIMIZE' => self::OPTIMIZE_SYMBOL, - 'OPTIMIZER_COSTS' => self::OPTIMIZER_COSTS_SYMBOL, - 'OPTION' => self::OPTION_SYMBOL, - 'OPTIONALLY' => self::OPTIONALLY_SYMBOL, - 'OPTIONS' => self::OPTIONS_SYMBOL, - 'OR' => self::OR_SYMBOL, - 'ORDER' => self::ORDER_SYMBOL, - 'OUT' => self::OUT_SYMBOL, - 'OUTER' => self::OUTER_SYMBOL, - 'OUTFILE' => self::OUTFILE_SYMBOL, - 'OWNER' => self::OWNER_SYMBOL, - 'PACK_KEYS' => self::PACK_KEYS_SYMBOL, - 'PAGE' => self::PAGE_SYMBOL, - 'PARSER' => self::PARSER_SYMBOL, - 'PARTIAL' => self::PARTIAL_SYMBOL, - 'PARTITION' => self::PARTITION_SYMBOL, - 'PARTITIONING' => self::PARTITIONING_SYMBOL, - 'PARTITIONS' => self::PARTITIONS_SYMBOL, - 'PASSWORD' => self::PASSWORD_SYMBOL, - 'PHASE' => self::PHASE_SYMBOL, - 'PLUGIN' => self::PLUGIN_SYMBOL, - 'PLUGIN_DIR' => self::PLUGIN_DIR_SYMBOL, - 'PLUGINS' => self::PLUGINS_SYMBOL, - 'POINT' => self::POINT_SYMBOL, - 'POLYGON' => self::POLYGON_SYMBOL, - 'PORT' => self::PORT_SYMBOL, - 'POSITION' => self::POSITION_SYMBOL, - 'PRECEDES' => self::PRECEDES_SYMBOL, - 'PRECISION' => self::PRECISION_SYMBOL, - 'PREPARE' => self::PREPARE_SYMBOL, - 'PRESERVE' => self::PRESERVE_SYMBOL, - 'PREV' => self::PREV_SYMBOL, - 'PRIMARY' => self::PRIMARY_SYMBOL, - 'PRIVILEGES' => self::PRIVILEGES_SYMBOL, - 'PROCEDURE' => self::PROCEDURE_SYMBOL, - 'PROCESS' => self::PROCESS_SYMBOL, - 'PROCESSLIST' => self::PROCESSLIST_SYMBOL, - 'PROFILE' => self::PROFILE_SYMBOL, - 'PROFILES' => self::PROFILES_SYMBOL, - 'PROXY' => self::PROXY_SYMBOL, - 'PURGE' => self::PURGE_SYMBOL, - 'QUARTER' => self::QUARTER_SYMBOL, - 'QUERY' => self::QUERY_SYMBOL, - 'QUICK' => self::QUICK_SYMBOL, - 'RANGE' => self::RANGE_SYMBOL, - 'READ' => self::READ_SYMBOL, - 'READ_ONLY' => self::READ_ONLY_SYMBOL, - 'READ_WRITE' => self::READ_WRITE_SYMBOL, - 'READS' => self::READS_SYMBOL, - 'REAL' => self::REAL_SYMBOL, - 'REBUILD' => self::REBUILD_SYMBOL, - 'RECOVER' => self::RECOVER_SYMBOL, - 'REDO_BUFFER_SIZE' => self::REDO_BUFFER_SIZE_SYMBOL, - 'REDOFILE' => self::REDOFILE_SYMBOL, - 'REDUNDANT' => self::REDUNDANT_SYMBOL, - 'REFERENCES' => self::REFERENCES_SYMBOL, - 'REGEXP' => self::REGEXP_SYMBOL, - 'RELAY' => self::RELAY_SYMBOL, - 'RELAY_LOG_FILE' => self::RELAY_LOG_FILE_SYMBOL, - 'RELAY_LOG_POS' => self::RELAY_LOG_POS_SYMBOL, - 'RELAY_THREAD' => self::RELAY_THREAD_SYMBOL, - 'RELAYLOG' => self::RELAYLOG_SYMBOL, - 'RELEASE' => self::RELEASE_SYMBOL, - 'RELOAD' => self::RELOAD_SYMBOL, - 'REMOVE' => self::REMOVE_SYMBOL, - 'RENAME' => self::RENAME_SYMBOL, - 'REORGANIZE' => self::REORGANIZE_SYMBOL, - 'REPAIR' => self::REPAIR_SYMBOL, - 'REPEAT' => self::REPEAT_SYMBOL, - 'REPEATABLE' => self::REPEATABLE_SYMBOL, - 'REPLACE' => self::REPLACE_SYMBOL, - 'REPLICATE_DO_DB' => self::REPLICATE_DO_DB_SYMBOL, - 'REPLICATE_DO_TABLE' => self::REPLICATE_DO_TABLE_SYMBOL, - 'REPLICATE_IGNORE_DB' => self::REPLICATE_IGNORE_DB_SYMBOL, - 'REPLICATE_IGNORE_TABLE' => self::REPLICATE_IGNORE_TABLE_SYMBOL, - 'REPLICATE_REWRITE_DB' => self::REPLICATE_REWRITE_DB_SYMBOL, - 'REPLICATE_WILD_DO_TABLE' => self::REPLICATE_WILD_DO_TABLE_SYMBOL, - 'REPLICATE_WILD_IGNORE_TABLE' => self::REPLICATE_WILD_IGNORE_TABLE_SYMBOL, - 'REPLICATION' => self::REPLICATION_SYMBOL, - 'REQUIRE' => self::REQUIRE_SYMBOL, - 'RESET' => self::RESET_SYMBOL, - 'RESIGNAL' => self::RESIGNAL_SYMBOL, - 'RESTORE' => self::RESTORE_SYMBOL, - 'RESTRICT' => self::RESTRICT_SYMBOL, - 'RESUME' => self::RESUME_SYMBOL, - 'RETURN' => self::RETURN_SYMBOL, - 'RETURNED_SQLSTATE' => self::RETURNED_SQLSTATE_SYMBOL, - 'RETURNS' => self::RETURNS_SYMBOL, - 'REVERSE' => self::REVERSE_SYMBOL, - 'REVOKE' => self::REVOKE_SYMBOL, - 'RIGHT' => self::RIGHT_SYMBOL, - 'RLIKE' => self::RLIKE_SYMBOL, - 'ROLLBACK' => self::ROLLBACK_SYMBOL, - 'ROLLUP' => self::ROLLUP_SYMBOL, - 'ROTATE' => self::ROTATE_SYMBOL, - 'ROUTINE' => self::ROUTINE_SYMBOL, - 'ROW' => self::ROW_SYMBOL, - 'ROW_COUNT' => self::ROW_COUNT_SYMBOL, - 'ROW_FORMAT' => self::ROW_FORMAT_SYMBOL, - 'ROWS' => self::ROWS_SYMBOL, - 'RTREE' => self::RTREE_SYMBOL, - 'SAVEPOINT' => self::SAVEPOINT_SYMBOL, - 'SCHEDULE' => self::SCHEDULE_SYMBOL, - 'SCHEMA' => self::SCHEMA_SYMBOL, - 'SCHEMA_NAME' => self::SCHEMA_NAME_SYMBOL, - 'SCHEMAS' => self::SCHEMAS_SYMBOL, - 'SECOND' => self::SECOND_SYMBOL, - 'SECOND_MICROSECOND' => self::SECOND_MICROSECOND_SYMBOL, - 'SECURITY' => self::SECURITY_SYMBOL, - 'SELECT' => self::SELECT_SYMBOL, - 'SENSITIVE' => self::SENSITIVE_SYMBOL, - 'SEPARATOR' => self::SEPARATOR_SYMBOL, - 'SERIAL' => self::SERIAL_SYMBOL, - 'SERIALIZABLE' => self::SERIALIZABLE_SYMBOL, - 'SERVER' => self::SERVER_SYMBOL, - 'SERVER_OPTIONS' => self::SERVER_OPTIONS_SYMBOL, - 'SESSION' => self::SESSION_SYMBOL, - 'SESSION_USER' => self::SESSION_USER_SYMBOL, - 'SET' => self::SET_SYMBOL, - 'SET_VAR' => self::SET_VAR_SYMBOL, - 'SHARE' => self::SHARE_SYMBOL, - 'SHOW' => self::SHOW_SYMBOL, - 'SHUTDOWN' => self::SHUTDOWN_SYMBOL, - 'SIGNAL' => self::SIGNAL_SYMBOL, - 'SIGNED' => self::SIGNED_SYMBOL, - 'SIMPLE' => self::SIMPLE_SYMBOL, - 'SLAVE' => self::SLAVE_SYMBOL, - 'SLOW' => self::SLOW_SYMBOL, - 'SMALLINT' => self::SMALLINT_SYMBOL, - 'SNAPSHOT' => self::SNAPSHOT_SYMBOL, - 'SOCKET' => self::SOCKET_SYMBOL, - 'SOME' => self::SOME_SYMBOL, - 'SONAME' => self::SONAME_SYMBOL, - 'SOUNDS' => self::SOUNDS_SYMBOL, - 'SOURCE' => self::SOURCE_SYMBOL, - 'SPATIAL' => self::SPATIAL_SYMBOL, - 'SPECIFIC' => self::SPECIFIC_SYMBOL, - 'SQL' => self::SQL_SYMBOL, - 'SQL_AFTER_GTIDS' => self::SQL_AFTER_GTIDS_SYMBOL, - 'SQL_AFTER_MTS_GAPS' => self::SQL_AFTER_MTS_GAPS_SYMBOL, - 'SQL_BEFORE_GTIDS' => self::SQL_BEFORE_GTIDS_SYMBOL, - 'SQL_BIG_RESULT' => self::SQL_BIG_RESULT_SYMBOL, - 'SQL_BUFFER_RESULT' => self::SQL_BUFFER_RESULT_SYMBOL, - 'SQL_CACHE' => self::SQL_CACHE_SYMBOL, - 'SQL_CALC_FOUND_ROWS' => self::SQL_CALC_FOUND_ROWS_SYMBOL, - 'SQL_NO_CACHE' => self::SQL_NO_CACHE_SYMBOL, - 'SQL_SMALL_RESULT' => self::SQL_SMALL_RESULT_SYMBOL, - 'SQL_THREAD' => self::SQL_THREAD_SYMBOL, - 'SQL_TSI_DAY' => self::SQL_TSI_DAY_SYMBOL, - 'SQL_TSI_HOUR' => self::SQL_TSI_HOUR_SYMBOL, - 'SQL_TSI_MICROSECOND' => self::SQL_TSI_MICROSECOND_SYMBOL, - 'SQL_TSI_MINUTE' => self::SQL_TSI_MINUTE_SYMBOL, - 'SQL_TSI_MONTH' => self::SQL_TSI_MONTH_SYMBOL, - 'SQL_TSI_QUARTER' => self::SQL_TSI_QUARTER_SYMBOL, - 'SQL_TSI_SECOND' => self::SQL_TSI_SECOND_SYMBOL, - 'SQL_TSI_WEEK' => self::SQL_TSI_WEEK_SYMBOL, - 'SQL_TSI_YEAR' => self::SQL_TSI_YEAR_SYMBOL, - 'SQLEXCEPTION' => self::SQLEXCEPTION_SYMBOL, - 'SQLSTATE' => self::SQLSTATE_SYMBOL, - 'SQLWARNING' => self::SQLWARNING_SYMBOL, - 'SSL' => self::SSL_SYMBOL, - 'STACKED' => self::STACKED_SYMBOL, - 'START' => self::START_SYMBOL, - 'STARTING' => self::STARTING_SYMBOL, - 'STARTS' => self::STARTS_SYMBOL, - 'STATS_AUTO_RECALC' => self::STATS_AUTO_RECALC_SYMBOL, - 'STATS_PERSISTENT' => self::STATS_PERSISTENT_SYMBOL, - 'STATS_SAMPLE_PAGES' => self::STATS_SAMPLE_PAGES_SYMBOL, - 'STATUS' => self::STATUS_SYMBOL, - 'STD' => self::STD_SYMBOL, - 'STDDEV' => self::STDDEV_SYMBOL, - 'STDDEV_POP' => self::STDDEV_POP_SYMBOL, - 'STDDEV_SAMP' => self::STDDEV_SAMP_SYMBOL, - 'STOP' => self::STOP_SYMBOL, - 'STORAGE' => self::STORAGE_SYMBOL, - 'STORED' => self::STORED_SYMBOL, - 'STRAIGHT_JOIN' => self::STRAIGHT_JOIN_SYMBOL, - 'STRING' => self::STRING_SYMBOL, - 'SUBCLASS_ORIGIN' => self::SUBCLASS_ORIGIN_SYMBOL, - 'SUBDATE' => self::SUBDATE_SYMBOL, - 'SUBJECT' => self::SUBJECT_SYMBOL, - 'SUBPARTITION' => self::SUBPARTITION_SYMBOL, - 'SUBPARTITIONS' => self::SUBPARTITIONS_SYMBOL, - 'SUBSTR' => self::SUBSTR_SYMBOL, - 'SUBSTRING' => self::SUBSTRING_SYMBOL, - 'SUM' => self::SUM_SYMBOL, - 'SUPER' => self::SUPER_SYMBOL, - 'SUSPEND' => self::SUSPEND_SYMBOL, - 'SWAPS' => self::SWAPS_SYMBOL, - 'SWITCHES' => self::SWITCHES_SYMBOL, - 'SYSDATE' => self::SYSDATE_SYMBOL, - 'SYSTEM_USER' => self::SYSTEM_USER_SYMBOL, - 'TABLE' => self::TABLE_SYMBOL, - 'TABLE_CHECKSUM' => self::TABLE_CHECKSUM_SYMBOL, - 'TABLE_NAME' => self::TABLE_NAME_SYMBOL, - 'TABLE_REF_PRIORITY' => self::TABLE_REF_PRIORITY_SYMBOL, - 'TABLES' => self::TABLES_SYMBOL, - 'TABLESPACE' => self::TABLESPACE_SYMBOL, - 'TEMPORARY' => self::TEMPORARY_SYMBOL, - 'TEMPTABLE' => self::TEMPTABLE_SYMBOL, - 'TERMINATED' => self::TERMINATED_SYMBOL, - 'TEXT' => self::TEXT_SYMBOL, - 'THAN' => self::THAN_SYMBOL, - 'THEN' => self::THEN_SYMBOL, - 'TIME' => self::TIME_SYMBOL, - 'TIMESTAMP' => self::TIMESTAMP_SYMBOL, - 'TIMESTAMP_ADD' => self::TIMESTAMP_ADD_SYMBOL, - 'TIMESTAMP_DIFF' => self::TIMESTAMP_DIFF_SYMBOL, - 'TINYBLOB' => self::TINYBLOB_SYMBOL, - 'TINYINT' => self::TINYINT_SYMBOL, - 'TINYTEXT' => self::TINYTEXT_SYMBOL, - 'TO' => self::TO_SYMBOL, - 'TRAILING' => self::TRAILING_SYMBOL, - 'TRANSACTION' => self::TRANSACTION_SYMBOL, - 'TRIGGER' => self::TRIGGER_SYMBOL, - 'TRIGGERS' => self::TRIGGERS_SYMBOL, - 'TRIM' => self::TRIM_SYMBOL, - 'TRUE' => self::TRUE_SYMBOL, - 'TRUNCATE' => self::TRUNCATE_SYMBOL, - 'TYPE' => self::TYPE_SYMBOL, - 'TYPES' => self::TYPES_SYMBOL, - 'UDF_RETURNS' => self::UDF_RETURNS_SYMBOL, - 'UNCOMMITTED' => self::UNCOMMITTED_SYMBOL, - 'UNDEFINED' => self::UNDEFINED_SYMBOL, - 'UNDO' => self::UNDO_SYMBOL, - 'UNDO_BUFFER_SIZE' => self::UNDO_BUFFER_SIZE_SYMBOL, - 'UNDOFILE' => self::UNDOFILE_SYMBOL, - 'UNICODE' => self::UNICODE_SYMBOL, - 'UNINSTALL' => self::UNINSTALL_SYMBOL, - 'UNION' => self::UNION_SYMBOL, - 'UNIQUE' => self::UNIQUE_SYMBOL, - 'UNKNOWN' => self::UNKNOWN_SYMBOL, - 'UNLOCK' => self::UNLOCK_SYMBOL, - 'UNSIGNED' => self::UNSIGNED_SYMBOL, - 'UNTIL' => self::UNTIL_SYMBOL, - 'UPDATE' => self::UPDATE_SYMBOL, - 'UPGRADE' => self::UPGRADE_SYMBOL, - 'USAGE' => self::USAGE_SYMBOL, - 'USE' => self::USE_SYMBOL, - 'USE_FRM' => self::USE_FRM_SYMBOL, - 'USER' => self::USER_SYMBOL, - 'USER_RESOURCES' => self::USER_RESOURCES_SYMBOL, - 'USING' => self::USING_SYMBOL, - 'UTC_DATE' => self::UTC_DATE_SYMBOL, - 'UTC_TIME' => self::UTC_TIME_SYMBOL, - 'UTC_TIMESTAMP' => self::UTC_TIMESTAMP_SYMBOL, - 'VALIDATION' => self::VALIDATION_SYMBOL, - 'VALUE' => self::VALUE_SYMBOL, - 'VALUES' => self::VALUES_SYMBOL, - 'VAR_POP' => self::VAR_POP_SYMBOL, - 'VAR_SAMP' => self::VAR_SAMP_SYMBOL, - 'VARBINARY' => self::VARBINARY_SYMBOL, - 'VARCHAR' => self::VARCHAR_SYMBOL, - 'VARCHARACTER' => self::VARCHARACTER_SYMBOL, - 'VARIABLES' => self::VARIABLES_SYMBOL, - 'VARIANCE' => self::VARIANCE_SYMBOL, - 'VARYING' => self::VARYING_SYMBOL, - 'VIEW' => self::VIEW_SYMBOL, - 'VIRTUAL' => self::VIRTUAL_SYMBOL, - 'WAIT' => self::WAIT_SYMBOL, - 'WARNINGS' => self::WARNINGS_SYMBOL, - 'WEEK' => self::WEEK_SYMBOL, - 'WEIGHT_STRING' => self::WEIGHT_STRING_SYMBOL, - 'WHEN' => self::WHEN_SYMBOL, - 'WHERE' => self::WHERE_SYMBOL, - 'WHILE' => self::WHILE_SYMBOL, - 'WITH' => self::WITH_SYMBOL, - 'WITHOUT' => self::WITHOUT_SYMBOL, - 'WORK' => self::WORK_SYMBOL, - 'WRAPPER' => self::WRAPPER_SYMBOL, - 'WRITE' => self::WRITE_SYMBOL, - 'X509' => self::X509_SYMBOL, - 'XA' => self::XA_SYMBOL, - 'XID' => self::XID_SYMBOL, - 'XML' => self::XML_SYMBOL, - 'XOR' => self::XOR_SYMBOL, - 'YEAR' => self::YEAR_SYMBOL, - 'YEAR_MONTH' => self::YEAR_MONTH_SYMBOL, - 'ZEROFILL' => self::ZEROFILL_SYMBOL, - - // Tokens from MySQL 8.0: - 'ACTIVE' => self::ACTIVE_SYMBOL, - 'ADMIN' => self::ADMIN_SYMBOL, - 'ARRAY' => self::ARRAY_SYMBOL, - 'ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS' => self::ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS_SYMBOL, - 'BUCKETS' => self::BUCKETS_SYMBOL, - 'CLONE' => self::CLONE_SYMBOL, - 'COMPONENT' => self::COMPONENT_SYMBOL, - 'CUME_DIST' => self::CUME_DIST_SYMBOL, - 'DEFINITION' => self::DEFINITION_SYMBOL, - 'DENSE_RANK' => self::DENSE_RANK_SYMBOL, - 'DESCRIPTION' => self::DESCRIPTION_SYMBOL, - 'EMPTY' => self::EMPTY_SYMBOL, - 'ENFORCED' => self::ENFORCED_SYMBOL, - 'ENGINE_ATTRIBUTE' => self::ENGINE_ATTRIBUTE_SYMBOL, - 'EXCEPT' => self::EXCEPT_SYMBOL, - 'EXCLUDE' => self::EXCLUDE_SYMBOL, - 'FAILED_LOGIN_ATTEMPTS' => self::FAILED_LOGIN_ATTEMPTS_SYMBOL, - 'FIRST_VALUE' => self::FIRST_VALUE_SYMBOL, - 'FOLLOWING' => self::FOLLOWING_SYMBOL, - 'GET_MASTER_PUBLIC_KEY_SYM' => self::GET_MASTER_PUBLIC_KEY_SYMBOL, - 'GET_SOURCE_PUBLIC_KEY' => self::GET_SOURCE_PUBLIC_KEY_SYMBOL, - 'GROUPING' => self::GROUPING_SYMBOL, - 'GROUPS' => self::GROUPS_SYMBOL, - 'GTID_ONLY' => self::GTID_ONLY_SYMBOL, - 'HISTOGRAM' => self::HISTOGRAM_SYMBOL, - 'HISTORY' => self::HISTORY_SYMBOL, - 'INACTIVE' => self::INACTIVE_SYMBOL, - 'INTERSECT' => self::INTERSECT_SYMBOL, - 'INVISIBLE' => self::INVISIBLE_SYMBOL, - 'JSON_ARRAYAGG' => self::JSON_ARRAYAGG_SYMBOL, - 'JSON_OBJECTAGG' => self::JSON_OBJECTAGG_SYMBOL, - 'JSON_TABLE' => self::JSON_TABLE_SYMBOL, - 'JSON_VALUE' => self::JSON_VALUE_SYMBOL, - 'KEYRING' => self::KEYRING_SYMBOL, - 'LAG' => self::LAG_SYMBOL, - 'LAST_VALUE' => self::LAST_VALUE_SYMBOL, - 'LATERAL' => self::LATERAL_SYMBOL, - 'LEAD' => self::LEAD_SYMBOL, - 'LOCKED' => self::LOCKED_SYMBOL, - 'MASTER_COMPRESSION_ALGORITHM' => self::MASTER_COMPRESSION_ALGORITHM_SYMBOL, - 'MASTER_PUBLIC_KEY_PATH' => self::MASTER_PUBLIC_KEY_PATH_SYMBOL, - 'MASTER_TLS_CIPHERSUITES' => self::MASTER_TLS_CIPHERSUITES_SYMBOL, - 'MASTER_ZSTD_COMPRESSION_LEVEL' => self::MASTER_ZSTD_COMPRESSION_LEVEL_SYMBOL, - 'MEMBER' => self::MEMBER_SYMBOL, - 'NESTED' => self::NESTED_SYMBOL, - 'NETWORK_NAMESPACE' => self::NETWORK_NAMESPACE_SYMBOL, - 'NOWAIT' => self::NOWAIT_SYMBOL, - 'NTH_VALUE' => self::NTH_VALUE_SYMBOL, - 'NTILE' => self::NTILE_SYMBOL, - 'NULLS' => self::NULLS_SYMBOL, - 'OF' => self::OF_SYMBOL, - 'OFF' => self::OFF_SYMBOL, - 'OJ' => self::OJ_SYMBOL, - 'OLD' => self::OLD_SYMBOL, - 'OPTIONAL' => self::OPTIONAL_SYMBOL, - 'ORDINALITY' => self::ORDINALITY_SYMBOL, - 'ORGANIZATION' => self::ORGANIZATION_SYMBOL, - 'OTHERS' => self::OTHERS_SYMBOL, - 'OVER' => self::OVER_SYMBOL, - 'PASSWORD_LOCK_TIME' => self::PASSWORD_LOCK_TIME_SYMBOL, - 'PATH' => self::PATH_SYMBOL, - 'PERCENT_RANK' => self::PERCENT_RANK_SYMBOL, - 'PERSIST' => self::PERSIST_SYMBOL, - 'PERSIST_ONLY' => self::PERSIST_ONLY_SYMBOL, - 'PRECEDING' => self::PRECEDING_SYMBOL, - 'PRIVILEGE_CHECKS_USER' => self::PRIVILEGE_CHECKS_USER_SYMBOL, - 'RANDOM' => self::RANDOM_SYMBOL, - 'RANK' => self::RANK_SYMBOL, - 'RECURSIVE' => self::RECURSIVE_SYMBOL, - 'REDO_LOG' => self::REDO_LOG_SYMBOL, - 'REFERENCE' => self::REFERENCE_SYMBOL, - 'REMOTE' => self::REMOTE_SYMBOL, - 'REQUIRE_ROW_FORMAT' => self::REQUIRE_ROW_FORMAT_SYMBOL, - 'REQUIRE_TABLE_PRIMARY_KEY_CHECK' => self::REQUIRE_TABLE_PRIMARY_KEY_CHECK_SYMBOL, - 'RESOURCE' => self::RESOURCE_SYMBOL, - 'RESPECT' => self::RESPECT_SYMBOL, - 'RESTART' => self::RESTART_SYMBOL, - 'RETAIN' => self::RETAIN_SYMBOL, - 'RETURNING' => self::RETURNING_SYMBOL, - 'REUSE' => self::REUSE_SYMBOL, - 'ROLE' => self::ROLE_SYMBOL, - 'ROW_NUMBER' => self::ROW_NUMBER_SYMBOL, - 'SECONDARY' => self::SECONDARY_SYMBOL, - 'SECONDARY_ENGINE' => self::SECONDARY_ENGINE_SYMBOL, - 'SECONDARY_ENGINE_ATTRIBUTE' => self::SECONDARY_ENGINE_ATTRIBUTE_SYMBOL, - 'SECONDARY_LOAD' => self::SECONDARY_LOAD_SYMBOL, - 'SECONDARY_UNLOAD' => self::SECONDARY_UNLOAD_SYMBOL, - 'SKIP' => self::SKIP_SYMBOL, - 'SOURCE_AUTO_POSITION' => self::SOURCE_AUTO_POSITION_SYMBOL, - 'SOURCE_BIND' => self::SOURCE_BIND_SYMBOL, - 'SOURCE_COMPRESSION_ALGORITHM' => self::SOURCE_COMPRESSION_ALGORITHM_SYMBOL, - 'SOURCE_CONNECT_RETRY' => self::SOURCE_CONNECT_RETRY_SYMBOL, - 'SOURCE_CONNECTION_AUTO_FAILOVER' => self::SOURCE_CONNECTION_AUTO_FAILOVER_SYMBOL, - 'SOURCE_DELAY' => self::SOURCE_DELAY_SYMBOL, - 'SOURCE_HEARTBEAT_PERIOD' => self::SOURCE_HEARTBEAT_PERIOD_SYMBOL, - 'SOURCE_HOST' => self::SOURCE_HOST_SYMBOL, - 'SOURCE_LOG_FILE' => self::SOURCE_LOG_FILE_SYMBOL, - 'SOURCE_LOG_POS' => self::SOURCE_LOG_POS_SYMBOL, - 'SOURCE_PASSWORD' => self::SOURCE_PASSWORD_SYMBOL, - 'SOURCE_PORT' => self::SOURCE_PORT_SYMBOL, - 'SOURCE_PUBLIC_KEY_PATH' => self::SOURCE_PUBLIC_KEY_PATH_SYMBOL, - 'SOURCE_RETRY_COUNT' => self::SOURCE_RETRY_COUNT_SYMBOL, - 'SOURCE_SSL' => self::SOURCE_SSL_SYMBOL, - 'SOURCE_SSL_CA' => self::SOURCE_SSL_CA_SYMBOL, - 'SOURCE_SSL_CAPATH' => self::SOURCE_SSL_CAPATH_SYMBOL, - 'SOURCE_SSL_CERT' => self::SOURCE_SSL_CERT_SYMBOL, - 'SOURCE_SSL_CIPHER' => self::SOURCE_SSL_CIPHER_SYMBOL, - 'SOURCE_SSL_CRL' => self::SOURCE_SSL_CRL_SYMBOL, - 'SOURCE_SSL_CRLPATH' => self::SOURCE_SSL_CRLPATH_SYMBOL, - 'SOURCE_SSL_KEY' => self::SOURCE_SSL_KEY_SYMBOL, - 'SOURCE_SSL_VERIFY_SERVER_CERT' => self::SOURCE_SSL_VERIFY_SERVER_CERT_SYMBOL, - 'SOURCE_TLS_CIPHERSUITES' => self::SOURCE_TLS_CIPHERSUITES_SYMBOL, - 'SOURCE_TLS_VERSION' => self::SOURCE_TLS_VERSION_SYMBOL, - 'SOURCE_USER' => self::SOURCE_USER_SYMBOL, - 'SOURCE_ZSTD_COMPRESSION_LEVEL' => self::SOURCE_ZSTD_COMPRESSION_LEVEL_SYMBOL, - 'SRID' => self::SRID_SYMBOL, - 'STREAM' => self::STREAM_SYMBOL, - 'SYSTEM' => self::SYSTEM_SYMBOL, - 'THREAD_PRIORITY' => self::THREAD_PRIORITY_SYMBOL, - 'TIES' => self::TIES_SYMBOL, - 'TLS' => self::TLS_SYMBOL, - 'UNBOUNDED' => self::UNBOUNDED_SYMBOL, - 'VCPU' => self::VCPU_SYMBOL, - 'VISIBLE' => self::VISIBLE_SYMBOL, - 'WINDOW' => self::WINDOW_SYMBOL, - 'ZONE' => self::ZONE_SYMBOL, - ); - - /** - * Tokens that represent function calls when followed by a parenthesis. - */ - const FUNCTIONS = array( - self::ADDDATE_SYMBOL => true, - self::BIT_AND_SYMBOL => true, - self::BIT_OR_SYMBOL => true, - self::BIT_XOR_SYMBOL => true, - self::CAST_SYMBOL => true, - self::COUNT_SYMBOL => true, - self::CURDATE_SYMBOL => true, - self::CURRENT_DATE_SYMBOL => true, - self::CURRENT_TIME_SYMBOL => true, - self::CURTIME_SYMBOL => true, - self::DATE_ADD_SYMBOL => true, - self::DATE_SUB_SYMBOL => true, - self::EXTRACT_SYMBOL => true, - self::GROUP_CONCAT_SYMBOL => true, - self::MAX_SYMBOL => true, - self::MID_SYMBOL => true, - self::MIN_SYMBOL => true, - self::NOW_SYMBOL => true, - self::POSITION_SYMBOL => true, - self::SESSION_USER_SYMBOL => true, - self::STD_SYMBOL => true, - self::STDDEV_POP_SYMBOL => true, - self::STDDEV_SAMP_SYMBOL => true, - self::STDDEV_SYMBOL => true, - self::SUBDATE_SYMBOL => true, - self::SUBSTR_SYMBOL => true, - self::SUBSTRING_SYMBOL => true, - self::SUM_SYMBOL => true, - self::SYSDATE_SYMBOL => true, - self::SYSTEM_USER_SYMBOL => true, - self::TRIM_SYMBOL => true, - self::VAR_POP_SYMBOL => true, - self::VAR_SAMP_SYMBOL => true, - self::VARIANCE_SYMBOL => true, - ); - - /** - * Tokens that are functionally equivalent and can be used interchangeably. - * - * Some of the synonyms may have a different keyword or function status and - * version constraints, hence the synonym conversion needs to be applied - * at the end of the tokenization process, after all other transformations. - * - * E.g.: NOW is a non-reserved keyword that needs to be used with "()" while - * CURRENT_TIMESTAMP is a reserved keyword that can be used without "()". - */ - const SYNONYMS = array( - self::CHARACTER_SYMBOL => self::CHAR_SYMBOL, - self::CURRENT_DATE_SYMBOL => self::CURDATE_SYMBOL, - self::CURRENT_TIME_SYMBOL => self::CURTIME_SYMBOL, - self::CURRENT_TIMESTAMP_SYMBOL => self::NOW_SYMBOL, - self::DAYOFMONTH_SYMBOL => self::DAY_SYMBOL, - self::DEC_SYMBOL => self::DECIMAL_SYMBOL, - self::DISTINCTROW_SYMBOL => self::DISTINCT_SYMBOL, - self::FIELDS_SYMBOL => self::COLUMNS_SYMBOL, - self::FLOAT4_SYMBOL => self::FLOAT_SYMBOL, - self::FLOAT8_SYMBOL => self::DOUBLE_SYMBOL, - self::GEOMCOLLECTION_SYMBOL => self::GEOMETRYCOLLECTION_SYMBOL, - self::INT1_SYMBOL => self::TINYINT_SYMBOL, - self::INT2_SYMBOL => self::SMALLINT_SYMBOL, - self::INT3_SYMBOL => self::MEDIUMINT_SYMBOL, - self::INT4_SYMBOL => self::INT_SYMBOL, - self::INT8_SYMBOL => self::BIGINT_SYMBOL, - self::INTEGER_SYMBOL => self::INT_SYMBOL, - self::IO_THREAD_SYMBOL => self::RELAY_THREAD_SYMBOL, - self::LOCALTIME_SYMBOL => self::NOW_SYMBOL, - self::LOCALTIMESTAMP_SYMBOL => self::NOW_SYMBOL, - self::MID_SYMBOL => self::SUBSTRING_SYMBOL, - self::MIDDLEINT_SYMBOL => self::MEDIUMINT_SYMBOL, - self::NDB_SYMBOL => self::NDBCLUSTER_SYMBOL, - self::RLIKE_SYMBOL => self::REGEXP_SYMBOL, - self::SCHEMA_SYMBOL => self::DATABASE_SYMBOL, - self::SCHEMAS_SYMBOL => self::DATABASES_SYMBOL, - self::SESSION_USER_SYMBOL => self::USER_SYMBOL, - self::SOME_SYMBOL => self::ANY_SYMBOL, - self::SQL_TSI_DAY_SYMBOL => self::DAY_SYMBOL, - self::SQL_TSI_HOUR_SYMBOL => self::HOUR_SYMBOL, - self::SQL_TSI_MICROSECOND_SYMBOL => self::MICROSECOND_SYMBOL, - self::SQL_TSI_MINUTE_SYMBOL => self::MINUTE_SYMBOL, - self::SQL_TSI_MONTH_SYMBOL => self::MONTH_SYMBOL, - self::SQL_TSI_QUARTER_SYMBOL => self::QUARTER_SYMBOL, - self::SQL_TSI_SECOND_SYMBOL => self::SECOND_SYMBOL, - self::SQL_TSI_WEEK_SYMBOL => self::WEEK_SYMBOL, - self::SQL_TSI_YEAR_SYMBOL => self::YEAR_SYMBOL, - self::STDDEV_POP_SYMBOL => self::STD_SYMBOL, - self::STDDEV_SYMBOL => self::STD_SYMBOL, - self::SUBSTR_SYMBOL => self::SUBSTRING_SYMBOL, - self::SYSTEM_USER_SYMBOL => self::USER_SYMBOL, - self::VAR_POP_SYMBOL => self::VARIANCE_SYMBOL, - self::VARCHARACTER_SYMBOL => self::VARCHAR_SYMBOL, - ); - - /** - * Version constraints for version-specific tokens. - * - * This is a map of tokens to the MySQL server versions in which they were - * introduced (positive number) or removed (negative number). Tokens that - * were both introduced and later removed are not included in this list - * and are handled by manual version checks in the tokenization process. - * - * See: - * https://dev.mysql.com/doc/mysqld-version-reference/en/keywords.html - * - * @TODO Verify the version specifiers and ranges against the list above. - * - * Positive number: >= (introduced in ) - * Negative number: < (removed in ) - */ - const VERSIONS = array( - // MySQL 5 - self::ACCOUNT_SYMBOL => 50707, - self::ALWAYS_SYMBOL => 50707, - self::ANALYSE_SYMBOL => -80000, - self::AUTHORS_SYMBOL => -50700, - self::CHANNEL_SYMBOL => 50706, - self::COMPRESSION_SYMBOL => 50707, - self::CONTRIBUTORS_SYMBOL => -50700, - self::CURRENT_SYMBOL => 50604, - self::DEFAULT_AUTH_SYMBOL => 50604, - self::DES_KEY_FILE_SYMBOL => -80003, - self::ENCRYPTION_SYMBOL => 50711, - self::EXPIRE_SYMBOL => 50606, - self::EXPORT_SYMBOL => 50606, - self::FILE_BLOCK_SIZE_SYMBOL => 50707, - self::FILTER_SYMBOL => 50700, - self::FOLLOWS_SYMBOL => 50700, - self::GENERATED_SYMBOL => 50707, - self::GET_SYMBOL => 50604, - self::GROUP_REPLICATION_SYMBOL => 50707, - self::INNODB_SYMBOL => 50711, - self::INSTANCE_SYMBOL => 50713, - self::JSON_SYMBOL => 50708, - self::MASTER_AUTO_POSITION_SYMBOL => 50605, - self::MASTER_BIND_SYMBOL => 50602, - self::MASTER_RETRY_COUNT_SYMBOL => 50601, - self::MASTER_SSL_CRL_SYMBOL => 50603, - self::MASTER_SSL_CRLPATH_SYMBOL => 50603, - self::MASTER_TLS_VERSION_SYMBOL => 50713, - self::NEVER_SYMBOL => 50704, - self::NUMBER_SYMBOL => 50606, - self::OLD_PASSWORD_SYMBOL => -50706, - self::ONLY_SYMBOL => 50605, - self::OPTIMIZER_COSTS_SYMBOL => 50706, - self::PLUGIN_DIR_SYMBOL => 50604, - self::PRECEDES_SYMBOL => 50700, - self::REDOFILE_SYMBOL => -80000, - self::REPLICATE_DO_DB_SYMBOL => 50700, - self::REPLICATE_DO_TABLE_SYMBOL => 50700, - self::REPLICATE_IGNORE_DB_SYMBOL => 50700, - self::REPLICATE_IGNORE_TABLE_SYMBOL => 50700, - self::REPLICATE_REWRITE_DB_SYMBOL => 50700, - self::REPLICATE_WILD_DO_TABLE_SYMBOL => 50700, - self::REPLICATE_WILD_IGNORE_TABLE_SYMBOL => 50700, - self::ROTATE_SYMBOL => 50713, - self::SQL_AFTER_MTS_GAPS_SYMBOL => 50606, - self::SQL_CACHE_SYMBOL => -80000, - self::STACKED_SYMBOL => 50700, - self::STORED_SYMBOL => 50707, - self::TABLE_REF_PRIORITY_SYMBOL => -80000, - self::VALIDATION_SYMBOL => 50706, - self::VIRTUAL_SYMBOL => 50707, - self::XID_SYMBOL => 50704, - - // MySQL 8 - self::ACTIVE_SYMBOL => 80014, - self::ADMIN_SYMBOL => 80000, - self::ARRAY_SYMBOL => 80017, - self::ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS_SYMBOL => 80000, - self::ATTRIBUTE_SYMBOL => 80021, - self::BUCKETS_SYMBOL => 80000, - self::CLONE_SYMBOL => 80000, - self::COMPONENT_SYMBOL => 80000, - self::CUME_DIST_SYMBOL => 80000, - self::DEFINITION_SYMBOL => 80011, - self::DENSE_RANK_SYMBOL => 80000, - self::DESCRIPTION_SYMBOL => 80011, - self::EMPTY_SYMBOL => 80000, - self::ENFORCED_SYMBOL => 80017, - self::ENGINE_ATTRIBUTE_SYMBOL => 80021, - self::EXCEPT_SYMBOL => 80000, - self::EXCLUDE_SYMBOL => 80000, - self::FAILED_LOGIN_ATTEMPTS_SYMBOL => 80019, - self::FIRST_VALUE_SYMBOL => 80000, - self::FOLLOWING_SYMBOL => 80000, - self::GEOMCOLLECTION_SYMBOL => 80000, - self::GET_MASTER_PUBLIC_KEY_SYMBOL => 80000, - self::GET_SOURCE_PUBLIC_KEY_SYMBOL => 80000, - self::GROUPING_SYMBOL => 80000, - self::GROUPS_SYMBOL => 80000, - self::GTID_ONLY_SYMBOL => 80000, - self::HISTOGRAM_SYMBOL => 80000, - self::HISTORY_SYMBOL => 80000, - self::INACTIVE_SYMBOL => 80014, - self::INTERSECT_SYMBOL => 80031, - self::INVISIBLE_SYMBOL => 80000, - self::JSON_ARRAYAGG_SYMBOL => 80000, - self::JSON_OBJECTAGG_SYMBOL => 80000, - self::JSON_TABLE_SYMBOL => 80000, - self::JSON_VALUE_SYMBOL => 80021, - self::KEYRING_SYMBOL => 80024, - self::LAG_SYMBOL => 80000, - self::LAST_VALUE_SYMBOL => 80000, - self::LATERAL_SYMBOL => 80014, - self::LEAD_SYMBOL => 80000, - self::LOCKED_SYMBOL => 80000, - self::MASTER_COMPRESSION_ALGORITHM_SYMBOL => 80018, - self::MASTER_PUBLIC_KEY_PATH_SYMBOL => 80000, - self::MASTER_TLS_CIPHERSUITES_SYMBOL => 80018, - self::MASTER_ZSTD_COMPRESSION_LEVEL_SYMBOL => 80018, - self::MEMBER_SYMBOL => 80017, - self::NESTED_SYMBOL => 80000, - self::NETWORK_NAMESPACE_SYMBOL => 80017, - self::NOWAIT_SYMBOL => 80000, - self::NTH_VALUE_SYMBOL => 80000, - self::NTILE_SYMBOL => 80000, - self::NULLS_SYMBOL => 80000, - self::OF_SYMBOL => 80000, - self::OFF_SYMBOL => 80019, - self::OJ_SYMBOL => 80017, - self::OLD_SYMBOL => 80014, - self::OPTIONAL_SYMBOL => 80013, - self::ORDINALITY_SYMBOL => 80000, - self::ORGANIZATION_SYMBOL => 80011, - self::OTHERS_SYMBOL => 80000, - self::OVER_SYMBOL => 80000, - self::PASSWORD_LOCK_TIME_SYMBOL => 80019, - self::PATH_SYMBOL => 80000, - self::PERCENT_RANK_SYMBOL => 80000, - self::PERSIST_ONLY_SYMBOL => 80000, - self::PERSIST_SYMBOL => 80000, - self::PRECEDING_SYMBOL => 80000, - self::PRIVILEGE_CHECKS_USER_SYMBOL => 80018, - self::RANDOM_SYMBOL => 80018, - self::RANK_SYMBOL => 80000, - self::RECURSIVE_SYMBOL => 80000, - self::REDO_LOG_SYMBOL => 80021, - self::REFERENCE_SYMBOL => 80011, - self::REQUIRE_ROW_FORMAT_SYMBOL => 80019, - self::REQUIRE_TABLE_PRIMARY_KEY_CHECK_SYMBOL => 80019, - self::RESOURCE_SYMBOL => 80000, - self::RESPECT_SYMBOL => 80000, - self::RESTART_SYMBOL => 80011, - self::RETAIN_SYMBOL => 80014, - self::REUSE_SYMBOL => 80000, - self::RETURNING_SYMBOL => 80021, - self::ROLE_SYMBOL => 80000, - self::ROW_NUMBER_SYMBOL => 80000, - self::SECONDARY_ENGINE_ATTRIBUTE_SYMBOL => 80021, - self::SECONDARY_ENGINE_SYMBOL => 80013, - self::SECONDARY_LOAD_SYMBOL => 80013, - self::SECONDARY_SYMBOL => 80013, - self::SECONDARY_UNLOAD_SYMBOL => 80013, - self::SKIP_SYMBOL => 80000, - self::SOURCE_AUTO_POSITION_SYMBOL => 80000, - self::SOURCE_BIND_SYMBOL => 80000, - self::SOURCE_COMPRESSION_ALGORITHM_SYMBOL => 80000, - self::SOURCE_CONNECT_RETRY_SYMBOL => 80000, - self::SOURCE_CONNECTION_AUTO_FAILOVER_SYMBOL => 80000, - self::SOURCE_DELAY_SYMBOL => 80000, - self::SOURCE_HEARTBEAT_PERIOD_SYMBOL => 80000, - self::SOURCE_HOST_SYMBOL => 80000, - self::SOURCE_LOG_FILE_SYMBOL => 80000, - self::SOURCE_LOG_POS_SYMBOL => 80000, - self::SOURCE_PASSWORD_SYMBOL => 80000, - self::SOURCE_PORT_SYMBOL => 80000, - self::SOURCE_PUBLIC_KEY_PATH_SYMBOL => 80000, - self::SOURCE_RETRY_COUNT_SYMBOL => 80000, - self::SOURCE_SSL_CA_SYMBOL => 80000, - self::SOURCE_SSL_CAPATH_SYMBOL => 80000, - self::SOURCE_SSL_CERT_SYMBOL => 80000, - self::SOURCE_SSL_CIPHER_SYMBOL => 80000, - self::SOURCE_SSL_CRL_SYMBOL => 80000, - self::SOURCE_SSL_CRLPATH_SYMBOL => 80000, - self::SOURCE_SSL_KEY_SYMBOL => 80000, - self::SOURCE_SSL_SYMBOL => 80000, - self::SOURCE_SSL_VERIFY_SERVER_CERT_SYMBOL => 80000, - self::SOURCE_TLS_CIPHERSUITES_SYMBOL => 80000, - self::SOURCE_TLS_VERSION_SYMBOL => 80000, - self::SOURCE_USER_SYMBOL => 80000, - self::SOURCE_ZSTD_COMPRESSION_LEVEL_SYMBOL => 80000, - self::SRID_SYMBOL => 80000, - self::STREAM_SYMBOL => 80019, - self::SYSTEM_SYMBOL => 80000, - self::THREAD_PRIORITY_SYMBOL => 80000, - self::TIES_SYMBOL => 80000, - self::TLS_SYMBOL => 80016, - self::UNBOUNDED_SYMBOL => 80000, - self::VCPU_SYMBOL => 80000, - self::VISIBLE_SYMBOL => 80000, - self::WINDOW_SYMBOL => 80000, - self::ZONE_SYMBOL => 80022, - ); - - /** - * Identifier-like strings that may represent underscore-prefixed charset names. - * - * Includes charsets from both MySQL 5 and 8; via "SHOW CHARACTER SET"/docs: - * https://dev.mysql.com/doc/refman/5.7/en/charset-charsets.html - * https://dev.mysql.com/doc/refman/8.4/en/charset-charsets.html - * - * @TODO: Make the list respect the MySQL version. The _utf8 underscore charset - * exists only on MySQL 5, and maybe some others are version-dependant too. - * We can check this using SHOW CHARACTER SET on different MySQL versions. - */ - const UNDERSCORE_CHARSETS = array( - '_armscii8' => true, - '_ascii' => true, - '_big5' => true, - '_binary' => true, - '_cp1250' => true, - '_cp1251' => true, - '_cp1256' => true, - '_cp1257' => true, - '_cp850' => true, - '_cp852' => true, - '_cp866' => true, - '_cp932' => true, - '_dec8' => true, - '_eucjpms' => true, - '_euckr' => true, - '_gb18030' => true, - '_gb2312' => true, - '_gbk' => true, - '_geostd8' => true, - '_greek' => true, - '_hebrew' => true, - '_hp8' => true, - '_keybcs2' => true, - '_koi8r' => true, - '_koi8u' => true, - '_latin1' => true, - '_latin2' => true, - '_latin5' => true, - '_latin7' => true, - '_macce' => true, - '_macroman' => true, - '_sjis' => true, - '_swe7' => true, - '_tis620' => true, - '_ucs2' => true, - '_ujis' => true, - '_utf16' => true, - '_utf16le' => true, - '_utf32' => true, - '_utf8' => true, - '_utf8mb3' => true, - '_utf8mb4' => true, - ); - - /** - * The SQL payload to tokenize. - * - * @var string - */ - private $sql; - - /** - * Byte length of the SQL payload. - * - * @var int - */ - private $sql_length; - - /** - * The version of the MySQL server that the SQL payload is intended for. - * - * This is used to determine which tokens are valid for the given MySQL - * version, and how some tokens should be interpreted. - * - * @var int - */ - private $mysql_version; - - /** - * The SQL modes that should be considered active during tokenization. - * - * This is an integer that represents currently active SQL modes as a bitmask. - * The SQL modes are defined as "SQL_MODE_"-prefixed constants in this class. - * The list of the SQL modes isn't exhaustive, as only some affect tokenization. - * - * @var int - */ - private $sql_modes = 0; - - /** - * How many bytes from the original SQL payload have been read and tokenized. - * - * This is an internal cursor that is used to track the current position in - * the SQL payload during tokenization. When used as an index in the SQL - * payload, it points to the next byte to read. - * - * @var int - */ - private $bytes_already_read = 0; - - /** - * Byte offset in the SQL payload where current token starts. - * - * This is used to extract the token bytes after the token is processed. - * The bytes of the current token are represented by "$this->sql" in range - * from "$this->token_starts_at" to "$this->bytes_already_read - 1". - * - * @var int - */ - private $token_starts_at = 0; - - /** - * The type of the current token. - * - * When a token is successfully recognized and read, this value is set to the - * constant representing the token type. When no token was read yet, or the - * end of the SQL payload or an invalid token is reached, this value is null. - * - * @var int|null - */ - private $token_type; - - /** - * Whether the tokenizer is inside an active MySQL-specific comment. - * - * MySQL supports a special comment syntax whose content is recognized as - * a comment by most database engines, but can be treated as SQL by MySQL: - * - * 1. /*! ... - The content is treated as SQL. - * 2. /*!12345 - The content is treated as SQL when "MySQL version >= 12345". - * - * @var bool - */ - private $in_mysql_comment = false; - - /** - * @param string $sql The SQL payload to tokenize. - * @param int $mysql_version The version of the MySQL server that the SQL payload is intended for. - * @param string[] $sql_modes The SQL modes that should be considered active during tokenization. - */ - public function __construct( - string $sql, - int $mysql_version = 80038, - array $sql_modes = array() - ) { - $this->sql = $sql; - $this->sql_length = strlen( $sql ); - $this->mysql_version = $mysql_version; - - foreach ( $sql_modes as $sql_mode ) { - $sql_mode = strtoupper( $sql_mode ); - if ( 'HIGH_NOT_PRECEDENCE' === $sql_mode ) { - $this->sql_modes |= self::SQL_MODE_HIGH_NOT_PRECEDENCE; - } elseif ( 'PIPES_AS_CONCAT' === $sql_mode ) { - $this->sql_modes |= self::SQL_MODE_PIPES_AS_CONCAT; - } elseif ( 'IGNORE_SPACE' === $sql_mode ) { - $this->sql_modes |= self::SQL_MODE_IGNORE_SPACE; - } elseif ( 'NO_BACKSLASH_ESCAPES' === $sql_mode ) { - $this->sql_modes |= self::SQL_MODE_NO_BACKSLASH_ESCAPES; - } - } - } - - /** - * Read the next token from the SQL payload and return it as a token object. - * - * This method reads bytes from the SQL payload until a token is recognized. - * It starts from "$this->sql[ $this->bytes_already_read ]", advances the - * number of bytes read, and returns a boolean indicating whether a token - * was successfully recognized and read. When the end of the SQL payload - * or an invalid token is reached, the method returns false. - * - * @return bool Whether a token was successfully recognized and read. - */ - public function next_token(): bool { - // We already reached the end of the SQL payload or an invalid token. - // Don't attempt to read any more bytes, and bail out immediately. - if ( - self::EOF === $this->token_type - || ( null === $this->token_type && $this->bytes_already_read > 0 ) - ) { - $this->token_type = null; - return false; - } - - // Skip leading whitespace inline for optimal performance. - $this->bytes_already_read += strspn( $this->sql, self::WHITESPACE_MASK, $this->bytes_already_read ); - - do { - $this->token_starts_at = $this->bytes_already_read; - $this->token_type = $this->read_next_token(); - } while ( - self::WHITESPACE === $this->token_type - || self::COMMENT === $this->token_type - || self::MYSQL_COMMENT_START === $this->token_type - || self::MYSQL_COMMENT_END === $this->token_type - ); - - // Invalid input. - if ( null === $this->token_type ) { - return false; - } - return true; - } - - /** - * Return the current token represented as a WP_MySQL_Token object. - * - * When no token was read yet, or the end of the SQL payload or an invalid - * token is reached, the method returns null. - * - * @TODO: Consider referential stability ($lexer->get_token() === $lexer->get_token()), - * or separate getters for the token type and token bytes (no token objects). - * - * @return WP_MySQL_Token|null An object representing the next recognized token or null. - */ - public function get_token(): ?WP_MySQL_Token { - if ( null === $this->token_type ) { - return null; - } - return new WP_MySQL_Token( - $this->token_type, - $this->token_starts_at, - $this->bytes_already_read - $this->token_starts_at, - $this->sql, - $this->is_sql_mode_active( self::SQL_MODE_NO_BACKSLASH_ESCAPES ) - ); - } - - /** - * Read all remaining tokens from the SQL payload and return them as an array. - * - * This method starts from the current position in the SQL payload, as marked - * by "$this->sql[ $this->bytes_already_read ]", and reads all tokens until - * the end of the SQL payload is reached, returning an array of token objects. - * - * When an invalid token is reached, the method stops and returns the partial - * sequence of valid tokens. In this case, the EOF token will not be included. - * - * This method can be used to tokenize the whole SQL payload at once, at the - * expense of storing all token objects in memory at the same time. - * - * @return WP_MySQL_Token[] An array of token objects representing the remaining tokens. - */ - public function remaining_tokens(): array { - $tokens = array(); - $no_backslash_escapes_sql_mode_set = $this->is_sql_mode_active( - self::SQL_MODE_NO_BACKSLASH_ESCAPES - ); - - while ( true ) { - // Bail on EOF, or on a null token type once at least one byte has - // been consumed (read_next_token() hit invalid input mid-stream). - if ( - self::EOF === $this->token_type - || ( null === $this->token_type && $this->bytes_already_read > 0 ) - ) { - $this->token_type = null; - break; - } - - // Skip leading whitespace inline for optimal performance. - $this->bytes_already_read += strspn( $this->sql, self::WHITESPACE_MASK, $this->bytes_already_read ); - - do { - $this->token_starts_at = $this->bytes_already_read; - $this->token_type = $this->read_next_token(); - } while ( - self::WHITESPACE === $this->token_type - || self::COMMENT === $this->token_type - || self::MYSQL_COMMENT_START === $this->token_type - || self::MYSQL_COMMENT_END === $this->token_type - ); - - if ( null === $this->token_type ) { - break; - } - - $tokens[] = new WP_MySQL_Token( - $this->token_type, - $this->token_starts_at, - $this->bytes_already_read - $this->token_starts_at, - $this->sql, - $no_backslash_escapes_sql_mode_set - ); - - if ( self::EOF === $this->token_type ) { - $this->token_type = null; - break; - } - } - return $tokens; - } - - /** - * The version of the MySQL server that the SQL payload is intended for. - * - * This represents the MySQL server version that the lexer is set up to - * consider when tokenizing the SQL payload. - * - * @return int The MySQL server version that the lexer is set up to consider. - */ - public function get_mysql_version(): int { - return $this->mysql_version; - } - - /** - * Whether an SQL mode is set to be considered as active during tokenization. - * The SQL modes are defined as "SQL_MODE_"-prefixed constants in this class. - * - * @param int $mode The SQL mode to check, an "SQL_MODE_"-prefixed constant. - * @return bool Whether the given SQL mode is active. - */ - public function is_sql_mode_active( int $mode ): bool { - return ( $this->sql_modes & $mode ) !== 0; - } - - /** - * Get the numeric token ID for a given token name. - * - * @param string $token_name The name of the token. - * @return int|null The token ID for the given token name; null when not found. - */ - public static function get_token_id( string $token_name ): ?int { - $constant_name = self::class . '::' . $token_name; - if ( ! defined( $constant_name ) ) { - return null; - } - return constant( $constant_name ); - } - - /** - * Get the name of a token for a given token ID. - * - * This method is intended to be used only for testing and debugging purposes, - * when tokens need to be presented by their names in a human-readable form. - * It should not be used in production code, as it's not performance-optimized. - * - * @param int $token_id The numeric token ID. - * @return string The token name for the given token ID; null when not found. - */ - public static function get_token_name( int $token_id ): ?string { - $reflection = new ReflectionClass( self::class ); - // Reverse the array, as some constant values in the class can conflict, - // and tokens are defined at the end of the class constant definitions. - // @TODO: Consider are more robust way to determine the token name. - // E.g., prefix all token constant names with a common prefix. - $constants = array_reverse( $reflection->getConstants() ); - $token_name = array_search( $token_id, $constants, true ); - return $token_name ? $token_name : null; - } - - private function read_next_token(): ?int { - $byte = $this->sql[ $this->bytes_already_read ] ?? null; - $next_byte = $this->sql[ $this->bytes_already_read + 1 ] ?? null; - - // A map for a single-byte symbol fast path. - static $single_byte_ops = array( - '(' => self::OPEN_PAR_SYMBOL, - ')' => self::CLOSE_PAR_SYMBOL, - ',' => self::COMMA_SYMBOL, - ';' => self::SEMICOLON_SYMBOL, - '+' => self::PLUS_OPERATOR, - '~' => self::BITWISE_NOT_OPERATOR, - '%' => self::MOD_OPERATOR, - '^' => self::BITWISE_XOR_OPERATOR, - '?' => self::PARAM_MARKER, - '{' => self::OPEN_CURLY_SYMBOL, - '}' => self::CLOSE_CURLY_SYMBOL, - '=' => self::EQUAL_OPERATOR, - ); - - // Fast path for keywords and identifiers. - // `$byte > "\x7F"` catches any non-ASCII byte (0x80-0xFF); read_identifier() - // restricts the accepted identifier codepoints to U+0080-U+FFFF. - // `"'" !== $next_byte` defers x'..', n'..' and similar special - // literals to their dedicated branches below; only single quotes - // form those, regardless of SQL mode. - if ( - ( - ( $byte >= 'a' && $byte <= 'z' ) - || ( $byte >= 'A' && $byte <= 'Z' ) - || $byte > "\x7F" - ) - && "'" !== $next_byte - ) { - $started_at = $this->bytes_already_read; - $type = $this->read_identifier(); - if ( - self::IDENTIFIER === $type - // When preceded by a dot, it is always an identifier. - && ! ( $started_at > 0 && '.' === $this->sql[ $started_at - 1 ] ) - ) { - // Inline the keyword lookup on the hot identifier path: most - // identifiers are not keywords, so this avoids two method calls - // (token-bytes extraction + keyword determination) per token. - $keyword = self::TOKENS[ strtoupper( - substr( $this->sql, $started_at, $this->bytes_already_read - $started_at ) - ) ] ?? self::IDENTIFIER; - if ( self::IDENTIFIER !== $keyword ) { - $type = $this->resolve_keyword_type( $keyword ); - } - } - } elseif ( null !== $byte && isset( $single_byte_ops[ $byte ] ) ) { - // Fast path for single-byte symbols. - $this->bytes_already_read += 1; - $type = $single_byte_ops[ $byte ]; - } elseif ( "'" === $byte || '"' === $byte || '`' === $byte ) { - $type = $this->read_quoted_text(); - } elseif ( null !== $byte && strspn( $byte, self::DIGIT_MASK ) > 0 ) { - $type = $this->read_number(); - } elseif ( '.' === $byte ) { - if ( null !== $next_byte && strspn( $next_byte, self::DIGIT_MASK ) > 0 ) { - $type = $this->read_number(); - } else { - $this->bytes_already_read += 1; - $type = self::DOT_SYMBOL; - } - } elseif ( ':' === $byte ) { - $this->bytes_already_read += 1; // Consume the ':'. - if ( '=' === $next_byte ) { - $this->bytes_already_read += 1; // Consume the '='. - $type = self::ASSIGN_OPERATOR; - } else { - $type = self::COLON_SYMBOL; - } - } elseif ( '<' === $byte ) { - $this->bytes_already_read += 1; // Consume the '<'. - if ( '=' === $next_byte ) { - $this->bytes_already_read += 1; // Consume the '='. - if ( '>' === ( $this->sql[ $this->bytes_already_read ] ?? null ) ) { - $this->bytes_already_read += 1; // Consume the '>'. - $type = self::NULL_SAFE_EQUAL_OPERATOR; - } else { - $type = self::LESS_OR_EQUAL_OPERATOR; - } - } elseif ( '>' === $next_byte ) { - $this->bytes_already_read += 1; // Consume the '>'. - $type = self::NOT_EQUAL_OPERATOR; - } elseif ( '<' === $next_byte ) { - $this->bytes_already_read += 1; // Consume the '<'. - $type = self::SHIFT_LEFT_OPERATOR; - } else { - $type = self::LESS_THAN_OPERATOR; - } - } elseif ( '>' === $byte ) { - $this->bytes_already_read += 1; // Consume the '>'. - if ( '=' === $next_byte ) { - $this->bytes_already_read += 1; // Consume the '='. - $type = self::GREATER_OR_EQUAL_OPERATOR; - } elseif ( '>' === $next_byte ) { - $this->bytes_already_read += 1; // Consume the '>'. - $type = self::SHIFT_RIGHT_OPERATOR; - } else { - $type = self::GREATER_THAN_OPERATOR; - } - } elseif ( '!' === $byte ) { - $this->bytes_already_read += 1; // Consume the '!'. - if ( '=' === $next_byte ) { - $this->bytes_already_read += 1; // Consume the '='. - $type = self::NOT_EQUAL_OPERATOR; - } else { - $type = self::LOGICAL_NOT_OPERATOR; - } - } elseif ( '-' === $byte ) { - if ( - '-' === $next_byte - && $this->bytes_already_read + 2 < $this->sql_length - && strspn( $this->sql[ $this->bytes_already_read + 2 ], self::WHITESPACE_MASK ) > 0 - ) { - $type = $this->read_line_comment(); - } elseif ( '>' === $next_byte ) { - $this->bytes_already_read += 2; // Consume the '->'. - if ( '>' === ( $this->sql[ $this->bytes_already_read ] ?? null ) ) { - $this->bytes_already_read += 1; // Consume the '>'. - if ( $this->mysql_version >= 50713 ) { - $type = self::JSON_UNQUOTED_SEPARATOR_SYMBOL; - } else { - return null; // Invalid input. - } - } else { - if ( $this->mysql_version >= 50708 ) { - $type = self::JSON_SEPARATOR_SYMBOL; - } else { - return null; // Invalid input. - } - } - } else { - $this->bytes_already_read += 1; // Consume the '-'. - $type = self::MINUS_OPERATOR; - } - } elseif ( '*' === $byte ) { - $this->bytes_already_read += 1; - if ( '/' === $next_byte && $this->in_mysql_comment ) { - $this->bytes_already_read += 1; // Consume the '/'. - $type = self::MYSQL_COMMENT_END; - $this->in_mysql_comment = false; - } else { - $type = self::MULT_OPERATOR; - } - } elseif ( '/' === $byte ) { - if ( '*' === $next_byte ) { - if ( '!' === ( $this->sql[ $this->bytes_already_read + 2 ] ?? null ) ) { - $type = $this->read_mysql_comment(); - } else { - $this->bytes_already_read += 2; // Consume the '/*'. - $this->read_comment_content(); - $type = self::COMMENT; - } - } else { - $this->bytes_already_read += 1; - $type = self::DIV_OPERATOR; - } - } elseif ( '&' === $byte ) { - $this->bytes_already_read += 1; // Consume the '&'. - if ( '&' === $next_byte ) { - $this->bytes_already_read += 1; // Consume the '&'. - $type = self::LOGICAL_AND_OPERATOR; - } else { - $type = self::BITWISE_AND_OPERATOR; - } - } elseif ( '|' === $byte ) { - $this->bytes_already_read += 1; // Consume the '|'. - if ( '|' === $next_byte ) { - $this->bytes_already_read += 1; // Consume the '|'. - $type = $this->is_sql_mode_active( self::SQL_MODE_PIPES_AS_CONCAT ) - ? self::CONCAT_PIPES_SYMBOL - : self::LOGICAL_OR_OPERATOR; - } else { - $type = self::BITWISE_OR_OPERATOR; - } - } elseif ( '@' === $byte ) { - $this->bytes_already_read += 1; // Consume the '@'. - - if ( '@' === $next_byte ) { - $this->bytes_already_read += 1; // Consume the second '@'. - $type = self::AT_AT_SIGN_SYMBOL; - } else { - /** - * Check whether the '@' marks an unquoted user-defined variable: - * https://dev.mysql.com/doc/refman/8.4/en/user-variables.html - * - * Rules: - * 1. Starts with a '@'. - * 2. Allowed following characters are ASCII a-z, A-Z, 0-9, _, ., $. - */ - $length = strspn( $this->sql, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.$', $this->bytes_already_read ); - if ( $length > 0 ) { - $this->bytes_already_read += $length; - $type = self::AT_TEXT_SUFFIX; - } else { - $type = self::AT_SIGN_SYMBOL; - } - } - } elseif ( '\\' === $byte ) { - $this->bytes_already_read += 1; // Consume the '\'. - if ( 'N' === $next_byte ) { - $this->bytes_already_read += 1; // Consume the 'N'. - $type = self::NULL2_SYMBOL; - } else { - return null; // Invalid input. - } - } elseif ( '#' === $byte ) { - $type = $this->read_line_comment(); - } elseif ( null !== $byte && strspn( $byte, self::WHITESPACE_MASK ) > 0 ) { - $this->bytes_already_read += strspn( $this->sql, self::WHITESPACE_MASK, $this->bytes_already_read ); - $type = self::WHITESPACE; - } elseif ( ( 'x' === $byte || 'X' === $byte || 'b' === $byte || 'B' === $byte ) && "'" === $next_byte ) { - $type = $this->read_number(); - } elseif ( ( 'n' === $byte || 'N' === $byte ) && "'" === $next_byte ) { - $this->bytes_already_read += 1; // n/N - $type = $this->read_quoted_text( "'" ); - if ( self::SINGLE_QUOTED_TEXT === $type ) { - $type = self::NCHAR_TEXT; - } - } elseif ( null === $byte ) { - $type = self::EOF; - } else { - $started_at = $this->bytes_already_read; - $type = $this->read_identifier(); - if ( self::IDENTIFIER === $type ) { - // When preceded by a dot, it is always an identifier. - if ( $started_at > 0 && '.' === $this->sql[ $started_at - 1 ] ) { - $type = self::IDENTIFIER; - } elseif ( '_' === $byte && isset( self::UNDERSCORE_CHARSETS[ strtolower( $this->get_current_token_bytes() ) ] ) ) { - $type = self::UNDERSCORE_CHARSET; - } else { - $type = $this->determine_identifier_or_keyword_type( $this->get_current_token_bytes() ); - } - } - } - return $type; - } - - private function get_current_token_bytes(): string { - return substr( - $this->sql, - $this->token_starts_at, - $this->bytes_already_read - $this->token_starts_at - ); - } - - /** - * Read an unquoted identifier. - * - * This function reads characters that are allowed in an unquoted identifier. - * An identifier cannot consist solely of digits, but this function doesn't - * ensure that explicitly, as numbers are processed before identifiers in - * the tokenization process, recognizing all digit-only sequences as numbers. - * - * Rules: - * 1. Allowed characters are ASCII a-z, A-Z, 0-9, _, $, and Unicode U+0080-U+FFFF. - * 2. Unquoted identifiers may begin with a digit but may not consist solely of digits. - * - * See: - * https://dev.mysql.com/doc/refman/8.4/en/identifiers.html - */ - private function read_identifier(): ?int { - $started_at = $this->bytes_already_read; - while ( true ) { - // First, let's try to parse an ASCII sequence. - $this->bytes_already_read += strspn( - $this->sql, - 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$', - $this->bytes_already_read - ); - - // Check if the following byte can be part of a multibyte character - // in the range of U+0080 to U+FFFF before looking at further bytes. - // If it can't, bail out early to avoid unnecessary UTF-8 decoding. - // Identifiers are usually ASCII-only, so we can optimize for that. - $byte_1 = ord( - $this->sql[ $this->bytes_already_read ] ?? "\0" - ); - if ( $byte_1 < 0xC2 || $byte_1 > 0xEF ) { - break; - } - - // Look for a valid 2-byte UTF-8 symbol. Covers range U+0080 - U+07FF. - $byte_2 = ord( - $this->sql[ $this->bytes_already_read + 1 ] ?? "\0" - ); - if ( - $byte_1 <= 0xDF - && $byte_2 >= 0x80 && $byte_2 <= 0xBF - ) { - $this->bytes_already_read += 2; - continue; - } - - // Look for a valid 3-byte UTF-8 symbol in range U+0800 - U+FFFF. - $byte_3 = ord( - $this->sql[ $this->bytes_already_read + 2 ] ?? "\0" - ); - if ( - $byte_1 <= 0xEF - && $byte_2 >= 0x80 && $byte_2 <= 0xBF - && $byte_3 >= 0x80 && $byte_3 <= 0xBF - // Exclude surrogate range U+D800 to U+DFFF: - && ! ( 0xED === $byte_1 && $byte_2 >= 0xA0 ) - // Exclude overlong encodings: - && ! ( 0xE0 === $byte_1 && $byte_2 < 0xA0 ) - ) { - $this->bytes_already_read += 3; - continue; - } - - // Not a valid identifier character. - break; - } - - // An identifier cannot consist solely of digits, but we don't need to - // ensure that explicitly, as numbers are processed before identifiers. - - return $this->bytes_already_read - $started_at > 0 - ? self::IDENTIFIER - : null; // Invalid input. - } - - private function read_number(): ?int { - // @TODO: Support numeric-only identifier parts after "." (e.g., 1ea10.1). - - $byte = $this->sql[ $this->bytes_already_read ] ?? null; - $next_byte = $this->sql[ $this->bytes_already_read + 1 ] ?? null; - $third_byte = $this->sql[ $this->bytes_already_read + 2 ] ?? null; - - if ( - // HEX number in the form of 0xN. - ( - '0' === $byte - && 'x' === $next_byte - && null !== $third_byte - && strspn( $third_byte, self::HEX_DIGIT_MASK ) > 0 - ) - // HEX number in the form of x'N' or X'N'. - || ( ( 'x' === $byte || 'X' === $byte ) && "'" === $next_byte ) - ) { - $is_quoted = "'" === $next_byte; - $this->bytes_already_read += 2; // Consume "0x" or "x'". - $this->bytes_already_read += strspn( $this->sql, self::HEX_DIGIT_MASK, $this->bytes_already_read ); - if ( $is_quoted ) { - if ( - $this->bytes_already_read >= $this->sql_length - || "'" !== $this->sql[ $this->bytes_already_read ] - ) { - return null; // Invalid input. - } - $this->bytes_already_read += 1; // Consume the "'". - } - $type = self::HEX_NUMBER; - } elseif ( - // BIN number in the form of 0bN. - ( - '0' === $byte - && 'b' === $next_byte - && ( '0' === $third_byte || '1' === $third_byte ) - ) - // BIN number in the form of b'N' or B'N'. - || ( ( 'b' === $byte || 'B' === $byte ) && "'" === $next_byte ) - ) { - $is_quoted = "'" === $next_byte; - $this->bytes_already_read += 2; // Consume "0b" or "b'". - $this->bytes_already_read += strspn( $this->sql, '01', $this->bytes_already_read ); - if ( $is_quoted ) { - if ( - $this->bytes_already_read >= $this->sql_length - || "'" !== $this->sql[ $this->bytes_already_read ] - ) { - return null; // Invalid input. - } - $this->bytes_already_read += 1; // Consume the "'". - } - $type = self::BIN_NUMBER; - } else { - // Here, we have a sequence starting with N or .N, where N is a digit. - - // 1. Try integer first. - $this->bytes_already_read += strspn( $this->sql, self::DIGIT_MASK, $this->bytes_already_read ); - $type = self::INT_NUMBER; - - // 2. In case of N. or .N, it's a decimal or float number. - if ( '.' === ( $this->sql[ $this->bytes_already_read ] ?? null ) ) { - $this->bytes_already_read += 1; - $type = self::DECIMAL_NUMBER; - $this->bytes_already_read += strspn( $this->sql, self::DIGIT_MASK, $this->bytes_already_read ); - } - - // 3. When exponent is present, it's a float number. - $byte = $this->sql[ $this->bytes_already_read ] ?? null; - $next_byte = $this->sql[ $this->bytes_already_read + 1 ] ?? null; - $has_exponent = - ( 'e' === $byte || 'E' === $byte ) - && null !== $next_byte - && ( - strspn( $next_byte, self::DIGIT_MASK ) > 0 - || ( - ( '+' === $next_byte || '-' === $next_byte ) - && $this->bytes_already_read + 2 < $this->sql_length - && strspn( $this->sql[ $this->bytes_already_read + 2 ], self::DIGIT_MASK ) > 0 - ) - ); - if ( $has_exponent ) { - $this->bytes_already_read += 1; // Consume the 'e' or 'E'. - $this->bytes_already_read += 1; // Consume the '+', '-', or digit. - $this->bytes_already_read += strspn( $this->sql, self::DIGIT_MASK, $this->bytes_already_read ); - $type = self::FLOAT_NUMBER; - } - } - - /* - * In MySQL, when an input matches both a number and an identifier, the - * number always wins. However, if the number is followed by a non-numeric - * identifier-like character, then it is recognized as an identifier... - * Unless it's a float number, which ignores any subsequent input. - * - * Examples: - * - "1234" (integer) vs. "1234a" (identifier) - * - "0b01" (bin) vs. "0b012" (identifier) - * - "0xa1" (hex) vs. "0xa1x" (identifier) - * - "12.3" (decimal) vs. "12.3a" (identifier) - * - "1e10" (float) vs. "1e10a" (float, followed by identifier) - */ - $text = $this->get_current_token_bytes(); - $possible_identifier_prefix = - self::INT_NUMBER === $type - || ( '0' === $text[0] && ( 'b' === $text[1] || 'x' === $text[1] ) ); - - /* - * When we match some subsequent identifier bytes, it's an identifier. - * Note that the "$this->read_identifier()" method doesn't check that - * the identifier doesn't consist solely of digits. This is an advantage - * here, as we can look only at subsequent bytes instead of backtracking - * to the beginning of the number (for valid identifiers like 0b019). - */ - if ( $possible_identifier_prefix && self::IDENTIFIER === $this->read_identifier() ) { - $type = self::IDENTIFIER; - } - - // Determine integer type. - if ( self::INT_NUMBER === $type ) { - // Fast path for most integers. - $bytes = $this->get_current_token_bytes(); - if ( strlen( $bytes ) < 10 ) { - return self::INT_NUMBER; - } - - // Remove leading zeros. - $bytes = substr( $bytes, strspn( $bytes, '0' ) ); - $length = strlen( $bytes ); - - // Determine integer type based on its length and value. - if ( $length < 10 ) { - return self::INT_NUMBER; - } elseif ( 10 === $length ) { - return strcmp( $bytes, '2147483647' ) > 0 - ? self::LONG_NUMBER - : self::INT_NUMBER; - } elseif ( $length < 19 ) { - return self::LONG_NUMBER; - } elseif ( 19 === $length ) { - return strcmp( $bytes, '9223372036854775807' ) > 0 - ? self::ULONGLONG_NUMBER - : self::LONG_NUMBER; - } elseif ( 20 === $length ) { - return strcmp( $bytes, '18446744073709551615' ) > 0 - ? self::DECIMAL_NUMBER - : self::ULONGLONG_NUMBER; - } else { - return self::DECIMAL_NUMBER; - } - } - return $type; - } - - /** - * Quoted literals and identifiers: - * https://dev.mysql.com/doc/refman/8.4/en/string-literals.html - * https://dev.mysql.com/doc/refman/8.4/en/identifiers.html - * - * Rules: - * 1. Quotes can be escaped by doubling them ('', "", ``). - * 2. Backslashes escape the next character, unless NO_BACKSLASH_ESCAPES is set. - */ - private function read_quoted_text(): ?int { - $quote = $this->sql[ $this->bytes_already_read ]; - $this->bytes_already_read += 1; // Consume the quote. - - $no_backslash_escapes = $this->is_sql_mode_active( - self::SQL_MODE_NO_BACKSLASH_ESCAPES - ); - - // We need to look for the closing quote in a loop, as it can be escaped, - // in which case the escape sequence is consumed and the loop continues. - $at = $this->bytes_already_read; - while ( true ) { - $quote_at = strpos( $this->sql, $quote, $at ); - if ( false === $quote_at ) { - return null; // Invalid input. - } - $at = $quote_at; - - /* - * By default, quotes can be escaped with a "\". - * When NO_BACKSLASH_ESCAPES SQL mode is active, the "\" treated as - * a regular character. - * - * The quote is escaped only when the number of preceding backslashes - * is odd - "\" is an escape sequence, "\\" is an escaped backslash, - * "\\\" is an escaped backslash and an escape sequence, and so on. - * - * The `($at - $i - 1) >= 0` guard prevents PHP's negative-string- - * offset wraparound (PHP 7.1+) when the closing-quote candidate - * sits at the very start of the input. The `?? null` covers - * positive out-of-range indexes belt-and-suspenders. - */ - if ( ! $no_backslash_escapes ) { - $i = 0; - while ( ( $at - $i - 1 ) >= 0 && '\\' === ( $this->sql[ $at - $i - 1 ] ?? null ) ) { - $i += 1; - } - if ( 1 === $i % 2 ) { - $at += 1; - continue; - } - } - - // Check if the quote is doubled. - if ( ( $this->sql[ $at + 1 ] ?? null ) === $quote ) { - $at += 2; - continue; - } - - break; - } - $at += 1; - - $this->bytes_already_read = $at; - - if ( '`' === $quote ) { - return self::BACK_TICK_QUOTED_ID; - } elseif ( '"' === $quote ) { - return self::DOUBLE_QUOTED_TEXT; - } else { - return self::SINGLE_QUOTED_TEXT; - } - } - - private function read_line_comment(): int { - $this->bytes_already_read += strcspn( $this->sql, "\r\n", $this->bytes_already_read ); - return self::COMMENT; - } - - private function read_mysql_comment(): int { - // @TODO: Consider supporting optimizer hints (/*+ ... */) or document - // that they are not supported. - // @TODO: Implement six-digit version number support (from MySQL 8.4). - - // MySQL-specific comment in one of the following forms: - // 1. /*! ... */ - The content is treated as SQL. - // 2. /*!12345 ... */ - The content is treated as SQL when "MySQL version >= 12345". - $this->bytes_already_read += 3; // Consume the '/*!'. - - // Check if the next 5 characters are digits. - $digit_count = strspn( $this->sql, self::DIGIT_MASK, $this->bytes_already_read, 5 ); - $is_version_comment = 5 === $digit_count; - - // For version comments, extract the version number. - $version = $is_version_comment - ? (int) substr( $this->sql, $this->bytes_already_read, $digit_count ) - : 0; - - if ( $this->mysql_version < $version ) { - // Version not satisfied. Treat the content as a regular comment. - $this->read_comment_content(); - return self::COMMENT; - } else { - // Version satisfied or not specified. Treat the content as SQL code. - $this->bytes_already_read += $digit_count; // Skip the version number. - $this->in_mysql_comment = true; - return self::MYSQL_COMMENT_START; - } - } - - private function read_comment_content(): void { - $comment_end = strpos( $this->sql, '*/', $this->bytes_already_read ); - if ( false === $comment_end ) { - $this->bytes_already_read = $this->sql_length; - } else { - $this->bytes_already_read = $comment_end + 2; - } - } - - private function determine_identifier_or_keyword_type( string $value ): int { - $type = self::TOKENS[ strtoupper( $value ) ] ?? self::IDENTIFIER; - if ( self::IDENTIFIER === $type ) { - return self::IDENTIFIER; - } - return $this->resolve_keyword_type( $type ); - } - - /** - * Resolve a keyword token id matched in self::TOKENS, applying version gating, - * function-call lookahead, the SQL_MODE_HIGH_NOT_PRECEDENCE rule, and synonyms. - * - * @param int $type A token id already matched in self::TOKENS (never IDENTIFIER). - */ - private function resolve_keyword_type( int $type ): int { - - // Apply MySQL version specifics (positive number: >= , negative number: < ). - if ( isset( self::VERSIONS[ $type ] ) ) { - $version = self::VERSIONS[ $type ]; - if ( $this->mysql_version < $version || -$version >= $this->mysql_version ) { - return self::IDENTIFIER; - } - } - - // Apply MySQL version ranges manually. - if ( - self::MAX_STATEMENT_TIME_SYMBOL === $type - && ! ( $this->mysql_version >= 50704 && $this->mysql_version < 50708 ) - ) { - return self::IDENTIFIER; - } - - if ( - self::NONBLOCKING_SYMBOL === $type - && ! ( $this->mysql_version >= 50700 && $this->mysql_version < 50706 ) - ) { - return self::IDENTIFIER; - } - - if ( - self::REMOTE_SYMBOL === $type - && ( $this->mysql_version >= 80003 && $this->mysql_version < 80014 ) - ) { - return self::IDENTIFIER; - } - - // Determine function calls. - if ( isset( self::FUNCTIONS[ $type ] ) ) { - // Skip any whitespace character if the SQL mode says they should be ignored. - if ( $this->is_sql_mode_active( self::SQL_MODE_IGNORE_SPACE ) ) { - $this->bytes_already_read += strspn( $this->sql, self::WHITESPACE_MASK, $this->bytes_already_read ); - } - if ( '(' !== ( $this->sql[ $this->bytes_already_read ] ?? null ) ) { - return self::IDENTIFIER; - } - } - - // With "SQL_MODE_HIGH_NOT_PRECEDENCE" enabled, "NOT" needs to be emitted as a higher priority NOT2 symbol. - if ( self::NOT_SYMBOL === $type && $this->is_sql_mode_active( self::SQL_MODE_HIGH_NOT_PRECEDENCE ) ) { - $type = self::NOT2_SYMBOL; - } - - // Apply synonyms. - return self::SYNONYMS[ $type ] ?? $type; - } -} diff --git a/packages/mysql-on-sqlite/src/mysql/class-wp-mysql-parser.php b/packages/mysql-on-sqlite/src/mysql/class-wp-mysql-parser.php deleted file mode 100644 index 69282b9c4..000000000 --- a/packages/mysql-on-sqlite/src/mysql/class-wp-mysql-parser.php +++ /dev/null @@ -1,63 +0,0 @@ - $tokens The parser tokens. - */ - public function reset_tokens( array $tokens ): void { - $this->tokens = $tokens; - $this->position = 0; - $this->current_ast = null; - } - - /** - * Parse the next query from the input SQL string. - * - * This method reads tokens until a query is parsed, or the parsing fails. - * It returns a boolean indicating whether a query was successfully parsed. - * - * Example: - * - * // Parse all queries in the input SQL string. - * $parser = new WP_MySQL_Parser( $sql ); - * while ( $parser->next_query() ) { - * $ast = $parser->get_query_ast(); - * if ( ! $ast ) { - * // The parsing failed. - * } - * // The query was successfully parsed. - * } - * - * @return bool Whether a query was successfully parsed. - */ - public function next_query(): bool { - if ( $this->position >= count( $this->tokens ) ) { - return false; - } - $this->current_ast = $this->parse(); - return true; - } - - /** - * Get the current query AST. - * - * When no query has been parsed yet, the parsing failed, or the end of the - * input was reached, this method returns null. - * - * @see WP_MySQL_Parser::next_query() for usage example. - * - * @return WP_Parser_Node|null The current query AST, or null if no query was parsed. - */ - public function get_query_ast(): ?WP_Parser_Node { - return $this->current_ast; - } -} diff --git a/packages/mysql-on-sqlite/src/mysql/class-wp-mysql-token.php b/packages/mysql-on-sqlite/src/mysql/class-wp-mysql-token.php deleted file mode 100644 index 0840bc2f2..000000000 --- a/packages/mysql-on-sqlite/src/mysql/class-wp-mysql-token.php +++ /dev/null @@ -1,187 +0,0 @@ -id = $id; - $this->start = $start; - $this->length = $length; - $this->input = $input; - - $this->sql_mode_no_backslash_escapes_enabled = $sql_mode_no_backslash_escapes_enabled; - } - - /** - * Get the name of the token. - * - * This method is intended to be used only for testing and debugging purposes, - * when tokens need to be presented by their names in a human-readable form. - * It should not be used in production code, as it's not performance-optimized. - * - * @return string The token name. - */ - public function get_name(): string { - $name = WP_MySQL_Lexer::get_token_name( $this->id ); - if ( null === $name ) { - $name = 'UNKNOWN'; - } - return $name; - } - - /** - * Get the real unquoted value of the token. - * - * @return string The token value. - */ - public function get_value(): string { - $value = $this->get_bytes(); - if ( - WP_MySQL_Lexer::SINGLE_QUOTED_TEXT === $this->id - || WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT === $this->id - || WP_MySQL_Lexer::BACK_TICK_QUOTED_ID === $this->id - ) { - // Remove bounding quotes. - $quote = $value[0]; - $value = substr( $value, 1, -1 ); - - /* - * When the NO_BACKSLASH_ESCAPES SQL mode is enabled, we only need to - * handle escaped bounding quotes, as the other characters preserve - * their literal values. - */ - if ( $this->sql_mode_no_backslash_escapes_enabled ) { - return str_replace( $quote . $quote, $quote, $value ); - } - - /** - * Unescape MySQL escape sequences. - * - * MySQL string literals use backslash as an escape character, and - * the string bounding quotes can also be escaped by being doubled. - * - * The escaping is done according to the following rules: - * - * 1. Some special character escape sequences are recognized. - * For example, "\n" is a newline character, "\0" is ASCII NULL. - * 2. A specific treatment is applied to "\%" and "\_" sequences. - * This is due to their special meaning for pattern matching. - * 3. Other backslash-prefixed characters resolve to their literal - * values. For example, "\x" represents "x", "\\" represents "\". - * - * Despite looking similar, these rules are different from the C-style - * string escaping, so we cannot use "strip(c)slashes()" in this case. - * - * See: https://dev.mysql.com/doc/refman/8.4/en/string-literals.html - */ - $backslash = chr( 92 ); - $replacements = array( - /* - * MySQL special character escape sequences. - */ - ( $backslash . '0' ) => chr( 0 ), // An ASCII NULL character (\0). - ( $backslash . "'" ) => chr( 39 ), // A single quote character ('). - ( $backslash . '"' ) => chr( 34 ), // A double quote character ("). - ( $backslash . 'b' ) => chr( 8 ), // A backspace character. - ( $backslash . 'n' ) => chr( 10 ), // A newline (linefeed) character (\n). - ( $backslash . 'r' ) => chr( 13 ), // A carriage return character (\r). - ( $backslash . 't' ) => chr( 9 ), // A tab character (\t). - ( $backslash . 'Z' ) => chr( 26 ), // An ASCII 26 (Control+Z) character. - - /* - * Normalize escaping of "%" and "_" characters. - * - * MySQL has unusual handling for "\%" and "\_" in all string literals. - * While other sequences follow the C-style escaping ("\?" is "?", etc.), - * "\%" resolves to "\%" and "\_" resolves to "\_" (unlike in C strings). - * - * This means that "\%" behaves like "\\%", and "\_" behaves like "\\_". - * To preserve this behavior, we need to add a second backslash here. - * - * From https://dev.mysql.com/doc/refman/8.4/en/string-literals.html: - * > The \% and \_ sequences are used to search for literal instances - * > of % and _ in pattern-matching contexts where they would otherwise - * > be interpreted as wildcard characters. If you use \% or \_ outside - * > of pattern-matching contexts, they evaluate to the strings \% and - * > \_, not to % and _. - */ - ( $backslash . '%' ) => $backslash . $backslash . '%', - ( $backslash . '_' ) => $backslash . $backslash . '_', - - /* - * Preserve a double backslash as-is, so that the trailing backslash - * is not consumed as the beginning of an escape sequence like "\n". - * - * Resolving "\\" to "\" will be handled in the next step, where all - * other backslash-prefixed characters resolve to their literal values. - */ - ( $backslash . $backslash ) - => $backslash . $backslash, - - /* - * The bounding quotes can also be escaped by being doubled. - */ - ( $quote . $quote ) => $quote, - ); - - /* - * Apply the replacements. - * - * It is important to use "strtr()" and not "str_replace()", because - * "str_replace()" applies replacements one after another, modifying - * intermediate changes rather than just the original string: - * - * - str_replace( [ 'a', 'b' ], [ 'b', 'c' ], 'ab' ); // 'cc' (bad) - * - strtr( 'ab', [ 'a' => 'b', 'b' => 'c' ] ); // 'bc' (good) - */ - $value = strtr( $value, $replacements ); - - /* - * A backslash with any other character represents the character itself. - * That is, \x evaluates to x, \\ evaluates to \, and \🙂 evaluates to 🙂. - */ - $preg_quoted_backslash = preg_quote( $backslash ); - $value = preg_replace( "/$preg_quoted_backslash(.)/u", '$1', $value ); - } - return $value; - } - - /** - * Get the token representation as a string. - * - * This method is intended to be used only for testing and debugging purposes, - * when tokens need to be presented in a human-readable form. It should not - * be used in production code, as it's not performance-optimized. - * - * @return string - */ - public function __toString(): string { - return $this->get_value() . '<' . $this->id . ',' . $this->get_name() . '>'; - } -} diff --git a/packages/mysql-on-sqlite/src/mysql/mysql-grammar.php b/packages/mysql-on-sqlite/src/mysql/mysql-grammar.php deleted file mode 100644 index e68646fbb..000000000 --- a/packages/mysql-on-sqlite/src/mysql/mysql-grammar.php +++ /dev/null @@ -1,4 +0,0 @@ -2000,'rules_names'=>['query','%f1','%f2','%f3','simpleStatement','%f4','%f5','%f6','%f7','alterStatement','%f8','%f9','%f10','alterInstance','%f11','%f12','%f13','%f14','%f15','%f16','%f17','%f18','%f19','%f20','%f21','%f22','%f23','%f24','alterDatabase','%f25','%f26','%f27','%f28','%f29','%f30','%f31','%f32','%f33','%f34','alterEvent','%f35','%f36','%f37','%f38','%f39','%f40','%f41','%f42','%f43','%f44','alterLogfileGroup','%f45','alterLogfileGroupOptions','%f46','%f47','alterLogfileGroupOption','alterServer','%f48','%f49','%f50','alterTable','%f51','%f52','%f53','%f54','alterTableActions','%f55','%f56','%f57','alterCommandList','%f58','%f59','alterCommandsModifierList','%f60','standaloneAlterCommands','%f61','%f62','%f63','%f64','%f65','%f66','%f67','alterPartition','%f68','%f69','%f70','%f71','%f72','alterList','%f73','%f74','%f75','alterCommandsModifier','%f76','%f77','%f78','%f79','alterListItem','%f80','%f81','%f82','%f83','%f84','%f85','%f86','%f87','%f88','%f89','%f90','%f91','%f92','%f93','%f94','%f95','%f96','%f97','%f98','%f99','%f100','%f101','%f102','place','restrict','%f103','%f104','alterOrderList','%f105','%f106','alterAlgorithmOption','%f107','alterLockOption','%f108','%f109','%f110','indexLockAndAlgorithm','withValidation','%f111','%f112','removePartitioning','allOrPartitionNameList','alterTablespace','%f113','%f114','%f115','%f116','%f117','%f118','%f119','%f120','%f121','%f122','%f123','%f124','alterUndoTablespace','%f125','%f126','undoTableSpaceOptions','%f127','undoTableSpaceOption','%f128','alterTablespaceOptions','%f129','alterTablespaceOption','%f130','changeTablespaceOption','%f131','%f132','alterView','%f133','viewTail','%f134','viewSelect','%f135','viewCheckOption','%f136','createStatement','%f137','%f138','%f139','%f140','%f141','%f142','createDatabase','createDatabaseOption','%f143','%f144','createTable','%f145','%f146','%f147','%f148','%f149','%f150','%f151','tableElementList','%f152','tableElement','%f153','%f154','duplicateAsQueryExpression','%f155','queryExpressionOrParens','%f156','createRoutine','%f157','%f158','%f159','createProcedure','%f160','%f161','%f162','%f163','%f164','%f165','createFunction','%f166','%f167','%f168','%f169','%f170','%f171','createUdf','%f172','routineCreateOption','%f173','routineAlterOptions','routineOption','%f174','%f175','createIndex','%f176','%f177','%f178','%f179','%f180','%f181','%f182','%f183','%f184','%f185','%f186','indexNameAndType','%f187','%f188','createIndexTarget','%f189','createLogfileGroup','%f190','%f191','logfileGroupOptions','%f192','logfileGroupOption','createServer','%f193','serverOptions','%f194','serverOption','%f195','%f196','createTablespace','%f197','createUndoTablespace','tsDataFileName','%f198','%f199','%f200','tsDataFile','%f201','tablespaceOptions','%f202','tablespaceOption','%f203','%f204','%f205','tsOptionInitialSize','tsOptionUndoRedoBufferSize','%f206','tsOptionAutoextendSize','tsOptionMaxSize','tsOptionExtentSize','tsOptionNodegroup','%f207','tsOptionEngine','tsOptionEngineAttribute','tsOptionWait','%f208','tsOptionComment','tsOptionFileblockSize','tsOptionEncryption','%f209','createView','viewReplaceOrAlgorithm','viewAlgorithm','%f210','viewSuid','%f211','%f212','createTrigger','%f213','%f214','%f215','%f216','triggerFollowsPrecedesClause','%f217','%f218','%f219','%f220','%f221','createEvent','%f222','%f223','%f224','%f225','%f226','createRole','%f227','createSpatialReference','srsAttribute','dropStatement','%f228','%f229','%f230','%f231','%f232','dropDatabase','dropEvent','dropFunction','dropProcedure','dropIndex','%f233','dropLogfileGroup','%f234','%f235','%f236','dropLogfileGroupOption','dropServer','%f237','dropTable','%f238','%f239','%f240','dropTableSpace','%f241','%f242','%f243','dropTrigger','%f244','dropView','%f245','dropRole','dropSpatialReference','dropUndoTablespace','%f246','renameTableStatement','%f247','%f248','renamePair','%f249','truncateTableStatement','importStatement','%f250','callStatement','%f251','%f252','%f253','%f254','deleteStatement','%f255','%f256','%f257','%f258','%f259','%f260','%f261','%f262','%f263','%f264','%f265','partitionDelete','%f266','deleteStatementOption','doStatement','%f267','%f268','%f269','handlerStatement','%f270','%f271','%f272','%f273','handlerReadOrScan','%f274','%f275','%f276','%f277','%f278','%f279','%f280','%f281','%f282','insertStatement','%f283','%f284','%f285','%f286','%f287','%f288','%f289','insertLockOption','%f290','insertFromConstructor','%f291','%f292','%f293','fields','%f294','insertValues','%f295','insertQueryExpression','%f296','%f297','valueList','%f298','%f299','values','%f300','%f301','%f302','valuesReference','insertUpdateList','%f303','%f304','%f305','%f306','%f307','%f308','%f309','loadStatement','%f310','%f311','dataOrXml','xmlRowsIdentifiedBy','%f312','%f313','%f314','loadDataFileTail','%f315','%f316','%f317','%f318','loadDataFileTargetList','%f319','fieldOrVariableList','%f320','%f321','%f322','%f323','replaceStatement','%f324','%f325','%f326','selectStatement','selectStatementWithInto','%f327','%f328','queryExpression','%f329','%f330','%f331','%f332','%f333','%f334','queryExpressionBody','%f335','%f336','%f337','%f338','%f339','queryTerm','%f340','%f341','%f342','%f343','queryExpressionParens','queryPrimary','%f344','%f345','%f346','%f347','%f348','%f349','%f350','%f351','querySpecification','%f352','%f353','subquery','querySpecOption','limitClause','simpleLimitClause','%f354','limitOptions','%f355','%f356','limitOption','%f357','intoClause','%f358','%f359','%f360','%f361','%f362','%f363','procedureAnalyseClause','%f364','%f365','%f366','havingClause','%f367','windowClause','%f368','windowDefinition','windowSpec','%f369','%f370','%f371','%f372','%f373','windowSpecDetails','%f374','%f375','%f376','%f377','windowFrameClause','windowFrameUnits','windowFrameExtent','windowFrameStart','windowFrameBetween','windowFrameBound','windowFrameExclusion','%f378','%f379','%f380','withClause','%f381','commonTableExpression','%f382','groupByClause','olapOption','%f383','orderClause','direction','fromClause','%f384','%f385','tableReferenceList','%f386','%f387','tableValueConstructor','%f388','explicitTable','rowValueExplicit','selectOption','%f389','%f390','%f391','lockingClauseList','%f392','%f393','lockingClause','%f394','%f395','%f396','%f397','lockStrengh','%f398','lockedRowAction','%f399','selectItemList','%f400','%f401','%f402','selectItem','selectAlias','%f403','whereClause','%f404','tableReference','%f405','%f406','%f407','escapedTableReference','%f408','joinedTable','%f409','%f410','%f411','%f412','naturalJoinType','%f413','%f414','innerJoinType','%f415','outerJoinType','%f416','tableFactor','%f417','%f418','singleTable','singleTableParens','%f419','%f420','derivedTable','%f421','%f422','%f423','tableReferenceListParens','%f424','tableFunction','%f425','columnsClause','%f426','%f427','%f428','%f429','jtColumn','%f430','%f431','%f432','%f433','onEmptyOrError','onEmpty','onError','jtOnResponse','setOperationOption','%f434','tableAlias','%f435','%f436','%f437','indexHintList','%f438','%f439','indexHint','indexHintType','keyOrIndex','%f440','constraintKeyType','indexHintClause','%f441','%f442','indexList','%f443','indexListElement','%f444','%f445','updateStatement','%f446','%f447','transactionOrLockingStatement','%f448','%f449','%f450','%f451','transactionStatement','%f452','%f453','%f454','beginWork','%f455','transactionCharacteristicList','%f456','transactionCharacteristic','%f457','%f458','savepointStatement','%f459','%f460','%f461','%f462','%f463','%f464','%f465','lockStatement','%f466','%f467','%f468','%f469','%f470','lockItem','lockOption','xaStatement','%f471','%f472','%f473','%f474','%f475','%f476','%f477','%f478','%f479','%f480','xaConvert','%f481','%f482','%f483','%f484','xid','%f485','%f486','%f487','%f488','replicationStatement','%f489','%f490','%f491','%f492','%f493','%f494','%f495','%f496','%f497','%f498','%f499','resetOption','%f500','masterResetOptions','%f501','%f502','%f503','%f504','replicationLoad','%f505','changeMaster','%f506','changeMasterOptions','%f507','masterOption','privilegeCheckDef','tablePrimaryKeyCheckDef','masterTlsCiphersuitesDef','masterFileDef','%f508','serverIdList','%f509','%f510','%f511','%f512','%f513','changeReplication','%f514','%f515','%f516','%f517','changeReplicationSourceOptions','%f518','replicationSourceOption','%f519','%f520','%f521','%f522','%f523','%f524','%f525','%f526','%f527','%f528','%f529','%f530','%f531','%f532','%f533','%f534','%f535','%f536','%f537','%f538','%f539','%f540','%f541','%f542','%f543','%f544','%f545','%f546','%f547','%f548','%f549','filterDefinition','%f550','filterDbList','%f551','%f552','filterTableList','%f553','%f554','filterStringList','%f555','filterWildDbTableString','%f556','filterDbPairList','%f557','%f558','%f559','slave','%f560','%f561','slaveUntilOptions','%f562','%f563','%f564','%f565','%f566','slaveConnectionOptions','%f567','%f568','%f569','%f570','%f571','%f572','%f573','%f574','%f575','%f576','slaveThreadOptions','%f577','slaveThreadOption','groupReplication','%f578','preparedStatement','%f579','%f580','%f581','executeStatement','%f582','%f583','executeVarList','%f584','cloneStatement','%f585','%f586','%f587','%f588','%f589','%f590','%f591','dataDirSSL','ssl','accountManagementStatement','%f592','%f593','%f594','alterUser','%f595','%f596','alterUserTail','%f597','%f598','%f599','%f600','%f601','%f602','userFunction','createUser','%f603','%f604','createUserTail','%f605','%f606','%f607','%f608','%f609','%f610','%f611','%f612','defaultRoleClause','%f613','%f614','%f615','requireClause','%f616','%f617','%f618','connectOptions','%f619','accountLockPasswordExpireOptions','%f620','%f621','%f622','%f623','%f624','%f625','%f626','%f627','%f628','%f629','%f630','dropUser','%f631','%f632','grant','%f633','%f634','%f635','%f636','%f637','%f638','%f639','%f640','%f641','%f642','%f643','%f644','grantTargetList','%f645','%f646','grantOptions','%f647','%f648','%f649','exceptRoleList','withRoles','%f650','%f651','%f652','grantAs','versionedRequireClause','%f653','%f654','renameUser','%f655','%f656','%f657','revoke','%f658','%f659','%f660','%f661','%f662','%f663','%f664','%f665','%f666','%f667','%f668','onTypeTo','%f669','%f670','%f671','%f672','aclType','%f673','roleOrPrivilegesList','%f674','%f675','%f676','roleOrPrivilege','%f677','%f678','%f679','%f680','%f681','%f682','%f683','%f684','%f685','%f686','%f687','grantIdentifier','%f688','%f689','%f690','%f691','requireList','%f692','%f693','requireListElement','grantOption','%f694','setRole','%f695','%f696','%f697','%f698','roleList','%f699','%f700','role','%f701','%f702','%f703','tableAdministrationStatement','%f704','%f705','%f706','%f707','%f708','%f709','%f710','%f711','%f712','%f713','histogram','%f714','%f715','checkOption','%f716','repairType','%f717','%f718','installUninstallStatment','%f719','%f720','installOptionType','%f721','installSetValue','%f722','%f723','installSetValueList','%f724','setStatement','%f725','startOptionValueList','%f726','%f727','%f728','%f729','%f730','%f731','%f732','%f733','%f734','%f735','%f736','transactionCharacteristics','%f737','%f738','transactionAccessMode','%f739','isolationLevel','%f740','%f741','%f742','optionValueListContinued','%f743','optionValueNoOptionType','%f744','%f745','optionValue','%f746','setSystemVariable','startOptionValueListFollowingOptionType','optionValueFollowingOptionType','setExprOrDefault','%f747','%f748','%f749','showStatement','%f750','%f751','%f752','%f753','%f754','%f755','%f756','%f757','%f758','%f759','%f760','%f761','%f762','%f763','%f764','%f765','%f766','%f767','%f768','%f769','%f770','%f771','%f772','%f773','%f774','%f775','%f776','%f777','%f778','%f779','%f780','%f781','%f782','%f783','showCommandType','%f784','nonBlocking','%f785','%f786','fromOrIn','inDb','profileType','%f787','%f788','otherAdministrativeStatement','%f789','%f790','%f791','%f792','%f793','%f794','keyCacheListOrParts','%f795','keyCacheList','%f796','%f797','assignToKeycache','assignToKeycachePartition','%f798','cacheKeyList','keyUsageElement','%f799','keyUsageList','%f800','%f801','flushOption','%f802','%f803','%f804','logType','%f805','flushTables','%f806','%f807','%f808','flushTablesOptions','%f809','%f810','preloadTail','%f811','%f812','preloadList','%f813','%f814','preloadKeys','%f815','adminPartition','resourceGroupManagement','%f816','%f817','%f818','createResourceGroup','%f819','%f820','resourceGroupVcpuList','%f821','%f822','vcpuNumOrRange','%f823','resourceGroupPriority','resourceGroupEnableDisable','%f824','alterResourceGroup','%f825','setResourceGroup','%f826','%f827','threadIdList','%f828','dropResourceGroup','utilityStatement','%f829','%f830','describeStatement','%f831','%f832','%f833','explainStatement','%f834','%f835','%f836','%f837','%f838','%f839','%f840','explainableStatement','%f841','%f842','%f843','helpCommand','useCommand','restartServer','%f844','expr','%f845','%f846','%f847','%f848','%f849','%f850','%f851','%f852','%f853','%f854','boolPri','%f855','%f856','compOp','%f857','predicate','%f858','%f859','%f860','%f861','predicateOperations','%f862','%f863','%f864','bitExpr','%f865','%f866','%f867','%f868','%f869','%f870','simpleExpr','%f871','%f872','%f873','%f874','%f875','%f876','%f877','%f878','%f879','%f880','%f881','%f882','simpleExprBody','%f883','%f884','%f885','%f886','%f887','%f888','%f889','%f890','%f891','arrayCast','%f892','jsonOperator','%f893','%f894','%f895','%f896','%f897','%f898','%f899','%f900','%f901','%f902','%f903','%f904','%f905','%f906','%f907','%f908','sumExpr','%f909','%f910','%f911','%f912','%f913','%f914','%f915','%f916','%f917','%f918','%f919','%f920','%f921','%f922','%f923','%f924','%f925','%f926','%f927','%f928','%f929','%f930','%f931','%f932','%f933','%f934','%f935','%f936','groupingOperation','%f937','%f938','%f939','windowFunctionCall','%f940','%f941','%f942','%f943','%f944','windowingClause','%f945','%f946','leadLagInfo','%f947','%f948','nullTreatment','%f949','%f950','jsonFunction','inSumExpr','identListArg','%f951','identList','%f952','%f953','fulltextOptions','%f954','%f955','%f956','%f957','%f958','%f959','%f960','%f961','runtimeFunctionCall','%f962','%f963','%f964','%f965','%f966','%f967','%f968','%f969','%f970','%f971','%f972','%f973','%f974','%f975','%f976','%f977','%f978','%f979','%f980','%f981','geometryFunction','%f982','%f983','timeFunctionParameters','fractionalPrecision','%f984','weightStringLevels','%f985','%f986','%f987','%f988','weightStringLevelListItem','%f989','%f990','%f991','dateTimeTtype','trimFunction','%f992','%f993','%f994','substringFunction','%f995','%f996','%f997','%f998','%f999','%f1000','functionCall','%f1001','udfExprList','%f1002','udfExpr','variable','userVariable','%f1003','%f1004','systemVariable','internalVariableName','%f1005','%f1006','%f1007','whenExpression','thenExpression','elseExpression','%f1008','%f1009','%f1010','%f1011','castType','%f1012','%f1013','%f1014','%f1015','%f1016','exprList','%f1017','charset','notRule','not2Rule','interval','%f1018','intervalTimeStamp','exprListWithParentheses','exprWithParentheses','simpleExprWithParentheses','%f1019','orderList','%f1020','orderExpression','%f1021','groupList','%f1022','groupingExpression','channel','%f1023','compoundStatement','returnStatement','ifStatement','%f1024','ifBody','%f1025','thenStatement','%f1026','compoundStatementList','%f1027','%f1028','%f1029','caseStatement','%f1030','elseStatement','%f1031','labeledBlock','unlabeledBlock','label','%f1032','%f1033','beginEndBlock','labeledControl','unlabeledControl','loopBlock','whileDoBlock','repeatUntilBlock','%f1034','spDeclarations','%f1035','spDeclaration','%f1036','variableDeclaration','%f1037','conditionDeclaration','spCondition','%f1038','sqlstate','%f1039','handlerDeclaration','%f1040','%f1041','handlerCondition','cursorDeclaration','iterateStatement','leaveStatement','%f1042','getDiagnostics','%f1043','%f1044','%f1045','%f1046','%f1047','%f1048','%f1049','signalAllowedExpr','statementInformationItem','%f1050','%f1051','conditionInformationItem','%f1052','%f1053','signalInformationItemName','%f1054','signalStatement','%f1055','%f1056','%f1057','%f1058','%f1059','%f1060','resignalStatement','%f1061','%f1062','%f1063','%f1064','signalInformationItem','cursorOpen','cursorClose','%f1065','cursorFetch','%f1066','%f1067','%f1068','%f1069','schedule','%f1070','%f1071','columnDefinition','checkOrReferences','%f1072','checkConstraint','constraintEnforcement','%f1073','tableConstraintDef','%f1074','%f1075','%f1076','%f1077','%f1078','%f1079','%f1080','constraintName','fieldDefinition','%f1081','%f1082','%f1083','%f1084','%f1085','%f1086','%f1087','%f1088','%f1089','%f1090','%f1091','%f1092','%f1093','columnAttribute','%f1094','%f1095','%f1096','%f1097','%f1098','%f1099','%f1100','columnFormat','storageMedia','gcolAttribute','%f1101','%f1102','%f1103','references','%f1104','%f1105','%f1106','%f1107','%f1108','%f1109','%f1110','deleteOption','%f1111','%f1112','keyList','%f1113','keyPart','%f1114','keyListWithExpression','%f1115','keyPartOrExpression','keyListVariants','%f1116','%f1117','indexType','%f1118','indexOption','commonIndexOption','%f1119','visibility','indexTypeClause','%f1120','fulltextIndexOption','spatialIndexOption','dataTypeDefinition','%f1121','%f1122','%f1123','%f1124','dataType','%f1125','%f1126','%f1127','%f1128','%f1129','%f1130','%f1131','%f1132','%f1133','%f1134','%f1135','nchar','realType','fieldLength','%f1136','%f1137','fieldOptions','%f1138','%f1139','charsetWithOptBinary','%f1140','ascii','unicode','wsNumCodepoints','typeDatetimePrecision','charsetName','%f1141','collationName','%f1142','%f1143','%f1144','createTableOptions','%f1145','%f1146','createTableOptionsSpaceSeparated','%f1147','createTableOption','%f1148','%f1149','%f1150','%f1151','%f1152','%f1153','%f1154','%f1155','%f1156','%f1157','%f1158','%f1159','%f1160','%f1161','%f1162','ternaryOption','%f1163','defaultCollation','defaultEncryption','defaultCharset','%f1164','%f1165','%f1166','partitionClause','%f1167','%f1168','%f1169','%f1170','partitionTypeDef','%f1171','%f1172','%f1173','subPartitions','%f1174','%f1175','partitionKeyAlgorithm','%f1176','%f1177','partitionDefinitions','%f1178','%f1179','%f1180','%f1181','partitionDefinition','%f1182','%f1183','%f1184','%f1185','%f1186','%f1187','partitionValuesIn','%f1188','partitionOption','%f1189','%f1190','subpartitionDefinition','%f1191','partitionValueItemListParen','%f1192','partitionValueItem','definerClause','ifExists','ifNotExists','%f1193','procedureParameter','%f1194','functionParameter','collate','typeWithOptCollate','schemaIdentifierPair','%f1195','viewRefList','%f1196','%f1197','updateList','%f1198','updateElement','%f1199','charsetClause','%f1200','fieldsClause','%f1201','fieldTerm','%f1202','linesClause','lineTerm','%f1203','%f1204','userList','%f1205','%f1206','createUserList','%f1207','%f1208','alterUserList','%f1209','%f1210','createUserEntry','%f1211','%f1212','%f1213','%f1214','%f1215','%f1216','%f1217','%f1218','%f1219','%f1220','%f1221','alterUserEntry','%f1222','%f1223','%f1224','%f1225','%f1226','%f1227','%f1228','%f1229','%f1230','%f1231','%f1232','retainCurrentPassword','discardOldPassword','replacePassword','%f1233','userIdentifierOrText','%f1234','%f1235','user','likeClause','likeOrWhere','onlineOption','noWriteToBinLog','usePartition','%f1236','fieldIdentifier','%f1237','%f1238','%f1239','columnInternalRef','%f1240','columnInternalRefList','%f1241','columnRef','insertIdentifier','indexName','indexRef','%f1242','tableWild','%f1243','schemaName','schemaRef','procedureName','procedureRef','functionName','functionRef','triggerName','triggerRef','viewName','%f1244','viewRef','%f1245','tablespaceName','tablespaceRef','logfileGroupName','logfileGroupRef','eventName','eventRef','udfName','serverName','serverRef','engineRef','tableName','%f1246','filterTableRef','%f1247','tableRefWithWildcard','%f1248','%f1249','%f1250','tableRef','%f1251','%f1252','tableRefList','%f1253','%f1254','tableAliasRefList','%f1255','parameterName','labelIdentifier','labelRef','roleIdentifier','roleRef','pluginRef','componentRef','resourceGroupRef','windowName','pureIdentifier','%f1256','%f1257','identifier','%f1258','identifierList','%f1259','identifierListWithParentheses','qualifiedIdentifier','%f1260','simpleIdentifier','%f1261','%f1262','dotIdentifier','ulong_number','real_ulong_number','ulonglong_number','real_ulonglong_number','%f1263','%f1264','literal','%f1265','signedLiteral','%f1266','stringList','%f1267','textStringLiteral','%f1268','textString','textStringHash','%f1269','%f1270','textLiteral','%f1271','textStringNoLinebreak','%f1272','textStringLiteralList','%f1273','numLiteral','boolLiteral','nullLiteral','temporalLiteral','floatOptions','standardFloatOptions','precision','textOrIdentifier','lValueIdentifier','roleIdentifierOrText','sizeNumber','parentheses','equal','optionType','varIdentType','setVarIdentType','identifierKeyword','%f1274','%f1275','%f1276','%f1277','%f1278','identifierKeywordsAmbiguous1RolesAndLabels','identifierKeywordsAmbiguous2Labels','labelKeyword','%f1279','%f1280','%f1281','identifierKeywordsAmbiguous3Roles','identifierKeywordsUnambiguous','%f1282','%f1283','%f1284','roleKeyword','%f1285','%f1286','%f1287','lValueKeyword','identifierKeywordsAmbiguous4SystemVariables','roleOrIdentifierKeyword','%f1288','%f1289','%f1290','roleOrLabelKeyword','%f1291','%f1292','%f1293','%f1294','%f1295','%f1296','%f1297'],'grammar'=>[[[-1],[2001,2003]],[[2004],[2668]],[[-1],[0]],[[755,2002],[-1]],[[2009],[2175],[2318],[2353],[2358],[2005],[2361],[2366],[2381],[2385],[2400],[2437],[2457],[2461],[2656],[2659],[2712],[2829],[2006],[2848],[2991],[3010],[3020],[3057],[2007],[3102],[3168],[2008],[3489],[3496]],[[2359]],[[2838]],[[3145]],[[3472]],[[11,2012]],[[2153]],[[2225],[0]],[[2060],[2028],[422,3783,2011],[206,3785,2011],[2167],[2039],[2140],[2010],[2050],[2056],[2013]],[[2026]],[[33]],[[844],[2014]],[[2017],[0]],[[373,480,383,165]],[[451,845,2016]],[[2020],[0]],[[373,480,383,165]],[[451,845,200,57,3830,2019]],[[156],[140]],[[2022,844,846]],[[451,847]],[[482,2015,316,265],[2018],[2021],[2023],[2024]],[[244,2025]],[[3781],[0]],[[109,2027,2031]],[[615,112,139,357]],[[2183,2030],[2183]],[[2030],[2029]],[[3690],[0]],[[2040],[0]],[[2042],[0]],[[2043],[0]],[[2046],[0]],[[2047],[0]],[[2048],[0]],[[2032,170,3797,2033,2034,2035,2036,2037,2038]],[[383,490,3510]],[[371],[0]],[[383,79,2041,418]],[[453,590,3830]],[[2045],[0]],[[383,514]],[[156],[140,2044]],[[75,3859]],[[147,3425]],[[2052],[0]],[[288,217,3795,4,603,3859,2049]],[[2054,2051],[2054],[0]],[[2055,2051]],[[750],[0]],[[2053,2055]],[[2274],[2282],[2284]],[[503,3800,2254]],[[3761],[0]],[[2062],[0]],[[2065],[0]],[[2057,2058,574,3810,2059]],[[232]],[[2061]],[[2066],[0]],[[2067],[0]],[[2063,2074],[2069,2064],[3653],[2138]],[[2072,750]],[[3653],[2138]],[[2070],[0]],[[2072,2068],[2088]],[[750,2088]],[[2073,2071],[2073],[0]],[[2092,2071]],[[750,2092]],[[141,572],[234,572],[2082],[2076]],[[722],[723]],[[2075]],[[3762],[0]],[[3005,2078],[3005],[0]],[[3007,2079],[3007],[0]],[[2084],[0]],[[2135],[0]],[[4,405,2077,2083],[148,405,3832],[438,405,2077,2139],[388,405,2077,2139,2077],[14,405,2077,2139],[62,405,2139,2078],[455,405,2077,2139,2079],[67,405,2077,3842],[597,405,2139],[454,405,2077,2080],[172,405,3830,645,574,3810,2081],[2085],[2086]],[[3668],[404,3842]],[[3832,248,3668]],[[141,405,2139,572]],[[234,405,2139,572]],[[2091,2087],[2091],[0]],[[2089,2087]],[[2097],[3627]],[[2097],[2092],[3627]],[[750,2090]],[[2128],[2130],[2135]],[[72],[0]],[[2121],[0]],[[2116],[0]],[[3697],[0]],[[4,2093,2099],[4,3519],[55,2093,3765,3830,3528,2094],[348,2093,3765,3528,2094],[148,2107],[140,263],[156,263],[11,2093,3765,2111],[2112],[2113],[2114],[2115],[453,2095,3802],[2117],[94,590,3406,2119,2096],[198],[393,45,2125],[2120]],[[3514],[0]],[[3830,3528,2098,2094],[753,2194,748]],[[3765]],[[3765],[0]],[[2101]],[[2100],[2102]],[[62,3830]],[[86,3830]],[[2122],[0]],[[2093,3765,2106],[199,265,2103],[420,265],[2645,3776],[2104],[2105]],[[3413]],[[2108],[3849]],[[506,3582]],[[506,128,2109],[148,128],[2110]],[[11,236,3776,3582]],[[11,62,3830,3517]],[[11,86,3830,3517]],[[453,72,3765,590,3830]],[[590],[17]],[[453,2645,3776,590,3775]],[[128]],[[2118],[3618]],[[615,403]],[[6,3830],[191]],[[471],[49]],[[2551],[0]],[[2126,2124],[2126],[0]],[[3837,2123,2124]],[[750,3837,2123]],[[763],[0]],[[9,2127,2129]],[[128],[3830]],[[287,2127,2131]],[[128],[3830]],[[2130],[0]],[[2128],[0]],[[2128,2132],[2130,2133]],[[2137]],[[645],[646]],[[2136,625]],[[452,403]],[[10],[3832]],[[572,3793,2151]],[[4],[148]],[[2143,2142],[2143],[0]],[[2053,2164]],[[2145],[0]],[[2164,2142]],[[434],[436]],[[55,111,3859,2144],[2146],[371,1]],[[2147]],[[2160]],[[2160],[0]],[[2141,111,3859,2150],[2148],[453,590,3830],[2149]],[[2156],[0]],[[605,572,3793,506,2154,2152]],[[724],[725]],[[2157,2155],[2157],[0]],[[2158,2155]],[[2053,2158]],[[2282]],[[2161,2159],[2161],[0]],[[2162,2159]],[[2053,2162]],[[238,2127,3875],[2277],[2278],[2282],[2163],[2284],[2288]],[[2283]],[[238,2127,3875],[2277],[2278]],[[2292],[0]],[[2294],[0]],[[2165,2032,2166,636,3790,2169]],[[3771],[0]],[[2168,17,2171]],[[2173],[0]],[[2201,2170]],[[2174],[0]],[[645,2172,62,391]],[[50],[284]],[[97,2179]],[[2314]],[[2316]],[[2261]],[[2182],[2186],[2214],[2207],[2221],[2246],[2290],[2297],[2229],[2252],[2259],[2308],[2176],[2177],[2178]],[[3692],[0]],[[2183,2181],[2183],[0]],[[109,2180,3780,2181]],[[3649],[3647],[2184]],[[3648]],[[577],[0]],[[2185,574,2180,3802,2192]],[[2188],[0]],[[753,2194,748]],[[3624],[0]],[[3653],[0]],[[2199],[0]],[[275,3810],[753,275,3810,748],[2187,2189,2190,2191]],[[2195,2193],[2195],[0]],[[2196,2193]],[[750,2196]],[[3513],[3519]],[[2200],[0]],[[17],[0]],[[2197,2198,2201]],[[458],[232]],[[2465],[2483]],[[755],[0]],[[97,2204,2202,-1]],[[2207],[2214],[2221]],[[2212],[0]],[[2223,2206],[2223],[0]],[[2032,422,2209,3782,753,2205,748,2206,3425]],[[2180]],[[2208]],[[2211,2210],[2211],[0]],[[750,3694]],[[3694,2210]],[[2219],[0]],[[2032,206,2216,3784,753,2213,748,474,3698,2206,3425]],[[2180]],[[2215]],[[2218,2217],[2218],[0]],[[750,3696]],[[3696,2217]],[[8],[0]],[[2220,206,3798,474,2222,520,3859]],[[556],[249],[437],[126]],[[2226],[2041,137]],[[2223,2224],[2223]],[[2224]],[[75,3859],[267,537],[373,537],[90,537],[433,537,112],[347,537,112],[537,496,2227]],[[130],[250]],[[2134],[0]],[[2057,2238,2228]],[[3583],[0]],[[3775,2230]],[[2241],[0]],[[2231],[2232]],[[609],[0]],[[3579,2235],[3579],[0]],[[3585,2236],[3585],[0]],[[3586,2237],[3586],[0]],[[2234,236,2233,2244,2235],[205,236,3775,2244,2236],[523,236,3775,2244,2237]],[[3775],[0]],[[2243],[0]],[[2239,2240]],[[621],[599]],[[2242,3577]],[[383,3810,3574]],[[2249],[0]],[[288,217,3794,4,2247,3859,2245]],[[603],[440]],[[2250,2248],[2250],[0]],[[2251,2248]],[[2053,2251]],[[2274],[2275],[2280],[2282],[2284],[2286]],[[503,3799,199,112,648,3872,2254]],[[2255,2253],[2255],[0]],[[390,753,2256,2253,748]],[[750,2256]],[[224,3859],[109,3859],[618,3859],[406,3859],[519,3859],[398,3859],[413,3841]],[[2260],[0]],[[2268],[0]],[[572,3792,2262,2257,2258]],[[620,288,217,3795]],[[605,572,3792,4,2266,2152]],[[2265],[4,2266]],[[2264],[0]],[[4,2266]],[[2263]],[[111,3859]],[[2269,2267],[2269],[0]],[[2270,2267]],[[2053,2270]],[[2274],[2277],[2278],[2279],[2280],[2282],[2271],[2284],[2286],[2272],[2273]],[[2283]],[[2287]],[[2288]],[[238,2127,3875]],[[2276,2127,3875]],[[604],[441]],[[23,2127,3875]],[[324,2127,3875]],[[181,2127,3875]],[[368,2127,3842]],[[553],[0]],[[2281,163,2127,3801]],[[848,2127,3853]],[[2285]],[[638],[374]],[[75,2127,3859]],[[189,2127,3875]],[[158,2127,3853]],[[2291],[0]],[[2289,2032,2166,636,3788,2169]],[[394,458,2165],[2292]],[[9,763,2293]],[[602],[335],[578]],[[537,496,2295]],[[130],[250]],[[2302],[0]],[[2032,594,2299,3786,2300,2301,383,3810,200,153,487,2296,3425]],[[2180]],[[2298]],[[28],[6]],[[242],[614],[133]],[[2304]],[[197],[415]],[[2303,3872]],[[2309],[0]],[[2312],[0]],[[2313],[0]],[[2032,170,2180,3796,383,490,3510,2305,2306,2307,147,3425]],[[383,79,2041,418]],[[2311],[0]],[[383,514]],[[156],[140,2310]],[[75,3859]],[[659,2180,2984]],[[2317,2315],[2317],[0]],[[394,458,523,718,710,3844,2315],[523,718,710,2180,3844,2315]],[[357,580,3861],[715,580,3861],[717,3861,230,45,3844],[716,580,3861]],[[148,2322]],[[2349]],[[2350]],[[2351]],[[2324],[2325],[2326],[2327],[2328],[2330],[2335],[2337],[2341],[2345],[2347],[2319],[2320],[2321]],[[3691],[0]],[[109,2323,3781]],[[170,2323,3797]],[[206,2323,3785]],[[422,2323,3783]],[[2057,236,3776,383,3810,2228]],[[2333],[0]],[[288,217,3795,2329]],[[2332,2331],[2332],[0]],[[2053,2334]],[[2334,2331]],[[2284],[2282]],[[503,2323,3800]],[[2339],[0]],[[2185,2338,2323,3813,2336]],[[574],[571]],[[471],[49]],[[2344],[0]],[[572,3793,2340]],[[2343,2342],[2343],[0]],[[2053,2334]],[[2334,2342]],[[594,2323,3787]],[[2348],[0]],[[636,2323,3701,2346]],[[471],[49]],[[659,2323,2984]],[[523,718,710,2323,3844]],[[605,572,3793,2152]],[[2355,2352],[2355],[0]],[[453,2354,2356,2352]],[[574],[571]],[[750,2356]],[[3810,590,3802]],[[574],[0]],[[597,2357,3810]],[[234,574,203,3863]],[[2363],[0]],[[48,3783,2360]],[[3404],[0]],[[753,2362,748]],[[2368],[0]],[[2380,2365],[2380],[0]],[[2364,133,2365,2377]],[[2543]],[[2367]],[[2636]],[[2371],[0]],[[2369]],[[2585],[0]],[[2378],[0]],[[2550],[0]],[[2499],[0]],[[3816,621,2555,2372],[3810,2370,2373,2372,2374,2375]],[[203,2376],[3816,203,2555,2372]],[[2379]],[[405,753,3832,748]],[[431],[295],[431],[232]],[[147,2384]],[[2578]],[[3404]],[[2382],[2383]],[[219,2389]],[[2498],[0]],[[66],[435,2390,2372,2386]],[[2636],[0]],[[3810,387,2388],[3830,2387]],[[2391],[3830,2394]],[[191],[367]],[[191],[367],[419],[268]],[[763],[769],[765],[768],[764]],[[2392],[2393,753,2424,748]],[[2408],[0]],[[232],[0]],[[248],[0]],[[3763],[0]],[[2429],[0]],[[242,2395,2396,2397,3810,2398,2407,2399]],[[2428]],[[2403],[0]],[[2401]],[[2428]],[[2406],[0]],[[2404]],[[2410,2402],[506,3704,2405],[2418]],[[295],[131],[223]],[[2412],[0]],[[2409,2416]],[[2414],[0]],[[753,2411,748]],[[2415,2413],[2415],[0]],[[3774,2413]],[[750,3774]],[[2417,2421]],[[626],[627]],[[2201],[753,2411,748,2201]],[[2424],[0]],[[2422,2420],[2422],[0]],[[753,2419,748,2420]],[[750,753,2419,748]],[[2427,2423],[2427],[0]],[[2425,2423]],[[3191],[128]],[[3191],[128]],[[750,2426]],[[17,3830,2168]],[[383,151,265,614,3704]],[[2438],[0]],[[284],[0]],[[2439],[0]],[[3708],[0]],[[2441],[0]],[[3710],[0]],[[3714],[0]],[[281,2440,2430,2431,237,3859,2432,248,574,3810,2398,2433,2434,2435,2436,2445]],[[295],[82]],[[458],[232]],[[112],[653]],[[484,230,45,3855]],[[2447],[0]],[[2450],[0]],[[2448],[0]],[[2442,2443,2444]],[[278],[484]],[[232,787,2446]],[[506,3704]],[[2452],[0]],[[753,2449,748]],[[2455,2451],[2455],[0]],[[2453,2451]],[[3773],[3383]],[[3773],[3383]],[[750,2454]],[[2458],[0]],[[458,2456,2397,3810,2398,2459]],[[295],[131]],[[2410],[506,3704],[2418]],[[2566],[0]],[[2465,2460],[2462]],[[753,2462,748],[2465,2506,2460],[2465,2566,2506]],[[2467],[0]],[[2470],[0]],[[2463,2468,2464]],[[2543]],[[2466]],[[2472,2374,2386],[2483,2374,2386]],[[2513]],[[2469]],[[2476,2471],[2476],[0]],[[2478,2471]],[[663]],[[608],[2473]],[[2634],[0]],[[2474,2475,2478]],[[2482,2477],[2482],[0]],[[2479,2477]],[[2484],[2483]],[[2484],[2483]],[[811,2475,2480]],[[2481]],[[753,2465,2460,748]],[[2493],[2485],[2486]],[[2558]],[[2560]],[[2562,2487],[2562],[0]],[[2506],[0]],[[2552],[0]],[[2547],[0]],[[2517],[0]],[[2495],[0]],[[497,2487,2578,2488,2489,2372,2490,2491,2492]],[[2519]],[[2494]],[[2483]],[[10],[143],[555],[223],[536],[531],[532],[534]],[[276,2501]],[[276,2504]],[[2503],[0]],[[2504,2500]],[[750],[381]],[[2502,2504]],[[3830],[2505]],[[754],[791],[788],[787]],[[248,2511]],[[3872],[3383]],[[3872],[3383]],[[2510,2509],[2510],[0]],[[750,2508]],[[396,3853,2433,2435,2436],[150,3853],[2507,2509]],[[2516],[0]],[[422,13,753,2512,748]],[[2515],[0]],[[750,787]],[[787,2514]],[[221,3191]],[[2520,2518],[2520],[0]],[[699,2521,2518]],[[750,2521]],[[3826,17,2522]],[[753,2528,748]],[[2533],[0]],[[2529],[0]],[[2530],[0]],[[3826],[0]],[[2531],[0]],[[405,45,3416,2374,2523],[2524,2550,2523],[2525,2374,2533],[2526,2527,2374,2523]],[[405,45,3416]],[[405,45,3416]],[[405,45,3416]],[[2539],[0]],[[2534,2535,2532]],[[484],[432],[683]],[[2536],[2537]],[[698,693],[3843,693],[754,693],[247,3191,3409,693],[101,487]],[[30,2538,15,2538]],[[2536],[698,682],[3843,682],[754,682],[247,3191,3409,682]],[[680,2540]],[[101,487],[217],[697],[373,690]],[[665],[0]],[[2544,2542],[2544],[0]],[[645,2541,2545,2542]],[[750,2545]],[[3830,2168,17,2496]],[[2548],[0]],[[217,45,3416,2546]],[[645,481],[2549]],[[645,99]],[[393,45,3416]],[[18],[134]],[[203,2553]],[[149],[2555]],[[2556,2554],[2556],[0]],[[2587,2554]],[[750,2587]],[[2559,2557],[2559],[0]],[[626,2561,2557]],[[750,2561]],[[574,3810]],[[487,753,2419,748]],[[2497],[535],[2563],[2564]],[[533]],[[325,763,3842]],[[2569,2565],[2569]],[[2565]],[[2571],[0]],[[2573],[0]],[[200,2574,2567,2568],[287,251,508,346]],[[668,3816]],[[2570]],[[2576]],[[2572]],[[614],[2575]],[[508]],[[669,670],[671]],[[2580,2577],[2580],[0]],[[2579,2577]],[[2582],[775]],[[750,2582]],[[2583],[0]],[[3778],[3191,2581]],[[2198,2584]],[[3830],[3853]],[[643,3191]],[[2593,2586],[2593],[0]],[[2590,2586]],[[3830]],[[2588],[732]],[[2605],[752,2589,2591,747]],[[2605,2586]],[[2594],[0]],[[2601,2587,2592],[2603,2587,2595],[2598,2605]],[[383,3191],[621,3834]],[[383,3191],[621,3834]],[[239],[0]],[[395],[0]],[[359,2596,261],[359,2599,2597,261]],[[272],[478]],[[2602],[0]],[[2600,261],[555]],[[239],[98]],[[2604,2597,261]],[[272],[478]],[[2608],[2609],[2612],[2616],[2606]],[[2618]],[[2640],[0]],[[3810,2398,2388,2607]],[[753,2610,748]],[[2608],[2609]],[[2614],[0]],[[2496,2388,2611],[2615]],[[3771]],[[2613]],[[726,2496,2388,2168]],[[753,2617,748]],[[2555],[2616]],[[701,753,3191,750,3853,2620,748,2388]],[[2621,2619],[2621],[0]],[[71,753,2625,2619,748]],[[750,2625]],[[2627],[0]],[[174],[0]],[[2630],[0]],[[3830,200,703],[3830,3592,2622,2623,704,3853,2624],[702,704,3853,2620]],[[3697]],[[2626]],[[2632],[0]],[[2631],[0]],[[2631,2628],[2632,2629]],[[2633,383,700]],[[2633,383,165]],[[165],[376],[128,3853]],[[143],[10]],[[2638],[0]],[[2635,3830]],[[763]],[[17],[2637]],[[2643,2639],[2643]],[[2639]],[[2648],[0]],[[2651],[0]],[[2644,2645,2641,753,2651,748],[620,2645,2641,753,2642,748]],[[198],[232]],[[265],[236]],[[2645],[0]],[[420,265],[609,2646]],[[200,2649]],[[261],[393,45],[217,45]],[[2652,2650],[2652],[0]],[[2653,2650]],[[750,2653]],[[3830],[420]],[[2658],[0]],[[295],[0]],[[2654,614,2655,2396,2555,506,3704,2372,2374,2375]],[[2543]],[[2657]],[[2664],[2675],[2683],[2691]],[[2670],[0]],[[647],[0]],[[2666],[0]],[[2667],[0]],[[543,592,2660],[77,2661,2662,2663]],[[373],[0]],[[15,2665,54]],[[2665,450]],[[29,2661]],[[2671,2669],[2671],[0]],[[2672,2669]],[[750,2672]],[[645,85,517],[2674]],[[649],[386]],[[435,2673]],[[489,3830],[480,2661,2681],[450,489,3830]],[[2677],[0]],[[15,2665,54]],[[2679],[0]],[[2665,450]],[[489],[0]],[[590,2680,3830],[2676,2678]],[[2685,2682],[2685],[0]],[[287,2684,2689,2682],[2686],[611,2688]],[[571],[574]],[[750,2689]],[[287,244,200,27]],[[244]],[[571],[574],[2687]],[[3810,2388,2690]],[[435,2431],[2655,649]],[[651,2701]],[[543],[29]],[[2694],[0]],[[261],[472]],[[2696],[0]],[[200,340]],[[2698],[0]],[[566,2695]],[[2700],[0]],[[384,407]],[[2692,2707,2693],[159,2707,2697],[417,2707],[77,2707,2699],[480,2707],[439,2702]],[[2705],[0]],[[2704],[0]],[[94,652]],[[2703]],[[2710],[0]],[[3855,2706]],[[2709],[0]],[[750,3841]],[[750,3855,2708]],[[2715,2711],[2715],[0]],[[428,2713,289,2714],[2733],[468,2724,2711],[2718],[2804],[2719],[2731],[2720]],[[32],[316]],[[590,3859],[28,3191]],[[750,2724]],[[2717],[0]],[[2323,3387]],[[468,658,2716]],[[2749]],[[2827]],[[2726],[0]],[[10],[0]],[[3423],[0]],[[316,2721],[2725],[514,2722,2723]],[[430,47]],[[2730]],[[3842]],[[3844]],[[2727],[2728]],[[590,2729]],[[281,2732,203,316]],[[112],[574,3810]],[[55,316,590,2735,2723]],[[2736,2734],[2736],[0]],[[2737,2734]],[[750,2737]],[[300,763,3861],[729,763,3861],[297,763,3861],[318,763,3861],[303,763,3861],[304,763,3841],[298,763,3841],[305,763,3841],[299,763,3841],[314,763,3841],[308,763,3861],[307,763,3861],[317,763,3861],[309,763,3861],[738,763,2740],[310,763,3861],[313,763,3861],[315,763,3841],[311,763,3859],[312,763,3861],[712,763,3861],[713,763,3841],[319,763,3841],[233,763,2743],[735,763,3853],[736,763,3841],[296,763,3841],[737,763,2738],[739,763,3841],[742,763,2739],[2741]],[[3755],[376]],[[743],[383],[744]],[[3861],[376]],[[301,763,3861],[302,763,3843],[447,763,3861],[448,763,3841]],[[2746],[0]],[[753,2742,748]],[[2745,2744],[2745],[0]],[[750,3841]],[[3841,2744]],[[2750,2747],[2750],[0]],[[2752],[0]],[[55,459,522,590,2754,2723],[55,459,190,2788,2747,2748]],[[750,2788]],[[3423]],[[2751]],[[2755,2753],[2755],[0]],[[2756,2753]],[[750,2756]],[[2757,763,3861],[2758,763,3861],[2759,763,3861],[2760,763,3861],[2761,763,3841],[2762,763,3861],[2763,763,3843],[2764,763,3841],[2765,763,3841],[2766,763,3841],[2767,763,3841],[817,763,3841],[2768,763,3841],[2769,763,3853],[2770,763,3841],[2771,763,3841],[2772,763,3861],[2773,763,3861],[2774,763,3861],[2775,763,3859],[2776,763,3861],[2777,763,3861],[2778,763,3861],[2779,763,3841],[2780,763,3861],[2781,763,2740],[2782,763,3861],[2783,763,3841],[729,763,3861],[233,763,2743],[841,763,3841],[737,763,2738],[739,763,3841],[742,763,2739],[842,763,3841],[447,763,3861],[448,763,3841]],[[814],[297]],[[820],[300]],[[838],[318]],[[823],[303]],[[824],[304]],[[821],[301]],[[822],[302]],[[813],[296]],[[819],[319]],[[816],[298]],[[826],[305]],[[818],[299]],[[815],[735]],[[839],[736]],[[827],[314]],[[828],[308]],[[829],[307]],[[830],[309]],[[832],[311]],[[833],[312]],[[834],[313]],[[831],[310]],[[835],[315]],[[837],[317]],[[836],[738]],[[825],[712]],[[840],[713]],[[2790],[0]],[[2793],[0]],[[2796],[0]],[[2800],[0]],[[460,763,753,2784,748],[461,763,753,2784,748],[462,763,753,2785,748],[463,763,753,2785,748],[464,763,753,2786,748],[465,763,753,2786,748],[466,763,753,2787,748]],[[2791,2789],[2791],[0]],[[3781,2789]],[[750,3781]],[[2794,2792],[2794],[0]],[[3804,2792]],[[750,3804]],[[2797,2795],[2797],[0]],[[2798,2795]],[[750,2798]],[[3861]],[[2801,2799],[2801],[0]],[[3699,2799]],[[750,3699]],[[2824],[0]],[[2805],[0]],[[543,514,2802,2803,2813,2723],[552,514,2802,2723]],[[613,2807]],[[2812,2806],[2812],[0]],[[2811,2806]],[[530],[528]],[[2808,763,3855]],[[529]],[[2741],[2809],[2810]],[[750,2741]],[[2822],[0]],[[2815],[0]],[[618,763,3855]],[[2817],[0]],[[406,763,3855]],[[2819],[0]],[[129,763,3855]],[[2821],[0]],[[409,763,3855]],[[2814,2816,2818,2820]],[[2825,2823],[2825],[0]],[[2826,2823]],[[750,2826]],[[449],[538]],[[2828,210]],[[543],[552]],[[417,3830,203,2830],[2833],[2831,417,3830]],[[3859],[3383]],[[123],[148]],[[2834],[0]],[[173,3830,2832]],[[621,2836]],[[2837,2835],[2837],[0]],[[3383,2835]],[[750,3383]],[[677,2844]],[[2840],[0]],[[200,459]],[[2846],[0]],[[244,203,3758,749,3841,230,45,3853,2841]],[[3877],[0]],[[284,112,139,2843,3853],[676,2839],[2842]],[[2847],[0]],[[2847],[112,139,2843,3853,2845]],[[467,2665,539]],[[2849],[2863],[2897],[2900],[2929],[2933],[2850]],[[2852]],[[2979]],[[2854],[0]],[[11,618,2851,2855]],[[3691]],[[2853]],[[2858],[2861,2866]],[[2862],[3758]],[[10],[369],[2984]],[[2856,128,659,2857]],[[3724]],[[3721]],[[2859],[2860]],[[618,3876]],[[97,618,2865,3721,2875,2866]],[[3692]],[[2864],[0]],[[2874],[0]],[[75],[812]],[[2867,3855]],[[2870],[0]],[[2868]],[[2879],[0]],[[2883],[0]],[[2885,2873],[2885],[0]],[[2871,2872,2873,2869]],[[2878],[0]],[[2877],[0]],[[128,659,2984]],[[2876]],[[467,2881]],[[539],[650],[369]],[[2973],[2880]],[[2884,2882],[2884]],[[645,2882]],[[322,3841],[327,3841],[321,3841],[328,3841]],[[2,2886],[406,2894],[740,2895],[741,3842]],[[287],[611]],[[2888],[0]],[[247,3842,122],[365],[128]],[[3842],[128]],[[3842,122],[128]],[[2892],[0]],[[128],[719]],[[467,101,2891]],[[177,2887],[705,2889],[706,247,2890],[2893]],[[3842],[698]],[[2899],[0]],[[148,618,2896,3718]],[[3691]],[[2898]],[[215,2912]],[[2902],[0]],[[645,660,391]],[[2952,590,3718,2901]],[[421],[0]],[[2952],[10,2904]],[[2907],[0]],[[645,215,391]],[[2950],[0]],[[2926],[0]],[[2916],[0]],[[2925],[0]],[[2903],[2905,383,2908,2968,590,2913,2909,2910,2911],[427,383,3758,590,2913,2906]],[[2914],[2915]],[[3721]],[[3718]],[[2918],[2919]],[[2977,2917],[2977]],[[645,2917]],[[645,215,391]],[[663,2984]],[[645,659,2923]],[[2920],[0]],[[2984],[10,2922],[369],[128]],[[2921],[0]],[[17,618,2924]],[[2927]],[[2879]],[[2930,2928],[2930],[0]],[[453,618,3758,590,3758,2928]],[[750,3758,590,3758]],[[2935],[0]],[[2944],[0]],[[477,2931,2942,2932]],[[3691]],[[2934]],[[2952,203,3718]],[[2945],[0]],[[2939],[0]],[[2937,203,3718]],[[383,2908,2968,2938]],[[2940],[750,215,391,203,3718]],[[2936],[2952,2945,203,3718],[10,2904,2941],[427,383,3758,203,3718]],[[232,610,618]],[[2943]],[[2946],[2949]],[[383,2908,2968]],[[2948],[0]],[[383,2908,2968]],[[2947]],[[574],[206],[422]],[[2953,2951],[2953],[0]],[[2956,2951]],[[750,2956]],[[2965],[0]],[[483],[0]],[[2959],[2960,2168],[2962],[2963],[215,391],[509,110],[97,2954],[287,571],[459,2966],[509,636],[11,2955]],[[792],[746,3872]],[[3874,2957],[3874,2168]],[[2958]],[[497],[242],[614],[443]],[[97],[148]],[[2961,659]],[[133],[616],[236],[148],[173],[451],[510],[423],[188],[427],[565],[170],[594]],[[483],[572],[618],[636]],[[577,571],[2964]],[[65],[514]],[[2969],[0]],[[775,2967],[3781,751,2971],[3781],[3810]],[[751,775]],[[3810]],[[775],[2970]],[[2975,2972],[2975],[0]],[[2976,2972]],[[15],[0]],[[2974,2976]],[[63,3855],[259,3855],[559,3855]],[[215,391],[322,3841],[327,3841],[321,3841],[328,3841]],[[2982],[0]],[[506,659,2984],[506,659,2980],[506,128,659,2981,590,2984],[506,659,10,2978]],[[369],[128]],[[2984],[369],[10]],[[663,2984]],[[2985,2983],[2985],[0]],[[2987,2983]],[[750,2987]],[[2988],[0]],[[3874,2986]],[[746,3872],[792]],[[2994],[0]],[[2997],[0]],[[14,2077,2992,3813,2989],[62,2995,3813,2078],[61,2996,3813,2990],[388,2077,2998,3813],[455,2077,2999,3813,2079]],[[574],[571]],[[3002]],[[2993]],[[574],[571]],[[574],[571]],[[431],[180]],[[574],[571]],[[574],[571]],[[3003],[0]],[[3004],[0]],[[614,674,383,3832,3000,3001],[148,674,383,3832]],[[645,787,675]],[[621,112,3853]],[[200,615],[3006]],[[431],[184],[333],[180],[56]],[[431],[180],[619]],[[3011],[0]],[[3012,3009],[3012],[0]],[[245,410,3830,520,3853],[245,664,3863,3008],[607,410,3823],[607,664,3824,3009]],[[506,3018]],[[750,3824]],[[214],[658]],[[3013],[0]],[[3014,3387,3877,3016]],[[383],[3191]],[[3019,3017],[3019],[0]],[[3015,3017]],[[750,3015]],[[506,3022]],[[3023],[0]],[[592,3034],[406,3021,3877,3028],[3031],[3045,3043],[3878,3051]],[[200,3758]],[[382,753,3855,748]],[[406,753,3855,748]],[[3753],[0]],[[3751],[0]],[[3855,3026,3027],[3855,3026,3027],[3024],[3025]],[[3030],[0]],[[200,3758]],[[406,3029,590,734,3026,3027]],[[3035],[0]],[[3036],[0]],[[3037,3032],[3039,3033]],[[750,3039]],[[750,3037]],[[435,3038]],[[649],[386]],[[258,274,3041]],[[76],[601]],[[456,435],[435,3040],[500]],[[3044,3042],[3044],[0]],[[3042]],[[750,3048]],[[3387,3877,3053],[3708],[3383,3877,3191],[3050,3877,3053],[356,3047]],[[128]],[[3877,3191],[3618,2096],[3046]],[[3878,3387,3877,3053],[3045]],[[3880],[0]],[[745,3049,3387]],[[3052,3043],[592,3034]],[[3387,3877,3053]],[[3191],[3054],[3056]],[[128],[383],[10],[32]],[[487],[710]],[[3055]],[[509,3091]],[[22]],[[3801],[10]],[[547],[354],[289]],[[203],[251]],[[32],[316]],[[225],[547,3094,2723]],[[33],[446]],[[3066],[0]],[[251,3855]],[[3068],[0]],[[203,3843]],[[180]],[[3071],[0]],[[3069]],[[236],[235],[263]],[[639],[166]],[[3075,3074],[3075],[0]],[[750,3099]],[[3077],[0]],[[3099,3074]],[[3079],[0]],[[200,430,787]],[[547],[631]],[[93]],[[3083],[0]],[[200,3758]],[[618,3758]],[[109,2180,3781],[170,3797],[206,3785],[422,3783],[574,3810],[594,3787],[636,3790],[3084]],[[3760],[0]],[[3092],[0]],[[3098],[0]],[[204],[0]],[[3878],[0]],[[3058],[110,3086],[3087,571,3088,3086],[3089,593,3088,3086],[169,3088,3086],[574,547,3088,3086],[387,571,3088,3086],[408],[163,3059,3060],[3087,71,3061,3810,3088,3086],[3062,289],[514,3063],[3064,169,3065,3067,2386,2723],[3070,3072,3097,3810,3088,2372],[2281,162],[95,753,775,748,3073],[639,2386],[166,2386],[426],[425,3076,3078,2386],[3090,3080,3086],[3089,424],[3406,3086],[70,3086],[3081],[421],[216,200,3758,621,3718],[216,3082],[316,547],[97,3085],[422,547,3086],[206,547,3086],[422,68,3783],[206,68,3785]],[[204],[3093]],[[180,3089]],[[3096],[0]],[[370],[0]],[[3095]],[[203],[251]],[[3097,3830]],[[40,255],[91,568],[400,185],[3100]],[[10],[96],[256],[334],[522],[567]],[[3107],[0]],[[33,3859],[47,236,3109,251,3103],[196,2077,3106],[266,3101,3191],[281,236,248,47,3136],[3108]],[[3830],[128]],[[3105,3104],[3105],[0]],[[750,3123]],[[3129],[3123,3104]],[[84],[430]],[[510]],[[3111],[3115]],[[3112,3110],[3112],[0]],[[3114,3110]],[[750,3114]],[[3117],[0]],[[3810,3113]],[[3810,405,753,2139,748,3113]],[[3120],[0]],[[2645,753,3116,748]],[[3830],[420]],[[3121,3119],[3121],[0]],[[3118,3119]],[[750,3118]],[[3127],[0]],[[3124],[3122,289],[445,289,2723],[3125],[3126]],[[136],[225],[421],[547],[617]],[[430,47]],[[389]],[[32],[163],[165],[208],[515]],[[3132],[0]],[[3130,3128]],[[571],[574]],[[3133],[0]],[[645,435,287],[3813,3131]],[[3134],[645,435,287]],[[200,179]],[[3137],[0]],[[3810,3144,3113,3135],[3139]],[[232,270]],[[3140,3138],[3140],[0]],[[3142,3138]],[[750,3142]],[[3143],[0]],[[3810,3113,3141]],[[232,270]],[[405,753,2139,748]],[[3149],[3160],[3162],[3167]],[[3152],[0]],[[3157],[0]],[[3158],[0]],[[97,709,217,3830,599,2843,3150,3146,3147,3148]],[[618],[710]],[[3153,3151],[3153],[0]],[[711,2843,3155,3151]],[[2053,3155]],[[3156],[0]],[[787,3154]],[[773,787]],[[708,2843,787]],[[156],[140]],[[198],[0]],[[11,709,217,3825,3146,3147,3148,3159]],[[3163],[0]],[[506,709,217,3830,3161]],[[200,3165]],[[3166,3164],[3166],[0]],[[3842,3164]],[[2053,3842]],[[148,709,217,3825,3159]],[[3175],[3171],[3187],[3188],[3169]],[[3189]],[[3173],[0]],[[3172,3810,3170]],[[178],[135],[134]],[[3855],[3773]],[[3182],[0]],[[3176,3174,3183]],[[178],[135],[134]],[[180]],[[404]],[[201,763,3872]],[[14,201,763,3872]],[[14]],[[3177],[3178],[3179],[3180],[3181]],[[2461],[3185],[3186]],[[2366],[2400],[2457],[2656]],[[3184]],[[200,84,3842]],[[222,3872]],[[620,3830]],[[714]],[[3198,3190],[3198],[0]],[[3193,3190]],[[3196],[0]],[[3202,3192],[3197]],[[596],[183],[610]],[[3407],[0]],[[257,3195,3194]],[[371,3191]],[[3199,3191],[654,3191],[3200,3191]],[[15],[770]],[[394],[772]],[[3203,3201],[3203],[0]],[[3207,3201]],[[257,3195,376],[3205,3204,2496],[3205,3207]],[[10],[16]],[[763],[777],[764],[765],[768],[769],[776]],[[3210],[0]],[[3216,3206]],[[668],[0]],[[733,3208,3414]],[[3195,3212],[3209],[521,275,3216]],[[3214],[0]],[[251,3213],[30,3216,15,3207],[275,3223,3211],[444,3216]],[[2496],[753,3404,748]],[[168,3223]],[[3217,3215],[3217],[0]],[[3223,3215]],[[760,3216],[3218,3216],[3219,3216],[3220,247,3191,3409],[3221,3216],[757,3216],[759,3216]],[[775],[762],[774],[145],[349]],[[778],[773]],[[778],[773]],[[779],[780]],[[3224,3222],[3224],[0]],[[3226,3222]],[[761,3226]],[[3227],[0]],[[3236,3225]],[[69,3872]],[[3237],[0]],[[487],[0]],[[3320],[0]],[[3246],[0]],[[3191],[0]],[[3245,3233],[3245]],[[3393],[0]],[[3248],[0]],[[3847],[3265],[3382,3228],[754],[3238],[3239],[3240,3223],[3408,3223],[3229,753,3404,748],[2623,2496],[752,3830,3191,747],[320,3315,7,753,3216,3230,748],[32,3223],[3244],[52,753,3191,17,3398,3231,748],[51,3232,3233,3234,159],[94,753,3191,750,3398,748],[94,753,3191,621,3618,748],[128,753,3837,748],[626,753,3837,748],[247,3191,3409,778,3191],[3377],[3329],[3773,3235]],[[3877,3191]],[[3294]],[[3298]],[[778],[773],[758]],[[247],[0]],[[3617],[0]],[[52,753,3191,21,586,843,3241,3853,17,113,3242,748]],[[3243]],[[3391,3392]],[[3247]],[[731]],[[3249],[3250]],[[766,3853]],[[767,3853]],[[143],[0]],[[3267],[0]],[[3270],[0]],[[3273],[0]],[[3276],[0]],[[3278],[0]],[[3280],[0]],[[3282],[0]],[[3284],[0]],[[3286],[0]],[[3288],[0]],[[3290],[0]],[[3291],[0]],[[3293],[0]],[[26,753,3251,3314,748,3252],[3268,753,3314,748,3253],[3271],[95,753,2722,775,748,3254],[95,753,3274,748,3255],[345,753,3251,3314,748,3256],[326,753,3251,3314,748,3257],[551,753,3314,748,3258],[632,753,3314,748,3259],[548,753,3314,748,3260],[635,753,3314,748,3261],[564,753,3251,3314,748,3262],[218,753,3251,3404,2374,3263,748,3264]],[[3304]],[[3266]],[[35],[36],[38]],[[3304]],[[3269]],[[3313]],[[3304]],[[3272]],[[2722,775],[3314],[143,3404]],[[3304]],[[3275]],[[3304]],[[3277]],[[3304]],[[3279]],[[3304]],[[3281]],[[3304]],[[3283]],[[3304]],[[3285]],[[3304]],[[3287]],[[3304]],[[3289]],[[499,3855]],[[3304]],[[3292]],[[672,753,3404,748]],[[3307],[0]],[[3310],[0]],[[3303],[0]],[[3299,3876,3304],[688,3414,3304],[3300,753,3191,3295,748,3296,3304],[3301,3413,3296,3304],[687,753,3191,750,3223,748,3297,3296,3304]],[[696],[694],[679],[678],[692]],[[686],[684]],[[681],[685]],[[191],[268]],[[203,3302]],[[691,3305]],[[3826],[2522]],[[3309],[0]],[[750,3308,3306]],[[3843],[754],[3830],[3383]],[[750,3191]],[[3311,689]],[[695],[232]],[[3304],[0]],[[667,753,3314,748,3312],[666,753,3314,750,3314,748,3312]],[[2722,3191]],[[3317],[753,3317,748]],[[3318,3316],[3318],[0]],[[3837,3316]],[[750,3837]],[[3321],[0]],[[251,41,346],[251,359,267,346,3319],[645,430,176]],[[645,430,176]],[[3330],[0]],[[3876],[0]],[[3331,3324],[3331]],[[3335],[0]],[[3353],[0]],[[3340],[0]],[[3343],[0]],[[60,753,3404,3322,748],[105,3323],[116,3413],[122,3413],[229,3413],[242,753,3191,750,3191,750,3191,750,3191,748],[247,753,3191,3324,748],[3334],[272,753,3191,750,3191,748],[343,3413],[350,3413],[478,753,3191,750,3191,748],[495,3413],[586,3413],[583,753,3191,3325,748],[3366],[618,3876],[626,3413],[656,3413],[3336,753,3191,750,3337,748],[100,3323],[108,3326],[3338,753,3191,750,247,3191,3409,748],[182,753,3409,203,3191,748],[213,753,3365,750,3191,748],[372,3326],[414,753,3216,251,3191,748],[3370],[569,3326],[3339,753,3411,750,3191,750,3191,748],[622,3323],[624,3326],[623,3326],[19,3413],[58,3413],[67,3412],[70,3413],[109,3876],[231,753,3191,750,3191,750,3191,748],[201,753,3191,750,3191,3327,748],[337,3413],[349,753,3191,750,3191,748],[3341],[3342],[429,3413],[457,753,3191,750,3191,748],[458,753,3191,750,3191,750,3191,748],[476,3413],[485,3876],[597,753,3191,750,3191,748],[640,753,3191,3328,748],[641,753,3191,17,60,748],[641,753,3191,3349,748],[3350]],[[621,3618]],[[750,3191]],[[3333],[0]],[[851,3398]],[[850,753,3223,750,3859,3332,2624,748]],[[750,3191]],[[5],[558]],[[3191],[247,3191,3409]],[[114],[115]],[[584],[585]],[[750,3191]],[[382,753,3859,748]],[[406,3413]],[[750,3191]],[[3345],[0]],[[17,60,3616]],[[3356]],[[3348],[0]],[[3346]],[[17,32,3616],[3344,3347],[750,3841,750,3841,750,3841]],[[3351],[211,753,2362,748],[279,3412],[351,3412],[352,3412],[353,3412],[411,753,3191,750,3191,748],[412,3412]],[[90,753,3191,750,3191,748]],[[3354],[0]],[[753,3352,748]],[[3355]],[[787]],[[274,3359]],[[3358,3357],[3358],[0]],[[750,3361]],[[3842,773,3842],[3361,3357]],[[3364],[0]],[[3842,3360]],[[18],[134]],[[476],[0]],[[3362,3363],[476]],[[116],[586],[113],[583]],[[595,753,3369,748]],[[3368],[0]],[[203,3191]],[[3191,3367],[269,3232,203,3191],[591,3232,203,3191],[43,3232,203,3191]],[[563,753,3191,3375,748]],[[3372],[0]],[[750,3191]],[[3374],[0]],[[200,3191]],[[750,3191,3371],[203,3191,3373]],[[3379],[0]],[[3827,753,3376,748],[3835,753,2362,748]],[[3380,3378],[3380],[0]],[[3381,3378]],[[750,3381]],[[3191,2581]],[[3383],[3386]],[[746,3872],[792]],[[3879],[0]],[[3840],[0]],[[745,3384,3872,3385]],[[3390],[128,3840]],[[3830,3385]],[[3873,3385]],[[3388],[3389]],[[642,3191]],[[582,3191]],[[154,3191]],[[3606],[0]],[[3612],[0]],[[249],[0]],[[3869],[0]],[[32,3394],[60,3394,3395],[3604,3394],[512,3396],[612,3396],[116],[586,3242],[113,3242],[126,3397],[656],[3399],[3400],[3402]],[[262]],[[3605]],[[3870],[0]],[[195,3401]],[[3405,3403],[3405],[0]],[[3191,3403]],[[750,3191]],[[60,506],[58]],[[371],[800]],[[771],[800]],[[3411],[3410]],[[494],[341],[342],[226],[228],[227],[119],[121],[120],[118],[655]],[[337],[495],[343],[229],[122],[640],[350],[429],[656]],[[753,3404,748]],[[753,3191,748]],[[753,3223,748]],[[3417,3415],[3417],[0]],[[3418,3415]],[[750,3418]],[[3191,2123]],[[3421,3419],[3421],[0]],[[3422,3419]],[[750,3422]],[[3191]],[[3424]],[[200,57,3861]],[[2004],[3426],[3427],[3437],[3441],[3442],[3447],[3448],[3470],[3469],[3502],[3505],[3503]],[[475,3191]],[[231,3429,159,231]],[[3430],[0]],[[3191,3431,3428]],[[155,3429],[154,3433]],[[582,3433]],[[3434,3432],[3434]],[[3432]],[[3425,755]],[[3438,3435],[3438]],[[3439],[0]],[[51,3232,3435,3436,159,51]],[[3391,3431]],[[154,3433]],[[3820],[0]],[[3443,3446,3440]],[[3446]],[[3819,749]],[[3453],[0]],[[3433],[0]],[[29,3444,3445,159]],[[3443,3448,3440]],[[3449],[3450],[3451]],[[294,3433,159,294]],[[644,3191,147,3433,159,644]],[[457,3433,613,3191,159,457]],[[3454,3452],[3454]],[[3452]],[[3455,755]],[[3457],[3459],[3464],[3468]],[[3458],[0]],[[127,3832,3592,2096,3456]],[[128,3191]],[[127,3830,83,200,3460]],[[3841],[3462]],[[627],[0]],[[526,3461,3859]],[[3466,3463],[3466],[0]],[[127,3465,219,200,3467,3463,3425]],[[92],[175],[605]],[[750,3467]],[[3460],[3830],[527],[3407,202],[525]],[[127,3830,106,200,2461]],[[260,3820]],[[271,3820]],[[3474],[0]],[[207,3471,138,3479]],[[540]],[[101],[3473]],[[3476,3475],[3476],[0]],[[750,3481]],[[3478,3477],[3478],[0]],[[750,3484]],[[3481,3475],[83,3480,3484,3477]],[[3847],[3382],[3837]],[[3482,763,3483]],[[3382],[3830]],[[377],[485]],[[3485,763,3486]],[[3382],[3830]],[[3487],[473]],[[64],[557],[87],[89],[88],[53],[492],[576],[73],[107],[336],[355]],[[3493],[0]],[[511,3490,3488]],[[3830],[3462]],[[3492,3491],[3492],[0]],[[750,3501]],[[506,3501,3491]],[[3497],[0]],[[3500],[0]],[[469,3494,3495]],[[3830],[3462]],[[3499,3498],[3499],[0]],[[750,3501]],[[506,3501,3498]],[[3487,763,3480]],[[387,3830]],[[66,3830]],[[3507],[0]],[[186,3504,3830,248,3832]],[[367],[0]],[[3506,203]],[[3511],[0]],[[3512],[0]],[[21,3191],[171,3191,3409,3508,3509]],[[542,3191]],[[160,3191]],[[3765,3528,2098]],[[3515],[3556]],[[3516]],[[62,3413]],[[2041,730]],[[3527],[0]],[[3520,2232,3574,2235],[205,2646,2239,3574,2236],[523,2646,2239,3574,2237],[3518,3525]],[[265],[236]],[[420,265],[609,2646]],[[3517]],[[3524],[0]],[[3522]],[[3521,2232,3574,2235],[199,265,2239,3567,3556],[3516,3523]],[[3830],[0]],[[86,3526]],[[3592,3539]],[[3530],[0]],[[209,12]],[[3532],[0]],[[637],[554]],[[3542,3533],[3542],[0]],[[3533]],[[3552,3535],[3552],[0]],[[3535]],[[3534],[3536]],[[2096,3529,17,3413,3531,3537]],[[3538],[3533]],[[420],[0]],[[265],[0]],[[2041,3867],[3543],[128,3545],[3546],[383,614,372,3326],[24],[501,128,627],[3540,265],[609,3541],[75,3859],[3697],[74,3550],[553,3551],[3547],[3548],[3549]],[[371,720]],[[3413]],[[3849],[372,3326],[3544]],[[3582]],[[707,3844]],[[3518,3516]],[[3517]],[[192],[152],[128]],[[142],[334],[128]],[[609,3541],[75,3855],[3195,376],[3540,265]],[[3834],[0]],[[3558],[0]],[[3563],[0]],[[443,3810,3553,3554,3555]],[[204],[402],[513]],[[320,3557]],[[3560],[0]],[[383,133,3564]],[[3562],[0]],[[383,614,3564]],[[383,614,3564,3559],[383,133,3564,3561]],[[3565],[506,3867],[373,3],[506,128]],[[471],[49]],[[3568,3566],[3568],[0]],[[753,3569,3566,748]],[[750,3569]],[[3830,3394,2123]],[[3572,3570],[3572],[0]],[[753,3573,3570,748]],[[750,3573]],[[3569],[3413,2123]],[[3575],[3576]],[[3571]],[[3567]],[[3578]],[[44],[488],[220]],[[3580],[3583]],[[264,2127,3841],[75,3859],[3581]],[[3582]],[[662],[661]],[[3584,3577]],[[621],[599]],[[3580],[645,401,3830]],[[3580]],[[3592,-1]],[[3609],[0]],[[3871],[0]],[[32],[0]],[[3601],[0]],[[3593,3394,3588],[3595,3589,3588],[3596,3397,3588],[37,3394],[3597],[3598,3606,3395],[60,3394,3395],[32,3394],[3599,3606,3590],[3604,3394,3590],[628,3606],[656,3394,3588],[116],[586,3242],[583,3242],[113,3242],[587],[39,3394],[3600],[293,628],[293,3591,3395],[589,3395],[580,3394,3395],[332,3395],[291,3395],[164,3851,3395],[506,3851,3395],[501],[3602],[3603]],[[249],[588],[516],[331],[31]],[[416],[0]],[[437],[146,3594]],[[195],[126],[378],[192]],[[42],[41]],[[60,633],[629]],[[358,629],[379],[361,629],[358,60,633],[361,633]],[[330],[290]],[[60,633],[629]],[[262]],[[212],[211],[411],[352],[279],[351],[412],[353]],[[361],[358,60]],[[437],[146,3594]],[[753,3607,748]],[[3844],[783]],[[3610,3608],[3610]],[[3608]],[[512],[612],[657]],[[3613],[0]],[[3614],[3615],[46],[3406,3618,3590],[32,3611]],[[3406,3618]],[[19,3590],[32,19]],[[606,3590],[32,606]],[[753,3842,748]],[[753,787,748]],[[3872],[32],[3619]],[[128]],[[3872],[3621],[3622]],[[128]],[[32]],[[3625,3623],[3625],[0]],[[3629,3623]],[[2053,3629]],[[3629,3626],[3629]],[[3626]],[[3813],[0]],[[163,2127,3801],[3631],[323,2127,3843],[344,2127,3843],[25,2127,3841],[406,2127,3853],[75,2127,3853],[3632],[3633],[24,2127,3843],[399,2127,3645],[3634,2127,3645],[3635,2127,3841],[132,2127,3841],[486,2127,3636],[608,2127,753,3628,748],[3649],[3647],[243,2127,3637],[112,139,2127,3855],[236,139,2127,3855],[572,3639,3830],[553,3640],[84,2127,3855],[264,2127,3841],[3641],[3642],[3643],[3644]],[[376],[3872]],[[721,2843,3630]],[[81,2127,3855]],[[158,2127,3855]],[[544],[545],[546]],[[61],[575]],[[128],[152],[192],[80],[442],[78]],[[373],[191],[268]],[[2127]],[[3638],[0]],[[142],[334]],[[543,592]],[[848,2127,3855]],[[849,2127,3855]],[[2277]],[[3841],[128]],[[128],[0]],[[3646,69,2127,3620]],[[3646,158,2127,3853]],[[3646,3406,2127,3618]],[[3654],[0]],[[3662],[0]],[[3668],[0]],[[405,45,3658,3650,3651,3652]],[[404,3842]],[[277],[0]],[[3665],[0]],[[3832],[0]],[[3655,265,3656,753,3657,748],[3655,220,753,3216,748],[3659,3660]],[[432],[280]],[[753,3216,748],[71,753,3657,748]],[[3664],[0]],[[561,45,3655,3663,3661]],[[220,753,3216,748],[265,3656,3834]],[[560,3842]],[[3666]],[[9,763,3842]],[[3669,3667],[3669],[0]],[[753,3673,3667,748]],[[750,3673]],[[3675],[0]],[[3682,3671],[3682],[0]],[[3678],[0]],[[405,3830,3670,3671,3672]],[[3687],[329]],[[626,273,581,3674],[626,251,3680]],[[3677,3676],[3677],[0]],[[750,3685]],[[753,3685,3676,748]],[[3681,3679],[3681],[0]],[[3687],[753,3687,3679,748]],[[750,3687]],[[572,2127,3830],[2281,163,2127,3801],[368,2127,3842],[3683,2127,3842],[3684,139,2127,3859],[75,2127,3859]],[[323],[344]],[[112],[236]],[[561,3872,3671]],[[3688,3686],[3688],[0]],[[753,3689,3686,748]],[[750,3689]],[[3216],[329]],[[130,763,3758]],[[231,174]],[[231,3407,174]],[[3695],[0]],[[3693,3696]],[[251],[397],[240]],[[3818,3698]],[[69,3620]],[[3592,2096]],[[753,3781,750,3781,748]],[[3702,3700],[3702],[0]],[[3790,3700]],[[750,3790]],[[3705,3703],[3705],[0]],[[3706,3703]],[[750,3706]],[[3773,3877,3707]],[[3191],[128]],[[3406,3618]],[[3712,3709],[3712]],[[71,3709]],[[392],[0]],[[579,45,3855],[3711,157,45,3855],[167,45,3855]],[[3715,3713],[3715]],[[278,3713]],[[3716,45,3855]],[[579],[541]],[[3719,3717],[3719],[0]],[[3758,3717]],[[750,3758]],[[3722,3720],[3722],[0]],[[3727,3720]],[[750,3727]],[[3725,3723],[3725],[0]],[[3739,3723]],[[750,3739]],[[3738],[0]],[[3758,3726]],[[406]],[[3730],[0]],[[3728]],[[45,3855]],[[3733],[0]],[[17,3856],[3731]],[[3735],[0]],[[645,3872]],[[3734,45,734,406]],[[45,3729,3855],[645,3872,3732],[3736]],[[230,3737]],[[3740,3750]],[[2862],[3758]],[[3742],[0]],[[645,3872]],[[734,406]],[[3743],[3855]],[[3746],[0]],[[17,3856,3027]],[[3748],[0]],[[3741,45,3744,3026,3027],[645,3872,3745]],[[3752]],[[230,3747],[3749],[0]],[[727,101,406]],[[141,728,406]],[[458,3855]],[[3757],[0]],[[3872,3754]],[[3872],[0]],[[746,3756],[792]],[[3755],[105,3323]],[[275,3853]],[[3759],[2585]],[[385],[380]],[[284],[375]],[[3764]],[[405,3834]],[[3767],[3768]],[[3840],[3835,3385]],[[3766]],[[3830]],[[3830]],[[3772,3770],[3772],[0]],[[753,3769,3770,748]],[[750,3769]],[[3765]],[[3773],[3778]],[[3830]],[[3765]],[[3779],[0]],[[3830,751,3777,775]],[[3830,751]],[[3830]],[[3830]],[[3835]],[[3835]],[[3835]],[[3835]],[[3835]],[[3835]],[[3835],[3789]],[[3840]],[[3835],[3791]],[[3840]],[[3830]],[[3830]],[[3830]],[[3830]],[[3835]],[[3835]],[[3830]],[[3872]],[[3872]],[[3872]],[[3835],[3803]],[[3840]],[[3781,3840]],[[3809],[0]],[[3830,3805]],[[3808],[0]],[[751,775]],[[751,775],[3840,3807]],[[3835],[3811]],[[3840]],[[3814,3812],[3814],[0]],[[3810,3812]],[[750,3810]],[[3817,3815],[3817],[0]],[[3806,3815]],[[750,3806]],[[3830]],[[3827],[3889]],[[3819]],[[3827],[3898]],[[3821]],[[3830]],[[3853]],[[3830]],[[3830]],[[3828],[3829]],[[793],[781]],[[784]],[[3827],[3881]],[[3833,3831],[3833],[0]],[[3830,3831]],[[750,3830]],[[753,3832,748]],[[3830,3385]],[[3838],[0]],[[3830,3836],[3839]],[[3840,3385]],[[3840,3840]],[[751,3830]],[[787],[786],[788],[791],[783],[785]],[[787],[786],[788],[791]],[[787],[788],[791],[783],[785]],[[787],[3845],[791],[788]],[[786]],[[794],[0]],[[3859],[3865],[3868],[3867],[3866],[3846,3848]],[[786],[782]],[[3847],[778,3841],[773,3841]],[[3852,3850],[3852],[0]],[[753,3855,3850,748]],[[750,3855]],[[790],[3854]],[[784]],[[3853],[786],[782]],[[3853],[3857]],[[786]],[[3853,3858],[3853],[0]],[[3860,3858]],[[3846,3853],[789]],[[3853]],[[3864,3862],[3864],[0]],[[3853,3862]],[[750,3853]],[[787],[788],[791],[783],[785]],[[596],[183]],[[376],[801]],[[116,3853],[586,3853],[583,3853]],[[3606],[3871]],[[3606]],[[753,787,750,787,748]],[[3830],[3853]],[[3827],[3902]],[[3821],[3853]],[[3844],[3827]],[[753,748]],[[763],[756]],[[658],[673],[214],[284],[502]],[[214,751],[284,751],[502,751]],[[658,751],[673,751],[214,751],[284,751],[502,751]],[[3885],[3886]],[[510]],[[714]],[[3889],[3904],[173],[3882],[3883]],[[3884]],[[3894],[3887],[3888],[3893],[3903]],[[173],[714],[510]],[[19],[29],[46],[47],[58],[61],[677],[75],[77],[90],[123],[147],[159],[196],[197],[219],[222],[234],[245],[267],[373],[415],[417],[455],[468],[480],[489],[512],[514],[543],[552],[597],[606],[607],[651]],[[3891],[3892]],[[3908],[170],[188],[369],[423],[427],[451],[459],[709],[565]],[[3890]],[[3894],[3893],[3903]],[[170],[188],[369],[423],[427],[451],[459],[709],[565]],[[3895],[3897]],[[3],[2],[724],[5],[660],[6],[7],[8],[9],[12],[16],[21],[812],[23],[24],[25],[26],[27],[33],[37],[40],[41],[42],[44],[675],[50],[53],[54],[56],[57],[63],[64],[65],[66],[67],[68],[70],[71],[74],[73],[76],[78],[79],[664],[80],[81],[82],[84],[85],[87],[88],[89],[91],[96],[101],[107],[111],[112],[113],[116],[122],[129],[130],[715],[132],[716],[138],[139],[140],[141],[142],[150],[151],[152],[156],[158],[160],[730],[162],[163],[848],[164],[166],[165],[168],[169],[171],[172],[680],[176],[177],[179],[180],[181],[184],[185],[189],[190],[191],[192],[682],[201],[202],[204],[208],[211],[212],[213],[713],[840],[216],[210],[841],[220],[674],[705],[225],[224],[229],[230],[233],[725],[235],[238],[844],[243],[244],[661],[250],[255],[256],[258],[259],[262],[264],[847],[268],[270],[273],[274],[279],[280],[670],[286],[288],[289],[296],[735],[298],[299],[319],[300],[729],[301],[302],[303],[304],[712],[305],[306],[307],[308],[309],[310],[312],[311],[313],[314],[316],[738],[317],[318],[736],[321],[322],[323],[324],[327],[328],[333],[334],[335],[336],[337],[340],[343],[344],[346],[348],[350],[351],[352],[353],[354],[355],[356],[357],[358],[361],[363],[702],[365],[366],[367],[368],[671],[374],[689],[377],[379],[381],[732],[728],[384],[386],[387],[719],[390],[703],[717],[690],[398],[399],[400],[401],[402],[403],[404],[406],[704],[407],[408],[409],[410],[411],[412],[413],[693],[418],[419],[421],[737],[424],[426],[425],[429],[430],[431],[434],[438],[439],[441],[846],[442],[718],[445],[446],[447],[448],[449],[452],[842],[454],[456],[460],[462],[461],[463],[466],[464],[465],[617],[695],[470],[472],[727],[473],[851],[474],[706],[476],[659],[481],[482],[483],[485],[486],[488],[490],[492],[721],[849],[722],[720],[723],[495],[496],[500],[501],[503],[508],[513],[669],[515],[517],[519],[520],[521],[813],[814],[815],[817],[816],[818],[819],[820],[821],[822],[823],[824],[825],[826],[829],[828],[830],[831],[833],[832],[834],[827],[835],[522],[836],[837],[838],[839],[528],[529],[530],[532],[535],[538],[707],[540],[542],[544],[545],[546],[547],[553],[556],[557],[558],[559],[560],[561],[566],[567],[568],[571],[572],[575],[576],[577],[578],[580],[581],[708],[697],[584],[585],[583],[586],[845],[592],[593],[598],[599],[698],[601],[602],[603],[604],[610],[613],[615],[618],[619],[625],[627],[631],[711],[636],[662],[638],[639],[640],[641],[646],[647],[648],[650],[652],[653],[656],[843]],[[731],[741],[735],[738],[736],[733],[744],[740],[737],[734],[739],[742],[743],[583],[586]],[[3896]],[[3900],[3901]],[[3908],[3904]],[[3899]],[[3894],[3888],[3903]],[[3894],[3887],[3888],[3893]],[[214],[284],[658],[673],[502]],[[3905],[3906],[3907]],[[2],[19],[12],[27],[29],[46],[47],[58],[61],[677],[66],[75],[77],[90],[123],[147],[159],[196],[197],[201],[210],[219],[222],[224],[245],[661],[267],[373],[387],[390],[398],[401],[413],[415],[417],[452],[455],[468],[470],[659],[480],[489],[720],[721],[722],[723],[496],[503],[512],[519],[514],[520],[543],[552],[597],[606],[607],[615],[662],[648],[651]],[[510]],[[234]],[[3909],[3910],[3912],[3914],[3915]],[[3],[724],[5],[6],[7],[8],[9],[13],[16],[21],[22],[24],[23],[25],[26],[33],[37],[40],[42],[41],[44],[675],[50],[53],[54],[56],[57],[63],[65],[64],[67],[68],[70],[73],[74],[71],[76],[78],[79],[664],[80],[81],[82],[84],[85],[87],[89],[88],[91],[93],[96],[101],[107],[112],[111],[113],[116],[122],[129],[130],[132],[136],[716],[138],[139],[140],[141],[142],[150],[151],[152],[158],[160],[164],[163],[162],[165],[166],[168],[169],[171],[680],[176],[179],[180],[181],[185],[184],[682],[202],[156],[204],[189],[190],[191],[192],[208],[212],[211],[213],[216],[214],[220],[674],[705],[225],[229],[230],[233],[250],[235],[238],[244],[725],[255],[256],[258],[259],[243],[262],[850],[264],[268],[270],[273],[274],[279],[280],[284],[670],[286],[288],[289],[323],[316],[319],[300],[304],[301],[302],[318],[303],[712],[306],[298],[305],[299],[314],[308],[307],[317],[309],[310],[311],[312],[313],[296],[321],[322],[325],[324],[327],[328],[333],[334],[335],[336],[337],[340],[343],[344],[348],[346],[350],[351],[352],[353],[354],[355],[357],[356],[358],[361],[363],[702],[365],[367],[366],[374],[368],[689],[671],[377],[379],[381],[728],[382],[384],[719],[703],[717],[690],[399],[400],[402],[403],[404],[406],[704],[407],[409],[410],[408],[411],[412],[693],[418],[419],[708],[421],[424],[425],[426],[429],[430],[431],[434],[438],[439],[441],[440],[442],[445],[446],[447],[448],[449],[676],[454],[456],[460],[461],[462],[463],[464],[465],[466],[617],[695],[472],[727],[473],[474],[706],[476],[481],[482],[483],[485],[486],[488],[490],[492],[495],[501],[500],[502],[508],[513],[669],[515],[517],[521],[522],[528],[529],[530],[533],[532],[535],[538],[707],[540],[542],[544],[545],[546],[547],[553],[556],[557],[558],[559],[561],[560],[565],[566],[567],[568],[576],[571],[575],[572],[577],[578],[580],[581],[697],[592],[593],[583],[584],[585],[586],[598],[599],[600],[698],[601],[602],[604],[603],[610],[613],[618],[619],[631],[711],[636],[627],[639],[638],[640],[647],[641],[650],[652],[653],[656]],[[510]],[[99],[234],[206],[484],[487]],[[3911]],[[172],[177],[386],[565],[625],[646]],[[3913]],[[660]]]]; diff --git a/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-lexer.php b/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-lexer.php deleted file mode 100644 index def8ca3fa..000000000 --- a/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-lexer.php +++ /dev/null @@ -1,3 +0,0 @@ -materialize_native_children(); - parent::append_child( $node ); - } - - /** @inheritDoc */ - public function merge_fragment( $node ) { - $this->materialize_native_children(); - if ( $node instanceof self ) { - $node->materialize_native_children(); - } - parent::merge_fragment( $node ); - } - - /** @inheritDoc */ - public function has_child(): bool { - if ( $this->was_mutated ) { - return parent::has_child(); - } - return wp_sqlite_mysql_native_ast_has_child( $this ); - } - - /** @inheritDoc */ - public function has_child_node( ?string $rule_name = null ): bool { - if ( $this->was_mutated ) { - return parent::has_child_node( $rule_name ); - } - return wp_sqlite_mysql_native_ast_has_child_node( $this, $rule_name ); - } - - /** @inheritDoc */ - public function has_child_token( ?int $token_id = null ): bool { - if ( $this->was_mutated ) { - return parent::has_child_token( $token_id ); - } - return wp_sqlite_mysql_native_ast_has_child_token( $this, $token_id ); - } - - /** @inheritDoc */ - public function get_first_child() { - if ( $this->was_mutated ) { - return parent::get_first_child(); - } - return wp_sqlite_mysql_native_ast_get_first_child( $this ); - } - - /** @inheritDoc */ - public function get_first_child_node( ?string $rule_name = null ): ?WP_Parser_Node { - if ( $this->was_mutated ) { - return parent::get_first_child_node( $rule_name ); - } - return wp_sqlite_mysql_native_ast_get_first_child_node( $this, $rule_name ); - } - - /** @inheritDoc */ - public function get_first_child_token( ?int $token_id = null ): ?WP_Parser_Token { - if ( $this->was_mutated ) { - return parent::get_first_child_token( $token_id ); - } - return wp_sqlite_mysql_native_ast_get_first_child_token( $this, $token_id ); - } - - /** @inheritDoc */ - public function get_first_descendant_node( ?string $rule_name = null ): ?WP_Parser_Node { - if ( $this->was_mutated ) { - return parent::get_first_descendant_node( $rule_name ); - } - return wp_sqlite_mysql_native_ast_get_first_descendant_node( $this, $rule_name ); - } - - /** @inheritDoc */ - public function get_first_descendant_token( ?int $token_id = null ): ?WP_Parser_Token { - if ( $this->was_mutated ) { - return parent::get_first_descendant_token( $token_id ); - } - return wp_sqlite_mysql_native_ast_get_first_descendant_token( $this, $token_id ); - } - - /** @inheritDoc */ - public function get_children(): array { - if ( $this->was_mutated ) { - return parent::get_children(); - } - return wp_sqlite_mysql_native_ast_get_children( $this ); - } - - /** @inheritDoc */ - public function get_child_nodes( ?string $rule_name = null ): array { - if ( $this->was_mutated ) { - return parent::get_child_nodes( $rule_name ); - } - return wp_sqlite_mysql_native_ast_get_child_nodes( $this, $rule_name ); - } - - /** @inheritDoc */ - public function get_child_tokens( ?int $token_id = null ): array { - if ( $this->was_mutated ) { - return parent::get_child_tokens( $token_id ); - } - return wp_sqlite_mysql_native_ast_get_child_tokens( $this, $token_id ); - } - - /** @inheritDoc */ - public function get_descendants(): array { - if ( $this->was_mutated ) { - return parent::get_descendants(); - } - return wp_sqlite_mysql_native_ast_get_descendants( $this ); - } - - /** @inheritDoc */ - public function get_descendant_nodes( ?string $rule_name = null ): array { - if ( $this->was_mutated ) { - return parent::get_descendant_nodes( $rule_name ); - } - return wp_sqlite_mysql_native_ast_get_descendant_nodes( $this, $rule_name ); - } - - /** @inheritDoc */ - public function get_descendant_tokens( ?int $token_id = null ): array { - if ( $this->was_mutated ) { - return parent::get_descendant_tokens( $token_id ); - } - return wp_sqlite_mysql_native_ast_get_descendant_tokens( $this, $token_id ); - } - - /** @inheritDoc */ - public function get_start(): int { - if ( $this->was_mutated ) { - return parent::get_start(); - } - return wp_sqlite_mysql_native_ast_get_start( $this ); - } - - /** @inheritDoc */ - public function get_length(): int { - if ( $this->was_mutated ) { - return parent::get_length(); - } - return wp_sqlite_mysql_native_ast_get_length( $this ); - } - - private function materialize_native_children(): void { - if ( $this->was_mutated ) { - return; - } - - $this->children = wp_sqlite_mysql_native_ast_get_children( $this ); - $this->was_mutated = true; - if ( function_exists( 'wp_sqlite_mysql_native_ast_materialize_wrapper' ) ) { - wp_sqlite_mysql_native_ast_materialize_wrapper( $this ); - } - } -} diff --git a/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-parser.php b/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-parser.php deleted file mode 100644 index 76244ad75..000000000 --- a/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-parser.php +++ /dev/null @@ -1,14 +0,0 @@ - - */ -function wp_sqlite_mysql_native_export_grammar( WP_Parser_Grammar $grammar ): array { - return array( - 'highest_terminal_id' => $grammar->highest_terminal_id, - 'rules' => $grammar->rules, - 'lookahead_is_match_possible' => $grammar->lookahead_is_match_possible, - 'rule_names' => $grammar->rule_names, - 'fragment_ids' => $grammar->fragment_ids, - ); -} diff --git a/packages/mysql-on-sqlite/src/mysql/native/trait-wp-mysql-native-parser-impl.php b/packages/mysql-on-sqlite/src/mysql/native/trait-wp-mysql-native-parser-impl.php deleted file mode 100644 index c53e96e53..000000000 --- a/packages/mysql-on-sqlite/src/mysql/native/trait-wp-mysql-native-parser-impl.php +++ /dev/null @@ -1,55 +0,0 @@ -native`. `WP_Parser`'s state (`$grammar`, `$tokens`, - * `$position`) stays inert in native mode — the trait's overrides - * never read it. - * - * Adding a public method here is enough to plumb a new public method - * through to the native parser; the using class does not need touching. - */ -trait WP_MySQL_Native_Parser_Impl { - /** - * @var WP_MySQL_Native_Parser - */ - private $native; - - /** - * @param WP_Parser_Grammar $grammar - * @param array|WP_MySQL_Native_Token_Stream $tokens - */ - public function __construct( WP_Parser_Grammar $grammar, $tokens ) { - // WP_Parser's `array $tokens` constructor signature can't accept - // the native token stream object; its `$this->tokens` / - // `$this->position` state is inert in native mode anyway, so we - // pass an empty array to satisfy the parent contract and keep - // the actual tokens on the native parser. - parent::__construct( $grammar, array() ); - $this->native = new WP_MySQL_Native_Parser( $grammar, $tokens ); - } - - /** - * @param array|WP_MySQL_Native_Token_Stream $tokens - */ - public function reset_tokens( $tokens ): void { - $this->native->reset_tokens( $tokens ); - } - - public function next_query(): bool { - return $this->native->next_query(); - } - - public function get_query_ast(): ?WP_Parser_Node { - return $this->native->get_query_ast(); - } - - public function parse() { - return $this->native->parse(); - } -} diff --git a/packages/mysql-on-sqlite/src/parser/class-wp-parser-grammar.php b/packages/mysql-on-sqlite/src/parser/class-wp-parser-grammar.php deleted file mode 100644 index 9bf30b973..000000000 --- a/packages/mysql-on-sqlite/src/parser/class-wp-parser-grammar.php +++ /dev/null @@ -1,140 +0,0 @@ -inflate( $rules ); - } - - public function get_rule_name( $rule_id ) { - return $this->rule_names[ $rule_id ]; - } - - public function get_rule_id( $rule_name ) { - return array_search( $rule_name, $this->rule_names, true ); - } - - /** - * Inflate the grammar to an internal representation optimized for parsing. - * - * The input grammar is a compressed PHP array to minimize the file size. - * Every rule and token in the compressed grammar is encoded as an integer. - */ - private function inflate( $grammar ) { - $this->lowest_non_terminal_id = $grammar['rules_offset']; - $this->highest_terminal_id = $this->lowest_non_terminal_id - 1; - - foreach ( $grammar['rules_names'] as $rule_index => $rule_name ) { - $this->rule_names[ $rule_index + $grammar['rules_offset'] ] = $rule_name; - $this->rules[ $rule_index + $grammar['rules_offset'] ] = array(); - - /** - * Treat all intermediate rules as fragments to inline before returning - * the final parse tree to the API consumer. - * - * The original grammar was too difficult to parse with rules like: - * - * query ::= EOF | ((simpleStatement | beginWork) ((SEMICOLON_SYMBOL EOF?) | EOF)) - * - * We've factored rule fragments, such as `EOF?`, into separate rules, such as `%EOF_zero_or_one`. - * This is super useful for parsing, but it limits the API consumer's ability to - * reason about the parse tree. - * - * Fragments are intermediate rules that are not part of the original grammar. - * They are prefixed with a "%" to be distinguished from the original rules. - */ - if ( '%' === $rule_name[0] ) { - $this->fragment_ids[ $rule_index + $grammar['rules_offset'] ] = true; - } - } - - $this->rules = array(); - foreach ( $grammar['grammar'] as $rule_index => $branches ) { - $rule_id = $rule_index + $grammar['rules_offset']; - $this->rules[ $rule_id ] = $branches; - } - - /** - * Compute a rule => [token => true] lookup table for each rule - * that starts with a terminal OR with another rule that already - * has a lookahead mapping. - * - * This is similar to left-factoring the grammar, even if not quite - * the same. - * - * This enables us to quickly bail out from checking branches that - * cannot possibly match the current token. This increased the parser - * speed by a whopping 80%! - * - * @TODO: Explore these possible next steps: - * - * * Compute a rule => [token => branch[]] list lookup table and only - * process the branches that have a chance of matching the current token. - * * Actually left-factor the grammar as much as possible. This, however, - * could inflate the serialized grammar size. - */ - // 5 iterations seem to give us all the speed gains we can get from this. - for ( $i = 0; $i < 5; $i++ ) { - foreach ( $grammar['grammar'] as $rule_index => $branches ) { - $rule_id = $rule_index + $grammar['rules_offset']; - if ( isset( $this->lookahead_is_match_possible[ $rule_id ] ) ) { - continue; - } - $rule_lookup = array(); - $first_symbol_can_be_expanded_to_all_terminals = true; - foreach ( $branches as $branch ) { - $terminals = false; - $branch_starts_with_terminal = $branch[0] < $this->lowest_non_terminal_id; - if ( $branch_starts_with_terminal ) { - $terminals = array( $branch[0] ); - } elseif ( isset( $this->lookahead_is_match_possible[ $branch[0] ] ) ) { - $terminals = array_keys( $this->lookahead_is_match_possible[ $branch[0] ] ); - } - - if ( false === $terminals ) { - $first_symbol_can_be_expanded_to_all_terminals = false; - break; - } - foreach ( $terminals as $terminal ) { - $rule_lookup[ $terminal ] = true; - } - } - if ( $first_symbol_can_be_expanded_to_all_terminals ) { - $this->lookahead_is_match_possible[ $rule_id ] = $rule_lookup; - } - } - } - } -} diff --git a/packages/mysql-on-sqlite/src/parser/class-wp-parser-node.php b/packages/mysql-on-sqlite/src/parser/class-wp-parser-node.php deleted file mode 100644 index b61f38d5e..000000000 --- a/packages/mysql-on-sqlite/src/parser/class-wp-parser-node.php +++ /dev/null @@ -1,384 +0,0 @@ -rule_id = $rule_id; - $this->rule_name = $rule_name; - } - - public function append_child( $node ) { - $this->children[] = $node; - } - - /** - * Flatten the matched rule fragments as if their children were direct - * descendants of the current rule. - * - * What are rule fragments? - * - * When we initially parse the grammar file, it has compound rules such - * as this one: - * - * query ::= EOF | ((simpleStatement | beginWork) ((SEMICOLON_SYMBOL EOF?) | EOF)) - * - * Building a parser that can understand such rules is way more complex than building - * a parser that only follows simple rules, so we flatten those compound rules into - * simpler ones. The above rule would be flattened to: - * - * query ::= EOF | %query0 - * %query0 ::= %%query01 %%query02 - * %%query01 ::= simpleStatement | beginWork - * %%query02 ::= SEMICOLON_SYMBOL EOF_zero_or_one | EOF - * EOF_zero_or_one ::= EOF | ε - * - * This factorization happens in "convert-grammar.php". - * - * "Fragments" are intermediate artifacts whose names are not in the original grammar. - * They are extremely useful for the parser, but the API consumer should never have to - * worry about them. Fragment names start with a percent sign ("%"). - * - * The code below inlines every fragment back in its parent rule. - * - * We could optimize this. The current $match may be discarded later on so any inlining - * effort here would be wasted. However, inlining seems cheap and doing it bottom-up here - * is **much** easier than reprocessing the parse tree top-down later on. - * - * The following parse tree: - * - * [ - * 'query' => [ - * [ - * '%query01' => [ - * [ - * 'simpleStatement' => [ - * MySQLToken(MySQLLexer::WITH_SYMBOL, 'WITH') - * ], - * '%query02' => [ - * [ - * 'simpleStatement' => [ - * MySQLToken(MySQLLexer::WITH_SYMBOL, 'WITH') - * ] - * ], - * ] - * ] - * ] - * ] - * ] - * - * Would be inlined as: - * - * [ - * 'query' => [ - * [ - * 'simpleStatement' => [ - * MySQLToken(MySQLLexer::WITH_SYMBOL, 'WITH') - * ] - * ], - * [ - * 'simpleStatement' => [ - * MySQLToken(MySQLLexer::WITH_SYMBOL, 'WITH') - * ] - * ] - * ] - * ] - */ - public function merge_fragment( $node ) { - $this->children = array_merge( $this->children, $node->children ); - } - - /** - * Check if this node has any child nodes or tokens. - * - * @return bool True if this node has any child nodes or tokens, false otherwise. - */ - public function has_child(): bool { - return count( $this->children ) > 0; - } - - /** - * Check if this node has any child nodes. - * - * @param string|null $rule_name Optional. A node rule name to check for. - * @return bool True if any child nodes are found, false otherwise. - */ - public function has_child_node( ?string $rule_name = null ): bool { - foreach ( $this->children as $child ) { - if ( - $child instanceof WP_Parser_Node - && ( null === $rule_name || $child->rule_name === $rule_name ) - ) { - return true; - } - } - return false; - } - - /** - * Check if this node has any child tokens. - * - * @param int|null $token_id Optional. A token ID to check for. - * @return bool True if any child tokens are found, false otherwise. - */ - public function has_child_token( ?int $token_id = null ): bool { - foreach ( $this->children as $child ) { - if ( - $child instanceof WP_Parser_Token - && ( null === $token_id || $child->id === $token_id ) - ) { - return true; - } - } - return false; - } - - /** - * Get the first child node or token of this node. - * - * @return WP_Parser_Node|WP_Parser_Token|null The first child node or token; - * null when no children are found. - */ - public function get_first_child() { - return $this->children[0] ?? null; - } - - /** - * Get the first child node of this node. - * - * @param string|null $rule_name Optional. A node rule name to check for. - * @return WP_Parser_Node|null The first matching child node; null when no children are found. - */ - public function get_first_child_node( ?string $rule_name = null ): ?WP_Parser_Node { - foreach ( $this->children as $child ) { - if ( - $child instanceof WP_Parser_Node - && ( null === $rule_name || $child->rule_name === $rule_name ) - ) { - return $child; - } - } - return null; - } - - /** - * Get the first child token of this node. - * - * @param int|null $token_id Optional. A token ID to check for. - * @return WP_Parser_Token|null The first matching child token; null when no children are found. - */ - public function get_first_child_token( ?int $token_id = null ): ?WP_Parser_Token { - foreach ( $this->children as $child ) { - if ( - $child instanceof WP_Parser_Token - && ( null === $token_id || $child->id === $token_id ) - ) { - return $child; - } - } - return null; - } - - /** - * Get the first descendant node of this node. - * - * The node children are traversed recursively in a depth-first order until - * a matching descendant node is found, or the entire subtree is searched. - * - * @param string|null $rule_name Optional. A node rule name to check for. - * @return WP_Parser_Node|null The first matching descendant node; null when no descendants are found. - */ - public function get_first_descendant_node( ?string $rule_name = null ): ?WP_Parser_Node { - for ( $i = 0; $i < count( $this->children ); $i++ ) { - $child = $this->children[ $i ]; - if ( ! $child instanceof WP_Parser_Node ) { - continue; - } - if ( null === $rule_name || $child->rule_name === $rule_name ) { - return $child; - } - $node = $child->get_first_descendant_node( $rule_name ); - if ( $node ) { - return $node; - } - } - return null; - } - - /** - * Get the first descendant token of this node. - * - * The node children are traversed recursively in a depth-first order until - * a matching descendant token is found, or the entire subtree is searched. - * - * @param int|null $token_id Optional. A token ID to check for. - * @return WP_Parser_Token|null The first matching descendant token; null when no descendants are found. - */ - public function get_first_descendant_token( ?int $token_id = null ): ?WP_Parser_Token { - for ( $i = 0; $i < count( $this->children ); $i++ ) { - $child = $this->children[ $i ]; - if ( $child instanceof WP_Parser_Token ) { - if ( null === $token_id || $child->id === $token_id ) { - return $child; - } - } else { - $token = $child->get_first_descendant_token( $token_id ); - if ( $token ) { - return $token; - } - } - } - return null; - } - - /** - * Get all children of this node. - * - * @return array An array of all child nodes and tokens of this node. - */ - public function get_children(): array { - return $this->children; - } - - /** - * Get all child nodes of this node. - * - * @param string|null $rule_name Optional. A node rule name to check for. - * @return WP_Parser_Node[] An array of all matching child nodes. - */ - public function get_child_nodes( ?string $rule_name = null ): array { - $nodes = array(); - foreach ( $this->children as $child ) { - if ( - $child instanceof WP_Parser_Node - && ( null === $rule_name || $child->rule_name === $rule_name ) - ) { - $nodes[] = $child; - } - } - return $nodes; - } - - /** - * Get all child tokens of this node. - * - * @param int|null $token_id Optional. A token ID to check for. - * @return WP_Parser_Token[] An array of all matching child tokens. - */ - public function get_child_tokens( ?int $token_id = null ): array { - $tokens = array(); - foreach ( $this->children as $child ) { - if ( - $child instanceof WP_Parser_Token - && ( null === $token_id || $child->id === $token_id ) - ) { - $tokens[] = $child; - } - } - return $tokens; - } - - /** - * Get all descendants of this node. - * - * The descendants are collected using a depth-first pre-order NLR traversal. - * This produces a natural ordering that corresponds to the original input. - * - * @return array An array of all descendant nodes and tokens of this node. - */ - public function get_descendants(): array { - $descendants = array(); - foreach ( $this->children as $child ) { - if ( $child instanceof WP_Parser_Node ) { - $descendants[] = $child; - $descendants = array_merge( $descendants, $child->get_descendants() ); - } else { - $descendants[] = $child; - } - } - return $descendants; - } - - /** - * Get all descendant nodes of this node. - * - * The descendants are collected using a depth-first pre-order NLR traversal. - * This produces a natural ordering that corresponds to the original input. - * All matching nodes are collected during the traversal. - * - * @param string|null $rule_name Optional. A node rule name to check for. - * @return WP_Parser_Node[] An array of all matching descendant nodes. - */ - public function get_descendant_nodes( ?string $rule_name = null ): array { - $nodes = array(); - foreach ( $this->children as $child ) { - if ( ! $child instanceof WP_Parser_Node ) { - continue; - } - if ( null === $rule_name || $child->rule_name === $rule_name ) { - $nodes[] = $child; - } - $nodes = array_merge( $nodes, $child->get_descendant_nodes( $rule_name ) ); - } - return $nodes; - } - - /** - * Get all descendant tokens of this node. - * - * The descendants are collected using a depth-first pre-order NLR traversal. - * This produces a natural ordering that corresponds to the original input. - * All matching tokens are collected during the traversal. - * - * @param int|null $token_id Optional. A token ID to check for. - * @return WP_Parser_Token[] An array of all matching descendant tokens. - */ - public function get_descendant_tokens( ?int $token_id = null ): array { - $tokens = array(); - foreach ( $this->children as $child ) { - if ( $child instanceof WP_Parser_Token ) { - if ( null === $token_id || $child->id === $token_id ) { - $tokens[] = $child; - } - } else { - $tokens = array_merge( $tokens, $child->get_descendant_tokens( $token_id ) ); - } - } - return $tokens; - } - - /** - * Get the byte offset in the input string where this node begins. - * - * @return int The byte offset in the input string where this node begins. - */ - public function get_start(): int { - return $this->get_first_descendant_token()->start; - } - - /** - * Get the byte length of this node in the input string. - * - * @return int The byte length of this node in the input string. - */ - public function get_length(): int { - $tokens = $this->get_descendant_tokens(); - $first_token = $tokens[0]; - $last_token = $tokens[ count( $tokens ) - 1 ]; - return $last_token->start + $last_token->length - $first_token->start; - } -} diff --git a/packages/mysql-on-sqlite/src/parser/class-wp-parser-token.php b/packages/mysql-on-sqlite/src/parser/class-wp-parser-token.php deleted file mode 100644 index 4132ba382..000000000 --- a/packages/mysql-on-sqlite/src/parser/class-wp-parser-token.php +++ /dev/null @@ -1,77 +0,0 @@ -id = $id; - $this->start = $start; - $this->length = $length; - $this->input = $input; - } - - /** - * Get the raw bytes of the token from the input. - * - * @return string The token bytes. - */ - public function get_bytes(): string { - return substr( $this->input, $this->start, $this->length ); - } - - /** - * Get the real unquoted value of the token. - * - * @return string The token value. - */ - public function get_value(): string { - return $this->get_bytes(); - } -} diff --git a/packages/mysql-on-sqlite/src/parser/class-wp-parser.php b/packages/mysql-on-sqlite/src/parser/class-wp-parser.php deleted file mode 100644 index 4436892fa..000000000 --- a/packages/mysql-on-sqlite/src/parser/class-wp-parser.php +++ /dev/null @@ -1,124 +0,0 @@ -grammar = $grammar; - $this->tokens = $tokens; - $this->position = 0; - } - - public function parse() { - // @TODO: Make the starting rule lookup non-grammar-specific. - $query_rule_id = $this->grammar->get_rule_id( 'query' ); - $ast = $this->parse_recursive( $query_rule_id ); - return false === $ast ? null : $ast; - } - - private function parse_recursive( $rule_id ) { - $is_terminal = $rule_id <= $this->grammar->highest_terminal_id; - if ( $is_terminal ) { - if ( $this->position >= count( $this->tokens ) ) { - return false; - } - - if ( WP_Parser_Grammar::EMPTY_RULE_ID === $rule_id ) { - return true; - } - - if ( $this->tokens[ $this->position ]->id === $rule_id ) { - ++$this->position; - return $this->tokens[ $this->position - 1 ]; - } - return false; - } - - $branches = $this->grammar->rules[ $rule_id ]; - if ( ! count( $branches ) ) { - return false; - } - - // Bale out from processing the current branch if none of its rules can - // possibly match the current token. - if ( isset( $this->grammar->lookahead_is_match_possible[ $rule_id ] ) ) { - $token_id = $this->tokens[ $this->position ]->id; - if ( - ! isset( $this->grammar->lookahead_is_match_possible[ $rule_id ][ $token_id ] ) && - ! isset( $this->grammar->lookahead_is_match_possible[ $rule_id ][ WP_Parser_Grammar::EMPTY_RULE_ID ] ) - ) { - return false; - } - } - - $rule_name = $this->grammar->rule_names[ $rule_id ]; - $starting_position = $this->position; - foreach ( $branches as $branch ) { - $this->position = $starting_position; - $node = new WP_Parser_Node( $rule_id, $rule_name ); - $branch_matches = true; - foreach ( $branch as $subrule_id ) { - $subnode = $this->parse_recursive( $subrule_id ); - if ( false === $subnode ) { - $branch_matches = false; - break; - } elseif ( true === $subnode ) { - /* - * The subrule was matched without actually matching a token. - * This means a special empty "ε" (epsilon) rule was matched. - * An "ε" rule in a grammar matches an empty input of 0 bytes. - * It is used to represent optional grammar productions. - */ - continue; - } elseif ( is_array( $subnode ) && 0 === count( $subnode ) ) { - continue; - } - if ( is_array( $subnode ) && ! count( $subnode ) ) { - continue; - } - if ( isset( $this->grammar->fragment_ids[ $subrule_id ] ) ) { - $node->merge_fragment( $subnode ); - } else { - $node->append_child( $subnode ); - } - } - - // Negative lookahead for INTO after a valid SELECT statement. - // If we match a SELECT statement, but there is an INTO keyword after it, - // we're in the wrong branch and need to leave matching to a later rule. - // @TODO: Extract this to the "WP_MySQL_Parser" class, or add support - // for right-associative rules, which could solve this. - // See: https://github.com/mysql/mysql-workbench/blob/8.0.38/library/parsers/grammars/MySQLParser.g4#L994 - // See: https://github.com/antlr/antlr4/issues/488 - $la = $this->tokens[ $this->position ] ?? null; - if ( $la && 'selectStatement' === $rule_name && WP_MySQL_Lexer::INTO_SYMBOL === $la->id ) { - $branch_matches = false; - } - - if ( true === $branch_matches ) { - break; - } - } - - if ( ! $branch_matches ) { - $this->position = $starting_position; - return false; - } - - if ( ! $node->has_child() ) { - return true; - } - - return $node; - } -} diff --git a/packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php b/packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php index 7130fb631..04a9361b2 100644 --- a/packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php +++ b/packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php @@ -16,9 +16,22 @@ */ class WP_PDO_MySQL_On_SQLite extends PDO { /** - * The path to the MySQL SQL grammar file. - */ - const MYSQL_GRAMMAR_PATH = __DIR__ . '/../mysql/mysql-grammar.php'; + * Grammar rules of transaction and locking statements. + * + * These are the "simple_statement" alternatives that control transactions + * and locking ("BEGIN" is handled separately, as it is not part of the + * "simple_statement" rule). + */ + const TRANSACTION_OR_LOCKING_STATEMENTS = array( + 'start', + 'commit', + 'rollback', + 'savepoint', + 'release', + 'lock', + 'unlock', + 'xa', + ); /** * The minimum required version of SQLite. @@ -58,59 +71,59 @@ class WP_PDO_MySQL_On_SQLite extends PDO { */ const DATA_TYPE_MAP = array( // Numeric data types: - WP_MySQL_Lexer::BIT_SYMBOL => 'INTEGER', - WP_MySQL_Lexer::BOOL_SYMBOL => 'INTEGER', - WP_MySQL_Lexer::BOOLEAN_SYMBOL => 'INTEGER', - WP_MySQL_Lexer::TINYINT_SYMBOL => 'INTEGER', - WP_MySQL_Lexer::SMALLINT_SYMBOL => 'INTEGER', - WP_MySQL_Lexer::MEDIUMINT_SYMBOL => 'INTEGER', - WP_MySQL_Lexer::INT_SYMBOL => 'INTEGER', - WP_MySQL_Lexer::INTEGER_SYMBOL => 'INTEGER', - WP_MySQL_Lexer::BIGINT_SYMBOL => 'INTEGER', - WP_MySQL_Lexer::FLOAT_SYMBOL => 'REAL', - WP_MySQL_Lexer::DOUBLE_SYMBOL => 'REAL', - WP_MySQL_Lexer::REAL_SYMBOL => 'REAL', - WP_MySQL_Lexer::DECIMAL_SYMBOL => 'REAL', - WP_MySQL_Lexer::DEC_SYMBOL => 'REAL', - WP_MySQL_Lexer::FIXED_SYMBOL => 'REAL', - WP_MySQL_Lexer::NUMERIC_SYMBOL => 'REAL', + WP_MySQL_Tokens::KEYWORDS['BIT'] => 'INTEGER', + WP_MySQL_Tokens::KEYWORDS['BOOL'] => 'INTEGER', + WP_MySQL_Tokens::KEYWORDS['BOOLEAN'] => 'INTEGER', + WP_MySQL_Tokens::KEYWORDS['TINYINT'] => 'INTEGER', + WP_MySQL_Tokens::KEYWORDS['SMALLINT'] => 'INTEGER', + WP_MySQL_Tokens::KEYWORDS['MEDIUMINT'] => 'INTEGER', + WP_MySQL_Tokens::KEYWORDS['INT'] => 'INTEGER', + WP_MySQL_Tokens::KEYWORDS['INTEGER'] => 'INTEGER', + WP_MySQL_Tokens::KEYWORDS['BIGINT'] => 'INTEGER', + WP_MySQL_Tokens::KEYWORDS['FLOAT'] => 'REAL', + WP_MySQL_Tokens::KEYWORDS['DOUBLE'] => 'REAL', + WP_MySQL_Tokens::KEYWORDS['REAL'] => 'REAL', + WP_MySQL_Tokens::KEYWORDS['DECIMAL'] => 'REAL', + WP_MySQL_Tokens::KEYWORDS['DEC'] => 'REAL', + WP_MySQL_Tokens::KEYWORDS['FIXED'] => 'REAL', + WP_MySQL_Tokens::KEYWORDS['NUMERIC'] => 'REAL', // String data types: - WP_MySQL_Lexer::CHAR_SYMBOL => 'TEXT', - WP_MySQL_Lexer::VARCHAR_SYMBOL => 'TEXT', - WP_MySQL_Lexer::NCHAR_SYMBOL => 'TEXT', - WP_MySQL_Lexer::NVARCHAR_SYMBOL => 'TEXT', - WP_MySQL_Lexer::TINYTEXT_SYMBOL => 'TEXT', - WP_MySQL_Lexer::TEXT_SYMBOL => 'TEXT', - WP_MySQL_Lexer::MEDIUMTEXT_SYMBOL => 'TEXT', - WP_MySQL_Lexer::LONGTEXT_SYMBOL => 'TEXT', - WP_MySQL_Lexer::ENUM_SYMBOL => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['CHAR'] => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['VARCHAR'] => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['NCHAR'] => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['NVARCHAR'] => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['TINYTEXT'] => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['TEXT'] => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['MEDIUMTEXT'] => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['LONGTEXT'] => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['ENUM'] => 'TEXT', // Date and time data types: - WP_MySQL_Lexer::DATE_SYMBOL => 'TEXT', - WP_MySQL_Lexer::TIME_SYMBOL => 'TEXT', - WP_MySQL_Lexer::DATETIME_SYMBOL => 'TEXT', - WP_MySQL_Lexer::TIMESTAMP_SYMBOL => 'TEXT', - WP_MySQL_Lexer::YEAR_SYMBOL => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['DATE'] => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['TIME'] => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['DATETIME'] => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['TIMESTAMP'] => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['YEAR'] => 'TEXT', // Binary data types: - WP_MySQL_Lexer::BINARY_SYMBOL => 'BLOB', - WP_MySQL_Lexer::VARBINARY_SYMBOL => 'BLOB', - WP_MySQL_Lexer::TINYBLOB_SYMBOL => 'BLOB', - WP_MySQL_Lexer::BLOB_SYMBOL => 'BLOB', - WP_MySQL_Lexer::MEDIUMBLOB_SYMBOL => 'BLOB', - WP_MySQL_Lexer::LONGBLOB_SYMBOL => 'BLOB', + WP_MySQL_Tokens::KEYWORDS['BINARY'] => 'BLOB', + WP_MySQL_Tokens::KEYWORDS['VARBINARY'] => 'BLOB', + WP_MySQL_Tokens::KEYWORDS['TINYBLOB'] => 'BLOB', + WP_MySQL_Tokens::KEYWORDS['BLOB'] => 'BLOB', + WP_MySQL_Tokens::KEYWORDS['MEDIUMBLOB'] => 'BLOB', + WP_MySQL_Tokens::KEYWORDS['LONGBLOB'] => 'BLOB', // Spatial data types: - WP_MySQL_Lexer::GEOMETRY_SYMBOL => 'TEXT', - WP_MySQL_Lexer::POINT_SYMBOL => 'TEXT', - WP_MySQL_Lexer::LINESTRING_SYMBOL => 'TEXT', - WP_MySQL_Lexer::POLYGON_SYMBOL => 'TEXT', - WP_MySQL_Lexer::MULTIPOINT_SYMBOL => 'TEXT', - WP_MySQL_Lexer::MULTILINESTRING_SYMBOL => 'TEXT', - WP_MySQL_Lexer::MULTIPOLYGON_SYMBOL => 'TEXT', - WP_MySQL_Lexer::GEOMCOLLECTION_SYMBOL => 'TEXT', - WP_MySQL_Lexer::GEOMETRYCOLLECTION_SYMBOL => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['GEOMETRY'] => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['POINT'] => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['LINESTRING'] => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['POLYGON'] => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['MULTIPOINT'] => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['MULTILINESTRING'] => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['MULTIPOLYGON'] => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['GEOMCOLLECTION'] => 'TEXT', + WP_MySQL_Tokens::KEYWORDS['GEOMETRYCOLLECTION'] => 'TEXT', // SERIAL, SET, and JSON types are handled in the translation process. ); @@ -404,18 +417,14 @@ class WP_PDO_MySQL_On_SQLite extends PDO { public $client_info; /** - * A MySQL query parser grammar. + * A shared MySQL parser instance. * - * @var WP_Parser_Grammar - */ - private static $mysql_grammar; - - /** - * A reusable parser instance for MySQL queries. + * The parser is stateless across parses, so a single instance holding the + * materialized parse tables is shared by all driver instances. * - * @var WP_MySQL_Parser|null + * @var WP_MySQL_Parser */ - private $mysql_parser = null; + private static $mysql_parser; /** * The main database name. @@ -740,9 +749,9 @@ public function __construct( // Register SQLite functions. $this->user_defined_functions = WP_SQLite_PDO_User_Defined_Functions::register_for( $this->connection->get_pdo() ); - // Load MySQL grammar. - if ( null === self::$mysql_grammar ) { - self::$mysql_grammar = new WP_Parser_Grammar( require self::MYSQL_GRAMMAR_PATH ); + // Load the MySQL parser. + if ( null === self::$mysql_parser ) { + self::$mysql_parser = new WP_MySQL_Parser( require WP_MySQL_Parser::PARSE_TABLE_PATH ); } // Initialize information schema builder. @@ -873,55 +882,46 @@ public function query( string $query, ?int $fetch_mode = null, ...$fetch_mode_ar try { // Parse the MySQL query. - $parser = $this->create_parser( $query ); - $parser->next_query(); - $ast = $parser->get_query_ast(); - if ( null === $ast ) { + $asts = $this->parse_mysql_query( $query ); + if ( null === $asts ) { throw $this->new_driver_exception( 'Failed to parse the MySQL query.' ); } - if ( $parser->next_query() ) { + if ( count( $asts ) > 1 ) { throw $this->new_driver_exception( 'Multi-query is not supported.' ); } + $ast = $asts[0]; /* * Determine if we need to wrap the translated queries in a transaction. * * [GRAMMAR] - * query: - * EOF - * | (simpleStatement | beginWork) (SEMICOLON_SYMBOL EOF? | EOF) + * sql_statement: END_OF_INPUT | simple_statement_or_begin (';' opt_end_of_input | END_OF_INPUT) + * simple_statement_or_begin: simple_statement | begin_stmt */ - $child_node = $ast->get_first_child_node(); + $child_node = $ast->get_first_child_node(); + $statement_node = null; + if ( null !== $child_node && 'simple_statement' === $child_node->get_first_child_node()->rule_name ) { + $statement_node = $child_node->get_first_child_node()->get_first_child_node(); + } if ( - null === $child_node - || 'beginWork' === $child_node->rule_name - || $child_node->has_child_node( 'transactionOrLockingStatement' ) - || $child_node->has_child_node( 'selectStatement' ) + null === $statement_node + || 'select_stmt' === $statement_node->rule_name + || in_array( $statement_node->rule_name, self::TRANSACTION_OR_LOCKING_STATEMENTS, true ) ) { $wrap_in_transaction = false; } else { $wrap_in_transaction = true; } - /* - * Detect read-only statements before opening the wrapper transaction. - * - * [GRAMMAR] - * simpleStatement: selectStatement | showStatement | utilityStatement | ... - */ - if ( null !== $child_node && $child_node->has_child_node() ) { - $statement_node = $child_node->get_first_child_node(); + // Detect read-only statements before opening the wrapper transaction. + if ( null !== $statement_node ) { if ( - 'selectStatement' === $statement_node->rule_name - || 'showStatement' === $statement_node->rule_name + 'select_stmt' === $statement_node->rule_name + || str_starts_with( $statement_node->rule_name, 'show_' ) + || 'describe_stmt' === $statement_node->rule_name ) { $this->is_readonly = true; - } elseif ( 'utilityStatement' === $statement_node->rule_name ) { - $utility_subnode = $statement_node->get_first_child_node(); - if ( null !== $utility_subnode && 'describeStatement' === $utility_subnode->rule_name ) { - $this->is_readonly = true; - } } } @@ -1161,37 +1161,108 @@ public function get_insert_id() { } /** - * Tokenize a MySQL query and initialize a parser. + * Parse a MySQL query string into a list of per-statement ASTs. * - * @param string $query The MySQL query to parse. - * @return WP_MySQL_Parser A parser initialized for the MySQL query. + * @param string $query The MySQL query string (possibly multi-statement). + * @return WP_Parser_Node[]|null One "sql_statement" AST per statement, or + * null when a statement fails to parse. */ - public function create_parser( string $query ): WP_MySQL_Parser { - $lexer = new WP_MySQL_Lexer( - $query, - 80038, - $this->active_sql_modes - ); - $tokens = $lexer instanceof WP_MySQL_Native_Lexer - ? $lexer->native_token_stream() - : $lexer->remaining_tokens(); - return $this->reset_or_create_parser( $tokens ); + public function parse_mysql_query( string $query ): ?array { + $asts = $this->parse_mysql_query_with_sql_modes( $query, $this->active_sql_modes ); + if ( null !== $asts ) { + return $asts; + } + + /* + * Retry with the ANSI_QUOTES SQL mode. + * + * Without ANSI_QUOTES, a double-quoted string is a string literal, so + * it is invalid in identifier positions (e.g., ADD INDEX "name" (...)). + * MySQL rejects such statements, but WordPress relies on them (dbDelta + * can produce double-quoted index names). Retrying with ANSI_QUOTES + * accepts these statements, while preserving the MySQL string-literal + * semantics for every query that parses without it. + */ + if ( + ! in_array( 'ANSI_QUOTES', $this->active_sql_modes, true ) + && str_contains( $query, '"' ) + ) { + return $this->parse_mysql_query_with_sql_modes( + $query, + array_merge( $this->active_sql_modes, array( 'ANSI_QUOTES' ) ) + ); + } + return null; } /** - * Reset the reusable parser with new tokens or create it on first use. + * Parse a MySQL query string using a specific set of SQL modes. + * + * The MySQL grammar parses a single SQL statement, so the token stream is + * split on the top-level ';' separators and each statement is parsed on + * its own, mirroring how MySQL clients split multi-statement input. * - * @param array|object $tokens Parser tokens. - * @return WP_MySQL_Parser A parser initialized for the token stream. + * @param string $query The MySQL query string (possibly multi-statement). + * @param string[] $sql_modes The SQL modes to use for tokenization. + * @return WP_Parser_Node[]|null One "sql_statement" AST per statement, or + * null when a statement fails to parse. */ - private function reset_or_create_parser( $tokens ): WP_MySQL_Parser { - if ( null === $this->mysql_parser || ! method_exists( $this->mysql_parser, 'reset_tokens' ) ) { - $this->mysql_parser = new WP_MySQL_Parser( self::$mysql_grammar, $tokens ); - } else { - $this->mysql_parser->reset_tokens( $tokens ); + private function parse_mysql_query_with_sql_modes( string $query, array $sql_modes ): ?array { + $lexer = new WP_MySQL_Lexer( + $query, + 80038, + $sql_modes + ); + $tokens = $lexer->remaining_tokens(); + + /* + * Split the token stream into statements at the ';' separators. The + * separator can only ever occur at the top level of a statement, as + * string and comment contents are enclosed in single tokens, and the + * driver doesn't support compound statements (stored programs). + * + * A valid stream is terminated by an END_OF_INPUT and an END_MARKER + * token; the terminators are stripped here and re-appended to each + * single-statement token list. For invalid input, the lexer emits a + * partial stream with no terminators, which is a parse error. + */ + $end_of_input = null; + $end_marker = null; + $statements = array(); + $statement = array(); + foreach ( $tokens as $token ) { + if ( WP_MySQL_Tokens::END_OF_INPUT === $token->id ) { + $end_of_input = $token; + } elseif ( WP_MySQL_Tokens::END_MARKER === $token->id ) { + $end_marker = $token; + } else { + $statement[] = $token; + if ( WP_MySQL_Tokens::SEMICOLON_SYMBOL === $token->id ) { + $statements[] = $statement; + $statement = array(); + } + } + } + if ( null === $end_of_input || null === $end_marker ) { + return null; + } + if ( count( $statement ) > 0 || 0 === count( $statements ) ) { + $statements[] = $statement; } - return $this->mysql_parser; + $asts = array(); + foreach ( $statements as $statement ) { + $statement[] = $end_of_input; + $statement[] = $end_marker; + + // The root "start_entry" node wraps a single "sql_statement" node. + $ast = self::$mysql_parser->parse( $statement ); + if ( null === $ast ) { + return null; + } + $asts[] = $ast->get_first_child_node(); + } + return $asts; } /** @@ -1383,145 +1454,121 @@ public function execute_sqlite_query( string $sql, array $params = array() ): PD * @throws WP_SQLite_Driver_Exception When the query is not supported. */ private function execute_mysql_query( WP_Parser_Node $node ): void { - if ( 'query' !== $node->rule_name ) { + if ( 'sql_statement' !== $node->rule_name ) { throw $this->new_driver_exception( - sprintf( 'Expected "query" node, got: "%s"', $node->rule_name ) + sprintf( 'Expected "sql_statement" node, got: "%s"', $node->rule_name ) ); } /* * [GRAMMAR] - * query: - * EOF - * | (simpleStatement | beginWork) (SEMICOLON_SYMBOL EOF? | EOF) + * sql_statement: END_OF_INPUT | simple_statement_or_begin (';' opt_end_of_input | END_OF_INPUT) + * simple_statement_or_begin: simple_statement | begin_stmt */ - $children = $node->get_child_nodes(); + $children = $node->get_child_nodes( 'simple_statement_or_begin' ); if ( count( $children ) !== 1 ) { throw $this->new_driver_exception( sprintf( 'Expected 1 child node, got: %d', count( $children ) ) ); } - if ( 'beginWork' === $children[0]->rule_name ) { + $node = $children[0]->get_first_child_node(); + if ( 'begin_stmt' === $node->rule_name ) { $this->begin_user_transaction(); return; } - if ( 'simpleStatement' !== $children[0]->rule_name ) { + if ( 'simple_statement' !== $node->rule_name ) { throw $this->new_driver_exception( - sprintf( 'Expected "simpleStatement" node, got: "%s"', $children[0]->rule_name ) + sprintf( 'Expected "simple_statement" node, got: "%s"', $node->rule_name ) ); } - // Process the "simpleStatement" AST node. - $node = $children[0]->get_first_child_node(); + // Process the "simple_statement" AST node. + $node = $node->get_first_child_node(); switch ( $node->rule_name ) { - case 'transactionOrLockingStatement': + case 'start': + case 'commit': + case 'rollback': + case 'savepoint': + case 'release': + case 'lock': + case 'unlock': + case 'xa': $this->execute_transaction_or_locking_statement( $node ); break; - case 'selectStatement': + case 'select_stmt': $this->execute_select_statement( $node ); break; - case 'insertStatement': - case 'replaceStatement': + case 'insert_stmt': + case 'replace_stmt': $this->execute_insert_or_replace_statement( $node ); break; - case 'updateStatement': + case 'update_stmt': $this->execute_update_statement( $node ); break; - case 'deleteStatement': + case 'delete_stmt': $this->execute_delete_statement( $node ); break; - case 'createStatement': - $subtree = $node->get_first_child_node(); - switch ( $subtree->rule_name ) { - case 'createDatabase': - /* - * TODO: - * We could support this by creating a new SQLite database - * file (e.g., $slugified_db_name.sqlite). - * - * Alternatively, it could be a no-op, in combination with - * DROP DATABASE deleting the data file and recreating it. - */ - case 'createTable': - $this->execute_create_table_statement( $node ); - break; - case 'createIndex': - $this->execute_create_index_statement( $node ); - break; - default: - throw $this->new_not_supported_exception( - sprintf( - 'statement type: "%s" > "%s"', - $node->rule_name, - $subtree->rule_name - ) - ); - } + case 'create': + /* + * The "create" rule covers CREATE DATABASE and other CREATE + * statements without a dedicated grammar rule. + * + * TODO: + * We could support CREATE DATABASE by creating a new SQLite + * database file (e.g., $slugified_db_name.sqlite). + * + * Alternatively, it could be a no-op, in combination with + * DROP DATABASE deleting the data file and recreating it. + */ + throw $this->new_not_supported_exception( + sprintf( 'statement type: "%s"', $node->rule_name ) + ); + case 'create_table_stmt': + $this->execute_create_table_statement( $node ); break; - case 'alterStatement': - $subtree = $node->get_first_child_node(); - switch ( $subtree->rule_name ) { - case 'alterTable': - $this->execute_alter_table_statement( $node ); - break; - default: - throw $this->new_not_supported_exception( - sprintf( - 'statement type: "%s" > "%s"', - $node->rule_name, - $subtree->rule_name - ) - ); - } + case 'create_index_stmt': + $this->execute_create_index_statement( $node ); break; - case 'dropStatement': - $subtree = $node->get_first_child_node(); - switch ( $subtree->rule_name ) { - case 'dropTable': - $this->execute_drop_table_statement( $node ); - break; - case 'dropIndex': - $this->execute_drop_index_statement( $node ); - break; - default: - $query = $this->translate( $node ); - $this->last_result_statement = $this->execute_sqlite_query( $query ); - } + case 'alter_table_stmt': + $this->execute_alter_table_statement( $node ); + break; + case 'drop_table_stmt': + $this->execute_drop_table_statement( $node ); break; - case 'truncateTableStatement': + case 'drop_index_stmt': + $this->execute_drop_index_statement( $node ); + break; + case 'drop_database_stmt': + case 'drop_view_stmt': + case 'drop_trigger_stmt': + $query = $this->translate( $node ); + $this->last_result_statement = $this->execute_sqlite_query( $query ); + break; + case 'truncate_stmt': $this->execute_truncate_table_statement( $node ); break; - case 'setStatement': + case 'set': $this->execute_set_statement( $node ); break; - case 'showStatement': - $this->execute_show_statement( $node ); + case 'describe_stmt': + $this->execute_describe_statement( $node ); break; - case 'utilityStatement': - $subtree = $node->get_first_child_node(); - switch ( $subtree->rule_name ) { - case 'describeStatement': - $this->execute_describe_statement( $subtree ); - break; - case 'useCommand': - $this->execute_use_statement( $subtree ); - break; - default: - throw $this->new_not_supported_exception( - sprintf( - 'statement type: "%s" > "%s"', - $node->rule_name, - $subtree->rule_name - ) - ); - } + case 'use': + $this->execute_use_statement( $node ); break; - case 'tableAdministrationStatement': + case 'analyze_table_stmt': + case 'check_table_stmt': + case 'optimize_table_stmt': + case 'repair_table_stmt': $this->execute_administration_statement( $node ); break; default: + if ( str_starts_with( $node->rule_name, 'show_' ) ) { + $this->execute_show_statement( $node ); + break; + } throw $this->new_not_supported_exception( sprintf( 'statement type: "%s"', $node->rule_name ) ); @@ -1678,64 +1725,47 @@ private function rollback_user_transaction(): void { /** * Execute a MySQL transaction or locking statement in SQLite. * - * @param WP_Parser_Node $node The "transactionOrLockingStatement" AST node. + * @param WP_Parser_Node $node The transaction or locking statement AST node + * ("start", "commit", "rollback", "savepoint", + * "release", "lock", "unlock", or "xa"). * @throws WP_SQLite_Driver_Exception When the query execution fails. */ private function execute_transaction_or_locking_statement( WP_Parser_Node $node ): void { - $subnode = $node->get_first_child_node(); - $token = $node->get_first_descendant_token(); - - switch ( $subnode->rule_name ) { - case 'transactionStatement': + switch ( $node->rule_name ) { + case 'start': // START TRANSACTION. - if ( WP_MySQL_Lexer::START_SYMBOL === $token->id ) { - $this->begin_user_transaction(); - return; - } - + $this->begin_user_transaction(); + return; + case 'commit': // COMMIT. - if ( WP_MySQL_Lexer::COMMIT_SYMBOL === $token->id ) { - $this->commit_user_transaction(); - return; - } - - break; - case 'savepointStatement': - $savepoint_name = $this->translate( $subnode->get_first_child_node( 'identifier' ) ); - + $this->commit_user_transaction(); + return; + case 'rollback': // ROLLBACK/ROLLBACK TO SAVEPOINT . - if ( WP_MySQL_Lexer::ROLLBACK_SYMBOL === $token->id ) { - if ( null === $savepoint_name ) { - $this->rollback_user_transaction(); - } else { - $this->execute_sqlite_query( sprintf( 'ROLLBACK TO SAVEPOINT %s', $savepoint_name ) ); - } - return; - } - - // SAVEPOINT. - if ( WP_MySQL_Lexer::SAVEPOINT_SYMBOL === $token->id ) { - $this->execute_sqlite_query( sprintf( 'SAVEPOINT %s', $savepoint_name ) ); - return; - } - - // RELEASE SAVEPOINT. - if ( WP_MySQL_Lexer::RELEASE_SYMBOL === $token->id ) { - $this->execute_sqlite_query( sprintf( 'RELEASE SAVEPOINT %s', $savepoint_name ) ); - return; + $savepoint_name = $this->translate( $node->get_first_child_node( 'ident' ) ); + if ( null === $savepoint_name ) { + $this->rollback_user_transaction(); + } else { + $this->execute_sqlite_query( sprintf( 'ROLLBACK TO SAVEPOINT %s', $savepoint_name ) ); } - - break; - case 'lockStatement': + return; + case 'savepoint': + // SAVEPOINT . + $savepoint_name = $this->translate( $node->get_first_child_node( 'ident' ) ); + $this->execute_sqlite_query( sprintf( 'SAVEPOINT %s', $savepoint_name ) ); + return; + case 'release': + // RELEASE SAVEPOINT . + $savepoint_name = $this->translate( $node->get_first_child_node( 'ident' ) ); + $this->execute_sqlite_query( sprintf( 'RELEASE SAVEPOINT %s', $savepoint_name ) ); + return; + case 'lock': // LOCK TABLE/LOCK TABLES. - if ( - WP_MySQL_Lexer::LOCK_SYMBOL === $token->id - && $subnode->has_child_node( 'lockItem' ) - ) { + $lock_list = $node->get_first_child_node( 'table_lock_list' ); + if ( null !== $lock_list ) { // Check if the table(s) exists. - $lock_items = $subnode->get_child_nodes( 'lockItem' ); - foreach ( $lock_items as $lock_item ) { - $table_ref = $lock_item->get_first_child_node( 'tableRef' ); + foreach ( $lock_list->get_flattened_child_nodes( 'table_lock' ) as $lock_item ) { + $table_ref = $lock_item->get_first_child_node( 'table_ident' ); $database = $this->get_database_name( $table_ref ); $table_name = $this->unquote_sqlite_identifier( $this->translate( $table_ref ) ); if ( 'information_schema' === strtolower( $database ) ) { @@ -1764,14 +1794,10 @@ private function execute_transaction_or_locking_statement( WP_Parser_Node $node return; } + break; + case 'unlock': // UNLOCK TABLES/UNLOCK TABLE. - if ( - WP_MySQL_Lexer::UNLOCK_SYMBOL === $token->id - && ( - $subnode->has_child_token( WP_MySQL_Lexer::TABLE_SYMBOL ) - || $subnode->has_child_token( WP_MySQL_Lexer::TABLES_SYMBOL ) - ) - ) { + if ( $node->has_child_node( 'table_or_tables' ) ) { // Commit the transaction when created by the LOCK statement. if ( $this->table_lock_active && $this->inTransaction() ) { $this->commit_user_transaction(); @@ -1784,11 +1810,7 @@ private function execute_transaction_or_locking_statement( WP_Parser_Node $node } throw $this->new_not_supported_exception( - sprintf( - 'statement type: "%s" > "%s"', - $node->rule_name, - $subnode->rule_name - ) + sprintf( 'statement type: "%s"', $node->rule_name ) ); } @@ -1801,44 +1823,38 @@ private function execute_transaction_or_locking_statement( WP_Parser_Node $node private function execute_select_statement( WP_Parser_Node $node ): void { /* * [GRAMMAR] - * selectStatement: - * queryExpression lockingClauseList? - * | selectStatementWithInto + * select_stmt: + * query_expression + * | query_expression locking_clause_list + * | query_expression_parens + * | select_stmt_with_into */ // First, translate the query, before we modify last found rows count. $query = $this->translate( $node->get_first_child() ); $has_sql_calc_found_rows = null !== $node->get_first_descendant_token( - WP_MySQL_Lexer::SQL_CALC_FOUND_ROWS_SYMBOL + WP_MySQL_Tokens::KEYWORDS['SQL_CALC_FOUND_ROWS'] ); // Handle SQL_CALC_FOUND_ROWS. if ( true === $has_sql_calc_found_rows ) { // Recursively find a query expression with the first LIMIT or SELECT. - $query_expr = $node->get_first_descendant_node( 'queryExpression' ); + $query_expr = $node->get_first_descendant_node( 'query_expression' ); while ( true ) { - if ( $query_expr->has_child_node( 'limitClause' ) ) { + if ( $query_expr->has_child_node( 'opt_limit_clause' ) ) { break; } - $query_expr_parens = $query_expr->get_first_child_node( 'queryExpressionParens' ); - if ( null !== $query_expr_parens ) { - $query_expr = $query_expr_parens->get_first_child_node( 'queryExpression' ); - continue; - } - - $query_expr_body = $query_expr->get_first_child_node( 'queryExpressionBody' ); + $query_expr_body = $query_expr->get_first_child_node( 'query_expression_body' ); if ( count( $query_expr_body->get_children() ) > 1 ) { break; } - $query_term = $query_expr_body->get_first_child_node( 'queryTerm' ); - if ( - count( $query_term->get_children() ) === 1 - && $query_term->has_child_node( 'queryExpressionParens' ) - ) { - $query_expr = $query_term->get_first_child_node( 'queryExpressionParens' )->get_first_child_node( 'queryExpression' ); + // Descend into a parenthesized query expression, if any. + $query_expr_parens = $query_expr_body->get_first_child_node( 'query_expression_parens' ); + if ( null !== $query_expr_parens ) { + $query_expr = $query_expr_parens->get_first_descendant_node( 'query_expression' ); continue; } @@ -1848,7 +1864,7 @@ private function execute_select_statement( WP_Parser_Node $node ): void { // Exclude the limit clause from the expression. $count_expr = new WP_Parser_Node( $query_expr->rule_id, $query_expr->rule_name ); foreach ( $query_expr->get_children() as $child ) { - if ( ! ( $child instanceof WP_Parser_Node && 'limitClause' === $child->rule_name ) ) { + if ( ! ( $child instanceof WP_Parser_Node && 'opt_limit_clause' === $child->rule_name ) ) { $count_expr->append_child( $child ); } } @@ -1885,9 +1901,9 @@ private function execute_insert_or_replace_statement( WP_Parser_Node $node ): vo $is_token = $child instanceof WP_MySQL_Token; $is_node = $child instanceof WP_Parser_Node; - if ( $child instanceof WP_Parser_Node && 'tableRef' === $child->rule_name ) { + if ( $child instanceof WP_Parser_Node && 'table_ident' === $child->rule_name ) { // MySQL supports INSERT without the INTO keyword; SQLite requires it. - if ( ! $node->has_child_token( WP_MySQL_Lexer::INTO_SYMBOL ) ) { + if ( ! $node->has_child_node( 'opt_INTO' ) ) { $parts[] = 'INTO'; } @@ -1898,25 +1914,25 @@ private function execute_insert_or_replace_statement( WP_Parser_Node $node ): vo } // Skip the SET keyword in "INSERT INTO ... SET ..." syntax. - if ( $is_token && WP_MySQL_Lexer::SET_SYMBOL === $child->id ) { + if ( $is_token && WP_MySQL_Tokens::KEYWORDS['SET'] === $child->id ) { continue; } - if ( $is_token && WP_MySQL_Lexer::IGNORE_SYMBOL === $child->id ) { - // Translate "UPDATE IGNORE" to "UPDATE OR IGNORE". + if ( $is_node && 'opt_ignore' === $child->rule_name ) { + // Translate "INSERT IGNORE" to "INSERT OR IGNORE". $parts[] = 'OR IGNORE'; } elseif ( $is_node && ( - 'insertFromConstructor' === $child->rule_name - || 'insertQueryExpression' === $child->rule_name - || 'updateList' === $child->rule_name + 'insert_from_constructor' === $child->rule_name + || 'insert_query_expression' === $child->rule_name + || 'update_list' === $child->rule_name ) ) { - $table_ref = $node->get_first_child_node( 'tableRef' ); + $table_ref = $node->get_first_child_node( 'table_ident' ); $table_name = $this->unquote_sqlite_identifier( $this->translate( $table_ref ) ); $parts[] = $this->translate_insert_or_replace_body( $table_name, $child ); - } elseif ( $is_node && 'insertUpdateList' === $child->rule_name ) { + } elseif ( $is_node && 'opt_insert_update_list' === $child->rule_name ) { /* * Translate "ON DUPLICATE KEY UPDATE" to "ON CONFLICT DO UPDATE SET". * @@ -2009,8 +2025,8 @@ private function execute_update_statement( WP_Parser_Node $node ): void { // @TODO: Add support for UPDATE with multiple tables and JOINs. // SQLite supports them in the FROM clause. - $has_order = $node->has_child_node( 'orderClause' ); - $has_limit = $node->has_child_node( 'simpleLimitClause' ); + $has_order = $node->has_child_node( 'opt_order_clause' ); + $has_limit = $node->has_child_node( 'opt_simple_limit' ); /* * SQLite doesn't support UPDATE with ORDER BY/LIMIT. @@ -2025,10 +2041,10 @@ private function execute_update_statement( WP_Parser_Node $node ): void { if ( $has_order || $has_limit ) { $where_subquery = 'SELECT rowid FROM ' . $this->translate_sequence( array( - $node->get_first_child_node( 'tableReferenceList' ), - $node->get_first_child_node( 'whereClause' ), - $node->get_first_child_node( 'orderClause' ), - $node->get_first_child_node( 'simpleLimitClause' ), + $node->get_first_child_node( 'table_reference_list' ), + $node->get_first_child_node( 'opt_where_clause' ), + $node->get_first_child_node( 'opt_order_clause' ), + $node->get_first_child_node( 'opt_simple_limit' ), ) ); } @@ -2037,14 +2053,14 @@ private function execute_update_statement( WP_Parser_Node $node ): void { * Translate the UPDATE statement parts. * * [GRAMMAR] - * updateStatement: - * withClause? UPDATE_SYMBOL LOW_PRIORITY_SYMBOL? IGNORE_SYMBOL? tableReferenceList - * SET_SYMBOL updateList whereClause? orderClause? simpleLimitClause? + * update_stmt: + * opt_with_clause UPDATE_SYM opt_low_priority opt_ignore table_reference_list + * SET_SYM update_list opt_where_clause opt_order_clause opt_simple_limit */ // Collect all tables used in the UPDATE clause (e.g, UPDATE t1, t2 JOIN t3). $table_alias_map = $this->create_table_reference_map( - $node->get_first_child_node( 'tableReferenceList' ) + $node->get_first_child_node( 'table_reference_list' ) ); /* @@ -2063,13 +2079,13 @@ private function execute_update_statement( WP_Parser_Node $node ): void { } // Determine whether the UPDATE statement modifies multiple tables. - $update_list_node = $node->get_first_child_node( 'updateList' ); + $update_list_node = $node->get_first_child_node( 'update_list' ); $update_target = null; $updates_multiple_tables = false; if ( count( $table_alias_map ) > 1 ) { - foreach ( $update_list_node->get_child_nodes( 'updateElement' ) as $update_element ) { - $column_ref = $update_element->get_first_child_node( 'columnRef' ); - $column_ref_parts = $column_ref->get_descendant_nodes( 'identifier' ); + foreach ( $update_list_node->get_flattened_child_nodes( 'update_elem' ) as $update_element ) { + $column_ref = $update_element->get_first_child_node( 'simple_ident_nospvar' ); + $column_ref_parts = $column_ref->get_descendant_nodes( 'ident' ); $table_or_alias = count( $column_ref_parts ) > 1 ? $this->unquote_sqlite_identifier( $this->translate( $column_ref_parts[0] ) ) : null; @@ -2151,10 +2167,10 @@ private function execute_update_statement( WP_Parser_Node $node ): void { } // Translate WITH clause. - $with = $this->translate( $node->get_first_child_node( 'withClause' ) ); + $with = $this->translate( $node->get_first_child_node( 'opt_with_clause' ) ); // Translate "UPDATE IGNORE" to "UPDATE OR IGNORE". - $or_ignore = $node->has_child_token( WP_MySQL_Lexer::IGNORE_SYMBOL ) + $or_ignore = $node->has_child_node( 'opt_ignore' ) ? 'OR IGNORE' : null; @@ -2205,9 +2221,9 @@ private function execute_update_statement( WP_Parser_Node $node ): void { $order_clause = null; $limit_clause = null; } else { - $where_clause = $this->translate( $node->get_first_child_node( 'whereClause' ) ); - $order_clause = $this->translate( $node->get_first_child_node( 'orderClause' ) ); - $limit_clause = $this->translate( $node->get_first_child_node( 'simpleLimitClause' ) ); + $where_clause = $this->translate( $node->get_first_child_node( 'opt_where_clause' ) ); + $order_clause = $this->translate( $node->get_first_child_node( 'opt_order_clause' ) ); + $limit_clause = $this->translate( $node->get_first_child_node( 'opt_simple_limit' ) ); } // With JOINs, we need to use the JOIN expressions in the WHERE clause. @@ -2253,11 +2269,11 @@ private function execute_delete_statement( WP_Parser_Node $node ): void { * We will rewrite such statements into a SELECT to fetch the ROWIDs of * the rows to delete and then execute a DELETE statement for each table. */ - $alias_ref_list = $node->get_first_child_node( 'tableAliasRefList' ); + $alias_ref_list = $node->get_first_child_node( 'table_alias_ref_list' ); if ( null !== $alias_ref_list ) { // 1. Get table aliases targeted by the DELETE statement. $table_aliases = array(); - foreach ( $alias_ref_list->get_child_nodes() as $alias_ref ) { + foreach ( $alias_ref_list->get_flattened_child_nodes( 'table_ident_opt_wild' ) as $alias_ref ) { $table_aliases[] = $this->unquote_sqlite_identifier( $this->translate( $alias_ref ) ); @@ -2265,10 +2281,10 @@ private function execute_delete_statement( WP_Parser_Node $node ): void { // 2. Create an alias to table name map. $alias_map = array(); - $table_ref_list = $node->get_first_child_node( 'tableReferenceList' ); - foreach ( $table_ref_list->get_descendant_nodes( 'singleTable' ) as $single_table ) { - $table_ref = $single_table->get_first_child_node( 'tableRef' ); - $alias_node = $single_table->get_first_child_node( 'tableAlias' ); + $table_ref_list = $node->get_first_child_node( 'table_reference_list' ); + foreach ( $table_ref_list->get_descendant_nodes( 'single_table' ) as $single_table ) { + $table_ref = $single_table->get_first_child_node( 'table_ident' ); + $alias_node = $single_table->get_first_child_node( 'opt_table_alias' ); if ( $alias_node ) { $alias = $this->unquote_sqlite_identifier( $this->translate( $alias_node ) ); } else { @@ -2288,9 +2304,9 @@ private function execute_delete_statement( WP_Parser_Node $node ): void { } // 3. Compose the SELECT query to fetch ROWIDs to delete. - $where_clause = $node->get_first_child_node( 'whereClause' ); + $where_clause = $node->get_first_child_node( 'opt_where_clause' ); if ( null !== $where_clause ) { - $where = $this->translate( $where_clause->get_first_child_node( 'expr' ) ); + $where = $this->translate( $where_clause->get_first_descendant_node( 'expr' ) ); } $select_list = array(); @@ -2334,7 +2350,7 @@ private function execute_delete_statement( WP_Parser_Node $node ): void { // @TODO: Translate DELETE with JOIN to use a subquery. - $table_ref = $node->get_first_child_node( 'tableRef' ); + $table_ref = $node->get_first_child_node( 'table_ident' ); $database = $this->get_database_name( $table_ref ); if ( 'information_schema' === strtolower( $database ) ) { throw $this->new_access_denied_to_information_schema_exception(); @@ -2349,16 +2365,16 @@ private function execute_delete_statement( WP_Parser_Node $node ): void { * Will be rewritten to: * DELETE FROM t WHERE rowid IN ( SELECT rowid FROM t WHERE c = 2 LIMIT 1 ); */ - $has_order = $node->has_child_node( 'orderClause' ); - $has_limit = $node->has_child_node( 'simpleLimitClause' ); + $has_order = $node->has_child_node( 'opt_order_clause' ); + $has_limit = $node->has_child_node( 'opt_simple_limit' ); if ( $has_order || $has_limit ) { $where_subquery = 'SELECT rowid FROM ' . $this->translate_sequence( array( $table_ref, - $node->get_first_child_node( 'tableAlias' ), - $node->get_first_child_node( 'whereClause' ), - $node->get_first_child_node( 'orderClause' ), - $node->get_first_child_node( 'simpleLimitClause' ), + $node->get_first_child_node( 'opt_table_alias' ), + $node->get_first_child_node( 'opt_where_clause' ), + $node->get_first_child_node( 'opt_order_clause' ), + $node->get_first_child_node( 'opt_simple_limit' ), ) ); @@ -2383,13 +2399,11 @@ private function execute_delete_statement( WP_Parser_Node $node ): void { * @throws WP_SQLite_Driver_Exception When the query execution fails. */ private function execute_create_table_statement( WP_Parser_Node $node ): void { - $subnode = $node->get_first_child_node(); - // Handle TEMPORARY keyword. - $table_is_temporary = $subnode->has_child_token( WP_MySQL_Lexer::TEMPORARY_SYMBOL ); + $table_is_temporary = $node->has_child_node( 'opt_temporary' ); // Handle CREATE TABLE ... [AS] SELECT. - $element_list = $subnode->get_first_child_node( 'tableElementList' ); + $element_list = $node->get_first_child_node( 'table_element_list' ); if ( null === $element_list ) { /* * While SQLite supports CREATE TABLE ... AS SELECT statements, @@ -2404,7 +2418,7 @@ private function execute_create_table_statement( WP_Parser_Node $node ): void { } // Get table name. - $table_name_node = $subnode->get_first_child_node( 'tableName' ); + $table_name_node = $node->get_first_child_node( 'table_ident' ); $database = $this->get_database_name( $table_name_node ); $table_name = $this->unquote_sqlite_identifier( $this->translate( $table_name_node ) ); @@ -2413,7 +2427,7 @@ private function execute_create_table_statement( WP_Parser_Node $node ): void { } // Handle IF NOT EXISTS. - if ( $subnode->has_child_node( 'ifNotExists' ) ) { + if ( $node->has_child_node( 'opt_if_not_exists' ) ) { $tables_table = $this->information_schema_builder->get_table_name( $table_is_temporary, 'tables' ); $table_exists = $this->execute_sqlite_query( sprintf( @@ -2454,7 +2468,7 @@ private function execute_create_table_statement( WP_Parser_Node $node ): void { * @throws WP_SQLite_Driver_Exception When the query execution fails. */ private function execute_alter_table_statement( WP_Parser_Node $node ): void { - $table_ref = $node->get_first_descendant_node( 'tableRef' ); + $table_ref = $node->get_first_descendant_node( 'table_ident' ); $database = $this->get_database_name( $table_ref ); $table_name = $this->unquote_sqlite_identifier( $this->translate( $table_ref ) ); if ( 'information_schema' === strtolower( $database ) ) { @@ -2481,35 +2495,47 @@ private function execute_alter_table_statement( WP_Parser_Node $node ): void { array_column( $column_names, 'COLUMN_NAME_LOWERCASE' ), array_column( $column_names, 'COLUMN_NAME' ) ); - foreach ( $node->get_descendant_nodes( 'alterListItem' ) as $action ) { + foreach ( $node->get_descendant_nodes( 'alter_list_item' ) as $action ) { $first_token = $action->get_first_child_token(); switch ( $first_token->id ) { - case WP_MySQL_Lexer::DROP_SYMBOL: - $name = $this->translate( $action->get_first_child_node( 'fieldIdentifier' ) ); + case WP_MySQL_Tokens::KEYWORDS['DROP']: + // Skip DROP actions that don't target a column (e.g. keys + // and constraints; they also carry an "ident" child node). + $is_column_drop = ( + ! $action->has_child_node( 'key_or_index' ) + && ! $action->has_child_token( WP_MySQL_Tokens::KEYWORDS['PRIMARY'] ) + && ! $action->has_child_token( WP_MySQL_Tokens::KEYWORDS['FOREIGN'] ) + && ! $action->has_child_token( WP_MySQL_Tokens::KEYWORDS['CHECK'] ) + && ! $action->has_child_token( WP_MySQL_Tokens::KEYWORDS['CONSTRAINT'] ) + ); + $name = $is_column_drop + ? $this->translate( $action->get_first_child_node( 'ident' ) ) + : null; if ( null !== $name ) { $name = $this->unquote_sqlite_identifier( $name ); unset( $column_map[ strtolower( $name ) ] ); } break; - case WP_MySQL_Lexer::CHANGE_SYMBOL: + case WP_MySQL_Tokens::KEYWORDS['CHANGE']: + $idents = $action->get_child_nodes( 'ident' ); $old_name = $this->unquote_sqlite_identifier( - $this->translate( $action->get_first_child_node( 'fieldIdentifier' ) ) + $this->translate( $idents[0] ) ); $new_name = $this->unquote_sqlite_identifier( - $this->translate( $action->get_first_child_node( 'identifier' ) ) + $this->translate( $idents[1] ) ); $column_map[ strtolower( $old_name ) ] = $new_name; break; - case WP_MySQL_Lexer::RENAME_SYMBOL: - $column_ref = $action->get_first_child_node( 'fieldIdentifier' ); - if ( null !== $column_ref ) { + case WP_MySQL_Tokens::KEYWORDS['RENAME']: + if ( $action->has_child_token( WP_MySQL_Tokens::KEYWORDS['COLUMN'] ) ) { + $idents = $action->get_child_nodes( 'ident' ); $old_name = $this->unquote_sqlite_identifier( - $this->translate( $column_ref ) + $this->translate( $idents[0] ) ); $new_name = $this->unquote_sqlite_identifier( - $this->translate( $action->get_first_child_node( 'identifier' ) ) + $this->translate( $idents[1] ) ); $column_map[ strtolower( $old_name ) ] = $new_name; @@ -2523,7 +2549,7 @@ private function execute_alter_table_statement( WP_Parser_Node $node ): void { * table options (e.g. ALTER TABLE t AUTO_INCREMENT = N). These don't * change the schema, so the recreate would be a pointless full copy. */ - if ( count( $node->get_descendant_nodes( 'alterListItem' ) ) > 0 ) { + if ( count( $node->get_descendant_nodes( 'alter_list_item' ) ) > 0 ) { $this->information_schema_builder->record_alter_table( $node ); $this->recreate_table_from_information_schema( $table_is_temporary, $table_name, $column_map ); } @@ -2547,9 +2573,8 @@ private function execute_drop_table_statement( WP_Parser_Node $node ): void { // MySQL supports removing multiple tables in a single query DROP query. // In SQLite, we need to execute each DROP TABLE statement separately. - $child_node = $node->get_first_child_node(); - $table_refs = $child_node->get_first_child_node( 'tableRefList' )->get_child_nodes(); - $table_is_temporary = $child_node->has_child_token( WP_MySQL_Lexer::TEMPORARY_SYMBOL ); + $table_refs = $node->get_first_child_node( 'table_list' )->get_flattened_child_nodes( 'table_ident' ); + $table_is_temporary = $node->has_child_node( 'opt_temporary' ); $queries = array(); foreach ( $table_refs as $table_ref ) { $database = $this->get_database_name( $table_ref ); @@ -2558,16 +2583,16 @@ private function execute_drop_table_statement( WP_Parser_Node $node ): void { } $parts = array(); - foreach ( $child_node->get_children() as $child ) { + foreach ( $node->get_children() as $child ) { $is_token = $child instanceof WP_MySQL_Token; // Skip the TEMPORARY keyword. - if ( $is_token && WP_MySQL_Lexer::TEMPORARY_SYMBOL === $child->id ) { + if ( ! $is_token && 'opt_temporary' === $child->rule_name ) { continue; } // Replace table list with the current table reference. - if ( ! $is_token && 'tableRefList' === $child->rule_name ) { + if ( ! $is_token && 'table_list' === $child->rule_name ) { // Add a "temp." schema prefix for temporary tables. $prefix = $table_is_temporary ? '`temp`.' : ''; $part = $prefix . $this->translate( $table_ref ); @@ -2579,7 +2604,7 @@ private function execute_drop_table_statement( WP_Parser_Node $node ): void { $parts[] = $part; } } - $queries[] = 'DROP ' . implode( ' ', $parts ); + $queries[] = implode( ' ', $parts ); } foreach ( $queries as $query ) { @@ -2594,7 +2619,7 @@ private function execute_drop_table_statement( WP_Parser_Node $node ): void { * @throws WP_SQLite_Driver_Exception When the query execution fails. */ private function execute_truncate_table_statement( WP_Parser_Node $node ): void { - $table_ref = $node->get_first_child_node( 'tableRef' ); + $table_ref = $node->get_first_child_node( 'table_ident' ); $database = $this->get_database_name( $table_ref ); $table_name = $this->unquote_sqlite_identifier( $this->translate( $table_ref ) ); if ( 'information_schema' === strtolower( $database ) ) { @@ -2625,11 +2650,9 @@ private function execute_truncate_table_statement( WP_Parser_Node $node ): void * @throws WP_SQLite_Driver_Exception When the query execution fails. */ private function execute_create_index_statement( WP_Parser_Node $node ): void { - $create_index = $node->get_first_child_node( 'createIndex' ); - $target = $create_index->get_first_child_node( 'createIndexTarget' ); - $table_ref = $target->get_first_child_node( 'tableRef' ); - $database = $this->get_database_name( $table_ref ); - $table_name = $this->unquote_sqlite_identifier( $this->translate( $table_ref ) ); + $table_ref = $node->get_first_child_node( 'table_ident' ); + $database = $this->get_database_name( $table_ref ); + $table_name = $this->unquote_sqlite_identifier( $this->translate( $table_ref ) ); if ( 'information_schema' === strtolower( $database ) ) { throw $this->new_access_denied_to_information_schema_exception(); @@ -2638,23 +2661,19 @@ private function execute_create_index_statement( WP_Parser_Node $node ): void { $this->information_schema_builder->record_create_index( $node ); $index_name = $this->unquote_sqlite_identifier( - $this->translate( $create_index->get_first_child_node( 'indexName' ) ) + $this->translate( $node->get_first_child_node( 'ident' ) ) ); - $is_unique = $create_index->has_child_token( WP_MySQL_Lexer::UNIQUE_SYMBOL ); + $is_unique = $node->has_child_node( 'opt_unique' ); // Get the key parts. - $key_list_variants = $target->get_first_child_node( 'keyListVariants' ); - $key_list_nodes = $key_list_variants->get_first_child_node()->get_child_nodes(); - foreach ( $key_list_nodes as $key_list_node ) { - if ( 'keyPartOrExpression' === $key_list_node->rule_name ) { - $key_part_node = $key_list_node->get_first_child(); - } else { - $key_part_node = $key_list_node; - } - - if ( 'keyPart' === $key_part_node->rule_name ) { - $key_part = $this->translate( $key_part_node->get_first_child_node( 'identifier' ) ); - $direction = $key_part_node->get_first_child_node( 'direction' ); + $key_list = $node->get_first_child_node( 'key_list_with_expression' ); + $key_parts = array(); + foreach ( $key_list->get_flattened_child_nodes( 'key_part_with_expression' ) as $key_list_node ) { + $key_part_node = $key_list_node->get_first_child_node(); + + if ( 'key_part' === $key_part_node->rule_name ) { + $key_part = $this->translate( $key_part_node->get_first_child_node( 'ident' ) ); + $direction = $key_part_node->get_first_child_node( 'opt_ordering_direction' ); if ( null !== $direction ) { $key_part .= ' ' . $this->translate( $direction ); } @@ -2670,7 +2689,7 @@ private function execute_create_index_statement( WP_Parser_Node $node ): void { 'CREATE %sINDEX %s ON %s (%s)', $is_unique ? 'UNIQUE ' : '', $this->quote_sqlite_identifier( $sqlite_index_name ), - $this->translate( $target->get_first_child_node( 'tableRef' ) ), + $this->translate( $table_ref ), implode( ', ', $key_parts ) ) ); @@ -2683,9 +2702,8 @@ private function execute_create_index_statement( WP_Parser_Node $node ): void { * @throws WP_SQLite_Driver_Exception When the query execution fails. */ private function execute_drop_index_statement( WP_Parser_Node $node ): void { - $drop_index = $node->get_first_child_node( 'dropIndex' ); - $table_ref = $drop_index->get_first_child_node( 'tableRef' ); - $database = $this->get_database_name( $table_ref ); + $table_ref = $node->get_first_child_node( 'table_ident' ); + $database = $this->get_database_name( $table_ref ); if ( 'information_schema' === strtolower( $database ) ) { throw $this->new_access_denied_to_information_schema_exception(); } @@ -2694,7 +2712,7 @@ private function execute_drop_index_statement( WP_Parser_Node $node ): void { $table_name = $this->unquote_sqlite_identifier( $this->translate( $table_ref ) ); $index_name = $this->unquote_sqlite_identifier( - $this->translate( $drop_index->get_first_child_node( 'indexRef' ) ) + $this->translate( $node->get_first_child_node( 'ident' ) ) ); /* @@ -2720,39 +2738,33 @@ private function execute_drop_index_statement( WP_Parser_Node $node ): void { /** * Translate and execute a MySQL SHOW statement in SQLite. * - * @param WP_Parser_Node $node The "showStatement" AST node. + * @param WP_Parser_Node $node The "show_*_stmt" AST node. * @throws WP_SQLite_Driver_Exception When the query execution fails. */ private function execute_show_statement( WP_Parser_Node $node ): void { - $tokens = $node->get_child_tokens(); - $keyword1 = $tokens[1]; - $keyword2 = $tokens[2] ?? null; - - switch ( $keyword1->id ) { - case WP_MySQL_Lexer::COLLATION_SYMBOL: + switch ( $node->rule_name ) { + case 'show_collation_stmt': $this->execute_show_collation_statement( $node ); return; - case WP_MySQL_Lexer::DATABASES_SYMBOL: + case 'show_databases_stmt': $this->execute_show_databases_statement( $node ); return; - case WP_MySQL_Lexer::COLUMNS_SYMBOL: - case WP_MySQL_Lexer::FIELDS_SYMBOL: + case 'show_columns_stmt': $this->execute_show_columns_statement( $node ); return; - case WP_MySQL_Lexer::CREATE_SYMBOL: - if ( WP_MySQL_Lexer::TABLE_SYMBOL === $keyword2->id ) { - $table_ref = $node->get_first_child_node( 'tableRef' ); + case 'show_create_table_stmt': + $table_ref = $node->get_first_child_node( 'table_ident' ); $database = $this->get_database_name( $table_ref ); $table_name = $this->unquote_sqlite_identifier( $this->translate( $table_ref ) ); // Refuse SHOW CREATE TABLE for information schema tables, // as we don't have the table definitions at the moment. - if ( 'information_schema' === strtolower( $database ) ) { - throw $this->new_driver_exception( - sprintf( "SHOW command denied to user 'sqlite'@'%%' for table '%s'", $table_name ), - '42000' - ); - } + if ( 'information_schema' === strtolower( $database ) ) { + throw $this->new_driver_exception( + sprintf( "SHOW command denied to user 'sqlite'@'%%' for table '%s'", $table_name ), + '42000' + ); + } $table_is_temporary = $this->information_schema_builder->temporary_table_exists( $table_name ); @@ -2784,15 +2796,11 @@ private function execute_show_statement( WP_Parser_Node $node ): void { null === $sql ? array() : array( array( $table_name, $sql ) ) ); $this->found_rows = null === $sql ? 0 : 1; - return; - } - break; - case WP_MySQL_Lexer::INDEX_SYMBOL: - case WP_MySQL_Lexer::INDEXES_SYMBOL: - case WP_MySQL_Lexer::KEYS_SYMBOL: + return; + case 'show_keys_stmt': $this->execute_show_index_statement( $node ); return; - case WP_MySQL_Lexer::GRANTS_SYMBOL: + case 'show_grants_stmt': $this->last_result_statement = $this->create_result_statement_from_data( array( 'Grants for root@%' ), array( array( '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' ) ) @@ -2810,13 +2818,13 @@ private function execute_show_statement( WP_Parser_Node $node ): void { ); $this->found_rows = 1; return; - case WP_MySQL_Lexer::TABLE_SYMBOL: + case 'show_table_status_stmt': $this->execute_show_table_status_statement( $node ); return; - case WP_MySQL_Lexer::TABLES_SYMBOL: + case 'show_tables_stmt': $this->execute_show_tables_statement( $node ); return; - case WP_MySQL_Lexer::VARIABLES_SYMBOL: + case 'show_variables_stmt': $this->last_column_meta = array( array( 'native_type' => 'STRING', @@ -2846,25 +2854,21 @@ private function execute_show_statement( WP_Parser_Node $node ): void { } throw $this->new_not_supported_exception( - sprintf( - 'statement type: "%s" > "%s"', - $node->rule_name, - $keyword1->get_value() - ) + sprintf( 'statement type: "%s"', $node->rule_name ) ); } /** * Translate and execute a MySQL SHOW COLLATION statement in SQLite. * - * @param WP_Parser_Node $node The "showStatement" AST node. + * @param WP_Parser_Node $node The "show_collation_stmt" AST node. */ private function execute_show_collation_statement( WP_Parser_Node $node ): void { $definition = $this->information_schema_builder ->get_computed_information_schema_table_definition( 'collations' ); // LIKE and WHERE clauses. - $like_or_where = $node->get_first_child_node( 'likeOrWhere' ); + $like_or_where = $node->get_first_child_node( 'opt_wild_or_where' ); if ( $like_or_where ) { $condition = $this->translate_show_like_or_where_condition( $like_or_where, 'collation_name' ); } @@ -2898,7 +2902,7 @@ private function execute_show_databases_statement( WP_Parser_Node $node ): void $schemata_table = $this->information_schema_builder->get_table_name( false, 'schemata' ); // LIKE and WHERE clauses. - $like_or_where = $node->get_first_child_node( 'likeOrWhere' ); + $like_or_where = $node->get_first_child_node( 'opt_wild_or_where' ); if ( $like_or_where ) { $condition = $this->translate_show_like_or_where_condition( $like_or_where, 'schema_name' ); } @@ -2930,8 +2934,8 @@ private function execute_show_databases_statement( WP_Parser_Node $node ): void */ private function execute_show_index_statement( WP_Parser_Node $node ): void { // Get database and table name. - $table_ref = $node->get_first_child_node( 'tableRef' ); - $in_db = $node->get_first_child_node( 'inDb' ); + $table_ref = $node->get_first_child_node( 'table_ident' ); + $in_db = $node->get_first_child_node( 'opt_db' ); if ( $in_db ) { // FROM/IN database. $database = $this->get_database_name( $in_db ); @@ -2941,7 +2945,8 @@ private function execute_show_index_statement( WP_Parser_Node $node ): void { $table_name = $this->unquote_sqlite_identifier( $this->translate( $table_ref ) ); // WHERE clause. - $where = $node->get_first_child_node( 'whereClause' ); + $where_clause = $node->get_first_child_node( 'opt_where_clause' ); + $where = $where_clause ? $where_clause->get_first_child_node( 'where_clause' ) : null; if ( null !== $where ) { $value = $this->translate( $where->get_first_child_node( 'expr' ) ); $condition = sprintf( 'AND %s', $value ); @@ -3023,17 +3028,17 @@ private function execute_show_index_statement( WP_Parser_Node $node ): void { */ private function execute_show_table_status_statement( WP_Parser_Node $node ): void { // FROM/IN database. - $in_db = $node->get_first_child_node( 'inDb' ); + $in_db = $node->get_first_child_node( 'opt_db' ); if ( null === $in_db ) { $database = $this->db_name; } else { $database = $this->unquote_sqlite_identifier( - $this->translate( $in_db->get_first_child_node( 'identifier' ) ) + $this->translate( $in_db->get_first_child_node( 'ident' ) ) ); } // LIKE and WHERE clauses. - $like_or_where = $node->get_first_child_node( 'likeOrWhere' ); + $like_or_where = $node->get_first_child_node( 'opt_wild_or_where' ); if ( null !== $like_or_where ) { $condition = $this->translate_show_like_or_where_condition( $like_or_where, 'Name' ); } @@ -3110,24 +3115,24 @@ private function execute_show_table_status_statement( WP_Parser_Node $node ): vo */ private function execute_show_tables_statement( WP_Parser_Node $node ): void { // FROM/IN database. - $in_db = $node->get_first_child_node( 'inDb' ); + $in_db = $node->get_first_child_node( 'opt_db' ); if ( null === $in_db ) { $database = $this->db_name; } else { $database = $this->unquote_sqlite_identifier( - $this->translate( $in_db->get_first_child_node( 'identifier' ) ) + $this->translate( $in_db->get_first_child_node( 'ident' ) ) ); } // LIKE and WHERE clauses. - $like_or_where = $node->get_first_child_node( 'likeOrWhere' ); + $like_or_where = $node->get_first_child_node( 'opt_wild_or_where' ); if ( null !== $like_or_where ) { $condition = $this->translate_show_like_or_where_condition( $like_or_where, 'table_name' ); } // Handle the FULL keyword. - $command_type = $node->get_first_child_node( 'showCommandType' ); - $is_full = $command_type && $command_type->has_child_token( WP_MySQL_Lexer::FULL_SYMBOL ); + $command_type = $node->get_first_child_node( 'opt_show_cmd_type' ); + $is_full = $command_type && $command_type->has_child_token( WP_MySQL_Tokens::KEYWORDS['FULL'] ); // Fetch table information. $table_tables = $this->information_schema_builder->get_table_name( @@ -3163,8 +3168,8 @@ private function execute_show_columns_statement( WP_Parser_Node $node ): void { // TODO: EXTENDED, FULL // Get database and table name. - $table_ref = $node->get_first_child_node( 'tableRef' ); - $in_db = $node->get_first_child_node( 'inDb' ); + $table_ref = $node->get_first_child_node( 'table_ident' ); + $in_db = $node->get_first_child_node( 'opt_db' ); if ( $in_db ) { // FROM/IN database. $database = $this->get_database_name( $in_db ); @@ -3192,14 +3197,14 @@ private function execute_show_columns_statement( WP_Parser_Node $node ): void { } // LIKE and WHERE clauses. - $like_or_where = $node->get_first_child_node( 'likeOrWhere' ); + $like_or_where = $node->get_first_child_node( 'opt_wild_or_where' ); if ( null !== $like_or_where ) { $condition = $this->translate_show_like_or_where_condition( $like_or_where, 'column_name' ); } // Handle the FULL keyword. - $command_type = $node->get_first_child_node( 'showCommandType' ); - $is_full = $command_type && $command_type->has_child_token( WP_MySQL_Lexer::FULL_SYMBOL ); + $command_type = $node->get_first_child_node( 'opt_show_cmd_type' ); + $is_full = $command_type && $command_type->has_child_token( WP_MySQL_Tokens::KEYWORDS['FULL'] ); // Fetch column information. $columns_table = $this->information_schema_builder->get_table_name( $table_is_temporary, 'columns' ); @@ -3254,7 +3259,7 @@ private function execute_show_columns_statement( WP_Parser_Node $node ): void { * @throws WP_SQLite_Driver_Exception When the query execution fails. */ private function execute_describe_statement( WP_Parser_Node $node ): void { - $table_ref = $node->get_first_child_node( 'tableRef' ); + $table_ref = $node->get_first_child_node( 'table_ident' ); $database = $this->get_database_name( $table_ref ); $table_name = $this->unquote_sqlite_identifier( $this->translate( $table_ref ) ); @@ -3294,7 +3299,7 @@ private function execute_describe_statement( WP_Parser_Node $node ): void { */ private function execute_use_statement( WP_Parser_Node $node ): void { $database_name = $this->unquote_sqlite_identifier( - $this->translate( $node->get_first_child_node( 'identifier' ) ) + $this->translate( $node->get_first_child_node( 'ident' ) ) ); $database_name = strtolower( $database_name ); @@ -3314,7 +3319,7 @@ private function execute_use_statement( WP_Parser_Node $node ): void { /** * Translate and execute a MySQL SET statement in SQLite. * - * @param WP_Parser_Node $node The "setStatement" AST node. + * @param WP_Parser_Node $node The "set" AST node. * @throws WP_SQLite_Driver_Exception When the query execution fails. */ private function execute_set_statement( WP_Parser_Node $node ): void { @@ -3326,23 +3331,23 @@ private function execute_set_statement( WP_Parser_Node $node ): void { * * This will be flattened into a single array of grammar node lists: * [ - * [ , , ], - * [ , , , ], - * [ , , ], - * [ , , ], + * [ <@>, , , ], + * [ , , , ], + * [ <@>, <@>, , , , ], + * [ <@>, <@>, , , ], * ] */ $subnode = $node->get_first_child_node(); - if ( $subnode->has_child_node( 'optionValueNoOptionType' ) ) { - $start_node = $subnode->get_first_child_node( 'optionValueNoOptionType' ); + if ( $subnode->has_child_node( 'option_value_no_option_type' ) ) { + $start_node = $subnode->get_first_child_node( 'option_value_no_option_type' ); $definitions = array( $start_node->get_children() ); - } elseif ( $subnode->has_child_node( 'startOptionValueListFollowingOptionType' ) ) { + } elseif ( $subnode->has_child_node( 'start_option_value_list_following_option_type' ) ) { $start_node = $subnode - ->get_first_child_node( 'startOptionValueListFollowingOptionType' ) - ->get_first_child_node( 'optionValueFollowingOptionType' ) ?? $node; + ->get_first_child_node( 'start_option_value_list_following_option_type' ) + ->get_first_child_node( 'option_value_following_option_type' ) ?? $node; $definitions = array( array_merge( - array( $subnode->get_first_child_node( 'optionType' ) ), + array( $subnode->get_first_child_node( 'option_type' ) ), $start_node->get_children() ), ); @@ -3350,67 +3355,89 @@ private function execute_set_statement( WP_Parser_Node $node ): void { $definitions = array( $subnode->get_children() ); } - $continue_node = $subnode->get_first_child_node( 'optionValueListContinued' ); + // The continued list follows either the first option value or the + // "start_option_value_list_following_option_type" node. + $continue_node = $subnode->get_first_descendant_node( 'option_value_list_continued' ); if ( $continue_node ) { - foreach ( $continue_node->get_child_nodes( 'optionValue' ) as $child ) { - $node = $child->get_first_child_node( 'optionValueNoOptionType' ) ?? $child; - $definitions[] = $node->get_child_nodes(); + $option_value_list = $continue_node->get_first_child_node( 'option_value_list' ); + foreach ( $option_value_list->get_flattened_child_nodes( 'option_value' ) as $child ) { + $inner = $child->get_first_child_node( 'option_value_no_option_type' ); + if ( null !== $inner ) { + $definitions[] = $inner->get_children(); + } else { + $following = $child->get_first_child_node( 'option_value_following_option_type' ); + $definitions[] = array_merge( + array( $child->get_first_child_node( 'option_type' ) ), + $following->get_children() + ); + } } } /* * 2. Iterate and process the SET definitions. * - * When an "optionType" node is encountered (such as "SESSION var = ..."), + * When an "option_type" node is encountered (such as "SESSION var = ..."), * it's value is used for all following system variable assignments that - * have no type keyword specified, until the next "optionType" is found. + * have no type keyword specified, until the next "option_type" is found. * * This doesn't apply to "@@" type prefixes (such as "@@SESSION.var_name"), * which always impact only the immediately following system variable. */ - $default_type = WP_MySQL_Lexer::SESSION_SYMBOL; + $default_type = WP_MySQL_Tokens::KEYWORDS['SESSION']; foreach ( $definitions as $definition ) { - // Check if the definition starts with an "optionType" node with + // Check if the definition starts with an "option_type" node with // one of the SESSION, GLOBAL, PERSIST, or PERSIST_ONLY tokens. $part = array_shift( $definition ); - if ( $part instanceof WP_Parser_Node && 'optionType' === $part->rule_name ) { + if ( $part instanceof WP_Parser_Node && 'option_type' === $part->rule_name ) { $default_type = $part->get_first_child_token()->id; $part = array_shift( $definition ); } if ( $part instanceof WP_MySQL_Token - && WP_MySQL_Lexer::NAMES_SYMBOL === $part->id + && WP_MySQL_Tokens::KEYWORDS['NAMES'] === $part->id ) { // "SET NAMES ..." is a no-op for now. // TODO: Validate charset compatibility with UTF-8. // See: https://github.com/WordPress/sqlite-database-integration/issues/192 } elseif ( $part instanceof WP_Parser_Node - && 'charsetClause' === $part->rule_name + && 'character_set' === $part->rule_name ) { // "SET CHARACTER SET ..." is a no-op for now. // TODO: Validate charset compatibility with UTF-8. // See: https://github.com/WordPress/sqlite-database-integration/issues/192 } elseif ( $part instanceof WP_Parser_Node - && ( - 'internalVariableName' === $part->rule_name - || 'setSystemVariable' === $part->rule_name - ) + && 'lvalue_variable' === $part->rule_name ) { - // Set a system variable. + // Set a system variable without a type prefix. array_shift( $definition ); // Remove the '='. $value = array_shift( $definition ); $this->execute_set_system_variable_statement( $part, $value, $default_type ); } elseif ( - $part instanceof WP_Parser_Node - && 'userVariable' === $part->rule_name + $part instanceof WP_MySQL_Token + && WP_MySQL_Tokens::AT_SIGN_SYMBOL === $part->id ) { - // Set a user variable. - array_shift( $definition ); // Remove the '='. - $value = array_shift( $definition ); - $this->execute_set_user_variable_statement( $part, $value ); + $next = array_shift( $definition ); + if ( $next instanceof WP_MySQL_Token && WP_MySQL_Tokens::AT_SIGN_SYMBOL === $next->id ) { + // Set a system variable with a "@@[type.]" prefix. + $type = $default_type; + $next = array_shift( $definition ); + if ( $next instanceof WP_Parser_Node && 'opt_set_var_ident_type' === $next->rule_name ) { + $type = $next->get_first_child_token()->id; + $next = array_shift( $definition ); + } + array_shift( $definition ); // Remove the '='. + $value = array_shift( $definition ); + $this->execute_set_system_variable_statement( $next, $value, $type ); + } else { + // Set a user variable ("@var"). + array_shift( $definition ); // Remove the '='. + $value = array_shift( $definition ); + $this->execute_set_user_variable_statement( $next, $value ); + } } else { throw $this->new_not_supported_exception( sprintf( 'SET statement: %s', $node->rule_name ) @@ -3424,35 +3451,24 @@ private function execute_set_statement( WP_Parser_Node $node ): void { /** * Translate and execute a MySQL SET statement for system variables. * - * @param WP_Parser_Node $set_var_node The "internalVariableName" or "setSystemVariable" AST node. - * @param WP_Parser_Node $value_node The "setExprOrDefault" AST node. - * @param int $default_type The currently active default variable type. + * @param WP_Parser_Node $set_var_node The "lvalue_variable" AST node. + * @param WP_Parser_Node $value_node The "set_expr_or_default" AST node. + * @param int $type The variable type. * One of the SESSION, GLOBAL, PERSIST, PERSIST_ONLY tokens. * @throws WP_SQLite_Driver_Exception When the query execution fails. */ private function execute_set_system_variable_statement( WP_Parser_Node $set_var_node, WP_Parser_Node $value_node, - int $default_type + int $type ): void { // Get the variable name. - $internal_variable_name = 'setSystemVariable' === $set_var_node->rule_name - ? $set_var_node->get_first_child_node( 'internalVariableName' ) - : $set_var_node; - $name = strtolower( $this->unquote_sqlite_identifier( - $this->translate( $internal_variable_name ) + $this->translate( $set_var_node ) ) ); - // Get the type attribute (one of SESSION, GLOBAL, PERSIST, PERSIST_ONLY). - $type = $default_type; - if ( $set_var_node->has_child_node( 'setVarIdentType' ) ) { - $var_ident_type = $set_var_node->get_first_child_node( 'setVarIdentType' ); - $type = $var_ident_type->get_first_child_token()->id; - } - /* * Some MySQL system variables values can be set using an unquoted pure * identifier rather than a string literal. This includes non-reserved @@ -3468,9 +3484,9 @@ private function execute_set_system_variable_statement( * * In this cases, we need to use the value directly without attempting * to evaluate the expression, as that would result in a query error. - * In the grammar, unquoted identifiers are captured by "columnRef". + * In the grammar, unquoted identifiers are captured by "simple_ident". */ - $identifier = $this->translate( $value_node->get_first_descendant_node( 'columnRef' ) ); + $identifier = $this->translate( $value_node->get_first_descendant_node( 'simple_ident' ) ); if ( $identifier && $identifier === $this->translate( $value_node ) ) { $value = $this->unquote_sqlite_identifier( $identifier ); } elseif ( ! $value_node->has_child_node( 'expr' ) ) { @@ -3496,18 +3512,18 @@ private function execute_set_system_variable_statement( $value = 'on' === $lowercase_value ? 1 : 0; } - if ( WP_MySQL_Lexer::SESSION_SYMBOL === $type ) { + if ( WP_MySQL_Tokens::KEYWORDS['SESSION'] === $type ) { if ( 'sql_mode' === $name ) { $modes = explode( ',', strtoupper( $value ) ); $this->active_sql_modes = $modes; } else { $this->session_system_variables[ $name ] = $value; } - } elseif ( WP_MySQL_Lexer::GLOBAL_SYMBOL === $type ) { + } elseif ( WP_MySQL_Tokens::KEYWORDS['GLOBAL'] === $type ) { throw $this->new_not_supported_exception( "SET statement type: 'GLOBAL'" ); - } elseif ( WP_MySQL_Lexer::PERSIST_SYMBOL === $type ) { + } elseif ( WP_MySQL_Tokens::KEYWORDS['PERSIST'] === $type ) { throw $this->new_not_supported_exception( "SET statement type: 'PERSIST'" ); - } elseif ( WP_MySQL_Lexer::PERSIST_ONLY_SYMBOL === $type ) { + } elseif ( WP_MySQL_Tokens::KEYWORDS['PERSIST_ONLY'] === $type ) { throw $this->new_not_supported_exception( "SET statement type: 'PERSIST_ONLY'" ); } @@ -3517,7 +3533,7 @@ private function execute_set_system_variable_statement( /** * Translate and execute a MySQL SET statement for user variables. * - * @param WP_Parser_Node $user_variable The "userVariable" AST node. + * @param WP_Parser_Node $user_variable The "ident_or_text" AST node with the user variable name. * @param WP_Parser_Node $expr The "expr" AST node. * @throws WP_SQLite_Driver_Exception When the query execution fails. */ @@ -3525,10 +3541,9 @@ private function execute_set_user_variable_statement( WP_Parser_Node $user_variable, WP_Parser_Node $expr ): void { - $name = $this->unquote_sqlite_identifier( - $this->translate( $user_variable->get_first_child() ) + $name = strtolower( + $this->unquote_sqlite_identifier( $this->translate( $user_variable ) ) ); - $name = strtolower( substr( $name, 1 ) ); // Remove '@', normalize case. $value = $this->evaluate_expression( $expr ); $this->user_variables[ $name ] = $value; @@ -3548,9 +3563,9 @@ private function execute_set_user_variable_statement( */ private function execute_administration_statement( WP_Parser_Node $node ): void { $first_token = $node->get_first_child_token(); - $table_ref_list = $node->get_first_child_node( 'tableRefList' ); + $table_ref_list = $node->get_first_child_node( 'table_list' ); $results = array(); - foreach ( $table_ref_list->get_child_nodes( 'tableRef' ) as $table_ref ) { + foreach ( $table_ref_list->get_flattened_child_nodes( 'table_ident' ) as $table_ref ) { $database = $this->get_database_name( $table_ref ); if ( 'information_schema' === strtolower( $database ) ) { throw $this->new_access_denied_to_information_schema_exception(); @@ -3560,11 +3575,11 @@ private function execute_administration_statement( WP_Parser_Node $node ): void $quoted_table_name = $this->quote_sqlite_identifier( $table_name ); try { switch ( $first_token->id ) { - case WP_MySQL_Lexer::ANALYZE_SYMBOL: + case WP_MySQL_Tokens::KEYWORDS['ANALYZE']: $stmt = $this->execute_sqlite_query( sprintf( 'ANALYZE %s', $quoted_table_name ) ); $errors = $stmt->fetchAll( PDO::FETCH_COLUMN ); break; - case WP_MySQL_Lexer::CHECK_SYMBOL: + case WP_MySQL_Tokens::KEYWORDS['CHECK']: $stmt = $this->execute_sqlite_query( sprintf( 'PRAGMA integrity_check(%s)', $quoted_table_name ) ); @@ -3573,8 +3588,8 @@ private function execute_administration_statement( WP_Parser_Node $node ): void array_shift( $errors ); } break; - case WP_MySQL_Lexer::OPTIMIZE_SYMBOL: - case WP_MySQL_Lexer::REPAIR_SYMBOL: + case WP_MySQL_Tokens::KEYWORDS['OPTIMIZE']: + case WP_MySQL_Tokens::KEYWORDS['REPAIR']: /* * SQLite doesn't support OPTIMIZE and REPAIR TABLE commands. * We will recreate the table and copy the data instead. @@ -3717,39 +3732,37 @@ private function translate( $node ): ?string { $rule_name = $node->rule_name; switch ( $rule_name ) { - case 'queryExpression': + case 'query_expression': return $this->translate_query_expression( $node ); - case 'querySpecification': + case 'query_specification': return $this->translate_query_specification( $node ); - case 'tableRef': + case 'table_ident': return $this->translate_table_ref( $node ); - case 'qualifiedIdentifier': - case 'tableRefWithWildcard': - $parts = $node->get_descendant_nodes( 'identifier' ); + case 'table_wild': + $parts = $node->get_descendant_nodes( 'ident' ); + if ( count( $parts ) === 2 ) { + return $this->translate_qualified_identifier( $parts[0], $parts[1] ) . '.*'; + } + return $this->translate_qualified_identifier( null, $parts[0] ) . '.*'; + case 'table_ident_opt_wild': + // A multi-table DELETE target; the optional ".*" suffix is dropped. + $parts = $node->get_descendant_nodes( 'ident' ); if ( count( $parts ) === 2 ) { return $this->translate_qualified_identifier( $parts[0], $parts[1] ); } return $this->translate_qualified_identifier( null, $parts[0] ); - case 'fieldIdentifier': - case 'simpleIdentifier': - $parts = $node->get_descendant_nodes( 'identifier' ); + case 'simple_ident': + case 'simple_ident_nospvar': + $parts = $node->get_descendant_nodes( 'ident' ); if ( count( $parts ) === 3 ) { return $this->translate_qualified_identifier( $parts[0], $parts[1], $parts[2] ); } elseif ( count( $parts ) === 2 ) { return $this->translate_qualified_identifier( null, $parts[0], $parts[1] ); } return $this->translate_qualified_identifier( null, null, $parts[0] ); - case 'tableWild': - $parts = $node->get_descendant_nodes( 'identifier' ); - if ( count( $parts ) === 2 ) { - return $this->translate_qualified_identifier( $parts[0], $parts[1] ) . '.*'; - } - return $this->translate_qualified_identifier( null, $parts[0] ) . '.*'; - case 'dotIdentifier': - return $this->translate_sequence( $node->get_children(), '' ); - case 'identifierKeyword': + case 'ident_keyword': return '`' . $this->translate( $node->get_first_child() ) . '`'; - case 'pureIdentifier': + case 'IDENT_sys': $value = $this->translate_pure_identifier( $node ); /* @@ -3765,9 +3778,23 @@ private function translate( $node ): ?string { } } return $value; - case 'textStringLiteral': + /* + * The "text_literal" rule covers string literals in expressions; the + * TEXT_STRING_* rules wrap a string token used in non-expression + * positions (e.g., a string SELECT alias, "SELECT a AS 'name'"). + * Without an explicit case, the raw unquoted token value would + * leak into the output. + */ + case 'text_literal': + case 'TEXT_STRING_sys': + case 'TEXT_STRING_literal': + case 'TEXT_STRING_password': + case 'TEXT_STRING_hash': + case 'TEXT_STRING_validated': + case 'TEXT_STRING_filesystem': + case 'TEXT_STRING_sys_nonewline': return $this->translate_string_literal( $node ); - case 'dataType': + case 'type': case 'nchar': $child = $node->get_first_child(); if ( $child instanceof WP_Parser_Node ) { @@ -3777,9 +3804,9 @@ private function translate( $node ): ?string { // Handle optional prefixes (data type is the second token): // 1. LONG VARCHAR, LONG CHAR(ACTER) VARYING, LONG VARBINARY. // 2. NATIONAL CHAR, NATIONAL VARCHAR, NATIONAL CHAR(ACTER) VARYING. - if ( WP_MySQL_Lexer::LONG_SYMBOL === $child->id ) { + if ( WP_MySQL_Tokens::KEYWORDS['LONG'] === $child->id ) { $child = $node->get_child_tokens()[1] ?? null; - } elseif ( WP_MySQL_Lexer::NATIONAL_SYMBOL === $child->id ) { + } elseif ( WP_MySQL_Tokens::KEYWORDS['NATIONAL'] === $child->id ) { $child = $node->get_child_tokens()[1] ?? null; } @@ -3793,7 +3820,7 @@ private function translate( $node ): ?string { } // SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE. - if ( WP_MySQL_Lexer::SERIAL_SYMBOL === $child->id ) { + if ( WP_MySQL_Tokens::KEYWORDS['SERIAL'] === $child->id ) { return 'INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE'; } @@ -3801,54 +3828,53 @@ private function translate( $node ): ?string { throw $this->new_not_supported_exception( sprintf( 'data type: %s', $child->get_value() ) ); - case 'selectItem': + case 'select_item': return $this->translate_select_item( $node ); - case 'fromClause': + case 'from_clause': // FROM DUAL is MySQL-specific syntax that means "FROM no tables" // and it is equivalent to omitting the FROM clause entirely. - if ( $node->has_child_token( WP_MySQL_Lexer::DUAL_SYMBOL ) ) { + $from_tables = $node->get_first_child_node( 'from_tables' ); + if ( $from_tables && $from_tables->has_child_token( WP_MySQL_Tokens::KEYWORDS['DUAL'] ) ) { return null; } return $this->translate_sequence( $node->get_children() ); - case 'simpleExprBody': + case 'simple_expr': return $this->translate_simple_expr_body( $node ); - case 'predicateOperations': - $token = $node->get_first_child_token(); - if ( WP_MySQL_Lexer::LIKE_SYMBOL === $token->id ) { + case 'predicate': + if ( $node->has_child_token( WP_MySQL_Tokens::KEYWORDS['LIKE'] ) ) { return $this->translate_like( $node ); - } elseif ( WP_MySQL_Lexer::REGEXP_SYMBOL === $token->id ) { + } + if ( $node->has_child_token( WP_MySQL_Tokens::KEYWORDS['REGEXP'] ) ) { return $this->translate_regexp_functions( $node ); } return $this->translate_sequence( $node->get_children() ); - case 'runtimeFunctionCall': + case 'function_call_keyword': + case 'function_call_nonkeyword': + case 'function_call_conflict': return $this->translate_runtime_function_call( $node ); - case 'functionCall': + case 'now': + /* + * 1) SQLite doesn't support CURRENT_TIMESTAMP() with parentheses. + * 2) In MySQL, CURRENT_TIMESTAMP and CURRENT_TIMESTAMP() are an + * alias of NOW(). In SQLite, there is no NOW() function. + */ + return 'CURRENT_TIMESTAMP'; + case 'function_call_generic': return $this->translate_function_call( $node ); - case 'substringFunction': - $nodes = $node->get_child_nodes(); - if ( count( $nodes ) === 2 ) { - return sprintf( - 'SUBSTR(%s, %s)', - $this->translate( $nodes[0] ), - $this->translate( $nodes[1] ) - ); - } else { - return sprintf( - 'SUBSTR(%s, %s, %s)', - $this->translate( $nodes[0] ), - $this->translate( $nodes[1] ), - $this->translate( $nodes[2] ) - ); + case 'rvalue_system_or_user_variable': + // A user variable ("@var") or a system variable ("@@var") read. + if ( count( $node->get_child_tokens( WP_MySQL_Tokens::AT_SIGN_SYMBOL ) ) < 2 ) { + return $this->translate_user_variable( $node ); } - case 'systemVariable': - $var_ident_type = $node->get_first_child_node( 'varIdentType' ); - $type_token = $var_ident_type ? $var_ident_type->get_first_child_token() : null; - $original_name = $this->unquote_sqlite_identifier( - $this->translate( $node->get_first_child_node( 'textOrIdentifier' ) ) + + $type_node = $node->get_first_child_node( 'opt_rvalue_system_variable_type' ); + $type_token = $type_node ? $type_node->get_first_child_token() : null; + $original_name = $this->unquote_sqlite_identifier( + $this->translate( $node->get_first_child_node( 'rvalue_system_variable' ) ) ); $name = strtolower( $original_name ); - $type = $type_token ? $type_token->id : WP_MySQL_Lexer::SESSION_SYMBOL; + $type = $type_token ? $type_token->id : WP_MySQL_Tokens::KEYWORDS['SESSION']; if ( 'sql_mode' === $name ) { $value = implode( ',', $this->active_sql_modes ); } elseif ( 'version' === $name ) { @@ -3861,7 +3887,7 @@ private function translate( $node ): ?string { ); } elseif ( 'version_comment' === $name ) { $value = 'MySQL Community Server - GPL'; - } elseif ( WP_MySQL_Lexer::SESSION_SYMBOL === $type ) { + } elseif ( WP_MySQL_Tokens::KEYWORDS['SESSION'] === $type ) { $value = $this->session_system_variables[ $name ] ?? null; } else { // When we have no value, it's reasonable to use NULL. @@ -3878,18 +3904,7 @@ private function translate( $node ): ?string { return $this->quote_sqlite_value( $value ); } return (string) $value; - case 'userVariable': - $name = $this->unquote_sqlite_identifier( $this->translate( $node->get_first_child() ) ); - $name = strtolower( substr( $name, 1 ) ); // Remove '@', normalize case. - $value = $this->user_variables[ $name ] ?? null; - if ( null === $value ) { - return 'NULL'; - } - if ( is_string( $value ) ) { - return $this->quote_sqlite_value( $value ); - } - return (string) $value; - case 'castType': + case 'cast_type': $first_child = $node->get_first_child(); if ( $first_child instanceof WP_Parser_Node ) { $first_token = $first_child->get_first_child_token(); @@ -3897,43 +3912,43 @@ private function translate( $node ): ?string { $first_token = $first_child; } switch ( $first_token->id ) { - case WP_MySQL_Lexer::BINARY_SYMBOL: + case WP_MySQL_Tokens::KEYWORDS['BINARY']: return 'BLOB'; - case WP_MySQL_Lexer::CHAR_SYMBOL: - case WP_MySQL_Lexer::NCHAR_SYMBOL: - case WP_MySQL_Lexer::NATIONAL_SYMBOL: - case WP_MySQL_Lexer::DATE_SYMBOL: - case WP_MySQL_Lexer::TIME_SYMBOL: - case WP_MySQL_Lexer::DATETIME_SYMBOL: - case WP_MySQL_Lexer::JSON_SYMBOL: + case WP_MySQL_Tokens::KEYWORDS['CHAR']: + case WP_MySQL_Tokens::KEYWORDS['NCHAR']: + case WP_MySQL_Tokens::KEYWORDS['NATIONAL']: + case WP_MySQL_Tokens::KEYWORDS['DATE']: + case WP_MySQL_Tokens::KEYWORDS['TIME']: + case WP_MySQL_Tokens::KEYWORDS['DATETIME']: + case WP_MySQL_Tokens::KEYWORDS['JSON']: return 'TEXT'; - case WP_MySQL_Lexer::SIGNED_SYMBOL: - case WP_MySQL_Lexer::UNSIGNED_SYMBOL: + case WP_MySQL_Tokens::KEYWORDS['SIGNED']: + case WP_MySQL_Tokens::KEYWORDS['UNSIGNED']: // @TODO: Emulate UNSIGNED semantics. MySQL wraps negative // values, but SQLite has no unsigned integer type. return 'INTEGER'; - case WP_MySQL_Lexer::DECIMAL_SYMBOL: - case WP_MySQL_Lexer::FLOAT_SYMBOL: - case WP_MySQL_Lexer::REAL_SYMBOL: - case WP_MySQL_Lexer::DOUBLE_SYMBOL: + case WP_MySQL_Tokens::KEYWORDS['DECIMAL']: + case WP_MySQL_Tokens::KEYWORDS['FLOAT']: + case WP_MySQL_Tokens::KEYWORDS['REAL']: + case WP_MySQL_Tokens::KEYWORDS['DOUBLE']: return 'REAL'; default: throw $this->new_not_supported_exception( sprintf( 'cast type: %s', $first_child->get_value() ) ); } - case 'defaultCollation': + case 'default_collation': // @TODO: Check and save in information schema. return null; - case 'duplicateAsQueryExpression': + case 'duplicate_as_qe': // @TODO: How to handle IGNORE/REPLACE? // The "AS" keyword is optional in MySQL, but required in SQLite. return 'AS ' . $this->translate( $node->get_first_child_node() ); - case 'indexHint': - case 'indexHintList': + case 'opt_key_definition': + // Index hints (USE/FORCE/IGNORE INDEX) are not supported in SQLite. return null; - case 'lockingClause': + case 'locking_clause': // SQLite doesn't support locking clauses (SELECT ... FOR UPDATE). // They are not needed in SQLite due to the database file locking. return null; @@ -3942,6 +3957,26 @@ private function translate( $node ): ?string { } } + /** + * Translate a MySQL user variable read ("@var") to SQLite. + * + * @param WP_Parser_Node $node The AST node holding the "@" sign and the + * user variable name. + * @return string The translated value. + */ + private function translate_user_variable( WP_Parser_Node $node ): string { + $name = $this->unquote_sqlite_identifier( $this->translate( $node->get_first_child_node() ) ); + $name = strtolower( $name ); + $value = $this->user_variables[ $name ] ?? null; + if ( null === $value ) { + return 'NULL'; + } + if ( is_string( $value ) ) { + return $this->quote_sqlite_value( $value ); + } + return (string) $value; + } + /** * Translate a MySQL token to SQLite. * @@ -3950,9 +3985,9 @@ private function translate( $node ): ?string { */ private function translate_token( WP_MySQL_Token $token ): ?string { switch ( $token->id ) { - case WP_MySQL_Lexer::EOF: + case WP_MySQL_Tokens::END_OF_INPUT: return null; - case WP_MySQL_Lexer::BIN_NUMBER: + case WP_MySQL_Tokens::BIN_NUMBER: /* * There are no binary literals in SQLite. We need to convert all * MySQL binary string values to HEX strings in SQLite (x'...'). @@ -3977,7 +4012,7 @@ private function translate_token( WP_MySQL_Token $token ): ?string { $byte_count = (int) ceil( strlen( $value ) / 8 ); $hex = str_pad( $hex, $byte_count * 2, '0', STR_PAD_LEFT ); return sprintf( "x'%s'", $hex ); - case WP_MySQL_Lexer::HEX_NUMBER: + case WP_MySQL_Tokens::HEX_NUMBER: /* * In MySQL, "0x" prefixed values represent binary literal values, * while in SQLite, that would be a hexadecimal number. Therefore, @@ -3988,16 +4023,16 @@ private function translate_token( WP_MySQL_Token $token ): ?string { return sprintf( "x'%s'", substr( $value, 2 ) ); } return $value; - case WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL: + case WP_MySQL_Tokens::KEYWORDS['AUTO_INCREMENT']: return 'AUTOINCREMENT'; - case WP_MySQL_Lexer::BINARY_SYMBOL: + case WP_MySQL_Tokens::KEYWORDS['BINARY']: /* * "BINARY expr" is translated in "translate_simple_expr_body()". * Returning null here is a safety net for any unhandled context * where a bare BINARY token would otherwise leak into the output. */ return null; - case WP_MySQL_Lexer::SQL_CALC_FOUND_ROWS_SYMBOL: + case WP_MySQL_Tokens::KEYWORDS['SQL_CALC_FOUND_ROWS']: /* * The "SQL_CALC_FOUND_ROWS" keyword is implemented in the select * statement translation and then removed from the output here. @@ -4038,12 +4073,16 @@ private function translate_sequence( array $nodes, string $separator = ' ' ): ?s /** * Translate a MySQL string literal to SQLite. * - * @param WP_Parser_Node $node The "textStringLiteral" AST node. + * @param WP_Parser_Node $node The "text_literal" AST node. * @return string The translated value. */ private function translate_string_literal( WP_Parser_Node $node ): string { - $token = $node->get_first_child_token(); - $value = $token->get_value(); + /* + * A text literal is a sequence of one or more string tokens, possibly + * with a character set prefix ("_utf8mb4'abc'"). MySQL concatenates + * adjacent string literals ("'a' 'b'" is equivalent to "'ab'"). + */ + $value = $this->collect_text_literal_value( $node ); /* * Translate datetime literals. @@ -4087,10 +4126,31 @@ private function translate_string_literal( WP_Parser_Node $node ): string { return $this->quote_sqlite_value( $value ); } + /** + * Collect the string value of a text literal node. + * + * The value of adjacent string tokens is concatenated, and the character + * set prefix ("_utf8mb4'abc'"), if any, is omitted. + * + * @param WP_Parser_Node $node The "text_literal" AST node. + * @return string The string value of the literal. + */ + private function collect_text_literal_value( WP_Parser_Node $node ): string { + $value = ''; + foreach ( $node->get_children() as $child ) { + if ( $child instanceof WP_Parser_Node ) { + $value .= $this->collect_text_literal_value( $child ); + } elseif ( WP_MySQL_Tokens::UNDERSCORE_CHARSET !== $child->id ) { + $value .= $child->get_value(); + } + } + return $value; + } + /** * Translate a MySQL pure identifier to SQLite. * - * @param WP_Parser_Node $node The "pureIdentifier" AST node. + * @param WP_Parser_Node $node The "IDENT_sys" AST node. * @return string The translated value. */ private function translate_pure_identifier( WP_Parser_Node $node ): string { @@ -4171,16 +4231,20 @@ private function translate_qualified_identifier( * @throws WP_SQLite_Driver_Exception When the translation fails. */ private function translate_query_expression( WP_Parser_Node $node ): string { - // Get the query expression subnode under which we need to look for the - // SELECT item list node. This prevents searching under "withClause". - $query_expr_main = ( - $node->get_first_child_node( 'queryExpressionBody' ) - ?? $node->get_first_child_node( 'queryExpressionParens' ) + /* + * Get the query expression subnode under which we need to look for the + * SELECT item list node. This prevents searching under "with_clause". + * + * Set operations nest through the left-recursive "query_expression_body" + * rule, so any top-level UNION/EXCEPT/INTERSECT operator appears as a + * direct child token of the outermost "query_expression_body" node. + */ + $query_expr_main = $node->get_first_child_node( 'query_expression_body' ); + $has_set_operation = ( + $query_expr_main->has_child_token( WP_MySQL_Tokens::KEYWORDS['UNION'] ) + || $query_expr_main->has_child_token( WP_MySQL_Tokens::KEYWORDS['EXCEPT'] ) + || $query_expr_main->has_child_token( WP_MySQL_Tokens::KEYWORDS['INTERSECT'] ) ); - $query_term = $query_expr_main->get_first_descendant_node( 'queryTerm' ); - $has_union = $query_expr_main->has_child_token( WP_MySQL_Lexer::UNION_SYMBOL ); - $has_except = $query_expr_main->has_child_token( WP_MySQL_Lexer::EXCEPT_SYMBOL ); - $has_intersect = $query_term->has_child_token( WP_MySQL_Lexer::INTERSECT_SYMBOL ); /* * When the ORDER BY clause is present, we need to disambiguate the item @@ -4189,30 +4253,28 @@ private function translate_query_expression( WP_Parser_Node $node ): string { * @see WP_SQLite_Driver::disambiguate_item() */ $disambiguated_order_list = array(); - $order_clause = $node->get_first_child_node( 'orderClause' ); - if ( $order_clause && ! $has_union && ! $has_except && ! $has_intersect ) { + $order_clause = $node->get_first_child_node( 'opt_order_clause' ); + if ( $order_clause && ! $has_set_operation ) { /* * [GRAMMAR] - * queryExpression: (withClause)? ( - * queryExpressionBody orderClause? limitClause? - * | queryExpressionParens orderClause? limitClause? - * ) (procedureAnalyseClause)? + * query_expression: opt_with_clause query_expression_body + * opt_order_clause opt_limit_clause */ // Create the SELECT item disambiguation map. - $select_item_list = $query_expr_main->get_first_descendant_node( 'selectItemList' ); + $select_item_list = $query_expr_main->get_first_descendant_node( 'select_item_list' ); $disambiguation_map = $this->create_select_item_disambiguation_map( $select_item_list ); - // For each "orderList" item, search for a matching SELECT item. + // For each "order_list" item, search for a matching SELECT item. $disambiguated_order_list = array(); - $order_list = $order_clause->get_first_child_node( 'orderList' ); - foreach ( $order_list->get_child_nodes() as $order_item ) { + $order_list = $order_clause->get_first_descendant_node( 'order_list' ); + foreach ( $order_list->get_flattened_child_nodes( 'order_expr' ) as $order_item ) { /* * [GRAMMAR] - * orderExpression: expr direction? + * order_expr: expr opt_ordering_direction */ $order_expr = $order_item->get_first_child_node( 'expr' ); - $order_direction = $order_item->get_first_child_node( 'direction' ); + $order_direction = $order_item->get_first_child_node( 'opt_ordering_direction' ); $disambiguated_item = $this->disambiguate_item( $disambiguation_map, $order_expr ); $disambiguated_order_list[] = sprintf( @@ -4226,7 +4288,7 @@ private function translate_query_expression( WP_Parser_Node $node ): string { // the one that was constructed using the disambiguation algorithm. $parts = array(); foreach ( $node->get_children() as $child ) { - if ( $child instanceof WP_Parser_Node && 'orderClause' === $child->rule_name ) { + if ( $child instanceof WP_Parser_Node && 'opt_order_clause' === $child->rule_name ) { $parts[] = 'ORDER BY ' . implode( ', ', $disambiguated_order_list ); } else { $parts[] = $this->translate( $child ); @@ -4247,8 +4309,8 @@ private function translate_query_expression( WP_Parser_Node $node ): string { * @return string|null */ private function translate_query_specification( WP_Parser_Node $node ): string { - $group_by = $node->get_first_child_node( 'groupByClause' ); - $having = $node->get_first_child_node( 'havingClause' ); + $group_by = $node->get_first_child_node( 'opt_group_clause' ); + $having = $node->get_first_child_node( 'opt_having_clause' ); /* * When the GROUP BY or HAVING clause is present, we need to disambiguate @@ -4260,7 +4322,7 @@ private function translate_query_specification( WP_Parser_Node $node ): string { $having_clause = null; if ( $group_by || $having ) { // Build a SELECT list disambiguation map for both GROUP BY and HAVING. - $select_item_list = $node->get_first_child_node( 'selectItemList' ); + $select_item_list = $node->get_first_child_node( 'select_item_list' ); $disambiguation_map = $this->create_select_item_disambiguation_map( $select_item_list ); // Disambiguate the GROUP BY clause column references. @@ -4268,10 +4330,11 @@ private function translate_query_specification( WP_Parser_Node $node ): string { if ( $group_by ) { /* * [GRAMMAR] - * groupByClause: GROUP_SYMBOL BY_SYMBOL orderList olapOption? + * opt_group_clause: GROUP_SYM BY group_list olap_opt + * group_list: group_list ',' grouping_expr | grouping_expr */ - $group_by_list = $group_by->get_first_child_node( 'orderList' ); - foreach ( $group_by_list->get_child_nodes() as $group_by_item ) { + $group_by_list = $group_by->get_first_child_node( 'group_list' ); + foreach ( $group_by_list->get_flattened_child_nodes( 'grouping_expr' ) as $group_by_item ) { $group_by_expr = $group_by_item->get_first_child_node( 'expr' ); $disambiguated_item = $this->disambiguate_item( $disambiguation_map, $group_by_expr ); $disambiguated_group_by_list[] = $disambiguated_item ?? $this->translate( $group_by_expr ); @@ -4284,7 +4347,7 @@ private function translate_query_specification( WP_Parser_Node $node ): string { if ( $having ) { /* * [GRAMMAR] - * havingClause: HAVING_SYMBOL expr + * opt_having_clause: HAVING expr */ $having_expr = $having->get_first_child_node(); $having_expr_children = $having_expr->get_children(); @@ -4303,9 +4366,9 @@ private function translate_query_specification( WP_Parser_Node $node ): string { // items with the ones that were disambiguated using the SELECT list. $parts = array(); foreach ( $node->get_children() as $child ) { - if ( $child instanceof WP_Parser_Node && 'groupByClause' === $child->rule_name ) { + if ( $child instanceof WP_Parser_Node && 'opt_group_clause' === $child->rule_name ) { $parts[] = $group_by_clause; - } elseif ( $child instanceof WP_Parser_Node && 'havingClause' === $child->rule_name ) { + } elseif ( $child instanceof WP_Parser_Node && 'opt_having_clause' === $child->rule_name ) { // SQLite doesn't allow using the "HAVING" clause without "GROUP BY". // In such cases, let's prefix the "HAVING" clause with "GROUP BY 1". if ( ! $group_by ) { @@ -4335,10 +4398,10 @@ private function translate_simple_expr_body( WP_Parser_Node $node ): string { $token = $node->get_first_child_token(); // Translate "VALUES(col)" to "excluded.col" in ON DUPLICATE KEY UPDATE. - if ( null !== $token && WP_MySQL_Lexer::VALUES_SYMBOL === $token->id ) { + if ( null !== $token && WP_MySQL_Tokens::KEYWORDS['VALUES'] === $token->id ) { return sprintf( '`excluded`.%s', - $this->translate( $node->get_first_child_node( 'simpleIdentifier' ) ) + $this->translate( $node->get_first_child_node( 'simple_ident_nospvar' ) ) ); } @@ -4348,15 +4411,15 @@ private function translate_simple_expr_body( WP_Parser_Node $node ): string { * The MySQL BINARY operator enforces byte-by-byte string comparison. * In SQLite, COLLATE BINARY is equivalent in comparison contexts. */ - if ( null !== $token && WP_MySQL_Lexer::BINARY_SYMBOL === $token->id ) { - $expr = $node->get_first_child_node( 'simpleExpr' ); + if ( null !== $token && WP_MySQL_Tokens::KEYWORDS['BINARY'] === $token->id ) { + $expr = $node->get_first_child_node( 'simple_expr' ); return sprintf( '%s COLLATE BINARY', $this->translate( $expr ) ); } // Translate "CAST(expr AS type)" to its SQLite equivalent. - if ( null !== $token && WP_MySQL_Lexer::CAST_SYMBOL === $token->id ) { + if ( null !== $token && WP_MySQL_Tokens::KEYWORDS['CAST'] === $token->id ) { $expr = $node->get_first_child_node( 'expr' ); - $cast_type = $node->get_first_child_node( 'castType' ); + $cast_type = $node->get_first_child_node( 'cast_type' ); return $this->translate_cast_expr( $expr, $cast_type ); } @@ -4367,9 +4430,9 @@ private function translate_simple_expr_body( WP_Parser_Node $node ): string { * 1. CONVERT(expr, type): Equivalent to CAST(expr AS type). * 2. CONVERT(expr USING charset): Converts the character set. */ - if ( null !== $token && WP_MySQL_Lexer::CONVERT_SYMBOL === $token->id ) { + if ( null !== $token && WP_MySQL_Tokens::KEYWORDS['CONVERT'] === $token->id ) { $expr = $node->get_first_child_node( 'expr' ); - $cast_type = $node->get_first_child_node( 'castType' ); + $cast_type = $node->get_first_child_node( 'cast_type' ); if ( null !== $cast_type ) { // CONVERT(expr, type): Translate to cast expression. @@ -4400,7 +4463,7 @@ private function translate_cast_expr( WP_Parser_Node $expr, WP_Parser_Node $cast * Emitting "CAST(expr AS BLOB)" would break equality against TEXT values * due to SQLite's storage-class ordering (BLOB > TEXT). */ - if ( $cast_type->has_child_token( WP_MySQL_Lexer::BINARY_SYMBOL ) ) { + if ( $cast_type->has_child_token( WP_MySQL_Tokens::KEYWORDS['BINARY'] ) ) { return sprintf( 'CAST(%s AS TEXT) COLLATE BINARY', $this->translate( $expr ) ); } return sprintf( 'CAST(%s AS %s)', $this->translate( $expr ), $this->translate( $cast_type ) ); @@ -4409,19 +4472,35 @@ private function translate_cast_expr( WP_Parser_Node $expr, WP_Parser_Node $cast /** * Translate a MySQL LIKE expression to SQLite. * - * @param WP_Parser_Node $node The "predicateOperations" AST node. + * @param WP_Parser_Node $node The "predicate" AST node with a LIKE operator. * @return string The translated value. * @throws WP_SQLite_Driver_Exception When the translation fails. */ private function translate_like( WP_Parser_Node $node ): string { - $tokens = $node->get_descendant_tokens(); - $is_binary = isset( $tokens[1] ) && WP_MySQL_Lexer::BINARY_SYMBOL === $tokens[1]->id; + /* + * [GRAMMAR] + * predicate: bit_expr (not)? LIKE simple_expr (ESCAPE simple_expr)? | ... + */ + $children = $node->get_children(); + $like_index = 0; + foreach ( $children as $i => $child ) { + if ( $child instanceof WP_MySQL_Token && WP_MySQL_Tokens::KEYWORDS['LIKE'] === $child->id ) { + $like_index = $i; + break; + } + } + $pattern = $children[ $like_index + 1 ] ?? null; + $is_binary = ( + $pattern instanceof WP_Parser_Node + && null !== $pattern->get_first_child_token( WP_MySQL_Tokens::KEYWORDS['BINARY'] ) + ); + $has_escape = $node->has_child_token( WP_MySQL_Tokens::KEYWORDS['ESCAPE'] ); if ( true === $is_binary ) { - $children = $node->get_children(); return sprintf( - 'GLOB _helper_like_to_glob_pattern(%s)', - $this->translate( $children[1] ) + '%s GLOB _helper_like_to_glob_pattern(%s)', + $this->translate_sequence( array_slice( $children, 0, $like_index ) ), + $this->translate( $pattern ) ); } @@ -4443,8 +4522,8 @@ private function translate_like( WP_Parser_Node $node ): string { * We'll probably need to overload the like() function: * https://www.sqlite.org/lang_corefunc.html#like */ - $statement = $this->translate_sequence( $node->get_children() ); - if ( $this->is_sql_mode_active( 'NO_BACKSLASH_ESCAPES' ) ) { + $statement = $this->translate_sequence( $children ); + if ( $has_escape || $this->is_sql_mode_active( 'NO_BACKSLASH_ESCAPES' ) ) { return $statement; } return $statement . " ESCAPE '\\'"; @@ -4453,13 +4532,27 @@ private function translate_like( WP_Parser_Node $node ): string { /** * Translate MySQL REGEXP expression to SQLite. * - * @param WP_Parser_Node $node The "predicateOperations" AST node. + * @param WP_Parser_Node $node The "predicate" AST node with a REGEXP operator. * @return string The translated value. * @throws WP_SQLite_Driver_Exception When the translation fails. */ private function translate_regexp_functions( WP_Parser_Node $node ): string { - $tokens = $node->get_descendant_tokens(); - $is_binary = isset( $tokens[1] ) && WP_MySQL_Lexer::BINARY_SYMBOL === $tokens[1]->id; + /* + * [GRAMMAR] + * predicate: bit_expr (not)? REGEXP bit_expr | ... + */ + $children = $node->get_children(); + $regexp_index = 0; + foreach ( $children as $i => $child ) { + if ( $child instanceof WP_MySQL_Token && WP_MySQL_Tokens::KEYWORDS['REGEXP'] === $child->id ) { + $regexp_index = $i; + break; + } + } + $subject = $this->translate_sequence( array_slice( $children, 0, $regexp_index ) ); + $pattern = $children[ $regexp_index + 1 ] ?? null; + $pattern_token = $pattern instanceof WP_Parser_Node ? $pattern->get_first_descendant_token() : null; + $is_binary = null !== $pattern_token && WP_MySQL_Tokens::KEYWORDS['BINARY'] === $pattern_token->id; /* * If the query says REGEXP BINARY, the comparison is byte-by-byte @@ -4475,15 +4568,19 @@ private function translate_regexp_functions( WP_Parser_Node $node ): string { * regular expressions anyway. */ if ( true === $is_binary ) { - return 'REGEXP CHAR(0) || ' . $this->translate( $node->get_first_child_node() ); + return $subject . ' REGEXP CHAR(0) || ' . $this->translate( $pattern ); } - return 'REGEXP ' . $this->translate( $node->get_first_child_node() ); + return $subject . ' REGEXP ' . $this->translate( $pattern ); } /** * Translate a MySQL runtime function call to SQLite. * - * @param WP_Parser_Node $node The "runtimeFunctionCall" AST node. + * Runtime function calls are functions with their own grammar rules, i.e. + * the "function_call_keyword", "function_call_nonkeyword", and + * "function_call_conflict" AST nodes. + * + * @param WP_Parser_Node $node The function call AST node. * @return string The translated value. * @throws WP_SQLite_Driver_Exception When the translation fails. */ @@ -4494,18 +4591,17 @@ private function translate_runtime_function_call( WP_Parser_Node $node ): string } switch ( $child->id ) { - case WP_MySQL_Lexer::DATABASE_SYMBOL: + case WP_MySQL_Tokens::KEYWORDS['DATABASE']: return $this->quote_sqlite_value( $this->db_name ); - case WP_MySQL_Lexer::CURRENT_TIMESTAMP_SYMBOL: - case WP_MySQL_Lexer::NOW_SYMBOL: + case WP_MySQL_Tokens::KEYWORDS['CURRENT_TIMESTAMP']: /* * 1) SQLite doesn't support CURRENT_TIMESTAMP() with parentheses. * 2) In MySQL, CURRENT_TIMESTAMP and CURRENT_TIMESTAMP() are an * alias of NOW(). In SQLite, there is no NOW() function. */ return 'CURRENT_TIMESTAMP'; - case WP_MySQL_Lexer::DATE_ADD_SYMBOL: - case WP_MySQL_Lexer::DATE_SUB_SYMBOL: + case WP_MySQL_Tokens::KEYWORDS['DATE_ADD']: + case WP_MySQL_Tokens::KEYWORDS['DATE_SUB']: $nodes = $node->get_child_nodes(); $value = $this->translate( $nodes[1] ); $unit = $this->translate( $nodes[2] ); @@ -4516,17 +4612,33 @@ private function translate_runtime_function_call( WP_Parser_Node $node ): string return sprintf( "DATETIME(%s, '%s' || %s || ' %s')", $this->translate( $nodes[0] ), - WP_MySQL_Lexer::DATE_SUB_SYMBOL === $child->id ? '-' : '+', + WP_MySQL_Tokens::KEYWORDS['DATE_SUB'] === $child->id ? '-' : '+', $value, $unit ); - case WP_MySQL_Lexer::LEFT_SYMBOL: + case WP_MySQL_Tokens::KEYWORDS['LEFT']: $nodes = $node->get_child_nodes(); return sprintf( 'SUBSTR(%s, 1, %s)', $this->translate( $nodes[0] ), $this->translate( $nodes[1] ) ); + case WP_MySQL_Tokens::KEYWORDS['SUBSTRING']: + $nodes = $node->get_child_nodes(); + if ( count( $nodes ) === 2 ) { + return sprintf( + 'SUBSTR(%s, %s)', + $this->translate( $nodes[0] ), + $this->translate( $nodes[1] ) + ); + } else { + return sprintf( + 'SUBSTR(%s, %s, %s)', + $this->translate( $nodes[0] ), + $this->translate( $nodes[1] ), + $this->translate( $nodes[2] ) + ); + } default: return $this->translate_sequence( $node->get_children() ); } @@ -4535,7 +4647,7 @@ private function translate_runtime_function_call( WP_Parser_Node $node ): string /** * Translate a MySQL function call to SQLite. * - * @param WP_Parser_Node $node The "functionCall" AST node. + * @param WP_Parser_Node $node The "function_call_generic" AST node. * @return string The translated value. * @throws WP_SQLite_Driver_Exception When the translation fails. */ @@ -4545,9 +4657,10 @@ private function translate_function_call( WP_Parser_Node $node ): string { $this->unquote_sqlite_identifier( $this->translate( $nodes[0] ) ) ); - $args = array(); - if ( isset( $nodes[1] ) ) { - foreach ( $nodes[1]->get_child_nodes() as $child ) { + $args = array(); + $args_list = $node->get_first_child_node( 'opt_udf_expr_list' ); + if ( null !== $args_list ) { + foreach ( $args_list->get_first_child_node()->get_flattened_child_nodes( 'udf_expr' ) as $child ) { $args[] = $this->translate( $child ); } } @@ -4743,17 +4856,17 @@ public function translate_select_item( WP_Parser_Node $node ): string { * First, let's translate the select item subtree. * * [GRAMMAR] - * selectItem: tableWild | (expr selectAlias?) + * select_item: table_wild | expr select_alias */ $item = $this->translate_sequence( $node->get_children() ); // A table wildcard (e.g., "SELECT *, t.*, ...") never has an alias. - if ( $node->has_child_node( 'tableWild' ) ) { + if ( $node->has_child_node( 'table_wild' ) ) { return $item; } // When an explicit alias is provided, we can use it as is. - $alias = $node->get_first_child_node( 'selectAlias' ); + $alias = $node->get_first_child_node( 'select_alias' ); if ( $alias ) { return $item; } @@ -4769,7 +4882,7 @@ public function translate_select_item( WP_Parser_Node $node ): string { * In this case, SQLite uses the same logic as MySQL, so using the value * as is without adding an explicit alias will produce the correct result. */ - $column_ref = $node->get_first_descendant_node( 'columnRef' ); + $column_ref = $node->get_first_descendant_node( 'simple_ident' ); $is_column_ref = $column_ref && $item === $this->translate( $column_ref ); if ( $is_column_ref ) { return $item; @@ -4784,19 +4897,19 @@ public function translate_select_item( WP_Parser_Node $node ): string { * For example, for "SELECT 'abc'", the resulting column name is "abc" * in MySQL, but would be "'abc'" in SQLite if an alias was not used. * - * Descend the AST until we reach a textStringLiteral. If at any level + * Descend the AST until we reach a text_literal. If at any level * we don't have a single child node, bail out; it's not a bare literal. */ $current = $node; - while ( 'textStringLiteral' !== $current->rule_name ) { + while ( 'text_literal' !== $current->rule_name ) { $children = $current->get_children(); if ( 1 !== count( $children ) || ! $children[0] instanceof WP_Parser_Node ) { break; } $current = $children[0]; } - if ( 'textStringLiteral' === $current->rule_name ) { - $alias = $current->get_first_child_token()->get_value(); + if ( 'text_literal' === $current->rule_name ) { + $alias = $this->collect_text_literal_value( $current ); // When the literal value contains a NULL byte, MySQL truncates the // resulting identifier at the position of the first one of them. @@ -4850,13 +4963,13 @@ public function translate_select_item( WP_Parser_Node $node ): string { * * The same logic will be applied to table references in JOIN clauses as well. * - * @param WP_Parser_Node $node The "tableRef" AST node. + * @param WP_Parser_Node $node The "table_ident" AST node. * @return string The translated value. * @throws WP_SQLite_Driver_Exception When the translation fails. */ public function translate_table_ref( WP_Parser_Node $node ): string { // The table reference is in "." or "
" format. - $parts = $node->get_descendant_nodes( 'identifier' ); + $parts = $node->get_descendant_nodes( 'ident' ); $table = array_pop( $parts ); $schema = array_pop( $parts ); @@ -4891,7 +5004,7 @@ public function translate_table_ref( WP_Parser_Node $node ): string { )->fetchAll( PDO::FETCH_COLUMN ); if ( count( $columns ) === 0 ) { - return $this->translate_sequence( $node->get_children() ); + return $this->translate_qualified_identifier( $schema, $table ); } // List all columns in the table, replacing columns targeting database @@ -4958,7 +5071,7 @@ public function translate_table_ref( WP_Parser_Node $node ): string { $this->quote_sqlite_identifier( $table_name ) ); } - return $this->translate_sequence( $node->get_children() ); + return $this->translate_qualified_identifier( $schema, $table ); } /** @@ -5075,11 +5188,11 @@ private function apply_auto_increment_table_option( ): void { // Find the last AUTO_INCREMENT = N option (MySQL uses the last one). $value = null; - foreach ( $node->get_descendant_nodes( 'createTableOption' ) as $option ) { - if ( ! $option->has_child_token( WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL ) ) { + foreach ( $node->get_descendant_nodes( 'create_table_option' ) as $option ) { + if ( ! $option->has_child_token( WP_MySQL_Tokens::KEYWORDS['AUTO_INCREMENT'] ) ) { continue; } - $number_node = $option->get_first_child_node( 'ulonglong_number' ); + $number_node = $option->get_first_child_node( 'ulonglong_num' ); if ( null === $number_node ) { continue; } @@ -5140,16 +5253,15 @@ private function apply_auto_increment_table_option( /** * Translate a MySQL SHOW LIKE ... or SHOW WHERE ... condition to SQLite. * - * @param WP_Parser_Node $like_or_where The "likeOrWhere" AST node. + * @param WP_Parser_Node $like_or_where The "opt_wild_or_where" AST node. * @param string $like_column The column name to use in the LIKE clause ("table_name", "column_name", etc.). * @return string The translated value. * @throws WP_SQLite_Driver_Exception When the translation fails. */ private function translate_show_like_or_where_condition( WP_Parser_Node $like_or_where, string $like_column ): string { - $like_clause = $like_or_where->get_first_child_node( 'likeClause' ); - if ( null !== $like_clause ) { - $value = $this->translate( - $like_clause->get_first_child_node( 'textStringLiteral' ) + if ( $like_or_where->has_child_token( WP_MySQL_Tokens::KEYWORDS['LIKE'] ) ) { + $value = $this->translate_string_literal( + $like_or_where->get_first_child_node( 'TEXT_STRING_literal' ) ); return sprintf( "AND %s LIKE %s ESCAPE '\\'", @@ -5158,7 +5270,7 @@ private function translate_show_like_or_where_condition( WP_Parser_Node $like_or ); } - $where_clause = $like_or_where->get_first_child_node( 'whereClause' ); + $where_clause = $like_or_where->get_first_child_node( 'where_clause' ); if ( null !== $where_clause ) { $value = $this->translate( $where_clause->get_first_child_node( 'expr' ) @@ -5221,7 +5333,7 @@ private function translate_show_like_or_where_condition( WP_Parser_Node $like_or * https://dev.mysql.com/doc/refman/8.4/en/data-type-defaults.html#data-type-defaults-implicit * * @param string $table_name The name of the target table. - * @param WP_Parser_Node $node The "insertQueryExpression" or "insertValues" AST node. + * @param WP_Parser_Node $node The "insert_from_constructor", "insert_query_expression", or "update_list" AST node. * @return string The translated INSERT query body. */ private function translate_insert_or_replace_body( @@ -5265,17 +5377,17 @@ private function translate_insert_or_replace_body( // Get a list of columns that are targeted by the INSERT or REPLACE query. // This is either an explicit column list, or all columns of the table. $insert_list = array(); - $fields_node = $node->get_first_child_node( 'fields' ); + $fields_node = $node->get_first_child_node( 'insert_columns' ); if ( $fields_node ) { // "INSERT INTO ... (column1, column2, ...)" - foreach ( $fields_node->get_child_nodes() as $field ) { + foreach ( $fields_node->get_flattened_child_nodes( 'insert_column' ) as $field ) { $column_name = $this->unquote_sqlite_identifier( $this->translate( $field ) ); $insert_list[] = strtolower( $column_name ); } - } elseif ( 'updateList' === $node->rule_name ) { + } elseif ( 'update_list' === $node->rule_name ) { // "INSERT INTO ... SET column1 = value1, column2 = value2, ..." - foreach ( $node->get_child_nodes( 'updateElement' ) as $update_element ) { - $column_ref = $update_element->get_first_child_node( 'columnRef' ); + foreach ( $node->get_flattened_child_nodes( 'update_elem' ) as $update_element ) { + $column_ref = $update_element->get_first_child_node( 'simple_ident_nospvar' ); $column_name = $this->unquote_sqlite_identifier( $this->translate( $column_ref ) ); $insert_list[] = strtolower( $column_name ); } @@ -5336,10 +5448,10 @@ function ( $column ) use ( $is_strict_mode, $insert_map ) { * INSERT INTO ... SELECT FROM () */ $select_list = array(); - if ( 'insertQueryExpression' === $node->rule_name ) { + if ( 'insert_query_expression' === $node->rule_name ) { // When inserting from a SELECT query, we don't know the column names. // Let's wrap the query with a "SELECT (...) LIMIT 0" to obtain them. - $expr = $node->get_first_child_node( 'queryExpressionOrParens' ); + $expr = $node->get_first_child_node( 'query_expression_with_opt_locking_clauses' ); $stmt = $this->execute_sqlite_query( 'SELECT * FROM (' . $this->translate( $expr ) . ') LIMIT 1' ); @@ -5431,7 +5543,7 @@ function ( $column ) use ( $is_strict_mode, $insert_map ) { * AUTO_INCREMENT columns are excluded. A NULL value advances * the sequence regardless of the column's nullability. */ - $is_insert_from_select = 'insertQueryExpression' === $node->rule_name; + $is_insert_from_select = 'insert_query_expression' === $node->rule_name; if ( ! $is_strict_mode && ! $is_auto_increment @@ -5448,9 +5560,9 @@ function ( $column ) use ( $is_strict_mode, $insert_map ) { } // Wrap the original insert VALUES, SELECT, or SET list in a FROM clause. - if ( 'insertFromConstructor' === $node->rule_name ) { + if ( 'insert_from_constructor' === $node->rule_name ) { // VALUES (...) - $insert_values = $node->get_first_child_node( 'insertValues' ); + $insert_values = $node->get_first_child_node( 'insert_values' ); $from = $this->translate( $insert_values ); /** @@ -5465,11 +5577,11 @@ function ( $column ) use ( $is_strict_mode, $insert_map ) { */ $is_values_naming_supported = version_compare( $this->get_sqlite_version(), '3.33.0', '>=' ); if ( ! $is_values_naming_supported ) { - $values_list = $insert_values->get_first_child_node( 'valueList' ); - $values = $values_list->get_first_child_node( 'values' ); + $values_list = $insert_values->get_first_child_node( 'values_list' ); + $first_row = $values_list->get_flattened_child_nodes( 'row_value' )[0]; + $values = $first_row->get_first_descendant_node( 'values' ); $value_count = ( - count( $values->get_child_nodes( 'expr' ) ) - + count( $values->get_child_nodes( WP_MySQL_Lexer::DEFAULT_SYMBOL ) ) + null === $values ? 0 : count( $values->get_flattened_child_nodes( 'expr_or_default' ) ) ); $columns_list = ''; @@ -5479,16 +5591,16 @@ function ( $column ) use ( $is_strict_mode, $insert_map ) { } $from = 'SELECT ' . $columns_list . ' WHERE FALSE UNION ALL ' . $from; } - } elseif ( 'insertQueryExpression' === $node->rule_name ) { + } elseif ( 'insert_query_expression' === $node->rule_name ) { // SELECT ... $from = $this->translate( - $node->get_first_child_node( 'queryExpressionOrParens' ) + $node->get_first_child_node( 'query_expression_with_opt_locking_clauses' ) ); } else { // SET c1 = v1, c2 = v2, ... $values = array(); - foreach ( $node->get_child_nodes( 'updateElement' ) as $update_element ) { - $values[] = $this->translate( $update_element->get_first_child_node( 'expr' ) ); + foreach ( $node->get_flattened_child_nodes( 'update_elem' ) as $update_element ) { + $values[] = $this->translate( $update_element->get_first_child_node( 'expr_or_default' ) ); } $from = 'VALUES (' . implode( ', ', $values ) . ')'; } @@ -5537,11 +5649,11 @@ function ( $column ) use ( $is_strict_mode, $insert_map ) { * https://dev.mysql.com/doc/refman/8.4/en/data-type-defaults.html#data-type-defaults-implicit * * @param string $table_name The name of the target table. - * @param WP_Parser_Node $parent_node The "updateList" AST node parent node. + * @param WP_Parser_Node $parent_node The "update_list" AST node parent node. * @return string The translated UPDATE list. */ private function translate_update_list( string $table_name, WP_Parser_Node $parent_node ): string { - $node = $parent_node->get_first_child_node( 'updateList' ); + $node = $parent_node->get_first_child_node( 'update_list' ); // This method is always used with the main database. $database = $this->get_saved_db_name( $this->main_db_name ); @@ -5580,10 +5692,11 @@ private function translate_update_list( string $table_name, WP_Parser_Node $pare // Translate the UPDATE list, emulating IMPLICIT DEFAULTs for NULL values. $fragment = ''; - foreach ( $node->get_child_nodes() as $i => $update_element ) { - $column_ref = $update_element->get_first_child_node( 'columnRef' ); - $column_ref_parts = $column_ref->get_descendant_nodes( 'identifier' ); - $expr = $update_element->get_first_child_node( 'expr' ); + foreach ( $node->get_flattened_child_nodes( 'update_elem' ) as $i => $update_element ) { + $column_ref = $update_element->get_first_child_node( 'simple_ident_nospvar' ); + $column_ref_parts = $column_ref->get_descendant_nodes( 'ident' ); + $expr_or_default = $update_element->get_first_child_node( 'expr_or_default' ); + $expr = $expr_or_default ? $expr_or_default->get_first_child_node( 'expr' ) : null; // Get column info. $column_name = $this->unquote_sqlite_identifier( $this->translate( end( $column_ref_parts ) ) ); @@ -5618,7 +5731,7 @@ private function translate_update_list( string $table_name, WP_Parser_Node $pare * updating to a NULL value saves an IMPLICIT DEFAULT value instead. * This behavior does not apply to ON DUPLICATE KEY UPDATE clauses. */ - $is_on_duplicate_key_update = 'insertUpdateList' === $parent_node->rule_name; + $is_on_duplicate_key_update = 'opt_insert_update_list' === $parent_node->rule_name; if ( ! $is_strict_mode && ! $is_nullable && ! $is_on_duplicate_key_update ) { $implicit_default = self::DATA_TYPE_IMPLICIT_DEFAULT_MAP[ $data_type ] ?? null; if ( null !== $implicit_default ) { @@ -5686,24 +5799,23 @@ private function store_last_column_meta_from_statement( PDOStatement $stmt ): vo private function unnest_parenthesized_expression( WP_Parser_Node $node ): WP_Parser_Node { $children = $node->get_children(); - // Descend the "expr -> boolPri -> predicate -> bitExpr -> simpleExpr" -> "simpleExprBody" + // Descend the "expr -> bool_pri -> predicate -> bit_expr -> simple_expr" // tree, when on each level we have only a single child node (expression nesting). if ( 1 === count( $children ) && $children[0] instanceof WP_Parser_Node - && in_array( $children[0]->rule_name, array( 'expr', 'boolPri', 'predicate', 'bitExpr', 'simpleExpr', 'simpleExprBody' ), true ) + && in_array( $children[0]->rule_name, array( 'expr', 'bool_pri', 'predicate', 'bit_expr', 'simple_expr' ), true ) ) { $unnested = $this->unnest_parenthesized_expression( $children[0] ); return $unnested === $children[0] ? $node : $unnested; } - // Unnest "OPEN_PAR_SYMBOL exprList CLOSE_PAR_SYMBOL" to "exprList". + // Unnest "OPEN_PAR_SYMBOL expr CLOSE_PAR_SYMBOL" to "expr". if ( count( $children ) === 3 - && $children[0] instanceof WP_MySQL_Token && WP_MySQL_Lexer::OPEN_PAR_SYMBOL === $children[0]->id - && $children[1] instanceof WP_Parser_Node && 'exprList' === $children[1]->rule_name - && $children[2] instanceof WP_MySQL_Token && WP_MySQL_Lexer::CLOSE_PAR_SYMBOL === $children[2]->id - && 1 === count( $children[1]->get_children() ) + && $children[0] instanceof WP_MySQL_Token && WP_MySQL_Tokens::OPEN_PAR_SYMBOL === $children[0]->id + && $children[1] instanceof WP_Parser_Node && 'expr' === $children[1]->rule_name + && $children[2] instanceof WP_MySQL_Token && WP_MySQL_Tokens::CLOSE_PAR_SYMBOL === $children[2]->id ) { return $this->unnest_parenthesized_expression( $children[1] ); } @@ -5748,10 +5860,10 @@ private function unnest_parenthesized_expression( WP_Parser_Node $node ): WP_Par * null when the expression cannot be disambiguated. */ private function disambiguate_item( array $disambiguation_map, WP_Parser_Node $expr ) { - // Skip when there is no column in the expression (no "columnRef" node), - // or when the column is already qualified (has a "dotIdentifier" node). - $column_ref = $expr->get_first_descendant_node( 'columnRef' ); - if ( ! $column_ref || $column_ref->get_first_descendant_node( 'dotIdentifier' ) ) { + // Skip when there is no column in the expression (no "simple_ident" + // node), or when the column is already qualified (multiple parts). + $column_ref = $expr->get_first_descendant_node( 'simple_ident' ); + if ( ! $column_ref || count( $column_ref->get_descendant_nodes( 'ident' ) ) > 1 ) { return null; } @@ -5789,35 +5901,35 @@ private function disambiguate_item( array $disambiguation_map, WP_Parser_Node $e private function create_select_item_disambiguation_map( WP_Parser_Node $select_item_list ): array { // Create a map of SELECT item column names to their qualified values. $disambiguation_map = array(); - foreach ( $select_item_list->get_child_nodes() as $select_item ) { + foreach ( $select_item_list->get_flattened_child_nodes( 'select_item' ) as $select_item ) { /* * [GRAMMAR] - * selectItem: tableWild | (expr selectAlias?) + * select_item: table_wild | expr select_alias */ - // Skip when a "tableWild" node is used (no "expr" node). + // Skip when a "table_wild" node is used (no "expr" node). $select_item_expr = $select_item->get_first_child_node( 'expr' ); if ( ! $select_item_expr ) { continue; } // A SELECT item alias always needs to be preserved as-is. - $alias = $select_item->get_first_child_node( 'selectAlias' ); + $alias = $select_item->get_first_child_node( 'select_alias' ); if ( $alias ) { $alias_value = $this->translate( $alias->get_first_child_node() ); $disambiguation_map[ $alias_value ] = array( $alias_value ); continue; } - // Skip when there is no column listed (no "columnRef" node). - $select_column_ref = $select_item_expr->get_first_descendant_node( 'columnRef' ); + // Skip when there is no column listed (no "simple_ident" node). + $select_column_ref = $select_item_expr->get_first_descendant_node( 'simple_ident' ); if ( ! $select_column_ref ) { continue; } - // Skip when the column reference is not qualified (no "dotIdentifier" node). - $dot_identifiers = $select_column_ref->get_descendant_nodes( 'dotIdentifier' ); - if ( 0 === count( $dot_identifiers ) ) { + // Skip when the column reference is not qualified (a single part). + $ident_parts = $select_column_ref->get_descendant_nodes( 'ident' ); + if ( count( $ident_parts ) < 2 ) { continue; } @@ -5831,8 +5943,8 @@ private function create_select_item_disambiguation_map( WP_Parser_Node $select_i continue; } - // The column name is the last "dotIdentifier" node. - $key = $this->translate( end( $dot_identifiers )->get_first_child_node() ); + // The column name is the last "ident" part. + $key = $this->translate( end( $ident_parts ) ); $disambiguation_map[ $key ] = $disambiguation_map[ $key ] ?? array(); $disambiguation_map[ $key ][] = $column_value; @@ -5841,7 +5953,7 @@ private function create_select_item_disambiguation_map( WP_Parser_Node $select_i } /** - * Analyze a "tableReferenceList" AST node and extract table data. + * Analyze a "table_reference_list" AST node and extract table data. * * This method extracts table data for all tables that are used at the root * level of a given query, including tables that are referenced using JOINs. @@ -5861,64 +5973,101 @@ private function create_select_item_disambiguation_map( WP_Parser_Node $select_i * Which is equivalent to: * SELECT * FROM (t1 CROSS JOIN t2) JOIN t3 ON 1 * - * @param WP_Parser_Node $node The "tableReferenceList" AST node. + * @param WP_Parser_Node $node The "table_reference_list" AST node. * @return array The table reference map (table alias => array of table data). */ private function create_table_reference_map( WP_Parser_Node $node ): array { $table_map = array(); + foreach ( $node->get_flattened_child_nodes( 'table_reference' ) as $table_ref ) { + $this->collect_table_reference_data( $table_ref, null, $table_map ); + } + return $table_map; + } - // Collect all table references, including the ones used in JOINs. - $table_refs = array(); - foreach ( $node->get_child_nodes( 'tableReference' ) as $table_ref ) { - $table_refs[] = $table_ref; - foreach ( $table_ref->get_child_nodes( 'joinedTable' ) as $joined_table ) { - $table_refs[] = $joined_table; + /** + * Collect table data from a table reference into a table reference map. + * + * JOINs nest through the left operand of the "joined_table" rule, so the + * reference tree is descended recursively, attaching each join's ON + * expression to the table on the right side of the join (as in MySQL). + * + * @see WP_PDO_MySQL_On_SQLite::create_table_reference_map() + * + * @param WP_Parser_Node $node The "table_reference" AST node. + * @param WP_Parser_Node|null $join_expr The ON expression the table is joined with, if any. + * @param array $table_map The table reference map to populate. + */ + private function collect_table_reference_data( + WP_Parser_Node $node, + ?WP_Parser_Node $join_expr, + array &$table_map + ): void { + // Descend into JOINs ("table_reference: table_factor | joined_table"). + $joined_table = $node->get_first_child_node( 'joined_table' ); + if ( null !== $joined_table ) { + $operands = $joined_table->get_child_nodes( 'table_reference' ); + $on_expr = $joined_table->get_first_child_node( 'expr' ); + $this->collect_table_reference_data( $operands[0], $join_expr, $table_map ); + if ( isset( $operands[1] ) ) { + $this->collect_table_reference_data( $operands[1], $on_expr, $table_map ); } + return; } - // Process each table reference, extracting table data. - foreach ( $table_refs as $table_ref ) { - $table_factor = $table_ref->get_first_descendant_node( 'tableFactor' ); - $join_expr = $table_ref->get_first_child_node( 'expr' ); - $child = $table_factor->get_first_child_node(); - - // Descend all "singleTableParens" nodes to get the "singleTable" node. - if ( 'singleTableParens' === $child->rule_name ) { - $child = $child->get_first_descendant_node( 'singleTable' ); - } - - if ( 'singleTable' === $child->rule_name ) { - // Extract data from the "singleTable" node. - $table_ref = $child->get_first_child_node( 'tableRef' ); - $name = $this->translate( $table_ref ); - $alias_node = $child->get_first_child_node( 'tableAlias' ); - $alias = $alias_node ? $this->translate( $alias_node->get_first_child_node( 'identifier' ) ) : null; - - $table_map[ $this->unquote_sqlite_identifier( $alias ?? $name ) ] = array( - 'database' => $this->get_database_name( $table_ref ), - 'table_name' => $this->unquote_sqlite_identifier( $name ), - 'table_expr' => null, - 'join_expr' => $this->translate( $join_expr ), - ); - } elseif ( 'derivedTable' === $child->rule_name ) { - // Extract data from the "derivedTable" node. - $subquery = $child->get_first_descendant_node( 'subquery' ); - $alias_node = $child->get_first_child_node( 'tableAlias' ); - $alias = $alias_node ? $this->translate( $alias_node->get_first_child_node( 'identifier' ) ) : null; - - $table_map[ $this->unquote_sqlite_identifier( $alias ) ] = array( - 'database' => null, - 'table_name' => null, - 'table_expr' => $this->translate( $subquery ), - 'join_expr' => $this->translate( $join_expr ), - ); - } elseif ( 'tableReferenceListParens' === $child->rule_name ) { - // Recursively process the "tableReferenceListParens" node. - $table_ref_list = $child->get_first_descendant_node( 'tableReferenceList' ); - $table_map = array_merge( $table_map, $this->create_table_reference_map( $table_ref_list ) ); + $table_factor = $node->get_first_child_node( 'table_factor' ); + if ( null === $table_factor ) { + return; + } + $child = $table_factor->get_first_child_node(); + + // Descend all "single_table_parens" nodes to get the "single_table" node. + if ( 'single_table_parens' === $child->rule_name ) { + $child = $child->get_first_descendant_node( 'single_table' ); + } + + if ( 'single_table' === $child->rule_name ) { + // Extract data from the "single_table" node. + $table_ref = $child->get_first_child_node( 'table_ident' ); + $name = $this->translate( $table_ref ); + $alias_node = $child->get_first_child_node( 'opt_table_alias' ); + $alias = $alias_node ? $this->translate( $alias_node->get_first_child_node( 'ident' ) ) : null; + + $table_map[ $this->unquote_sqlite_identifier( $alias ?? $name ) ] = array( + 'database' => $this->get_database_name( $table_ref ), + 'table_name' => $this->unquote_sqlite_identifier( $name ), + 'table_expr' => null, + 'join_expr' => $this->translate( $join_expr ), + ); + } elseif ( 'derived_table' === $child->rule_name ) { + // Extract data from the "derived_table" node. + $subquery = $child->get_first_descendant_node( 'subquery' ); + $alias_node = $child->get_first_child_node( 'opt_table_alias' ); + $alias = $alias_node ? $this->translate( $alias_node->get_first_child_node( 'ident' ) ) : null; + + $table_map[ $this->unquote_sqlite_identifier( $alias ) ] = array( + 'database' => null, + 'table_name' => null, + 'table_expr' => $this->translate( $subquery ), + 'join_expr' => $this->translate( $join_expr ), + ); + } elseif ( 'table_reference_list_parens' === $child->rule_name ) { + // Recursively process the "table_reference_list_parens" node. + $table_ref_list = $child->get_first_descendant_node( 'table_reference_list' ); + foreach ( $table_ref_list->get_flattened_child_nodes( 'table_reference' ) as $table_ref ) { + $this->collect_table_reference_data( $table_ref, null, $table_map ); + } + } elseif ( 'joined_table_parens' === $child->rule_name ) { + // Process the parenthesized JOIN like an unparenthesized one. + $inner = $child->get_first_child_node( 'joined_table' ); + if ( null !== $inner ) { + $operands = $inner->get_child_nodes( 'table_reference' ); + $on_expr = $inner->get_first_child_node( 'expr' ); + $this->collect_table_reference_data( $operands[0], $join_expr, $table_map ); + if ( isset( $operands[1] ) ) { + $this->collect_table_reference_data( $operands[1], $on_expr, $table_map ); + } } } - return $table_map; } /** @@ -6094,20 +6243,20 @@ private function get_saved_db_name( ?string $db_name = null ): string { /** * Get the database name from one of fully-qualified name AST nodes. * - * @param WP_Parser_Node $node The AST node. One of "tableName", "tableRef", or "inDb". + * @param WP_Parser_Node $node The AST node. One of "table_ident" or "opt_db". * @return string The database name. */ private function get_database_name( WP_Parser_Node $node ): string { - if ( 'tableName' === $node->rule_name || 'tableRef' === $node->rule_name ) { - $parts = $node->get_descendant_nodes( 'identifier' ); + if ( 'table_ident' === $node->rule_name ) { + $parts = $node->get_descendant_nodes( 'ident' ); if ( count( $parts ) > 1 ) { return $this->unquote_sqlite_identifier( $this->translate( $parts[0] ) ); } else { return $this->db_name; } - } elseif ( 'inDb' === $node->rule_name ) { + } elseif ( 'opt_db' === $node->rule_name ) { return $this->unquote_sqlite_identifier( - $this->translate( $node->get_first_child_node( 'identifier' ) ) + $this->translate( $node->get_first_child_node( 'ident' ) ) ); } @@ -6312,8 +6461,8 @@ private function get_sqlite_create_table_statement( } elseif ( str_contains( $column['EXTRA'], 'DEFAULT_GENERATED' ) ) { // Handle DEFAULT values with expressions (DEFAULT_GENERATED). // Translate the default clause from MySQL to SQLite. - $ast = $this->create_parser( 'SELECT ' . $column['COLUMN_DEFAULT'] )->parse(); - $expr = $ast->get_first_descendant_node( 'selectItem' )->get_first_child_node(); + $ast = $this->parse_mysql_query( 'SELECT ' . $column['COLUMN_DEFAULT'] )[0]; + $expr = $ast->get_first_descendant_node( 'select_item' )->get_first_child_node(); $default_clause = $this->translate( $expr ); $query .= sprintf( ' DEFAULT (%s)', $default_clause ); } else { @@ -6438,8 +6587,8 @@ function ( $column ) { } // Translate the check clause from MySQL to SQLite. - $ast = $this->create_parser( 'SELECT ' . $check_constraint['CHECK_CLAUSE'] )->parse(); - $expr = $ast->get_first_descendant_node( 'selectItem' )->get_first_child_node(); + $ast = $this->parse_mysql_query( 'SELECT ' . $check_constraint['CHECK_CLAUSE'] )[0]; + $expr = $ast->get_first_descendant_node( 'select_item' )->get_first_child_node(); $check_clause = $this->translate( $expr ); $sql = sprintf( diff --git a/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-driver.php b/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-driver.php index 04ec0630c..b0778b485 100644 --- a/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-driver.php +++ b/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-driver.php @@ -168,13 +168,14 @@ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mo } /** - * Tokenize a MySQL query and initialize a parser. + * Parse a MySQL query string into a list of per-statement ASTs. * - * @param string $query The MySQL query to parse. - * @return WP_MySQL_Parser A parser initialized for the MySQL query. + * @param string $query The MySQL query string (possibly multi-statement). + * @return WP_Parser_Node[]|null One "sql_statement" AST per statement, or + * null when a statement fails to parse. */ - public function create_parser( string $query ): WP_MySQL_Parser { - return $this->mysql_on_sqlite_driver->create_parser( $query ); + public function parse_mysql_query( string $query ): ?array { + return $this->mysql_on_sqlite_driver->parse_mysql_query( $query ); } /** diff --git a/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-information-schema-builder.php b/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-information-schema-builder.php index 8e84a9f24..5ccfe3955 100644 --- a/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-information-schema-builder.php +++ b/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-information-schema-builder.php @@ -195,47 +195,47 @@ class WP_SQLite_Information_Schema_Builder { * This is used to store column data types in the information schema. */ const TOKEN_TO_TYPE_MAP = array( - WP_MySQL_Lexer::INT_SYMBOL => 'int', - WP_MySQL_Lexer::TINYINT_SYMBOL => 'tinyint', - WP_MySQL_Lexer::SMALLINT_SYMBOL => 'smallint', - WP_MySQL_Lexer::MEDIUMINT_SYMBOL => 'mediumint', - WP_MySQL_Lexer::BIGINT_SYMBOL => 'bigint', - WP_MySQL_Lexer::REAL_SYMBOL => 'double', - WP_MySQL_Lexer::DOUBLE_SYMBOL => 'double', - WP_MySQL_Lexer::FLOAT_SYMBOL => 'float', - WP_MySQL_Lexer::DECIMAL_SYMBOL => 'decimal', - WP_MySQL_Lexer::NUMERIC_SYMBOL => 'decimal', - WP_MySQL_Lexer::FIXED_SYMBOL => 'decimal', - WP_MySQL_Lexer::BIT_SYMBOL => 'bit', - WP_MySQL_Lexer::BOOL_SYMBOL => 'tinyint', - WP_MySQL_Lexer::BOOLEAN_SYMBOL => 'tinyint', - WP_MySQL_Lexer::BINARY_SYMBOL => 'binary', - WP_MySQL_Lexer::VARBINARY_SYMBOL => 'varbinary', - WP_MySQL_Lexer::YEAR_SYMBOL => 'year', - WP_MySQL_Lexer::DATE_SYMBOL => 'date', - WP_MySQL_Lexer::TIME_SYMBOL => 'time', - WP_MySQL_Lexer::TIMESTAMP_SYMBOL => 'timestamp', - WP_MySQL_Lexer::DATETIME_SYMBOL => 'datetime', - WP_MySQL_Lexer::TINYBLOB_SYMBOL => 'tinyblob', - WP_MySQL_Lexer::BLOB_SYMBOL => 'blob', - WP_MySQL_Lexer::MEDIUMBLOB_SYMBOL => 'mediumblob', - WP_MySQL_Lexer::LONGBLOB_SYMBOL => 'longblob', - WP_MySQL_Lexer::TINYTEXT_SYMBOL => 'tinytext', - WP_MySQL_Lexer::TEXT_SYMBOL => 'text', - WP_MySQL_Lexer::MEDIUMTEXT_SYMBOL => 'mediumtext', - WP_MySQL_Lexer::LONGTEXT_SYMBOL => 'longtext', - WP_MySQL_Lexer::ENUM_SYMBOL => 'enum', - WP_MySQL_Lexer::SET_SYMBOL => 'set', - WP_MySQL_Lexer::SERIAL_SYMBOL => 'bigint', - WP_MySQL_Lexer::GEOMETRY_SYMBOL => 'geometry', - WP_MySQL_Lexer::GEOMETRYCOLLECTION_SYMBOL => 'geomcollection', - WP_MySQL_Lexer::POINT_SYMBOL => 'point', - WP_MySQL_Lexer::MULTIPOINT_SYMBOL => 'multipoint', - WP_MySQL_Lexer::LINESTRING_SYMBOL => 'linestring', - WP_MySQL_Lexer::MULTILINESTRING_SYMBOL => 'multilinestring', - WP_MySQL_Lexer::POLYGON_SYMBOL => 'polygon', - WP_MySQL_Lexer::MULTIPOLYGON_SYMBOL => 'multipolygon', - WP_MySQL_Lexer::JSON_SYMBOL => 'json', + WP_MySQL_Tokens::KEYWORDS['INT'] => 'int', + WP_MySQL_Tokens::KEYWORDS['TINYINT'] => 'tinyint', + WP_MySQL_Tokens::KEYWORDS['SMALLINT'] => 'smallint', + WP_MySQL_Tokens::KEYWORDS['MEDIUMINT'] => 'mediumint', + WP_MySQL_Tokens::KEYWORDS['BIGINT'] => 'bigint', + WP_MySQL_Tokens::KEYWORDS['REAL'] => 'double', + WP_MySQL_Tokens::KEYWORDS['DOUBLE'] => 'double', + WP_MySQL_Tokens::KEYWORDS['FLOAT'] => 'float', + WP_MySQL_Tokens::KEYWORDS['DECIMAL'] => 'decimal', + WP_MySQL_Tokens::KEYWORDS['NUMERIC'] => 'decimal', + WP_MySQL_Tokens::KEYWORDS['FIXED'] => 'decimal', + WP_MySQL_Tokens::KEYWORDS['BIT'] => 'bit', + WP_MySQL_Tokens::KEYWORDS['BOOL'] => 'tinyint', + WP_MySQL_Tokens::KEYWORDS['BOOLEAN'] => 'tinyint', + WP_MySQL_Tokens::KEYWORDS['BINARY'] => 'binary', + WP_MySQL_Tokens::KEYWORDS['VARBINARY'] => 'varbinary', + WP_MySQL_Tokens::KEYWORDS['YEAR'] => 'year', + WP_MySQL_Tokens::KEYWORDS['DATE'] => 'date', + WP_MySQL_Tokens::KEYWORDS['TIME'] => 'time', + WP_MySQL_Tokens::KEYWORDS['TIMESTAMP'] => 'timestamp', + WP_MySQL_Tokens::KEYWORDS['DATETIME'] => 'datetime', + WP_MySQL_Tokens::KEYWORDS['TINYBLOB'] => 'tinyblob', + WP_MySQL_Tokens::KEYWORDS['BLOB'] => 'blob', + WP_MySQL_Tokens::KEYWORDS['MEDIUMBLOB'] => 'mediumblob', + WP_MySQL_Tokens::KEYWORDS['LONGBLOB'] => 'longblob', + WP_MySQL_Tokens::KEYWORDS['TINYTEXT'] => 'tinytext', + WP_MySQL_Tokens::KEYWORDS['TEXT'] => 'text', + WP_MySQL_Tokens::KEYWORDS['MEDIUMTEXT'] => 'mediumtext', + WP_MySQL_Tokens::KEYWORDS['LONGTEXT'] => 'longtext', + WP_MySQL_Tokens::KEYWORDS['ENUM'] => 'enum', + WP_MySQL_Tokens::KEYWORDS['SET'] => 'set', + WP_MySQL_Tokens::KEYWORDS['SERIAL'] => 'bigint', + WP_MySQL_Tokens::KEYWORDS['GEOMETRY'] => 'geometry', + WP_MySQL_Tokens::KEYWORDS['GEOMETRYCOLLECTION'] => 'geomcollection', + WP_MySQL_Tokens::KEYWORDS['POINT'] => 'point', + WP_MySQL_Tokens::KEYWORDS['MULTIPOINT'] => 'multipoint', + WP_MySQL_Tokens::KEYWORDS['LINESTRING'] => 'linestring', + WP_MySQL_Tokens::KEYWORDS['MULTILINESTRING'] => 'multilinestring', + WP_MySQL_Tokens::KEYWORDS['POLYGON'] => 'polygon', + WP_MySQL_Tokens::KEYWORDS['MULTIPOLYGON'] => 'multipolygon', + WP_MySQL_Tokens::KEYWORDS['JSON'] => 'json', ); /** @@ -484,10 +484,10 @@ public function ensure_temporary_information_schema_tables(): void { /** * Analyze CREATE TABLE statement and record data in the information schema. * - * @param WP_Parser_Node $node The "createStatement" AST node with "createTable" child. + * @param WP_Parser_Node $node The "create_table_stmt" AST node. */ public function record_create_table( WP_Parser_Node $node ): void { - $table_name_node = $node->get_first_descendant_node( 'tableName' ); + $table_name_node = $node->get_first_descendant_node( 'table_ident' ); $table_name = $this->get_table_name_from_node( $table_name_node ); $table_engine = $this->get_table_engine( $node ); $table_row_format = 'MyISAM' === $table_engine ? 'Fixed' : 'Dynamic'; @@ -499,8 +499,7 @@ public function record_create_table( WP_Parser_Node $node ): void { * 1. Track that we're processing a temporary table. * 2. Ensure that the temporary information schema tables exist. */ - $subnode = $node->get_first_child_node(); - $table_is_temporary = $subnode->has_child_token( WP_MySQL_Lexer::TEMPORARY_SYMBOL ); + $table_is_temporary = $node->has_child_node( 'opt_temporary' ); if ( $table_is_temporary && ! $this->temporary_information_schema_exists ) { $this->ensure_temporary_information_schema_tables(); } @@ -549,8 +548,8 @@ public function record_create_table( WP_Parser_Node $node ): void { // 2. Columns. $column_position = 1; - foreach ( $node->get_descendant_nodes( 'columnDefinition' ) as $column_node ) { - $column_name = $this->get_value( $column_node->get_first_child_node( 'fieldIdentifier' ) ); + foreach ( $node->get_descendant_nodes( 'column_def' ) as $column_node ) { + $column_name = $this->get_value( $column_node->get_first_child_node( 'ident' ) ); // Column definition. $column_data = $this->extract_column_data( @@ -634,7 +633,7 @@ public function record_create_table( WP_Parser_Node $node ): void { } // 3. Constraints and indexes. - foreach ( $node->get_descendant_nodes( 'tableConstraintDef' ) as $constraint_node ) { + foreach ( $node->get_descendant_nodes( 'table_constraint_def' ) as $constraint_node ) { $this->record_add_constraint_or_index( $table_is_temporary, $table_name, $constraint_node ); } } @@ -642,12 +641,12 @@ public function record_create_table( WP_Parser_Node $node ): void { /** * Analyze ALTER TABLE statement and record data in the information schema. * - * @param WP_Parser_Node $node The "alterStatement" AST node with "alterTable" child. + * @param WP_Parser_Node $node The "alter_table_stmt" AST node. */ public function record_alter_table( WP_Parser_Node $node ): void { - $table_ref = $node->get_first_descendant_node( 'tableRef' ); + $table_ref = $node->get_first_descendant_node( 'table_ident' ); $table_name = $this->get_table_name_from_node( $table_ref ); - $actions = $node->get_descendant_nodes( 'alterListItem' ); + $actions = $node->get_descendant_nodes( 'alter_list_item' ); // Check if a temporary table with the given name exists. $table_is_temporary = $this->temporary_table_exists( $table_name ); @@ -656,28 +655,28 @@ public function record_alter_table( WP_Parser_Node $node ): void { $first_token = $action->get_first_child_token(); // ADD - if ( WP_MySQL_Lexer::ADD_SYMBOL === $first_token->id ) { + if ( WP_MySQL_Tokens::KEYWORDS['ADD'] === $first_token->id ) { // ADD [COLUMN] (...[, ...]) - $column_definitions = $action->get_descendant_nodes( 'columnDefinition' ); + $column_definitions = $action->get_descendant_nodes( 'column_def' ); if ( count( $column_definitions ) > 0 ) { foreach ( $column_definitions as $column_definition ) { - $name = $this->get_value( $column_definition->get_first_child_node( 'identifier' ) ); + $name = $this->get_value( $column_definition->get_first_child_node( 'ident' ) ); $this->record_add_column( $table_is_temporary, $table_name, $name, $column_definition ); } continue; } // ADD [COLUMN] ... - $field_definition = $action->get_first_descendant_node( 'fieldDefinition' ); + $field_definition = $action->get_first_descendant_node( 'field_def' ); if ( null !== $field_definition ) { - $name = $this->get_value( $action->get_first_child_node( 'identifier' ) ); + $name = $this->get_value( $action->get_first_child_node( 'ident' ) ); $this->record_add_column( $table_is_temporary, $table_name, $name, $field_definition ); // @TODO: Handle FIRST/AFTER. continue; } // ADD constraint or index. - $constraint = $action->get_first_descendant_node( 'tableConstraintDef' ); + $constraint = $action->get_first_descendant_node( 'table_constraint_def' ); if ( null !== $constraint ) { $this->record_add_constraint_or_index( $table_is_temporary, $table_name, $constraint ); continue; @@ -687,76 +686,75 @@ public function record_alter_table( WP_Parser_Node $node ): void { } // CHANGE [COLUMN] - if ( WP_MySQL_Lexer::CHANGE_SYMBOL === $first_token->id ) { - $old_name = $this->get_value( $action->get_first_child_node( 'fieldIdentifier' ) ); - $new_name = $this->get_value( $action->get_first_child_node( 'identifier' ) ); + if ( WP_MySQL_Tokens::KEYWORDS['CHANGE'] === $first_token->id ) { + $identifiers = $action->get_child_nodes( 'ident' ); + $old_name = $this->get_value( $identifiers[0] ); + $new_name = $this->get_value( $identifiers[1] ); $this->record_change_column( $table_is_temporary, $table_name, $old_name, $new_name, - $action->get_first_descendant_node( 'fieldDefinition' ) + $action->get_first_descendant_node( 'field_def' ) ); continue; } // MODIFY [COLUMN] - if ( WP_MySQL_Lexer::MODIFY_SYMBOL === $first_token->id ) { - $name = $this->get_value( $action->get_first_child_node( 'fieldIdentifier' ) ); + if ( WP_MySQL_Tokens::KEYWORDS['MODIFY'] === $first_token->id ) { + $name = $this->get_value( $action->get_first_child_node( 'ident' ) ); $this->record_modify_column( $table_is_temporary, $table_name, $name, - $action->get_first_descendant_node( 'fieldDefinition' ) + $action->get_first_descendant_node( 'field_def' ) ); continue; } // DROP - if ( WP_MySQL_Lexer::DROP_SYMBOL === $first_token->id ) { + if ( WP_MySQL_Tokens::KEYWORDS['DROP'] === $first_token->id ) { // DROP CONSTRAINT - if ( $action->has_child_token( WP_MySQL_Lexer::CONSTRAINT_SYMBOL ) ) { - $name = $this->get_value( $action->get_first_child_node( 'identifier' ) ); + if ( $action->has_child_token( WP_MySQL_Tokens::KEYWORDS['CONSTRAINT'] ) ) { + $name = $this->get_value( $action->get_first_child_node( 'ident' ) ); $this->record_drop_constraint( $table_is_temporary, $table_name, $name ); continue; } // DROP PRIMARY KEY - if ( $action->has_child_token( WP_MySQL_Lexer::PRIMARY_SYMBOL ) ) { + if ( $action->has_child_token( WP_MySQL_Tokens::KEYWORDS['PRIMARY'] ) ) { $this->record_drop_key( $table_is_temporary, $table_name, 'PRIMARY' ); continue; } // DROP FOREIGN KEY - if ( $action->has_child_token( WP_MySQL_Lexer::FOREIGN_SYMBOL ) ) { - $field_identifier = $action->get_first_child_node( 'fieldIdentifier' ); - $identifiers = $field_identifier->get_descendant_nodes( 'identifier' ); - $name = $this->get_value( end( $identifiers ) ); + if ( $action->has_child_token( WP_MySQL_Tokens::KEYWORDS['FOREIGN'] ) ) { + $name = $this->get_value( $action->get_first_child_node( 'ident' ) ); $this->record_drop_foreign_key( $table_is_temporary, $table_name, $name ); continue; } // DROP CHECK - if ( $action->has_child_token( WP_MySQL_Lexer::CHECK_SYMBOL ) ) { - $name = $this->get_value( $action->get_first_child_node( 'identifier' ) ); + if ( $action->has_child_token( WP_MySQL_Tokens::KEYWORDS['CHECK'] ) ) { + $name = $this->get_value( $action->get_first_child_node( 'ident' ) ); $this->record_drop_check_constraint( $table_is_temporary, $table_name, $name ); continue; } + // DROP INDEX + if ( $action->has_child_node( 'key_or_index' ) ) { + $name = $this->get_value( $action->get_first_child_node( 'ident' ) ); + $this->record_drop_index_data( $table_is_temporary, $table_name, $name ); + continue; + } + // DROP [COLUMN] - $column_ref = $action->get_first_child_node( 'fieldIdentifier' ); + $column_ref = $action->get_first_child_node( 'ident' ); if ( null !== $column_ref ) { $name = $this->get_value( $column_ref ); $this->record_drop_column( $table_is_temporary, $table_name, $name ); continue; } - - // DROP INDEX - if ( $action->has_child_node( 'keyOrIndex' ) ) { - $name = $this->get_value( $action->get_first_child_node( 'indexRef' ) ); - $this->record_drop_index_data( $table_is_temporary, $table_name, $name ); - continue; - } } } } @@ -764,14 +762,12 @@ public function record_alter_table( WP_Parser_Node $node ): void { /** * Analyze DROP TABLE statement and record data in the information schema. * - * @param WP_Parser_Node $node The "dropStatement" AST node with "dropTable" child. + * @param WP_Parser_Node $node The "drop_table_stmt" AST node. */ public function record_drop_table( WP_Parser_Node $node ): void { - $child_node = $node->get_first_child_node(); + $has_temporary_keyword = $node->has_child_node( 'opt_temporary' ); - $has_temporary_keyword = $child_node->has_child_token( WP_MySQL_Lexer::TEMPORARY_SYMBOL ); - - $table_refs = $child_node->get_first_child_node( 'tableRefList' )->get_child_nodes(); + $table_refs = $node->get_first_child_node( 'table_list' )->get_flattened_child_nodes( 'table_ident' ); foreach ( $table_refs as $table_ref ) { $table_name = $this->get_table_name_from_node( $table_ref ); $table_is_temporary = $has_temporary_keyword || $this->temporary_table_exists( $table_name ); @@ -812,28 +808,25 @@ public function record_drop_table( WP_Parser_Node $node ): void { /** * Analyze CREATE INDEX definition and record data in the information schema. * - * @param WP_Parser_Node $node The "createStatement" AST node with "createIndex" child. + * @param WP_Parser_Node $node The "create_index_stmt" AST node. */ public function record_create_index( WP_Parser_Node $node ): void { - $create_index = $node->get_first_child_node( 'createIndex' ); - $target = $create_index->get_first_child_node( 'createIndexTarget' ); - $table_ref = $target->get_first_child_node( 'tableRef' ); - $table_name = $this->get_table_name_from_node( $table_ref ); + $table_ref = $node->get_first_child_node( 'table_ident' ); + $table_name = $this->get_table_name_from_node( $table_ref ); $table_is_temporary = $this->temporary_table_exists( $table_name ); - $this->record_add_index( $table_is_temporary, $table_name, $create_index ); + $this->record_add_index( $table_is_temporary, $table_name, $node ); } /** * Analyze DROP INDEX definition and record data in the information schema. * - * @param WP_Parser_Node $node The "dropStatement" AST node with "dropIndex" child. + * @param WP_Parser_Node $node The "drop_index_stmt" AST node. */ public function record_drop_index( WP_Parser_Node $node ): void { - $drop_index = $node->get_first_child_node( 'dropIndex' ); - $table_ref = $drop_index->get_first_child_node( 'tableRef' ); + $table_ref = $node->get_first_child_node( 'table_ident' ); $table_name = $this->get_table_name_from_node( $table_ref ); - $index_name = $this->get_value( $drop_index->get_first_child_node( 'indexRef' ) ); + $index_name = $this->get_value( $node->get_first_child_node( 'ident' ) ); $table_is_temporary = $this->temporary_table_exists( $table_name ); $this->record_drop_index_data( $table_is_temporary, $table_name, $index_name ); } @@ -844,7 +837,7 @@ public function record_drop_index( WP_Parser_Node $node ): void { * @param bool $table_is_temporary Whether the table is temporary. * @param string $table_name The table name. * @param string $column_name The column name. - * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node. + * @param WP_Parser_Node $node The "column_def" or "field_def" AST node. */ private function record_add_column( bool $table_is_temporary, @@ -904,7 +897,7 @@ private function record_add_column( * @param string $table_name The table name. * @param string $column_name The column name. * @param string $new_column_name The new column name when the column is renamed. - * @param WP_Parser_Node $node The "fieldDefinition" AST node. + * @param WP_Parser_Node $node The "field_def" AST node. */ private function record_change_column( bool $table_is_temporary, @@ -975,7 +968,7 @@ private function record_change_column( * @param bool $table_is_temporary Whether the table is temporary. * @param string $table_name The table name. * @param string $column_name The column name. - * @param WP_Parser_Node $node The "fieldDefinition" AST node. + * @param WP_Parser_Node $node The "field_def" AST node. */ private function record_modify_column( bool $table_is_temporary, @@ -1108,24 +1101,24 @@ private function record_drop_column( } /** - * Analyze ADD "tableConstraintDef" and record data in the information schema. + * Analyze ADD "table_constraint_def" and record data in the information schema. * * @param bool $table_is_temporary Whether the table is temporary. * @param string $table_name The table name. - * @param WP_Parser_Node $node The "tableConstraintDef" AST node. + * @param WP_Parser_Node $node The "table_constraint_def" AST node. */ private function record_add_constraint_or_index( bool $table_is_temporary, string $table_name, WP_Parser_Node $node ): void { - $child = $node->get_first_child(); - $first_child_token_id = $child instanceof WP_MySQL_Token ? $child->id : null; + $keyword = $this->get_constraint_keyword_token( $node ); + $keyword_id = null !== $keyword ? $keyword->id : null; if ( - WP_MySQL_Lexer::KEY_SYMBOL === $first_child_token_id - || WP_MySQL_Lexer::INDEX_SYMBOL === $first_child_token_id - || WP_MySQL_Lexer::FULLTEXT_SYMBOL === $first_child_token_id - || WP_MySQL_Lexer::SPATIAL_SYMBOL === $first_child_token_id + WP_MySQL_Tokens::KEYWORDS['KEY'] === $keyword_id + || WP_MySQL_Tokens::KEYWORDS['INDEX'] === $keyword_id + || WP_MySQL_Tokens::KEYWORDS['FULLTEXT'] === $keyword_id + || WP_MySQL_Tokens::KEYWORDS['SPATIAL'] === $keyword_id ) { $this->record_add_index( $table_is_temporary, $table_name, $node ); } else { @@ -1140,7 +1133,7 @@ private function record_add_constraint_or_index( * * @param bool $table_is_temporary Whether the table is temporary. * @param string $table_name The table name. - * @param WP_Parser_Node $node The "tableConstraintDef" or "createIndex" AST node. + * @param WP_Parser_Node $node The "table_constraint_def" or "create_index_stmt" AST node. */ private function record_add_index( bool $table_is_temporary, @@ -1167,7 +1160,8 @@ private function record_add_index( $this->sync_column_key_info( $table_is_temporary, $table_name ); // For UNIQUE index, save also constraint data. - if ( $node->has_child_token( WP_MySQL_Lexer::UNIQUE_SYMBOL ) ) { + $keyword = $this->get_constraint_keyword_token( $node ); + if ( null !== $keyword && WP_MySQL_Tokens::KEYWORDS['UNIQUE'] === $keyword->id ) { $constraint_data = $this->extract_table_constraint_data( $node, $table_name, @@ -1239,7 +1233,7 @@ private function record_drop_index_data( * * @param bool $table_is_temporary Whether the table is temporary. * @param string $table_name The table name. - * @param WP_Parser_Node $node The "tableConstraintDef" AST node. + * @param WP_Parser_Node $node The "table_constraint_def" AST node. */ private function record_add_constraint( bool $table_is_temporary, @@ -1247,20 +1241,15 @@ private function record_add_constraint( WP_Parser_Node $node ): void { // Get first constraint keyword. - $children = $node->get_children(); - if ( $children[0] instanceof WP_Parser_Node && 'constraintName' === $children[0]->rule_name ) { - $keyword = $children[1]; - } else { - $keyword = $children[0]; - } - if ( ! $keyword instanceof WP_MySQL_Token ) { - $keyword = $keyword->get_first_child_token(); - } + $keyword = $this->get_constraint_keyword_token( $node ); // PRIMARY KEY and UNIQUE require an index. if ( - WP_MySQL_Lexer::PRIMARY_SYMBOL === $keyword->id - || WP_MySQL_Lexer::UNIQUE_SYMBOL === $keyword->id + null !== $keyword + && ( + WP_MySQL_Tokens::KEYWORDS['PRIMARY'] === $keyword->id + || WP_MySQL_Tokens::KEYWORDS['UNIQUE'] === $keyword->id + ) ) { $statistics_data = $this->extract_index_statistics_data( $table_is_temporary, $table_name, $node ); $index_name = $statistics_data[0]['index_name']; @@ -1494,11 +1483,11 @@ private function record_drop_check_constraint( } /** - * Analyze "columnDefinition" or "fieldDefinition" AST node and extract column data. + * Analyze "column_def" or "field_def" AST node and extract column data. * * @param string $table_name The table name. * @param string $column_name The column name. - * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node. + * @param WP_Parser_Node $node The "column_def" or "field_def" AST node. * @param int $position The ordinal position of the column in the table. * @return array Column data for the information schema. */ @@ -1542,11 +1531,11 @@ private function extract_column_data( string $table_name, string $column_name, W } /** - * Analyze "columnDefinition" or "fieldDefinition" AST node and extract constraint data. + * Analyze "column_def" or "field_def" AST node and extract constraint data. * * @param string $table_name The table name. * @param string $column_name The column name. - * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node. + * @param WP_Parser_Node $node The "column_def" or "field_def" AST node. * @param bool $nullable Whether the column is nullable. * @return array|null Column statistics data for the information schema. */ @@ -1557,8 +1546,8 @@ private function extract_column_statistics_data( bool $nullable ): ?array { // Handle inline PRIMARY KEY and UNIQUE constraints. - $has_inline_primary_key = null !== $node->get_first_descendant_token( WP_MySQL_Lexer::KEY_SYMBOL ); - $has_inline_unique_key = null !== $node->get_first_descendant_token( WP_MySQL_Lexer::UNIQUE_SYMBOL ); + $has_inline_primary_key = null !== $node->get_first_descendant_token( WP_MySQL_Tokens::KEYWORDS['KEY'] ); + $has_inline_unique_key = null !== $node->get_first_descendant_token( WP_MySQL_Tokens::KEYWORDS['UNIQUE'] ); if ( $has_inline_primary_key || $has_inline_unique_key ) { $index_name = $has_inline_primary_key ? 'PRIMARY' : $column_name; return array( @@ -1585,11 +1574,11 @@ private function extract_column_statistics_data( } /** - * Analyze "tableConstraintDef" or "createIndex" AST node and extract index data. + * Analyze "table_constraint_def" or "create_index_stmt" AST node and extract index data. * * @param bool $table_is_temporary Whether the table is temporary. * @param string $table_name The table name. - * @param WP_Parser_Node $node The "tableConstraintDef" or "createIndex" AST node. + * @param WP_Parser_Node $node The "table_constraint_def" or "create_index_stmt" AST node. * @return array Index statistics data for the information schema. */ private function extract_index_statistics_data( @@ -1597,22 +1586,14 @@ private function extract_index_statistics_data( string $table_name, WP_Parser_Node $node ): array { - // Get first keyword. - $children = $node->get_children(); - $keyword = $children[0] instanceof WP_MySQL_Token ? $children[0] : $children[1]; - if ( ! $keyword instanceof WP_MySQL_Token ) { - $keyword = $keyword->get_first_child_token(); - } + // Get first constraint keyword. + $keyword = $this->get_constraint_keyword_token( $node ); - // Get key parts. - $key_list = $node->get_first_descendant_node( 'keyListVariants' )->get_first_child(); - if ( 'keyListWithExpression' === $key_list->rule_name ) { - $key_parts = array(); - foreach ( $key_list->get_descendant_nodes( 'keyPartOrExpression' ) as $key_part ) { - $key_parts[] = $key_part->get_first_child(); - } - } else { - $key_parts = $key_list->get_descendant_nodes( 'keyPart' ); + // Get key parts ("key_part" nodes, or "expr" nodes for functional key parts). + $key_list = $node->get_first_descendant_node( 'key_list_with_expression' ); + $key_parts = array(); + foreach ( $key_list->get_flattened_child_nodes( 'key_part_with_expression' ) as $key_part ) { + $key_parts[] = $key_part->get_first_child_node(); } // Get index column names. @@ -1707,9 +1688,9 @@ private function extract_index_statistics_data( } /** - * Extract table constraint data from the "tableConstraintDef" or "columnDefinition" AST node. + * Extract table constraint data from the "table_constraint_def" or "column_def" AST node. * - * @param WP_Parser_Node $node The "tableConstraintDef" or "columnDefinition" AST node. + * @param WP_Parser_Node $node The "table_constraint_def" or "column_def" AST node. * @param string $table_name The table name. * @param string $column_name The column name. * @return array|null Table constraint data for the information schema. @@ -1728,8 +1709,8 @@ public function extract_table_constraint_data( $name = $index_name ?? $this->get_table_constraint_name( $node, $table_name ); // Constraint enforcement. - $constraint_enforcement = $node->get_first_descendant_node( 'constraintEnforcement' ); - if ( $constraint_enforcement && $constraint_enforcement->has_child_token( WP_MySQL_Lexer::NOT_SYMBOL ) ) { + $constraint_enforcement = $node->get_first_descendant_node( 'constraint_enforcement' ); + if ( $constraint_enforcement && $constraint_enforcement->has_child_node( 'opt_not' ) ) { $enforced = 'NO'; } else { $enforced = 'YES'; @@ -1746,9 +1727,9 @@ public function extract_table_constraint_data( } /** - * Extract referential constraint data from the "tableConstraintDef" AST node. + * Extract referential constraint data from the "table_constraint_def" AST node. * - * @param WP_Parser_Node $node The "tableConstraintDef" AST node. + * @param WP_Parser_Node $node The "table_constraint_def" AST node. * @param string $table_name The table name. * @return array|null The referential constraint data as stored in information schema. */ @@ -1759,15 +1740,13 @@ private function extract_referential_constraint_data( WP_Parser_Node $node, stri } // Referenced table name. - $referenced_table = $references->get_first_child_node( 'tableRef' ); + $referenced_table = $references->get_first_child_node( 'table_ident' ); $referenced_table_name = $this->get_table_name_from_node( $referenced_table ); // Referenced column names. - $reference_parts = $references->get_first_child_node( 'identifierListWithParentheses' ) - ->get_first_child_node( 'identifierList' ) - ->get_child_nodes( 'identifier' ); + $reference_parts = $this->get_reference_list_columns( $references ); - // ON UPDATE and ON DELETE both use the "deleteOption" node. + // ON UPDATE and ON DELETE both use the "delete_option" node. $actions = $this->get_foreign_key_actions( $references ); $on_update = $actions['on_update']; $on_delete = $actions['on_delete']; @@ -1825,9 +1804,9 @@ private function extract_referential_constraint_data( WP_Parser_Node $node, stri } /** - * Extract key column usage data from the "tableConstraintDef" AST node. + * Extract key column usage data from the "table_constraint_def" AST node. * - * @param WP_Parser_Node $node The "tableConstraintDef" AST node. + * @param WP_Parser_Node $node The "table_constraint_def" AST node. * @param string $table_name The table name. * @param string $index_name The index name, when the constraint uses an index. * @return array The key column usage data as stored in information schema. @@ -1837,8 +1816,8 @@ private function extract_key_column_usage_data( string $table_name, ?string $index_name = null ): array { - $is_primary = $node->get_first_descendant_token( WP_MySQL_Lexer::PRIMARY_SYMBOL ); - $is_unique = $node->get_first_descendant_token( WP_MySQL_Lexer::UNIQUE_SYMBOL ); + $is_primary = $node->get_first_descendant_token( WP_MySQL_Tokens::KEYWORDS['PRIMARY'] ); + $is_unique = $node->get_first_descendant_token( WP_MySQL_Tokens::KEYWORDS['UNIQUE'] ); $references = $node->get_first_descendant_node( 'references' ); if ( null === $references && ! $is_primary && ! $is_unique ) { return array(); @@ -1846,15 +1825,13 @@ private function extract_key_column_usage_data( // Referenced table name and column names. if ( $references ) { - $referenced_table = $references->get_first_child_node( 'tableRef' ); - $referenced_identifiers = $referenced_table->get_descendant_nodes( 'identifier' ); + $referenced_table = $references->get_first_child_node( 'table_ident' ); + $referenced_identifiers = $referenced_table->get_child_nodes( 'ident' ); $referenced_table_schema = count( $referenced_identifiers ) > 1 ? $this->get_value( $referenced_identifiers[0] ) : self::SAVED_DATABASE_NAME; $referenced_table_name = $this->get_table_name_from_node( $referenced_table ); - $referenced_columns = $references->get_first_child_node( 'identifierListWithParentheses' ) - ->get_first_child_node( 'identifierList' ) - ->get_child_nodes( 'identifier' ); + $referenced_columns = $this->get_reference_list_columns( $references ); } else { $referenced_table_schema = null; $referenced_table_name = null; @@ -1865,15 +1842,12 @@ private function extract_key_column_usage_data( $name = $index_name ?? $this->get_table_constraint_name( $node, $table_name ); // Key parts. - if ( 'columnDefinition' === $node->rule_name ) { - $identifiers = $node - ->get_first_descendant_node( 'fieldIdentifier' ) - ->get_descendant_nodes( 'identifier' ); - $key_parts = array( end( $identifiers ) ); + if ( 'column_def' === $node->rule_name ) { + $key_parts = array( $node->get_first_child_node( 'ident' ) ); } else { $key_parts = array(); - foreach ( $node->get_descendant_nodes( 'keyPart' ) as $key_part ) { - $key_parts[] = $key_part->get_first_child_node( 'identifier' ); + foreach ( $node->get_descendant_nodes( 'key_part' ) as $key_part ) { + $key_parts[] = $key_part->get_first_child_node( 'ident' ); } } @@ -1899,19 +1873,19 @@ private function extract_key_column_usage_data( } /** - * Extract check constraint data from the "tableConstraintDef" AST node. + * Extract check constraint data from the "table_constraint_def" AST node. * - * @param WP_Parser_Node $node The "tableConstraintDef" AST node. + * @param WP_Parser_Node $node The "table_constraint_def" AST node. * @param string $table_name The table name. * @return array|null The check constraint data as stored in information schema. */ private function extract_check_constraint_data( WP_Parser_Node $node, string $table_name ): ?array { - $check_constraint = $node->get_first_descendant_node( 'checkConstraint' ); + $check_constraint = $node->get_first_descendant_node( 'check_constraint' ); if ( null === $check_constraint ) { return null; } - $expr = $check_constraint->get_first_child_node( 'exprWithParentheses' ); + $expr = $check_constraint->get_first_child_node( 'expr' ); $check_clause = $this->serialize_mysql_expression( $expr ); return array( @@ -1970,14 +1944,14 @@ private function sync_column_key_info( bool $table_is_temporary, string $table_n } /** - * Extract table name from one of fully-qualified name AST nodes. + * Extract table name from a fully-qualified name AST node. * - * @param WP_Parser_Node $node The AST node. One of "tableName" or "tableRef". + * @param WP_Parser_Node $node The "table_ident" AST node. * @return string The table name. */ private function get_table_name_from_node( WP_Parser_Node $node ): string { - if ( 'tableRef' === $node->rule_name || 'tableName' === $node->rule_name ) { - $parts = $node->get_descendant_nodes( 'identifier' ); + if ( 'table_ident' === $node->rule_name ) { + $parts = $node->get_child_nodes( 'ident' ); return $this->get_value( end( $parts ) ); } @@ -1987,13 +1961,19 @@ private function get_table_name_from_node( WP_Parser_Node $node ): string { } /** - * Extract table engine value from the "createStatement" AST node. + * Extract table engine value from the "create_table_stmt" AST node. * - * @param WP_Parser_Node $node The "createStatement" AST node with "createTable" child. + * @param WP_Parser_Node $node The "create_table_stmt" AST node. * @return string The table engine as stored in information schema. */ private function get_table_engine( WP_Parser_Node $node ): string { - $engine_node = $node->get_first_descendant_node( 'engineRef' ); + $engine_node = null; + foreach ( $node->get_descendant_nodes( 'create_table_option' ) as $option ) { + if ( $option->has_child_token( WP_MySQL_Tokens::KEYWORDS['ENGINE'] ) ) { + $engine_node = $option->get_first_child_node( 'ident_or_text' ); + break; + } + } if ( null === $engine_node ) { return 'InnoDB'; } @@ -2008,13 +1988,13 @@ private function get_table_engine( WP_Parser_Node $node ): string { } /** - * Extract table collation value from the "createStatement" AST node. + * Extract table collation value from the "create_table_stmt" AST node. * - * @param WP_Parser_Node $node The "createStatement" AST node with "createTable" child. + * @param WP_Parser_Node $node The "create_table_stmt" AST node. * @return string The table collation as stored in information schema. */ private function get_table_collation( WP_Parser_Node $node ): string { - $collate_node = $node->get_first_descendant_node( 'collationName' ); + $collate_node = $node->get_first_descendant_node( 'collation_name' ); if ( null === $collate_node ) { // @TODO: Use default DB collation or DB_CHARSET & DB_COLLATE. return 'utf8mb4_0900_ai_ci'; @@ -2023,30 +2003,30 @@ private function get_table_collation( WP_Parser_Node $node ): string { } /** - * Extract table comment from the "createStatement" AST node. + * Extract table comment from the "create_table_stmt" AST node. * - * @param WP_Parser_Node $node The "createStatement" AST node with "createTable" child. + * @param WP_Parser_Node $node The "create_table_stmt" AST node. * @return string The table comment as stored in information schema. */ private function get_table_comment( WP_Parser_Node $node ): string { - foreach ( $node->get_descendant_nodes( 'createTableOption' ) as $attr ) { - if ( $attr->has_child_token( WP_MySQL_Lexer::COMMENT_SYMBOL ) ) { - return $this->get_value( $attr->get_first_child_node( 'textStringLiteral' ) ); + foreach ( $node->get_descendant_nodes( 'create_table_option' ) as $attr ) { + if ( $attr->has_child_token( WP_MySQL_Tokens::KEYWORDS['COMMENT'] ) ) { + return $this->get_value( $attr->get_first_child_node( 'TEXT_STRING_sys' ) ); } } return ''; } /** - * Extract column default value from the "columnDefinition" or "fieldDefinition" AST node. + * Extract column default value from the "column_def" or "field_def" AST node. * - * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node. + * @param WP_Parser_Node $node The "column_def" or "field_def" AST node. * @return string The column default as stored in information schema. */ private function get_column_default( WP_Parser_Node $node ): ?string { $default_attr = null; - foreach ( $node->get_descendant_nodes( 'columnAttribute' ) as $attr ) { - if ( $attr->has_child_token( WP_MySQL_Lexer::DEFAULT_SYMBOL ) ) { + foreach ( $node->get_descendant_nodes( 'column_attribute' ) as $attr ) { + if ( $attr->has_child_token( WP_MySQL_Tokens::KEYWORDS['DEFAULT'] ) ) { $default_attr = $attr; } } @@ -2057,32 +2037,36 @@ private function get_column_default( WP_Parser_Node $node ): ?string { /* * [GRAMMAR] - * DEFAULT_SYMBOL ( - * signedLiteral - * | NOW_SYMBOL timeFunctionParameters? - * | {serverVersion >= 80013}? exprWithParentheses - * ) + * column_attribute: + * DEFAULT_SYM now_or_signed_literal + * | DEFAULT_SYM '(' expr ')' + * | ... + * now_or_signed_literal: now | signed_literal_or_null + * signed_literal_or_null: signed_literal | null_as_literal */ + $now_or_literal = $default_attr->get_first_child_node( 'now_or_signed_literal' ); + if ( $now_or_literal ) { + // DEFAULT NOW() + if ( $now_or_literal->has_child_node( 'now' ) ) { + return 'CURRENT_TIMESTAMP'; + } - // DEFAULT NOW() - if ( $default_attr->has_child_token( WP_MySQL_Lexer::NOW_SYMBOL ) ) { - return 'CURRENT_TIMESTAMP'; - } - - // DEFAULT signedLiteral - $signed_literal = $default_attr->get_first_child_node( 'signedLiteral' ); - if ( $signed_literal ) { - $literal = $signed_literal->get_first_child_node( 'literal' ); + $literal_or_null = $now_or_literal->get_first_child_node( 'signed_literal_or_null' ); // DEFAULT NULL - if ( $literal && $literal->has_child_node( 'nullLiteral' ) ) { + if ( $literal_or_null->has_child_node( 'null_as_literal' ) ) { return null; } + $signed_literal = $literal_or_null->get_first_child_node( 'signed_literal' ); + $literal = $signed_literal->get_first_child_node( 'literal' ); + // DEFAULT TRUE or DEFAULT FALSE - if ( $literal && $literal->has_child_node( 'boolLiteral' ) ) { - $bool_literal = $literal->get_first_child_node( 'boolLiteral' ); - return $bool_literal->has_child_token( WP_MySQL_Lexer::TRUE_SYMBOL ) ? '1' : '0'; + if ( $literal && $literal->has_child_token( WP_MySQL_Tokens::KEYWORDS['TRUE'] ) ) { + return '1'; + } + if ( $literal && $literal->has_child_token( WP_MySQL_Tokens::KEYWORDS['FALSE'] ) ) { + return '0'; } // @TODO: MySQL seems to normalize default values for numeric @@ -2090,38 +2074,38 @@ private function get_column_default( WP_Parser_Node $node ): ?string { return $this->get_value( $signed_literal ); } - // DEFAULT (expression) - MySQL 8.0.13+ supports exprWithParentheses - $expr_with_parens = $default_attr->get_first_child_node( 'exprWithParentheses' ); - if ( $expr_with_parens ) { - return $this->serialize_mysql_expression( $expr_with_parens ); + // DEFAULT (expression) - supported as of MySQL 8.0.13 + $expr = $default_attr->get_first_child_node( 'expr' ); + if ( $expr ) { + return $this->serialize_mysql_expression( $expr ); } throw new Exception( 'DEFAULT value of this type is not supported.' ); } /** - * Extract column nullability from the "columnDefinition" or "fieldDefinition" AST node. + * Extract column nullability from the "column_def" or "field_def" AST node. * - * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node. + * @param WP_Parser_Node $node The "column_def" or "field_def" AST node. * @return string The column nullability as stored in information schema. */ private function get_column_nullable( WP_Parser_Node $node ): string { // SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE. - $data_type = $node->get_first_descendant_node( 'dataType' ); - if ( null !== $data_type->get_first_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL ) ) { + $data_type = $node->get_first_descendant_node( 'type' ); + if ( null !== $data_type->get_first_descendant_token( WP_MySQL_Tokens::KEYWORDS['SERIAL'] ) ) { return 'NO'; } - foreach ( $node->get_descendant_nodes( 'columnAttribute' ) as $attr ) { + foreach ( $node->get_descendant_nodes( 'column_attribute' ) as $attr ) { // PRIMARY KEY columns are always NOT NULL. - if ( $attr->has_child_token( WP_MySQL_Lexer::KEY_SYMBOL ) ) { + if ( $attr->has_child_token( WP_MySQL_Tokens::KEYWORDS['KEY'] ) ) { return 'NO'; } // Check for NOT NULL attribute. if ( - $attr->has_child_token( WP_MySQL_Lexer::NOT_SYMBOL ) - && $attr->has_child_node( 'nullLiteral' ) + $attr->has_child_node( 'not' ) + && $attr->has_child_token( WP_MySQL_Tokens::KEYWORDS['NULL'] ) ) { return 'NO'; } @@ -2130,32 +2114,32 @@ private function get_column_nullable( WP_Parser_Node $node ): string { } /** - * Extract column key info from the "columnDefinition" or "fieldDefinition" AST node. + * Extract column key info from the "column_def" or "field_def" AST node. * - * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node. + * @param WP_Parser_Node $node The "column_def" or "field_def" AST node. * @return string The column key info as stored in information schema. */ private function get_column_key( WP_Parser_Node $node ): string { // 1. PRI: Column is a primary key or its any component. if ( - null !== $node->get_first_descendant_token( WP_MySQL_Lexer::KEY_SYMBOL ) + null !== $node->get_first_descendant_token( WP_MySQL_Tokens::KEYWORDS['KEY'] ) ) { return 'PRI'; } // SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE. - $data_type = $node->get_first_descendant_node( 'dataType' ); - if ( null !== $data_type->get_first_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL ) ) { + $data_type = $node->get_first_descendant_node( 'type' ); + if ( null !== $data_type->get_first_descendant_token( WP_MySQL_Tokens::KEYWORDS['SERIAL'] ) ) { return 'PRI'; } // 2. UNI: Column has UNIQUE constraint. - if ( null !== $node->get_first_descendant_token( WP_MySQL_Lexer::UNIQUE_SYMBOL ) ) { + if ( null !== $node->get_first_descendant_token( WP_MySQL_Tokens::KEYWORDS['UNIQUE'] ) ) { return 'UNI'; } // 3. MUL: Column has INDEX. - if ( null !== $node->get_first_descendant_token( WP_MySQL_Lexer::INDEX_SYMBOL ) ) { + if ( null !== $node->get_first_descendant_token( WP_MySQL_Tokens::KEYWORDS['INDEX'] ) ) { return 'MUL'; } @@ -2163,35 +2147,36 @@ private function get_column_key( WP_Parser_Node $node ): string { } /** - * Extract column extra from the "columnDefinition" or "fieldDefinition" AST node. + * Extract column extra from the "column_def" or "field_def" AST node. * - * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node. + * @param WP_Parser_Node $node The "column_def" or "field_def" AST node. * @return string The column extra as stored in information schema. */ private function get_column_extra( WP_Parser_Node $node ): string { $extras = array(); - $attributes = $node->get_descendant_nodes( 'columnAttribute' ); + $attributes = $node->get_descendant_nodes( 'column_attribute' ); // SERIAL - $data_type = $node->get_first_descendant_node( 'dataType' ); - if ( null !== $data_type->get_first_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL ) ) { + $data_type = $node->get_first_descendant_node( 'type' ); + if ( null !== $data_type->get_first_descendant_token( WP_MySQL_Tokens::KEYWORDS['SERIAL'] ) ) { return 'auto_increment'; } // AUTO_INCREMENT columns can't have a DEFAULT value. foreach ( $attributes as $attr ) { - if ( $attr->has_child_token( WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL ) ) { + if ( $attr->has_child_token( WP_MySQL_Tokens::KEYWORDS['AUTO_INCREMENT'] ) ) { return 'auto_increment'; } } // Check whether DEFAULT value is generated. foreach ( $attributes as $attr ) { + $now_or_literal = $attr->get_first_child_node( 'now_or_signed_literal' ); if ( - $attr->has_child_token( WP_MySQL_Lexer::DEFAULT_SYMBOL ) + $attr->has_child_token( WP_MySQL_Tokens::KEYWORDS['DEFAULT'] ) && ( - $attr->has_child_node( 'exprWithParentheses' ) - || $attr->has_child_token( WP_MySQL_Lexer::NOW_SYMBOL ) + $attr->has_child_node( 'expr' ) + || ( null !== $now_or_literal && $now_or_literal->has_child_node( 'now' ) ) ) ) { $extras[] = 'DEFAULT_GENERATED'; @@ -2201,45 +2186,45 @@ private function get_column_extra( WP_Parser_Node $node ): string { // Check for ON UPDATE CURRENT_TIMESTAMP. foreach ( $attributes as $attr ) { if ( - $attr->has_child_token( WP_MySQL_Lexer::ON_SYMBOL ) - && $attr->has_child_token( WP_MySQL_Lexer::UPDATE_SYMBOL ) + $attr->has_child_token( WP_MySQL_Tokens::KEYWORDS['ON'] ) + && $attr->has_child_token( WP_MySQL_Tokens::KEYWORDS['UPDATE'] ) ) { $extras[] = 'on update CURRENT_TIMESTAMP'; } } // Check for generated columns. - if ( $node->get_first_descendant_token( WP_MySQL_Lexer::VIRTUAL_SYMBOL ) ) { + if ( $node->get_first_descendant_token( WP_MySQL_Tokens::KEYWORDS['VIRTUAL'] ) ) { $extras[] = 'VIRTUAL GENERATED'; - } elseif ( $node->get_first_descendant_token( WP_MySQL_Lexer::STORED_SYMBOL ) ) { + } elseif ( $node->get_first_descendant_token( WP_MySQL_Tokens::KEYWORDS['STORED'] ) ) { $extras[] = 'STORED GENERATED'; } return implode( ' ', $extras ); } /** - * Extract column comment from the "columnDefinition" or "fieldDefinition" AST node. + * Extract column comment from the "column_def" or "field_def" AST node. * - * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node. + * @param WP_Parser_Node $node The "column_def" or "field_def" AST node. * @return string The column comment as stored in information schema. */ private function get_column_comment( WP_Parser_Node $node ): string { - foreach ( $node->get_descendant_nodes( 'columnAttribute' ) as $attr ) { - if ( $attr->has_child_token( WP_MySQL_Lexer::COMMENT_SYMBOL ) ) { - return $this->get_value( $attr->get_first_child_node( 'textLiteral' ) ); + foreach ( $node->get_descendant_nodes( 'column_attribute' ) as $attr ) { + if ( $attr->has_child_token( WP_MySQL_Tokens::KEYWORDS['COMMENT'] ) ) { + return $this->get_value( $attr->get_first_child_node( 'TEXT_STRING_sys' ) ); } } return ''; } /** - * Extract column data type from the "columnDefinition" or "fieldDefinition" AST node. + * Extract column data type from the "column_def" or "field_def" AST node. * - * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node. + * @param WP_Parser_Node $node The "column_def" or "field_def" AST node. * @return array{ string, string } The data type and column type as stored in information schema. */ private function get_column_data_types( WP_Parser_Node $node ): array { - $type_node = $node->get_first_descendant_node( 'dataType' ); + $type_node = $node->get_first_descendant_node( 'type' ); $type = $type_node->get_descendant_tokens(); $token = $type[0]; @@ -2251,36 +2236,36 @@ private function get_column_data_types( WP_Parser_Node $node ): array { // NCHAR/NATIONAL VARCHAR // CHAR/CHARACTER/NCHAR VARYING // NATIONAL CHAR/CHARACTER VARYING - WP_MySQL_Lexer::VARCHAR_SYMBOL === $token->id - || WP_MySQL_Lexer::NVARCHAR_SYMBOL === $token->id - || ( isset( $type[1] ) && WP_MySQL_Lexer::VARCHAR_SYMBOL === $type[1]->id ) - || ( isset( $type[1] ) && WP_MySQL_Lexer::VARYING_SYMBOL === $type[1]->id ) - || ( isset( $type[2] ) && WP_MySQL_Lexer::VARYING_SYMBOL === $type[2]->id ) + WP_MySQL_Tokens::KEYWORDS['VARCHAR'] === $token->id + || WP_MySQL_Tokens::KEYWORDS['NVARCHAR'] === $token->id + || ( isset( $type[1] ) && WP_MySQL_Tokens::KEYWORDS['VARCHAR'] === $type[1]->id ) + || ( isset( $type[1] ) && WP_MySQL_Tokens::KEYWORDS['VARYING'] === $type[1]->id ) + || ( isset( $type[2] ) && WP_MySQL_Tokens::KEYWORDS['VARYING'] === $type[2]->id ) ) { $type = 'varchar'; } elseif ( // CHAR, NCHAR, NATIONAL CHAR - WP_MySQL_Lexer::CHAR_SYMBOL === $token->id - || WP_MySQL_Lexer::NCHAR_SYMBOL === $token->id - || isset( $type[1] ) && WP_MySQL_Lexer::CHAR_SYMBOL === $type[1]->id + WP_MySQL_Tokens::KEYWORDS['CHAR'] === $token->id + || WP_MySQL_Tokens::KEYWORDS['NCHAR'] === $token->id + || isset( $type[1] ) && WP_MySQL_Tokens::KEYWORDS['CHAR'] === $type[1]->id ) { $type = 'char'; } elseif ( // LONG VARBINARY - WP_MySQL_Lexer::LONG_SYMBOL === $token->id - && isset( $type[1] ) && WP_MySQL_Lexer::VARBINARY_SYMBOL === $type[1]->id + WP_MySQL_Tokens::KEYWORDS['LONG'] === $token->id + && isset( $type[1] ) && WP_MySQL_Tokens::KEYWORDS['VARBINARY'] === $type[1]->id ) { $type = 'mediumblob'; } elseif ( // LONG CHAR/CHARACTER, LONG CHAR/CHARACTER VARYING - WP_MySQL_Lexer::LONG_SYMBOL === $token->id - && isset( $type[1] ) && WP_MySQL_Lexer::CHAR_SYMBOL === $type[1]->id + WP_MySQL_Tokens::KEYWORDS['LONG'] === $token->id + && isset( $type[1] ) && WP_MySQL_Tokens::KEYWORDS['CHAR'] === $type[1]->id ) { $type = 'mediumtext'; } elseif ( // LONG VARCHAR - WP_MySQL_Lexer::LONG_SYMBOL === $token->id - && isset( $type[1] ) && WP_MySQL_Lexer::VARCHAR_SYMBOL === $type[1]->id + WP_MySQL_Tokens::KEYWORDS['LONG'] === $token->id + && isset( $type[1] ) && WP_MySQL_Tokens::KEYWORDS['VARCHAR'] === $type[1]->id ) { $type = 'mediumtext'; } else { @@ -2290,15 +2275,15 @@ private function get_column_data_types( WP_Parser_Node $node ): array { // Get full type. $full_type = $type; if ( 'enum' === $type || 'set' === $type ) { - $string_list = $type_node->get_first_descendant_node( 'stringList' ); - $values = $string_list->get_child_nodes( 'textString' ); + $string_list = $type_node->get_first_descendant_node( 'string_list' ); + $values = $string_list->get_flattened_child_nodes( 'text_string' ); foreach ( $values as $i => $value ) { $values[ $i ] = "'" . str_replace( "'", "''", $this->get_value( $value ) ) . "'"; } $full_type .= '(' . implode( ',', $values ) . ')'; } - $field_length = $type_node->get_first_descendant_node( 'fieldLength' ); + $field_length = $type_node->get_first_descendant_node( 'field_length' ); if ( null !== $field_length ) { if ( 'decimal' === $type || 'float' === $type || 'double' === $type ) { $full_type .= rtrim( $this->get_value( $field_length ), ')' ) . ',0)'; @@ -2319,14 +2304,14 @@ private function get_column_data_types( WP_Parser_Node $node ): array { $full_type .= $this->get_value( $precision ); } - $datetime_precision = $type_node->get_first_descendant_node( 'typeDatetimePrecision' ); + $datetime_precision = $type_node->get_first_descendant_node( 'type_datetime_precision' ); if ( null !== $datetime_precision ) { $full_type .= $this->get_value( $datetime_precision ); } if ( - WP_MySQL_Lexer::BOOL_SYMBOL === $token->id - || WP_MySQL_Lexer::BOOLEAN_SYMBOL === $token->id + WP_MySQL_Tokens::KEYWORDS['BOOL'] === $token->id + || WP_MySQL_Tokens::KEYWORDS['BOOLEAN'] === $token->id ) { $full_type .= '(1)'; // Add length for booleans. } @@ -2342,14 +2327,14 @@ private function get_column_data_types( WP_Parser_Node $node ): array { // UNSIGNED. // SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE. if ( - $type_node->get_first_descendant_token( WP_MySQL_Lexer::UNSIGNED_SYMBOL ) - || $type_node->get_first_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL ) + $type_node->get_first_descendant_token( WP_MySQL_Tokens::KEYWORDS['UNSIGNED'] ) + || $type_node->get_first_descendant_token( WP_MySQL_Tokens::KEYWORDS['SERIAL'] ) ) { $full_type .= ' unsigned'; } // ZEROFILL. - if ( $type_node->get_first_descendant_token( WP_MySQL_Lexer::ZEROFILL_SYMBOL ) ) { + if ( $type_node->get_first_descendant_token( WP_MySQL_Tokens::KEYWORDS['ZEROFILL'] ) ) { $full_type .= ' zerofill'; } @@ -2357,9 +2342,9 @@ private function get_column_data_types( WP_Parser_Node $node ): array { } /** - * Extract column charset and collation from the "columnDefinition" or "fieldDefinition" AST node. + * Extract column charset and collation from the "column_def" or "field_def" AST node. * - * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node. + * @param WP_Parser_Node $node The "column_def" or "field_def" AST node. * @param string $data_type The column data type as stored in information schema. * @return array{ string|null, string|null } The column charset and collation as stored in information schema. */ @@ -2382,32 +2367,33 @@ private function get_column_charset_and_collation( WP_Parser_Node $node, string $is_binary = false; // Charset. - $charset_node = $node->get_first_descendant_node( 'charsetWithOptBinary' ); + $charset_node = $node->get_first_descendant_node( 'opt_charset_with_opt_binary' ); if ( null !== $charset_node ) { - $charset_name_node = $charset_node->get_first_child_node( 'charsetName' ); + $charset_name_node = $charset_node->get_first_child_node( 'charset_name' ); if ( null !== $charset_name_node ) { $charset = strtolower( $this->get_value( $charset_name_node ) ); - } elseif ( $charset_node->has_child_token( WP_MySQL_Lexer::ASCII_SYMBOL ) ) { + } elseif ( $charset_node->has_child_token( WP_MySQL_Tokens::KEYWORDS['ASCII'] ) ) { $charset = 'latin1'; - } elseif ( $charset_node->has_child_token( WP_MySQL_Lexer::UNICODE_SYMBOL ) ) { + } elseif ( $charset_node->has_child_token( WP_MySQL_Tokens::KEYWORDS['UNICODE'] ) ) { $charset = 'ucs2'; - } elseif ( $charset_node->has_child_token( WP_MySQL_Lexer::BYTE_SYMBOL ) ) { + } elseif ( $charset_node->has_child_token( WP_MySQL_Tokens::KEYWORDS['BYTE'] ) ) { // @TODO: This changes varchar to varbinary. } // @TODO: "DEFAULT" - if ( $charset_node->has_child_token( WP_MySQL_Lexer::BINARY_SYMBOL ) ) { + if ( + $charset_node->has_child_token( WP_MySQL_Tokens::KEYWORDS['BINARY'] ) + || $charset_node->has_child_node( 'opt_bin_mod' ) + ) { $is_binary = true; } } else { // National charsets (in MySQL, it's "utf8"). - $data_type_node = $node->get_first_descendant_node( 'dataType' ); + $data_type_node = $node->get_first_descendant_node( 'type' ); if ( $data_type_node->has_child_node( 'nchar' ) - || $data_type_node->has_child_token( WP_MySQL_Lexer::NCHAR_SYMBOL ) - || $data_type_node->has_child_token( WP_MySQL_Lexer::NATIONAL_SYMBOL ) - || $data_type_node->has_child_token( WP_MySQL_Lexer::NVARCHAR_SYMBOL ) + || $data_type_node->has_child_node( 'nvarchar' ) ) { $charset = 'utf8'; } @@ -2419,7 +2405,7 @@ private function get_column_charset_and_collation( WP_Parser_Node $node, string } // Collation. - $collation_node = $node->get_first_descendant_node( 'collationName' ); + $collation_node = $node->get_first_descendant_node( 'collation_name' ); if ( null !== $collation_node ) { $collation = strtolower( $this->get_value( $collation_node ) ); } @@ -2449,9 +2435,9 @@ private function get_column_charset_and_collation( WP_Parser_Node $node, string } /** - * Extract column length info from the "columnDefinition" or "fieldDefinition" AST node. + * Extract column length info from the "column_def" or "field_def" AST node. * - * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node. + * @param WP_Parser_Node $node The "column_def" or "field_def" AST node. * @param string $data_type The column data type as stored in information schema. * @param string|null $charset The column charset as stored in information schema. * @return array{ int|null, int|null } The column char length and octet length as stored in information schema. @@ -2475,7 +2461,7 @@ private function get_column_lengths( WP_Parser_Node $node, string $data_type, ?s || 'varchar' === $data_type || 'varbinary' === $data_type ) { - $field_length = $node->get_first_descendant_node( 'fieldLength' ); + $field_length = $node->get_first_descendant_node( 'field_length' ); if ( null === $field_length ) { $length = 1; } else { @@ -2492,8 +2478,8 @@ private function get_column_lengths( WP_Parser_Node $node, string $data_type, ?s // For ENUM and SET, we need to check the longest value. if ( 'enum' === $data_type || 'set' === $data_type ) { - $string_list = $node->get_first_descendant_node( 'stringList' ); - $values = $string_list->get_child_nodes( 'textString' ); + $string_list = $node->get_first_descendant_node( 'string_list' ); + $values = $string_list->get_flattened_child_nodes( 'text_string' ); $length = 0; foreach ( $values as $value ) { if ( 'enum' === $data_type ) { @@ -2517,9 +2503,9 @@ private function get_column_lengths( WP_Parser_Node $node, string $data_type, ?s } /** - * Extract column precision and scale from the "columnDefinition" or "fieldDefinition" AST node. + * Extract column precision and scale from the "column_def" or "field_def" AST node. * - * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node. + * @param WP_Parser_Node $node The "column_def" or "field_def" AST node. * @param string $data_type The column data type as stored in information schema. * @return array{ int|null, int|null } The column precision and scale as stored in information schema. */ @@ -2533,13 +2519,13 @@ private function get_column_numeric_attributes( WP_Parser_Node $node, string $da } elseif ( 'int' === $data_type ) { return array( 10, 0 ); } elseif ( 'bigint' === $data_type ) { - if ( null !== $node->get_first_descendant_token( WP_MySQL_Lexer::UNSIGNED_SYMBOL ) ) { + if ( null !== $node->get_first_descendant_token( WP_MySQL_Tokens::KEYWORDS['UNSIGNED'] ) ) { return array( 20, 0 ); } // SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE. - $data_type = $node->get_first_descendant_node( 'dataType' ); - if ( null !== $data_type->get_first_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL ) ) { + $data_type = $node->get_first_descendant_node( 'type' ); + if ( null !== $data_type->get_first_descendant_token( WP_MySQL_Tokens::KEYWORDS['SERIAL'] ) ) { return array( 20, 0 ); } @@ -2548,7 +2534,7 @@ private function get_column_numeric_attributes( WP_Parser_Node $node, string $da // For bit columns, we need to check the precision. if ( 'bit' === $data_type ) { - $field_length = $node->get_first_descendant_node( 'fieldLength' ); + $field_length = $node->get_first_descendant_node( 'field_length' ); if ( null === $field_length ) { return array( 1, null ); } @@ -2560,7 +2546,7 @@ private function get_column_numeric_attributes( WP_Parser_Node $node, string $da $scale = null; $precision_node = $node->get_first_descendant_node( 'precision' ); if ( null !== $precision_node ) { - $values = $precision_node->get_descendant_tokens( WP_MySQL_Lexer::INT_NUMBER ); + $values = $precision_node->get_descendant_tokens( WP_MySQL_Tokens::INT_NUMBER ); $precision = (int) $values[0]->get_value(); $scale = (int) $values[1]->get_value(); } @@ -2571,8 +2557,8 @@ private function get_column_numeric_attributes( WP_Parser_Node $node, string $da return array( $precision ?? 22, $scale ); } elseif ( 'decimal' === $data_type ) { if ( null === $precision ) { - // Only precision can be specified ("fieldLength" in the grammar). - $field_length = $node->get_first_descendant_node( 'fieldLength' ); + // Only precision can be specified ("field_length" in the grammar). + $field_length = $node->get_first_descendant_node( 'field_length' ); if ( null !== $field_length ) { $precision = (int) trim( $this->get_value( $field_length ), '()' ); } @@ -2584,15 +2570,15 @@ private function get_column_numeric_attributes( WP_Parser_Node $node, string $da } /** - * Extract column date/time precision from the "columnDefinition" or "fieldDefinition" AST node. + * Extract column date/time precision from the "column_def" or "field_def" AST node. * - * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node. + * @param WP_Parser_Node $node The "column_def" or "field_def" AST node. * @param string $data_type The column data type as stored in information schema. * @return int|null The date/time precision as stored in information schema. */ private function get_column_datetime_precision( WP_Parser_Node $node, string $data_type ): ?int { if ( 'time' === $data_type || 'datetime' === $data_type || 'timestamp' === $data_type ) { - $precision = $node->get_first_descendant_node( 'typeDatetimePrecision' ); + $precision = $node->get_first_descendant_node( 'type_datetime_precision' ); if ( null === $precision ) { return 0; } else { @@ -2603,34 +2589,34 @@ private function get_column_datetime_precision( WP_Parser_Node $node, string $da } /** - * Extract column generation expression from the "columnDefinition" or "fieldDefinition" AST node. + * Extract column generation expression from the "column_def" or "field_def" AST node. * - * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node. + * @param WP_Parser_Node $node The "column_def" or "field_def" AST node. * @return string The column generation expression as stored in information schema. */ private function get_column_generation_expression( WP_Parser_Node $node ): string { - if ( null !== $node->get_first_descendant_token( WP_MySQL_Lexer::GENERATED_SYMBOL ) ) { - $expr = $node->get_first_descendant_node( 'exprWithParentheses' ); - return $this->get_value( $expr ); + if ( null !== $node->get_first_descendant_token( WP_MySQL_Tokens::KEYWORDS['GENERATED'] ) ) { + $expr = $node->get_first_descendant_node( 'expr' ); + return '(' . $this->get_value( $expr ) . ')'; } return ''; } /** - * Extract table constraint name from the "tableConstraintDef" or "columnDefinition" AST node. + * Extract table constraint name from the "table_constraint_def" or "column_def" AST node. * - * @param WP_Parser_Node $node The "tableConstraintDef" or "columnDefinition" AST node. + * @param WP_Parser_Node $node The "table_constraint_def" or "column_def" AST node. * @param string $table_name The table name. * @return string|null The table constraint name. */ public function get_table_constraint_name( WP_Parser_Node $node, string $table_name ): ?string { - $name_node = $node->get_first_child_node( 'constraintName' ); - if ( null !== $name_node ) { - return $this->get_value( $name_node->get_first_child_node( 'identifier' ) ); + $name_node = $node->get_first_child_node( 'opt_constraint_name' ); + if ( null !== $name_node && null !== $name_node->get_first_descendant_node( 'ident' ) ) { + return $this->get_value( $name_node->get_first_descendant_node( 'ident' ) ); } $foreign_key = $node->get_first_descendant_node( 'references' ); - $check_constraint = $node->get_first_descendant_node( 'checkConstraint' ); + $check_constraint = $node->get_first_descendant_node( 'check_constraint' ); // FOREIGN KEY and CHECK constraints without a name get a generated name. if ( $foreign_key || $check_constraint ) { @@ -2675,36 +2661,36 @@ public function get_table_constraint_name( WP_Parser_Node $node, string $table_n } /** - * Extract table constraint type from the "tableConstraintDef" or "columnDefinition" AST node. + * Extract table constraint type from the "table_constraint_def" or "column_def" AST node. * - * @param WP_Parser_Node $node The "tableConstraintDef" or "columnDefinition" AST node. + * @param WP_Parser_Node $node The "table_constraint_def" or "column_def" AST node. * @return string|null The table constraint type as stored in information schema. */ private function get_table_constraint_type( WP_Parser_Node $node ): ?string { - if ( $node->get_first_descendant_token( WP_MySQL_Lexer::PRIMARY_SYMBOL ) ) { + if ( $node->get_first_descendant_token( WP_MySQL_Tokens::KEYWORDS['PRIMARY'] ) ) { return 'PRIMARY KEY'; } - if ( $node->get_first_descendant_token( WP_MySQL_Lexer::UNIQUE_SYMBOL ) ) { + if ( $node->get_first_descendant_token( WP_MySQL_Tokens::KEYWORDS['UNIQUE'] ) ) { return 'UNIQUE'; } if ( $node->get_first_descendant_node( 'references' ) ) { return 'FOREIGN KEY'; } - if ( $node->get_first_descendant_node( 'checkConstraint' ) ) { + if ( $node->get_first_descendant_node( 'check_constraint' ) ) { return 'CHECK'; } return null; } /** - * Extract index name from the "tableConstraintDef" AST node. + * Extract index name from the "table_constraint_def" AST node. * - * @param WP_Parser_Node $node The "tableConstraintDef" or "createIndex" AST node. + * @param WP_Parser_Node $node The "table_constraint_def" or "create_index_stmt" AST node. * @param string $table_name The table name. * @return string The index name as stored in information schema. */ private function get_index_name( WP_Parser_Node $node, string $table_name ): string { - if ( $node->get_first_descendant_token( WP_MySQL_Lexer::PRIMARY_SYMBOL ) ) { + if ( $node->get_first_descendant_token( WP_MySQL_Tokens::KEYWORDS['PRIMARY'] ) ) { return 'PRIMARY'; } @@ -2714,11 +2700,19 @@ private function get_index_name( WP_Parser_Node $node, string $table_name ): str * When both index and constraint name are defined, the index name will * be used. E.g., in "CONSTRAINT c UNIQUE u (id)", the name will be "u". */ - $name_node = $node->get_first_descendant_node( 'indexName' ); - if ( null === $name_node && $node->has_child_node( 'constraintName' ) ) { + if ( 'create_index_stmt' === $node->rule_name ) { + $name_node = $node->get_first_child_node( 'ident' ); + } else { + $name_and_type = $node->get_first_child_node( 'opt_index_name_and_type' ); + $opt_ident = null !== $name_and_type + ? $name_and_type->get_first_child_node( 'opt_ident' ) + : $node->get_first_child_node( 'opt_ident' ); + $name_node = null !== $opt_ident ? $opt_ident->get_first_child_node( 'ident' ) : null; + } + if ( null === $name_node && $node->has_child_node( 'opt_constraint_name' ) ) { $name_node = $node - ->get_first_child_node( 'constraintName' ) - ->get_first_child_node( 'identifier' ); + ->get_first_child_node( 'opt_constraint_name' ) + ->get_first_descendant_node( 'ident' ); } if ( null === $name_node ) { @@ -2727,11 +2721,11 @@ private function get_index_name( WP_Parser_Node $node, string $table_name ): str * If any part is an expression, the name will be "functional_index". * If the name is already used, we need to append a number. */ - $subnode = $node->get_first_child_node( 'keyListVariants' )->get_first_child_node(); - if ( null !== $subnode->get_first_descendant_node( 'exprWithParentheses' ) ) { + $subnode = $node->get_first_descendant_node( 'key_list_with_expression' ); + if ( null !== $subnode->get_first_descendant_node( 'expr' ) ) { $name = 'functional_index'; } else { - $name = $this->get_value( $subnode->get_first_descendant_node( 'identifier' ) ); + $name = $this->get_value( $subnode->get_first_descendant_node( 'ident' ) ); } // Check if the name is already used. @@ -2777,15 +2771,55 @@ private function get_index_name( WP_Parser_Node $node, string $table_name ): str } /** - * Extract index non-unique value from the "tableConstraintDef" AST node. + * Extract the first constraint keyword token (PRIMARY, UNIQUE, KEY, INDEX, + * FULLTEXT, SPATIAL, FOREIGN, or CHECK) from a constraint or index AST node. + * + * @param WP_Parser_Node $node The "table_constraint_def" or "create_index_stmt" AST node. + * @return WP_MySQL_Token|null The first constraint keyword token. + */ + private function get_constraint_keyword_token( WP_Parser_Node $node ): ?WP_MySQL_Token { + if ( 'create_index_stmt' === $node->rule_name ) { + if ( $node->has_child_node( 'opt_unique' ) ) { + return $node->get_first_child_node( 'opt_unique' )->get_first_child_token(); + } + if ( $node->has_child_token( WP_MySQL_Tokens::KEYWORDS['FULLTEXT'] ) ) { + return $node->get_first_child_token( WP_MySQL_Tokens::KEYWORDS['FULLTEXT'] ); + } + if ( $node->has_child_token( WP_MySQL_Tokens::KEYWORDS['SPATIAL'] ) ) { + return $node->get_first_child_token( WP_MySQL_Tokens::KEYWORDS['SPATIAL'] ); + } + return $node->get_first_child_token( WP_MySQL_Tokens::KEYWORDS['INDEX'] ); + } + + foreach ( $node->get_children() as $child ) { + // Skip the "CONSTRAINT [name]" prefix. + if ( $child instanceof WP_Parser_Node && 'opt_constraint_name' === $child->rule_name ) { + continue; + } + + // FULLTEXT, SPATIAL, and FOREIGN appear as direct tokens. + if ( $child instanceof WP_MySQL_Token ) { + return $child; + } + + // PRIMARY [KEY], UNIQUE, KEY, INDEX, and CHECK appear under the + // "constraint_key_type", "key_or_index", and "check_constraint" + // wrapper nodes. + return $child->get_first_descendant_token(); + } + return null; + } + + /** + * Extract index non-unique value from the "table_constraint_def" AST node. * * @param WP_MySQL_Token $token The first constraint keyword. * @return int The value of non-unique as stored in information schema. */ private function get_index_non_unique( WP_MySQL_Token $token ): int { if ( - WP_MySQL_Lexer::PRIMARY_SYMBOL === $token->id - || WP_MySQL_Lexer::UNIQUE_SYMBOL === $token->id + WP_MySQL_Tokens::KEYWORDS['PRIMARY'] === $token->id + || WP_MySQL_Tokens::KEYWORDS['UNIQUE'] === $token->id ) { return 0; } @@ -2793,9 +2827,9 @@ private function get_index_non_unique( WP_MySQL_Token $token ): int { } /** - * Extract index type from the "tableConstraintDef" AST node. + * Extract index type from the "table_constraint_def" AST node. * - * @param WP_Parser_Node $node The "tableConstraintDef" or "createIndex" AST node. + * @param WP_Parser_Node $node The "table_constraint_def" or "create_index_stmt" AST node. * @param WP_MySQL_Token $token The first constraint keyword. * @param bool $has_spatial_column Whether the index contains a spatial column. * @return string The index type as stored in information schema. @@ -2806,7 +2840,7 @@ private function get_index_type( bool $has_spatial_column ): string { // Handle "USING ..." clause. - $index_type_node = $node->get_first_descendant_node( 'indexType' ); + $index_type_node = $node->get_first_descendant_node( 'index_type' ); if ( null !== $index_type_node ) { $index_type = strtoupper( $this->get_value( $index_type_node ) ); if ( 'RTREE' === $index_type ) { @@ -2819,9 +2853,9 @@ private function get_index_type( } // Derive index type from its definition. - if ( WP_MySQL_Lexer::FULLTEXT_SYMBOL === $token->id ) { + if ( WP_MySQL_Tokens::KEYWORDS['FULLTEXT'] === $token->id ) { return 'FULLTEXT'; - } elseif ( WP_MySQL_Lexer::SPATIAL_SYMBOL === $token->id ) { + } elseif ( WP_MySQL_Tokens::KEYWORDS['SPATIAL'] === $token->id ) { return 'SPATIAL'; } @@ -2834,37 +2868,37 @@ private function get_index_type( } /** - * Extract index comment from the "tableConstraintDef" AST node. + * Extract index comment from the "table_constraint_def" AST node. * - * @param WP_Parser_Node $node The "tableConstraintDef" or "createIndex" AST node. + * @param WP_Parser_Node $node The "table_constraint_def" or "create_index_stmt" AST node. * @return string The index comment as stored in information schema. */ public function get_index_comment( WP_Parser_Node $node ): string { - foreach ( $node->get_descendant_nodes( 'commonIndexOption' ) as $attr ) { - if ( $attr->has_child_token( WP_MySQL_Lexer::COMMENT_SYMBOL ) ) { - return $this->get_value( $attr->get_first_child_node( 'textLiteral' ) ); + foreach ( $node->get_descendant_nodes( 'common_index_option' ) as $attr ) { + if ( $attr->has_child_token( WP_MySQL_Tokens::KEYWORDS['COMMENT'] ) ) { + return $this->get_value( $attr->get_first_child_node( 'TEXT_STRING_sys' ) ); } } return ''; } /** - * Extract index column name from the "keyPart" AST node. + * Extract index column name from the "key_part" AST node. * - * @param WP_Parser_Node $node The "keyPart" AST node. + * @param WP_Parser_Node $node The "key_part" AST node. * @return string The index column name as stored in information schema. */ private function get_index_column_name( WP_Parser_Node $node ): ?string { - if ( 'keyPart' !== $node->rule_name ) { + if ( 'key_part' !== $node->rule_name ) { return null; } - return $this->get_value( $node->get_first_descendant_node( 'identifier' ) ); + return $this->get_value( $node->get_first_descendant_node( 'ident' ) ); } /** - * Extract index column name from the "keyPart" AST node. + * Extract index column name from the "key_part" AST node. * - * @param WP_Parser_Node $node The "keyPart" AST node. + * @param WP_Parser_Node $node The "key_part" AST node. * @param string $index_type The index type as stored in information schema. * @return string The index column name as stored in information schema. */ @@ -2873,7 +2907,7 @@ private function get_index_column_collation( WP_Parser_Node $node, string $index return null; } - $collate_node = $node->get_first_descendant_node( 'direction' ); + $collate_node = $node->get_first_descendant_node( 'ordering_direction' ); if ( null === $collate_node ) { return 'A'; } @@ -2882,9 +2916,9 @@ private function get_index_column_collation( WP_Parser_Node $node, string $index } /** - * Extract index column sub-part value from the "keyPart" AST node. + * Extract index column sub-part value from the "key_part" AST node. * - * @param WP_Parser_Node $node The "keyPart" AST node. + * @param WP_Parser_Node $node The "key_part" AST node. * @param int|null $max_length The maximum character length of the index column. * @param bool $is_spatial Whether the index column is a spatial column. * @return int|null The index column sub-part value as stored in information schema. @@ -2894,21 +2928,38 @@ private function get_index_column_sub_part( ?int $max_length, bool $is_spatial ): ?int { - $field_length = $node->get_first_descendant_node( 'fieldLength' ); - if ( null === $field_length ) { + // A prefix length appears as bare "( INT_NUMBER )" tokens in "key_part". + $length_token = 'key_part' === $node->rule_name + ? $node->get_first_child_token( WP_MySQL_Tokens::INT_NUMBER ) + : null; + if ( null === $length_token ) { if ( $is_spatial ) { return 32; } return null; } - $value = (int) trim( $this->get_value( $field_length ), '()' ); + $value = (int) $length_token->get_value(); if ( null !== $max_length && $value >= $max_length ) { return $max_length; } return $value; } + /** + * Extract referenced column "ident" nodes from the "references" AST node. + * + * @param WP_Parser_Node $node The "references" AST node. + * @return WP_Parser_Node[] The referenced column "ident" nodes. + */ + private function get_reference_list_columns( WP_Parser_Node $node ): array { + $ref_list = $node->get_first_child_node( 'opt_ref_list' ); + if ( null === $ref_list ) { + return array(); + } + return $ref_list->get_first_child_node( 'reference_list' )->get_flattened_child_nodes( 'ident' ); + } + /** * Extract foreign key UPDATE and DELETE actions from the "references" AST node. * @@ -2916,15 +2967,16 @@ private function get_index_column_sub_part( * @return array The foreign key actions as stored in information schema. */ private function get_foreign_key_actions( WP_Parser_Node $node ): array { - $children = $node->get_children(); + $on_update_delete = $node->get_first_child_node( 'opt_on_update_delete' ); + $children = null !== $on_update_delete ? $on_update_delete->get_children() : array(); - // ON UPDATE and ON DELETE both use the "deleteOption" node. + // ON UPDATE and ON DELETE both use the "delete_option" node. $update_option = null; $delete_option = null; foreach ( $children as $i => $child ) { - if ( $child instanceof WP_MySQL_Token && WP_MySQL_Lexer::UPDATE_SYMBOL === $child->id ) { + if ( $child instanceof WP_MySQL_Token && WP_MySQL_Tokens::KEYWORDS['UPDATE'] === $child->id ) { $update_option = $children[ $i + 1 ]; - } elseif ( $child instanceof WP_MySQL_Token && WP_MySQL_Lexer::DELETE_SYMBOL === $child->id ) { + } elseif ( $child instanceof WP_MySQL_Token && WP_MySQL_Tokens::KEYWORDS['DELETE'] === $child->id ) { $delete_option = $children[ $i + 1 ]; } } @@ -2942,15 +2994,15 @@ private function get_foreign_key_actions( WP_Parser_Node $node ): array { $tokens = $option->get_descendant_tokens(); $token1_id = isset( $tokens[0] ) ? $tokens[0]->id : null; $token2_id = isset( $tokens[1] ) ? $tokens[1]->id : null; - if ( WP_MySQL_Lexer::NO_SYMBOL === $token1_id ) { + if ( WP_MySQL_Tokens::KEYWORDS['NO'] === $token1_id ) { $result[ $action ] = 'NO ACTION'; - } elseif ( WP_MySQL_Lexer::RESTRICT_SYMBOL === $token1_id ) { + } elseif ( WP_MySQL_Tokens::KEYWORDS['RESTRICT'] === $token1_id ) { $result[ $action ] = 'RESTRICT'; - } elseif ( WP_MySQL_Lexer::CASCADE_SYMBOL === $token1_id ) { + } elseif ( WP_MySQL_Tokens::KEYWORDS['CASCADE'] === $token1_id ) { $result[ $action ] = 'CASCADE'; - } elseif ( WP_MySQL_Lexer::SET_SYMBOL === $token1_id && WP_MySQL_Lexer::NULL_SYMBOL === $token2_id ) { + } elseif ( WP_MySQL_Tokens::KEYWORDS['SET'] === $token1_id && WP_MySQL_Tokens::KEYWORDS['NULL'] === $token2_id ) { $result[ $action ] = 'SET NULL'; - } elseif ( WP_MySQL_Lexer::SET_SYMBOL === $token1_id && WP_MySQL_Lexer::DEFAULT_SYMBOL === $token2_id ) { + } elseif ( WP_MySQL_Tokens::KEYWORDS['SET'] === $token1_id && WP_MySQL_Tokens::KEYWORDS['DEFAULT'] === $token2_id ) { $result[ $action ] = 'SET DEFAULT'; } else { throw new \Exception( sprintf( 'Unsupported foreign key action: %s', $option->get_value() ) ); @@ -2981,9 +3033,9 @@ private function is_spatial_data_type( string $data_type ): bool { * * @TODO: This should be done in a more correct way, for names maybe allowing * descending only a single-child hierarchy, such as these: - * identifier -> pureIdentifier -> IDENTIFIER - * identifier -> pureIdentifier -> BACKTICK_QUOTED_ID - * identifier -> pureIdentifier -> DOUBLE_QUOTED_TEXT + * ident -> IDENT_sys -> IDENTIFIER + * ident -> IDENT_sys -> BACK_TICK_QUOTED_ID + * ident -> IDENT_sys -> DOUBLE_QUOTED_TEXT * etc. * * For saving "DEFAULT ..." in column definitions, we actually need to @@ -3004,7 +3056,7 @@ private function get_value( WP_Parser_Node $node ): string { * This is because SQLite doesn't support case-insensitive Unicode * character matching: https://sqlite.org/faq.html#q18 */ - if ( 'pureIdentifier' === $child->rule_name ) { + if ( 'IDENT_sys' === $child->rule_name ) { for ( $i = 0; $i < strlen( $value ); $i++ ) { if ( ord( $value[ $i ] ) > 127 ) { throw new Exception( 'The SQLite driver only supports ASCII characters in identifiers.' ); @@ -3041,12 +3093,6 @@ private function get_value( WP_Parser_Node $node ): string { * @return string The serialized value of the node. */ private function serialize_mysql_expression( WP_Parser_Node $node ): string { - // The wrapping parentheses are generally not stored, although in MySQL, - // this varies by expression type as per the expression formatter logic. - if ( 'exprWithParentheses' === $node->rule_name ) { - return $this->serialize_mysql_expression( $node->get_first_child_node( 'expr' ) ); - } - $value = ''; $last_token_id = null; foreach ( $node->get_descendant_tokens() as $i => $token ) { @@ -3055,10 +3101,10 @@ private function serialize_mysql_expression( WP_Parser_Node $node ): string { // calls, depending on the value of the "IGNORE_SPACE" SQL mode. if ( 0 === $i - || WP_MySQL_Lexer::OPEN_PAR_SYMBOL === $token->id - || WP_MySQL_Lexer::CLOSE_PAR_SYMBOL === $token->id - || WP_MySQL_Lexer::OPEN_PAR_SYMBOL === $last_token_id - || WP_MySQL_Lexer::CLOSE_PAR_SYMBOL === $last_token_id + || WP_MySQL_Tokens::OPEN_PAR_SYMBOL === $token->id + || WP_MySQL_Tokens::CLOSE_PAR_SYMBOL === $token->id + || WP_MySQL_Tokens::OPEN_PAR_SYMBOL === $last_token_id + || WP_MySQL_Tokens::CLOSE_PAR_SYMBOL === $last_token_id ) { $value .= $token->get_bytes(); } else { diff --git a/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-information-schema-reconstructor.php b/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-information-schema-reconstructor.php index 103bb8c12..a25852bbb 100644 --- a/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-information-schema-reconstructor.php +++ b/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-information-schema-reconstructor.php @@ -73,11 +73,12 @@ public function ensure_correct_information_schema(): void { $ast = $wp_tables[ $table ]; } else { // Other table (a WordPress plugin or unrelated to WordPress). - $sql = $this->generate_create_table_statement( $table ); - $ast = $this->driver->create_parser( $sql )->parse(); - if ( null === $ast ) { + $sql = $this->generate_create_table_statement( $table ); + $asts = $this->driver->parse_mysql_query( $sql ); + if ( null === $asts ) { throw new WP_SQLite_Driver_Exception( $this->driver, 'Failed to parse the MySQL query.' ); } + $ast = $asts[0]; } /* @@ -108,13 +109,13 @@ public function ensure_correct_information_schema(): void { * @param string $table_name The name of the table to drop. */ private function record_drop_table( string $table_name ): void { - $sql = sprintf( 'DROP TABLE %s', $this->connection->quote_identifier( $table_name ) ); // TODO: mysql quote - $ast = $this->driver->create_parser( $sql )->parse(); - if ( null === $ast ) { + $sql = sprintf( 'DROP TABLE %s', $this->connection->quote_identifier( $table_name ) ); // TODO: mysql quote + $asts = $this->driver->parse_mysql_query( $sql ); + if ( null === $asts ) { throw new WP_SQLite_Driver_Exception( $this->driver, 'Failed to parse the MySQL query.' ); } $this->schema_builder->record_drop_table( - $ast->get_first_descendant_node( 'dropStatement' ) + $asts[0]->get_first_descendant_node( 'drop_table_stmt' ) ); } @@ -228,17 +229,16 @@ private function get_wp_create_table_statements(): array { } // Parse the schema. - $parser = $this->driver->create_parser( $schema ); - $wp_tables = array(); - while ( $parser->next_query() ) { - $ast = $parser->get_query_ast(); - if ( null === $ast ) { - throw new WP_SQLite_Driver_Exception( $this->driver, 'Failed to parse the MySQL query.' ); - } + $asts = $this->driver->parse_mysql_query( $schema ); + if ( null === $asts ) { + throw new WP_SQLite_Driver_Exception( $this->driver, 'Failed to parse the MySQL query.' ); + } - $create_node = $ast->get_first_descendant_node( 'createStatement' ); - if ( $create_node && $create_node->has_child_node( 'createTable' ) ) { - $name_node = $create_node->get_first_descendant_node( 'tableName' ); + $wp_tables = array(); + foreach ( $asts as $ast ) { + $create_node = $ast->get_first_descendant_node( 'create_table_stmt' ); + if ( $create_node ) { + $name_node = $create_node->get_first_descendant_node( 'table_ident' ); $name = $this->unquote_mysql_identifier( substr( $schema, $name_node->get_start(), $name_node->get_length() ) ); diff --git a/packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Translation_Tests.php b/packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Translation_Tests.php index 119ec3ac0..7bdc0dcf3 100644 --- a/packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Translation_Tests.php +++ b/packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Translation_Tests.php @@ -3,13 +3,6 @@ use PHPUnit\Framework\TestCase; class WP_SQLite_Driver_Translation_Tests extends TestCase { - const GRAMMAR_PATH = __DIR__ . '/../src/mysql/mysql-grammar.php'; - - /** - * @var WP_Parser_Grammar - */ - private static $grammar; - /** * @var WP_SQLite_Driver */ @@ -20,10 +13,6 @@ class WP_SQLite_Driver_Translation_Tests extends TestCase { */ private $strict_suffix; - public static function setUpBeforeClass(): void { - self::$grammar = new WP_Parser_Grammar( include self::GRAMMAR_PATH ); - } - public function setUp(): void { $this->driver = new WP_SQLite_Driver( new WP_SQLite_Connection( array( 'path' => ':memory:' ) ), @@ -99,6 +88,17 @@ public function testSelect(): void { 'SELECT * FROM `t1` LEFT JOIN `t2` ON `t1`.`id` = `t2`.`t1_id` WHERE `t1`.`name` = \'abc\'', "SELECT * FROM t1 LEFT JOIN t2 ON t1.id = t2.t1_id WHERE t1.name = 'abc'" ); + + // A string SELECT alias must stay quoted (used by WP_Site_Health). + $this->assertQuery( + "SELECT `c` AS 'table' FROM `t`", + "SELECT c AS 'table' FROM t" + ); + + $this->assertQuery( + "SELECT `c` 'a ''quoted'' alias' FROM `t`", + "SELECT c 'a ''quoted'' alias' FROM t" + ); } public function testConvert(): void { diff --git a/packages/mysql-on-sqlite/tests/bootstrap.php b/packages/mysql-on-sqlite/tests/bootstrap.php index 3afa3b9dd..c0247d1a1 100644 --- a/packages/mysql-on-sqlite/tests/bootstrap.php +++ b/packages/mysql-on-sqlite/tests/bootstrap.php @@ -9,10 +9,6 @@ define( 'WP_SQLITE_UNSAFE_ENABLE_UNSUPPORTED_VERSIONS', true ); } -if ( '1' === getenv( 'WP_SQLITE_REQUIRE_NATIVE_PARSER_EXTENSION' ) ) { - require_once __DIR__ . '/tools/verify-native-parser-extension.php'; -} - // Configure the test environment. error_reporting( E_ALL ); define( 'FQDB', ':memory:' ); diff --git a/packages/mysql-on-sqlite/tests/mysql/WP_MySQL_Lexer_Tests.php b/packages/mysql-on-sqlite/tests/mysql/WP_MySQL_Lexer_Tests.php deleted file mode 100644 index 383b03f57..000000000 --- a/packages/mysql-on-sqlite/tests/mysql/WP_MySQL_Lexer_Tests.php +++ /dev/null @@ -1,409 +0,0 @@ -assertTrue( $lexer->next_token() ); - $this->assertSame( WP_MySQL_Lexer::SELECT_SYMBOL, $lexer->get_token()->id ); - - // id - $this->assertTrue( $lexer->next_token() ); - $this->assertSame( WP_MySQL_Lexer::IDENTIFIER, $lexer->get_token()->id ); - - // FROM - $this->assertTrue( $lexer->next_token() ); - $this->assertSame( WP_MySQL_Lexer::FROM_SYMBOL, $lexer->get_token()->id ); - - // users - $this->assertTrue( $lexer->next_token() ); - $this->assertSame( WP_MySQL_Lexer::IDENTIFIER, $lexer->get_token()->id ); - - // EOF - $this->assertTrue( $lexer->next_token() ); - $this->assertSame( WP_MySQL_Lexer::EOF, $lexer->get_token()->id ); - - // No more tokens. - $this->assertFalse( $lexer->next_token() ); - $this->assertNull( $lexer->get_token() ); - - // Again, no more tokens. - $this->assertFalse( $lexer->next_token() ); - $this->assertNull( $lexer->get_token() ); - } - - public function test_tokenize_invalid_input(): void { - $lexer = new WP_MySQL_Lexer( "SELECT x'ab01xyz'" ); - - // SELECT - $this->assertTrue( $lexer->next_token() ); - $this->assertSame( WP_MySQL_Lexer::SELECT_SYMBOL, $lexer->get_token()->id ); - - // Invalid input. - $this->assertFalse( $lexer->next_token() ); - $this->assertNull( $lexer->get_token() ); - - // No more tokens. - $this->assertFalse( $lexer->next_token() ); - $this->assertNull( $lexer->get_token() ); - - // Again, no more tokens. - $this->assertFalse( $lexer->next_token() ); - $this->assertNull( $lexer->get_token() ); - } - - /** - * Test that the whole U+0080 to U+FFFF UTF-8 range is valid in an identifier. - * The validity is checked against PCRE with the "u" (PCRE_UTF8) modifier set. - */ - public function test_identifier_utf8_range(): void { - for ( $i = 0x80; $i < 0xffff; $i += 1 ) { - $value = mb_chr( $i, 'UTF-8' ); - - $lexer = new WP_MySQL_Lexer( $value ); - $this->assertTrue( $lexer->next_token() ); - - $type = $lexer->get_token()->id; - $is_valid = preg_match( '/^[\x{0080}-\x{ffff}]$/u', $value ); - if ( $is_valid ) { - $this->assertSame( WP_MySQL_Lexer::IDENTIFIER, $type ); - } else { - $this->assertSame( WP_MySQL_Lexer::EOF, $type ); - } - } - } - - /** - * Test all valid and invalid 2-byte UTF-8 sequences in an identifier. - * The validity is checked against PCRE with the "u" (PCRE_UTF8) modifier set. - * - * Start both bytes from 128 and go up to 255 to include all invalid 2-byte - * UTF-8 sequences as well, and ensure that they won't match as identifiers. - */ - public function test_identifier_utf8_two_byte_sequences(): void { - for ( $byte_1 = 128; $byte_1 <= 255; $byte_1 += 1 ) { - for ( $byte_2 = 128; $byte_2 <= 255; $byte_2 += 1 ) { - $value = chr( $byte_1 ) . chr( $byte_2 ); - - $lexer = new WP_MySQL_Lexer( $value ); - $result = $lexer->next_token(); - $token = $lexer->get_token(); - - $is_valid = preg_match( '/^[\x{0080}-\x{ffff}]$/u', $value ); - if ( $is_valid ) { - $this->assertTrue( $result ); - $this->assertSame( WP_MySQL_Lexer::IDENTIFIER, $token->id ); - } else { - $this->assertFalse( $result ); - $this->assertNull( $token ); - } - } - } - } - - /** - * Test all valid and invalid 3-byte UTF-8 sequences in an identifier. - * The validity is checked against PCRE with the "u" (PCRE_UTF8) modifier set. - * - * Start the first byte from 0xE0 to mark the beginning of a 3-byte sequence. - * Start bytes 2 and 3 from 128 and go up to 255 to include all invalid 3-byte - * UTF-8 sequences as well, and ensure that they won't match as identifiers. - */ - public function test_identifier_utf8_three_byte_sequences(): void { - for ( $byte_1 = 0xE0; $byte_1 <= 0xFF; $byte_1 += 1 ) { - for ( $byte_2 = 128; $byte_2 <= 255; $byte_2 += 1 ) { - for ( $byte_3 = 128; $byte_3 <= 255; $byte_3 += 1 ) { - $value = chr( $byte_1 ) . chr( $byte_2 ) . chr( $byte_3 ); - - $lexer = new WP_MySQL_Lexer( $value ); - $result = $lexer->next_token(); - $token = $lexer->get_token(); - - $is_valid = preg_match( '/^[\x{0080}-\x{ffff}]$/u', $value ); - if ( $is_valid ) { - $this->assertTrue( $result ); - $this->assertSame( WP_MySQL_Lexer::IDENTIFIER, $token->id ); - } else { - $this->assertFalse( $result ); - $this->assertNull( $token ); - } - } - } - } - } - - /** - * @dataProvider data_integer_types - */ - public function test_integer_types( $input, $expected ): void { - $lexer = new WP_MySQL_Lexer( $input ); - $this->assertTrue( $lexer->next_token() ); - $this->assertSame( $expected, $lexer->get_token()->id ); - } - - public function data_integer_types(): array { - return array( - array( '0', WP_MySQL_Lexer::INT_NUMBER ), - array( '123', WP_MySQL_Lexer::INT_NUMBER ), - array( '2147483647', WP_MySQL_Lexer::INT_NUMBER ), - array( '00000000001', WP_MySQL_Lexer::INT_NUMBER ), - array( '00000000002147483647', WP_MySQL_Lexer::INT_NUMBER ), - - array( '2147483648', WP_MySQL_Lexer::LONG_NUMBER ), - array( '123456789123456789', WP_MySQL_Lexer::LONG_NUMBER ), - array( '9223372036854775807', WP_MySQL_Lexer::LONG_NUMBER ), - array( '00000000002147483648', WP_MySQL_Lexer::LONG_NUMBER ), - array( '00000000009223372036854775807', WP_MySQL_Lexer::LONG_NUMBER ), - - array( '9223372036854775808', WP_MySQL_Lexer::ULONGLONG_NUMBER ), - array( '12345678912345678912', WP_MySQL_Lexer::ULONGLONG_NUMBER ), - array( '18446744073709551615', WP_MySQL_Lexer::ULONGLONG_NUMBER ), - array( '00000000000000000009223372036854775808', WP_MySQL_Lexer::ULONGLONG_NUMBER ), - array( '000000000000000000018446744073709551615', WP_MySQL_Lexer::ULONGLONG_NUMBER ), - - array( '18446744073709551616', WP_MySQL_Lexer::DECIMAL_NUMBER ), - array( '23456789123456789123', WP_MySQL_Lexer::DECIMAL_NUMBER ), - array( '123456789123456789123456789', WP_MySQL_Lexer::DECIMAL_NUMBER ), - array( '0000000000000000000018446744073709551616', WP_MySQL_Lexer::DECIMAL_NUMBER ), - array( '00000000000000000000123456789123456789123456789', WP_MySQL_Lexer::DECIMAL_NUMBER ), - ); - } - - /** - * Numbers vs. identifiers: - * - * In MySQL, when an input matches both a number and an identifier, the number always wins. - * However, when the number is followed by a non-numeric identifier-like character, it is - * considered an identifier... unless it's a float number, which ignores subsequent input. - * - * @dataProvider data_identifier_or_number - */ - public function test_identifier_or_number( $input, $expected ): void { - $lexer = new WP_MySQL_Lexer( $input ); - $actual = array_map( - function ( $token ) { - return $token->id; - }, - $lexer->remaining_tokens() - ); - - // Compare token names to get more readable error messages. - $this->assertSame( - $this->get_token_names( $expected ), - $this->get_token_names( $actual ) - ); - } - - public function data_identifier_or_number(): array { - return array( - // integer - array( '123', array( WP_MySQL_Lexer::INT_NUMBER, WP_MySQL_Lexer::EOF ) ), - array( '123abc', array( WP_MySQL_Lexer::IDENTIFIER, WP_MySQL_Lexer::EOF ) ), // identifier - - // binary - array( '0b01', array( WP_MySQL_Lexer::BIN_NUMBER, WP_MySQL_Lexer::EOF ) ), - array( '0b01xyz', array( WP_MySQL_Lexer::IDENTIFIER, WP_MySQL_Lexer::EOF ) ), // identifier - array( '0b', array( WP_MySQL_Lexer::IDENTIFIER, WP_MySQL_Lexer::EOF ) ), // identifier - array( "b'01'", array( WP_MySQL_Lexer::BIN_NUMBER, WP_MySQL_Lexer::EOF ) ), - array( "b'01xyz'", array() ), // invalid input - array( "b''", array( WP_MySQL_Lexer::BIN_NUMBER, WP_MySQL_Lexer::EOF ) ), - array( "b'", array() ), // invalid input - array( "b'01", array() ), // invalid input - - // hex - array( '0xab01', array( WP_MySQL_Lexer::HEX_NUMBER, WP_MySQL_Lexer::EOF ) ), - array( '0xab01xyz', array( WP_MySQL_Lexer::IDENTIFIER, WP_MySQL_Lexer::EOF ) ), // identifier - array( '0x', array( WP_MySQL_Lexer::IDENTIFIER, WP_MySQL_Lexer::EOF ) ), // identifier - array( "x'ab01'", array( WP_MySQL_Lexer::HEX_NUMBER, WP_MySQL_Lexer::EOF ) ), - array( "x'ab01xyz'", array() ), // invalid input - array( "x''", array( WP_MySQL_Lexer::HEX_NUMBER, WP_MySQL_Lexer::EOF ) ), - array( "x'", array() ), // invalid input - array( "x'ab", array() ), // invalid input - - // decimal - array( '123.456', array( WP_MySQL_Lexer::DECIMAL_NUMBER, WP_MySQL_Lexer::EOF ) ), - array( '.123', array( WP_MySQL_Lexer::DECIMAL_NUMBER, WP_MySQL_Lexer::EOF ) ), - array( '123.', array( WP_MySQL_Lexer::DECIMAL_NUMBER, WP_MySQL_Lexer::EOF ) ), - array( '123.456abc', array( WP_MySQL_Lexer::DECIMAL_NUMBER, WP_MySQL_Lexer::IDENTIFIER, WP_MySQL_Lexer::EOF ) ), // not identifier - array( '.123abc', array( WP_MySQL_Lexer::DECIMAL_NUMBER, WP_MySQL_Lexer::IDENTIFIER, WP_MySQL_Lexer::EOF ) ), // not identifier - array( '123.abc', array( WP_MySQL_Lexer::DECIMAL_NUMBER, WP_MySQL_Lexer::IDENTIFIER, WP_MySQL_Lexer::EOF ) ), // not identifier - - // float - array( '1e10', array( WP_MySQL_Lexer::FLOAT_NUMBER, WP_MySQL_Lexer::EOF ) ), - array( '1e+10', array( WP_MySQL_Lexer::FLOAT_NUMBER, WP_MySQL_Lexer::EOF ) ), - array( '1e-10', array( WP_MySQL_Lexer::FLOAT_NUMBER, WP_MySQL_Lexer::EOF ) ), - array( '.1e10', array( WP_MySQL_Lexer::FLOAT_NUMBER, WP_MySQL_Lexer::EOF ) ), - array( '.1e+10', array( WP_MySQL_Lexer::FLOAT_NUMBER, WP_MySQL_Lexer::EOF ) ), - array( '.1e-10', array( WP_MySQL_Lexer::FLOAT_NUMBER, WP_MySQL_Lexer::EOF ) ), - array( '1.1e10', array( WP_MySQL_Lexer::FLOAT_NUMBER, WP_MySQL_Lexer::EOF ) ), - array( '1.1e-10', array( WP_MySQL_Lexer::FLOAT_NUMBER, WP_MySQL_Lexer::EOF ) ), - array( '1.1e+10', array( WP_MySQL_Lexer::FLOAT_NUMBER, WP_MySQL_Lexer::EOF ) ), - array( '1e10abc', array( WP_MySQL_Lexer::FLOAT_NUMBER, WP_MySQL_Lexer::IDENTIFIER, WP_MySQL_Lexer::EOF ) ), // not identifier (this differs from INT/BIN/HEX numbers) - array( '1e+10abc', array( WP_MySQL_Lexer::FLOAT_NUMBER, WP_MySQL_Lexer::IDENTIFIER, WP_MySQL_Lexer::EOF ) ), // not identifier - array( '1e-10abc', array( WP_MySQL_Lexer::FLOAT_NUMBER, WP_MySQL_Lexer::IDENTIFIER, WP_MySQL_Lexer::EOF ) ), // not identifier - array( '.1e10abc', array( WP_MySQL_Lexer::FLOAT_NUMBER, WP_MySQL_Lexer::IDENTIFIER, WP_MySQL_Lexer::EOF ) ), // not identifier - array( '.1e+10abc', array( WP_MySQL_Lexer::FLOAT_NUMBER, WP_MySQL_Lexer::IDENTIFIER, WP_MySQL_Lexer::EOF ) ), // not identifier - array( '.1e-10abc', array( WP_MySQL_Lexer::FLOAT_NUMBER, WP_MySQL_Lexer::IDENTIFIER, WP_MySQL_Lexer::EOF ) ), // not identifier - array( '1.1e10abc', array( WP_MySQL_Lexer::FLOAT_NUMBER, WP_MySQL_Lexer::IDENTIFIER, WP_MySQL_Lexer::EOF ) ), // not identifier - array( '1.1e+10abc', array( WP_MySQL_Lexer::FLOAT_NUMBER, WP_MySQL_Lexer::IDENTIFIER, WP_MySQL_Lexer::EOF ) ), // not identifier - array( '1.1e-10abc', array( WP_MySQL_Lexer::FLOAT_NUMBER, WP_MySQL_Lexer::IDENTIFIER, WP_MySQL_Lexer::EOF ) ), // not identifier - - // non-numbers - array( '.SELECT', array( WP_MySQL_Lexer::DOT_SYMBOL, WP_MySQL_Lexer::IDENTIFIER, WP_MySQL_Lexer::EOF ) ), // not decimal or float - array( '1+e10', array( WP_MySQL_Lexer::INT_NUMBER, WP_MySQL_Lexer::PLUS_OPERATOR, WP_MySQL_Lexer::IDENTIFIER, WP_MySQL_Lexer::EOF ) ), // not float - array( '1-e10', array( WP_MySQL_Lexer::INT_NUMBER, WP_MySQL_Lexer::MINUS_OPERATOR, WP_MySQL_Lexer::IDENTIFIER, WP_MySQL_Lexer::EOF ) ), // not float - ); - } - - /** - * Test that unclosed quoted strings with trailing backslashes do not - * cause out-of-bounds string access in read_quoted_text(). - * - * The backslash-counting loop walks backward from the position returned - * by strcspn(). When the closing quote is missing, strcspn() returns - * the remaining string length. If the last byte is a backslash, the - * loop treats the absent quote as escaped and advances past the end - * of the string. On the next iteration, the loop accesses an invalid - * string offset, triggering "Uninitialized string offset" warnings. - * - * @dataProvider data_unclosed_strings_with_backslashes - */ - public function test_unclosed_string_with_trailing_backslash( string $sql ): void { - set_error_handler( - function ( $severity, $message, $file, $line ) { - throw new \ErrorException( $message, 0, $severity, $file, $line ); - }, - E_WARNING | E_NOTICE - ); - - try { - $lexer = new WP_MySQL_Lexer( $sql ); - while ( $lexer->next_token() ) { - // Consume all tokens. - } - } finally { - restore_error_handler(); - } - - // If we reach here without an ErrorException, no OOB access occurred. - $this->assertNull( $lexer->get_token() ); - } - - public function data_unclosed_strings_with_backslashes(): array { - return array( - 'single-quoted trailing backslash' => array( "SELECT '\\" ), - 'double-quoted trailing backslash' => array( 'SELECT "\\' ), - 'even trailing backslashes' => array( "SELECT '\\\\" ), - 'odd trailing backslashes' => array( "SELECT '\\\\\\" ), - 'backslash-only single-quoted' => array( "'\\" ), - 'backslash-only double-quoted' => array( '"\\' ), - ); - } - - /** - * Regression: valid strings with escapes must still tokenize correctly. - * - * @dataProvider data_valid_escaped_strings - */ - public function test_valid_escaped_string( string $sql, int $expected_token_id ): void { - $lexer = new WP_MySQL_Lexer( $sql ); - $this->assertTrue( $lexer->next_token() ); - $this->assertSame( $expected_token_id, $lexer->get_token()->id ); - } - - public function data_valid_escaped_strings(): array { - return array( - 'escaped single quote' => array( "'it\\'s'", WP_MySQL_Lexer::SINGLE_QUOTED_TEXT ), - 'trailing escaped backslash' => array( "'path\\\\'", WP_MySQL_Lexer::SINGLE_QUOTED_TEXT ), - 'doubled single quote' => array( "'it''s'", WP_MySQL_Lexer::SINGLE_QUOTED_TEXT ), - 'empty single-quoted string' => array( "''", WP_MySQL_Lexer::SINGLE_QUOTED_TEXT ), - 'escaped double quote' => array( '"col\\"name"', WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT ), - 'backtick identifier' => array( '`my_column`', WP_MySQL_Lexer::BACK_TICK_QUOTED_ID ), - ); - } - - /** - * Test that a chunk boundary splitting a quoted string with a trailing - * backslash does not cause an out-of-bounds string access. - * - * This simulates streaming SQL processing where a buffer boundary falls - * inside a string literal right after a backslash escape character. - */ - public function test_chunk_boundary_inside_escaped_string(): void { - set_error_handler( - function ( $severity, $message, $file, $line ) { - throw new \ErrorException( $message, 0, $severity, $file, $line ); - }, - E_WARNING | E_NOTICE - ); - - try { - // Build a SQL string where a backslash falls at the chunk boundary. - // The string content before the boundary is padded to place the - // backslash at exactly position $chunk_size - 1. - $chunk_size = 8192; - - // "SELECT '" = 8 bytes, so we need chunk_size - 8 - 1 bytes of - // padding before the trailing backslash to place '\' at the last - // byte of the chunk. - $padding = str_repeat( 'A', $chunk_size - 8 - 1 ); - $sql = "SELECT '" . $padding . '\\'; - - // The chunk is exactly $chunk_size bytes. The last byte is '\'. - // The lexer should handle this as an unclosed string without OOB. - $this->assertSame( $chunk_size, strlen( $sql ) ); - - $lexer = new WP_MySQL_Lexer( $sql ); - while ( $lexer->next_token() ) { - // Consume all tokens. - } - } finally { - restore_error_handler(); - } - - $this->assertNull( $lexer->get_token() ); - } - - /** - * A charset-introducer-like name used as a qualified member (after a dot) - * must lex as an identifier. A real charset introducer only appears before - * a string literal, never as the member of a qualified reference. - * - * @dataProvider data_underscore_charset_after_dot - */ - public function test_underscore_charset_name_after_dot_is_identifier( string $sql, int $token_index, int $expected_id ): void { - $tokens = ( new WP_MySQL_Lexer( $sql ) )->remaining_tokens(); - $this->assertSame( - WP_MySQL_Lexer::get_token_name( $expected_id ), - $tokens[ $token_index ]->get_name(), - $sql - ); - } - - /** - * @return array - */ - public function data_underscore_charset_after_dot(): array { - return array( - // `t . _utf8` - the member name must be an identifier, not a charset. - 'charset name after dot is identifier' => array( 't._utf8', 2, WP_MySQL_Lexer::IDENTIFIER ), - 'other charset name after dot' => array( 'a._binary', 2, WP_MySQL_Lexer::IDENTIFIER ), - // A genuine charset introducer (before a string) stays a charset. - 'charset introducer before string' => array( "_utf8'x'", 0, WP_MySQL_Lexer::UNDERSCORE_CHARSET ), - // A non-charset underscore name after a dot stays an identifier. - 'non-charset underscore name after dot' => array( 't._foo', 2, WP_MySQL_Lexer::IDENTIFIER ), - ); - } - - private function get_token_names( array $token_types ): array { - return array_map( - function ( $token_type ) { - return WP_MySQL_Lexer::get_token_name( $token_type ); - }, - $token_types - ); - } -} diff --git a/packages/mysql-on-sqlite/tests/mysql/WP_MySQL_Server_Suite_Lexer_Tests.php b/packages/mysql-on-sqlite/tests/mysql/WP_MySQL_Server_Suite_Lexer_Tests.php deleted file mode 100644 index 63012a3d5..000000000 --- a/packages/mysql-on-sqlite/tests/mysql/WP_MySQL_Server_Suite_Lexer_Tests.php +++ /dev/null @@ -1,34 +0,0 @@ -fail( "Failed to open file '$path'." ); - } - - try { - while ( ( $record = fgetcsv( $handle, null, ',', '"', '\\' ) ) !== false ) { - $query = $record[0]; - $lexer = new WP_MySQL_Lexer( $query ); - $tokens = $lexer->remaining_tokens(); - $this->assertNotEmpty( $tokens, "Failed to tokenize query: $query" ); - } - } finally { - fclose( $handle ); - } - } -} diff --git a/packages/mysql-on-sqlite/tests/mysql/WP_MySQL_Server_Suite_Parser_Tests.php b/packages/mysql-on-sqlite/tests/mysql/WP_MySQL_Server_Suite_Parser_Tests.php deleted file mode 100644 index 027698849..000000000 --- a/packages/mysql-on-sqlite/tests/mysql/WP_MySQL_Server_Suite_Parser_Tests.php +++ /dev/null @@ -1,102 +0,0 @@ - true, - 'select 1ea10.1a20,1e+ 1e+10 from 1ea10' => true, - "聠聡聢聣聤聬聭聮聯聰聲聽隆垄拢陇楼卤潞禄录陆戮 聶職聳聴\n0聲5\n1聲5\n2聲5\n3聲5\n4\n\nSET NAMES gb18030" => true, - "alter user mysqltest_7@ identified by 'systpass'" => true, - "SELECT 'a%' LIKE 'a!%' ESCAPE '!', 'a%' LIKE 'a!' || '%' ESCAPE '!'" => true, - "SELECT 'a%' NOT LIKE 'a!%' ESCAPE '!', 'a%' NOT LIKE 'a!' || '%' ESCAPE '!'" => true, - "SELECT 'a%' LIKE 'a!%' ESCAPE '$', 'a%' LIKE 'a!' || '%' ESCAPE '$'" => true, - "SELECT 'a%' NOT LIKE 'a!%' ESCAPE '$', 'a%' NOT LIKE 'a!' || '%' ESCAPE '$'" => true, - 'ALTER SCHEMA s1 READ ONLY DEFAULT' => true, - ); - - /** - * @var WP_Parser_Grammar - */ - private static $grammar; - - public static function setUpBeforeClass(): void { - self::$grammar = new WP_Parser_Grammar( include self::GRAMMAR_PATH ); - } - - /** - * Parse all queries from the MySQL server test suite and make sure - * it produces some AST and doesn't throw any exceptions. - * - * The queries need to be batched and parsed in a loop, since the data set - * is too large for PHPUnit to run a test per query, causing memory errors. - * - * @dataProvider data_parse_mysql_test_suite - */ - public function test_parse_mysql_test_suite( array $batch ): void { - foreach ( $batch as $record ) { - $query = $record[0]; - - $lexer = new WP_MySQL_Lexer( $query ); - $tokens = $lexer->remaining_tokens(); - $this->assertNotEmpty( $tokens, "Failed to tokenize query: $query" ); - - $parser = new WP_MySQL_Parser( self::$grammar, $tokens ); - $ast = $parser->parse(); - - if ( self::KNOWN_FAILURES[ $query ] ?? false ) { - if ( null !== $ast ) { - $this->assertNull( $ast, "Parsing succeeded, but was expected to fail for query: $query" ); - } - continue; - } - - $this->assertNotNull( $ast, "Failed to parse query: $query" ); - } - } - - public function data_parse_mysql_test_suite(): Generator { - $path = __DIR__ . '/data/mysql-server-tests-queries.csv'; - $handle = @fopen( $path, 'r' ); - if ( false === $handle ) { - $this->fail( "Failed to open file '$path'." ); - } - - try { - $data = array(); - $batch = 1; - while ( ( $record = fgetcsv( $handle, null, ',', '"', '\\' ) ) !== false ) { - $data[] = $record; - if ( count( $data ) === 1000 ) { - yield "batch-$batch" => array( $data ); - $batch += 1; - $data = array(); - } - } - if ( count( $data ) > 0 ) { - yield "batch-$batch" => array( $data ); - } - } finally { - fclose( $handle ); - } - } - - /** - * By default, PHPUnit will dump the whole data set in the error message - * when a data provider is used. However, here we are working with a lot - * of data, and therefore we suppress the data dump for message clarity. - */ - public function getDataSetAsString( bool $include_data = true ): string { - return parent::getDataSetAsString( false ); - } -} diff --git a/packages/mysql-on-sqlite/tests/mysql/native/WP_MySQL_Native_Parser_Node_Cycle_Tests.php b/packages/mysql-on-sqlite/tests/mysql/native/WP_MySQL_Native_Parser_Node_Cycle_Tests.php deleted file mode 100644 index 576721620..000000000 --- a/packages/mysql-on-sqlite/tests/mysql/native/WP_MySQL_Native_Parser_Node_Cycle_Tests.php +++ /dev/null @@ -1,267 +0,0 @@ -markTestSkipped( 'Native MySQL parser extension is not loaded.' ); - } - // Force a clean slate before each test — ASTs from earlier tests - // must not pollute the memory measurements below. - gc_collect_cycles(); - } - - private function parse( string $sql ): WP_Parser_Node { - static $grammar = null; - if ( null === $grammar ) { - $grammar = new WP_Parser_Grammar( include __DIR__ . '/../../../src/mysql/mysql-grammar.php' ); - } - $lexer = new WP_MySQL_Lexer( $sql ); - $tokens = $lexer instanceof WP_MySQL_Native_Lexer - ? $lexer->native_token_stream() - : $lexer->remaining_tokens(); - $parser = new WP_MySQL_Parser( $grammar, $tokens ); - $tree = $parser->parse(); - $this->assertNotNull( $tree, 'Failed to parse SQL: ' . $sql ); - return $tree; - } - - /** - * Hostile loop: parse and walk many ASTs in a tight loop, only - * `gc_collect_cycles()` between iterations. Memory must plateau. - * - * If wrapper registry entries or cache pointers are not released, peak - * memory grows linearly with iteration count. With cleanup in place, the - * working set stays bounded. - */ - public function test_repeated_parse_walk_drop_does_not_leak(): void { - $sql = 'SELECT a, b, c FROM t WHERE a + b * c IN (1, 2, 3) AND d = 4'; - - // Warm-up: do enough work that allocator overhead is amortized - // before we sample the floor. - for ( $i = 0; $i < 20; $i++ ) { - $ast = $this->parse( $sql ); - $ast->get_descendants(); - $ast = null; - gc_collect_cycles(); - } - $baseline = memory_get_usage(); - - // Now run substantially more iterations and assert the working - // set stays within a small multiple of the warm-up floor. - for ( $i = 0; $i < 500; $i++ ) { - $ast = $this->parse( $sql ); - $ast->get_descendants(); - $ast = null; - gc_collect_cycles(); - } - $after = memory_get_usage(); - - // 4 MB headroom — generous, but a leaking cache adds tens of MB - // across 500 iterations on this query. - $delta = $after - $baseline; - $this->assertLessThan( - 4 * 1024 * 1024, - $delta, - sprintf( - 'Memory grew %.1f MB across 500 parse-walk-drop cycles; the per-AST cache is not being collected.', - $delta / 1024 / 1024 - ) - ); - } - - /** - * After dropping the AST and triggering GC, the entire wrapper - * graph must be reclaimable. We hand out one descendant, drop the - * root, then drop the descendant — the next gc cycle must reclaim - * the rest of the cached wrappers. - */ - public function test_drop_then_gc_reclaims_cached_wrappers(): void { - $sql = 'SELECT a, b, c FROM t WHERE a + b * c IN (1, 2, 3) AND d = 4'; - - // Establish a memory floor with no AST live. - gc_collect_cycles(); - $floor = memory_get_usage(); - - $ast = $this->parse( $sql ); - $descendant = $ast->get_first_descendant_node(); - $this->assertNotNull( $descendant ); - $ast = null; - $descendant = null; - gc_collect_cycles(); - - $after = memory_get_usage(); - $delta = $after - $floor; - // Generous bound — but tens of MB of leaked wrappers would blow it. - $this->assertLessThan( - 1 * 1024 * 1024, - $delta, - sprintf( - 'After dropping the AST and the descendant and running gc, %.1f MB of cached wrappers remain.', - $delta / 1024 / 1024 - ) - ); - } - - /** - * Holding a child wrapper *outlives* the variable holding the root. - * The child's registry entry must keep the AST alive (no UAF when the - * bridge is called on the orphaned child). Once the child is also dropped, - * the registry entry must be released. - */ - public function test_orphaned_child_keeps_ast_alive_then_collects(): void { - $sql = 'SELECT a, b, c FROM t WHERE a + b * c IN (1, 2, 3)'; - $child = ( function () use ( $sql ) { - $ast = $this->parse( $sql ); - return $ast->get_first_descendant_node(); - } )(); - - // Root variable is gone; only the child reference remains, but the - // registry entry still pins the AST. The child must still be - // functional — accessing it must not crash. - $this->assertNotNull( $child ); - $this->assertIsString( $child->rule_name ); - // The child's own children should also resolve without UAF. - $grand = $child->get_first_child(); - $this->assertNotNull( $grand ); - - // Now drop the child too; the AST + cache should be reclaimable. - $child = null; - $grand = null; - gc_collect_cycles(); - // If the registry entry was released, this assertion always passes; - // the real signal is the absence of a segfault during teardown. - $this->addToAssertionCount( 1 ); - } - - /** - * Mutating a cached wrapper through `append_child` before dropping - * the AST must not block collection. The mutated wrapper's - * `$children` array now contains a non-cached node; that must not keep - * stale registry/cache entries alive. - */ - public function test_mutation_before_drop_does_not_block_collection(): void { - $sql = 'SELECT 1 + 2'; - - gc_collect_cycles(); - $floor = memory_get_usage(); - - for ( $i = 0; $i < 200; $i++ ) { - $ast = $this->parse( $sql ); - $child = $ast->get_first_child_node(); - $injected = new WP_Parser_Node( 0, 'synthetic-' . $i ); - $ast->append_child( $injected ); - // Touch the cache after mutation to keep wrappers live. - $ast->get_descendants(); - $ast = null; - $child = null; - $injected = null; - gc_collect_cycles(); - } - $after = memory_get_usage(); - $delta = $after - $floor; - $this->assertLessThan( - 4 * 1024 * 1024, - $delta, - sprintf( - 'Memory grew %.1f MB across 200 mutate-then-drop cycles.', - $delta / 1024 / 1024 - ) - ); - } - - /** - * Two ASTs alive simultaneously, then dropped in interleaved order. - * Dropping AST A must not affect AST B's cached wrappers; both must - * eventually collect once unreferenced. - */ - public function test_overlapping_asts_do_not_corrupt_each_other(): void { - $ast_a = $this->parse( 'SELECT a FROM ta WHERE a > 1' ); - $ast_b = $this->parse( 'SELECT b FROM tb WHERE b < 9' ); - - $child_a = $ast_a->get_first_descendant_node(); - $child_b = $ast_b->get_first_descendant_node(); - - // Drop A first and run gc; B must remain fully functional. - $ast_a = null; - $child_a = null; - gc_collect_cycles(); - - $this->assertNotNull( $child_b ); - $walk = $ast_b->get_descendants(); - $this->assertNotEmpty( $walk ); - - // Drop B too; walk one of its still-held descendants — the cache - // is still alive because $child_b pins it. - $ast_b = null; - $this->assertIsString( $child_b->rule_name ); - - $child_b = null; - $walk = null; - gc_collect_cycles(); - $this->addToAssertionCount( 1 ); - } - - /** - * Re-walk + drop + collect across many iterations. This is the - * "translator pass on each query" shape of real workloads. The wrapper - * registry and cache must not create a memory cliff under repeated walks. - */ - public function test_rewalk_loop_stays_bounded(): void { - $sql = 'SELECT a, b, c, d, e FROM t WHERE (a + b) * (c - d) > e AND f IN (1,2,3,4,5)'; - - gc_collect_cycles(); - // Warm-up. - for ( $i = 0; $i < 10; $i++ ) { - $ast = $this->parse( $sql ); - for ( $r = 0; $r < 10; $r++ ) { - $ast->get_descendants(); - } - $ast = null; - gc_collect_cycles(); - } - $floor = memory_get_usage(); - - for ( $i = 0; $i < 200; $i++ ) { - $ast = $this->parse( $sql ); - for ( $r = 0; $r < 10; $r++ ) { - $ast->get_descendants(); - } - $ast = null; - gc_collect_cycles(); - } - $after = memory_get_usage(); - $delta = $after - $floor; - $this->assertLessThan( - 4 * 1024 * 1024, - $delta, - sprintf( - 'Rewalk loop grew memory by %.1f MB; cache likely uncollectable.', - $delta / 1024 / 1024 - ) - ); - } -} diff --git a/packages/mysql-on-sqlite/tests/mysql/native/WP_MySQL_Native_Parser_Node_Identity_Tests.php b/packages/mysql-on-sqlite/tests/mysql/native/WP_MySQL_Native_Parser_Node_Identity_Tests.php deleted file mode 100644 index 066fd38d4..000000000 --- a/packages/mysql-on-sqlite/tests/mysql/native/WP_MySQL_Native_Parser_Node_Identity_Tests.php +++ /dev/null @@ -1,142 +0,0 @@ -markTestSkipped( 'Native MySQL parser extension is not loaded.' ); - } - } - - private function parse( string $sql ): WP_Parser_Node { - static $grammar = null; - if ( null === $grammar ) { - $grammar = new WP_Parser_Grammar( include __DIR__ . '/../../../src/mysql/mysql-grammar.php' ); - } - $lexer = new WP_MySQL_Lexer( $sql ); - $tokens = $lexer instanceof WP_MySQL_Native_Lexer - ? $lexer->native_token_stream() - : $lexer->remaining_tokens(); - $parser = new WP_MySQL_Parser( $grammar, $tokens ); - $tree = $parser->parse(); - $this->assertNotNull( $tree, 'Failed to parse SQL: ' . $sql ); - return $tree; - } - - public function test_get_first_child_node_returns_same_instance(): void { - $tree = $this->parse( 'SELECT 1 + 2' ); - - $first = $tree->get_first_child_node(); - $second = $tree->get_first_child_node(); - - $this->assertNotNull( $first ); - $this->assertSame( $first, $second ); - } - - public function test_native_wrapper_does_not_store_native_ast_handle(): void { - $tree = $this->parse( 'SELECT 1 + 2' ); - - $reflection = new ReflectionObject( $tree ); - - $this->assertFalse( $reflection->hasProperty( 'native_ast' ) ); - $this->assertFalse( $reflection->hasProperty( 'native_node_index' ) ); - } - - public function test_get_children_returns_same_instances_across_calls(): void { - $tree = $this->parse( 'SELECT 1, 2, 3' ); - - $first_pass = $tree->get_children(); - $second_pass = $tree->get_children(); - - $this->assertSameSize( $first_pass, $second_pass ); - foreach ( $first_pass as $i => $child ) { - if ( $child instanceof WP_Parser_Node ) { - $this->assertSame( $child, $second_pass[ $i ] ); - } - } - } - - public function test_descendant_lookup_shares_identity_with_child_lookup(): void { - $tree = $this->parse( 'SELECT 1 + 2' ); - - $descendant = $tree->get_first_descendant_node(); - $this->assertNotNull( $descendant ); - - // Walk down to the same node via direct children. We don't know the - // exact depth, so we descend until we hit the descendant we found. - $cursor = $tree; - while ( null !== $cursor && $cursor !== $descendant ) { - $next = $cursor->get_first_child_node(); - if ( $next === $cursor ) { - break; - } - $cursor = $next; - } - - $this->assertSame( $descendant, $cursor, 'Descendant and child lookups must return the same wrapper instance.' ); - } - - public function test_mutation_on_child_survives_re_read(): void { - $tree = $this->parse( 'SELECT 1 + 2' ); - - $child = $tree->get_first_child_node(); - $this->assertNotNull( $child ); - - // Mutate via the public WP_Parser_Node API. This catches regressions - // where accessors hand back fresh wrappers and lose state written - // through a previously returned child. - $child->rule_name = 'mutated-rule'; - - $same_child = $tree->get_first_child_node(); - $this->assertSame( $child, $same_child ); - $this->assertSame( 'mutated-rule', $same_child->rule_name ); - } - - public function test_materialized_child_survives_re_read_from_native_parent(): void { - $tree = $this->parse( 'SELECT 1 + 2' ); - - $child = $tree->get_first_child_node(); - $this->assertNotNull( $child ); - - $synthetic = new WP_Parser_Node( 0, 'synthetic' ); - $child->append_child( $synthetic ); - - $same_child = $tree->get_first_child_node(); - $this->assertSame( $child, $same_child ); - $this->assertTrue( - in_array( $synthetic, $same_child->get_children(), true ), - 'Materialized live child wrappers must stay discoverable through the parent native cache.' - ); - } - - public function test_mutation_survives_parent_materialization(): void { - $tree = $this->parse( 'SELECT 1 + 2' ); - - $child = $tree->get_first_child_node(); - $this->assertNotNull( $child ); - $child->rule_name = 'before-materialize'; - - // Force the parent to materialize its native children by appending - // a sibling. After this, the parent walks $this->children directly. - $sibling = new WP_Parser_Node( 0, 'synthetic' ); - $tree->append_child( $sibling ); - - $children = $tree->get_children(); - $this->assertContains( $child, $children, 'Materialized children must include the previously-mutated wrapper.' ); - $this->assertSame( 'before-materialize', $child->rule_name ); - } -} diff --git a/packages/mysql-on-sqlite/tests/mysql/native/WP_MySQL_Parser_Instanceof_Tests.php b/packages/mysql-on-sqlite/tests/mysql/native/WP_MySQL_Parser_Instanceof_Tests.php deleted file mode 100644 index d308d5585..000000000 --- a/packages/mysql-on-sqlite/tests/mysql/native/WP_MySQL_Parser_Instanceof_Tests.php +++ /dev/null @@ -1,39 +0,0 @@ -native_token_stream() - : $lexer->remaining_tokens(); - $parser = new WP_MySQL_Parser( $grammar, $tokens ); - - $this->assertInstanceOf( WP_Parser::class, $parser ); - $this->assertInstanceOf( WP_MySQL_Parser::class, $parser ); - } - - public function test_parser_returns_an_ast(): void { - $grammar = new WP_Parser_Grammar( include __DIR__ . '/../../../src/mysql/mysql-grammar.php' ); - $lexer = new WP_MySQL_Lexer( 'SELECT 1 + 2' ); - $tokens = $lexer instanceof WP_MySQL_Native_Lexer - ? $lexer->native_token_stream() - : $lexer->remaining_tokens(); - $parser = new WP_MySQL_Parser( $grammar, $tokens ); - - $ast = $parser->parse(); - $this->assertNotNull( $ast ); - $this->assertInstanceOf( WP_Parser_Node::class, $ast ); - } -} diff --git a/packages/mysql-on-sqlite/tests/tools/dump-ast.php b/packages/mysql-on-sqlite/tests/tools/dump-ast.php index 3e7716ced..56f8f5ac1 100644 --- a/packages/mysql-on-sqlite/tests/tools/dump-ast.php +++ b/packages/mysql-on-sqlite/tests/tools/dump-ast.php @@ -2,7 +2,9 @@ /** * This script runs the MySQL lexer & parser on a single query and dumps its AST. - * It is useful for testing and testing the lexer and parser functionality. + * It is useful for testing and debugging the lexer and parser functionality. + * + * Usage: php dump-ast.php "SELECT 1" */ // throw exception if anything fails @@ -12,28 +14,36 @@ function ( $severity, $message, $file, $line ) { } ); -require_once __DIR__ . '/../../src/parser/class-wp-parser.php'; -require_once __DIR__ . '/../../src/parser/class-wp-parser-grammar.php'; -require_once __DIR__ . '/../../src/parser/class-wp-parser-node.php'; -require_once __DIR__ . '/../../src/parser/class-wp-parser-token.php'; -require_once __DIR__ . '/../../src/mysql/class-wp-mysql-lexer.php'; -require_once __DIR__ . '/../../src/mysql/class-wp-mysql-parser.php'; -require_once __DIR__ . '/../../src/mysql/class-wp-mysql-token.php'; +require_once __DIR__ . '/../../src/load.php'; -$grammar_data = include __DIR__ . '/../../src/mysql/mysql-grammar.php'; -$grammar = new WP_Parser_Grammar( $grammar_data ); - -// Edit the query below to test different inputs: -$lexer = new WP_MySQL_Lexer( 'SELECT 1' ); +$query = $argv[1] ?? 'SELECT 1'; +$lexer = new WP_MySQL_Lexer( $query ); $tokens = $lexer->remaining_tokens(); echo "Tokens:\n"; foreach ( $tokens as $token ) { echo $token, "\n"; } -$parser = new WP_MySQL_Parser( $grammar, $tokens ); -$ast = $parser->parse(); + +$parser = new WP_MySQL_Parser( require WP_MySQL_Parser::PARSE_TABLE_PATH ); +$ast = $parser->parse( $tokens ); echo "\n\n"; echo "AST:\n"; -var_dump( $ast ); +if ( null === $ast ) { + echo "PARSE ERROR\n"; + exit( 1 ); +} + +function dump_node( $node, int $depth = 0 ): void { + $pad = str_repeat( ' ', $depth ); + if ( $node instanceof WP_Parser_Node ) { + echo $pad, $node->rule_name, "\n"; + foreach ( $node->get_children() as $child ) { + dump_node( $child, $depth + 1 ); + } + } else { + echo $pad, $node->get_name(), '<', $node->get_value(), ">\n"; + } +} +dump_node( $ast ); diff --git a/packages/mysql-on-sqlite/tests/tools/run-lexer-benchmark.php b/packages/mysql-on-sqlite/tests/tools/run-lexer-benchmark.php index 23ecd1b43..01d6f8040 100644 --- a/packages/mysql-on-sqlite/tests/tools/run-lexer-benchmark.php +++ b/packages/mysql-on-sqlite/tests/tools/run-lexer-benchmark.php @@ -4,10 +4,6 @@ * Benchmark the MySQL lexer over the checked-in MySQL server test corpus and * report its tokenization throughput (queries lexed per second). * - * Mirrors run-parser-benchmark.php: it loads through src/load.php, so when the - * native wp_mysql_parser extension is loaded the benchmark runs the native - * lexer through the same public WP_MySQL_Lexer wrapper that the driver uses. - * * JIT / opcache are start-up ini settings, so this script does not toggle them; * it reports the active configuration so every run is self-describing. Run it * twice to compare without and with the tracing JIT (the lexer behaves very @@ -56,8 +52,6 @@ function ( $severity, $message, $file, $line ) { } } -// Use the integration loader so an already-loaded native extension selects -// the same public lexer class that runtime code uses. require_once __DIR__ . '/../../src/load.php'; // Load the bounded checked-in corpus before timing so file IO is excluded @@ -76,17 +70,12 @@ function ( $severity, $message, $file, $line ) { } $query_count = count( $queries ); -// Lex the whole corpus once. Calling native_token_stream() vs remaining_tokens() -// mirrors how the driver consumes the chosen lexer. -$native = class_exists( 'WP_MySQL_Native_Lexer', false ); -$lex_corpus = function () use ( $queries, $native ) { +// Lex the whole corpus once. +$lex_corpus = function () use ( $queries ) { foreach ( $queries as $query ) { $lexer = new WP_MySQL_Lexer( $query ); - $tokens = $native && $lexer instanceof WP_MySQL_Native_Lexer - ? $lexer->native_token_stream() - : $lexer->remaining_tokens(); - $count = is_array( $tokens ) ? count( $tokens ) : $tokens->count(); - if ( 0 === $count ) { + $tokens = $lexer->remaining_tokens(); + if ( 0 === count( $tokens ) ) { throw new Exception( 'Failed to tokenize query: ' . $query ); } } @@ -120,26 +109,25 @@ function ( $severity, $message, $file, $line ) { $opcache_status = function_exists( 'opcache_get_status' ) ? opcache_get_status( false ) : false; $opcache_on = is_array( $opcache_status ); $jit_on = $opcache_on && ! empty( $opcache_status['jit']['on'] ); -$implementation = ( extension_loaded( 'wp_mysql_parser' ) && $native ) ? 'native-extension' : 'php'; +$implementation = 'php'; if ( $json ) { echo json_encode( array( - 'benchmark' => 'mysql-lexer', - 'implementation' => $implementation, - 'extension_loaded' => extension_loaded( 'wp_mysql_parser' ), - 'opcache' => $opcache_on, - 'jit' => $jit_on, - 'queries' => $query_count, - 'warmup' => $warmup, - 'iterations' => $iterations, - 'qps' => $best, // Headline (best pass); kept as "qps" for compatibility. - 'qps_best' => $best, - 'qps_median' => $median, - 'qps_mean' => $mean, - 'qps_worst' => $worst, - 'spread' => $spread, - 'php_version' => PHP_VERSION, + 'benchmark' => 'mysql-lexer', + 'implementation' => $implementation, + 'opcache' => $opcache_on, + 'jit' => $jit_on, + 'queries' => $query_count, + 'warmup' => $warmup, + 'iterations' => $iterations, + 'qps' => $best, // Headline (best pass); kept as "qps" for compatibility. + 'qps_best' => $best, + 'qps_median' => $median, + 'qps_mean' => $mean, + 'qps_worst' => $worst, + 'spread' => $spread, + 'php_version' => PHP_VERSION, ), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ), "\n"; diff --git a/packages/mysql-on-sqlite/tests/tools/run-native-extension-benchmark.php b/packages/mysql-on-sqlite/tests/tools/run-native-extension-benchmark.php deleted file mode 100644 index e4e304a9a..000000000 --- a/packages/mysql-on-sqlite/tests/tools/run-native-extension-benchmark.php +++ /dev/null @@ -1,113 +0,0 @@ -= $limit ) { - break; - } -} - -function benchmark_php_mysql_lexer( $queries ) { - $start = microtime( true ); - foreach ( $queries as $query ) { - $lexer = new WP_MySQL_Lexer( $query ); - $tokens = $lexer->remaining_tokens(); - if ( count( $tokens ) === 0 ) { - throw new Exception( 'Failed to tokenize query: ' . $query ); - } - } - $duration = microtime( true ) - $start; - return array( - 'available' => true, - 'duration' => $duration, - 'qps' => count( $queries ) / $duration, - ); -} - -function benchmark_native_mysql_lexer( $queries ) { - if ( ! class_exists( 'WP_MySQL_Native_Lexer', false ) ) { - return array( - 'available' => false, - 'reason' => 'The wp_mysql_parser extension is not loaded.', - ); - } - - $start = microtime( true ); - foreach ( $queries as $query ) { - $lexer = new WP_MySQL_Native_Lexer( $query ); - $tokens = $lexer->native_token_stream(); - if ( 0 === $tokens->count() ) { - throw new Exception( 'Failed to tokenize query with native lexer: ' . $query ); - } - } - $duration = microtime( true ) - $start; - return array( - 'available' => true, - 'duration' => $duration, - 'qps' => count( $queries ) / $duration, - ); -} - -$php = benchmark_php_mysql_lexer( $queries ); -$native = benchmark_native_mysql_lexer( $queries ); - -$result = array( - 'benchmark' => 'mysql-lexer-native-extension', - 'queries' => count( $queries ), - 'php_version' => PHP_VERSION, - 'extension_loaded' => extension_loaded( 'wp_mysql_parser' ), - 'pure_php' => $php, - 'native_extension' => $native, -); - -if ( ! empty( $native['available'] ) ) { - $result['speedup'] = $native['qps'] / $php['qps']; -} - -if ( $json ) { - echo json_encode( $result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ), "\n"; - exit; -} - -printf( "Benchmarked %d MySQL queries on PHP %s.\n", $result['queries'], PHP_VERSION ); -printf( "Pure PHP lexer: %.5fs @ %d QPS\n", $php['duration'], $php['qps'] ); -if ( empty( $native['available'] ) ) { - printf( "Native extension: unavailable (%s)\n", $native['reason'] ); - exit; -} -printf( "Native extension: %.5fs @ %d QPS\n", $native['duration'], $native['qps'] ); -printf( "Speedup: %.2fx\n", $result['speedup'] ); diff --git a/packages/mysql-on-sqlite/tests/tools/run-parser-benchmark.php b/packages/mysql-on-sqlite/tests/tools/run-parser-benchmark.php index 6b77ea899..b2dbaf87e 100644 --- a/packages/mysql-on-sqlite/tests/tools/run-parser-benchmark.php +++ b/packages/mysql-on-sqlite/tests/tools/run-parser-benchmark.php @@ -25,8 +25,6 @@ function ( $severity, $message, $file, $line ) { } } -// Use the integration loader so an already-loaded native extension selects -// the same public lexer/parser classes that runtime code uses. require_once __DIR__ . '/../../src/load.php'; function get_stats( $total, $failures, $exceptions ) { @@ -40,9 +38,8 @@ function get_stats( $total, $failures, $exceptions ) { ); } -// Load the MySQL grammar. -$grammar_data = include __DIR__ . '/../../src/mysql/mysql-grammar.php'; -$grammar = new WP_Parser_Grammar( $grammar_data ); +// Load the MySQL parser. +$parser = new WP_MySQL_Parser( require WP_MySQL_Parser::PARSE_TABLE_PATH ); // Load the bounded checked-in corpus before timing so file IO is excluded // from the benchmark. @@ -68,15 +65,12 @@ function get_stats( $total, $failures, $exceptions ) { foreach ( $queries as $query ) { try { $lexer = new WP_MySQL_Lexer( $query ); - $tokens = $lexer instanceof WP_MySQL_Native_Lexer - ? $lexer->native_token_stream() - : $lexer->remaining_tokens(); - if ( ( is_array( $tokens ) ? count( $tokens ) : $tokens->count() ) === 0 ) { + $tokens = $lexer->remaining_tokens(); + if ( count( $tokens ) === 0 ) { throw new Exception( 'Failed to tokenize query: ' . $query ); } - $parser = new WP_MySQL_Parser( $grammar, $tokens ); - $ast = $parser->parse(); + $ast = $parser->parse( $tokens ); if ( null === $ast ) { $failures[] = $query; } @@ -95,15 +89,14 @@ function get_stats( $total, $failures, $exceptions ) { if ( $json ) { echo json_encode( array( - 'benchmark' => 'mysql-parser', - 'implementation' => class_exists( 'WP_MySQL_Native_Parser', false ) ? 'native-extension' : 'php', - 'extension_loaded' => extension_loaded( 'wp_mysql_parser' ), - 'queries' => $processed, - 'duration' => $duration, - 'qps' => $qps, - 'failures' => count( $failures ), - 'exceptions' => count( $exceptions ), - 'php_version' => PHP_VERSION, + 'benchmark' => 'mysql-parser', + 'implementation' => 'php', + 'queries' => $processed, + 'duration' => $duration, + 'qps' => $qps, + 'failures' => count( $failures ), + 'exceptions' => count( $exceptions ), + 'php_version' => PHP_VERSION, ), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ), "\n"; diff --git a/packages/mysql-on-sqlite/tests/tools/verify-native-parser-extension.php b/packages/mysql-on-sqlite/tests/tools/verify-native-parser-extension.php deleted file mode 100644 index 84e99ba58..000000000 --- a/packages/mysql-on-sqlite/tests/tools/verify-native-parser-extension.php +++ /dev/null @@ -1,101 +0,0 @@ -hasProperty( 'native' ) ) { - wp_sqlite_native_parser_verification_fail( $context ); - } - - $native_property = $reflection->getProperty( 'native' ); - $native_property->setAccessible( true ); - if ( ! ( $native_property->getValue( $parser ) instanceof WP_MySQL_Native_Parser ) ) { - wp_sqlite_native_parser_verification_fail( $context ); - } -} - -/** - * Run the native parser verification. - */ -function wp_sqlite_verify_native_parser_extension(): void { - if ( ! class_exists( 'WP_MySQL_Native_Lexer', false ) || ! class_exists( 'WP_MySQL_Native_Parser', false ) ) { - wp_sqlite_native_parser_verification_fail( 'Native MySQL parser extension is not loaded.' ); - } - - $lexer = new WP_MySQL_Lexer( 'SELECT ID, post_title FROM wp_posts WHERE ID IN (1, 2, 3)' ); - if ( ! ( $lexer instanceof WP_MySQL_Native_Lexer ) ) { - wp_sqlite_native_parser_verification_fail( 'WP_MySQL_Lexer did not resolve to the native implementation.' ); - } - - $tokens = $lexer->native_token_stream(); - $rules = include __DIR__ . '/../../src/mysql/mysql-grammar.php'; - $grammar = new WP_Parser_Grammar( $rules ); - $parser = new WP_MySQL_Parser( $grammar, $tokens ); - wp_sqlite_assert_native_parser_delegate( - $parser, - 'WP_MySQL_Parser did not create a native parser delegate.' - ); - - $parser_ast = $parser->parse(); - if ( ! ( $parser_ast instanceof WP_MySQL_Native_Parser_Node ) || 'query' !== $parser_ast->rule_name ) { - wp_sqlite_native_parser_verification_fail( 'Native parser did not produce the expected query AST.' ); - } - - $driver = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname=wp;' ); - $parser = $driver->create_parser( 'SELECT 1' ); - wp_sqlite_assert_native_parser_delegate( - $parser, - 'WP_PDO_MySQL_On_SQLite did not create a native parser delegate.' - ); - - $parser->next_query(); - $ast = $parser->get_query_ast(); - if ( ! ( $ast instanceof WP_MySQL_Native_Parser_Node ) ) { - wp_sqlite_native_parser_verification_fail( 'WP_PDO_MySQL_On_SQLite did not produce a native-backed AST.' ); - } - - $reflection = new ReflectionObject( $ast ); - if ( $reflection->hasProperty( 'native_ast' ) || $reflection->hasProperty( 'native_node_index' ) ) { - wp_sqlite_native_parser_verification_fail( 'Native wrapper still stores Rust AST handle properties.' ); - } - - $first = $ast->get_first_child_node(); - if ( ! ( $first instanceof WP_MySQL_Native_Parser_Node ) ) { - wp_sqlite_native_parser_verification_fail( 'Native wrapper did not return a native-backed child node.' ); - } - - if ( $first !== $ast->get_first_child_node() ) { - wp_sqlite_native_parser_verification_fail( 'Native wrapper identity is not stable across reads.' ); - } - - $synthetic = new WP_Parser_Node( 0, 'synthetic' ); - $first->append_child( $synthetic ); - $same_first = $ast->get_first_child_node(); - if ( $same_first !== $first || ! in_array( $synthetic, $same_first->get_children(), true ) ) { - wp_sqlite_native_parser_verification_fail( 'Materialized native wrapper was lost from the parent cache.' ); - } -} - -wp_sqlite_verify_native_parser_extension(); diff --git a/packages/mysql-parser/src/class-wp-mysql-lexer.php b/packages/mysql-parser/src/class-wp-mysql-lexer.php index 318d1b07b..2cfae41f6 100644 --- a/packages/mysql-parser/src/class-wp-mysql-lexer.php +++ b/packages/mysql-parser/src/class-wp-mysql-lexer.php @@ -39,6 +39,7 @@ class WP_MySQL_Lexer { const SQL_MODE_PIPES_AS_CONCAT = 2; const SQL_MODE_IGNORE_SPACE = 4; const SQL_MODE_NO_BACKSLASH_ESCAPES = 8; + const SQL_MODE_ANSI_QUOTES = 16; /** * Character masks for frequently used character classes. @@ -277,6 +278,8 @@ public function __construct( $this->sql_modes |= self::SQL_MODE_IGNORE_SPACE; } elseif ( 'NO_BACKSLASH_ESCAPES' === $sql_mode ) { $this->sql_modes |= self::SQL_MODE_NO_BACKSLASH_ESCAPES; + } elseif ( 'ANSI_QUOTES' === $sql_mode ) { + $this->sql_modes |= self::SQL_MODE_ANSI_QUOTES; } } @@ -1111,6 +1114,10 @@ private function read_quoted_text(): ?int { if ( '`' === $quote ) { return WP_MySQL_Tokens::BACK_TICK_QUOTED_ID; } elseif ( '"' === $quote ) { + // With the ANSI_QUOTES SQL mode, '"' quotes an identifier, not a string. + if ( $this->is_sql_mode_active( self::SQL_MODE_ANSI_QUOTES ) ) { + return WP_MySQL_Tokens::BACK_TICK_QUOTED_ID; + } return WP_MySQL_Tokens::DOUBLE_QUOTED_TEXT; } else { return WP_MySQL_Tokens::SINGLE_QUOTED_TEXT; diff --git a/packages/mysql-parser/src/class-wp-mysql-parser.php b/packages/mysql-parser/src/class-wp-mysql-parser.php index c9076abf8..9f50fa028 100644 --- a/packages/mysql-parser/src/class-wp-mysql-parser.php +++ b/packages/mysql-parser/src/class-wp-mysql-parser.php @@ -19,8 +19,10 @@ * The constructor applies the patches and points each state at its shared row, * so the parse loop is two plain array lookups per step. * - * AST contract: each node carries the grammar rule name it was reduced by. By - * default every rule materialises a node, including the grammar's deep + * AST contract: each node carries the grammar rule name it was reduced by. + * Reductions with no children produce no node, so empty optionals (opt_*) and + * Bison's mid-rule action rules ($@N) never appear in the tree. By default + * every other rule materialises a node, including the grammar's deep * single-child wrapper chains (expr -> bool_pri -> predicate -> bit_expr -> * ...). Passing $inline_unit_productions = true to the constructor inlines * unit productions whose only child is itself a node — over half of all @@ -34,6 +36,15 @@ * by the production with that number negated. */ class WP_MySQL_Parser { + /** + * The path to the generated parse table file. + * + * The table is data, not a class, so it is not covered by autoloading. + * Consumers can use this path to load it without depending on the + * package file layout: new WP_MySQL_Parser( require WP_MySQL_Parser::PARSE_TABLE_PATH ). + */ + const PARSE_TABLE_PATH = __DIR__ . '/grammar/parse-table.php'; + // ACTION: per-state sparse row (token id => action code) + default code. private $action; // State => row (shared between states). private $action_default; // State => default reduce code. @@ -47,9 +58,9 @@ class WP_MySQL_Parser { private $rule_len; // Production id => rhs length. private $rule_name; // Production id => rule name. - private $state_count; // Number of states (also the accept code). + private $state_count; // Number of states (also the accept code). private $start; // Start state. - private $end_token; // End-of-input token number ($end). + private $end_token; // End-of-input token number ($end). /** Whether unit productions over a node are inlined instead of wrapped. */ private $inline_unit_productions; @@ -170,21 +181,26 @@ public function parse( array $tokens ) { // Reduce: the handle is the production's rhs-length symbols at // symbol_stack[$base .. $top - 1]. Build the node in place by moving - // the stack pointer instead of splicing. In inlining mode, a unit - // production over a node passes the child through unchanged instead - // of wrapping it (see the AST contract in the class docblock). + // the stack pointer instead of splicing. A reduction with no children + // produces no node (null): empty optionals and Bison's mid-rule + // action rules ($@N) carry no information, so the AST contains no + // empty nodes. In inlining mode, a unit production over a node passes + // the child through unchanged instead of wrapping it (see the AST + // contract in the class docblock). $production = -$action_code; $lhs = $rule_lhs[ $production ]; $base = $top - $rule_len[ $production ]; if ( ! $inline || $base + 1 !== $top || ! $symbol_stack[ $base ] instanceof WP_Parser_Node ) { $children = array(); for ( $j = $base; $j < $top; $j++ ) { - $children[] = $symbol_stack[ $j ]; + if ( null !== $symbol_stack[ $j ] ) { + $children[] = $symbol_stack[ $j ]; + } if ( $j > $base ) { $symbol_stack[ $j ] = null; } } - $symbol_stack[ $base ] = new WP_Parser_Node( $lhs, $rule_name[ $production ], $children ); + $symbol_stack[ $base ] = $children ? new WP_Parser_Node( $lhs, $rule_name[ $production ], $children ) : null; } // GOTO on $lhs from the state now exposed under the handle. diff --git a/packages/mysql-parser/src/parser/class-wp-parser-node.php b/packages/mysql-parser/src/parser/class-wp-parser-node.php index a852d0c72..8b548610e 100644 --- a/packages/mysql-parser/src/parser/class-wp-parser-node.php +++ b/packages/mysql-parser/src/parser/class-wp-parser-node.php @@ -225,6 +225,33 @@ public function get_child_nodes( ?string $rule_name = null ): array { return $nodes; } + /** + * Get all child nodes of this node, flattening nested same-rule lists. + * + * Left-recursive grammar list rules nest through their own rule name + * ("list: list ',' item | item"). This method collects child nodes of the + * whole nested chain in source order, as if the list rule were flat. + * + * @param string|null $rule_name Optional. A node rule name to check for. + * @return WP_Parser_Node[] An array of all matching child nodes. + */ + public function get_flattened_child_nodes( ?string $rule_name = null ): array { + $nodes = array(); + foreach ( $this->children as $child ) { + if ( ! $child instanceof WP_Parser_Node ) { + continue; + } + if ( $child->rule_name === $this->rule_name ) { + foreach ( $child->get_flattened_child_nodes( $rule_name ) as $node ) { + $nodes[] = $node; + } + } elseif ( null === $rule_name || $child->rule_name === $rule_name ) { + $nodes[] = $child; + } + } + return $nodes; + } + /** * Get all child tokens of this node. * diff --git a/packages/mysql-on-sqlite/tests/parser/WP_Parser_Node_Tests.php b/packages/mysql-parser/tests/WP_Parser_Node_Tests.php similarity index 74% rename from packages/mysql-on-sqlite/tests/parser/WP_Parser_Node_Tests.php rename to packages/mysql-parser/tests/WP_Parser_Node_Tests.php index 80fbcb067..dbc1f9018 100644 --- a/packages/mysql-on-sqlite/tests/parser/WP_Parser_Node_Tests.php +++ b/packages/mysql-parser/tests/WP_Parser_Node_Tests.php @@ -1,7 +1,5 @@ get_descendant_tokens( 123 ) ); } + + public function testFlattenedChildNodes(): void { + // A left-recursive list as produced by a grammar list rule: + // list: list ',' item | item + // + // list [outer] + // |- list [inner] + // | |- list [innermost] + // | | |- item [a] + // | |- "," + // | |- item [b] + // |- "," + // |- item [c] + // |- other + $input = 'a, b, c'; + $outer = new WP_Parser_Node( 1, 'list' ); + $inner = new WP_Parser_Node( 1, 'list' ); + $innermost = new WP_Parser_Node( 1, 'list' ); + $item_a = new WP_Parser_Node( 2, 'item' ); + $item_b = new WP_Parser_Node( 2, 'item' ); + $item_c = new WP_Parser_Node( 2, 'item' ); + $other = new WP_Parser_Node( 3, 'other' ); + $comma_1 = new WP_Parser_Token( 100, 1, 1, $input ); + $comma_2 = new WP_Parser_Token( 100, 4, 1, $input ); + + $innermost->append_child( $item_a ); + $inner->append_child( $innermost ); + $inner->append_child( $comma_1 ); + $inner->append_child( $item_b ); + $outer->append_child( $inner ); + $outer->append_child( $comma_2 ); + $outer->append_child( $item_c ); + $outer->append_child( $other ); + + // The nested same-rule lists are flattened, in source order. + $this->assertSame( array( $item_a, $item_b, $item_c, $other ), $outer->get_flattened_child_nodes() ); + $this->assertSame( array( $item_a, $item_b, $item_c ), $outer->get_flattened_child_nodes( 'item' ) ); + $this->assertSame( array( $other ), $outer->get_flattened_child_nodes( 'other' ) ); + $this->assertSame( array(), $outer->get_flattened_child_nodes( 'missing' ) ); + + // Flattening works at any level of the nested list. + $this->assertSame( array( $item_a, $item_b ), $inner->get_flattened_child_nodes() ); + + // Same-rule children are always flattened through, never returned. + $this->assertSame( array(), $outer->get_flattened_child_nodes( 'list' ) ); + } } diff --git a/wp-setup.sh b/wp-setup.sh index 0e4321010..801ed2ab8 100755 --- a/wp-setup.sh +++ b/wp-setup.sh @@ -26,7 +26,26 @@ rm -rf "$WP_DIR" echo "Cloning the WordPress repository..." git clone --depth 1 --branch "$WP_VERSION" https://github.com/WordPress/wordpress-develop.git "$WP_DIR" -# 3. Add "docker-compose.override.yml" to the WordPress repository. +# 3. Install the SQLite driver Composer dependencies (wordpress/mysql-parser) +# into a dedicated, self-contained vendor directory (path repositories are +# mirrored instead of symlinked) used only by the WordPress containers. +# +# A production (--no-dev) install is required here: the driver development +# vendor directory contains PHPUnit, and loading it inside the WordPress +# test process would clash with the WordPress PHPUnit test runner. +# +# Note that Docker resolves the driver source mountpoint through the +# plugin's "wp-includes/database" symlink, so the driver lands in the +# containers at "plugins/mysql-on-sqlite/src", and the vendor directory +# is mounted next to it. +echo "Installing the SQLite driver Composer dependencies..." +rm -rf "$WP_DIR/driver-vendor" +# The vendor path must be absolute: Composer resolves a relative vendor-dir +# against the working directory, not against the current directory. +COMPOSER_VENDOR_DIR="$(cd "$WP_DIR" && pwd)/driver-vendor" COMPOSER_MIRROR_PATH_REPOS=1 \ + composer install --working-dir="$DIR/packages/mysql-on-sqlite" --no-dev --no-interaction + +# 4. Add "docker-compose.override.yml" to the WordPress repository. echo "Adding 'docker-compose.override.yml' to the WordPress repository..." cat << EOF > "$WP_DIR/docker-compose.override.yml" services: @@ -34,6 +53,7 @@ services: 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 + - ./driver-vendor:/var/www/src/wp-content/plugins/mysql-on-sqlite/vendor php: # PHP temporarily pinned to 8.3.10, see: https://github.com/WordPress/wordpress-develop/pull/9602 @@ -41,6 +61,7 @@ services: 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 + - ./driver-vendor:/var/www/src/wp-content/plugins/mysql-on-sqlite/vendor cli: # PHP temporarily pinned to 8.3.10, see: https://github.com/WordPress/wordpress-develop/pull/9602 @@ -48,20 +69,21 @@ services: 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 + - ./driver-vendor:/var/www/src/wp-content/plugins/mysql-on-sqlite/vendor EOF -# 4. Add "db.php" to the "wp-content" directory. +# 5. Add "db.php" to the "wp-content" directory. echo "Adding 'db.php' to the 'wp-content' directory..." rm -f "$WP_DIR"/src/wp-content/db.php cp "$DIR"/packages/plugin-sqlite-database-integration/db.copy "$WP_DIR"/src/wp-content/db.php sed -i.bak "s#'{SQLITE_IMPLEMENTATION_FOLDER_PATH}'#__DIR__.'/plugins/sqlite-database-integration'#g" "$WP_DIR"/src/wp-content/db.php sed -i.bak "s#{SQLITE_PLUGIN}#sqlite-database-integration/load.php#g" "$WP_DIR"/src/wp-content/db.php -# 5. Rewrite helper class WpdbExposedMethodsForTesting to extend WP_SQLite_DB. +# 6. Rewrite helper class WpdbExposedMethodsForTesting to extend WP_SQLite_DB. echo "Rewriting helper class 'WpdbExposedMethodsForTesting' to extend WP_SQLite_DB..." sed -i.bak "s#class WpdbExposedMethodsForTesting extends wpdb {#class WpdbExposedMethodsForTesting extends WP_SQLite_DB {#g" "$WP_DIR"/tests/phpunit/includes/utils.php -# 6. Install dependencies. +# 7. Install dependencies. echo "Installing dependencies..." npm --prefix "$WP_DIR" install npm --prefix "$WP_DIR" run build:dev