diff --git a/.gitattributes b/.gitattributes index 41f7464..6a7e147 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,7 +8,4 @@ phpunit.xml.dist export-ignore /.github export-ignore /grammar-tools export-ignore /tests export-ignore -/wip export-ignore -/wp-includes/mysql export-ignore -/wp-includes/parser export-ignore /wp-includes/sqlite/class-wp-sqlite-crosscheck-db.php export-ignore diff --git a/activate.php b/activate.php index e91e181..197f162 100644 --- a/activate.php +++ b/activate.php @@ -75,8 +75,8 @@ function ( $result ) { * When the plugin gets merged in wp-core, this is not to be ported. */ function sqlite_plugin_copy_db_file() { - // Bail early if the SQLite3 class does not exist. - if ( ! class_exists( 'SQLite3' ) ) { + // Bail early if the PDO SQLite extension is not loaded. + if ( ! extension_loaded( 'pdo_sqlite' ) ) { return; } diff --git a/admin-notices.php b/admin-notices.php index 8eaf253..a455cc8 100644 --- a/admin-notices.php +++ b/admin-notices.php @@ -19,11 +19,11 @@ function sqlite_plugin_admin_notice() { return; } - // If SQLite is not detected, bail early. - if ( ! class_exists( 'SQLite3' ) ) { + // If PDO SQLite is not loaded, bail early. + if ( ! extension_loaded( 'pdo_sqlite' ) ) { printf( '

%s

', - esc_html__( 'The SQLite Integration plugin is active, but the SQLite3 class is missing from your server. Please make sure that SQLite is enabled in your PHP installation.', 'sqlite-database-integration' ) + esc_html__( 'The SQLite Integration plugin is active, but the PDO SQLite extension is missing from your server. Please make sure that PDO SQLite is enabled in your PHP installation.', 'sqlite-database-integration' ) ); return; } diff --git a/admin-page.php b/admin-page.php index 6a44c0e..c89f4dd 100644 --- a/admin-page.php +++ b/admin-page.php @@ -47,11 +47,7 @@ function sqlite_integration_admin_screen() { ?>

- -
-

-
- +

@@ -130,7 +126,8 @@ function sqlite_plugin_adminbar_item( $admin_bar ) { global $wpdb; if ( defined( 'SQLITE_DB_DROPIN_VERSION' ) && defined( 'DB_ENGINE' ) && 'sqlite' === DB_ENGINE ) { - $title = '' . __( 'Database: SQLite', 'sqlite-database-integration' ) . ''; + $suffix = defined( 'WP_SQLITE_AST_DRIVER' ) && WP_SQLITE_AST_DRIVER ? ' (AST)' : ''; + $title = '' . __( 'Database: SQLite', 'sqlite-database-integration' ) . $suffix . ''; } elseif ( stripos( $wpdb->db_server_info(), 'maria' ) !== false ) { $title = '' . __( 'Database: MariaDB', 'sqlite-database-integration' ) . ''; } else { diff --git a/composer.json b/composer.json index dce2c54..975debc 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,9 @@ "issues": "https://github.com/wordpress/sqlite-database-integration/issues" }, "require": { - "php": ">=7.0" + "php": ">=7.0", + "ext-pdo": "*", + "ext-pdo_sqlite": "*" }, "require-dev": { "ext-mbstring": "*", diff --git a/grammar-tools/MySQLParser.g4 b/grammar-tools/MySQLParser.g4 index b8fc831..44669f2 100644 --- a/grammar-tools/MySQLParser.g4 +++ b/grammar-tools/MySQLParser.g4 @@ -153,13 +153,13 @@ alterStatement: | alterLogfileGroup | alterServer // ALTER USER is part of the user management rule. - | alterInstance /* @FIX: Add support for "ALTER INSTANCE ..." statement. */ + | alterInstance /* @CHANGED: Added support for "ALTER INSTANCE ..." statement. */ ) ; /* - * @FIX: - * Add support for "ALTER INSTANCE ..." statement. + * @CHANGED: + * Added support for "ALTER INSTANCE ..." statement. */ alterInstance: {serverVersion >= 50711}? INSTANCE_SYMBOL ( @@ -172,7 +172,7 @@ alterInstance: ; alterDatabase: - /* @FIX: Make "schemaRef" optional. */ + /* @CHANGED: Made "schemaRef" optional. */ DATABASE_SYMBOL schemaRef? ( createDatabaseOption+ | {serverVersion < 80000}? UPGRADE_SYMBOL DATA_SYMBOL DIRECTORY_SYMBOL NAME_SYMBOL @@ -218,8 +218,8 @@ alterTable: ;*/ /* - * @FIX: - * Fix "alterTableActions" to solve conflicts between "alterCommandsModifierList" and "alterCommandList". + * @CHANGED: + * Fixed "alterTableActions" to solve conflicts between "alterCommandsModifierList" and "alterCommandList". */ alterTableActions: (alterCommandsModifierList COMMA_SYMBOL)? standaloneAlterCommands @@ -234,8 +234,8 @@ alterTableActions: ;*/ /* - * @FIX: - * Fix "alterCommandList" to solve conflicts between "alterCommandsModifierList" prefixes. + * @CHANGED: + * Fixed "alterCommandList" to solve conflicts between "alterCommandsModifierList" prefixes. */ alterCommandList: alterCommandsModifierList (COMMA_SYMBOL alterList)? @@ -320,7 +320,7 @@ alterListItem: | signedLiteral ) | DROP_SYMBOL DEFAULT_SYMBOL - | {serverVersion >= 80023}? SET_SYMBOL visibility /* @FIX: Add missing SET VISIBLE/INVISIBLE clause. */ + | {serverVersion >= 80023}? SET_SYMBOL visibility /* @CHANGED: Added missing SET VISIBLE/INVISIBLE clause. */ ) | {serverVersion >= 80000}? ALTER_SYMBOL INDEX_SYMBOL indexRef visibility | {serverVersion >= 80017}? ALTER_SYMBOL CHECK_SYMBOL identifier constraintEnforcement @@ -352,8 +352,8 @@ restrict: ;*/ /* - * @FIX: - * Fix ALTER TABLE with ORDER to use 'qualifiedIdentifier' instead of just 'identifier'. + * @CHANGED: + * Fixed ALTER TABLE with ORDER to use 'qualifiedIdentifier' instead of just 'identifier'. * This is necessary to support "t.id" in a query like "ALTER TABLE t ORDER BY t.id". */ alterOrderList: @@ -425,7 +425,7 @@ alterTablespaceOption: | tsOptionAutoextendSize | tsOptionMaxSize | tsOptionEngine - | {serverVersion >= 80021}? tsOptionEngineAttribute /* @FIX: Add missing "ENGINE_ATTRIBUTE" option. */ + | {serverVersion >= 80021}? tsOptionEngineAttribute /* @CHANGED: Added missing "ENGINE_ATTRIBUTE" option. */ | tsOptionWait | tsOptionEncryption ; @@ -495,8 +495,8 @@ createDatabaseOption: ;*/ /* - * @FIX: - * Fix "createTable" to solve support "LIKE tableRef" and "LIKE (tableRef)". + * @CHANGED: + * Fixed "createTable" to solve support "LIKE tableRef" and "LIKE (tableRef)". * They need to come before "tableElementList" to avoid misinterpreting "LIKE". */ createTable: @@ -529,8 +529,8 @@ createRoutine: // Rule for external use only. ; /* - * @FIX: - * Add missing "ifNotExists?". + * @CHANGED: + * Added missing "ifNotExists?". */ createProcedure: definerClause? PROCEDURE_SYMBOL ({serverVersion >= 80029}? ifNotExists?) procedureName OPEN_PAR_SYMBOL ( @@ -539,8 +539,8 @@ createProcedure: ; /* - * @FIX: - * Add missing "ifNotExists?". + * @CHANGED: + * Added missing "ifNotExists?". */ createFunction: definerClause? FUNCTION_SYMBOL ({serverVersion >= 80029}? ifNotExists?) functionName OPEN_PAR_SYMBOL ( @@ -613,8 +613,8 @@ createIndex: ;*/ /* - * @FIX: - * Fix "indexNameAndType" to solve conflicts between "indexName USING_SYMBOL" + * @CHANGED: + * Fixed "indexNameAndType" to solve conflicts between "indexName USING_SYMBOL" * and "indexName TYPE_SYMBOL" prefix by moving them to a single branch. */ indexNameAndType: @@ -694,7 +694,7 @@ tablespaceOption: | tsOptionExtentSize | tsOptionNodegroup | tsOptionEngine - | {serverVersion >= 80021}? tsOptionEngineAttribute /* @FIX: Add missing "ENGINE_ATTRIBUTE" option. */ + | {serverVersion >= 80021}? tsOptionEngineAttribute /* @CHANGED: Added missing "ENGINE_ATTRIBUTE" option. */ | tsOptionWait | tsOptionComment | {serverVersion >= 50707}? tsOptionFileblockSize @@ -730,8 +730,8 @@ tsOptionEngine: ; /* - * @FIX: - * Add missing "ENGINE_ATTRIBUTE" option. + * @CHANGED: + * Added missing "ENGINE_ATTRIBUTE" option. */ tsOptionEngineAttribute: ENGINE_ATTRIBUTE_SYMBOL EQUAL_OPERATOR? textStringLiteral @@ -774,8 +774,8 @@ viewSuid: ; /* - * @FIX: - * Add missing "ifNotExists?". + * @CHANGED: + * Added missing "ifNotExists?". */ createTrigger: definerClause? TRIGGER_SYMBOL ({serverVersion >= 80029}? ifNotExists?) triggerName timing = (BEFORE_SYMBOL | AFTER_SYMBOL) event = ( @@ -961,8 +961,8 @@ deleteStatementOption: // opt_delete_option in sql_yacc.yy, but the name collide ;*/ /* - * @FIX: - * Reorder "selectItemList" and "exprList" to match "selectItemList", as we don't handle versions yet. + * @CHANGED: + * Reordered "selectItemList" and "exprList" to match "selectItemList", as we don't handle versions yet. */ doStatement: DO_SYMBOL ( @@ -1099,8 +1099,8 @@ replaceStatement: ;*/ /* - * @FIX: - * Fix "selectStatement" to solve conflicts between "queryExpressionParens" and "selectStatementWithInto". + * @CHANGED: + * Fixed "selectStatement" to solve conflicts between "queryExpressionParens" and "selectStatementWithInto". * Since "queryExpression" already contains "queryExpressionParens" as a subrule, we can remove it here. */ selectStatement: @@ -1145,7 +1145,7 @@ selectStatement: selectStatementWithInto: OPEN_PAR_SYMBOL selectStatementWithInto CLOSE_PAR_SYMBOL | queryExpression intoClause lockingClauseList? - | queryExpression lockingClauseList intoClause /* @FIX: Add missing "queryExpression" prefix. */ + | queryExpression lockingClauseList intoClause /* @CHANGED: Added missing "queryExpression" prefix. */ ; queryExpression: @@ -1166,8 +1166,8 @@ queryExpression: ;*/ /* - * @FIX: - * Implement missing "EXCEPT" and "INTERSECT" operators in the grammar. + * @CHANGED: + * Implemented missing "EXCEPT" and "INTERSECT" operators in the grammar. * Note that "INTERSECT" must have a higher precedence than "UNION" and "EXCEPT", * and is therefore evaluated first via "queryTerm" as per: * https://dev.mysql.com/doc/refman/8.0/en/set-operations.html @@ -1188,8 +1188,8 @@ queryTerm: ;*/ /* - * @FIX: - * Rewrite "queryExpressionParens" to keep only "queryExpression" within. + * @CHANGED: + * Rewrote "queryExpressionParens" to keep only "queryExpression" within. * This avoids conflict between "queryExpressionParens" and "queryExpression" * (which already contains "queryExpressionParens" as a subrule). */ @@ -1278,8 +1278,8 @@ windowSpec: ;*/ /* - * @FIX: - * Rewrite "windowSpecDetails" so to keep variants with "windowName?" last. + * @CHANGED: + * Rewrote "windowSpecDetails" so to keep variants with "windowName?" last. * We first need to try to match the symbols as keywords, only then as identifiers. * Identifiers can never take precedence over keywords in the grammar. * @@ -1544,7 +1544,7 @@ jtOnResponse: ;*/ /* - * @FIX: + * @CHANGED: * Renamed "unionOption" to "setOperationOption" as this is now used also for EXCEPT and INTERSECT. */ setOperationOption: @@ -1561,8 +1561,8 @@ tableAlias: ;*/ /* - * @FIX: - * Fix "indexHintList" to use only whitespace as a separator (not commas). + * @CHANGED: + * Fixed "indexHintList" to use only whitespace as a separator (not commas). */ indexHintList: indexHint+ @@ -1618,7 +1618,7 @@ transactionOrLockingStatement: ; transactionStatement: - /* @FIX: Use "transactionCharacteristicList" instead of "transactionCharacteristic". */ + /* @CHANGED: Used "transactionCharacteristicList" instead of "transactionCharacteristic". */ START_SYMBOL TRANSACTION_SYMBOL transactionCharacteristicList? | COMMIT_SYMBOL WORK_SYMBOL? (AND_SYMBOL NO_SYMBOL? CHAIN_SYMBOL)? ( NO_SYMBOL? RELEASE_SYMBOL @@ -1632,8 +1632,8 @@ beginWork: ; /* - * @FIX: - * Add "transactionCharacteristicList" to fix support for transaction with multiple characteristics. + * @CHANGED: + * Added "transactionCharacteristicList" to fix support for transaction with multiple characteristics. */ transactionCharacteristicList: transactionCharacteristic (COMMA_SYMBOL transactionCharacteristic)* @@ -1709,8 +1709,8 @@ xid: ;*/ /* - * @FIX: - * Fix "replicationStatement" to correctly support the "RESET PERSIST" statement. + * @CHANGED: + * Fixed "replicationStatement" to correctly support the "RESET PERSIST" statement. * The "ifExists" clause wasn't optional, and "identifier" was used instead of "qualifiedIdentifier". */ replicationStatement: @@ -1814,7 +1814,7 @@ serverIdList: ; changeReplication: - /* @FIX: Add support for "CHANGE REPLICATION SOURCE ..." statement. */ + /* @CHANGED: Added support for "CHANGE REPLICATION SOURCE ..." statement. */ CHANGE_SYMBOL REPLICATION_SYMBOL SOURCE_SYMBOL TO_SYMBOL changeReplicationSourceOptions channel? | CHANGE_SYMBOL REPLICATION_SYMBOL FILTER_SYMBOL filterDefinition ( COMMA_SYMBOL filterDefinition @@ -1822,16 +1822,16 @@ changeReplication: ; /* - * @FIX: - * Add support for "CHANGE REPLICATION SOURCE ..." statement. + * @CHANGED: + * Added support for "CHANGE REPLICATION SOURCE ..." statement. */ changeReplicationSourceOptions: replicationSourceOption (COMMA_SYMBOL replicationSourceOption)* ; /* - * @FIX: - * Add support for "CHANGE REPLICATION SOURCE ..." statement. + * @CHANGED: + * Added support for "CHANGE REPLICATION SOURCE ..." statement. */ replicationSourceOption: (SOURCE_BIND_SYMBOL | MASTER_BIND_SYMBOL) EQUAL_OPERATOR textStringNoLinebreak @@ -2013,12 +2013,12 @@ alterUser: ;*/ /* - * @FIX: - * 1. Support also "USER()" function call. - * 2. Fix matching "DEFAULT ROLE" by reordering rules. - * 3. Reorder "alterUserList" and "createUserList" to match "alterUserList", as we don't handle versions yet. + * @CHANGED: + * 1. Added support for "USER()" function call. + * 2. Fixed matching "DEFAULT ROLE" by reordering rules. + * 3. Reordered "alterUserList" and "createUserList" to match "alterUserList", as we don't handle versions yet. * 4. Removed "IDENTIFIED (WITH) BY ..." and "discardOldPassword"; see the fixed "alterUserEntry" rule. - * 5. Remove "FAILED_LOGIN_ATTEMPTS_SYMBOL" and "PASSWORD_LOCK_TIME_SYMBOL"; they are in "createUserTail" now. + * 5. Removed "FAILED_LOGIN_ATTEMPTS_SYMBOL" and "PASSWORD_LOCK_TIME_SYMBOL"; they are in "createUserTail" now. */ alterUserTail: {serverVersion >= 80000}? (userFunction | user) DEFAULT_SYMBOL ROLE_SYMBOL (ALL_SYMBOL | NONE_SYMBOL | roleList) @@ -2040,8 +2040,8 @@ createUser: //; /* - * @FIX: - * Add support COMMENT and ATTRIBUTE. The "(COMMENT_SYMBOL | ATTRIBUTE_SYMBOL) textString)?" was missing. + * @CHANGED: + * Added support COMMENT and ATTRIBUTE. The "(COMMENT_SYMBOL | ATTRIBUTE_SYMBOL) textString)?" was missing. */ createUserTail: {serverVersion >= 50706}? requireClause? connectOptions? accountLockPasswordExpireOptions* @@ -2089,8 +2089,8 @@ connectOptions: ;*/ /* - * @FIX: - * Add missing "PASSWORD_LOCK_TIME_SYMBOL" and "FAILED_LOGIN_ATTEMPTS_SYMBOL". + * @CHANGED: + * Added missing "PASSWORD_LOCK_TIME_SYMBOL" and "FAILED_LOGIN_ATTEMPTS_SYMBOL". */ accountLockPasswordExpireOptions: ACCOUNT_SYMBOL (LOCK_SYMBOL | UNLOCK_SYMBOL) @@ -2179,8 +2179,8 @@ renameUser: ;*/ /* - * @FIX: - * Fix "revoke" to support "IF EXISTS" and "REVOKE ALL ON ... FROM ...". + * @CHANGED: + * Fixed "revoke" to support "IF EXISTS" and "REVOKE ALL ON ... FROM ...". * 1. The "IF EXISTS" clause was missing in the original rule. * 2. The "(onTypeTo? FROM_SYMBOL userList)?" part was missing in the original rule. * 3. The "IGNORE UNKNOWN USER" clause was missing in the original rule. @@ -2214,12 +2214,12 @@ roleOrPrivilegesList: roleOrPrivilege: {serverVersion > 80000}? ( - /* @FIX: Reorder branches to solve conflict between them. */ + /* @CHANGED: Reordered branches to solve conflict between them. */ roleIdentifierOrText (AT_TEXT_SUFFIX | AT_SIGN_SYMBOL textOrIdentifier) | roleIdentifierOrText columnInternalRefList? ) | (SELECT_SYMBOL | INSERT_SYMBOL | UPDATE_SYMBOL | REFERENCES_SYMBOL) columnInternalRefList? - /* @FIX: Moved "CREATE/DROP ROLE" before "CREATE ..." and "DROP ..." to solve conflict. */ + /* @CHANGED: Moved "CREATE/DROP ROLE" before "CREATE ..." and "DROP ..." to solve conflict. */ | {serverVersion > 80000}? (CREATE_SYMBOL | DROP_SYMBOL) ROLE_SYMBOL | ( DELETE_SYMBOL @@ -2257,8 +2257,8 @@ roleOrPrivilege: ;*/ /* - * @FIX: - * Rewrite "grantIdentifier" to solve conflicts between "schemaRef DOT_SYMBOL tableRef" + * @CHANGED: + * Rewrote "grantIdentifier" to solve conflicts between "schemaRef DOT_SYMBOL tableRef" * and "schemaRef DOT_SYMBOL MULT_OPERATOR". Move them to a single branch, and order * "schemaRef" and "tableRef" after to preserve precedence of keywords over identifiers. */ @@ -2318,8 +2318,8 @@ role: ;*/ /* - * @FIX: - * Fix administration statements to support both "TABLE" and "TABLES" keywords. + * @CHANGED: + * Fixed administration statements to support both "TABLE" and "TABLES" keywords. * The original rule only supported "TABLE". */ tableAdministrationStatement: @@ -2343,8 +2343,8 @@ tableAdministrationStatement: ;*/ /* - * @FIX: - * Add missing optional "USING DATA 'json_data'" to UPDATE HISTOGRAM clause. + * @CHANGED: + * Added missing optional "USING DATA 'json_data'" to UPDATE HISTOGRAM clause. */ histogram: UPDATE_SYMBOL HISTOGRAM_SYMBOL ON_SYMBOL identifierList ( @@ -2369,7 +2369,7 @@ repairType: installUninstallStatment: // COMPONENT_SYMBOL is conditionally set in the lexer. action = INSTALL_SYMBOL type = PLUGIN_SYMBOL identifier SONAME_SYMBOL textStringLiteral - /* @FIX: Add missing "INSTALL COMPONENT" statement "SET ..." suffix. */ + /* @CHANGED: Added missing "INSTALL COMPONENT" statement "SET ..." suffix. */ | action = INSTALL_SYMBOL type = COMPONENT_SYMBOL textStringLiteralList (SET_SYMBOL installSetValueList)? | action = UNINSTALL_SYMBOL type = PLUGIN_SYMBOL pluginRef | action = UNINSTALL_SYMBOL type = COMPONENT_SYMBOL componentRef ( @@ -2378,8 +2378,8 @@ installUninstallStatment: ; /* - * @FIX: - * Add missing "INSTALL COMPONENT" statement "SET ..." suffix. + * @CHANGED: + * Added missing "INSTALL COMPONENT" statement "SET ..." suffix. */ installOptionType: GLOBAL_SYMBOL | PERSIST_SYMBOL; @@ -2520,7 +2520,7 @@ showStatement: | value = COLLATION_SYMBOL likeOrWhere? | {serverVersion < 50700}? value = CONTRIBUTORS_SYMBOL | value = PRIVILEGES_SYMBOL - /* @FIX: Moved "GRANTS_SYMBOL ... USING_SYMBOL" before "GRANTS_SYMBOL ..." to solve conflict. */ + /* @CHANGED: Moved "GRANTS_SYMBOL ... USING_SYMBOL" before "GRANTS_SYMBOL ..." to solve conflict. */ | value = GRANTS_SYMBOL FOR_SYMBOL user USING_SYMBOL userList | value = GRANTS_SYMBOL (FOR_SYMBOL user)? /*| value = GRANTS_SYMBOL FOR_SYMBOL user USING_SYMBOL userList */ @@ -2645,8 +2645,8 @@ logType: ; /* - * @FIX: - * Replace "identifierList" with "tableRefList" to correctly support qualified identifiers. + * @CHANGED: + * Replaced "identifierList" with "tableRefList" to correctly support qualified identifiers. */ flushTables: (TABLES_SYMBOL | TABLE_SYMBOL) ( @@ -2738,8 +2738,8 @@ dropResourceGroup: ;*/ /* - * @FIX: - * Reorder "explainStatement" and "describeStatement". + * @CHANGED: + * Reordered "explainStatement" and "describeStatement". * EXPLAIN can be followed by an identifier (matching a "describeStatement"), * but identifiers can never take precedence over keywords in the grammar. * @@ -2774,8 +2774,8 @@ describeStatement: ;*/ /* - * @FIX: - * Fix "explainStatement" to solve conflict between "ANALYZE ..." and "ANALYZE FORMAT=...". + * @CHANGED: + * Fixed "explainStatement" to solve conflict between "ANALYZE ..." and "ANALYZE FORMAT=...". * The "ANALYZE FORMAT=..." must be attempted to be matched before "ANALYZE ...". */ explainStatement: @@ -2823,8 +2823,8 @@ restartServer: ; /* - * @FIX: - * Factor left recursion. + * @CHANGED: + * Factored left recursion. */ expr: %expr_simple %expr_rr*; @@ -2847,9 +2847,9 @@ expr: %expr_simple %expr_rr*; ;*/ /* - * @FIX: - * 1. Factor left recursion. - * 2. Move "compOp (ALL_SYMBOL | ANY_SYMBOL)" before "compOp predicate" to avoid conflicts. + * @CHANGED: + * 1. Factored left recursion. + * 2. Movee "compOp (ALL_SYMBOL | ANY_SYMBOL)" before "compOp predicate" to avoid conflicts. */ boolPri: predicate %boolPri_rr* @@ -2904,8 +2904,8 @@ predicateOperations: ;*/ /* - * @FIX: - * Factor left recursion. + * @CHANGED: + * Factored left recursion. */ bitExpr: simpleExpr %bitExpr_rr*; @@ -2954,8 +2954,8 @@ bitExpr: simpleExpr %bitExpr_rr*; ;*/ /* - * @FIX: - * Factor left recursion. + * @CHANGED: + * Factored left recursion. */ simpleExpr: %simpleExpr_collate (CONCAT_PIPES_SYMBOL %simpleExpr_collate)*; @@ -2965,9 +2965,9 @@ simpleExpr: %simpleExpr_collate (CONCAT_PIPES_SYMBOL %simpleExpr_collate)*; literal # simpleExprLiteral | sumExpr # simpleExprSum | variable (equal expr)? # simpleExprVariable - | functionCall # simpleExprFunction - | runtimeFunctionCall # simpleExprRuntimeFunction - | columnRef jsonOperator? # simpleExprColumnRef + /*| functionCall # simpleExprFunction*/ + /*| runtimeFunctionCall # simpleExprRuntimeFunction*/ + /*| columnRef jsonOperator? # simpleExprColumnRef*/ | PARAM_MARKER # simpleExprParamMarker | {serverVersion >= 80000}? groupingOperation # simpleExprGroupingOperation | {serverVersion >= 80000}? windowFunctionCall # simpleExprWindowingFunction @@ -2978,7 +2978,7 @@ simpleExpr: %simpleExpr_collate (CONCAT_PIPES_SYMBOL %simpleExpr_collate)*; | OPEN_CURLY_SYMBOL identifier expr CLOSE_CURLY_SYMBOL # simpleExprOdbc | MATCH_SYMBOL identListArg AGAINST_SYMBOL OPEN_PAR_SYMBOL bitExpr fulltextOptions? CLOSE_PAR_SYMBOL # simpleExprMatch | BINARY_SYMBOL simpleExpr # simpleExprBinary - /* @FIX: Add support for CAST(... AT TIME ZONE ... AS DATETIME ...). */ + /* @CHANGED: Added support for CAST(... AT TIME ZONE ... AS DATETIME ...). */ | ({serverVersion >= 80022}? CAST_SYMBOL OPEN_PAR_SYMBOL expr AT_SYMBOL TIME_SYMBOL ZONE_SYMBOL INTERVAL_SYMBOL? textStringLiteral @@ -2991,6 +2991,10 @@ simpleExpr: %simpleExpr_collate (CONCAT_PIPES_SYMBOL %simpleExpr_collate)*; | DEFAULT_SYMBOL OPEN_PAR_SYMBOL simpleIdentifier CLOSE_PAR_SYMBOL # simpleExprDefault | VALUES_SYMBOL OPEN_PAR_SYMBOL simpleIdentifier CLOSE_PAR_SYMBOL # simpleExprValues | INTERVAL_SYMBOL expr interval PLUS_OPERATOR expr # simpleExprInterval + /* @CHANGED: Moved function calls and ref to the end to avoid conflicts with the above expressions. */ + | functionCall # simpleExprFunction + | runtimeFunctionCall # simpleExprRuntimeFunction + | columnRef jsonOperator? # simpleExprColumnRef ; arrayCast: @@ -3073,8 +3077,8 @@ windowingClause: ;*/ /* - * @FIX: - * Fix "leadLagInfo" to support "identifier" and "userVariable" as well. + * @CHANGED: + * Fixed "leadLagInfo" to support "identifier" and "userVariable" as well. */ leadLagInfo: COMMA_SYMBOL (ulonglong_number | PARAM_MARKER | identifier | userVariable) (COMMA_SYMBOL expr)? @@ -3119,7 +3123,7 @@ runtimeFunctionCall: | name = HOUR_SYMBOL exprWithParentheses | name = INSERT_SYMBOL OPEN_PAR_SYMBOL expr COMMA_SYMBOL expr COMMA_SYMBOL expr COMMA_SYMBOL expr CLOSE_PAR_SYMBOL | name = INTERVAL_SYMBOL OPEN_PAR_SYMBOL expr (COMMA_SYMBOL expr)+ CLOSE_PAR_SYMBOL - /* @FIX: Add support for "JSON_VALUE(..., '...' RETURNING ). */ + /* @CHANGED: Added support for "JSON_VALUE(..., '...' RETURNING ). */ | {serverVersion >= 80021}? name = JSON_VALUE_SYMBOL OPEN_PAR_SYMBOL simpleExpr COMMA_SYMBOL textLiteral (RETURNING_SYMBOL castType)? onEmptyOrError? CLOSE_PAR_SYMBOL | name = LEFT_SYMBOL OPEN_PAR_SYMBOL expr COMMA_SYMBOL expr CLOSE_PAR_SYMBOL @@ -3175,7 +3179,7 @@ runtimeFunctionCall: | name = WEEK_SYMBOL OPEN_PAR_SYMBOL expr (COMMA_SYMBOL expr)? CLOSE_PAR_SYMBOL | name = WEIGHT_STRING_SYMBOL OPEN_PAR_SYMBOL expr AS_SYMBOL CHAR_SYMBOL CLOSE_PAR_SYMBOL | name = WEIGHT_STRING_SYMBOL OPEN_PAR_SYMBOL expr ( - /* @FIX: Move "AS BINARY(...)" before "AS CHAR(...)" to solve conflict. */ + /* @CHANGED: Moved "AS BINARY(...)" before "AS CHAR(...)" to solve conflict. */ AS_SYMBOL BINARY_SYMBOL wsNumCodepoints | (AS_SYMBOL CHAR_SYMBOL wsNumCodepoints)? ( {serverVersion < 80000}? weightStringLevels @@ -3303,8 +3307,8 @@ elseExpression: /* - * @FIX: - * Fix CAST(2024 AS YEAR). + * @CHANGED: + * Fixed CAST(2024 AS YEAR). * The original grammar was missing the YEAR_SYMBOL in the "castType" rule. */ castType: @@ -3668,8 +3672,8 @@ constraintName: ;*/ /* - * @FIX: - * Fix "fieldDefinition" to solve conflict between "columnAttribute" and GENERATED/AS. + * @CHANGED: + * Fixed "fieldDefinition" to solve conflict between "columnAttribute" and GENERATED/AS. */ fieldDefinition: dataType ( @@ -3677,7 +3681,7 @@ fieldDefinition: VIRTUAL_SYMBOL | STORED_SYMBOL )? ( - /* @FIX: Reorder "columnAttribute*" and "gcolAttribute*" to match "columnAttribute*", as we don't handle versions yet. */ + /* @CHANGED: Reordered "columnAttribute*" and "gcolAttribute*" to match "columnAttribute*", as we don't handle versions yet. */ {serverVersion >= 80000}? columnAttribute* // Beginning with 8.0 the full attribute set is supported. | {serverVersion < 80000}? gcolAttribute* ) @@ -3693,7 +3697,7 @@ columnAttribute: | NOW_SYMBOL timeFunctionParameters? | {serverVersion >= 80013}? exprWithParentheses ) - | {serverVersion >= 80023}? visibility /* @FIX: Add missing VISIBLE/INVISIBLE attribute. */ + | {serverVersion >= 80023}? visibility /* @CHANGED: Added missing VISIBLE/INVISIBLE attribute. */ | value = ON_SYMBOL UPDATE_SYMBOL NOW_SYMBOL timeFunctionParameters? | value = AUTO_INCREMENT_SYMBOL | value = SERIAL_SYMBOL DEFAULT_SYMBOL VALUE_SYMBOL @@ -3744,7 +3748,7 @@ deleteOption: (RESTRICT_SYMBOL | CASCADE_SYMBOL) | SET_SYMBOL nullLiteral | NO_SYMBOL ACTION_SYMBOL - | SET_SYMBOL DEFAULT_SYMBOL /* @FIX: Add missing "SET DEFAULT" option. */ + | SET_SYMBOL DEFAULT_SYMBOL /* @CHANGED: Added missing "SET DEFAULT" option. */ ; keyList: @@ -3819,7 +3823,7 @@ dataType: // type in sql_yacc.yy | type = (FLOAT_SYMBOL | DECIMAL_SYMBOL | NUMERIC_SYMBOL | FIXED_SYMBOL) floatOptions? fieldOptions? | type = BIT_SYMBOL fieldLength? | type = (BOOL_SYMBOL | BOOLEAN_SYMBOL) - /* @FIX: Moved "CHAR_SYMBOL VARYING_SYMBOL" before "CHAR_SYMBOL ..." to solve conflict. */ + /* @CHANGED: Moved "CHAR_SYMBOL VARYING_SYMBOL" before "CHAR_SYMBOL ..." to solve conflict. */ | (type = CHAR_SYMBOL VARYING_SYMBOL | type = VARCHAR_SYMBOL) fieldLength charsetWithOptBinary? | type = CHAR_SYMBOL fieldLength? charsetWithOptBinary? /*| nchar fieldLength? BINARY_SYMBOL? */ @@ -3832,7 +3836,7 @@ dataType: // type in sql_yacc.yy | type = NATIONAL_SYMBOL CHAR_SYMBOL VARYING_SYMBOL | type = NCHAR_SYMBOL VARYING_SYMBOL ) fieldLength BINARY_SYMBOL? - /* @FIX: Moved "nchar fieldLength? BINARY_SYMBOL?" after othe nchar definitions to solve conflicts. */ + /* @CHANGED: Moved "nchar fieldLength? BINARY_SYMBOL?" after othe nchar definitions to solve conflicts. */ | nchar fieldLength? BINARY_SYMBOL? | type = VARBINARY_SYMBOL fieldLength | type = YEAR_SYMBOL fieldLength? fieldOptions? @@ -3958,7 +3962,7 @@ createTableOption: // In the order as they appear in the server grammar. | REDUNDANT_SYMBOL | COMPACT_SYMBOL ) - /* @FIX: Make "tablRefList" optional. */ + /* @CHANGED: Made "tablRefList" optional. */ | option = UNION_SYMBOL EQUAL_OPERATOR? OPEN_PAR_SYMBOL tableRefList? CLOSE_PAR_SYMBOL | defaultCharset | defaultCollation @@ -3976,7 +3980,7 @@ createTableOption: // In the order as they appear in the server grammar. | option = STORAGE_SYMBOL (DISK_SYMBOL | MEMORY_SYMBOL) | option = CONNECTION_SYMBOL EQUAL_OPERATOR? textString | option = KEY_BLOCK_SIZE_SYMBOL EQUAL_OPERATOR? ulong_number - /* @FIX: Add missing options. */ + /* @CHANGED: Added missing options. */ | {serverVersion >= 80021}? option = START_SYMBOL TRANSACTION_SYMBOL | {serverVersion >= 80021}? option = ENGINE_ATTRIBUTE_SYMBOL EQUAL_OPERATOR? textString | {serverVersion >= 80021}? option = SECONDARY_ENGINE_ATTRIBUTE_SYMBOL EQUAL_OPERATOR? textString @@ -4115,8 +4119,8 @@ updateList: ;*/ /* - * @FIX: - * Change "EQUAL_OPERATOR" to "equal" to add support for ":=". + * @CHANGED: + * Changed "EQUAL_OPERATOR" to "equal" to add support for ":=". */ updateElement: columnRef equal (expr | DEFAULT_SYMBOL) @@ -4183,10 +4187,10 @@ createUserEntry: // create_user in sql_yacc.yy ;*/ /* - * @FIX: - * Fix "alterUserEntry": - * 1. Support also "USER()" function call. - * 2. Add support for "RANDOM PASSWORD". + * @CHANGED: + * Fixed "alterUserEntry": + * 1. Added support for "USER()" function call. + * 2. Added support for "RANDOM PASSWORD". */ alterUserEntry: // alter_user in sql_yacc.yy (userFunction | user) ( @@ -4217,8 +4221,8 @@ replacePassword: ;*/ /* - * @FIX: - * Fix "userIdentifierOrText" to support omitting sequence after "@". + * @CHANGED: + * Fixed "userIdentifierOrText" to support omitting sequence after "@". */ userIdentifierOrText: textOrIdentifier (AT_SIGN_SYMBOL textOrIdentifier? | AT_TEXT_SUFFIX)? @@ -4593,8 +4597,8 @@ nullLiteral: // In sql_yacc.cc both 'NULL' and '\N' are mapped to NULL_SYM (whic ;*/ /* - * @FIX: - * Replace "SINGLE_QUOTED_TEXT" with "textStringLiteral" to support both ' and ", as per SQL_MODE. + * @CHANGED: + * Replaced "SINGLE_QUOTED_TEXT" with "textStringLiteral" to support both ' and ", as per SQL_MODE. */ temporalLiteral: DATE_SYMBOL textStringLiteral diff --git a/health-check.php b/health-check.php index 6c64550..cf69af3 100644 --- a/health-check.php +++ b/health-check.php @@ -31,7 +31,7 @@ function sqlite_plugin_filter_debug_data( $info ) { if ( 'sqlite' === $db_engine ) { $info['wp-database']['fields']['database_version'] = array( 'label' => __( 'SQLite version', 'sqlite-database-integration' ), - 'value' => class_exists( 'SQLite3' ) ? SQLite3::version()['versionString'] : null, + 'value' => $info['wp-database']['fields']['server_version'] ?? null, ); $info['wp-database']['fields']['database_file'] = array( diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php new file mode 100644 index 0000000..c1f4222 --- /dev/null +++ b/tests/WP_SQLite_Driver_Tests.php @@ -0,0 +1,3779 @@ +suppress_errors = false; + $GLOBALS['wpdb']->show_errors = true; + } + return; + } + + // Before each test, we create a new database + public function setUp(): void { + $this->sqlite = new PDO( 'sqlite::memory:' ); + + $this->engine = new WP_SQLite_Driver( + array( + 'connection' => $this->sqlite, + 'database' => 'wp', + ) + ); + $this->engine->query( + "CREATE TABLE _options ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + option_name TEXT NOT NULL default '', + option_value TEXT NOT NULL default '' + );" + ); + $this->engine->query( + "CREATE TABLE _dates ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + option_name TEXT NOT NULL default '', + option_value DATE NOT NULL + );" + ); + } + + private function assertQuery( $sql ) { + $retval = $this->engine->query( $sql ); + $this->assertNotFalse( $retval ); + return $retval; + } + + public function testRegexp() { + $this->assertQuery( + "INSERT INTO _options (option_name, option_value) VALUES ('rss_0123456789abcdef0123456789abcdef', '1');" + ); + $this->assertQuery( + "INSERT INTO _options (option_name, option_value) VALUES ('transient', '1');" + ); + + $this->assertQuery( "DELETE FROM _options WHERE option_name REGEXP '^rss_.+$'" ); + $this->assertQuery( 'SELECT * FROM _options' ); + $this->assertCount( 1, $this->engine->get_query_results() ); + } + + /** + * @dataProvider regexpOperators + */ + public function testRegexps( $operator, $regexp, $expected_result ) { + $this->assertQuery( + "INSERT INTO _options (option_name) VALUES ('rss_123'), ('RSS_123'), ('transient');" + ); + + $this->assertQuery( "SELECT ID, option_name FROM _options WHERE option_name $operator '$regexp' ORDER BY id LIMIT 1" ); + $this->assertEquals( + array( $expected_result ), + $this->engine->get_query_results() + ); + } + + public static function regexpOperators() { + $lowercase_rss = (object) array( + 'ID' => '1', + 'option_name' => 'rss_123', + ); + $uppercase_rss = (object) array( + 'ID' => '2', + 'option_name' => 'RSS_123', + ); + $lowercase_transient = (object) array( + 'ID' => '3', + 'option_name' => 'transient', + ); + return array( + array( 'REGEXP', '^RSS_.+$', $lowercase_rss ), + array( 'RLIKE', '^RSS_.+$', $lowercase_rss ), + array( 'REGEXP BINARY', '^RSS_.+$', $uppercase_rss ), + array( 'RLIKE BINARY', '^RSS_.+$', $uppercase_rss ), + array( 'NOT REGEXP', '^RSS_.+$', $lowercase_transient ), + array( 'NOT RLIKE', '^RSS_.+$', $lowercase_transient ), + array( 'NOT REGEXP BINARY', '^RSS_.+$', $lowercase_rss ), + array( 'NOT RLIKE BINARY', '^RSS_.+$', $lowercase_rss ), + ); + } + + public function testInsertDateNow() { + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('first', now());" + ); + + $this->assertQuery( 'SELECT YEAR(option_value) as y FROM _dates' ); + + $results = $this->engine->get_query_results(); + $this->assertCount( 1, $results ); + $this->assertEquals( gmdate( 'Y' ), $results[0]->y ); + } + + public function testUpdateWithLimit() { + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 00:00:45');" + ); + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2003-05-28 00:00:45');" + ); + + $this->assertQuery( + "UPDATE _dates SET option_value = '2001-05-27 10:08:48' WHERE option_name = 'first' ORDER BY option_name LIMIT 1;" + ); + + $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first';" ); + $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second';" ); + + $this->assertEquals( '2001-05-27 10:08:48', $result1[0]->option_value ); + $this->assertEquals( '2003-05-28 00:00:45', $result2[0]->option_value ); + + $this->assertQuery( + "UPDATE _dates SET option_value = '2001-05-27 10:08:49' WHERE option_name = 'first';" + ); + $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first';" ); + $this->assertEquals( '2001-05-27 10:08:49', $result1[0]->option_value ); + + $this->assertQuery( + "UPDATE _dates SET option_value = '2001-05-12 10:00:40' WHERE option_name in ( SELECT option_name from _dates );" + ); + $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first';" ); + $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second';" ); + $this->assertEquals( '2001-05-12 10:00:40', $result1[0]->option_value ); + $this->assertEquals( '2001-05-12 10:00:40', $result2[0]->option_value ); + } + + public function testUpdateWithLimitNoEndToken() { + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 00:00:45')" + ); + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2003-05-28 00:00:45')" + ); + + $this->assertQuery( + "UPDATE _dates SET option_value = '2001-05-27 10:08:48' WHERE option_name = 'first' ORDER BY option_name LIMIT 1" + ); + $results = $this->engine->get_query_results(); + + $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" ); + $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second'" ); + + $this->assertEquals( '2001-05-27 10:08:48', $result1[0]->option_value ); + $this->assertEquals( '2003-05-28 00:00:45', $result2[0]->option_value ); + + $this->assertQuery( + "UPDATE _dates SET option_value = '2001-05-27 10:08:49' WHERE option_name = 'first'" + ); + $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" ); + $this->assertEquals( '2001-05-27 10:08:49', $result1[0]->option_value ); + + $this->assertQuery( + "UPDATE _dates SET option_value = '2001-05-12 10:00:40' WHERE option_name in ( SELECT option_name from _dates )" + ); + $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" ); + $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second'" ); + $this->assertEquals( '2001-05-12 10:00:40', $result1[0]->option_value ); + $this->assertEquals( '2001-05-12 10:00:40', $result2[0]->option_value ); + } + + public function testUpdateWithoutWhereButWithSubSelect() { + $this->assertQuery( + "INSERT INTO _options (option_name, option_value) VALUES ('User 0000019', 'second');" + ); + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48');" + ); + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2003-05-27 10:08:48');" + ); + $return = $this->assertQuery( + "UPDATE _dates SET option_value = (SELECT option_value from _options WHERE option_name = 'User 0000019')" + ); + $this->assertSame( 2, $return, 'UPDATE query did not return 2 when two row were changed' ); + + $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" ); + $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second'" ); + $this->assertEquals( 'second', $result1[0]->option_value ); + $this->assertEquals( 'second', $result2[0]->option_value ); + } + + public function testUpdateWithoutWhereButWithLimit() { + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48');" + ); + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2003-05-27 10:08:48');" + ); + $return = $this->assertQuery( + "UPDATE _dates SET option_value = 'second' LIMIT 1" + ); + $this->assertSame( 1, $return, 'UPDATE query did not return 2 when two row were changed' ); + + $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" ); + $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second'" ); + $this->assertEquals( 'second', $result1[0]->option_value ); + $this->assertEquals( '2003-05-27 10:08:48', $result2[0]->option_value ); + } + + public function testCastAsBinary() { + $this->assertQuery( + // Use a confusing alias to make sure it replaces only the correct token + "SELECT CAST('ABC' AS BINARY) as `binary`;" + ); + $results = $this->engine->get_query_results(); + $this->assertCount( 1, $results ); + $this->assertEquals( 'ABC', $results[0]->binary ); + } + + public function testSelectFromDual() { + $result = $this->assertQuery( + 'SELECT 1 as output FROM DUAL' + ); + $this->assertEquals( 1, $result[0]->output ); + } + + public function testShowCreateTableNotFound() { + $this->assertQuery( + 'SHOW CREATE TABLE _no_such_table;' + ); + $results = $this->engine->get_query_results(); + $this->assertCount( 0, $results ); + } + + public function testShowCreateTable1() { + $this->assertQuery( + "CREATE TABLE _tmp_table ( + ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL, + option_name VARCHAR(255) default '', + option_value TEXT NOT NULL, + UNIQUE KEY option_name (option_name), + KEY composite (option_name, option_value) + );" + ); + + $this->assertQuery( + 'SHOW CREATE TABLE _tmp_table;' + ); + $results = $this->engine->get_query_results(); + $this->assertEquals( + "CREATE TABLE `_tmp_table` ( + `ID` bigint NOT NULL AUTO_INCREMENT, + `option_name` varchar(255) DEFAULT '', + `option_value` text NOT NULL, + PRIMARY KEY (`ID`), + UNIQUE KEY `option_name` (`option_name`), + KEY `composite` (`option_name`, `option_value`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci", + $results[0]->{'Create Table'} + ); + } + + public function testShowCreateTableQuoted() { + $this->assertQuery( + "CREATE TABLE _tmp_table ( + ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL, + option_name VARCHAR(255) default '', + option_value TEXT NOT NULL, + UNIQUE KEY option_name (option_name), + KEY composite (option_name, option_value) + );" + ); + + $this->assertQuery( + 'SHOW CREATE TABLE `_tmp_table`;' + ); + $results = $this->engine->get_query_results(); + $this->assertEquals( + "CREATE TABLE `_tmp_table` ( + `ID` bigint NOT NULL AUTO_INCREMENT, + `option_name` varchar(255) DEFAULT '', + `option_value` text NOT NULL, + PRIMARY KEY (`ID`), + UNIQUE KEY `option_name` (`option_name`), + KEY `composite` (`option_name`, `option_value`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci", + $results[0]->{'Create Table'} + ); + } + + public function testShowCreateTableSimpleTable() { + $this->assertQuery( + 'CREATE TABLE _tmp_table ( + ID BIGINT NOT NULL + );' + ); + + $this->assertQuery( + 'SHOW CREATE TABLE _tmp_table;' + ); + $results = $this->engine->get_query_results(); + $this->assertEquals( + 'CREATE TABLE `_tmp_table` ( + `ID` bigint NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci', + $results[0]->{'Create Table'} + ); + } + + public function testShowCreateTableWithAlterAndCreateIndex() { + $this->assertQuery( + "CREATE TABLE _tmp_table ( + ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL, + option_name VARCHAR(255) default '', + option_value TEXT NOT NULL + );" + ); + + $this->assertQuery( + 'ALTER TABLE _tmp_table CHANGE COLUMN option_name option_name SMALLINT NOT NULL default 14' + ); + + $this->assertQuery( + 'ALTER TABLE _tmp_table ADD INDEX option_name (option_name);' + ); + + $this->assertQuery( + 'SHOW CREATE TABLE _tmp_table;' + ); + $results = $this->engine->get_query_results(); + $this->assertEquals( + 'CREATE TABLE `_tmp_table` ( + `ID` bigint NOT NULL AUTO_INCREMENT, + `option_name` smallint NOT NULL DEFAULT \'14\', + `option_value` text NOT NULL, + PRIMARY KEY (`ID`), + KEY `option_name` (`option_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci', + $results[0]->{'Create Table'} + ); + } + + public function testCreateTablesWithIdenticalIndexNames() { + $this->assertQuery( + "CREATE TABLE _tmp_table_a ( + ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL, + option_name VARCHAR(255) default '', + option_value TEXT NOT NULL, + KEY `option_name` (`option_name`), + KEY `double__underscores` (`option_name`, `ID`) + );" + ); + + $this->assertQuery( + "CREATE TABLE _tmp_table_b ( + ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL, + option_name VARCHAR(255) default '', + option_value TEXT NOT NULL, + KEY `option_name` (`option_name`), + KEY `double__underscores` (`option_name`, `ID`) + );" + ); + } + + public function testShowCreateTablePreservesDoubleUnderscoreKeyNames() { + $this->assertQuery( + "CREATE TABLE _tmp__table ( + ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL, + option_name VARCHAR(255) default '', + option_value TEXT NOT NULL, + KEY `option_name` (`option_name`), + KEY `double__underscores` (`option_name`, `ID`) + );" + ); + + $this->assertQuery( + 'SHOW CREATE TABLE _tmp__table;' + ); + $results = $this->engine->get_query_results(); + $this->assertEquals( + 'CREATE TABLE `_tmp__table` ( + `ID` bigint NOT NULL AUTO_INCREMENT, + `option_name` varchar(255) DEFAULT \'\', + `option_value` text NOT NULL, + PRIMARY KEY (`ID`), + KEY `option_name` (`option_name`), + KEY `double__underscores` (`option_name`, `ID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci', + $results[0]->{'Create Table'} + ); + } + + public function testShowCreateTableWithPrimaryKeyColumnsReverseOrdered() { + $this->assertQuery( + 'CREATE TABLE `_tmp_table` ( + `ID_A` BIGINT NOT NULL, + `ID_B` BIGINT NOT NULL, + `ID_C` BIGINT NOT NULL, + PRIMARY KEY (`ID_B`, `ID_A`, `ID_C`) + );' + ); + + $this->assertQuery( + 'SHOW CREATE TABLE _tmp_table;' + ); + $results = $this->engine->get_query_results(); + $this->assertEquals( + 'CREATE TABLE `_tmp_table` ( + `ID_A` bigint NOT NULL, + `ID_B` bigint NOT NULL, + `ID_C` bigint NOT NULL, + PRIMARY KEY (`ID_B`, `ID_A`, `ID_C`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci', + $results[0]->{'Create Table'} + ); + } + + public function testShowCreateTableWithColumnKeys() { + $this->assertQuery( + "CREATE TABLE _tmp_table ( + `ID` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL, + `option_name` varchar(255) DEFAULT '', + `option_value` text NOT NULL DEFAULT '', + KEY _tmp_table__composite (option_name, option_value), + UNIQUE KEY _tmp_table__option_name (option_name) );" + ); + } + + public function testShowCreateTableWithCorrectDefaultValues() { + $this->assertQuery( + "CREATE TABLE _tmp__table ( + ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL, + default_empty_string VARCHAR(255) default '', + null_no_default VARCHAR(255) + );" + ); + + $this->assertQuery( + 'SHOW CREATE TABLE _tmp__table;' + ); + $results = $this->engine->get_query_results(); + $this->assertEquals( + 'CREATE TABLE `_tmp__table` ( + `ID` bigint NOT NULL AUTO_INCREMENT, + `default_empty_string` varchar(255) DEFAULT \'\', + `null_no_default` varchar(255) DEFAULT NULL, + PRIMARY KEY (`ID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci', + $results[0]->{'Create Table'} + ); + } + + public function testSelectIndexHintForce() { + $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('first');" ); + $result = $this->assertQuery( + 'SELECT 1 as output FROM _options FORCE INDEX (PRIMARY, post_parent) WHERE 1=1' + ); + $this->assertEquals( 1, $result[0]->output ); + } + + public function testSelectIndexHintUseGroup() { + $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('first');" ); + $result = $this->assertQuery( + 'SELECT 1 as output FROM _options USE KEY FOR GROUP BY (PRIMARY, post_parent) WHERE 1=1' + ); + $this->assertEquals( 1, $result[0]->output ); + } + + public function testDateAddFunction() { + // second + $result = $this->assertQuery( + 'SELECT DATE_ADD("2008-01-02 13:29:17", INTERVAL 1 SECOND) as output' + ); + $this->assertEquals( '2008-01-02 13:29:18', $result[0]->output ); + + // minute + $result = $this->assertQuery( + 'SELECT DATE_ADD("2008-01-02 13:29:17", INTERVAL 1 MINUTE) as output' + ); + $this->assertEquals( '2008-01-02 13:30:17', $result[0]->output ); + + // hour + $result = $this->assertQuery( + 'SELECT DATE_ADD("2008-01-02 13:29:17", INTERVAL 1 HOUR) as output' + ); + $this->assertEquals( '2008-01-02 14:29:17', $result[0]->output ); + + // day + $result = $this->assertQuery( + 'SELECT DATE_ADD("2008-01-02 13:29:17", INTERVAL 1 DAY) as output' + ); + $this->assertEquals( '2008-01-03 13:29:17', $result[0]->output ); + + // week + $result = $this->assertQuery( + 'SELECT DATE_ADD("2008-01-02 13:29:17", INTERVAL 1 WEEK) as output' + ); + $this->assertEquals( '2008-01-09 13:29:17', $result[0]->output ); + + // month + $result = $this->assertQuery( + 'SELECT DATE_ADD("2008-01-02 13:29:17", INTERVAL 1 MONTH) as output' + ); + $this->assertEquals( '2008-02-02 13:29:17', $result[0]->output ); + + // year + $result = $this->assertQuery( + 'SELECT DATE_ADD("2008-01-02 13:29:17", INTERVAL 1 YEAR) as output' + ); + $this->assertEquals( '2009-01-02 13:29:17', $result[0]->output ); + } + + public function testDateSubFunction() { + // second + $result = $this->assertQuery( + 'SELECT DATE_SUB("2008-01-02 13:29:17", INTERVAL 1 SECOND) as output' + ); + $this->assertEquals( '2008-01-02 13:29:16', $result[0]->output ); + + // minute + $result = $this->assertQuery( + 'SELECT DATE_SUB("2008-01-02 13:29:17", INTERVAL 1 MINUTE) as output' + ); + $this->assertEquals( '2008-01-02 13:28:17', $result[0]->output ); + + // hour + $result = $this->assertQuery( + 'SELECT DATE_SUB("2008-01-02 13:29:17", INTERVAL 1 HOUR) as output' + ); + $this->assertEquals( '2008-01-02 12:29:17', $result[0]->output ); + + // day + $result = $this->assertQuery( + 'SELECT DATE_SUB("2008-01-02 13:29:17", INTERVAL 1 DAY) as output' + ); + $this->assertEquals( '2008-01-01 13:29:17', $result[0]->output ); + + // week + $result = $this->assertQuery( + 'SELECT DATE_SUB("2008-01-02 13:29:17", INTERVAL 1 WEEK) as output' + ); + $this->assertEquals( '2007-12-26 13:29:17', $result[0]->output ); + + // month + $result = $this->assertQuery( + 'SELECT DATE_SUB("2008-01-02 13:29:17", INTERVAL 1 MONTH) as output' + ); + $this->assertEquals( '2007-12-02 13:29:17', $result[0]->output ); + + // year + $result = $this->assertQuery( + 'SELECT DATE_SUB("2008-01-02 13:29:17", INTERVAL 1 YEAR) as output' + ); + $this->assertEquals( '2007-01-02 13:29:17', $result[0]->output ); + } + + public function testLeftFunction1Char() { + $result = $this->assertQuery( + 'SELECT LEFT("abc", 1) as output' + ); + $this->assertEquals( 'a', $result[0]->output ); + } + + public function testLeftFunction5Chars() { + $result = $this->assertQuery( + 'SELECT LEFT("Lorem ipsum", 5) as output' + ); + $this->assertEquals( 'Lorem', $result[0]->output ); + } + + public function testLeftFunctionNullString() { + $result = $this->assertQuery( + 'SELECT LEFT(NULL, 5) as output' + ); + $this->assertEquals( null, $result[0]->output ); + } + + public function testLeftFunctionNullLength() { + $result = $this->assertQuery( + 'SELECT LEFT("Test", NULL) as output' + ); + $this->assertEquals( null, $result[0]->output ); + } + + public function testInsertSelectFromDual() { + $result = $this->assertQuery( + 'INSERT INTO _options (option_name, option_value) SELECT "A", "b" FROM DUAL WHERE ( SELECT NULL FROM DUAL ) IS NULL' + ); + $this->assertEquals( 1, $result ); + } + + public function testCreateTemporaryTable() { + $this->assertQuery( + "CREATE TEMPORARY TABLE _tmp_table ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + option_name TEXT NOT NULL default '', + option_value TEXT NOT NULL default '' + );" + ); + $this->assertQuery( + 'DROP TEMPORARY TABLE _tmp_table;' + ); + } + + public function testShowTablesLike() { + $this->assertQuery( + "CREATE TABLE _tmp_table ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + option_name TEXT NOT NULL default '', + option_value TEXT NOT NULL default '' + );" + ); + $this->assertQuery( + "CREATE TABLE _tmp_table_2 ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + option_name TEXT NOT NULL default '', + option_value TEXT NOT NULL default '' + );" + ); + + $this->assertQuery( + "SHOW TABLES LIKE '_tmp_table';" + ); + $this->assertEquals( + array( + (object) array( + 'Tables_in_wp' => '_tmp_table', + ), + ), + $this->engine->get_query_results() + ); + + $this->assertQuery( + "SHOW FULL TABLES LIKE '_tmp_table';" + ); + $this->assertEquals( + array( + (object) array( + 'Tables_in_wp' => '_tmp_table', + 'Table_type' => 'BASE TABLE', + ), + ), + $this->engine->get_query_results() + ); + } + + public function testShowTableStatusFrom() { + // Created in setUp() function + $this->assertQuery( 'DROP TABLE _options' ); + $this->assertQuery( 'DROP TABLE _dates' ); + + $this->assertQuery( + "CREATE TABLE _tmp_table ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + option_name TEXT NOT NULL default '', + option_value TEXT NOT NULL default '' + );" + ); + + $this->assertQuery( + 'SHOW TABLE STATUS FROM wp;' + ); + + $this->assertCount( + 1, + $this->engine->get_query_results() + ); + } + + public function testShowTableStatusIn() { + // Created in setUp() function + $this->assertQuery( 'DROP TABLE _options' ); + $this->assertQuery( 'DROP TABLE _dates' ); + + $this->assertQuery( + "CREATE TABLE _tmp_table ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + option_name TEXT NOT NULL default '', + option_value TEXT NOT NULL default '' + );" + ); + + $this->assertQuery( + 'SHOW TABLE STATUS IN wp;' + ); + + $this->assertCount( + 1, + $this->engine->get_query_results() + ); + } + + public function testShowTableStatusInTwoTables() { + // Created in setUp() function + $this->assertQuery( 'DROP TABLE _options' ); + $this->assertQuery( 'DROP TABLE _dates' ); + + $this->assertQuery( + "CREATE TABLE _tmp_table ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + option_name TEXT NOT NULL default '', + option_value TEXT NOT NULL default '' + );" + ); + + $this->assertQuery( + "CREATE TABLE _tmp_table2 ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + option_name TEXT NOT NULL default '', + option_value TEXT NOT NULL default '' + );" + ); + $this->assertQuery( + 'SHOW TABLE STATUS IN wp;' + ); + + $this->assertCount( + 2, + $this->engine->get_query_results() + ); + } + + public function testShowTableStatusLike() { + // Created in setUp() function + $this->assertQuery( 'DROP TABLE _options' ); + $this->assertQuery( 'DROP TABLE _dates' ); + + $this->assertQuery( + "CREATE TABLE _tmp_table1 ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + option_name TEXT NOT NULL default '', + option_value TEXT NOT NULL default '' + );" + ); + + $this->assertQuery( + "CREATE TABLE _tmp_table2 ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + option_name TEXT NOT NULL default '', + option_value TEXT NOT NULL default '' + );" + ); + + $this->assertQuery( + "CREATE TABLE _another_table ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + option_name TEXT NOT NULL default '', + option_value TEXT NOT NULL default '' + );" + ); + + $this->assertQuery( + "SHOW TABLE STATUS LIKE '_tmp_table%';" + ); + $this->assertCount( + 2, + $this->engine->get_query_results() + ); + $this->assertEquals( + '_tmp_table1', + $this->engine->get_query_results()[0]->Name + ); + } + + public function testShowTableStatusWhere() { + // Created in setUp() function + $this->assertQuery( 'DROP TABLE _options' ); + $this->assertQuery( 'DROP TABLE _dates' ); + + $this->assertQuery( + "CREATE TABLE _tmp_table1 ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + option_name TEXT NOT NULL default '', + option_value TEXT NOT NULL default '' + );" + ); + + $this->assertQuery( + "CREATE TABLE _tmp_table2 ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + option_name TEXT NOT NULL default '', + option_value TEXT NOT NULL default '' + );" + ); + + $this->assertQuery( + "SHOW TABLE STATUS WHERE SUBSTR(table_name, 11, 1) = '1'" + ); + $this->assertCount( + 1, + $this->engine->get_query_results() + ); + $this->assertEquals( + '_tmp_table1', + $this->engine->get_query_results()[0]->Name + ); + } + + public function testCreateTable() { + $result = $this->assertQuery( + "CREATE TABLE wptests_users ( + ID bigint(20) unsigned NOT NULL auto_increment, + user_login varchar(60) NOT NULL default '', + user_pass varchar(255) NOT NULL default '', + user_nicename varchar(50) NOT NULL default '', + user_email varchar(100) NOT NULL default '', + user_url varchar(100) NOT NULL default '', + user_registered datetime NOT NULL default '0000-00-00 00:00:00', + user_activation_key varchar(255) NOT NULL default '', + user_status int(11) NOT NULL default '0', + display_name varchar(250) NOT NULL default '', + PRIMARY KEY (ID), + KEY user_login_key (user_login), + KEY user_nicename (user_nicename), + KEY user_email (user_email) + ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci" + ); + $this->assertNull( $result ); + + $this->assertQuery( 'DESCRIBE wptests_users;' ); + $results = $this->engine->get_query_results(); + $this->assertEquals( + array( + (object) array( + 'Field' => 'ID', + 'Type' => 'bigint(20) unsigned', + 'Null' => 'NO', + 'Key' => 'PRI', + 'Default' => null, + 'Extra' => 'auto_increment', + ), + (object) array( + 'Field' => 'user_login', + 'Type' => 'varchar(60)', + 'Null' => 'NO', + 'Key' => 'MUL', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'user_pass', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'user_nicename', + 'Type' => 'varchar(50)', + 'Null' => 'NO', + 'Key' => 'MUL', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'user_email', + 'Type' => 'varchar(100)', + 'Null' => 'NO', + 'Key' => 'MUL', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'user_url', + 'Type' => 'varchar(100)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'user_registered', + 'Type' => 'datetime', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '0000-00-00 00:00:00', + 'Extra' => '', + ), + (object) array( + 'Field' => 'user_activation_key', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'user_status', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '0', + 'Extra' => '', + ), + (object) array( + 'Field' => 'display_name', + 'Type' => 'varchar(250)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + ), + $results + ); + } + + public function testCreateTableWithTrailingComma() { + $result = $this->assertQuery( + 'CREATE TABLE wptests_users ( + ID bigint(20) unsigned NOT NULL auto_increment, + PRIMARY KEY (ID) + ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci' + ); + $this->assertNull( $result ); + } + + public function testCreateTableSpatialIndex() { + $result = $this->assertQuery( + 'CREATE TABLE wptests_users ( + ID bigint(20) unsigned NOT NULL auto_increment, + UNIQUE KEY (ID) + )' + ); + $this->assertNull( $result ); + } + + public function testCreateTableWithMultiValueColumnTypeModifiers() { + $result = $this->assertQuery( + "CREATE TABLE wptests_users ( + ID bigint(20) unsigned NOT NULL auto_increment, + decimal_column DECIMAL(10,2) NOT NULL DEFAULT 0, + float_column FLOAT(10,2) NOT NULL DEFAULT 0, + enum_column ENUM('a', 'b', 'c') NOT NULL DEFAULT 'a', + PRIMARY KEY (ID) + )" + ); + $this->assertNull( $result ); + + $this->assertQuery( 'DESCRIBE wptests_users;' ); + $results = $this->engine->get_query_results(); + $this->assertEquals( + array( + (object) array( + 'Field' => 'ID', + 'Type' => 'bigint(20) unsigned', + 'Null' => 'NO', + 'Key' => 'PRI', + 'Default' => null, + 'Extra' => 'auto_increment', + ), + (object) array( + 'Field' => 'decimal_column', + 'Type' => 'decimal(10,2)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => 0, + 'Extra' => '', + ), + (object) array( + 'Field' => 'float_column', + 'Type' => 'float(10,2)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => 0, + 'Extra' => '', + ), + (object) array( + 'Field' => 'enum_column', + 'Type' => "enum('a','b','c')", + 'Null' => 'NO', + 'Key' => '', + 'Default' => 'a', + 'Extra' => '', + ), + ), + $results + ); + } + + public function testAlterTableAddAndDropColumn() { + $result = $this->assertQuery( + "CREATE TABLE _tmp_table ( + name varchar(20) NOT NULL default '' + );" + ); + $this->assertNull( $result ); + + $result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD COLUMN `column` int;' ); + $this->assertNull( $result ); + + $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $results = $this->engine->get_query_results(); + $this->assertEquals( + array( + (object) array( + 'Field' => 'name', + 'Type' => 'varchar(20)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'column', + 'Type' => 'int', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + ), + $results + ); + + $result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD `column2` int;' ); + $this->assertNull( $result ); + + $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $results = $this->engine->get_query_results(); + $this->assertEquals( + array( + (object) array( + 'Field' => 'name', + 'Type' => 'varchar(20)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'column', + 'Type' => 'int', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'column2', + 'Type' => 'int', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + ), + $results + ); + + $result = $this->assertQuery( 'ALTER TABLE _tmp_table DROP COLUMN `column`;' ); + $this->assertNull( $result ); + + $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $results = $this->engine->get_query_results(); + $this->assertEquals( + array( + (object) array( + 'Field' => 'name', + 'Type' => 'varchar(20)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'column2', + 'Type' => 'int', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + ), + $results + ); + + $result = $this->assertQuery( 'ALTER TABLE _tmp_table DROP `column2`;' ); + $this->assertNull( $result ); + + $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $results = $this->engine->get_query_results(); + $this->assertEquals( + array( + (object) array( + 'Field' => 'name', + 'Type' => 'varchar(20)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + ), + $results + ); + } + + public function testAlterTableAddNotNullVarcharColumn() { + $result = $this->assertQuery( + "CREATE TABLE _tmp_table ( + name varchar(20) NOT NULL default '' + );" + ); + + $result = $this->assertQuery( "ALTER TABLE _tmp_table ADD COLUMN `column` VARCHAR(20) NOT NULL DEFAULT 'foo';" ); + $this->assertNull( $result ); + + $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $results = $this->engine->get_query_results(); + $this->assertEquals( + array( + (object) array( + 'Field' => 'name', + 'Type' => 'varchar(20)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'column', + 'Type' => 'varchar(20)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => 'foo', + 'Extra' => '', + ), + ), + $results + ); + } + + public function testColumnWithOnUpdate() { + // CREATE TABLE with ON UPDATE + $this->assertQuery( + 'CREATE TABLE _tmp_table ( + id int(11) NOT NULL, + created_at timestamp NULL ON UPDATE CURRENT_TIMESTAMP + );' + ); + $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'created_at', + 'Type' => 'timestamp', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => 'on update CURRENT_TIMESTAMP', + ), + ), + $results + ); + + // ADD COLUMN with ON UPDATE + $this->assertQuery( + 'ALTER TABLE _tmp_table ADD COLUMN updated_at timestamp NULL ON UPDATE CURRENT_TIMESTAMP' + ); + $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'created_at', + 'Type' => 'timestamp', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => 'on update CURRENT_TIMESTAMP', + ), + (object) array( + 'Field' => 'updated_at', + 'Type' => 'timestamp', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => 'on update CURRENT_TIMESTAMP', + ), + ), + $results + ); + + // assert ON UPDATE triggers + $results = $this->assertQuery( "SELECT * FROM sqlite_master WHERE type = 'trigger'" ); + $this->assertEquals( + array( + (object) array( + 'type' => 'trigger', + 'name' => '_wp_sqlite__tmp_table_created_at_on_update', + 'tbl_name' => '_tmp_table', + 'rootpage' => '0', + 'sql' => "CREATE TRIGGER \"_wp_sqlite__tmp_table_created_at_on_update\"\n\t\t\tAFTER UPDATE ON \"_tmp_table\"\n\t\t\tFOR EACH ROW\n\t\t\tBEGIN\n\t\t\t UPDATE \"_tmp_table\" SET \"created_at\" = CURRENT_TIMESTAMP WHERE rowid = NEW.rowid;\n\t\t\tEND", + ), + (object) array( + 'type' => 'trigger', + 'name' => '_wp_sqlite__tmp_table_updated_at_on_update', + 'tbl_name' => '_tmp_table', + 'rootpage' => '0', + 'sql' => "CREATE TRIGGER \"_wp_sqlite__tmp_table_updated_at_on_update\"\n\t\t\tAFTER UPDATE ON \"_tmp_table\"\n\t\t\tFOR EACH ROW\n\t\t\tBEGIN\n\t\t\t UPDATE \"_tmp_table\" SET \"updated_at\" = CURRENT_TIMESTAMP WHERE rowid = NEW.rowid;\n\t\t\tEND", + ), + ), + $results + ); + + // on INSERT, no timestamps are expected + $this->assertQuery( 'INSERT INTO _tmp_table (id) VALUES (1)' ); + $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 1' ); + $this->assertNull( $result[0]->created_at ); + $this->assertNull( $result[0]->updated_at ); + + // on UPDATE, we expect timestamps in form YYYY-MM-DD HH:MM:SS + $this->assertQuery( 'UPDATE _tmp_table SET id = 2 WHERE id = 1' ); + $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 2' ); + $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->created_at ); + $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->updated_at ); + + // drop ON UPDATE + $this->assertQuery( + 'ALTER TABLE _tmp_table + CHANGE created_at created_at timestamp NULL, + CHANGE COLUMN updated_at updated_at timestamp NULL' + ); + $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'created_at', + 'Type' => 'timestamp', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'updated_at', + 'Type' => 'timestamp', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + ), + $results + ); + + // assert ON UPDATE triggers are removed + $results = $this->assertQuery( "SELECT * FROM sqlite_master WHERE type = 'trigger'" ); + $this->assertEquals( array(), $results ); + + // now, no timestamps are expected + $this->assertQuery( 'INSERT INTO _tmp_table (id) VALUES (10)' ); + $this->assertQuery( 'UPDATE _tmp_table SET id = 11 WHERE id = 10' ); + $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 11' ); + $this->assertNull( $result[0]->created_at ); + $this->assertNull( $result[0]->updated_at ); + } + + public function testColumnWithOnUpdateAndNoIdField() { + // CREATE TABLE with ON UPDATE + $this->assertQuery( + 'CREATE TABLE _tmp_table ( + name varchar(20) NOT NULL, + created_at timestamp NULL ON UPDATE CURRENT_TIMESTAMP + );' + ); + + // on INSERT, no timestamps are expected + $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('aaa')" ); + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name = 'aaa'" ); + $this->assertNull( $result[0]->created_at ); + + // on UPDATE, we expect timestamps in form YYYY-MM-DD HH:MM:SS + $this->assertQuery( "UPDATE _tmp_table SET name = 'bbb' WHERE name = 'aaa'" ); + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name = 'bbb'" ); + $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->created_at ); + } + + public function testChangeColumnWithOnUpdate() { + // CREATE TABLE with ON UPDATE + $this->assertQuery( + 'CREATE TABLE _tmp_table ( + id int(11) NOT NULL, + created_at timestamp NULL + );' + ); + $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'created_at', + 'Type' => 'timestamp', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + ), + $results + ); + + // no ON UPDATE is set + $this->assertQuery( 'INSERT INTO _tmp_table (id) VALUES (1)' ); + $this->assertQuery( 'UPDATE _tmp_table SET id = 1 WHERE id = 1' ); + $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 1' ); + $this->assertNull( $result[0]->created_at ); + + // CHANGE COLUMN to add ON UPDATE + $this->assertQuery( + 'ALTER TABLE _tmp_table CHANGE COLUMN created_at created_at timestamp NULL ON UPDATE CURRENT_TIMESTAMP' + ); + $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'created_at', + 'Type' => 'timestamp', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => 'on update CURRENT_TIMESTAMP', + ), + ), + $results + ); + + // now, ON UPDATE SHOULD BE SET + $this->assertQuery( 'UPDATE _tmp_table SET id = 1 WHERE id = 1' ); + $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 1' ); + $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->created_at ); + + // change column to remove ON UPDATE + $this->assertQuery( + 'ALTER TABLE _tmp_table CHANGE COLUMN created_at created_at timestamp NULL' + ); + $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'created_at', + 'Type' => 'timestamp', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + ), + $results + ); + + // now, no timestamp is expected + $this->assertQuery( 'INSERT INTO _tmp_table (id) VALUES (2)' ); + $this->assertQuery( 'UPDATE _tmp_table SET id = 2 WHERE id = 2' ); + $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 2' ); + $this->assertNull( $result[0]->created_at ); + } + + public function testAlterTableWithColumnFirstAndAfter() { + $this->assertQuery( + "CREATE TABLE _tmp_table ( + id int(11) NOT NULL, + name varchar(20) NOT NULL default '' + );" + ); + + // ADD COLUMN with FIRST + $this->assertQuery( + "ALTER TABLE _tmp_table ADD COLUMN new_first_column VARCHAR(255) NOT NULL DEFAULT '' FIRST" + ); + $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'name', + 'Type' => 'varchar(20)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'new_first_column', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + ), + $results + ); + + // ADD COLUMN with AFTER + $this->assertQuery( + "ALTER TABLE _tmp_table ADD COLUMN new_column VARCHAR(255) NOT NULL DEFAULT '' AFTER id" + ); + $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'name', + 'Type' => 'varchar(20)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'new_first_column', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'new_column', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + ), + $results + ); + + // CHANGE with FIRST + $this->assertQuery( + "ALTER TABLE _tmp_table CHANGE id id int(11) NOT NULL DEFAULT '0' FIRST" + ); + $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '0', + 'Extra' => '', + ), + (object) array( + 'Field' => 'name', + 'Type' => 'varchar(20)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'new_first_column', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'new_column', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + ), + $results + ); + + // CHANGE with AFTER + $this->assertQuery( + "ALTER TABLE _tmp_table CHANGE id id int(11) NOT NULL DEFAULT '0' AFTER name" + ); + $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '0', + 'Extra' => '', + ), + (object) array( + 'Field' => 'name', + 'Type' => 'varchar(20)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'new_first_column', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'new_column', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + ), + $results + ); + } + + public function testAlterTableWithMultiColumnFirstAndAfter() { + $this->assertQuery( + 'CREATE TABLE _tmp_table ( + id int(11) NOT NULL + );' + ); + + // ADD COLUMN + $this->assertQuery( + 'ALTER TABLE _tmp_table + ADD COLUMN new1 varchar(255) NOT NULL, + ADD COLUMN new2 varchar(255) NOT NULL FIRST, + ADD COLUMN new3 varchar(255) NOT NULL AFTER new1' + ); + $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'new1', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'new2', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'new3', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + ), + $results + ); + + // CHANGE + $this->assertQuery( + 'ALTER TABLE _tmp_table + CHANGE new1 new1 int(11) NOT NULL FIRST, + CHANGE new2 new2 int(11) NOT NULL, + CHANGE new3 new3 int(11) NOT NULL AFTER new2' + ); + $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'new1', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'new2', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'new3', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + ), + $results + ); + } + + public function testAlterTableAddIndex() { + $result = $this->assertQuery( + "CREATE TABLE _tmp_table ( + name varchar(20) NOT NULL default '' + );" + ); + + $result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD INDEX name (name);' ); + $this->assertNull( $result ); + + $this->assertQuery( 'SHOW INDEX FROM _tmp_table;' ); + $results = $this->engine->get_query_results(); + $this->assertEquals( + array( + (object) array( + 'Table' => '_tmp_table', + 'Non_unique' => '1', + 'Key_name' => 'name', + 'Seq_in_index' => '1', + 'Column_name' => 'name', + 'Collation' => 'A', + 'Cardinality' => '0', + 'Sub_part' => null, + 'Packed' => null, + 'Null' => '', + 'Index_type' => 'BTREE', + 'Comment' => '', + 'Index_comment' => '', + 'Visible' => 'YES', + 'Expression' => null, + ), + ), + $results + ); + } + + public function testAlterTableAddUniqueIndex() { + $result = $this->assertQuery( + "CREATE TABLE _tmp_table ( + name varchar(20) NOT NULL default '' + );" + ); + + $result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD UNIQUE INDEX name (name(20));' ); + $this->assertNull( $result ); + + $this->assertQuery( 'SHOW INDEX FROM _tmp_table;' ); + $results = $this->engine->get_query_results(); + $this->assertEquals( + array( + (object) array( + 'Table' => '_tmp_table', + 'Non_unique' => '0', + 'Key_name' => 'name', + 'Seq_in_index' => '1', + 'Column_name' => 'name', + 'Collation' => 'A', + 'Cardinality' => '0', + 'Sub_part' => '20', + 'Packed' => null, + 'Null' => '', + 'Index_type' => 'BTREE', + 'Comment' => '', + 'Index_comment' => '', + 'Visible' => 'YES', + 'Expression' => null, + ), + ), + $results + ); + } + + public function testAlterTableAddFulltextIndex() { + $result = $this->assertQuery( + "CREATE TABLE _tmp_table ( + name varchar(20) NOT NULL default '' + );" + ); + + $result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD FULLTEXT INDEX name (name);' ); + $this->assertNull( $result ); + + $this->assertQuery( 'SHOW INDEX FROM _tmp_table;' ); + $results = $this->engine->get_query_results(); + $this->assertEquals( + array( + (object) array( + 'Table' => '_tmp_table', + 'Non_unique' => '1', + 'Key_name' => 'name', + 'Seq_in_index' => '1', + 'Column_name' => 'name', + 'Collation' => null, + 'Cardinality' => '0', + 'Sub_part' => null, + 'Packed' => null, + 'Null' => '', + 'Index_type' => 'FULLTEXT', + 'Comment' => '', + 'Index_comment' => '', + 'Visible' => 'YES', + 'Expression' => null, + ), + ), + $results + ); + } + + public function testAlterTableModifyColumn() { + $this->assertQuery( + "CREATE TABLE _tmp_table ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + name varchar(20) NOT NULL default '', + lastname varchar(20) NOT NULL default '', + KEY composite (name, lastname), + UNIQUE KEY name (name) + );" + ); + // Insert a record + $result = $this->assertQuery( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (1, 'Johnny', 'Appleseed');" ); + $this->assertEquals( 1, $result ); + + // Primary key violation: + $error = ''; + try { + $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (1, 'Mike', 'Pearseed')" ); + } catch ( Throwable $e ) { + $error = $e->getMessage(); + } + $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.ID', $error ); + + // Unique constraint violation: + $error = ''; + try { + $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (2, 'Johnny', 'Appleseed')" ); + } catch ( Throwable $e ) { + $error = $e->getMessage(); + } + $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.name', $error ); + + // Rename the "name" field to "firstname": + $result = $this->engine->query( "ALTER TABLE _tmp_table CHANGE column name firstname varchar(50) NOT NULL default 'mark';" ); + $this->assertNull( $result ); + + // Confirm the original data is still there: + $result = $this->engine->query( 'SELECT * FROM _tmp_table;' ); + $this->assertCount( 1, $result ); + $this->assertEquals( 1, $result[0]->ID ); + $this->assertEquals( 'Johnny', $result[0]->firstname ); + $this->assertEquals( 'Appleseed', $result[0]->lastname ); + + // Confirm the primary key is intact: + $error = ''; + try { + $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname) VALUES (1, 'Mike', 'Pearseed')" ); + } catch ( Throwable $e ) { + $error = $e->getMessage(); + } + $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.ID', $error ); + + // Confirm the unique key is intact: + $error = ''; + try { + $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname) VALUES (2, 'Johnny', 'Appleseed')" ); + } catch ( Throwable $e ) { + $error = $e->getMessage(); + } + $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.firstname', $error ); + + // Confirm the autoincrement still works: + $result = $this->engine->query( "INSERT INTO _tmp_table (firstname, lastname) VALUES ('John', 'Doe');" ); + $this->assertEquals( true, $result ); + $result = $this->engine->query( "SELECT * FROM _tmp_table WHERE firstname='John';" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 2, $result[0]->ID ); + } + + + public function testAlterTableModifyColumnWithSkippedColumnKeyword() { + $this->assertQuery( + "CREATE TABLE _tmp_table ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + name varchar(20) NOT NULL default '', + lastname varchar(20) NOT NULL default '', + KEY composite (name, lastname), + UNIQUE KEY name (name) + );" + ); + // Insert a record + $result = $this->assertQuery( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (1, 'Johnny', 'Appleseed');" ); + $this->assertEquals( 1, $result ); + + // Primary key violation: + $error = ''; + try { + $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (1, 'Mike', 'Pearseed')" ); + } catch ( Throwable $e ) { + $error = $e->getMessage(); + } + $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.ID', $error ); + + // Unique constraint violation: + $error = ''; + try { + $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (2, 'Johnny', 'Appleseed')" ); + } catch ( Throwable $e ) { + $error = $e->getMessage(); + } + $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.name', $error ); + + // Rename the "name" field to "firstname": + $result = $this->engine->query( "ALTER TABLE _tmp_table CHANGE name firstname varchar(50) NOT NULL default 'mark';" ); + $this->assertNull( $result ); + + // Confirm the original data is still there: + $result = $this->engine->query( 'SELECT * FROM _tmp_table;' ); + $this->assertCount( 1, $result ); + $this->assertEquals( 1, $result[0]->ID ); + $this->assertEquals( 'Johnny', $result[0]->firstname ); + $this->assertEquals( 'Appleseed', $result[0]->lastname ); + + // Confirm the primary key is intact: + $error = ''; + try { + $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname) VALUES (1, 'Mike', 'Pearseed')" ); + } catch ( Throwable $e ) { + $error = $e->getMessage(); + } + $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.ID', $error ); + + // Confirm the unique key is intact: + $error = ''; + try { + $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname) VALUES (2, 'Johnny', 'Appleseed')" ); + } catch ( Throwable $e ) { + $error = $e->getMessage(); + } + $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.firstname', $error ); + + // Confirm the autoincrement still works: + $result = $this->engine->query( "INSERT INTO _tmp_table (firstname, lastname) VALUES ('John', 'Doe');" ); + $this->assertEquals( true, $result ); + $result = $this->engine->query( "SELECT * FROM _tmp_table WHERE firstname='John';" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 2, $result[0]->ID ); + } + + public function testAlterTableModifyColumnWithHyphens() { + $result = $this->assertQuery( + 'CREATE TABLE wptests_dbdelta_test2 ( + `foo-bar` varchar(255) DEFAULT NULL + )' + ); + $this->assertNull( $result ); + + $result = $this->assertQuery( + 'ALTER TABLE wptests_dbdelta_test2 CHANGE COLUMN `foo-bar` `foo-bar` text DEFAULT NULL' + ); + $this->assertNull( $result ); + + $result = $this->assertQuery( 'DESCRIBE wptests_dbdelta_test2;' ); + $this->assertNotFalse( $result ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'foo-bar', + 'Type' => 'text', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + ), + $result + ); + } + + public function testAlterTableModifyColumnComplexChange() { + $result = $this->assertQuery( + "CREATE TABLE _tmp_table ( + ID INTEGER NOT NULL, + name varchar(20) NOT NULL default '', + lastname varchar(20) default '', + date_as_string varchar(20) default '', + PRIMARY KEY (ID, name) + );" + ); + $this->assertNull( $result ); + + // Add a unique index + $result = $this->assertQuery( + 'ALTER TABLE _tmp_table ADD UNIQUE INDEX "test_unique_composite" (name, lastname);' + ); + $this->assertNull( $result ); + + // Add a regular index + $result = $this->assertQuery( + 'ALTER TABLE _tmp_table ADD INDEX "test_regular" (lastname);' + ); + $this->assertNull( $result ); + + // Confirm the table is well-behaved so far: + + // Insert a few records + $result = $this->assertQuery( + " + INSERT INTO _tmp_table (ID, name, lastname, date_as_string) + VALUES + (1, 'Johnny', 'Appleseed', '2002-01-01 12:53:13'), + (2, 'Mike', 'Foo', '2003-01-01 12:53:13'), + (3, 'Kate', 'Bar', '2004-01-01 12:53:13'), + (4, 'Anna', 'Pear', '2005-01-01 12:53:13') + ;" + ); + $this->assertEquals( 4, $result ); + + // Primary key violation: + $error = ''; + try { + $this->engine->query( "INSERT INTO _tmp_table (ID, name) VALUES (1, 'Johnny')" ); + } catch ( Throwable $e ) { + $error = $e->getMessage(); + } + $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.ID, _tmp_table.name', $error ); + + // Unique constraint violation: + $error = ''; + try { + $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (5, 'Kate', 'Bar');" ); + } catch ( Throwable $e ) { + $error = $e->getMessage(); + } + $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.name, _tmp_table.lastname', $error ); + + // No constraint violation: + $result = $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (5, 'Joanna', 'Bar');" ); + $this->assertEquals( 1, $result ); + + // Now – let's change a few columns: + $result = $this->engine->query( 'ALTER TABLE _tmp_table CHANGE COLUMN name firstname varchar(20)' ); + $this->assertNull( $result ); + + $result = $this->engine->query( 'ALTER TABLE _tmp_table CHANGE COLUMN date_as_string datetime datetime NOT NULL' ); + $this->assertNull( $result ); + + // Finally, let's confirm our data is intact and the table is still well-behaved: + $result = $this->engine->query( 'SELECT * FROM _tmp_table ORDER BY ID;' ); + $this->assertCount( 5, $result ); + $this->assertEquals( 1, $result[0]->ID ); + $this->assertEquals( 'Johnny', $result[0]->firstname ); + $this->assertEquals( 'Appleseed', $result[0]->lastname ); + $this->assertEquals( '2002-01-01 12:53:13', $result[0]->datetime ); + + // Primary key violation: + $error = ''; + try { + $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, datetime) VALUES (1, 'Johnny', '2010-01-01 12:53:13');" ); + } catch ( Throwable $e ) { + $error = $e->getMessage(); + } + $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.ID, _tmp_table.firstname', $error ); + + // Unique constraint violation: + $error = ''; + try { + $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname, datetime) VALUES (6, 'Kate', 'Bar', '2010-01-01 12:53:13');" ); + } catch ( Throwable $e ) { + $error = $e->getMessage(); + } + $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.firstname, _tmp_table.lastname', $error ); + + // No constraint violation: + $result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname, datetime) VALUES (6, 'Sophie', 'Bar', '2010-01-01 12:53:13');" ); + $this->assertEquals( 1, $result ); + } + + public function testCaseInsensitiveUniqueIndex() { + $result = $this->engine->query( + "CREATE TABLE _tmp_table ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + name varchar(20) NOT NULL default '', + lastname varchar(20) NOT NULL default '', + KEY name (name), + UNIQUE KEY uname (name), + UNIQUE KEY last (lastname) + );" + ); + $this->assertNull( $result ); + + $result1 = $this->engine->query( "INSERT INTO _tmp_table (name, lastname) VALUES ('first', 'last');" ); + $this->assertEquals( 1, $result1 ); + + $result1 = $this->engine->query( 'SELECT COUNT(*) num FROM _tmp_table;' ); + $this->assertEquals( 1, $result1[0]->num ); + + // Unique keys should be case-insensitive: + $error = ''; + try { + $this->assertQuery( + "INSERT INTO _tmp_table (name, lastname) VALUES ('FIRST', 'LAST' )" + ); + } catch ( Throwable $e ) { + $error = $e->getMessage(); + } + $this->assertStringContainsString( 'UNIQUE constraint failed', $error ); + + $result1 = $this->engine->query( 'SELECT COUNT(*) num FROM _tmp_table;' ); + $this->assertEquals( 1, $result1[0]->num ); + + // Unique keys should be case-insensitive: + $result1 = $this->assertQuery( + "INSERT IGNORE INTO _tmp_table (name) VALUES ('FIRST');" + ); + + self::assertEquals( 0, $result1 ); + + $result2 = $this->engine->get_query_results(); + $this->assertEquals( 0, $result2 ); + + $result1 = $this->engine->query( 'SELECT COUNT(*)num FROM _tmp_table;' ); + $this->assertEquals( 1, $result1[0]->num ); + + // Unique keys should be case-insensitive: + $result2 = $this->assertQuery( + "INSERT INTO _tmp_table (name, lastname) VALUES ('FIRSTname', 'LASTname' );" + ); + + $this->assertEquals( 1, $result2 ); + + $result1 = $this->engine->query( 'SELECT COUNT(*) num FROM _tmp_table;' ); + $this->assertEquals( 2, $result1[0]->num ); + } + + public function testOnDuplicateUpdate() { + $this->assertQuery( + "CREATE TABLE _tmp_table ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + name varchar(20) NOT NULL default '', + UNIQUE KEY myname (name) + );" + ); + + $result1 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('first');" ); + $this->assertEquals( 1, $result1 ); + + $result2 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('FIRST') ON DUPLICATE KEY UPDATE `name` = VALUES(`name`);" ); + $this->assertEquals( 1, $result2 ); + + $this->assertQuery( 'SELECT * FROM _tmp_table;' ); + $this->assertCount( 1, $this->engine->get_query_results() ); + $this->assertEquals( + array( + (object) array( + 'name' => 'FIRST', + 'ID' => 1, + ), + ), + $this->engine->get_query_results() + ); + } + + public function testTruncatesInvalidDates() { + $this->assertQuery( "INSERT INTO _dates (option_value) VALUES ('2022-01-01 14:24:12');" ); + $this->assertQuery( "INSERT INTO _dates (option_value) VALUES ('2022-31-01 14:24:12');" ); + + $this->assertQuery( 'SELECT * FROM _dates;' ); + $results = $this->engine->get_query_results(); + $this->assertCount( 2, $results ); + $this->assertEquals( '2022-01-01 14:24:12', $results[0]->option_value ); + $this->assertEquals( '0000-00-00 00:00:00', $results[1]->option_value ); + } + + public function testCaseInsensitiveSelect() { + $this->assertQuery( + "CREATE TABLE _tmp_table ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + name varchar(20) NOT NULL default '' + );" + ); + $this->assertQuery( + "INSERT INTO _tmp_table (name) VALUES ('first');" + ); + $this->assertQuery( "SELECT name FROM _tmp_table WHERE name = 'FIRST';" ); + $this->assertCount( 1, $this->engine->get_query_results() ); + $this->assertEquals( + array( + (object) array( + 'name' => 'first', + ), + ), + $this->engine->get_query_results() + ); + } + + public function testSelectBetweenDates() { + $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2016-01-15T00:00:00Z');" ); + $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2016-01-16T00:00:00Z');" ); + $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('third', '2016-01-17T00:00:00Z');" ); + $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('fourth', '2016-01-18T00:00:00Z');" ); + + $this->assertQuery( "SELECT * FROM _dates WHERE option_value BETWEEN '2016-01-15T00:00:00Z' AND '2016-01-17T00:00:00Z' ORDER BY ID;" ); + $results = $this->engine->get_query_results(); + $this->assertCount( 3, $results ); + $this->assertEquals( 'first', $results[0]->option_name ); + $this->assertEquals( 'second', $results[1]->option_name ); + $this->assertEquals( 'third', $results[2]->option_name ); + } + + public function testSelectFilterByDatesGtLt() { + $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2016-01-15T00:00:00Z');" ); + $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2016-01-16T00:00:00Z');" ); + $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('third', '2016-01-17T00:00:00Z');" ); + $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('fourth', '2016-01-18T00:00:00Z');" ); + + $this->assertQuery( + " + SELECT * FROM _dates + WHERE option_value > '2016-01-15 00:00:00' + AND option_value < '2016-01-17 00:00:00' + ORDER BY ID + " + ); + $results = $this->engine->get_query_results(); + $this->assertCount( 1, $results ); + $this->assertEquals( 'second', $results[0]->option_name ); + } + + public function testSelectFilterByDatesZeroHour() { + $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2014-10-21 00:42:29');" ); + $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2014-10-21 01:42:29');" ); + + $this->assertQuery( + ' + SELECT * FROM _dates + WHERE YEAR(option_value) = 2014 + AND MONTHNUM(option_value) = 10 + AND DAY(option_value) = 21 + AND HOUR(option_value) = 0 + AND MINUTE(option_value) = 42 + ' + ); + $results = $this->engine->get_query_results(); + $this->assertCount( 1, $results ); + $this->assertEquals( 'first', $results[0]->option_name ); + } + + public function testCorrectlyInsertsDatesAndStrings() { + $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('2016-01-15T00:00:00Z', '2016-01-15T00:00:00Z');" ); + + $this->assertQuery( 'SELECT * FROM _dates' ); + $results = $this->engine->get_query_results(); + $this->assertCount( 1, $results ); + $this->assertEquals( '2016-01-15 00:00:00', $results[0]->option_value ); + if ( '2016-01-15T00:00:00Z' !== $results[0]->option_name ) { + $this->markTestSkipped( 'A datetime-like string was rewritten to an SQLite format even though it was used as a text and not as a datetime.' ); + } + $this->assertEquals( '2016-01-15T00:00:00Z', $results[0]->option_name ); + } + + public function testTransactionRollback() { + $this->assertQuery( 'BEGIN' ); + $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('first');" ); + $this->assertQuery( 'SELECT * FROM _options;' ); + $this->assertCount( 1, $this->engine->get_query_results() ); + $this->assertQuery( 'ROLLBACK' ); + + $this->assertQuery( 'SELECT * FROM _options;' ); + $this->assertCount( 0, $this->engine->get_query_results() ); + } + + public function testTransactionCommit() { + $this->assertQuery( 'BEGIN' ); + $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('first');" ); + $this->assertQuery( 'SELECT * FROM _options;' ); + $this->assertCount( 1, $this->engine->get_query_results() ); + $this->assertQuery( 'COMMIT' ); + + $this->assertQuery( 'SELECT * FROM _options;' ); + $this->assertCount( 1, $this->engine->get_query_results() ); + } + + public function testStartTransactionCommand() { + $this->assertQuery( 'START TRANSACTION' ); + $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('first');" ); + $this->assertQuery( 'SELECT * FROM _options;' ); + $this->assertCount( 1, $this->engine->get_query_results() ); + $this->assertQuery( 'ROLLBACK' ); + + $this->assertQuery( 'SELECT * FROM _options;' ); + $this->assertCount( 0, $this->engine->get_query_results() ); + } + + public function testNestedTransactionWork() { + $this->assertQuery( 'BEGIN' ); + $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('first');" ); + $this->assertQuery( 'START TRANSACTION' ); + $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('second');" ); + $this->assertQuery( 'START TRANSACTION' ); + $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('third');" ); + $this->assertQuery( 'SELECT * FROM _options;' ); + $this->assertCount( 3, $this->engine->get_query_results() ); + + $this->assertQuery( 'ROLLBACK' ); + $this->assertQuery( 'SELECT * FROM _options;' ); + $this->assertCount( 2, $this->engine->get_query_results() ); + + $this->assertQuery( 'ROLLBACK' ); + $this->assertQuery( 'SELECT * FROM _options;' ); + $this->assertCount( 1, $this->engine->get_query_results() ); + + $this->assertQuery( 'COMMIT' ); + $this->assertQuery( 'SELECT * FROM _options;' ); + $this->assertCount( 1, $this->engine->get_query_results() ); + } + + public function testNestedTransactionWorkComplexModify() { + $this->assertQuery( 'BEGIN' ); + // Create a complex ALTER Table query where the first + // column is added successfully, but the second fails. + // Behind the scenes, this single MySQL query is split + // into multiple SQLite queries – some of them will + // succeed, some will fail. + $error = ''; + try { + $this->engine->query( + ' + ALTER TABLE _options + ADD COLUMN test varchar(20), + ADD COLUMN test varchar(20) + ' + ); + } catch ( Throwable $e ) { + $error = $e->getMessage(); + } + $this->assertStringContainsString( 'duplicate column name: test', $error ); + + // Commit the transaction. + $this->assertQuery( 'COMMIT' ); + + // Confirm the entire query failed atomically and no column was + // added to the table. + $this->assertQuery( 'DESCRIBE _options;' ); + $fields = $this->engine->get_query_results(); + + $this->assertEquals( + array( + (object) array( + 'Field' => 'ID', + 'Type' => 'int', + 'Null' => 'NO', + 'Key' => 'PRI', + 'Default' => null, + 'Extra' => 'auto_increment', + ), + (object) array( + 'Field' => 'option_name', + 'Type' => 'text', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'option_value', + 'Type' => 'text', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + ), + $fields + ); + } + + public function testCount() { + $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('first');" ); + $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('second');" ); + $this->assertQuery( 'SELECT COUNT(*) as count FROM _options;' ); + + $results = $this->engine->get_query_results(); + $this->assertCount( 1, $results ); + $this->assertSame( '2', $results[0]->count ); + } + + public function testUpdateDate() { + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48');" + ); + + $this->assertQuery( 'SELECT option_value FROM _dates' ); + + $results = $this->engine->get_query_results(); + $this->assertCount( 1, $results ); + $this->assertEquals( '2003-05-27 10:08:48', $results[0]->option_value ); + + $this->assertQuery( + "UPDATE _dates SET option_value = DATE_SUB(option_value, INTERVAL '2' YEAR);" + ); + + $this->assertQuery( 'SELECT option_value FROM _dates' ); + + $results = $this->engine->get_query_results(); + $this->assertCount( 1, $results ); + $this->assertEquals( '2001-05-27 10:08:48', $results[0]->option_value ); + } + + public function testInsertDateLiteral() { + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48');" + ); + + $this->assertQuery( 'SELECT option_value FROM _dates' ); + + $results = $this->engine->get_query_results(); + $this->assertCount( 1, $results ); + $this->assertEquals( '2003-05-27 10:08:48', $results[0]->option_value ); + } + + public function testSelectDate1() { + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2000-05-27 10:08:48');" + ); + + $this->assertQuery( + 'SELECT + YEAR( _dates.option_value ) as year, + MONTH( _dates.option_value ) as month, + DAYOFMONTH( _dates.option_value ) as dayofmonth, + MONTHNUM( _dates.option_value ) as monthnum, + WEEKDAY( _dates.option_value ) as weekday, + WEEK( _dates.option_value, 1 ) as week1, + HOUR( _dates.option_value ) as hour, + MINUTE( _dates.option_value ) as minute, + SECOND( _dates.option_value ) as second + FROM _dates' + ); + + $results = $this->engine->get_query_results(); + $this->assertCount( 1, $results ); + $this->assertEquals( '2000', $results[0]->year ); + $this->assertEquals( '5', $results[0]->month ); + $this->assertEquals( '27', $results[0]->dayofmonth ); + $this->assertEquals( '5', $results[0]->weekday ); + $this->assertEquals( '21', $results[0]->week1 ); + $this->assertEquals( '5', $results[0]->monthnum ); + $this->assertEquals( '10', $results[0]->hour ); + $this->assertEquals( '8', $results[0]->minute ); + $this->assertEquals( '48', $results[0]->second ); + } + + public function testSelectDate24HourFormat() { + $this->assertQuery( + " + INSERT INTO _dates (option_name, option_value) + VALUES + ('second', '2003-05-27 14:08:48'), + ('first', '2003-05-27 00:08:48'); + " + ); + + // HOUR(14:08) should yield 14 in the 24 hour format + $this->assertQuery( "SELECT HOUR( _dates.option_value ) as hour FROM _dates WHERE option_name = 'second'" ); + $results = $this->engine->get_query_results(); + $this->assertCount( 1, $results ); + $this->assertEquals( '14', $results[0]->hour ); + + // HOUR(00:08) should yield 0 in the 24 hour format + $this->assertQuery( "SELECT HOUR( _dates.option_value ) as hour FROM _dates WHERE option_name = 'first'" ); + $results = $this->engine->get_query_results(); + $this->assertCount( 1, $results ); + $this->assertEquals( '0', $results[0]->hour ); + + // Lookup by HOUR(00:08) = 0 should yield the right record + $this->assertQuery( + 'SELECT HOUR( _dates.option_value ) as hour FROM _dates + WHERE HOUR(_dates.option_value) = 0 ' + ); + + $results = $this->engine->get_query_results(); + $this->assertCount( 1, $results ); + $this->assertEquals( '0', $results[0]->hour ); + } + + public function testSelectByDateFunctions() { + $this->assertQuery( + " + INSERT INTO _dates (option_name, option_value) + VALUES ('second', '2014-10-21 00:42:29'); + " + ); + + // HOUR(14:08) should yield 14 in the 24 hour format + $this->assertQuery( + ' + SELECT * FROM _dates WHERE + year(option_value) = 2014 + AND monthnum(option_value) = 10 + AND day(option_value) = 21 + AND hour(option_value) = 0 + AND minute(option_value) = 42 + ' + ); + $results = $this->engine->get_query_results(); + $this->assertCount( 1, $results ); + } + + public function testSelectByDateFormat() { + $this->assertQuery( + " + INSERT INTO _dates (option_name, option_value) + VALUES ('second', '2014-10-21 00:42:29'); + " + ); + + // HOUR(14:08) should yield 14 in the 24 hour format + $this->assertQuery( + " + SELECT * FROM _dates WHERE DATE_FORMAT(option_value, '%H.%i') = 0.42 + " + ); + $results = $this->engine->get_query_results(); + $this->assertCount( 1, $results ); + } + + public function testInsertOnDuplicateKey() { + $this->assertQuery( + "CREATE TABLE _tmp_table ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + name varchar(20) NOT NULL default '', + UNIQUE KEY name (name) + );" + ); + $result1 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('first');" ); + $this->assertEquals( 1, $result1 ); + + $result2 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('FIRST') ON DUPLICATE KEY UPDATE name=VALUES(`name`);" ); + $this->assertEquals( 1, $result2 ); + + $this->assertQuery( 'SELECT COUNT(*) as cnt FROM _tmp_table' ); + $results = $this->engine->get_query_results(); + $this->assertEquals( 1, $results[0]->cnt ); + } + + public function testCreateTableCompositePk() { + $this->assertQuery( + 'CREATE TABLE wptests_term_relationships ( + object_id bigint(20) unsigned NOT NULL default 0, + term_taxonomy_id bigint(20) unsigned NOT NULL default 0, + term_order int(11) NOT NULL default 0, + PRIMARY KEY (object_id,term_taxonomy_id), + KEY term_taxonomy_id (term_taxonomy_id) + ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci' + ); + $result = $this->engine->query( 'INSERT INTO wptests_term_relationships VALUES (1,2,1),(1,3,2);' ); + $this->assertEquals( 2, $result ); + + $this->expectExceptionMessage( 'UNIQUE constraint failed: wptests_term_relationships.object_id, wptests_term_relationships.term_taxonomy_id' ); + $this->engine->query( 'INSERT INTO wptests_term_relationships VALUES (1,2,2),(1,3,1);' ); + } + + public function testDescribeAccurate() { + $result = $this->assertQuery( + 'CREATE TABLE wptests_term_relationships ( + object_id bigint(20) unsigned NOT NULL default 0, + term_taxonomy_id bigint(20) unsigned NOT NULL default 0, + term_name varchar(11) NOT NULL default 0, + PRIMARY KEY (object_id,term_taxonomy_id), + KEY term_taxonomy_id (term_taxonomy_id), + KEY compound_key (object_id(20),term_taxonomy_id(20)), + FULLTEXT KEY term_name (term_name) + ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci' + ); + $this->assertNotFalse( $result ); + + $result = $this->assertQuery( 'DESCRIBE wptests_term_relationships;' ); + $this->assertNotFalse( $result ); + + $fields = $this->engine->get_query_results(); + + $this->assertEquals( + array( + (object) array( + 'Field' => 'object_id', + 'Type' => 'bigint(20) unsigned', + 'Null' => 'NO', + 'Key' => 'PRI', + 'Default' => '0', + 'Extra' => '', + ), + (object) array( + 'Field' => 'term_taxonomy_id', + 'Type' => 'bigint(20) unsigned', + 'Null' => 'NO', + 'Key' => 'PRI', + 'Default' => '0', + 'Extra' => '', + ), + (object) array( + 'Field' => 'term_name', + 'Type' => 'varchar(11)', + 'Null' => 'NO', + 'Key' => 'MUL', + 'Default' => '0', + 'Extra' => '', + ), + ), + $fields + ); + } + + public function testAlterTableAddColumnChangesMySQLDataType() { + $result = $this->assertQuery( + 'CREATE TABLE _test ( + object_id bigint(20) unsigned NOT NULL default 0 + )' + ); + $this->assertNotFalse( $result ); + + $result = $this->assertQuery( "ALTER TABLE `_test` ADD COLUMN object_name varchar(255) NOT NULL DEFAULT 'adb';" ); + $this->assertNotFalse( $result ); + + $result = $this->assertQuery( 'DESCRIBE _test;' ); + $this->assertNotFalse( $result ); + $fields = $this->engine->get_query_results(); + + $this->assertEquals( + array( + (object) array( + 'Field' => 'object_id', + 'Type' => 'bigint(20) unsigned', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '0', + 'Extra' => '', + ), + (object) array( + 'Field' => 'object_name', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => 'adb', + 'Extra' => '', + ), + ), + $fields + ); + } + public function testShowGrantsFor() { + $result = $this->assertQuery( 'SHOW GRANTS FOR current_user();' ); + $this->assertEquals( + $result, + array( + (object) array( + 'Grants for root@localhost' => 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, SHUTDOWN, PROCESS, FILE, REFERENCES, INDEX, ALTER, SHOW DATABASES, SUPER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE USER, EVENT, TRIGGER, CREATE TABLESPACE, CREATE ROLE, DROP ROLE ON *.* TO `root`@`localhost` WITH GRANT OPTION', + ), + ) + ); + } + + public function testShowIndex() { + $result = $this->assertQuery( + 'CREATE TABLE wptests_term_relationships ( + object_id bigint(20) unsigned NOT NULL default 0, + term_taxonomy_id bigint(20) unsigned NOT NULL default 0, + term_name varchar(11) NOT NULL default 0, + geom_col geometry NOT NULL, + FULLTEXT KEY term_name_fulltext1 (term_name), + FULLTEXT INDEX term_name_fulltext2 (`term_name`), + SPATIAL KEY geom_col_spatial (geom_col), + PRIMARY KEY (object_id,term_taxonomy_id), + KEY term_taxonomy_id (term_taxonomy_id), + KEY compound_key (object_id,term_taxonomy_id) + ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci' + ); + $this->assertNotFalse( $result ); + + $result = $this->assertQuery( 'SHOW INDEX FROM wptests_term_relationships;' ); + $this->assertNotFalse( $result ); + + $this->assertEquals( + array( + (object) array( + 'Table' => 'wptests_term_relationships', + 'Non_unique' => '0', + 'Key_name' => 'PRIMARY', + 'Seq_in_index' => '1', + 'Column_name' => 'object_id', + 'Collation' => 'A', + 'Cardinality' => '0', + 'Sub_part' => null, + 'Packed' => null, + 'Null' => '', + 'Index_type' => 'BTREE', + 'Comment' => '', + 'Index_comment' => '', + 'Visible' => 'YES', + 'Expression' => null, + ), + (object) array( + 'Table' => 'wptests_term_relationships', + 'Non_unique' => '0', + 'Key_name' => 'PRIMARY', + 'Seq_in_index' => '2', + 'Column_name' => 'term_taxonomy_id', + 'Collation' => 'A', + 'Cardinality' => '0', + 'Sub_part' => null, + 'Packed' => null, + 'Null' => '', + 'Index_type' => 'BTREE', + 'Comment' => '', + 'Index_comment' => '', + 'Visible' => 'YES', + 'Expression' => null, + ), + (object) array( + 'Table' => 'wptests_term_relationships', + 'Non_unique' => '1', + 'Key_name' => 'geom_col_spatial', + 'Seq_in_index' => '1', + 'Column_name' => 'geom_col', + 'Collation' => 'A', + 'Cardinality' => '0', + 'Sub_part' => 32, + 'Packed' => null, + 'Null' => '', + 'Index_type' => 'SPATIAL', + 'Comment' => '', + 'Index_comment' => '', + 'Visible' => 'YES', + 'Expression' => null, + ), + (object) array( + 'Table' => 'wptests_term_relationships', + 'Non_unique' => '1', + 'Key_name' => 'term_taxonomy_id', + 'Seq_in_index' => '1', + 'Column_name' => 'term_taxonomy_id', + 'Collation' => 'A', + 'Cardinality' => '0', + 'Sub_part' => null, + 'Packed' => null, + 'Null' => '', + 'Index_type' => 'BTREE', + 'Comment' => '', + 'Index_comment' => '', + 'Visible' => 'YES', + 'Expression' => null, + ), + (object) array( + 'Table' => 'wptests_term_relationships', + 'Non_unique' => '1', + 'Key_name' => 'compound_key', + 'Seq_in_index' => '1', + 'Column_name' => 'object_id', + 'Collation' => 'A', + 'Cardinality' => '0', + 'Sub_part' => null, + 'Packed' => null, + 'Null' => '', + 'Index_type' => 'BTREE', + 'Comment' => '', + 'Index_comment' => '', + 'Visible' => 'YES', + 'Expression' => null, + ), + (object) array( + 'Table' => 'wptests_term_relationships', + 'Non_unique' => '1', + 'Key_name' => 'compound_key', + 'Seq_in_index' => '2', + 'Column_name' => 'term_taxonomy_id', + 'Collation' => 'A', + 'Cardinality' => '0', + 'Sub_part' => null, + 'Packed' => null, + 'Null' => '', + 'Index_type' => 'BTREE', + 'Comment' => '', + 'Index_comment' => '', + 'Visible' => 'YES', + 'Expression' => null, + ), + (object) array( + 'Table' => 'wptests_term_relationships', + 'Non_unique' => '1', + 'Key_name' => 'term_name_fulltext1', + 'Seq_in_index' => '1', + 'Column_name' => 'term_name', + 'Collation' => null, + 'Cardinality' => '0', + 'Sub_part' => null, + 'Packed' => null, + 'Null' => '', + 'Index_type' => 'FULLTEXT', + 'Comment' => '', + 'Index_comment' => '', + 'Visible' => 'YES', + 'Expression' => null, + ), + (object) array( + 'Table' => 'wptests_term_relationships', + 'Non_unique' => '1', + 'Key_name' => 'term_name_fulltext2', + 'Seq_in_index' => '1', + 'Column_name' => 'term_name', + 'Collation' => null, + 'Cardinality' => '0', + 'Sub_part' => null, + 'Packed' => null, + 'Null' => '', + 'Index_type' => 'FULLTEXT', + 'Comment' => '', + 'Index_comment' => '', + 'Visible' => 'YES', + 'Expression' => null, + ), + ), + $this->engine->get_query_results() + ); + } + + public function testShowVarianles(): void { + $this->assertQuery( 'SHOW VARIABLES' ); + $this->assertQuery( "SHOW VARIABLES LIKE 'version'" ); + $this->assertQuery( "SHOW VARIABLES WHERE Variable_name = 'version'" ); + $this->assertQuery( 'SHOW GLOBAL VARIABLES' ); + $this->assertQuery( 'SHOW SESSION VARIABLES' ); + } + + public function testInsertOnDuplicateKeyCompositePk() { + $result = $this->assertQuery( + 'CREATE TABLE wptests_term_relationships ( + object_id bigint(20) unsigned NOT NULL default 0, + term_taxonomy_id bigint(20) unsigned NOT NULL default 0, + term_order int(11) NOT NULL default 0, + PRIMARY KEY (object_id,term_taxonomy_id), + KEY term_taxonomy_id (term_taxonomy_id) + ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci' + ); + $this->assertNotFalse( $result ); + + $result1 = $this->assertQuery( 'INSERT INTO wptests_term_relationships VALUES (1,2,1),(1,3,2);' ); + $this->assertEquals( 2, $result1 ); + + $result2 = $this->assertQuery( 'INSERT INTO wptests_term_relationships VALUES (1,2,2),(1,3,1) ON DUPLICATE KEY UPDATE term_order = VALUES(term_order);' ); + $this->assertEquals( 2, $result2 ); + + $this->assertQuery( 'SELECT COUNT(*) as cnt FROM wptests_term_relationships' ); + $results = $this->engine->get_query_results(); + $this->assertEquals( 2, $results[0]->cnt ); + } + + public function testStringToFloatComparison() { + $this->assertQuery( "SELECT ('00.42' = 0.4200) as cmp;" ); + $results = $this->engine->get_query_results(); + if ( 1 !== $results[0]->cmp ) { + $this->markTestSkipped( 'Comparing a string and a float returns true in MySQL. In SQLite, they\'re different. Skipping. ' ); + } + $this->assertEquals( '1', $results[0]->cmp ); + + $this->assertQuery( "SELECT (0+'00.42' = 0.4200) as cmp;" ); + $results = $this->engine->get_query_results(); + $this->assertEquals( '1', $results[0]->cmp ); + } + + public function testZeroPlusStringToFloatComparison() { + + $this->assertQuery( "SELECT (0+'00.42' = 0.4200) as cmp;" ); + $results = $this->engine->get_query_results(); + $this->assertEquals( '1', $results[0]->cmp ); + + $this->assertQuery( "SELECT 0+'1234abcd' = 1234 as cmp;" ); + $results = $this->engine->get_query_results(); + $this->assertEquals( '1', $results[0]->cmp ); + } + + public function testCalcFoundRows() { + $result = $this->assertQuery( + "CREATE TABLE wptests_dummy ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + user_login TEXT NOT NULL default '' + );" + ); + $this->assertNotFalse( $result ); + + $result = $this->assertQuery( + "INSERT INTO wptests_dummy (user_login) VALUES ('test1');" + ); + $this->assertEquals( 1, $result ); + + $result = $this->assertQuery( + "INSERT INTO wptests_dummy (user_login) VALUES ('test2');" + ); + $this->assertEquals( 1, $result ); + + $result = $this->assertQuery( + 'SELECT SQL_CALC_FOUND_ROWS * FROM wptests_dummy ORDER BY ID LIMIT 1' + ); + $this->assertNotFalse( $result ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'test1', $result[0]->user_login ); + + $result = $this->assertQuery( + 'SELECT FOUND_ROWS()' + ); + $this->assertEquals( + array( + (object) array( + 'FOUND_ROWS()' => '2', + ), + ), + $result + ); + } + + public function testComplexSelectBasedOnDates() { + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48');" + ); + + $this->assertQuery( + 'SELECT SQL_CALC_FOUND_ROWS _dates.ID + FROM _dates + WHERE YEAR( _dates.option_value ) = 2003 AND MONTH( _dates.option_value ) = 5 AND DAYOFMONTH( _dates.option_value ) = 27 + ORDER BY _dates.option_value DESC + LIMIT 0, 10' + ); + + $results = $this->engine->get_query_results(); + $this->assertCount( 1, $results ); + } + + public function testUpdateReturnValue() { + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48');" + ); + + $return = $this->assertQuery( + "UPDATE _dates SET option_value = '2001-05-27 10:08:48'" + ); + $this->assertSame( 1, $return, 'UPDATE query did not return 1 when one row was changed' ); + + $return = $this->assertQuery( + "UPDATE _dates SET option_value = '2001-05-27 10:08:48'" + ); + if ( 1 === $return ) { + $this->markTestIncomplete( + 'SQLite UPDATE query returned 1 when no rows were changed. ' . + 'This is a database compatibility issue – MySQL would return 0 ' . + 'in the same scenario.' + ); + } + $this->assertSame( 0, $return, 'UPDATE query did not return 0 when no rows were changed' ); + } + + public function testOrderByField() { + $this->assertQuery( + "INSERT INTO _options (option_name, option_value) VALUES ('User 0000019', 'second');" + ); + $this->assertQuery( + "INSERT INTO _options (option_name, option_value) VALUES ('User 0000020', 'third');" + ); + $this->assertQuery( + "INSERT INTO _options (option_name, option_value) VALUES ('User 0000018', 'first');" + ); + + $this->assertQuery( 'SELECT FIELD(option_name, "User 0000018", "User 0000019", "User 0000020") as sorting_order FROM _options ORDER BY FIELD(option_name, "User 0000018", "User 0000019", "User 0000020")' ); + + $this->assertEquals( + array( + (object) array( + 'sorting_order' => '1', + ), + (object) array( + 'sorting_order' => '2', + ), + (object) array( + 'sorting_order' => '3', + ), + ), + $this->engine->get_query_results() + ); + + $this->assertQuery( 'SELECT option_value FROM _options ORDER BY FIELD(option_name, "User 0000018", "User 0000019", "User 0000020")' ); + + $this->assertEquals( + array( + (object) array( + 'option_value' => 'first', + ), + (object) array( + 'option_value' => 'second', + ), + (object) array( + 'option_value' => 'third', + ), + ), + $this->engine->get_query_results() + ); + } + + public function testFetchedDataIsStringified() { + $this->assertQuery( + "INSERT INTO _options (option_name, option_value) VALUES ('rss_0123456789abcdef0123456789abcdef', '1');" + ); + + $this->assertQuery( 'SELECT ID FROM _options' ); + + $this->assertEquals( + array( + (object) array( + 'ID' => '1', + ), + ), + $this->engine->get_query_results() + ); + } + + public function testCreateTableQuery() { + $this->assertQuery( + <<<'QUERY' + CREATE TABLE IF NOT EXISTS wptests_users ( + ID bigint(20) unsigned NOT NULL auto_increment, + user_login varchar(60) NOT NULL default '', + user_pass varchar(255) NOT NULL default '', + user_nicename varchar(50) NOT NULL default '', + user_email varchar(100) NOT NULL default '', + user_url varchar(100) NOT NULL default '', + user_registered datetime NOT NULL default '0000-00-00 00:00:00', + user_activation_key varchar(255) NOT NULL default '', + user_status int(11) NOT NULL default '0', + display_name varchar(250) NOT NULL default '', + PRIMARY KEY (ID), + KEY user_login_key (user_login), + KEY user_nicename (user_nicename), + KEY user_email (user_email) + ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci +QUERY + ); + $this->assertQuery( + <<<'QUERY' + INSERT INTO wptests_users VALUES (1,'admin','$P$B5ZQZ5ZQZ5ZQZ5ZQZ5ZQZ5ZQZ5ZQZ5','admin','admin@localhost', '', '2019-01-01 00:00:00', '', 0, 'admin'); +QUERY + ); + $rows = $this->assertQuery( 'SELECT * FROM wptests_users' ); + $this->assertCount( 1, $rows ); + + $this->assertQuery( 'SELECT SQL_CALC_FOUND_ROWS * FROM wptests_users' ); + $result = $this->assertQuery( 'SELECT FOUND_ROWS()' ); + $this->assertEquals( + array( + (object) array( + 'FOUND_ROWS()' => '1', + ), + ), + $result + ); + } + + public function testCreateTableIfNotExists(): void { + $this->assertQuery( + 'CREATE TABLE t (ID INTEGER, name TEXT)' + ); + $this->assertQuery( + 'CREATE TABLE IF NOT EXISTS t (ID INTEGER, name TEXT)' + ); + + $this->expectExceptionMessage( 'table `t` already exists' ); + $this->assertQuery( + 'CREATE TABLE t (ID INTEGER, name TEXT)' + ); + } + + public function testTranslatesComplexDelete() { + $this->sqlite->query( + "CREATE TABLE wptests_dummy ( + ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + user_login TEXT NOT NULL default '', + option_name TEXT NOT NULL default '', + option_value TEXT NOT NULL default '' + );" + ); + $this->sqlite->query( + "INSERT INTO wptests_dummy (user_login, option_name, option_value) VALUES ('admin', '_transient_timeout_test', '1675963960');" + ); + $this->sqlite->query( + "INSERT INTO wptests_dummy (user_login, option_name, option_value) VALUES ('admin', '_transient_test', '1675963960');" + ); + + $result = $this->assertQuery( + "DELETE a, b FROM wptests_dummy a, wptests_dummy b + WHERE a.option_name LIKE '\_transient\_%' + AND a.option_name NOT LIKE '\_transient\_timeout_%' + AND b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) );" + ); + $this->assertEquals( + 2, + $result + ); + } + + public function testTranslatesDoubleAlterTable() { + $result = $this->assertQuery( + 'ALTER TABLE _options + ADD INDEX test_index(option_name(140),option_value(51)), + DROP INDEX test_index, + ADD INDEX test_index2(option_name(140),option_value(51)) + ' + ); + $this->assertNull( $result ); + + $result = $this->assertQuery( + 'SHOW INDEX FROM _options' + ); + $this->assertCount( 3, $result ); + $this->assertEquals( 'PRIMARY', $result[0]->Key_name ); + $this->assertEquals( 'test_index2', $result[1]->Key_name ); + $this->assertEquals( 'test_index2', $result[2]->Key_name ); + } + + public function testTranslatesComplexSelect() { + $this->assertQuery( + "CREATE TABLE wptests_postmeta ( + meta_id bigint(20) unsigned NOT NULL auto_increment, + post_id bigint(20) unsigned NOT NULL default '0', + meta_key varchar(255) default NULL, + meta_value longtext, + PRIMARY KEY (meta_id), + KEY post_id (post_id), + KEY meta_key (meta_key(191)) + ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci" + ); + $this->assertQuery( + "CREATE TABLE wptests_posts ( + ID bigint(20) unsigned NOT NULL auto_increment, + post_status varchar(20) NOT NULL default 'open', + post_type varchar(20) NOT NULL default 'post', + post_date varchar(20) NOT NULL default 'post', + PRIMARY KEY (ID) + ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci" + ); + $result = $this->assertQuery( + "SELECT SQL_CALC_FOUND_ROWS wptests_posts.ID + FROM wptests_posts INNER JOIN wptests_postmeta ON ( wptests_posts.ID = wptests_postmeta.post_id ) + WHERE 1=1 + AND ( + NOT EXISTS ( + SELECT 1 FROM wptests_postmeta mt1 + WHERE mt1.post_ID = wptests_postmeta.post_ID + LIMIT 1 + ) + ) + AND ( + (wptests_posts.post_type = 'post' AND (wptests_posts.post_status = 'publish')) + ) + GROUP BY wptests_posts.ID + ORDER BY wptests_posts.post_date DESC + LIMIT 0, 10" + ); + + // No exception is good enough of a test for now + $this->assertTrue( true ); + } + + public function testTranslatesUtf8Insert() { + $this->assertQuery( + "INSERT INTO _options VALUES(1,'ąłółźćę†','ąłółźćę†')" + ); + $this->assertCount( + 1, + $this->assertQuery( 'SELECT * FROM _options' ) + ); + $this->assertQuery( 'DELETE FROM _options' ); + } + + public function testTranslatesRandom() { + $this->assertIsNumeric( + $this->sqlite->query( 'SELECT RAND() AS rand' )->fetchColumn() + ); + + $this->assertIsNumeric( + $this->sqlite->query( 'SELECT RAND(5) AS rand' )->fetchColumn() + ); + } + + public function testTranslatesUtf8SELECT() { + $this->assertQuery( + "INSERT INTO _options VALUES(1,'ąłółźćę†','ąłółźćę†')" + ); + $this->assertCount( + 1, + $this->assertQuery( 'SELECT * FROM _options' ) + ); + + $this->assertQuery( + "SELECT option_name as 'ą' FROM _options WHERE option_name='ąłółźćę†' AND option_value='ąłółźćę†'" + ); + + $this->assertEquals( + array( (object) array( 'ą' => 'ąłółźćę†' ) ), + $this->engine->get_query_results() + ); + + $this->assertQuery( + "SELECT option_name as 'ą' FROM _options WHERE option_name LIKE '%ółźć%'" + ); + + $this->assertEquals( + array( (object) array( 'ą' => 'ąłółźćę†' ) ), + $this->engine->get_query_results() + ); + + $this->assertQuery( 'DELETE FROM _options' ); + } + + public function testTranslateLikeBinary() { + // Create a temporary table for testing + $this->assertQuery( + 'CREATE TABLE _tmp_table ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + name varchar(20) + )' + ); + + // Insert data into the table + $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('first');" ); + $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('FIRST');" ); + $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('second');" ); + $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('');" ); + $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('%special%');" ); + $this->assertQuery( 'INSERT INTO _tmp_table (name) VALUES (NULL);' ); + $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('special%chars');" ); + $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('special_chars');" ); + $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('special\\\\chars');" ); + $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('aste*risk');" ); + $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('question?mark');" ); + + // Test exact string + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'first'" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'first', $result[0]->name ); + + // Test exact string with no matches + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'third'" ); + $this->assertCount( 0, $result ); + + // Test mixed case + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'First'" ); + $this->assertCount( 0, $result ); + + // Test % wildcard + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'f%'" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'first', $result[0]->name ); + + // Test % wildcard with no matches + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'x%'" ); + $this->assertCount( 0, $result ); + + // Test "%" character (not a wildcard) + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'special\\%chars'" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'special%chars', $result[0]->name ); + + // Test _ wildcard + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'f_rst'" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'first', $result[0]->name ); + + // Test _ wildcard with no matches + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'x_yz'" ); + $this->assertCount( 0, $result ); + + // Test "_" character (not a wildcard) + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'special\\_chars'" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'special_chars', $result[0]->name ); + + // Test escaping of "*" + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'aste*risk'" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'aste*risk', $result[0]->name ); + + // Test escaping of "*" with no matches + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'f*'" ); + $this->assertCount( 0, $result ); + + // Test escaping of "?" + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'question?mark'" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'question?mark', $result[0]->name ); + + // Test escaping of "?" with no matches + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'f?rst'" ); + $this->assertCount( 0, $result ); + + // Test escaping of character class + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY '[f]irst'" ); + $this->assertCount( 0, $result ); + + // Test NULL + $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE name LIKE BINARY NULL' ); + $this->assertCount( 0, $result ); + + // Test pattern with special characters using LIKE BINARY + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY '%special%'" ); + $this->assertCount( 4, $result ); + $this->assertEquals( '%special%', $result[0]->name ); + $this->assertEquals( 'special%chars', $result[1]->name ); + $this->assertEquals( 'special_chars', $result[2]->name ); + $this->assertEquals( 'special\chars', $result[3]->name ); + + // Test escaping - "\t" is a tab character + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'firs\\t'" ); + $this->assertCount( 0, $result ); + + // Test escaping - "\\t" is "t" (input resolves to "\t", which LIKE resolves to "t") + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'firs\\\\t'" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'first', $result[0]->name ); + + // Test escaping - "\%" is a "%" literal + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'special\\%chars'" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'special%chars', $result[0]->name ); + + // Test escaping - "\\%" is also a "%" literal + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'special\\\\%chars'" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'special%chars', $result[0]->name ); + + // Test escaping - "\\\%" is "\" and a wildcard + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'special\\\\\\%chars'" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'special\\chars', $result[0]->name ); + + // Test LIKE without BINARY + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE 'FIRST'" ); + $this->assertCount( 2, $result ); // Should match both 'first' and 'FIRST' + } + + public function testUniqueConstraints() { + $this->assertQuery( + "CREATE TABLE _tmp_table ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, + name varchar(20) NOT NULL default 'default-value', + unique_name varchar(20) NOT NULL default 'unique-default-value', + inline_unique_name varchar(30) NOT NULL default 'inline-unique-default-value' UNIQUE, + UNIQUE KEY unique_name (unique_name), + UNIQUE KEY compound_name (name, unique_name) + );" + ); + + // Insert a row with default values. + $this->assertQuery( 'INSERT INTO _tmp_table (ID) VALUES (1)' ); + $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE ID = 1' ); + $this->assertEquals( + array( + (object) array( + 'ID' => '1', + 'name' => 'default-value', + 'unique_name' => 'unique-default-value', + 'inline_unique_name' => 'inline-unique-default-value', + ), + ), + $result + ); + + // Insert another row. + $this->assertQuery( + "INSERT INTO _tmp_table VALUES (2, 'ANOTHER-VALUE', 'ANOTHER-UNIQUE-VALUE', 'ANOTHER-INLINE-UNIQUE-VALUE')" + ); + + // This should fail because of the UNIQUE constraints. + $error = ''; + try { + $this->assertQuery( + "UPDATE _tmp_table SET unique_name = 'unique-default-value' WHERE ID = 2" + ); + } catch ( Throwable $e ) { + $error = $e->getMessage(); + } + $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.unique_name', $error ); + + $error = ''; + try { + $this->assertQuery( + "UPDATE _tmp_table SET inline_unique_name = 'inline-unique-default-value' WHERE ID = 2" + ); + } catch ( Throwable $e ) { + $error = $e->getMessage(); + } + $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.inline_unique_name', $error ); + + // Updating "name" to the same value as the first row should pass. + $this->assertQuery( + "UPDATE _tmp_table SET name = 'default-value' WHERE ID = 2" + ); + $this->assertEquals( + array( + (object) array( + 'ID' => '2', + 'name' => 'default-value', + 'unique_name' => 'ANOTHER-UNIQUE-VALUE', + 'inline_unique_name' => 'ANOTHER-INLINE-UNIQUE-VALUE', + ), + ), + $this->assertQuery( 'SELECT * FROM _tmp_table WHERE ID = 2' ) + ); + + // Updating also "unique_name" should fail on the compound UNIQUE key. + $error = ''; + try { + $this->assertQuery( + "UPDATE _tmp_table SET inline_unique_name = 'inline-unique-default-value' WHERE ID = 2" + ); + } catch ( Throwable $e ) { + $error = $e->getMessage(); + } + $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.inline_unique_name', $error ); + + $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE ID = 2' ); + $this->assertEquals( + array( + (object) array( + 'ID' => '2', + 'name' => 'default-value', + 'unique_name' => 'ANOTHER-UNIQUE-VALUE', + 'inline_unique_name' => 'ANOTHER-INLINE-UNIQUE-VALUE', + ), + ), + $result + ); + } + + public function testDefaultNullValue() { + $this->assertQuery( + 'CREATE TABLE _tmp_table ( + name varchar(20) default NULL, + no_default varchar(20) NOT NULL + );' + ); + + $result = $this->assertQuery( + 'DESCRIBE _tmp_table;' + ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'name', + 'Type' => 'varchar(20)', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'no_default', + 'Type' => 'varchar(20)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + ), + $result + ); + } + + public function testCurrentTimestamp() { + // SELECT + $results = $this->assertQuery( + 'SELECT + current_timestamp AS t1, + CURRENT_TIMESTAMP AS t2, + current_timestamp() AS t3, + CURRENT_TIMESTAMP() AS t4' + ); + $this->assertIsArray( $results ); + $this->assertCount( 1, $results ); + $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $results[0]->t1 ); + $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $results[0]->t2 ); + $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $results[0]->t3 ); + + // INSERT + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('first', CURRENT_TIMESTAMP())" + ); + $results = $this->assertQuery( 'SELECT option_value AS t FROM _dates' ); + $this->assertCount( 1, $results ); + $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $results[0]->t ); + + // UPDATE + $this->assertQuery( "UPDATE _dates SET option_value = ''" ); + $results = $this->assertQuery( 'SELECT option_value AS t FROM _dates' ); + $this->assertCount( 1, $results ); + $this->assertEmpty( $results[0]->t ); + + $this->assertQuery( 'UPDATE _dates SET option_value = CURRENT_TIMESTAMP()' ); + $results = $this->assertQuery( 'SELECT option_value AS t FROM _dates' ); + $this->assertCount( 1, $results ); + $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $results[0]->t ); + + // DELETE + // We can only assert that the query passes. It is not guaranteed that we'll actually + // delete the existing record, as the delete query could fall into a different second. + $this->assertQuery( 'DELETE FROM _dates WHERE option_value = CURRENT_TIMESTAMP()' ); + } + + public function testGroupByHaving() { + $this->assertQuery( + 'CREATE TABLE _tmp_table ( + name varchar(20) + );' + ); + + $this->assertQuery( + "INSERT INTO _tmp_table VALUES ('a'), ('b'), ('b'), ('c'), ('c'), ('c')" + ); + + $result = $this->assertQuery( + 'SELECT name, COUNT(*) as count FROM _tmp_table GROUP BY name HAVING COUNT(*) > 1' + ); + $this->assertEquals( + array( + (object) array( + 'name' => 'b', + 'count' => '2', + ), + (object) array( + 'name' => 'c', + 'count' => '3', + ), + ), + $result + ); + } + + public function testHavingWithoutGroupBy() { + $this->assertQuery( + 'CREATE TABLE _tmp_table ( + name varchar(20) + );' + ); + + $this->assertQuery( + "INSERT INTO _tmp_table VALUES ('a'), ('b'), ('b'), ('c'), ('c'), ('c')" + ); + + // HAVING condition satisfied + $result = $this->assertQuery( + "SELECT 'T' FROM _tmp_table HAVING COUNT(*) > 1" + ); + $this->assertEquals( + array( + (object) array( + "'T'" => 'T', + ), + ), + $result + ); + + // HAVING condition not satisfied + $result = $this->assertQuery( + "SELECT 'T' FROM _tmp_table HAVING COUNT(*) > 100" + ); + $this->assertEquals( + array(), + $result + ); + + // DISTINCT ... HAVING, where only some results meet the HAVING condition + $result = $this->assertQuery( + 'SELECT DISTINCT name FROM _tmp_table HAVING COUNT(*) > 1' + ); + $this->assertEquals( + array( + (object) array( + 'name' => 'b', + ), + (object) array( + 'name' => 'c', + ), + ), + $result + ); + } + + /** + * @dataProvider mysqlVariablesToTest + */ + public function testSelectVariable( $variable_name ) { + // Make sure the query does not error + $this->assertQuery( "SELECT $variable_name;" ); + } + + public static function mysqlVariablesToTest() { + return array( + // NOTE: This list was derived from the variables used by the UpdraftPlus plugin. + // We will start here and plan to expand supported variables over time. + array( '@@character_set_client' ), + array( '@@character_set_results' ), + array( '@@collation_connection' ), + array( '@@GLOBAL.gtid_purged' ), + array( '@@GLOBAL.log_bin' ), + array( '@@GLOBAL.log_bin_trust_function_creators' ), + array( '@@GLOBAL.sql_mode' ), + array( '@@SESSION.max_allowed_packet' ), + array( '@@SESSION.sql_mode' ), + + // Intentionally mix letter casing to help demonstrate case-insensitivity + array( '@@cHarActer_Set_cLient' ), + array( '@@gLoBAL.gTiD_purGed' ), + array( '@@sEssIOn.sqL_moDe' ), + ); + } + + public function testLastInsertId(): void { + $this->assertQuery( + 'CREATE TABLE t ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(20) + );' + ); + + $this->assertQuery( "INSERT INTO t (name) VALUES ('a')" ); + $this->assertEquals( 1, $this->engine->get_insert_id() ); + + $this->assertQuery( "INSERT INTO t (name) VALUES ('b')" ); + $this->assertEquals( 2, $this->engine->get_insert_id() ); + } + + public function testCharLength(): void { + $this->assertQuery( + 'CREATE TABLE t ( + ID INTEGER PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(20) + );' + ); + + $this->assertQuery( "INSERT INTO t (name) VALUES ('a')" ); + $this->assertQuery( "INSERT INTO t (name) VALUES ('ab')" ); + $this->assertQuery( "INSERT INTO t (name) VALUES ('abc')" ); + + $this->assertQuery( 'SELECT CHAR_LENGTH(name) AS len FROM t' ); + $this->assertEquals( + array( + (object) array( 'len' => '1' ), + (object) array( 'len' => '2' ), + (object) array( 'len' => '3' ), + ), + $this->engine->get_query_results() + ); + } + + public function testNullCharactersInStrings(): void { + $this->assertQuery( + 'CREATE TABLE t (id INT, name VARCHAR(20))' + ); + $this->assertQuery( + "INSERT INTO t (name) VALUES ('a\0b')" + ); + $this->assertQuery( + 'SELECT name FROM t' + ); + $this->assertEquals( + array( + (object) array( 'name' => "a\0b" ), + ), + $this->engine->get_query_results() + ); + } + + public function testColumnDefaults(): void { + $this->assertQuery( + " + CREATE TABLE t ( + name varchar(255) DEFAULT 'CURRENT_TIMESTAMP', + type varchar(255) NOT NULL DEFAULT 'DEFAULT', + description varchar(250) NOT NULL DEFAULT '', + created_at timestamp DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) + " + ); + + $result = $this->assertQuery( 'SHOW CREATE TABLE t' ); + $this->assertEquals( + "CREATE TABLE `t` (\n" + . " `name` varchar(255) DEFAULT 'CURRENT_TIMESTAMP',\n" + . " `type` varchar(255) NOT NULL DEFAULT 'DEFAULT',\n" + . " `description` varchar(250) NOT NULL DEFAULT '',\n" + . " `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,\n" + . " `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP\n" + . ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci', + $result[0]->{'Create Table'} + ); + } + + public function testSelectNonExistentColumn(): void { + $this->assertQuery( + 'CREATE TABLE t (id INT)' + ); + + /* + * Here, we're basically testing that identifiers are escaped using + * backticks instead of double quotes. In SQLite, double quotes may + * fallback to a string literal and thus produce no error. + * + * See: + * https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted + */ + $this->expectExceptionMessage( 'no such column: non_existent_column' ); + $this->assertQuery( 'SELECT non_existent_column FROM t LIMIT 0' ); + } + + public function testUnion(): void { + $this->assertQuery( 'CREATE TABLE t (id INT, name VARCHAR(20))' ); + $this->assertQuery( "INSERT INTO t (id, name) VALUES (1, 'a')" ); + $this->assertQuery( "INSERT INTO t (id, name) VALUES (2, 'b')" ); + + $this->assertQuery( + 'SELECT name FROM t WHERE id = 1 UNION SELECT name FROM t WHERE id = 2' + ); + $this->assertEquals( + array( + (object) array( 'name' => 'a' ), + (object) array( 'name' => 'b' ), + ), + $this->engine->get_query_results() + ); + + $this->assertQuery( + 'SELECT name FROM t WHERE id = 1 UNION SELECT name FROM t WHERE id = 1' + ); + $this->assertEquals( + array( + (object) array( 'name' => 'a' ), + ), + $this->engine->get_query_results() + ); + } + + public function testUnionAll(): void { + $this->assertQuery( 'CREATE TABLE t (id INT, name VARCHAR(20))' ); + $this->assertQuery( "INSERT INTO t (id, name) VALUES (1, 'a')" ); + $this->assertQuery( "INSERT INTO t (id, name) VALUES (2, 'b')" ); + + $this->assertQuery( + 'SELECT name FROM t WHERE id = 1 UNION SELECT name FROM t WHERE id = 2' + ); + $this->assertEquals( + array( + (object) array( 'name' => 'a' ), + (object) array( 'name' => 'b' ), + ), + $this->engine->get_query_results() + ); + + $this->assertQuery( + 'SELECT name FROM t WHERE id = 1 UNION ALL SELECT name FROM t WHERE id = 1' + ); + $this->assertEquals( + array( + (object) array( 'name' => 'a' ), + (object) array( 'name' => 'a' ), + ), + $this->engine->get_query_results() + ); + } +} diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php new file mode 100644 index 0000000..2f1bed3 --- /dev/null +++ b/tests/WP_SQLite_Driver_Translation_Tests.php @@ -0,0 +1,1340 @@ +driver = new WP_SQLite_Driver( + array( + 'path' => ':memory:', + 'database' => 'wp', + ) + ); + } + + public function testSelect(): void { + $this->assertQuery( + 'SELECT 1', + 'SELECT 1' + ); + + $this->assertQuery( + 'SELECT * FROM `t`', + 'SELECT * FROM t' + ); + + $this->assertQuery( + 'SELECT `c` FROM `t`', + 'SELECT c FROM t' + ); + + $this->assertQuery( + 'SELECT ALL `c` FROM `t`', + 'SELECT ALL c FROM t' + ); + + $this->assertQuery( + 'SELECT DISTINCT `c` FROM `t`', + 'SELECT DISTINCT c FROM t' + ); + + $this->assertQuery( + 'SELECT `c1` , `c2` FROM `t`', + 'SELECT c1, c2 FROM t' + ); + + $this->assertQuery( + 'SELECT `t`.`c` FROM `t`', + 'SELECT t.c FROM t' + ); + + $this->assertQuery( + 'SELECT `c1` FROM `t` WHERE `c2` = \'abc\'', + "SELECT c1 FROM t WHERE c2 = 'abc'" + ); + + $this->assertQuery( + 'SELECT `c` FROM `t` GROUP BY `c`', + 'SELECT c FROM t GROUP BY c' + ); + + $this->assertQuery( + 'SELECT `c` FROM `t` ORDER BY `c` ASC', + 'SELECT c FROM t ORDER BY c ASC' + ); + + $this->assertQuery( + 'SELECT `c` FROM `t` LIMIT 10', + 'SELECT c FROM t LIMIT 10' + ); + + $this->assertQuery( + 'SELECT `c` FROM `t` GROUP BY `c` HAVING COUNT ( `c` ) > 1', + 'SELECT c FROM t GROUP BY c HAVING COUNT(c) > 1' + ); + + $this->assertQuery( + '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'" + ); + } + + public function testInsert(): void { + $this->assertQuery( + 'INSERT INTO `t` ( `c` ) VALUES ( 1 )', + 'INSERT INTO t (c) VALUES (1)' + ); + + $this->assertQuery( + 'INSERT INTO `s`.`t` ( `c` ) VALUES ( 1 )', + 'INSERT INTO s.t (c) VALUES (1)' + ); + + $this->assertQuery( + 'INSERT INTO `t` ( `c1` , `c2` ) VALUES ( 1 , 2 )', + 'INSERT INTO t (c1, c2) VALUES (1, 2)' + ); + + $this->assertQuery( + 'INSERT INTO `t` ( `c` ) VALUES ( 1 ) , ( 2 )', + 'INSERT INTO t (c) VALUES (1), (2)' + ); + + $this->assertQuery( + 'INSERT INTO `t1` SELECT * FROM `t2`', + 'INSERT INTO t1 SELECT * FROM t2' + ); + } + + public function testReplace(): void { + $this->assertQuery( + 'REPLACE INTO `t` ( `c` ) VALUES ( 1 )', + 'REPLACE INTO t (c) VALUES (1)' + ); + + $this->assertQuery( + 'REPLACE INTO `s`.`t` ( `c` ) VALUES ( 1 )', + 'REPLACE INTO s.t (c) VALUES (1)' + ); + + $this->assertQuery( + 'REPLACE INTO `t` ( `c1` , `c2` ) VALUES ( 1 , 2 )', + 'REPLACE INTO t (c1, c2) VALUES (1, 2)' + ); + + $this->assertQuery( + 'REPLACE INTO `t` ( `c` ) VALUES ( 1 ) , ( 2 )', + 'REPLACE INTO t (c) VALUES (1), (2)' + ); + + $this->assertQuery( + 'REPLACE INTO `t1` SELECT * FROM `t2`', + 'REPLACE INTO t1 SELECT * FROM t2' + ); + } + + public function testUpdate(): void { + $this->assertQuery( + 'UPDATE `t` SET `c` = 1', + 'UPDATE t SET c = 1' + ); + + $this->assertQuery( + 'UPDATE `s`.`t` SET `c` = 1', + 'UPDATE s.t SET c = 1' + ); + + $this->assertQuery( + 'UPDATE `t` SET `c1` = 1 , `c2` = 2', + 'UPDATE t SET c1 = 1, c2 = 2' + ); + + $this->assertQuery( + 'UPDATE `t` SET `c` = 1 WHERE `c` = 2', + 'UPDATE t SET c = 1 WHERE c = 2' + ); + + // UPDATE with LIMIT. + $this->assertQuery( + 'UPDATE `t` SET `c` = 1 WHERE rowid IN ( SELECT rowid FROM `t` LIMIT 1 )', + 'UPDATE t SET c = 1 LIMIT 1' + ); + + // UPDATE with ORDER BY and LIMIT. + $this->assertQuery( + 'UPDATE `t` SET `c` = 1 WHERE rowid IN ( SELECT rowid FROM `t` ORDER BY `c` ASC LIMIT 1 )', + 'UPDATE t SET c = 1 ORDER BY c ASC LIMIT 1' + ); + } + + public function testDelete(): void { + $this->assertQuery( + 'DELETE FROM `t`', + 'DELETE FROM t' + ); + + $this->assertQuery( + 'DELETE FROM `s`.`t`', + 'DELETE FROM s.t' + ); + + $this->assertQuery( + 'DELETE FROM `t` WHERE `c` = 1', + 'DELETE FROM t WHERE c = 1' + ); + } + + public function testCreateTable(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `id` INTEGER ) STRICT', + 'CREATE TABLE t (id INT)' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testCreateTableWithMultipleColumns(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `id` INTEGER, `name` TEXT COLLATE NOCASE, `score` REAL DEFAULT \'0.0\' ) STRICT', + 'CREATE TABLE t (id INT, name TEXT, score FLOAT DEFAULT 0.0)' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'name', 2, null, 'YES', 'text', 65535, 65535, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'text', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'score', 3, '0.0', 'YES', 'float', null, null, 12, null, null, null, null, 'float', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testCreateTableWithBasicConstraints(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) STRICT', + 'CREATE TABLE t (id INT NOT NULL PRIMARY KEY AUTO_INCREMENT)' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'id', 1, null, 'NO', 'int', null, null, 10, 0, null, null, null, 'int', 'PRI', 'auto_increment', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)' + . " VALUES ('wp', 't', 0, 'wp', 'PRIMARY', 1, 'id', 'A', 0, null, null, '', 'BTREE', '', '', 'YES', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testCreateTableWithEngine(): void { + // ENGINE is not supported in SQLite, we save it in information schema. + $this->assertQuery( + 'CREATE TABLE `t` ( `id` INTEGER ) STRICT', + 'CREATE TABLE t (id INT) ENGINE=MyISAM' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'MyISAM', 'FIXED', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testCreateTableWithCollate(): void { + // COLLATE is not supported in SQLite, we save it in information schema. + $this->assertQuery( + 'CREATE TABLE `t` ( `id` INTEGER ) STRICT', + 'CREATE TABLE t (id INT) COLLATE utf8mb4_czech_ci' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_czech_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testCreateTableWithPrimaryKey(): void { + /* + * PRIMARY KEY without AUTOINCREMENT: + * In this case, integer must be represented as INT, not INTEGER. SQLite + * treats "INTEGER PRIMARY KEY" as an alias for ROWID, causing unintended + * auto-increment-like behavior for a non-autoincrement column. + * + * See: + * https://www.sqlite.org/lang_createtable.html#rowids_and_the_integer_primary_key + */ + $this->assertQuery( + 'CREATE TABLE `t` ( `id` INT NOT NULL, PRIMARY KEY (`id`) ) STRICT', + 'CREATE TABLE t (id INT PRIMARY KEY)' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'id', 1, null, 'NO', 'int', null, null, 10, 0, null, null, null, 'int', 'PRI', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)' + . " VALUES ('wp', 't', 0, 'wp', 'PRIMARY', 1, 'id', 'A', 0, null, null, '', 'BTREE', '', '', 'YES', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testCreateTableWithPrimaryKeyAndAutoincrement(): void { + // With AUTOINCREMENT, we expect "INTEGER". + $this->assertQuery( + 'CREATE TABLE `t1` ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) STRICT', + 'CREATE TABLE t1 (id INT PRIMARY KEY AUTO_INCREMENT)' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't1', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't1', 'id', 1, null, 'NO', 'int', null, null, 10, 0, null, null, null, 'int', 'PRI', 'auto_increment', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)' + . " VALUES ('wp', 't1', 0, 'wp', 'PRIMARY', 1, 'id', 'A', 0, null, null, '', 'BTREE', '', '', 'YES', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't1'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't1'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't1'", + ) + ); + + // In SQLite, PRIMARY KEY must come before AUTOINCREMENT. + $this->assertQuery( + 'CREATE TABLE `t2` ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) STRICT', + 'CREATE TABLE t2 (id INT AUTO_INCREMENT PRIMARY KEY)' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't2', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't2', 'id', 1, null, 'NO', 'int', null, null, 10, 0, null, null, null, 'int', 'PRI', 'auto_increment', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)' + . " VALUES ('wp', 't2', 0, 'wp', 'PRIMARY', 1, 'id', 'A', 0, null, null, '', 'BTREE', '', '', 'YES', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't2'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't2'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't2'", + ) + ); + + // In SQLite, AUTOINCREMENT cannot be specified separately from PRIMARY KEY. + $this->assertQuery( + 'CREATE TABLE `t3` ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) STRICT', + 'CREATE TABLE t3 (id INT AUTO_INCREMENT, PRIMARY KEY(id))' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't3', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't3', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', 'auto_increment', 'select,insert,update,references', '', '', null)", + "SELECT column_name, data_type, is_nullable, character_maximum_length FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't3' AND column_name IN ('id')", + 'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)' + . " VALUES ('wp', 't3', 0, 'wp', 'PRIMARY', 1, 'id', 'A', 0, null, null, '', 'BTREE', '', '', 'YES', null)", + "WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't3' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't3' AND s.column_name = c.column_name", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't3'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't3'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't3'", + ) + ); + } + + // @TODO: IF NOT EXISTS + /*public function testCreateTableWithIfNotExists(): void { + $this->assertQuery( + 'CREATE TABLE IF NOT EXISTS "t" ( "id" INTEGER ) STRICT', + 'CREATE TABLE IF NOT EXISTS t (id INT)' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)", + ) + ); + }*/ + + public function testCreateTableWithInlineUniqueIndexes(): void { + $this->assertQuery( + array( + 'CREATE TABLE `t` ( `id` INTEGER, `name` TEXT COLLATE NOCASE ) STRICT', + 'CREATE UNIQUE INDEX `t__id` ON `t` (`id`)', + 'CREATE UNIQUE INDEX `t__name` ON `t` (`name`)', + ), + 'CREATE TABLE t (id INT UNIQUE, name TEXT UNIQUE)' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', 'UNI', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)' + . " VALUES ('wp', 't', 0, 'wp', 'id', 1, 'id', 'A', 0, null, null, 'YES', 'BTREE', '', '', 'YES', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'name', 2, null, 'YES', 'text', 65535, 65535, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'text', 'UNI', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)' + . " VALUES ('wp', 't', 0, 'wp', 'name', 1, 'name', 'A', 0, null, null, 'YES', 'BTREE', '', '', 'YES', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testCreateTableWithStandaloneUniqueIndexes(): void { + $this->assertQuery( + array( + 'CREATE TABLE `t` ( `id` INTEGER, `name` TEXT COLLATE NOCASE ) STRICT', + 'CREATE UNIQUE INDEX `t__id` ON `t` (`id`)', + 'CREATE UNIQUE INDEX `t__name` ON `t` (`name`)', + ), + 'CREATE TABLE t (id INT, name VARCHAR(100), UNIQUE (id), UNIQUE (name))' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'name', 2, null, 'YES', 'varchar', 100, 400, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'varchar(100)', '', '', 'select,insert,update,references', '', '', null)", + "SELECT column_name, data_type, is_nullable, character_maximum_length FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't' AND column_name IN ('id')", + 'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)' + . " VALUES ('wp', 't', 0, 'wp', 'id', 1, 'id', 'A', 0, null, null, 'YES', 'BTREE', '', '', 'YES', null)", + "WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name", + "SELECT column_name, data_type, is_nullable, character_maximum_length FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't' AND column_name IN ('name')", + 'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)' + . " VALUES ('wp', 't', 0, 'wp', 'name', 1, 'name', 'A', 0, null, null, 'YES', 'BTREE', '', '', 'YES', null)", + "WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testCreateTableFromSelectQuery(): void { + // CREATE TABLE AS SELECT ... + $this->assertQuery( + 'CREATE TABLE `t1` AS SELECT * FROM `t2` STRICT', + 'CREATE TABLE t1 AS SELECT * FROM t2' + ); + + // CREATE TABLE SELECT ... + // The "AS" keyword is optional in MySQL, but required in SQLite. + $this->assertQuery( + 'CREATE TABLE `t1` AS SELECT * FROM `t2` STRICT', + 'CREATE TABLE t1 SELECT * FROM t2' + ); + } + + public function testCreateTemporaryTable(): void { + $this->assertQuery( + 'CREATE TEMPORARY TABLE `t` ( `id` INTEGER ) STRICT', + 'CREATE TEMPORARY TABLE t (id INT)' + ); + + // With IF NOT EXISTS. + $this->assertQuery( + 'CREATE TEMPORARY TABLE IF NOT EXISTS `t` ( `id` INTEGER ) STRICT', + 'CREATE TEMPORARY TABLE IF NOT EXISTS t (id INT)' + ); + } + + public function testAlterTableAddColumn(): void { + $this->driver->query( 'CREATE TABLE t (id INT)' ); + $this->assertQuery( + array( + 'PRAGMA foreign_keys', + 'PRAGMA foreign_keys = OFF', + 'CREATE TABLE `` ( `id` INTEGER, `a` INTEGER ) STRICT', + 'INSERT INTO `` (`rowid`, `id`) SELECT `rowid`, `id` FROM `t`', + 'DROP TABLE `t`', + 'ALTER TABLE `` RENAME TO `t`', + 'PRAGMA foreign_key_check', + 'PRAGMA foreign_keys = ON', + ), + 'ALTER TABLE t ADD a INT' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + "SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'a', 2, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testAlterTableAddColumnWithNotNull(): void { + $this->driver->query( 'CREATE TABLE t (id INT)' ); + $this->assertQuery( + array( + 'PRAGMA foreign_keys', + 'PRAGMA foreign_keys = OFF', + 'CREATE TABLE `` ( `id` INTEGER, `a` INTEGER NOT NULL ) STRICT', + 'INSERT INTO `` (`rowid`, `id`) SELECT `rowid`, `id` FROM `t`', + 'DROP TABLE `t`', + 'ALTER TABLE `` RENAME TO `t`', + 'PRAGMA foreign_key_check', + 'PRAGMA foreign_keys = ON', + ), + 'ALTER TABLE t ADD a INT NOT NULL' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + "SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'a', 2, null, 'NO', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testAlterTableAddColumnWithDefault(): void { + $this->driver->query( 'CREATE TABLE t (id INT)' ); + $this->assertQuery( + array( + 'PRAGMA foreign_keys', + 'PRAGMA foreign_keys = OFF', + 'CREATE TABLE `` ( `id` INTEGER, `a` INTEGER DEFAULT \'0\' ) STRICT', + 'INSERT INTO `` (`rowid`, `id`) SELECT `rowid`, `id` FROM `t`', + 'DROP TABLE `t`', + 'ALTER TABLE `` RENAME TO `t`', + 'PRAGMA foreign_key_check', + 'PRAGMA foreign_keys = ON', + ), + 'ALTER TABLE t ADD a INT DEFAULT 0' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + "SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'a', 2, '0', 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testAlterTableAddColumnWithNotNullAndDefault(): void { + $this->driver->query( 'CREATE TABLE t (id INT)' ); + $this->assertQuery( + array( + 'PRAGMA foreign_keys', + 'PRAGMA foreign_keys = OFF', + 'CREATE TABLE `` ( `id` INTEGER, `a` INTEGER NOT NULL DEFAULT \'0\' ) STRICT', + 'INSERT INTO `` (`rowid`, `id`) SELECT `rowid`, `id` FROM `t`', + 'DROP TABLE `t`', + 'ALTER TABLE `` RENAME TO `t`', + 'PRAGMA foreign_key_check', + 'PRAGMA foreign_keys = ON', + ), + 'ALTER TABLE t ADD a INT NOT NULL DEFAULT 0' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + "SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'a', 2, '0', 'NO', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testAlterTableAddMultipleColumns(): void { + $this->driver->query( 'CREATE TABLE t (id INT)' ); + $this->assertQuery( + array( + 'PRAGMA foreign_keys', + 'PRAGMA foreign_keys = OFF', + 'CREATE TABLE `` ( `id` INTEGER, `a` INTEGER, `b` TEXT COLLATE NOCASE, `c` INTEGER ) STRICT', + 'INSERT INTO `` (`rowid`, `id`) SELECT `rowid`, `id` FROM `t`', + 'DROP TABLE `t`', + 'ALTER TABLE `` RENAME TO `t`', + 'PRAGMA foreign_key_check', + 'PRAGMA foreign_keys = ON', + ), + 'ALTER TABLE t ADD a INT, ADD b TEXT, ADD c BOOL' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + "SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'a', 2, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)", + "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'b', 3, null, 'YES', 'text', 65535, 65535, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'text', '', '', 'select,insert,update,references', '', '', null)", + "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'c', 4, null, 'YES', 'tinyint', null, null, 3, 0, null, null, null, 'tinyint(1)', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testAlterTableDropColumn(): void { + $this->driver->query( 'CREATE TABLE t (id INT, a TEXT)' ); + $this->assertQuery( + array( + 'PRAGMA foreign_keys', + 'PRAGMA foreign_keys = OFF', + 'CREATE TABLE `` ( `id` INTEGER ) STRICT', + 'INSERT INTO `` (`rowid`, `id`) SELECT `rowid`, `id` FROM `t`', + 'DROP TABLE `t`', + 'ALTER TABLE `` RENAME TO `t`', + 'PRAGMA foreign_key_check', + 'PRAGMA foreign_keys = ON', + ), + 'ALTER TABLE t DROP a' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + "SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "DELETE FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't' AND column_name = 'a'", + "DELETE FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' AND column_name = 'a'", + "WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); } + + public function testAlterTableDropMultipleColumns(): void { + $this->driver->query( 'CREATE TABLE t (id INT, a INT, b TEXT)' ); + $this->assertQuery( + array( + 'PRAGMA foreign_keys', + 'PRAGMA foreign_keys = OFF', + 'CREATE TABLE `` ( `id` INTEGER ) STRICT', + 'INSERT INTO `` (`rowid`, `id`) SELECT `rowid`, `id` FROM `t`', + 'DROP TABLE `t`', + 'ALTER TABLE `` RENAME TO `t`', + 'PRAGMA foreign_key_check', + 'PRAGMA foreign_keys = ON', + ), + 'ALTER TABLE t DROP a, DROP b' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + "SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "DELETE FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't' AND column_name = 'a'", + "DELETE FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' AND column_name = 'a'", + "WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name", + "DELETE FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't' AND column_name = 'b'", + "DELETE FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' AND column_name = 'b'", + "WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testAlterTableAddAndDropColumns(): void { + $this->driver->query( 'CREATE TABLE t (a INT)' ); + $this->assertQuery( + array( + 'PRAGMA foreign_keys', + 'PRAGMA foreign_keys = OFF', + 'CREATE TABLE `` ( `b` INTEGER ) STRICT', + 'INSERT INTO `` (`rowid`) SELECT `rowid` FROM `t`', + 'DROP TABLE `t`', + 'ALTER TABLE `` RENAME TO `t`', + 'PRAGMA foreign_key_check', + 'PRAGMA foreign_keys = ON', + ), + 'ALTER TABLE t ADD b INT, DROP a' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + "SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'b', 2, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)", + "DELETE FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't' AND column_name = 'a'", + "DELETE FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' AND column_name = 'a'", + "WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testAlterTableDropAndAddSingleColumn(): void { + $this->driver->query( 'CREATE TABLE t (a INT)' ); + $this->assertQuery( + array( + 'PRAGMA foreign_keys', + 'PRAGMA foreign_keys = OFF', + 'CREATE TABLE `` ( `a` INTEGER ) STRICT', + 'INSERT INTO `` (`rowid`) SELECT `rowid` FROM `t`', + 'DROP TABLE `t`', + 'ALTER TABLE `` RENAME TO `t`', + 'PRAGMA foreign_key_check', + 'PRAGMA foreign_keys = ON', + ), + 'ALTER TABLE t DROP a, ADD a INT' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + "SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "DELETE FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't' AND column_name = 'a'", + "DELETE FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' AND column_name = 'a'", + "WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name", + "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'a', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testBitDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `i1` INTEGER, `i2` INTEGER ) STRICT', + 'CREATE TABLE t (i1 BIT, i2 BIT(10))' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'i1', 1, null, 'YES', 'bit', null, null, 1, null, null, null, null, 'bit(1)', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'i2', 2, null, 'YES', 'bit', null, null, 10, null, null, null, null, 'bit(10)', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testBooleanDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `i1` INTEGER, `i2` INTEGER ) STRICT', + 'CREATE TABLE t (i1 BOOL, i2 BOOLEAN)' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'i1', 1, null, 'YES', 'tinyint', null, null, 3, 0, null, null, null, 'tinyint(1)', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'i2', 2, null, 'YES', 'tinyint', null, null, 3, 0, null, null, null, 'tinyint(1)', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testIntegerDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `i1` INTEGER, `i2` INTEGER, `i3` INTEGER, `i4` INTEGER, `i5` INTEGER, `i6` INTEGER ) STRICT', + 'CREATE TABLE t (i1 TINYINT, i2 SMALLINT, i3 MEDIUMINT, i4 INT, i5 INTEGER, i6 BIGINT)' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'i1', 1, null, 'YES', 'tinyint', null, null, 3, 0, null, null, null, 'tinyint', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'i2', 2, null, 'YES', 'smallint', null, null, 5, 0, null, null, null, 'smallint', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'i3', 3, null, 'YES', 'mediumint', null, null, 7, 0, null, null, null, 'mediumint', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'i4', 4, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'i5', 5, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'i6', 6, null, 'YES', 'bigint', null, null, 19, 0, null, null, null, 'bigint', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testFloatDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `f1` REAL, `f2` REAL, `f3` REAL, `f4` REAL ) STRICT', + 'CREATE TABLE t (f1 FLOAT, f2 DOUBLE, f3 DOUBLE PRECISION, f4 REAL)' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'f1', 1, null, 'YES', 'float', null, null, 12, null, null, null, null, 'float', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'f2', 2, null, 'YES', 'double', null, null, 22, null, null, null, null, 'double', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'f3', 3, null, 'YES', 'double', null, null, 22, null, null, null, null, 'double', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'f4', 4, null, 'YES', 'double', null, null, 22, null, null, null, null, 'double', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testDecimalTypes(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `f1` REAL, `f2` REAL, `f3` REAL, `f4` REAL ) STRICT', + 'CREATE TABLE t (f1 DECIMAL, f2 DEC, f3 FIXED, f4 NUMERIC)' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'f1', 1, null, 'YES', 'decimal', null, null, 10, 0, null, null, null, 'decimal(10,0)', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'f2', 2, null, 'YES', 'decimal', null, null, 10, 0, null, null, null, 'decimal(10,0)', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'f3', 3, null, 'YES', 'decimal', null, null, 10, 0, null, null, null, 'decimal(10,0)', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'f4', 4, null, 'YES', 'decimal', null, null, 10, 0, null, null, null, 'decimal(10,0)', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testCharDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `c1` TEXT COLLATE NOCASE, `c2` TEXT COLLATE NOCASE ) STRICT', + 'CREATE TABLE t (c1 CHAR, c2 CHAR(10))' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'c1', 1, null, 'YES', 'char', 1, 4, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'char(1)', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'c2', 2, null, 'YES', 'char', 10, 40, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'char(10)', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testVarcharDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `c1` TEXT COLLATE NOCASE, `c2` TEXT COLLATE NOCASE, `c3` TEXT COLLATE NOCASE ) STRICT', + 'CREATE TABLE t (c1 VARCHAR(255), c2 CHAR VARYING(255), c3 CHARACTER VARYING(255))' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'c1', 1, null, 'YES', 'varchar', 255, 1020, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'c2', 2, null, 'YES', 'varchar', 255, 1020, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'c3', 3, null, 'YES', 'varchar', 255, 1020, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testNationalCharDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `c1` TEXT COLLATE NOCASE, `c2` TEXT COLLATE NOCASE, `c3` TEXT COLLATE NOCASE, `c4` TEXT COLLATE NOCASE ) STRICT', + 'CREATE TABLE t (c1 NATIONAL CHAR, c2 NCHAR, c3 NATIONAL CHAR (10), c4 NCHAR(10))' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'c1', 1, null, 'YES', 'char', 1, 3, null, null, null, 'utf8', 'utf8_general_ci', 'char(1)', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'c2', 2, null, 'YES', 'char', 1, 3, null, null, null, 'utf8', 'utf8_general_ci', 'char(1)', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'c3', 3, null, 'YES', 'char', 10, 30, null, null, null, 'utf8', 'utf8_general_ci', 'char(10)', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'c4', 4, null, 'YES', 'char', 10, 30, null, null, null, 'utf8', 'utf8_general_ci', 'char(10)', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testNcharVarcharDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `c1` TEXT COLLATE NOCASE, `c2` TEXT COLLATE NOCASE, `c3` TEXT COLLATE NOCASE ) STRICT', + 'CREATE TABLE t (c1 NCHAR VARCHAR(255), c2 NCHAR VARYING(255), c3 NVARCHAR(255))' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'c1', 1, null, 'YES', 'varchar', 255, 765, null, null, null, 'utf8', 'utf8_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'c2', 2, null, 'YES', 'varchar', 255, 765, null, null, null, 'utf8', 'utf8_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'c3', 3, null, 'YES', 'varchar', 255, 765, null, null, null, 'utf8', 'utf8_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testNationalVarcharDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `c1` TEXT COLLATE NOCASE, `c2` TEXT COLLATE NOCASE, `c3` TEXT COLLATE NOCASE ) STRICT', + 'CREATE TABLE t (c1 NATIONAL VARCHAR(255), c2 NATIONAL CHAR VARYING(255), c3 NATIONAL CHARACTER VARYING(255))' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'c1', 1, null, 'YES', 'varchar', 255, 765, null, null, null, 'utf8', 'utf8_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'c2', 2, null, 'YES', 'varchar', 255, 765, null, null, null, 'utf8', 'utf8_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'c3', 3, null, 'YES', 'varchar', 255, 765, null, null, null, 'utf8', 'utf8_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testTextDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `t1` TEXT COLLATE NOCASE, `t2` TEXT COLLATE NOCASE, `t3` TEXT COLLATE NOCASE, `t4` TEXT COLLATE NOCASE ) STRICT', + 'CREATE TABLE t (t1 TINYTEXT, t2 TEXT, t3 MEDIUMTEXT, t4 LONGTEXT)' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 't1', 1, null, 'YES', 'tinytext', 255, 255, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'tinytext', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 't2', 2, null, 'YES', 'text', 65535, 65535, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'text', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 't3', 3, null, 'YES', 'mediumtext', 16777215, 16777215, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'mediumtext', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 't4', 4, null, 'YES', 'longtext', 4294967295, 4294967295, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'longtext', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testEnumDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `e` TEXT COLLATE NOCASE ) STRICT', + 'CREATE TABLE t (e ENUM("a", "b", "c"))' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'e', 1, null, 'YES', 'enum', 1, 4, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'enum(''a'',''b'',''c'')', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testDateAndTimeDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `d` TEXT COLLATE NOCASE, `t` TEXT COLLATE NOCASE, `dt` TEXT COLLATE NOCASE, `ts` TEXT COLLATE NOCASE, `y` TEXT COLLATE NOCASE ) STRICT', + 'CREATE TABLE t (d DATE, t TIME, dt DATETIME, ts TIMESTAMP, y YEAR)' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'd', 1, null, 'YES', 'date', null, null, null, null, null, null, null, 'date', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 't', 2, null, 'YES', 'time', null, null, null, null, 0, null, null, 'time', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'dt', 3, null, 'YES', 'datetime', null, null, null, null, 0, null, null, 'datetime', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'ts', 4, null, 'YES', 'timestamp', null, null, null, null, 0, null, null, 'timestamp', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'y', 5, null, 'YES', 'year', null, null, null, null, null, null, null, 'year', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testBinaryDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `b` INTEGER, `v` BLOB ) STRICT', + 'CREATE TABLE t (b BINARY, v VARBINARY(255))' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'b', 1, null, 'YES', 'binary', 1, 1, null, null, null, null, null, 'binary(1)', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'v', 2, null, 'YES', 'varbinary', 255, 255, null, null, null, null, null, 'varbinary(255)', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testBlobDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `b1` BLOB, `b2` BLOB, `b3` BLOB, `b4` BLOB ) STRICT', + 'CREATE TABLE t (b1 TINYBLOB, b2 BLOB, b3 MEDIUMBLOB, b4 LONGBLOB)' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'b1', 1, null, 'YES', 'tinyblob', 255, 255, null, null, null, null, null, 'tinyblob', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'b2', 2, null, 'YES', 'blob', 65535, 65535, null, null, null, null, null, 'blob', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'b3', 3, null, 'YES', 'mediumblob', 16777215, 16777215, null, null, null, null, null, 'mediumblob', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'b4', 4, null, 'YES', 'longblob', 4294967295, 4294967295, null, null, null, null, null, 'longblob', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testBasicSpatialDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `g1` TEXT COLLATE NOCASE, `g2` TEXT COLLATE NOCASE, `g3` TEXT COLLATE NOCASE, `g4` TEXT COLLATE NOCASE ) STRICT', + 'CREATE TABLE t (g1 GEOMETRY, g2 POINT, g3 LINESTRING, g4 POLYGON)' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'g1', 1, null, 'YES', 'geometry', null, null, null, null, null, null, null, 'geometry', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'g2', 2, null, 'YES', 'point', null, null, null, null, null, null, null, 'point', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'g3', 3, null, 'YES', 'linestring', null, null, null, null, null, null, null, 'linestring', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'g4', 4, null, 'YES', 'polygon', null, null, null, null, null, null, null, 'polygon', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testMultiObjectSpatialDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `g1` TEXT COLLATE NOCASE, `g2` TEXT COLLATE NOCASE, `g3` TEXT COLLATE NOCASE ) STRICT', + 'CREATE TABLE t (g1 MULTIPOINT, g2 MULTILINESTRING, g3 MULTIPOLYGON)' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'g1', 1, null, 'YES', 'multipoint', null, null, null, null, null, null, null, 'multipoint', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'g2', 2, null, 'YES', 'multilinestring', null, null, null, null, null, null, null, 'multilinestring', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'g3', 3, null, 'YES', 'multipolygon', null, null, null, null, null, null, null, 'multipolygon', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testGeometryCollectionDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `g1` TEXT COLLATE NOCASE, `g2` TEXT COLLATE NOCASE ) STRICT', + 'CREATE TABLE t (g1 GEOMCOLLECTION, g2 GEOMETRYCOLLECTION)' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'g1', 1, null, 'YES', 'geomcollection', null, null, null, null, null, null, null, 'geomcollection', '', '', 'select,insert,update,references', '', '', null)", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'g2', 2, null, 'YES', 'geomcollection', null, null, null, null, null, null, null, 'geomcollection', '', '', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testSerialDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE `t` ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) STRICT', + 'CREATE TABLE t (id SERIAL)' + ); + + $this->assertExecutedInformationSchemaQueries( + array( + 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)' + . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')", + 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)' + . " VALUES ('wp', 't', 'id', 1, null, 'NO', 'bigint', null, null, 20, 0, null, null, null, 'bigint unsigned', 'PRI', 'auto_increment', 'select,insert,update,references', '', '', null)", + "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'", + ) + ); + } + + public function testSystemVariables(): void { + $this->assertQuery( + 'SELECT NULL', + 'SELECT @@sql_mode' + ); + + $this->assertQuery( + 'SELECT NULL', + 'SELECT @@SESSION.sql_mode' + ); + + $this->assertQuery( + 'SELECT NULL', + 'SELECT @@GLOBAL.sql_mode' + ); + } + + public function testConcatFunction(): void { + $this->assertQuery( + "SELECT ('a' || 'b' || 'c')", + 'SELECT CONCAT("a", "b", "c")' + ); + } + + private function assertQuery( $expected, string $query ): void { + $error = null; + try { + $this->driver->query( $query ); + } catch ( Throwable $e ) { + $error = $e->getMessage(); + } + + // Check for SQLite syntax errors. + // This ensures that invalid SQLite syntax will always fail, even if it + // was the expected result. It prevents us from using wrong assertions. + if ( $error && preg_match( '/(SQLSTATE\[HY000].+syntax error\.)/i', $error, $matches ) ) { + $this->fail( + sprintf( "SQLite syntax error: %s\nMySQL query: %s", $matches[1], $query ) + ); + } + + $executed_queries = array_column( $this->driver->get_last_sqlite_queries(), 'sql' ); + + // Remove BEGIN and COMMIT/ROLLBACK queries. + if ( count( $executed_queries ) > 2 ) { + $executed_queries = array_values( array_slice( $executed_queries, 1, -1, true ) ); + } + + // Remove "information_schema" queries. + $executed_queries = array_values( + array_filter( + $executed_queries, + function ( $query ) { + return ! str_contains( $query, '_mysql_information_schema_' ); + } + ) + ); + + // Remove "select changes()" executed after some queries. + if ( + count( $executed_queries ) > 1 + && 'SELECT CHANGES()' === $executed_queries[ count( $executed_queries ) - 1 ] ) { + array_pop( $executed_queries ); + } + + if ( ! is_array( $expected ) ) { + $expected = array( $expected ); + } + + // Normalize whitespace. + foreach ( $executed_queries as $key => $executed_query ) { + $executed_queries[ $key ] = trim( preg_replace( '/\s+/', ' ', $executed_query ) ); + } + + // Normalize temporary table names. + foreach ( $executed_queries as $key => $executed_query ) { + $executed_queries[ $key ] = preg_replace( '/`_wp_sqlite_tmp_[^`]+`/', '``', $executed_query ); + } + + $this->assertSame( $expected, $executed_queries ); + } + + private function assertExecutedInformationSchemaQueries( array $expected ): void { + // Collect and normalize "information_schema" queries. + $queries = array(); + foreach ( $this->driver->get_last_sqlite_queries() as $query ) { + if ( ! str_contains( $query['sql'], '_mysql_information_schema_' ) ) { + continue; + } + + // Normalize whitespace. + $sql = trim( preg_replace( '/\s+/', ' ', $query['sql'] ) ); + + // Inline parameters. + $sql = str_replace( '?', '%s', $sql ); + $queries[] = sprintf( + $sql, + ...array_map( + function ( $param ) { + if ( null === $param ) { + return 'null'; + } + if ( is_string( $param ) ) { + return $this->driver->get_pdo()->quote( $param ); + } + return $param; + }, + $query['params'] + ) + ); + } + $this->assertSame( $expected, $queries ); + } +} diff --git a/tests/WP_SQLite_PDO_User_Defined_Functions_Tests.php b/tests/WP_SQLite_PDO_User_Defined_Functions_Tests.php index 9824148..4ba6ee1 100644 --- a/tests/WP_SQLite_PDO_User_Defined_Functions_Tests.php +++ b/tests/WP_SQLite_PDO_User_Defined_Functions_Tests.php @@ -10,7 +10,7 @@ class WP_SQLite_PDO_User_Defined_Functions_Tests extends TestCase { */ public function testFieldFunction( $expected, $args ) { $pdo = new PDO( 'sqlite::memory:' ); - $fns = new WP_SQLite_PDO_User_Defined_Functions( $pdo ); + $fns = WP_SQLite_PDO_User_Defined_Functions::register_for( $pdo ); $this->assertEquals( $expected, diff --git a/tests/bootstrap.php b/tests/bootstrap.php index b663505..4ba0fc7 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,11 +1,12 @@ assertTrue( $lexer->next_token() ); - $this->assertSame( WP_MySQL_Lexer::SELECT_SYMBOL, $lexer->get_token()->get_type() ); + $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()->get_type() ); + $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()->get_type() ); + $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()->get_type() ); + $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()->get_type() ); + $this->assertSame( WP_MySQL_Lexer::EOF, $lexer->get_token()->id ); // No more tokens. $this->assertFalse( $lexer->next_token() ); @@ -40,7 +40,7 @@ public function test_tokenize_invalid_input(): void { // SELECT $this->assertTrue( $lexer->next_token() ); - $this->assertSame( WP_MySQL_Lexer::SELECT_SYMBOL, $lexer->get_token()->get_type() ); + $this->assertSame( WP_MySQL_Lexer::SELECT_SYMBOL, $lexer->get_token()->id ); // Invalid input. $this->assertFalse( $lexer->next_token() ); @@ -66,7 +66,7 @@ public function test_identifier_utf8_range(): void { $lexer = new WP_MySQL_Lexer( $value ); $this->assertTrue( $lexer->next_token() ); - $type = $lexer->get_token()->get_type(); + $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 ); @@ -95,7 +95,7 @@ public function test_identifier_utf8_two_byte_sequences(): void { $is_valid = preg_match( '/^[\x{0080}-\x{ffff}]$/u', $value ); if ( $is_valid ) { $this->assertTrue( $result ); - $this->assertSame( WP_MySQL_Lexer::IDENTIFIER, $token->get_type() ); + $this->assertSame( WP_MySQL_Lexer::IDENTIFIER, $token->id ); } else { $this->assertFalse( $result ); $this->assertNull( $token ); @@ -125,7 +125,7 @@ public function test_identifier_utf8_three_byte_sequences(): void { $is_valid = preg_match( '/^[\x{0080}-\x{ffff}]$/u', $value ); if ( $is_valid ) { $this->assertTrue( $result ); - $this->assertSame( WP_MySQL_Lexer::IDENTIFIER, $token->get_type() ); + $this->assertSame( WP_MySQL_Lexer::IDENTIFIER, $token->id ); } else { $this->assertFalse( $result ); $this->assertNull( $token ); @@ -141,7 +141,7 @@ public function test_identifier_utf8_three_byte_sequences(): void { 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()->get_type() ); + $this->assertSame( $expected, $lexer->get_token()->id ); } public function data_integer_types(): array { @@ -185,7 +185,7 @@ public function test_identifier_or_number( $input, $expected ): void { $lexer = new WP_MySQL_Lexer( $input ); $actual = array_map( function ( $token ) { - return $token->get_type(); + return $token->id; }, $lexer->remaining_tokens() ); diff --git a/tests/mysql/WP_MySQL_Server_Suite_Parser_Tests.php b/tests/mysql/WP_MySQL_Server_Suite_Parser_Tests.php index 6a8a96d..e6bb128 100644 --- a/tests/mysql/WP_MySQL_Server_Suite_Parser_Tests.php +++ b/tests/mysql/WP_MySQL_Server_Suite_Parser_Tests.php @@ -17,7 +17,6 @@ class WP_MySQL_Server_Suite_Parser_Tests extends TestCase { 'SELECT 1 /*!99999 /* */ */' => true, 'select 1ea10.1a20,1e+ 1e+10 from 1ea10' => true, "聠聡聢聣聤聬聭聮聯聰聲聽隆垄拢陇楼卤潞禄录陆戮 聶職聳聴\n0聲5\n1聲5\n2聲5\n3聲5\n4\n\nSET NAMES gb18030" => true, - 'CREATE TABLE t1 (g GEOMCOLLECTION)' => 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, diff --git a/tests/parser/WP_Parser_Node_Tests.php b/tests/parser/WP_Parser_Node_Tests.php new file mode 100644 index 0000000..6f01d21 --- /dev/null +++ b/tests/parser/WP_Parser_Node_Tests.php @@ -0,0 +1,144 @@ +assertFalse( $node->has_child() ); + $this->assertFalse( $node->has_child_node() ); + $this->assertFalse( $node->has_child_token() ); + + $this->assertNull( $node->get_first_child() ); + $this->assertNull( $node->get_first_child_node() ); + $this->assertNull( $node->get_first_child_node( 'root' ) ); + $this->assertNull( $node->get_first_child_token() ); + $this->assertNull( $node->get_first_child_token( 1 ) ); + + $this->assertNull( $node->get_first_descendant_node() ); + $this->assertNull( $node->get_first_descendant_token() ); + + $this->assertEmpty( $node->get_children() ); + $this->assertEmpty( $node->get_child_nodes() ); + $this->assertEmpty( $node->get_child_nodes( 'root' ) ); + $this->assertEmpty( $node->get_child_tokens() ); + $this->assertEmpty( $node->get_child_tokens( 1 ) ); + + $this->assertEmpty( $node->get_descendants() ); + $this->assertEmpty( $node->get_descendant_nodes() ); + $this->assertEmpty( $node->get_descendant_nodes( 'root' ) ); + $this->assertEmpty( $node->get_descendant_tokens() ); + $this->assertEmpty( $node->get_descendant_tokens( 1 ) ); + } + + public function testNodeTree(): void { + // Prepare nodes and tokens. + $root = new WP_Parser_Node( 1, 'root' ); + $n_keyword = new WP_Parser_Node( 2, 'keyword' ); + $n_expr_a = new WP_Parser_Node( 3, 'expr' ); + $n_expr_b = new WP_Parser_Node( 3, 'expr' ); + $n_expr_c = new WP_Parser_Node( 3, 'expr' ); + $t_select = new WP_Parser_Token( 100, 'SELECT' ); + $t_comma = new WP_Parser_Token( 200, ',' ); + $t_plus = new WP_Parser_Token( 300, '+' ); + $t_one = new WP_Parser_Token( 400, '1' ); + $t_two_a = new WP_Parser_Token( 400, '2' ); + $t_two_b = new WP_Parser_Token( 400, '2' ); + $t_eof = new WP_Parser_Token( 500, '' ); + + // Prepare a tree. + // + // A simplified testing tree for an input like "SELECT 1 + 2, 2". + // + // root + // |- keyword + // | |- "SELECT" + // |- expr [a] + // | |- "1" + // | |- "+" + // | |- expr [c] + // | | |- "2" [b] + // |- "," + // |- expr [b] + // | |- "2" [a] + // |- EOF + $root->append_child( $n_keyword ); + $root->append_child( $n_expr_a ); + $root->append_child( $t_comma ); + $root->append_child( $n_expr_b ); + $root->append_child( $t_eof ); + + $n_keyword->append_child( $t_select ); + $n_expr_a->append_child( $t_one ); + $n_expr_a->append_child( $t_plus ); + $n_expr_a->append_child( $n_expr_c ); + $n_expr_b->append_child( $t_two_a ); + $n_expr_c->append_child( $t_two_b ); + + // Test "has" methods. + $this->assertTrue( $root->has_child() ); + $this->assertTrue( $root->has_child_node() ); + $this->assertTrue( $root->has_child_token() ); + + // Test single child methods. + $this->assertSame( $n_keyword, $root->get_first_child() ); + $this->assertSame( $n_keyword, $root->get_first_child_node() ); + $this->assertSame( $n_keyword, $root->get_first_child_node( 'keyword' ) ); + $this->assertSame( $n_expr_a, $root->get_first_child_node( 'expr' ) ); + $this->assertSame( $t_comma, $root->get_first_child_token() ); + $this->assertSame( $t_comma, $root->get_first_child_token( 200 ) ); + $this->assertNull( $root->get_first_child_token( 100 ) ); + + // Test multiple children methods. + $this->assertSame( array( $n_keyword, $n_expr_a, $t_comma, $n_expr_b, $t_eof ), $root->get_children() ); + $this->assertSame( array( $n_keyword, $n_expr_a, $n_expr_b ), $root->get_child_nodes() ); + $this->assertSame( array( $n_expr_a, $n_expr_b ), $root->get_child_nodes( 'expr' ) ); + $this->assertSame( array(), $root->get_child_nodes( 'root' ) ); + $this->assertSame( array( $t_comma, $t_eof ), $root->get_child_tokens() ); + $this->assertSame( array( $t_comma ), $root->get_child_tokens( 200 ) ); + $this->assertSame( array(), $root->get_child_tokens( 100 ) ); + + // Test single descendant methods. + // @TODO: Consider breadth-first search vs depth-first search. + $this->assertSame( $n_keyword, $root->get_first_descendant_node() ); + $this->assertSame( $n_expr_a, $root->get_first_descendant_node( 'expr' ) ); + $this->assertSame( null, $root->get_first_descendant_node( 'root' ) ); + $this->assertSame( $t_comma, $root->get_first_descendant_token() ); + $this->assertSame( $t_one, $root->get_first_descendant_token( 400 ) ); + $this->assertSame( null, $root->get_first_descendant_token( 123 ) ); + + // Test multiple descendant methods. + // @TODO: Consider breadth-first search vs depth-first search. + $this->assertSame( + array( $n_keyword, $n_expr_a, $t_comma, $n_expr_b, $t_eof, $t_select, $t_one, $t_plus, $n_expr_c, $t_two_a, $t_two_b ), + $root->get_descendants() + ); + $this->assertSame( + array( $n_keyword, $n_expr_a, $n_expr_b, $n_expr_c ), + $root->get_descendant_nodes() + ); + $this->assertSame( + array( $n_expr_a, $n_expr_b, $n_expr_c ), + $root->get_descendant_nodes( 'expr' ) + ); + $this->assertSame( + array(), + $root->get_descendant_nodes( 'root' ) + ); + $this->assertSame( + array( $t_comma, $t_eof, $t_select, $t_one, $t_plus, $t_two_a, $t_two_b ), + $root->get_descendant_tokens() + ); + $this->assertSame( + array( $t_one, $t_two_a, $t_two_b ), + $root->get_descendant_tokens( 400 ) + ); + $this->assertSame( + array(), + $root->get_descendant_tokens( 123 ) + ); + } +} diff --git a/tests/tools/dump-ast.php b/tests/tools/dump-ast.php index 5fa512d..0f2b8ee 100644 --- a/tests/tools/dump-ast.php +++ b/tests/tools/dump-ast.php @@ -12,12 +12,13 @@ function ( $severity, $message, $file, $line ) { } ); -require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-token.php'; -require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-lexer.php'; require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser.php'; require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-grammar.php'; require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-node.php'; +require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-token.php'; +require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-lexer.php'; require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-parser.php'; +require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-token.php'; $grammar_data = include __DIR__ . '/../../wp-includes/mysql/mysql-grammar.php'; $grammar = new WP_Parser_Grammar( $grammar_data ); diff --git a/tests/tools/dump-sqlite-query.php b/tests/tools/dump-sqlite-query.php new file mode 100644 index 0000000..3282262 --- /dev/null +++ b/tests/tools/dump-sqlite-query.php @@ -0,0 +1,35 @@ + ':memory:', + 'database' => 'wp', + ) +); + +$query = "SELECT * FROM t1 LEFT JOIN t2 ON t1.id = t2.t1_id WHERE t1.name = 'abc'"; + +$driver->query( $query ); + +$executed_queries = $driver->get_last_sqlite_queries(); +if ( count( $executed_queries ) > 2 ) { + // Remove BEGIN and COMMIT/ROLLBACK queries. + $executed_queries = array_values( array_slice( $executed_queries, 1, -1, true ) ); +} + +foreach ( $executed_queries as $executed_query ) { + printf( "Query: %s\n", $executed_query['sql'] ); + printf( "Params: %s\n", json_encode( $executed_query['params'] ) ); +} diff --git a/tests/tools/run-lexer-benchmark.php b/tests/tools/run-lexer-benchmark.php index 5fd0e2c..a86d0f0 100644 --- a/tests/tools/run-lexer-benchmark.php +++ b/tests/tools/run-lexer-benchmark.php @@ -12,6 +12,7 @@ function ( $severity, $message, $file, $line ) { } ); +require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-token.php'; require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-token.php'; require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-lexer.php'; diff --git a/tests/tools/run-parser-benchmark.php b/tests/tools/run-parser-benchmark.php index 6d16233..5d5f5e2 100644 --- a/tests/tools/run-parser-benchmark.php +++ b/tests/tools/run-parser-benchmark.php @@ -13,11 +13,12 @@ function ( $severity, $message, $file, $line ) { } ); -require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-token.php'; -require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-lexer.php'; require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-grammar.php'; require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-node.php'; +require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-token.php'; require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser.php'; +require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-token.php'; +require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-lexer.php'; require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-parser.php'; function getStats( $total, $failures, $exceptions ) { diff --git a/wip/SQLiteDriver.php b/wip/SQLiteDriver.php deleted file mode 100644 index b5cad10..0000000 --- a/wip/SQLiteDriver.php +++ /dev/null @@ -1,533 +0,0 @@ -', - '!=', - '<', - '<=', - '>', - '>=', - ';', - ); - - private static $functions = array( - 'ABS' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'AVG' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'COUNT' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'MAX' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'MIN' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'ROUND' => array( - 'argCount' => 2, - 'optionalArgs' => 1, - ), - 'SUM' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'LENGTH' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'UPPER' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'LOWER' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'COALESCE' => array( - 'argCount' => 2, - 'optionalArgs' => PHP_INT_MAX, - ), - 'SUBSTR' => array( - 'argCount' => 3, - 'optionalArgs' => 1, - ), - 'REPLACE' => array( - 'argCount' => 3, - 'optionalArgs' => 0, - ), - 'TRIM' => array( - 'argCount' => 3, - 'optionalArgs' => 2, - ), - 'DATE' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'TIME' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'DATETIME' => array( - 'argCount' => 2, - 'optionalArgs' => 1, - ), - 'JULIANDAY' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'STRFTIME' => array( - 'argCount' => 2, - 'optionalArgs' => 0, - ), - 'RANDOM' => array( - 'argCount' => 0, - 'optionalArgs' => 0, - ), - 'RANDOMBLOB' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'NULLIF' => array( - 'argCount' => 2, - 'optionalArgs' => 0, - ), - 'IFNULL' => array( - 'argCount' => 2, - 'optionalArgs' => 0, - ), - 'INSTR' => array( - 'argCount' => 2, - 'optionalArgs' => 0, - ), - 'HEX' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'QUOTE' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'LIKE' => array( - 'argCount' => 2, - 'optionalArgs' => 1, - ), - 'GLOB' => array( - 'argCount' => 2, - 'optionalArgs' => 0, - ), - 'CHAR' => array( - 'argCount' => 1, - 'optionalArgs' => PHP_INT_MAX, - ), - 'UNICODE' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'TOTAL' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'ZEROBLOB' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'PRINTF' => array( - 'argCount' => 2, - 'optionalArgs' => PHP_INT_MAX, - ), - 'LTRIM' => array( - 'argCount' => 2, - 'optionalArgs' => 1, - ), - 'RTRIM' => array( - 'argCount' => 2, - 'optionalArgs' => 1, - ), - 'BLOB' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'GROUP_CONCAT' => array( - 'argCount' => 1, - 'optionalArgs' => 1, - ), - 'JSON' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'JSON_ARRAY' => array( - 'argCount' => 1, - 'optionalArgs' => PHP_INT_MAX, - ), - 'JSON_OBJECT' => array( - 'argCount' => 1, - 'optionalArgs' => PHP_INT_MAX, - ), - 'JSON_QUOTE' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'JSON_VALID' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'JSON_ARRAY_LENGTH' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'JSON_EXTRACT' => array( - 'argCount' => 2, - 'optionalArgs' => PHP_INT_MAX, - ), - 'JSON_INSERT' => array( - 'argCount' => 2, - 'optionalArgs' => PHP_INT_MAX, - ), - 'JSON_REPLACE' => array( - 'argCount' => 2, - 'optionalArgs' => PHP_INT_MAX, - ), - 'JSON_SET' => array( - 'argCount' => 2, - 'optionalArgs' => PHP_INT_MAX, - ), - 'JSON_PATCH' => array( - 'argCount' => 2, - 'optionalArgs' => 0, - ), - 'JSON_REMOVE' => array( - 'argCount' => 1, - 'optionalArgs' => PHP_INT_MAX, - ), - 'JSON_TYPE' => array( - 'argCount' => 1, - 'optionalArgs' => PHP_INT_MAX, - ), - 'JSON_DEPTH' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'JSON_KEYS' => array( - 'argCount' => 1, - 'optionalArgs' => 1, - ), - 'JSON_GROUP_ARRAY' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - 'JSON_GROUP_OBJECT' => array( - 'argCount' => 1, - 'optionalArgs' => 0, - ), - ); - - public static function register_function( string $name, int $arg_count, int $optional_args = 0 ): void { - self::$functions[ strtoupper( $name ) ] = array( - 'argCount' => $arg_count, - 'optionalArgs' => $optional_args, - ); - } - - public static function raw( string $value ): SQLiteToken { - return self::create( SQLiteToken::TYPE_RAW, $value ); - } - - public static function identifier( string $value ): SQLiteToken { - return self::create( SQLiteToken::TYPE_IDENTIFIER, $value ); - } - - public static function value( $value ): SQLiteToken { - return self::create( SQLiteToken::TYPE_VALUE, self::escape_value( $value ) ); - } - - public static function double_quoted_value( $value ): SQLiteToken { - $value = substr( $value, 1, -1 ); - $value = str_replace( '\"', '"', $value ); - $value = str_replace( '""', '"', $value ); - return self::create( SQLiteToken::TYPE_VALUE, self::escape_value( $value ) ); - } - - public static function operator( string $value ): SQLiteToken { - $upper_value = strtoupper( $value ); - if ( ! in_array( $upper_value, self::$valid_operators, true ) ) { - throw new InvalidArgumentException( "Invalid SQLite operator or keyword: $value" ); - } - return self::create( SQLiteToken::TYPE_OPERATOR, $upper_value ); - } - - public static function create_function( string $name, array $expressions ): Expression { - $upper_name = strtoupper( $name ); - if ( ! isset( self::$functions[ $upper_name ] ) ) { - throw new InvalidArgumentException( "Unknown SQLite function: $name" ); - } - - $function_spec = self::$functions[ $upper_name ]; - $min_args = $function_spec['argCount'] - $function_spec['optionalArgs']; - $max_args = $function_spec['argCount']; - - if ( count( $expressions ) < $min_args || count( $expressions ) > $max_args ) { - throw new InvalidArgumentException( - "Function $name expects between $min_args and $max_args arguments, " . - count( $expressions ) . ' given.' - ); - } - - $tokens = array(); - $tokens[] = self::raw( $upper_name ); - $tokens[] = self::raw( '(' ); - - foreach ( $expressions as $index => $expression ) { - if ( $index > 0 ) { - $tokens[] = self::raw( ',' ); - } - if ( ! $expression instanceof Expression ) { - throw new InvalidArgumentException( 'All arguments must be instances of Expression' ); - } - $tokens = array_merge( $tokens, $expression->elements ); - } - - $tokens[] = self::raw( ')' ); - - return new SQLiteExpression( $tokens ); - } - - private static function create( string $type, string $value ): SQLiteToken { - if ( ! in_array( $type, self::$valid_types, true ) ) { - throw new InvalidArgumentException( "Invalid token type: $type" ); - } - return new SQLiteToken( $type, $value ); - } - - private static function escape_value( $value ): string { - if ( is_string( $value ) ) { - // Ensure the string is valid UTF-8, replace invalid characters with an empty string - $value = mb_convert_encoding( $value, 'UTF-8', 'UTF-8' ); - - // Escape single quotes by doubling them - $value = str_replace( "'", "''", $value ); - - // Escape backslashes by doubling them - $value = str_replace( '\\', '\\\\', $value ); - - // Remove null characters - $value = str_replace( "\0", '', $value ); - - // Return the escaped string enclosed in single quotes - return "'" . $value . "'"; - } elseif ( is_int( $value ) || is_float( $value ) ) { - return (string) $value; - } elseif ( is_bool( $value ) ) { - return $value ? '1' : '0'; - } elseif ( is_null( $value ) ) { - return 'NULL'; - } else { - throw new InvalidArgumentException( 'Unsupported value type: ' . gettype( $value ) ); - } - } -} - - -class SQLiteToken { - - const TYPE_RAW = 'TYPE_RAW'; - const TYPE_IDENTIFIER = 'TYPE_IDENTIFIER'; - const TYPE_VALUE = 'TYPE_VALUE'; - const TYPE_OPERATOR = 'TYPE_OPERATOR'; - - public $type; - public $value; - - public function __construct( string $type, $value ) { - $this->type = $type; - $this->value = $value; - } -} - -class SQLiteQueryBuilder { - private Expression $expression; - - public static function stringify( Expression $expression ) { - return ( new SQLiteQueryBuilder( $expression ) )->build_query(); - } - - public function __construct( Expression $expression ) { - $this->expression = $expression; - } - - public function build_query(): string { - $query_parts = array(); - foreach ( $this->expression->get_tokens() as $element ) { - if ( $element instanceof SQLiteToken ) { - $query_parts[] = $this->process_token( $element ); - } elseif ( $element instanceof Expression ) { - $query_parts[] = '(' . ( new self( $element ) )->build_query() . ')'; - } - } - return implode( ' ', $query_parts ); - } - - private function process_token( SQLiteToken $token ): string { - switch ( $token->type ) { - case SQLiteToken::TYPE_RAW: - case SQLiteToken::TYPE_OPERATOR: - return $token->value; - case SQLiteToken::TYPE_IDENTIFIER: - return '"' . str_replace( '"', '""', $token->value ) . '"'; - case SQLiteToken::TYPE_VALUE: - return $token->value; - default: - throw new InvalidArgumentException( 'Unknown token type: ' . $token->type ); - } - } -} - -class Expression { - - public $elements; - - public function __construct( array $elements = array() ) { - $new_elements = array(); - $elements = array_filter( $elements, fn( $x ) => $x ); - foreach ( $elements as $element ) { - if ( is_object( $element ) && $element instanceof Expression ) { - $new_elements = array_merge( $new_elements, $element->elements ); - } else { - $new_elements[] = $element; - } - } - $this->elements = $new_elements; - } - - public function get_tokens() { - return $this->elements; - } - - public function add_token( SQLiteToken $token ) { - $this->elements[] = $token; - } - - public function add_tokens( array $tokens ) { - foreach ( $tokens as $token ) { - $this->add_token( $token ); - } - } - - public function add_expression( $expression ) { - $this->add_token( $expression ); - } -} - -class SQLiteExpression extends Expression {} - -class MySQLToSQLiteDriver { - - private $pdo; - - public function __construct( $dsn, $username = null, $password = null, $options = array() ) { - /* phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO */ - $this->pdo = new PDO( $dsn, $username, $password, $options ); - } - - public function query( array $mysql_ast ) { - $transformer = new SQLTransformer( $mysql_ast, 'sqlite' ); - $expression = $transformer->transform(); - if ( null !== $expression ) { - $query_string = (string) $expression; - return $this->pdo->query( $query_string ); - } else { - throw new Exception( 'Failed to transform query.' ); - } - } -} - -// Example usage: - -// Sample parsed MySQL AST (Abstract Syntax Tree) -// $ast = [ -// 'type' => 'select', -// 'columns' => [ -// ['name' => '*', 'type' => 'ALL'], -// ['name' => 'created_at', 'type' => 'DATETIME'] -// ], -// 'from' => 'users', -// 'keywords' => ['SELECT', 'FROM'], -// 'options' => ['DISTINCT'] -// ]; - -// try { -// $driver = new MySQLToSQLiteDriver('sqlite::memory:'); -// $result = $driver->query($ast); -// foreach ($result as $row) { -// print_r($row); -// } -// } catch (Exception $e) { -// echo $e->getMessage(); -// } diff --git a/wip/run-mysql-driver.php b/wip/run-mysql-driver.php deleted file mode 100644 index 7f257d7..0000000 --- a/wip/run-mysql-driver.php +++ /dev/null @@ -1,656 +0,0 @@ -parse(); -// print_r($parse_tree); -// die(); -// echo 'a'; - -$query = <<run_query( $query ); -die(); -// $transformer = new SQLTransformer($parse_tree, 'sqlite'); -// $expression = $transformer->transform(); -// print_r($expression); - -class MySQLonSQLiteDriver { - private $grammar = false; - private $has_sql_calc_found_rows = false; - private $has_found_rows_call = false; - private $last_calc_rows_result = null; - - public function __construct( $grammar ) { - $this->grammar = $grammar; - } - - public function run_query( $query ) { - $this->has_sql_calc_found_rows = false; - $this->has_found_rows_call = false; - $this->last_calc_rows_result = null; - - $parser = new WP_MySQL_Parser( $this->grammar, tokenize_query( $query ) ); - $parse_tree = $parser->parse(); - $expr = $this->translate_query( $parse_tree ); - $expr = $this->rewrite_sql_calc_found_rows( $expr ); - - $sqlite_query = SQLiteQueryBuilder::stringify( $expr ) . ''; - - // Returning the expery just for now for testing. In the end, we'll - // run it and return the SQLite interaction result. - return $sqlite_query; - } - - private function rewrite_sql_calc_found_rows( SQLiteExpression $expr ) { - if ( $this->has_found_rows_call && ! $this->has_sql_calc_found_rows && null === $this->last_calc_rows_result ) { - throw new Exception( 'FOUND_ROWS() called without SQL_CALC_FOUND_ROWS' ); - } - - if ( $this->has_sql_calc_found_rows ) { - $expr_to_run = $expr; - if ( $this->has_found_rows_call ) { - $expr_without_found_rows = new SQLiteExpression( array() ); - foreach ( $expr->elements as $k => $element ) { - if ( SQLiteToken::TYPE_IDENTIFIER === $element->type && 'FOUND_ROWS' === $element->value ) { - $expr_without_found_rows->add_token( - SQLiteTokenFactory::value( 0 ) - ); - } else { - $expr_without_found_rows->add_token( $element ); - } - } - $expr_to_run = $expr_without_found_rows; - } - - // ...remove the LIMIT clause... - $query = 'SELECT COUNT(*) as cnt FROM (' . SQLiteQueryBuilder::stringify( $expr_to_run ) . ');'; - - // ...run $query... - // $result = ... - - $this->last_calc_rows_result = $result['cnt']; - } - - if ( ! $this->has_found_rows_call ) { - return $expr; - } - - $expr_with_found_rows_result = new SQLiteExpression( array() ); - foreach ( $expr->elements as $k => $element ) { - if ( SQLiteToken::TYPE_IDENTIFIER === $element->type && 'FOUND_ROWS' === $element->value ) { - $expr_with_found_rows_result->add_token( - SQLiteTokenFactory::value( $this->last_calc_rows_result ) - ); - } else { - $expr_with_found_rows_result->add_token( $element ); - } - } - return $expr_with_found_rows_result; - } - - private function translate_query( $parse_tree ) { - if ( null === $parse_tree ) { - return null; - } - - if ( $parse_tree instanceof WP_MySQL_Token ) { - $token = $parse_tree; - switch ( $token->type ) { - case WP_MySQL_Lexer::EOF: - return new SQLiteExpression( array() ); - - case WP_MySQL_Lexer::IDENTIFIER: - return new SQLiteExpression( - array( - SQLiteTokenFactory::identifier( - trim( $token->text, '`"' ) - ), - ) - ); - - default: - return new SQLiteExpression( - array( - SQLiteTokenFactory::raw( $token->text ), - ) - ); - } - } - - if ( ! ( $parse_tree instanceof WP_Parser_Node ) ) { - throw new Exception( 'translateQuery only accepts MySQLToken and ParseTree instances' ); - } - - $rule_name = $parse_tree->rule_name; - - switch ( $rule_name ) { - case 'indexHintList': - // SQLite doesn't support index hints. Let's - // skip them. - return null; - - case 'querySpecOption': - $token = $parse_tree->get_token(); - switch ( $token->type ) { - case WP_MySQL_Lexer::ALL_SYMBOL: - case WP_MySQL_Lexer::DISTINCT_SYMBOL: - return new SQLiteExpression( - array( - SQLiteTokenFactory::raw( $token->text ), - ) - ); - case WP_MySQL_Lexer::SQL_CALC_FOUND_ROWS_SYMBOL: - $this->has_sql_calc_found_rows = true; - // Fall through to default. - default: - // we'll need to run the current SQL query without any - // LIMIT clause, and then substitute the FOUND_ROWS() - // function with a literal number of rows found. - return new SQLiteExpression( array() ); - } - // Otherwise, fall through. - - case 'fromClause': - // Skip `FROM DUAL`. We only care about a singular - // FROM DUAL statement, as FROM mytable, DUAL is a syntax - // error. - if ( - $parse_tree->has_token( WP_MySQL_Lexer::DUAL_SYMBOL ) && - ! $parse_tree->has_child( 'tableReferenceList' ) - ) { - return null; - } - // Otherwise, fall through. - - case 'selectOption': - case 'interval': - case 'intervalTimeStamp': - case 'bitExpr': - case 'boolPri': - case 'lockStrengh': - case 'orderList': - case 'simpleExpr': - case 'columnRef': - case 'exprIs': - case 'exprAnd': - case 'primaryExprCompare': - case 'fieldIdentifier': - case 'dotIdentifier': - case 'identifier': - case 'literal': - case 'joinedTable': - case 'nullLiteral': - case 'boolLiteral': - case 'numLiteral': - case 'textLiteral': - case 'predicate': - case 'predicateExprBetween': - case 'primaryExprPredicate': - case 'pureIdentifier': - case 'unambiguousIdentifier': - case 'qualifiedIdentifier': - case 'query': - case 'queryExpression': - case 'queryExpressionBody': - case 'queryExpressionParens': - case 'queryPrimary': - case 'querySpecification': - case 'selectAlias': - case 'selectItem': - case 'selectItemList': - case 'selectStatement': - case 'simpleExprColumnRef': - case 'simpleExprFunction': - case 'outerJoinType': - case 'simpleExprSubQuery': - case 'simpleExprLiteral': - case 'compOp': - case 'simpleExprList': - case 'simpleStatement': - case 'subquery': - case 'exprList': - case 'expr': - case 'tableReferenceList': - case 'tableReference': - case 'tableRef': - case 'tableAlias': - case 'tableFactor': - case 'singleTable': - case 'udfExprList': - case 'udfExpr': - case 'withClause': - case 'whereClause': - case 'commonTableExpression': - case 'derivedTable': - case 'columnRefOrLiteral': - case 'orderClause': - case 'groupByClause': - case 'lockingClauseList': - case 'lockingClause': - case 'havingClause': - case 'direction': - case 'orderExpression': - $child_expressions = array(); - foreach ( $parse_tree->children as $child ) { - $child_expressions[] = $this->translate_query( $child ); - } - return new SQLiteExpression( $child_expressions ); - - case 'textStringLiteral': - return new SQLiteExpression( - array( - $parse_tree->has_token( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT ) ? - SQLiteTokenFactory::double_quoted_value( $parse_tree->get_token( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT )->text ) : false, - $parse_tree->has_token( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT ) ? - SQLiteTokenFactory::raw( $parse_tree->get_token( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT )->text ) : false, - ) - ); - - case 'functionCall': - return $this->translate_function_call( $parse_tree ); - - case 'runtimeFunctionCall': - return $this->translate_runtime_function_call( $parse_tree ); - - default: - // var_dump(count($ast->children)); - // foreach($ast->children as $child) { - // var_dump(get_class($child)); - // echo $child->getText(); - // echo "\n\n"; - // } - return new SQLiteExpression( - array( - SQLiteTokenFactory::raw( - $rule_name - ), - ) - ); - } - } - - private function translate_runtime_function_call( $parse_tree ): SQLiteExpression { - $name_token = $parse_tree->children[0]; - - switch ( strtoupper( $name_token->text ) ) { - case 'ADDDATE': - case 'DATE_ADD': - $args = $parse_tree->get_children( 'expr' ); - $interval = $parse_tree->get_child( 'interval' ); - $timespan = $interval->get_child( 'intervalTimeStamp' )->get_token()->text; - return SQLiteTokenFactory::create_function( - 'DATETIME', - array( - $this->translate_query( $args[0] ), - new SQLiteExpression( - array( - SQLiteTokenFactory::value( '+' ), - SQLiteTokenFactory::raw( '||' ), - $this->translate_query( $args[1] ), - SQLiteTokenFactory::raw( '||' ), - SQLiteTokenFactory::value( $timespan ), - ) - ), - ) - ); - - case 'DATE_SUB': - // return new Expression([ - // SQLiteTokenFactory::raw("DATETIME("), - // $args[0], - // SQLiteTokenFactory::raw(", '-'"), - // $args[1], - // SQLiteTokenFactory::raw(" days')") - // ]); - - case 'VALUES': - $column = $parse_tree->get_child()->get_descendant( 'pureIdentifier' ); - if ( ! $column ) { - throw new Exception( 'VALUES() calls without explicit column names are unsupported' ); - } - - $colname = $column->get_token()->extractValue(); - return new SQLiteExpression( - array( - SQLiteTokenFactory::raw( 'excluded.' ), - SQLiteTokenFactory::identifier( $colname ), - ) - ); - default: - throw new Exception( 'Unsupported function: ' . $name_token->text ); - } - } - - private function translate_function_call( $function_call_tree ): SQLiteExpression { - $name = $function_call_tree->get_child( 'pureIdentifier' )->get_token()->text; - $args = array(); - foreach ( $function_call_tree->get_child( 'udfExprList' )->get_children() as $node ) { - $args[] = $this->translate_query( $node ); - } - switch ( strtoupper( $name ) ) { - case 'ABS': - case 'ACOS': - case 'ASIN': - case 'ATAN': - case 'ATAN2': - case 'COS': - case 'DEGREES': - case 'TRIM': - case 'EXP': - case 'MAX': - case 'MIN': - case 'FLOOR': - case 'RADIANS': - case 'ROUND': - case 'SIN': - case 'SQRT': - case 'TAN': - case 'TRUNCATE': - case 'RANDOM': - case 'PI': - case 'LTRIM': - case 'RTRIM': - return SQLiteTokenFactory::create_function( $name, $args ); - - case 'CEIL': - case 'CEILING': - return SQLiteTokenFactory::create_function( 'CEIL', $args ); - - case 'COT': - return new Expression( - array( - SQLiteTokenFactory::raw( '1 / ' ), - SQLiteTokenFactory::create_function( 'TAN', $args ), - ) - ); - - case 'LN': - case 'LOG': - case 'LOG2': - return SQLiteTokenFactory::create_function( 'LOG', $args ); - - case 'LOG10': - return SQLiteTokenFactory::create_function( 'LOG10', $args ); - - // case 'MOD': - // return $this->transformBinaryOperation([ - // 'operator' => '%', - // 'left' => $args[0], - // 'right' => $args[1] - // ]); - - case 'POW': - case 'POWER': - return SQLiteTokenFactory::create_function( 'POW', $args ); - - // String functions - case 'ASCII': - return SQLiteTokenFactory::create_function( 'UNICODE', $args ); - case 'CHAR_LENGTH': - case 'LENGTH': - return SQLiteTokenFactory::create_function( 'LENGTH', $args ); - case 'CONCAT': - $concated = array( SQLiteTokenFactory::raw( '(' ) ); - foreach ( $args as $k => $arg ) { - $concated[] = $arg; - if ( $k < count( $args ) - 1 ) { - $concated[] = SQLiteTokenFactory::raw( '||' ); - } - } - $concated[] = SQLiteTokenFactory::raw( ')' ); - return new SQLiteExpression( $concated ); - // case 'CONCAT_WS': - // return new Expression([ - // SQLiteTokenFactory::raw("REPLACE("), - // implode(" || ", array_slice($args, 1)), - // SQLiteTokenFactory::raw(", '', "), - // $args[0], - // SQLiteTokenFactory::raw(")") - // ]); - case 'INSTR': - return SQLiteTokenFactory::create_function( 'INSTR', $args ); - case 'LCASE': - case 'LOWER': - return SQLiteTokenFactory::create_function( 'LOWER', $args ); - case 'LEFT': - return SQLiteTokenFactory::create_function( - 'SUBSTR', - array( - $args[0], - '1', - $args[1], - ) - ); - case 'LOCATE': - return SQLiteTokenFactory::create_function( - 'INSTR', - array( - $args[1], - $args[0], - ) - ); - case 'REPEAT': - return new Expression( - array( - SQLiteTokenFactory::raw( "REPLACE(CHAR(32), ' ', " ), - $args[0], - SQLiteTokenFactory::raw( ')' ), - ) - ); - - case 'REPLACE': - return new Expression( - array( - SQLiteTokenFactory::raw( 'REPLACE(' ), - implode( ', ', $args ), - SQLiteTokenFactory::raw( ')' ), - ) - ); - case 'RIGHT': - return new Expression( - array( - SQLiteTokenFactory::raw( 'SUBSTR(' ), - $args[0], - SQLiteTokenFactory::raw( ', -(' ), - $args[1], - SQLiteTokenFactory::raw( '))' ), - ) - ); - case 'SPACE': - return new Expression( - array( - SQLiteTokenFactory::raw( "REPLACE(CHAR(32), ' ', '')" ), - ) - ); - case 'SUBSTRING': - case 'SUBSTR': - return SQLiteTokenFactory::create_function( 'SUBSTR', $args ); - case 'UCASE': - case 'UPPER': - return SQLiteTokenFactory::create_function( 'UPPER', $args ); - - case 'DATE_FORMAT': - $mysql_date_format_to_sqlite_strftime = array( - '%a' => '%D', - '%b' => '%M', - '%c' => '%n', - '%D' => '%jS', - '%d' => '%d', - '%e' => '%j', - '%H' => '%H', - '%h' => '%h', - '%I' => '%h', - '%i' => '%M', - '%j' => '%z', - '%k' => '%G', - '%l' => '%g', - '%M' => '%F', - '%m' => '%m', - '%p' => '%A', - '%r' => '%h:%i:%s %A', - '%S' => '%s', - '%s' => '%s', - '%T' => '%H:%i:%s', - '%U' => '%W', - '%u' => '%W', - '%V' => '%W', - '%v' => '%W', - '%W' => '%l', - '%w' => '%w', - '%X' => '%Y', - '%x' => '%o', - '%Y' => '%Y', - '%y' => '%y', - ); - // @TODO: Implement as user defined function to avoid - // rewriting something that may be an expression as a string - $format = $args[1]->elements[0]->value; - $new_format = strtr( $format, $mysql_date_format_to_sqlite_strftime ); - - return SQLiteTokenFactory::create_function( - 'STRFTIME', - array( - new Expression( array( SQLiteTokenFactory::raw( $new_format ) ) ), - new Expression( array( $args[0] ) ), - ) - ); - case 'DATEDIFF': - return new Expression( - array( - SQLiteTokenFactory::create_function( 'JULIANDAY', array( $args[0] ) ), - SQLiteTokenFactory::raw( ' - ' ), - SQLiteTokenFactory::create_function( 'JULIANDAY', array( $args[1] ) ), - ) - ); - case 'DAYNAME': - return SQLiteTokenFactory::create_function( - 'STRFTIME', - array( '%w', ...$args ) - ); - case 'DAY': - case 'DAYOFMONTH': - return new Expression( - array( - SQLiteTokenFactory::raw( "CAST('" ), - SQLiteTokenFactory::create_function( 'STRFTIME', array( '%d', ...$args ) ), - SQLiteTokenFactory::raw( ") AS INTEGER'" ), - ) - ); - case 'DAYOFWEEK': - return new Expression( - array( - SQLiteTokenFactory::raw( "CAST('" ), - SQLiteTokenFactory::create_function( 'STRFTIME', array( '%w', ...$args ) ), - SQLiteTokenFactory::raw( ") + 1 AS INTEGER'" ), - ) - ); - case 'DAYOFYEAR': - return new Expression( - array( - SQLiteTokenFactory::raw( "CAST('" ), - SQLiteTokenFactory::create_function( 'STRFTIME', array( '%j', ...$args ) ), - SQLiteTokenFactory::raw( ") AS INTEGER'" ), - ) - ); - case 'HOUR': - return new Expression( - array( - SQLiteTokenFactory::raw( "CAST('" ), - SQLiteTokenFactory::create_function( 'STRFTIME', array( '%H', ...$args ) ), - SQLiteTokenFactory::raw( ") AS INTEGER'" ), - ) - ); - case 'MINUTE': - return new Expression( - array( - SQLiteTokenFactory::raw( "CAST('" ), - SQLiteTokenFactory::create_function( 'STRFTIME', array( '%M', ...$args ) ), - SQLiteTokenFactory::raw( ") AS INTEGER'" ), - ) - ); - case 'MONTH': - return new Expression( - array( - SQLiteTokenFactory::raw( "CAST('" ), - SQLiteTokenFactory::create_function( 'STRFTIME', array( '%m', ...$args ) ), - SQLiteTokenFactory::raw( ") AS INTEGER'" ), - ) - ); - case 'MONTHNAME': - return SQLiteTokenFactory::create_function( 'STRFTIME', array( '%m', ...$args ) ); - case 'NOW': - return new Expression( - array( - SQLiteTokenFactory::raw( 'CURRENT_TIMESTAMP()' ), - ) - ); - case 'SECOND': - return new Expression( - array( - SQLiteTokenFactory::raw( "CAST('" ), - SQLiteTokenFactory::create_function( 'STRFTIME', array( '%S', ...$args ) ), - SQLiteTokenFactory::raw( ") AS INTEGER'" ), - ) - ); - case 'TIMESTAMP': - return new Expression( - array( - SQLiteTokenFactory::raw( 'DATETIME(' ), - ...$args, - SQLiteTokenFactory::raw( ')' ), - ) - ); - case 'YEAR': - return new Expression( - array( - SQLiteTokenFactory::raw( "CAST('" ), - SQLiteTokenFactory::create_function( 'STRFTIME', array( '%Y', ...$args ) ), - SQLiteTokenFactory::raw( ") AS INTEGER'" ), - ) - ); - case 'FOUND_ROWS': - $this->has_found_rows_call = true; - return new Expression( - array( - // Post-processed in handleSqlCalcFoundRows() - SQLiteTokenFactory::raw( 'FOUND_ROWS' ), - ) - ); - default: - throw new Exception( 'Unsupported function: ' . $name ); - } - } -} diff --git a/wp-includes/mysql/class-wp-mysql-lexer.php b/wp-includes/mysql/class-wp-mysql-lexer.php index bcaca57..f2174ea 100644 --- a/wp-includes/mysql/class-wp-mysql-lexer.php +++ b/wp-includes/mysql/class-wp-mysql-lexer.php @@ -929,6 +929,7 @@ class WP_MySQL_Lexer { const SECONDARY_ENGINE_ATTRIBUTE_SYMBOL = 849; const JSON_VALUE_SYMBOL = 850; const RETURNING_SYMBOL = 851; + const GEOMCOLLECTION_SYMBOL = 852; // Comments const COMMENT = 900; @@ -1155,6 +1156,7 @@ class WP_MySQL_Lexer { '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, @@ -1810,6 +1812,7 @@ class WP_MySQL_Lexer { 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, @@ -1936,6 +1939,7 @@ class WP_MySQL_Lexer { 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, diff --git a/wp-includes/mysql/class-wp-mysql-token.php b/wp-includes/mysql/class-wp-mysql-token.php index 5251264..c812288 100644 --- a/wp-includes/mysql/class-wp-mysql-token.php +++ b/wp-includes/mysql/class-wp-mysql-token.php @@ -1,35 +1,39 @@ type = $type; - $this->text = $text; - } - - public function get_type() { - return $this->type; - } - - public function get_text() { - return $this->text; + public function get_name(): string { + $name = WP_MySQL_Lexer::get_token_name( $this->id ); + if ( null === $name ) { + $name = 'UNKNOWN'; + } + return $name; } - public function get_name() { - return WP_MySQL_Lexer::get_token_name( $this->type ); - } - - public function __toString() { - return $this->text . '<' . $this->type . ',' . $this->get_name() . '>'; + /** + * 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->value . '<' . $this->id . ',' . $this->get_name() . '>'; } } diff --git a/wp-includes/mysql/mysql-grammar.php b/wp-includes/mysql/mysql-grammar.php index 8c0bc7a..3aec6f4 100644 --- a/wp-includes/mysql/mysql-grammar.php +++ b/wp-includes/mysql/mysql-grammar.php @@ -1,4 +1,4 @@ 2000,'rules_names'=>['query','%f1','%f2','%f3','simpleStatement','%f4','%f5','%f6','%f7','alterStatement','%f8','%f9','%f10','%f11','alterInstance','%f12','%f13','%f14','%f15','%f16','%f17','%f18','%f19','%f20','%f21','%f22','%f23','%f24','%f25','%f26','%f27','alterDatabase','%f28','%f29','%f30','%f31','%f32','%f33','%f34','%f35','%f36','%f37','alterEvent','%f38','%f39','%f40','%f41','%f42','%f43','%f44','%f45','%f46','%f47','%f48','%f49','%f50','%f51','%f52','%f53','%f54','alterLogfileGroup','%f55','alterLogfileGroupOptions','%f56','%f57','%f58','alterLogfileGroupOption','alterServer','%f59','%f60','%f61','alterTable','%f62','%f63','%f64','%f65','%f66','alterTableActions','%f67','%f68','%f69','%f70','%f71','alterCommandList','%f72','%f73','%f74','alterCommandsModifierList','%f75','%f76','standaloneAlterCommands','%f77','%f78','%f79','%f80','%f81','%f82','%f83','%f84','%f85','%f86','%f87','%f88','%f89','%f90','alterPartition','%f91','%f92','%f93','%f94','%f95','%f96','alterList','%f97','%f98','%f99','%f100','alterCommandsModifier','%f101','%f102','%f103','%f104','%f105','%f106','%f107','%f108','alterListItem','%f109','%f110','%f111','%f112','%f113','%f114','%f115','%f116','%f117','%f118','%f119','%f120','%f121','%f122','%f123','%f124','%f125','%f126','%f127','%f128','%f129','%f130','%f131','%f132','%f133','%f134','place','restrict','%f135','%f136','alterOrderList','%f137','%f138','%f139','%f140','alterAlgorithmOption','%f141','%f142','alterLockOption','%f143','%f144','%f145','indexLockAndAlgorithm','withValidation','%f146','%f147','removePartitioning','allOrPartitionNameList','alterTablespace','%f148','%f149','%f150','%f151','%f152','%f153','%f154','%f155','%f156','%f157','%f158','%f159','%f160','%f161','%f162','alterUndoTablespace','%f163','%f164','undoTableSpaceOptions','%f165','%f166','%f167','undoTableSpaceOption','%f168','alterTablespaceOptions','%f169','%f170','%f171','%f172','alterTablespaceOption','%f173','%f174','changeTablespaceOption','%f175','%f176','%f177','alterView','%f178','viewTail','%f179','viewSelect','%f180','viewCheckOption','%f181','%f182','createStatement','%f183','%f184','%f185','%f186','%f187','%f188','createDatabase','createDatabaseOption','%f189','%f190','%f191','createTable','%f192','%f193','%f194','%f195','%f196','%f197','%f198','%f199','tableElementList','%f200','%f201','tableElement','%f202','%f203','duplicateAsQueryExpression','%f204','%f205','queryExpressionOrParens','%f206','createRoutine','%f207','%f208','%f209','%f210','createProcedure','%f211','%f212','%f213','%f214','%f215','%f216','%f217','%f218','%f219','%f220','%f221','createFunction','%f222','%f223','%f224','%f225','%f226','%f227','%f228','%f229','%f230','createUdf','%f231','%f232','routineCreateOption','%f233','routineAlterOptions','routineOption','%f234','%f235','%f236','createIndex','%f237','%f238','%f239','%f240','%f241','%f242','%f243','%f244','%f245','%f246','%f247','indexNameAndType','%f248','%f249','%f250','createIndexTarget','%f251','createLogfileGroup','%f252','%f253','logfileGroupOptions','%f254','%f255','%f256','logfileGroupOption','createServer','%f257','serverOptions','%f258','%f259','serverOption','%f260','%f261','createTablespace','%f262','%f263','%f264','createUndoTablespace','tsDataFileName','%f265','%f266','%f267','%f268','tsDataFile','%f269','tablespaceOptions','%f270','%f271','%f272','tablespaceOption','%f273','%f274','%f275','%f276','tsOptionInitialSize','%f277','tsOptionUndoRedoBufferSize','%f278','%f279','tsOptionAutoextendSize','%f280','tsOptionMaxSize','%f281','tsOptionExtentSize','%f282','tsOptionNodegroup','%f283','%f284','tsOptionEngine','%f285','tsOptionEngineAttribute','tsOptionWait','%f286','%f287','tsOptionComment','%f288','tsOptionFileblockSize','%f289','tsOptionEncryption','%f290','%f291','%f292','createView','%f293','viewReplaceOrAlgorithm','viewAlgorithm','%f294','viewSuid','%f295','%f296','%f297','createTrigger','%f298','%f299','%f300','%f301','%f302','triggerFollowsPrecedesClause','%f303','%f304','%f305','%f306','%f307','%f308','%f309','createEvent','%f310','%f311','%f312','%f313','%f314','%f315','%f316','%f317','%f318','%f319','%f320','createRole','%f321','%f322','%f323','createSpatialReference','srsAttribute','dropStatement','%f324','%f325','%f326','%f327','%f328','dropDatabase','%f329','dropEvent','%f330','dropFunction','%f331','dropProcedure','%f332','%f333','dropIndex','%f334','dropLogfileGroup','%f335','%f336','%f337','%f338','%f339','%f340','dropLogfileGroupOption','%f341','dropServer','%f342','%f343','%f344','dropTable','%f345','%f346','%f347','%f348','dropTableSpace','%f349','%f350','%f351','%f352','%f353','%f354','%f355','dropTrigger','%f356','%f357','dropView','%f358','%f359','%f360','dropRole','%f361','dropSpatialReference','%f362','dropUndoTablespace','%f363','renameTableStatement','%f364','%f365','%f366','renamePair','%f367','truncateTableStatement','importStatement','%f368','callStatement','%f369','%f370','%f371','%f372','%f373','deleteStatement','%f374','%f375','%f376','%f377','%f378','%f379','%f380','%f381','%f382','%f383','%f384','%f385','%f386','%f387','%f388','partitionDelete','%f389','deleteStatementOption','doStatement','%f390','%f391','%f392','handlerStatement','%f393','%f394','%f395','%f396','%f397','handlerReadOrScan','%f398','%f399','%f400','%f401','%f402','%f403','%f404','%f405','%f406','insertStatement','%f407','%f408','%f409','%f410','%f411','%f412','%f413','%f414','%f415','insertLockOption','%f416','insertFromConstructor','%f417','%f418','%f419','%f420','fields','%f421','%f422','insertValues','%f423','%f424','insertQueryExpression','%f425','%f426','valueList','%f427','%f428','%f429','%f430','values','%f431','%f432','%f433','%f434','%f435','valuesReference','insertUpdateList','%f436','%f437','%f438','%f439','%f440','%f441','%f442','%f443','loadStatement','%f444','%f445','%f446','%f447','dataOrXml','xmlRowsIdentifiedBy','%f448','%f449','%f450','loadDataFileTail','%f451','%f452','%f453','%f454','%f455','%f456','loadDataFileTargetList','%f457','fieldOrVariableList','%f458','%f459','%f460','%f461','%f462','%f463','%f464','replaceStatement','%f465','%f466','%f467','%f468','selectStatement','%f469','selectStatementWithInto','%f470','%f471','queryExpression','%f472','%f473','%f474','%f475','%f476','%f477','%f478','%f479','%f480','%f481','%f482','%f483','queryExpressionBody','%f484','%f485','%f486','%f487','%f488','%f489','queryTerm','%f490','%f491','%f492','%f493','%f494','%f495','%f496','queryExpressionParens','queryPrimary','%f497','%f498','%f499','%f500','%f501','%f502','%f503','%f504','%f505','querySpecification','%f506','%f507','%f508','subquery','querySpecOption','limitClause','simpleLimitClause','%f509','limitOptions','%f510','%f511','%f512','limitOption','%f513','intoClause','%f514','%f515','%f516','%f517','%f518','%f519','%f520','%f521','%f522','%f523','procedureAnalyseClause','%f524','%f525','%f526','%f527','%f528','havingClause','%f529','windowClause','%f530','%f531','windowDefinition','windowSpec','%f532','%f533','%f534','%f535','%f536','%f537','%f538','%f539','%f540','%f541','windowSpecDetails','%f542','%f543','%f544','%f545','%f546','%f547','%f548','windowFrameClause','windowFrameUnits','windowFrameExtent','windowFrameStart','windowFrameBetween','windowFrameBound','windowFrameExclusion','%f549','%f550','%f551','withClause','%f552','%f553','%f554','commonTableExpression','%f555','groupByClause','olapOption','%f556','orderClause','direction','fromClause','%f557','%f558','tableReferenceList','%f559','%f560','%f561','tableValueConstructor','%f562','%f563','explicitTable','%f564','rowValueExplicit','selectOption','%f565','%f566','%f567','lockingClauseList','%f568','%f569','lockingClause','%f570','%f571','%f572','%f573','%f574','%f575','lockStrengh','%f576','lockedRowAction','%f577','selectItemList','%f578','%f579','%f580','%f581','selectItem','%f582','selectAlias','%f583','whereClause','%f584','tableReference','%f585','%f586','%f587','%f588','escapedTableReference','%f589','joinedTable','%f590','%f591','%f592','%f593','%f594','naturalJoinType','%f595','%f596','innerJoinType','%f597','%f598','%f599','outerJoinType','%f600','tableFactor','%f601','%f602','%f603','%f604','singleTable','singleTableParens','%f605','%f606','%f607','derivedTable','%f608','%f609','%f610','%f611','%f612','%f613','tableReferenceListParens','%f614','%f615','tableFunction','%f616','columnsClause','%f617','%f618','%f619','%f620','%f621','jtColumn','%f622','%f623','%f624','%f625','%f626','onEmptyOrError','onEmpty','onError','jtOnResponse','setOperationOption','%f627','tableAlias','%f628','%f629','%f630','%f631','indexHintList','%f632','%f633','%f634','indexHint','indexHintType','keyOrIndex','%f635','constraintKeyType','indexHintClause','%f636','%f637','indexList','%f638','%f639','indexListElement','%f640','%f641','%f642','%f643','%f644','%f645','updateStatement','%f646','%f647','%f648','transactionOrLockingStatement','%f649','%f650','%f651','%f652','transactionStatement','%f653','%f654','%f655','%f656','%f657','%f658','%f659','beginWork','%f660','transactionCharacteristicList','%f661','%f662','transactionCharacteristic','%f663','%f664','%f665','savepointStatement','%f666','%f667','%f668','%f669','%f670','%f671','%f672','%f673','%f674','%f675','%f676','lockStatement','%f677','%f678','%f679','%f680','%f681','%f682','%f683','lockItem','%f684','%f685','lockOption','xaStatement','%f686','%f687','%f688','%f689','%f690','%f691','%f692','%f693','%f694','%f695','%f696','%f697','%f698','%f699','xaConvert','%f700','%f701','%f702','%f703','%f704','xid','%f705','%f706','%f707','%f708','%f709','%f710','replicationStatement','%f711','%f712','%f713','%f714','%f715','%f716','%f717','%f718','%f719','%f720','%f721','%f722','%f723','%f724','resetOption','%f725','masterResetOptions','%f726','%f727','%f728','%f729','replicationLoad','%f730','%f731','changeMaster','%f732','changeMasterOptions','%f733','%f734','masterOption','privilegeCheckDef','tablePrimaryKeyCheckDef','masterTlsCiphersuitesDef','masterFileDef','%f735','serverIdList','%f736','%f737','%f738','%f739','%f740','%f741','%f742','%f743','changeReplication','%f744','%f745','%f746','%f747','%f748','%f749','changeReplicationSourceOptions','%f750','%f751','replicationSourceOption','%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','%f784','%f785','filterDefinition','%f786','filterDbList','%f787','%f788','%f789','filterTableList','%f790','%f791','%f792','filterStringList','%f793','%f794','filterWildDbTableString','%f795','filterDbPairList','%f796','%f797','%f798','%f799','%f800','%f801','%f802','slave','%f803','%f804','%f805','slaveUntilOptions','%f806','%f807','%f808','%f809','%f810','%f811','slaveConnectionOptions','%f812','%f813','%f814','%f815','%f816','%f817','%f818','%f819','%f820','%f821','%f822','%f823','%f824','%f825','slaveThreadOptions','%f826','%f827','slaveThreadOption','groupReplication','%f828','preparedStatement','%f829','%f830','%f831','executeStatement','%f832','%f833','%f834','executeVarList','%f835','%f836','cloneStatement','%f837','%f838','%f839','%f840','%f841','%f842','%f843','%f844','%f845','dataDirSSL','%f846','ssl','accountManagementStatement','%f847','%f848','%f849','alterUser','%f850','%f851','%f852','alterUserTail','%f853','%f854','%f855','%f856','%f857','%f858','userFunction','createUser','%f859','%f860','createUserTail','%f861','%f862','%f863','%f864','%f865','%f866','%f867','%f868','%f869','defaultRoleClause','%f870','%f871','%f872','%f873','requireClause','%f874','%f875','%f876','connectOptions','%f877','%f878','accountLockPasswordExpireOptions','%f879','%f880','%f881','%f882','%f883','%f884','%f885','%f886','%f887','%f888','%f889','%f890','%f891','dropUser','%f892','%f893','%f894','grant','%f895','%f896','%f897','%f898','%f899','%f900','%f901','%f902','%f903','%f904','%f905','%f906','%f907','%f908','grantTargetList','%f909','%f910','grantOptions','%f911','%f912','%f913','exceptRoleList','withRoles','%f914','%f915','%f916','grantAs','versionedRequireClause','%f917','%f918','renameUser','%f919','%f920','%f921','%f922','revoke','%f923','%f924','%f925','%f926','%f927','%f928','%f929','%f930','%f931','%f932','%f933','%f934','%f935','%f936','%f937','%f938','onTypeTo','%f939','%f940','%f941','%f942','%f943','%f944','%f945','aclType','%f946','roleOrPrivilegesList','%f947','%f948','%f949','%f950','%f951','roleOrPrivilege','%f952','%f953','%f954','%f955','%f956','%f957','%f958','%f959','%f960','%f961','%f962','%f963','%f964','grantIdentifier','%f965','%f966','%f967','%f968','%f969','requireList','%f970','%f971','%f972','requireListElement','grantOption','%f973','setRole','%f974','%f975','%f976','%f977','%f978','roleList','%f979','%f980','%f981','role','%f982','%f983','%f984','%f985','%f986','%f987','%f988','%f989','%f990','tableAdministrationStatement','%f991','%f992','%f993','%f994','%f995','%f996','%f997','%f998','%f999','%f1000','%f1001','%f1002','histogram','%f1003','%f1004','%f1005','%f1006','checkOption','%f1007','repairType','%f1008','%f1009','installUninstallStatment','%f1010','%f1011','%f1012','%f1013','installOptionType','%f1014','installSetValue','%f1015','%f1016','installSetValueList','%f1017','%f1018','setStatement','%f1019','startOptionValueList','%f1020','%f1021','%f1022','%f1023','%f1024','%f1025','%f1026','%f1027','%f1028','%f1029','%f1030','%f1031','%f1032','%f1033','%f1034','%f1035','%f1036','transactionCharacteristics','%f1037','%f1038','%f1039','%f1040','transactionAccessMode','%f1041','isolationLevel','%f1042','%f1043','%f1044','optionValueListContinued','%f1045','%f1046','optionValueNoOptionType','%f1047','%f1048','%f1049','optionValue','%f1050','setSystemVariable','startOptionValueListFollowingOptionType','optionValueFollowingOptionType','setExprOrDefault','%f1051','%f1052','%f1053','showStatement','%f1054','%f1055','%f1056','%f1057','%f1058','%f1059','%f1060','%f1061','%f1062','%f1063','%f1064','%f1065','%f1066','%f1067','%f1068','%f1069','%f1070','%f1071','%f1072','%f1073','%f1074','%f1075','%f1076','%f1077','%f1078','%f1079','%f1080','%f1081','%f1082','%f1083','%f1084','%f1085','%f1086','%f1087','%f1088','%f1089','%f1090','%f1091','%f1092','%f1093','%f1094','%f1095','%f1096','%f1097','%f1098','%f1099','%f1100','%f1101','%f1102','%f1103','%f1104','%f1105','%f1106','%f1107','%f1108','%f1109','%f1110','%f1111','%f1112','%f1113','%f1114','%f1115','%f1116','%f1117','%f1118','%f1119','%f1120','%f1121','%f1122','showCommandType','%f1123','%f1124','nonBlocking','%f1125','%f1126','fromOrIn','inDb','profileType','%f1127','%f1128','%f1129','otherAdministrativeStatement','%f1130','%f1131','%f1132','%f1133','%f1134','%f1135','%f1136','%f1137','keyCacheListOrParts','%f1138','keyCacheList','%f1139','%f1140','%f1141','assignToKeycache','%f1142','assignToKeycachePartition','%f1143','cacheKeyList','keyUsageElement','%f1144','keyUsageList','%f1145','%f1146','%f1147','%f1148','flushOption','%f1149','%f1150','%f1151','logType','%f1152','flushTables','%f1153','%f1154','%f1155','%f1156','flushTablesOptions','%f1157','%f1158','%f1159','preloadTail','%f1160','%f1161','%f1162','preloadList','%f1163','%f1164','%f1165','%f1166','preloadKeys','%f1167','%f1168','adminPartition','resourceGroupManagement','%f1169','%f1170','%f1171','%f1172','createResourceGroup','%f1173','%f1174','%f1175','resourceGroupVcpuList','%f1176','%f1177','%f1178','%f1179','vcpuNumOrRange','%f1180','%f1181','%f1182','resourceGroupPriority','resourceGroupEnableDisable','%f1183','%f1184','%f1185','%f1186','alterResourceGroup','%f1187','setResourceGroup','%f1188','%f1189','%f1190','threadIdList','%f1191','%f1192','%f1193','%f1194','dropResourceGroup','utilityStatement','%f1195','%f1196','describeStatement','%f1197','%f1198','%f1199','%f1200','explainStatement','%f1201','%f1202','%f1203','%f1204','%f1205','%f1206','%f1207','%f1208','explainableStatement','%f1209','%f1210','%f1211','helpCommand','useCommand','restartServer','%f1212','expr','%f1213','%f1214','%f1215','%f1216','%f1217','%f1218','%f1219','%f1220','%f1221','%f1222','%f1223','boolPri','%f1224','%f1225','%f1226','compOp','%f1227','predicate','%f1228','%f1229','%f1230','%f1231','%f1232','%f1233','predicateOperations','%f1234','%f1235','%f1236','%f1237','bitExpr','%f1238','%f1239','%f1240','%f1241','%f1242','%f1243','simpleExpr','%f1244','%f1245','%f1246','%f1247','%f1248','%f1249','%f1250','%f1251','%f1252','%f1253','%f1254','%f1255','%f1256','%f1257','%f1258','%f1259','%f1260','%f1261','%f1262','%f1263','%f1264','%f1265','%f1266','%f1267','%f1268','%f1269','%f1270','arrayCast','%f1271','jsonOperator','%f1272','%f1273','%f1274','%f1275','%f1276','%f1277','%f1278','%f1279','%f1280','%f1281','%f1282','%f1283','%f1284','%f1285','%f1286','%f1287','%f1288','%f1289','%f1290','%f1291','%f1292','%f1293','sumExpr','%f1294','%f1295','%f1296','%f1297','%f1298','%f1299','%f1300','%f1301','%f1302','%f1303','%f1304','%f1305','%f1306','%f1307','%f1308','%f1309','%f1310','%f1311','%f1312','%f1313','%f1314','%f1315','%f1316','%f1317','%f1318','%f1319','%f1320','%f1321','%f1322','%f1323','%f1324','%f1325','%f1326','%f1327','%f1328','%f1329','%f1330','%f1331','%f1332','%f1333','%f1334','%f1335','groupingOperation','%f1336','%f1337','%f1338','%f1339','%f1340','windowFunctionCall','%f1341','%f1342','%f1343','%f1344','%f1345','%f1346','windowingClause','%f1347','%f1348','leadLagInfo','%f1349','%f1350','%f1351','nullTreatment','%f1352','%f1353','%f1354','jsonFunction','%f1355','inSumExpr','identListArg','%f1356','identList','%f1357','%f1358','%f1359','fulltextOptions','%f1360','%f1361','%f1362','%f1363','%f1364','%f1365','%f1366','%f1367','%f1368','%f1369','%f1370','%f1371','%f1372','%f1373','%f1374','runtimeFunctionCall','%f1375','%f1376','%f1377','%f1378','%f1379','%f1380','%f1381','%f1382','%f1383','%f1384','%f1385','%f1386','%f1387','%f1388','%f1389','%f1390','%f1391','%f1392','%f1393','%f1394','%f1395','%f1396','%f1397','%f1398','%f1399','%f1400','%f1401','%f1402','%f1403','%f1404','geometryFunction','%f1405','%f1406','timeFunctionParameters','fractionalPrecision','%f1407','weightStringLevels','%f1408','%f1409','%f1410','%f1411','%f1412','weightStringLevelListItem','%f1413','%f1414','%f1415','%f1416','dateTimeTtype','trimFunction','%f1417','%f1418','%f1419','%f1420','%f1421','%f1422','%f1423','substringFunction','%f1424','%f1425','%f1426','%f1427','%f1428','%f1429','%f1430','%f1431','%f1432','functionCall','%f1433','udfExprList','%f1434','%f1435','%f1436','udfExpr','variable','userVariable','%f1437','%f1438','systemVariable','internalVariableName','%f1439','%f1440','%f1441','%f1442','%f1443','whenExpression','thenExpression','elseExpression','%f1444','%f1445','%f1446','%f1447','%f1448','%f1449','%f1450','%f1451','%f1452','castType','%f1453','%f1454','%f1455','%f1456','%f1457','exprList','%f1458','%f1459','charset','notRule','not2Rule','interval','%f1460','intervalTimeStamp','exprListWithParentheses','exprWithParentheses','simpleExprWithParentheses','%f1461','orderList','%f1462','%f1463','%f1464','orderExpression','%f1465','groupList','%f1466','%f1467','groupingExpression','channel','%f1468','compoundStatement','returnStatement','ifStatement','%f1469','ifBody','%f1470','%f1471','thenStatement','%f1472','compoundStatementList','%f1473','%f1474','%f1475','%f1476','%f1477','caseStatement','%f1478','%f1479','elseStatement','%f1480','labeledBlock','unlabeledBlock','label','%f1481','%f1482','beginEndBlock','%f1483','labeledControl','unlabeledControl','loopBlock','whileDoBlock','repeatUntilBlock','%f1484','spDeclarations','%f1485','%f1486','spDeclaration','%f1487','%f1488','variableDeclaration','%f1489','%f1490','conditionDeclaration','spCondition','%f1491','sqlstate','%f1492','handlerDeclaration','%f1493','%f1494','%f1495','handlerCondition','cursorDeclaration','iterateStatement','leaveStatement','%f1496','getDiagnostics','%f1497','%f1498','%f1499','%f1500','%f1501','%f1502','%f1503','%f1504','%f1505','%f1506','signalAllowedExpr','statementInformationItem','%f1507','%f1508','conditionInformationItem','%f1509','%f1510','signalInformationItemName','%f1511','signalStatement','%f1512','%f1513','%f1514','%f1515','%f1516','%f1517','%f1518','%f1519','resignalStatement','%f1520','%f1521','%f1522','%f1523','%f1524','%f1525','%f1526','signalInformationItem','cursorOpen','cursorClose','%f1527','cursorFetch','%f1528','%f1529','%f1530','%f1531','%f1532','schedule','%f1533','%f1534','%f1535','%f1536','%f1537','columnDefinition','checkOrReferences','%f1538','checkConstraint','%f1539','constraintEnforcement','%f1540','%f1541','%f1542','%f1543','%f1544','%f1545','%f1546','%f1547','%f1548','tableConstraintDef','%f1549','%f1550','%f1551','%f1552','%f1553','%f1554','%f1555','%f1556','%f1557','%f1558','%f1559','%f1560','constraintName','fieldDefinition','%f1561','%f1562','%f1563','%f1564','%f1565','%f1566','%f1567','%f1568','%f1569','%f1570','%f1571','%f1572','%f1573','%f1574','%f1575','%f1576','%f1577','%f1578','%f1579','columnAttribute','%f1580','%f1581','%f1582','%f1583','%f1584','%f1585','%f1586','%f1587','%f1588','columnFormat','storageMedia','%f1589','%f1590','%f1591','gcolAttribute','%f1592','%f1593','%f1594','references','%f1595','%f1596','%f1597','%f1598','%f1599','%f1600','%f1601','%f1602','%f1603','%f1604','%f1605','deleteOption','%f1606','%f1607','keyList','%f1608','%f1609','%f1610','%f1611','keyPart','%f1612','keyListWithExpression','%f1613','%f1614','%f1615','keyPartOrExpression','keyListVariants','%f1616','%f1617','indexType','%f1618','indexOption','%f1619','commonIndexOption','%f1620','visibility','indexTypeClause','%f1621','fulltextIndexOption','spatialIndexOption','dataTypeDefinition','%f1622','%f1623','%f1624','%f1625','%f1626','%f1627','%f1628','%f1629','%f1630','%f1631','%f1632','%f1633','%f1634','%f1635','%f1636','%f1637','%f1638','%f1639','%f1640','%f1641','%f1642','%f1643','%f1644','%f1645','%f1646','%f1647','%f1648','%f1649','%f1650','dataType','%f1651','%f1652','%f1653','%f1654','%f1655','%f1656','%f1657','%f1658','%f1659','%f1660','%f1661','%f1662','nchar','%f1663','realType','fieldLength','%f1664','%f1665','fieldOptions','%f1666','%f1667','%f1668','%f1669','charsetWithOptBinary','%f1670','%f1671','%f1672','ascii','%f1673','unicode','wsNumCodepoints','typeDatetimePrecision','charsetName','%f1674','collationName','%f1675','%f1676','%f1677','createTableOptions','%f1678','%f1679','%f1680','%f1681','createTableOptionsSpaceSeparated','%f1682','%f1683','%f1684','%f1685','%f1686','%f1687','%f1688','%f1689','%f1690','%f1691','%f1692','%f1693','%f1694','%f1695','%f1696','%f1697','%f1698','%f1699','%f1700','createTableOption','%f1701','%f1702','%f1703','%f1704','%f1705','%f1706','%f1707','%f1708','%f1709','%f1710','%f1711','%f1712','%f1713','%f1714','%f1715','%f1716','%f1717','%f1718','%f1719','%f1720','%f1721','ternaryOption','%f1722','%f1723','defaultCollation','%f1724','%f1725','defaultEncryption','%f1726','%f1727','defaultCharset','%f1728','%f1729','%f1730','partitionClause','%f1731','%f1732','%f1733','%f1734','%f1735','%f1736','partitionTypeDef','%f1737','%f1738','%f1739','%f1740','%f1741','subPartitions','%f1742','%f1743','%f1744','%f1745','partitionKeyAlgorithm','%f1746','%f1747','partitionDefinitions','%f1748','%f1749','%f1750','%f1751','%f1752','partitionDefinition','%f1753','%f1754','%f1755','%f1756','%f1757','%f1758','%f1759','%f1760','%f1761','partitionValuesIn','%f1762','%f1763','%f1764','%f1765','%f1766','%f1767','%f1768','%f1769','%f1770','partitionOption','%f1771','%f1772','%f1773','subpartitionDefinition','%f1774','partitionValueItemListParen','%f1775','%f1776','partitionValueItem','definerClause','ifExists','ifNotExists','%f1777','procedureParameter','%f1778','%f1779','functionParameter','collate','%f1780','typeWithOptCollate','schemaIdentifierPair','%f1781','viewRefList','%f1782','%f1783','%f1784','updateList','%f1785','%f1786','updateElement','%f1787','charsetClause','%f1788','fieldsClause','%f1789','fieldTerm','%f1790','linesClause','lineTerm','%f1791','%f1792','userList','%f1793','%f1794','%f1795','createUserList','%f1796','%f1797','%f1798','alterUserList','%f1799','%f1800','%f1801','createUserEntry','%f1802','%f1803','%f1804','%f1805','%f1806','%f1807','%f1808','%f1809','%f1810','%f1811','%f1812','%f1813','%f1814','%f1815','%f1816','alterUserEntry','%f1817','%f1818','%f1819','%f1820','%f1821','%f1822','%f1823','%f1824','%f1825','%f1826','%f1827','%f1828','%f1829','%f1830','%f1831','%f1832','%f1833','retainCurrentPassword','discardOldPassword','replacePassword','%f1834','userIdentifierOrText','%f1835','%f1836','%f1837','%f1838','user','likeClause','likeOrWhere','onlineOption','noWriteToBinLog','usePartition','%f1839','%f1840','fieldIdentifier','columnName','%f1841','%f1842','columnInternalRef','%f1843','columnInternalRefList','%f1844','%f1845','columnRef','insertIdentifier','indexName','indexRef','%f1846','tableWild','%f1847','%f1848','schemaName','schemaRef','procedureName','procedureRef','functionName','functionRef','triggerName','triggerRef','viewName','viewRef','tablespaceName','tablespaceRef','logfileGroupName','logfileGroupRef','eventName','eventRef','udfName','serverName','serverRef','engineRef','tableName','filterTableRef','%f1849','tableRefWithWildcard','%f1850','%f1851','%f1852','%f1853','%f1854','tableRef','%f1855','tableRefList','%f1856','%f1857','%f1858','tableAliasRefList','%f1859','%f1860','parameterName','labelIdentifier','labelRef','roleIdentifier','roleRef','pluginRef','componentRef','resourceGroupRef','windowName','pureIdentifier','%f1861','%f1862','identifier','%f1863','identifierList','%f1864','%f1865','identifierListWithParentheses','%f1866','qualifiedIdentifier','%f1867','simpleIdentifier','%f1868','%f1869','%f1870','%f1871','dotIdentifier','ulong_number','real_ulong_number','ulonglong_number','real_ulonglong_number','%f1872','%f1873','literal','%f1874','signedLiteral','%f1875','stringList','%f1876','%f1877','textStringLiteral','%f1878','textString','textStringHash','%f1879','%f1880','textLiteral','%f1881','%f1882','textStringNoLinebreak','%f1883','textStringLiteralList','%f1884','%f1885','numLiteral','boolLiteral','nullLiteral','temporalLiteral','floatOptions','standardFloatOptions','precision','textOrIdentifier','lValueIdentifier','roleIdentifierOrText','sizeNumber','parentheses','equal','optionType','varIdentType','setVarIdentType','identifierKeyword','%f1886','%f1887','%f1888','%f1889','%f1890','identifierKeywordsAmbiguous1RolesAndLabels','identifierKeywordsAmbiguous2Labels','labelKeyword','%f1891','%f1892','%f1893','identifierKeywordsAmbiguous3Roles','identifierKeywordsUnambiguous','%f1894','%f1895','%f1896','roleKeyword','%f1897','%f1898','%f1899','lValueKeyword','identifierKeywordsAmbiguous4SystemVariables','roleOrIdentifierKeyword','%f1900','%f1901','%f1902','roleOrLabelKeyword','%f1903','%f1904','%f1905','%f1906','%f1907','%f1908','%f1909'],'grammar'=>[0=>[[-1],[2001,2003]],1=>[[2004],[2873]],2=>[[-1],[0]],3=>[[755,2002],[-1]],4=>[[2009],[2221],[2414],[2470],[2476],[2005],[2479],[2485],[2504],[2508],[2524],[2571],[2598],[2603],[2856],[2860],[2934],[3079],[2006],[3103],[3278],[3301],[3314],[3361],[2007],[3443],[3534],[2008],[3945],[3954]],5=>[[2477]],6=>[[3090]],7=>[[3498]],8=>[[3925]],9=>[[11,2013]],10=>[[2191]],12=>[[2285],[0]],13=>[[2071],[2031],[422,4388,2012],[206,4390,2012],[2212],[2042],[2175],[2010],[2060],[2067],[2014]],14=>[[2029]],15=>[[33]],16=>[[844],[2015]],19=>[[2018],[0]],18=>[[373,480,383,165]],20=>[[451,845,2019]],23=>[[2022],[0]],22=>[[373,480,383,165]],24=>[[451,845,200,57,4435,2023]],25=>[[156],[140]],26=>[[2025,844,846]],27=>[[451,847]],28=>[[482,2016,316,265],[2020],[2024],[2026],[2027]],29=>[[244,2028]],30=>[[4386],[0]],31=>[[109,2030,2034]],32=>[[615,112,139,357]],33=>[[2229,2033],[2229]],34=>[[2033],[2032]],391=>[[4273],[0]],43=>[[2044],[0]],46=>[[2047],[0]],48=>[[2049],[0]],53=>[[2054],[0]],55=>[[2056],[0]],57=>[[2058],[0]],42=>[[2391,170,4400,2043,2046,2048,2053,2055,2057]],44=>[[383,490,3972]],2023=>[[371],[0]],47=>[[383,79,4023,418]],49=>[[453,590,4435]],52=>[[2051],[0]],51=>[[383,514]],54=>[[156],[140,2052]],56=>[[75,4469]],58=>[[147,3869]],59=>[[2062],[0]],60=>[[288,217,4398,4,603,4469,2059]],64=>[[2065,2064],[2065],[0]],62=>[[2066,2064]],2157=>[[750],[0]],65=>[[4157,2066]],66=>[[2345],[2359],[2362]],67=>[[503,4403,2318]],427=>[[4363],[0]],73=>[[2074],[0]],70=>[[2077],[0]],71=>[[2427,2073,574,4414,2070]],72=>[[232]],74=>[[2072]],78=>[[2079],[0]],80=>[[2081],[0]],77=>[[2078,2090],[2083,2080],[4216],[2173]],79=>[[2087,750]],81=>[[4216],[2173]],84=>[[2085],[0]],83=>[[2087,2084],[2112]],85=>[[750,2112]],88=>[[2089,2088],[2089],[0]],87=>[[2117,2088]],89=>[[750,2117]],90=>[[141,572],[234,572],[2105],[2092]],91=>[[722],[723]],92=>[[2091]],1441=>[[4364],[0]],1273=>[[3296,3273],[3296],[0]],1277=>[[3298,3277],[3298],[0]],107=>[[2108],[0]],104=>[[2170],[0]],105=>[[4,405,3441,2106],[148,405,4437],[438,405,3441,2174],[388,405,3441,2174,3441],[14,405,3441,2174],[62,405,2174,3273],[455,405,3441,2174,3277],[67,405,3441,4451],[597,405,2174],[454,405,3441,2107],[172,405,4435,645,574,4414,2104],[2109],[2110]],106=>[[4237],[404,4451]],108=>[[4437,248,4237]],109=>[[141,405,2174,572]],110=>[[234,405,2174,572]],115=>[[2116,2115],[2116],[0]],112=>[[2113,2115]],113=>[[2126],[4161]],114=>[[2126],[2117],[4161]],116=>[[750,2114]],117=>[[2162],[2165],[2170]],136=>[[72],[0]],128=>[[2153],[0]],147=>[[2148],[0]],2282=>[[4281],[0]],126=>[[4,2136,2129],[4,3993],[55,2136,4372,4435,4007,2128],[348,2136,4372,4007,2128],[148,2138],[140,263],[156,263],[11,2136,4372,2142],[2143],[2144],[2145],[2146],[453,2147,4405],[2149],[94,590,3847,2151,4282],[198],[393,45,2157],[2152]],1977=>[[3979],[0]],129=>[[4435,4007,3977,2128],[753,2242,748]],130=>[[4372]],131=>[[4372],[0]],132=>[[2131]],133=>[[2130],[2132]],134=>[[62,4435]],135=>[[86,4435]],137=>[[2154],[0]],138=>[[2136,4372,2137],[199,265,2133],[420,265],[2840,4380],[2134],[2135]],139=>[[3854]],140=>[[2139],[4458]],141=>[[506,4082]],142=>[[506,128,2140],[148,128],[2141]],143=>[[11,236,4380,4082]],144=>[[11,62,4435,3983]],145=>[[11,86,4435,3983]],146=>[[453,72,4372,590,4435]],148=>[[590],[17]],149=>[[453,2840,4380,590,4379]],150=>[[128]],151=>[[2150],[4150]],152=>[[615,403]],153=>[[6,4435],[191]],154=>[[471],[49]],2071=>[[2724],[0]],159=>[[2160,2159],[2160],[0]],157=>[[4442,4071,2159]],160=>[[750,4442,4071]],2262=>[[763],[0]],162=>[[9,4262,2163]],163=>[[128],[4435]],165=>[[287,4262,2166]],166=>[[128],[4435]],167=>[[2165],[0]],168=>[[2162],[0]],169=>[[2162,2167],[2165,2168]],170=>[[2172]],171=>[[645],[646]],172=>[[2171,625]],173=>[[452,403]],174=>[[10],[4437]],175=>[[572,4396,2189]],176=>[[4],[148]],180=>[[2179,2180],[2179],[0]],179=>[[4157,2208]],184=>[[2182],[0]],182=>[[2208,2180]],183=>[[434],[436]],185=>[[55,111,4469,2184],[2183],[371,1]],186=>[[2185]],187=>[[2200]],188=>[[2200],[0]],189=>[[2176,111,4469,2188],[2186],[453,590,4435],[2187]],467=>[[2194],[0]],191=>[[605,572,4396,506,2192,2467]],192=>[[724],[725]],196=>[[2197,2196],[2197],[0]],194=>[[2198,2196]],197=>[[4157,2198]],198=>[[2359]],202=>[[2203,2202],[2203],[0]],200=>[[2205,2202]],203=>[[4157,2205]],205=>[[238,4262,4487],[2350],[2352],[2359],[2206],[2362],[2369]],206=>[[2361]],208=>[[238,4262,4487],[2350],[2352]],374=>[[2376],[0]],372=>[[2378],[0]],212=>[[2374,2391,2372,636,4394,2214]],1233=>[[4374],[0]],214=>[[3233,17,2216]],215=>[[2218],[0]],216=>[[2251,2215]],219=>[[2220],[0]],218=>[[645,2219,62,391]],220=>[[50],[284]],221=>[[97,2225]],222=>[[2408]],223=>[[2412]],224=>[[2328]],225=>[[2228],[2233],[2270],[2258],[2280],[2308],[2373],[2382],[2290],[2316],[2324],[2396],[2222],[2223],[2224]],1391=>[[4275],[0]],227=>[[2229,2227],[2229],[0]],228=>[[109,3391,4385,2227]],229=>[[4212],[4206],[2230]],230=>[[4209]],441=>[[577],[0]],233=>[[2441,574,3391,4405,2240]],236=>[[2235],[0]],235=>[[753,2242,748]],237=>[[4156],[0]],238=>[[4216],[0]],239=>[[2248],[0]],240=>[[275,4414],[753,275,4414,748],[2236,2237,2238,2239]],243=>[[2244,2243],[2244],[0]],242=>[[2245,2243]],244=>[[750,2245]],245=>[[3978],[3993]],249=>[[2250],[0]],762=>[[17],[0]],248=>[[2249,2762,2251]],250=>[[458],[232]],251=>[[2608],[2636]],252=>[[755],[0]],253=>[[97,2254,2252,-1]],254=>[[2258],[2270],[2280]],265=>[[2266],[0]],269=>[[2283,2269],[2283],[0]],258=>[[2391,422,2261,4387,753,2265,748,2269,3869]],260=>[[3391]],261=>[[2260]],264=>[[2263,2264],[2263],[0]],263=>[[750,4277]],266=>[[4277,2264]],277=>[[2278],[0]],270=>[[2391,206,2273,4389,753,2277,748,474,4283,2269,3869]],272=>[[3391]],273=>[[2272]],276=>[[2275,2276],[2275],[0]],275=>[[750,4280]],278=>[[4280,2276]],279=>[[8],[0]],280=>[[2279,206,4401,474,2281,520,4469]],281=>[[556],[249],[437],[126]],283=>[[2286],[4023,137]],284=>[[2283,2284],[2283]],285=>[[2284]],286=>[[75,4469],[267,537],[373,537],[90,537],[433,537,112],[347,537,112],[537,496,2287]],287=>[[130],[250]],428=>[[2169],[0]],290=>[[2427,2299,2428]],291=>[[4083],[0]],292=>[[4379,2291]],2000=>[[2302],[0]],294=>[[2292],[4000]],295=>[[609],[0]],2001=>[[4078,4001],[4078],[0]],1988=>[[4085,3988],[4085],[0]],1991=>[[4086,3991],[4086],[0]],299=>[[2295,236,2294,2306,4001],[205,236,4379,2306,3988],[523,236,4379,2306,3991]],2002=>[[4379],[0]],304=>[[2305],[0]],302=>[[4002,2304]],303=>[[621],[599]],305=>[[2303,4076]],306=>[[383,4414,4073]],307=>[[2311],[0]],308=>[[288,217,4397,4,2309,4469,2307]],309=>[[603],[440]],313=>[[2314,2313],[2314],[0]],311=>[[2315,2313]],314=>[[4157,2315]],315=>[[2345],[2347],[2356],[2359],[2362],[2365]],316=>[[503,4402,199,112,648,4484,2318]],319=>[[2320,2319],[2320],[0]],318=>[[390,753,2321,2319,748]],320=>[[750,2321]],321=>[[224,4469],[109,4469],[618,4469],[406,4469],[519,4469],[398,4469],[413,4450]],325=>[[2326],[0]],323=>[[2336],[0]],324=>[[572,4395,2329,2325,2323]],326=>[[620,288,217,4398]],328=>[[605,572,4395,4,2334,2467]],329=>[[2333],[4,2334]],332=>[[2331],[0]],331=>[[4,2334]],333=>[[2332]],334=>[[111,4469]],338=>[[2339,2338],[2339],[0]],336=>[[2340,2338]],339=>[[4157,2340]],340=>[[2345],[2350],[2352],[2354],[2356],[2359],[2341],[2362],[2365],[2342],[2343]],341=>[[2361]],342=>[[2367]],343=>[[2369]],345=>[[238,4262,4487]],347=>[[2348,4262,4487]],348=>[[604],[441]],350=>[[23,4262,4487]],352=>[[324,4262,4487]],354=>[[181,4262,4487]],356=>[[368,4262,4451]],2257=>[[553],[0]],359=>[[4257,163,4262,4404]],361=>[[848,4262,4463]],362=>[[2363]],363=>[[638],[374]],365=>[[75,4262,4469]],367=>[[189,4262,4487]],369=>[[158,4262,4463]],370=>[[2375],[0]],373=>[[2370,2391,2372,636,4393,2214]],375=>[[394,458,2374],[2376]],376=>[[9,763,2377]],377=>[[602],[335],[578]],378=>[[537,496,2379]],379=>[[130],[250]],381=>[[2388],[0]],382=>[[2391,594,2385,4391,2386,2387,383,4414,200,153,487,2381,3869]],384=>[[3391]],385=>[[2384]],386=>[[28],[6]],387=>[[242],[614],[133]],388=>[[2390]],389=>[[197],[415]],390=>[[2389,4484]],398=>[[2399],[0]],403=>[[2404],[0]],405=>[[2406],[0]],396=>[[2391,170,3391,4399,383,490,3972,2398,2403,2405,147,3869]],399=>[[383,79,4023,418]],402=>[[2401],[0]],401=>[[383,514]],404=>[[156],[140,2402]],406=>[[75,4469]],408=>[[659,3391,3264]],411=>[[2413,2411],[2413],[0]],412=>[[394,458,523,718,710,4453,2411],[523,718,710,3391,4453,2411]],413=>[[357,580,4472],[715,580,4472],[717,4472,230,45,4453],[716,580,4472]],414=>[[148,2418]],415=>[[2464]],416=>[[2466]],417=>[[2468]],418=>[[2420],[2422],[2424],[2426],[2429],[2431],[2440],[2444],[2449],[2457],[2460],[2415],[2416],[2417]],939=>[[4274],[0]],420=>[[109,2939,4386]],422=>[[170,2939,4400]],424=>[[206,2939,4390]],426=>[[422,2939,4388]],429=>[[2427,236,4380,383,4414,2428]],436=>[[2437],[0]],431=>[[288,217,4398,2436]],435=>[[2434,2435],[2434],[0]],434=>[[4157,2438]],437=>[[2438,2435]],438=>[[2362],[2359]],440=>[[503,2939,4403]],446=>[[2447],[0]],444=>[[2441,2445,2939,4416,2446]],445=>[[574],[571]],447=>[[471],[49]],454=>[[2455],[0]],449=>[[572,4396,2454]],453=>[[2452,2453],[2452],[0]],452=>[[4157,2438]],455=>[[2438,2453]],457=>[[594,2939,4392]],461=>[[2462],[0]],460=>[[636,2939,4286,2461]],462=>[[471],[49]],464=>[[659,2939,3264]],466=>[[523,718,710,2939,4453]],468=>[[605,572,4396,2467]],472=>[[2473,2472],[2473],[0]],470=>[[453,2471,2474,2472]],471=>[[574],[571]],473=>[[750,2474]],474=>[[4414,590,4405]],475=>[[574],[0]],476=>[[597,2475,4414]],477=>[[234,574,203,4474]],481=>[[2482],[0]],479=>[[48,4388,2481]],1807=>[[3844],[0]],482=>[[753,3807,748]],487=>[[2488],[0]],484=>[[2503,2484],[2503],[0]],485=>[[2487,133,2484,2500]],486=>[[2714]],488=>[[2486]],489=>[[2829]],493=>[[2491],[0]],491=>[[2489]],1415=>[[2765],[0]],494=>[[2501],[0]],1646=>[[2723],[0]],855=>[[2654],[0]],498=>[[4420,621,2728,3415],[4414,2493,2494,3415,3646,2855]],500=>[[203,2498],[4420,203,2728,3415]],501=>[[2502]],502=>[[405,753,4437,748]],503=>[[431],[295],[431],[232]],504=>[[147,2507]],505=>[[2756]],506=>[[3844]],507=>[[2505],[2506]],508=>[[219,2513]],1421=>[[2653],[0]],511=>[[66],[435,2514,3415,3421]],901=>[[2829],[0]],513=>[[4414,387,2901],[4435,2511]],514=>[[2515],[4435,2518]],515=>[[191],[367]],516=>[[191],[367],[419],[268]],517=>[[763],[769],[765],[768],[764]],518=>[[2516],[2517,753,2555,748]],519=>[[2534],[0]],852=>[[232],[0]],596=>[[248],[0]],791=>[[4365],[0]],523=>[[2562],[0]],524=>[[242,2519,2852,2596,4414,2791,2533,2523]],525=>[[2561]],531=>[[2527],[0]],527=>[[2525]],528=>[[2561]],532=>[[2530],[0]],530=>[[2528]],533=>[[2536,2531],[506,4290,2532],[2547]],534=>[[295],[131],[223]],538=>[[2539],[0]],536=>[[2538,2544]],546=>[[2541],[0]],539=>[[753,2546,748]],542=>[[2543,2542],[2543],[0]],541=>[[4378,2542]],543=>[[750,4378]],544=>[[2545,2550]],545=>[[626],[627]],547=>[[2251],[753,2546,748,2251]],736=>[[2555],[0]],552=>[[2553,2552],[2553],[0]],550=>[[753,2736,748,2552]],553=>[[750,753,2736,748]],558=>[[2559,2558],[2559],[0]],555=>[[2556,2558]],556=>[[3559],[128]],557=>[[3559],[128]],559=>[[750,2557]],561=>[[17,4435,3233]],562=>[[383,151,265,614,4290]],572=>[[2573],[0]],903=>[[284],[0]],574=>[[2575],[0]],667=>[[4295],[0]],568=>[[2577],[0]],668=>[[4297],[0]],669=>[[4301],[0]],571=>[[281,2576,2572,2903,237,4469,2574,248,574,4414,2791,2667,2568,2668,2669,2581]],573=>[[295],[82]],575=>[[458],[232]],576=>[[112],[653]],577=>[[484,230,45,4465]],583=>[[2584],[0]],579=>[[2588],[0]],585=>[[2586],[0]],581=>[[2583,2579,2585]],582=>[[278],[484]],584=>[[232,787,2582]],586=>[[506,4290]],587=>[[2590],[0]],588=>[[753,2587,748]],593=>[[2594,2593],[2594],[0]],590=>[[2591,2593]],591=>[[4377],[3816]],592=>[[4377],[3816]],594=>[[750,2592]],599=>[[2600],[0]],598=>[[458,2599,2596,4414,2791,2601]],600=>[[295],[131]],601=>[[2536],[506,4290],[2547]],635=>[[2742],[0]],603=>[[2608,2635],[2605]],605=>[[753,2605,748],[2608,2662,2635],[2608,2742,2662]],610=>[[2611],[0]],618=>[[2619],[0]],608=>[[2610,2616,2618]],609=>[[2714]],611=>[[2609]],616=>[[2621,3646,3421],[2636,3646,3421]],617=>[[2673]],619=>[[2617]],625=>[[2626,2625],[2626],[0]],621=>[[2628,2625]],622=>[[663]],623=>[[608],[2622]],631=>[[2827],[0]],626=>[[2623,2631,2628]],633=>[[2634,2633],[2634],[0]],628=>[[2629,2633]],629=>[[2637],[2636]],630=>[[2637],[2636]],632=>[[811,2631,2630]],634=>[[2632]],636=>[[753,2608,2635,748]],637=>[[2647],[2638],[2639]],638=>[[2732]],639=>[[2735]],640=>[[2738,2640],[2738],[0]],641=>[[2662],[0]],642=>[[2725],[0]],644=>[[2720],[0]],645=>[[2679],[0]],649=>[[2650],[0]],647=>[[497,2640,2756,2641,2642,3415,2644,2645,2649]],648=>[[2681]],650=>[[2648]],651=>[[2636]],652=>[[10],[143],[555],[223],[536],[531],[532],[534]],653=>[[276,2656]],654=>[[276,2660]],658=>[[2659],[0]],656=>[[2660,2658]],657=>[[750],[381]],659=>[[2657,2660]],660=>[[4435],[2661]],661=>[[754],[791],[788],[787]],662=>[[248,2671]],663=>[[4484],[3816]],664=>[[4484],[3816]],670=>[[2666,2670],[2666],[0]],666=>[[750,2664]],671=>[[396,4463,2667,2668,2669],[150,4463],[2663,2670]],677=>[[2678],[0]],673=>[[422,13,753,2677,748]],676=>[[2675],[0]],675=>[[750,787]],678=>[[787,2676]],679=>[[221,3559]],682=>[[2683,2682],[2683],[0]],681=>[[699,2684,2682]],683=>[[750,2684]],684=>[[4431,17,2685]],685=>[[753,2696,748]],695=>[[2704],[0]],697=>[[2698],[0]],699=>[[2700],[0]],692=>[[4431],[0]],701=>[[2702],[0]],696=>[[405,45,3857,3646,2695],[2697,2723,2695],[2699,3646,2704],[2692,2701,3646,2695]],698=>[[405,45,3857]],700=>[[405,45,3857]],702=>[[405,45,3857]],703=>[[2710],[0]],704=>[[2705,2706,2703]],705=>[[484],[432],[683]],706=>[[2707],[2708]],707=>[[698,693],[4452,693],[754,693],[247,3559,3850,693],[101,487]],708=>[[30,2709,15,2709]],709=>[[2707],[698,682],[4452,682],[754,682],[247,3559,3850,682]],710=>[[680,2711]],711=>[[101,487],[217],[697],[373,690]],712=>[[665],[0]],715=>[[2716,2715],[2716],[0]],714=>[[645,2712,2718,2715]],716=>[[750,2718]],718=>[[4435,3233,17,2651]],719=>[[2721],[0]],720=>[[217,45,3857,2719]],721=>[[645,481],[2722]],722=>[[645,99]],723=>[[393,45,3857]],724=>[[18],[134]],725=>[[203,2726]],726=>[[149],[2728]],729=>[[2730,2729],[2730],[0]],728=>[[2767,2729]],730=>[[750,2767]],733=>[[2734,2733],[2734],[0]],732=>[[626,2737,2733]],734=>[[750,2737]],735=>[[574,4414]],737=>[[487,753,2736,748]],738=>[[2652],[535],[2739],[2740]],739=>[[533]],740=>[[325,763,4451]],741=>[[2745,2741],[2745]],742=>[[2741]],747=>[[2748],[0]],750=>[[2751],[0]],745=>[[200,2752,2747,2750],[287,251,508,346]],746=>[[668,4420]],748=>[[2746]],749=>[[2754]],751=>[[2749]],752=>[[614],[2753]],753=>[[508]],754=>[[669,670],[671]],758=>[[2759,2758],[2759],[0]],756=>[[2757,2758]],757=>[[2761],[775]],759=>[[750,2761]],1813=>[[2763],[0]],761=>[[4382],[3559,3813]],763=>[[2762,2764]],764=>[[4435],[4463]],765=>[[643,3559]],771=>[[2774,2771],[2774],[0]],767=>[[2770,2771]],768=>[[4435]],769=>[[2768],[732]],770=>[[2789],[752,2769,2772,747]],772=>[[2789,2771]],775=>[[2776],[0]],774=>[[2783,2767,2775],[2787,2767,2777],[2780,2789]],776=>[[383,3559],[621,4440]],777=>[[383,3559],[621,4440]],778=>[[239],[0]],786=>[[395],[0]],780=>[[359,2778,261],[359,2781,2786,261]],781=>[[272],[478]],784=>[[2785],[0]],783=>[[2784,261],[555]],785=>[[239],[98]],787=>[[2788,2786,261]],788=>[[272],[478]],789=>[[2794],[2795],[2799],[2806],[2790]],790=>[[2809]],793=>[[2834],[0]],794=>[[4414,2791,2901,2793]],795=>[[753,2796,748]],796=>[[2794],[2795]],801=>[[2802],[0]],799=>[[2651,2901,2801],[2805]],800=>[[4374]],802=>[[2800]],805=>[[726,2651,2901,3233]],806=>[[753,2807,748]],807=>[[2728],[2806]],809=>[[701,753,3559,750,4463,2811,748,2901]],812=>[[2813,2812],[2813],[0]],811=>[[71,753,2817,2812,748]],813=>[[750,2817]],819=>[[2820],[0]],1606=>[[174],[0]],1749=>[[2823],[0]],817=>[[4435,200,703],[4435,4117,2819,3606,704,4463,3749],[702,704,4463,2811]],818=>[[4281]],820=>[[2818]],821=>[[2825],[0]],822=>[[2824],[0]],823=>[[2824,2821],[2825,2822]],824=>[[2826,383,700]],825=>[[2826,383,165]],826=>[[165],[376],[128,4463]],827=>[[143],[10]],831=>[[2832],[0]],829=>[[2831,4435]],830=>[[763]],832=>[[17],[2830]],833=>[[2838,2833],[2838]],834=>[[2833]],836=>[[2843],[0]],837=>[[2846],[0]],838=>[[2839,2840,2836,753,2846,748],[620,2840,2836,753,2837,748]],839=>[[198],[232]],840=>[[265],[236]],1995=>[[2840],[0]],842=>[[420,265],[609,3995]],843=>[[200,2844]],844=>[[261],[393,45],[217,45]],847=>[[2848,2847],[2848],[0]],846=>[[2849,2847]],848=>[[750,2849]],849=>[[4435],[420]],858=>[[2859],[0]],904=>[[295],[0]],856=>[[2858,614,2904,2852,2728,506,4290,3415,3646,2855]],857=>[[2714]],859=>[[2857]],860=>[[2865],[2882],[2894],[2906]],861=>[[2875],[0]],881=>[[647],[0]],867=>[[2868],[0]],870=>[[2871],[0]],865=>[[543,592,2861],[77,2881,2867,2870]],1101=>[[373],[0]],868=>[[15,3101,54]],871=>[[3101,450]],873=>[[29,2881]],876=>[[2877,2876],[2877],[0]],875=>[[2878,2876]],877=>[[750,2878]],878=>[[645,85,517],[2880]],879=>[[649],[386]],880=>[[435,2879]],882=>[[489,4435],[480,2881,2892],[450,489,4435]],890=>[[2885],[0]],885=>[[15,3101,54]],891=>[[2888],[0]],888=>[[3101,450]],889=>[[489],[0]],892=>[[590,2889,4435],[2890,2891]],896=>[[2897,2896],[2897],[0]],894=>[[287,2895,2902,2896],[2898],[611,2900]],895=>[[571],[574]],897=>[[750,2902]],898=>[[287,244,200,27]],899=>[[244]],900=>[[571],[574],[2899]],902=>[[4414,2901,2905]],905=>[[435,2903],[2904,649]],906=>[[651,2920]],907=>[[543],[29]],917=>[[2909],[0]],909=>[[261],[472]],912=>[[2911],[0]],911=>[[200,340]],918=>[[2914],[0]],914=>[[566,2912]],919=>[[2916],[0]],916=>[[384,407]],920=>[[2907,2927,2917],[159,2927,2918],[417,2927],[77,2927,2919],[480,2927],[439,2921]],921=>[[2925],[0]],924=>[[2923],[0]],923=>[[94,652]],925=>[[2924]],931=>[[2932],[0]],927=>[[4465,2931]],930=>[[2929],[0]],929=>[[750,4450]],932=>[[750,4465,2930]],937=>[[2938,2937],[2938],[0]],934=>[[428,2935,289,2936],[2959],[468,2949,2937],[2943],[3047],[2944],[2956],[2945]],935=>[[32],[316]],936=>[[590,4469],[28,3559]],938=>[[750,2949]],942=>[[2941],[0]],941=>[[2939,3820]],943=>[[468,658,2942]],944=>[[2979]],945=>[[3077]],946=>[[2951],[0]],1717=>[[10],[0]],1469=>[[3867],[0]],949=>[[316,2946],[2950],[514,3717,3469]],950=>[[430,47]],951=>[[2955]],952=>[[4451]],953=>[[4453]],954=>[[2952],[2953]],955=>[[590,2954]],956=>[[281,2957,203,316]],957=>[[112],[574,4414]],959=>[[55,316,590,2961,3469]],962=>[[2963,2962],[2963],[0]],961=>[[2964,2962]],963=>[[750,2964]],964=>[[300,763,4472],[729,763,4472],[297,763,4472],[318,763,4472],[303,763,4472],[304,763,4450],[298,763,4450],[305,763,4450],[299,763,4450],[314,763,4450],[308,763,4472],[307,763,4472],[317,763,4472],[309,763,4472],[738,763,2967],[310,763,4472],[313,763,4472],[315,763,4450],[311,763,4469],[312,763,4472],[712,763,4472],[713,763,4450],[319,763,4450],[233,763,2970],[735,763,4463],[736,763,4450],[296,763,4450],[737,763,2965],[739,763,4450],[742,763,2966],[2968]],965=>[[4355],[376]],966=>[[743],[383],[744]],967=>[[4472],[376]],968=>[[301,763,4472],[302,763,4452],[447,763,4472],[448,763,4450]],974=>[[2975],[0]],970=>[[753,2974,748]],973=>[[2972,2973],[2972],[0]],972=>[[750,4450]],975=>[[4450,2973]],980=>[[2981,2980],[2981],[0]],983=>[[2984],[0]],979=>[[55,459,522,590,2986,3469],[55,459,190,3024,2980,2983]],981=>[[750,3024]],982=>[[3867]],984=>[[2982]],987=>[[2988,2987],[2988],[0]],986=>[[2989,2987]],988=>[[750,2989]],989=>[[2990,763,4472],[2991,763,4472],[2992,763,4472],[2993,763,4472],[2994,763,4450],[2995,763,4472],[2996,763,4452],[2997,763,4450],[2998,763,4450],[2999,763,4450],[3000,763,4450],[817,763,4450],[3001,763,4450],[3002,763,4463],[3003,763,4450],[3004,763,4450],[3005,763,4472],[3006,763,4472],[3007,763,4472],[3008,763,4469],[3009,763,4472],[3010,763,4472],[3011,763,4472],[3012,763,4450],[3013,763,4472],[3014,763,2967],[3015,763,4472],[3016,763,4450],[729,763,4472],[233,763,2970],[841,763,4450],[737,763,2965],[739,763,4450],[742,763,2966],[842,763,4450],[447,763,4472],[448,763,4450]],990=>[[814],[297]],991=>[[820],[300]],992=>[[838],[318]],993=>[[823],[303]],994=>[[824],[304]],995=>[[821],[301]],996=>[[822],[302]],997=>[[813],[296]],998=>[[819],[319]],999=>[[816],[298]],1000=>[[826],[305]],1001=>[[818],[299]],1002=>[[815],[735]],1003=>[[839],[736]],1004=>[[827],[314]],1005=>[[828],[308]],1006=>[[829],[307]],1007=>[[830],[309]],1008=>[[832],[311]],1009=>[[833],[312]],1010=>[[834],[313]],1011=>[[831],[310]],1012=>[[835],[315]],1013=>[[837],[317]],1014=>[[836],[738]],1015=>[[825],[712]],1016=>[[840],[713]],1018=>[[3026],[0]],1020=>[[3030],[0]],1022=>[[3034],[0]],1023=>[[3039],[0]],1024=>[[460,763,753,3018,748],[461,763,753,3018,748],[462,763,753,3020,748],[463,763,753,3020,748],[464,763,753,3022,748],[465,763,753,3022,748],[466,763,753,3023,748]],1027=>[[3028,3027],[3028],[0]],1026=>[[4386,3027]],1028=>[[750,4386]],1031=>[[3032,3031],[3032],[0]],1030=>[[4406,3031]],1032=>[[750,4406]],1035=>[[3036,3035],[3036],[0]],1034=>[[3037,3035]],1036=>[[750,3037]],1037=>[[4472]],1040=>[[3041,3040],[3041],[0]],1039=>[[4284,3040]],1041=>[[750,4284]],1045=>[[3073],[0]],1048=>[[3049],[0]],1047=>[[543,514,3045,3048,3058,3469],[552,514,3045,3469]],1049=>[[613,3051]],1056=>[[3057,3056],[3057],[0]],1051=>[[3055,3056]],1052=>[[530],[528]],1053=>[[3052,763,4465]],1054=>[[529]],1055=>[[2968],[3053],[3054]],1057=>[[750,2968]],1058=>[[3071],[0]],1067=>[[3060],[0]],1060=>[[618,763,4465]],1068=>[[3062],[0]],1062=>[[406,763,4465]],1069=>[[3064],[0]],1064=>[[129,763,4465]],1070=>[[3066],[0]],1066=>[[409,763,4465]],1071=>[[3067,3068,3069,3070]],1074=>[[3075,3074],[3075],[0]],1073=>[[3076,3074]],1075=>[[750,3076]],1076=>[[449],[538]],1077=>[[3078,210]],1078=>[[543],[552]],1079=>[[417,4435,203,3080],[3083],[3081,417,4435]],1080=>[[4469],[3816]],1081=>[[123],[148]],1084=>[[3085],[0]],1083=>[[173,4435,3084]],1085=>[[621,3087]],1088=>[[3089,3088],[3089],[0]],1087=>[[3816,3088]],1089=>[[750,3816]],1090=>[[677,3097]],1096=>[[3092],[0]],1092=>[[200,459]],1093=>[[3100],[0]],1094=>[[244,203,4360,749,4450,230,45,4463,3093]],2183=>[[4489],[0]],1097=>[[284,112,139,4183,4463],[676,3096],[3094]],1099=>[[3102],[0]],1100=>[[3102],[112,139,4183,4463,3099]],1102=>[[467,3101,539]],1103=>[[3104],[3119],[3158],[3162],[3193],[3198],[3105]],1104=>[[3107]],1105=>[[3258]],1109=>[[3110],[0]],1107=>[[11,618,3109,3111]],1108=>[[4274]],1110=>[[3108]],1111=>[[3114],[3117,3122]],1112=>[[3118],[4360]],1113=>[[10],[369],[3264]],1114=>[[3112,128,659,3113]],1115=>[[4313]],1116=>[[4309]],1117=>[[3115],[3116]],1118=>[[618,4488]],1119=>[[97,618,3121,4309,3132,3122]],1120=>[[4275]],1121=>[[3120],[0]],1122=>[[3131],[0]],1123=>[[75],[812]],1124=>[[3123,4465]],1130=>[[3126],[0]],1126=>[[3124]],1127=>[[3137],[0]],1128=>[[3141],[0]],1129=>[[3144,3129],[3144],[0]],1131=>[[3127,3128,3129,3130]],1132=>[[3136],[0]],1135=>[[3134],[0]],1134=>[[128,659,3264]],1136=>[[3135]],1137=>[[467,3139]],1138=>[[539],[650],[369]],1139=>[[3251],[3138]],1142=>[[3143,3142],[3143]],1141=>[[645,3142]],1143=>[[322,4450],[327,4450],[321,4450],[328,4450]],1144=>[[2,3145],[406,3155],[740,3156],[741,4451]],1145=>[[287],[611]],1154=>[[3147],[0]],1147=>[[247,4451,122],[365],[128]],1148=>[[4451],[128]],1149=>[[4451,122],[128]],1152=>[[3151],[0]],1151=>[[128],[719]],1153=>[[467,101,3152]],1155=>[[177,3154],[705,3148],[706,247,3149],[3153]],1156=>[[4451],[698]],1160=>[[3161],[0]],1158=>[[148,618,3160,4305]],1159=>[[4274]],1161=>[[3159]],1162=>[[215,3176]],1165=>[[3164],[0]],1164=>[[645,660,391]],1166=>[[3225,590,4305,3165]],1210=>[[421],[0]],1168=>[[3225],[10,3210]],1175=>[[3170],[0]],1170=>[[645,215,391]],1218=>[[3223],[0]],1172=>[[3190],[0]],1173=>[[3180],[0]],1174=>[[3189],[0]],1176=>[[3166],[3168,383,3218,3245,590,3177,3172,3173,3174],[427,383,4360,590,3177,3175]],1177=>[[3178],[3179]],1178=>[[4309]],1179=>[[4305]],1180=>[[3182],[3183]],1181=>[[3256,3181],[3256]],1182=>[[645,3181]],1183=>[[645,215,391]],1184=>[[663,3264]],1185=>[[645,659,3187]],1186=>[[3184],[0]],1187=>[[3264],[10,3186],[369],[128]],1188=>[[3185],[0]],1189=>[[17,618,3188]],1190=>[[3191]],1191=>[[3137]],1194=>[[3195,3194],[3195],[0]],1193=>[[453,618,4360,590,4360,3194]],1195=>[[750,4360,590,4360]],1200=>[[3201],[0]],1213=>[[3214],[0]],1198=>[[477,3200,3211,3213]],1199=>[[4274]],1201=>[[3199]],1202=>[[3225,203,4305]],1203=>[[3215],[0]],1207=>[[3205],[0]],1205=>[[3203,203,4305]],1208=>[[383,3218,3245,3207]],1209=>[[3208],[750,215,391,203,4305]],1211=>[[3202],[3225,3215,203,4305],[10,3210,3209],[427,383,4360,203,4305]],1212=>[[232,610,618]],1214=>[[3212]],1215=>[[3217],[3222]],1217=>[[383,3218,3245]],1221=>[[3220],[0]],1220=>[[383,3218,3245]],1222=>[[3221]],1223=>[[574],[206],[422]],1226=>[[3227,3226],[3227],[0]],1225=>[[3231,3226]],1227=>[[750,3231]],1241=>[[3242],[0]],1230=>[[483],[0]],1231=>[[3235],[3236,3233],[3238],[3239],[215,391],[509,110],[97,3241],[287,571],[459,3243],[509,636],[11,3230]],1232=>[[792],[746,4484]],1234=>[[4486,3232],[4486,3233]],1235=>[[3234]],1236=>[[497],[242],[614],[443]],1237=>[[97],[148]],1238=>[[3237,659]],1239=>[[133],[616],[236],[148],[173],[451],[510],[423],[188],[427],[565],[170],[594]],1240=>[[483],[572],[618],[636]],1242=>[[577,571],[3240]],1243=>[[65],[514]],1246=>[[3247],[0]],1245=>[[775,3246],[4386,751,3249],[4386],[4414]],1247=>[[751,775]],1248=>[[4414]],1249=>[[775],[3248]],1253=>[[3254,3253],[3254],[0]],1251=>[[3255,3253]],1252=>[[15],[0]],1254=>[[3252,3255]],1255=>[[63,4465],[259,4465],[559,4465]],1256=>[[215,391],[322,4450],[327,4450],[321,4450],[328,4450]],1261=>[[3262],[0]],1258=>[[506,659,3264],[506,659,3259],[506,128,659,3260,590,3264],[506,659,10,3261]],1259=>[[369],[128]],1260=>[[3264],[369],[10]],1262=>[[663,3264]],1265=>[[3266,3265],[3266],[0]],1264=>[[3268,3265]],1266=>[[750,3268]],1269=>[[3270],[0]],1268=>[[4486,3269]],1270=>[[746,4484],[792]],1281=>[[3282],[0]],1285=>[[3286],[0]],1278=>[[14,3441,3279,4416,3281],[62,3283,4416,3273],[61,3284,4416,3285],[388,3441,3287,4416],[455,3441,3288,4416,3277]],1279=>[[574],[571]],1280=>[[3291]],1282=>[[3280]],1283=>[[574],[571]],1284=>[[574],[571]],1286=>[[431],[180]],1287=>[[574],[571]],1288=>[[574],[571]],1292=>[[3293],[0]],1294=>[[3295],[0]],1291=>[[614,674,383,4437,3292,3294],[148,674,383,4437]],1293=>[[645,787,675]],1295=>[[621,112,4463]],1296=>[[200,615],[3297]],1297=>[[431],[184],[333],[180],[56]],1298=>[[431],[180],[619]],1302=>[[3303],[0]],1304=>[[3305,3304],[3305],[0]],1301=>[[245,410,4435,520,4463],[245,664,4474,3302],[607,410,4428],[607,664,4429,3304]],1303=>[[506,3311]],1305=>[[750,4429]],1306=>[[214],[658]],1307=>[[3306],[0]],1308=>[[3307,3820,4489,3309]],1309=>[[383],[3559]],1312=>[[3313,3312],[3313],[0]],1311=>[[3308,3312]],1313=>[[750,3308]],1314=>[[506,3316]],1317=>[[3318],[0]],1316=>[[592,3334],[406,3317,4489,3325],[3331],[3348,3345],[4490,3355]],1318=>[[200,4360]],1319=>[[382,753,4465,748]],1320=>[[406,753,4465,748]],2343=>[[4353],[0]],2344=>[[4351],[0]],1325=>[[4465,4343,4344],[4465,4343,4344],[3319],[3320]],1328=>[[3327],[0]],1327=>[[200,4360]],1331=>[[406,3328,590,734,4343,4344]],1335=>[[3336],[0]],1337=>[[3338],[0]],1334=>[[3339,3335],[3341,3337]],1336=>[[750,3341]],1338=>[[750,3339]],1339=>[[435,3340]],1340=>[[649],[386]],1341=>[[258,274,3343]],1342=>[[76],[601]],1343=>[[456,435],[435,3342],[500]],1346=>[[3347,3346],[3347],[0]],1345=>[[3346]],1347=>[[750,3352]],1348=>[[3820,4489,3357],[4295],[3816,4489,3559],[3354,4489,3357],[356,3351]],1349=>[[128]],1351=>[[4489,3559],[4150,4282],[3349]],1352=>[[4490,3820,4489,3357],[3348]],1353=>[[4492],[0]],1354=>[[745,3353,3820]],1355=>[[3356,3345],[592,3334]],1356=>[[3820,4489,3357]],1357=>[[3559],[3358],[3360]],1358=>[[128],[383],[10],[32]],1359=>[[487],[710]],1360=>[[3359]],1361=>[[509,3430]],1362=>[[22]],1363=>[[4404],[10]],1364=>[[547],[354],[289]],1365=>[[203],[251]],1366=>[[32],[316]],1368=>[[225],[547,3434,3469]],1369=>[[33],[446]],1409=>[[3371],[0]],1371=>[[251,4465]],1410=>[[3373],[0]],1373=>[[203,4452]],1374=>[[180]],1413=>[[3376],[0]],1376=>[[3374]],1377=>[[236],[235],[263]],1378=>[[639],[166]],1381=>[[3380,3381],[3380],[0]],1380=>[[750,3439]],1419=>[[3383],[0]],1383=>[[3439,3381]],1420=>[[3385],[0]],1385=>[[200,430,787]],1386=>[[547],[631]],1387=>[[93]],1427=>[[3389],[0]],1389=>[[200,4360]],1390=>[[618,4360]],1392=>[[109,3391,4386],[170,4400],[206,4390],[422,4388],[574,4414],[594,4392],[636,4394],[3390]],1429=>[[4362],[0]],1406=>[[3431],[0]],1414=>[[3438],[0]],1432=>[[204],[0]],1422=>[[4490],[0]],1430=>[[3362],[110,3429],[3406,571,3414,3429],[3432,593,3414,3429],[169,3414,3429],[574,547,3414,3429],[387,571,3414,3429],[408],[163,3363,3364],[3406,71,3365,4414,3414,3429],[3366,289],[514,3368],[3369,169,3409,3410,3421,3469],[3413,3377,3437,4414,3414,3415],[4257,162],[95,753,775,748,3378],[639,3421],[166,3421],[426],[425,3419,3420,3421],[3422,3386,3429],[3432,424],[3847,3429],[70,3429],[3387],[421],[216,200,4360,621,4305],[216,3427],[316,547],[97,3392],[422,547,3429],[206,547,3429],[422,68,4388],[206,68,4390]],1431=>[[204],[3433]],1433=>[[180,3432]],1434=>[[3436],[0]],1435=>[[370],[0]],1436=>[[3435]],1437=>[[203],[251]],1438=>[[3437,4435]],1439=>[[40,255],[91,568],[400,185],[3440]],1440=>[[10],[96],[256],[334],[522],[567]],1449=>[[3450],[0]],1443=>[[33,4469],[47,236,3452,251,3444],[196,3441,3448],[266,3449,3559],[281,236,248,47,3485],[3451]],1444=>[[4435],[128]],1447=>[[3446,3447],[3446],[0]],1446=>[[750,3470]],1448=>[[3476],[3470,3447]],1450=>[[84],[430]],1451=>[[510]],1452=>[[3454],[3460]],1455=>[[3456,3455],[3456],[0]],1454=>[[3458,3455]],1456=>[[750,3458]],1492=>[[3462],[0]],1458=>[[4414,3492]],1460=>[[4414,405,753,2174,748,3492]],1461=>[[3465],[0]],1462=>[[2840,753,3461,748]],1463=>[[4435],[420]],1466=>[[3467,3466],[3467],[0]],1465=>[[3463,3466]],1467=>[[750,3463]],1468=>[[3474],[0]],1470=>[[3471],[3468,289],[445,289,3469],[3472],[3473]],1471=>[[136],[225],[421],[547],[617]],1472=>[[430,47]],1473=>[[389]],1474=>[[32],[163],[165],[208],[515]],1479=>[[3480],[0]],1476=>[[3477,3479]],1477=>[[571],[574]],1478=>[[3481],[0]],1480=>[[645,435,287],[4416,3478]],1481=>[[3482],[645,435,287]],1482=>[[200,179]],1486=>[[3487],[0]],1485=>[[4414,3497,3492,3486],[3489]],1487=>[[232,270]],1490=>[[3491,3490],[3491],[0]],1489=>[[3494,3490]],1491=>[[750,3494]],1495=>[[3496],[0]],1494=>[[4414,3492,3495]],1496=>[[232,270]],1497=>[[405,753,2174,748]],1498=>[[3503],[3522],[3524],[3533]],1518=>[[3507],[0]],1519=>[[3516],[0]],1520=>[[3517],[0]],1503=>[[97,709,217,4435,599,4183,3504,3518,3519,3520]],1504=>[[618],[710]],1509=>[[3510,3509],[3510],[0]],1507=>[[711,4183,3512,3509]],1510=>[[4157,3512]],1513=>[[3514],[0]],1512=>[[787,3513]],1514=>[[773,787]],1516=>[[708,4183,787]],1517=>[[156],[140]],1532=>[[198],[0]],1522=>[[11,709,217,4430,3518,3519,3520,3532]],1525=>[[3526],[0]],1524=>[[506,709,217,4435,3525]],1526=>[[200,3528]],1530=>[[3531,3530],[3531],[0]],1528=>[[4451,3530]],1531=>[[4157,4451]],1533=>[[148,709,217,4430,3532]],1534=>[[3542],[3537],[3555],[3556],[3535]],1535=>[[3557]],1539=>[[3540],[0]],1537=>[[3538,4414,3539]],1538=>[[178],[135],[134]],1540=>[[4465],[4377]],1549=>[[3550],[0]],1542=>[[3543,3549,3551]],1543=>[[178],[135],[134]],1544=>[[180]],1545=>[[404]],1546=>[[201,763,4484]],1547=>[[14,201,763,4484]],1548=>[[14]],1550=>[[3544],[3545],[3546],[3547],[3548]],1551=>[[2603],[3553],[3554]],1552=>[[2485],[2524],[2598],[2856]],1553=>[[3552]],1554=>[[200,84,4451]],1555=>[[222,4484]],1556=>[[620,4435]],1557=>[[714]],1558=>[[3567,3558],[3567],[0]],1559=>[[3561,3558]],1564=>[[3565],[0]],1561=>[[3571,3564],[3566]],1562=>[[596],[183],[610]],2040=>[[3848],[0]],1565=>[[257,4040,3562]],1566=>[[371,3559]],1567=>[[3568,3559],[654,3559],[3569,3559]],1568=>[[15],[770]],1569=>[[394],[772]],1570=>[[3573,3570],[3573],[0]],1571=>[[3577,3570]],1573=>[[257,4040,376],[3575,3574,2651],[3575,3577]],1574=>[[10],[16]],1575=>[[763],[777],[764],[765],[768],[769],[776]],1581=>[[3582],[0]],1577=>[[3589,3581]],1578=>[[668],[0]],1579=>[[733,3578,3855]],1582=>[[4040,3584],[3579],[521,275,3589]],1586=>[[3587],[0]],1584=>[[251,3585],[30,3589,15,3577],[275,3596,3586],[444,3589]],1585=>[[2651],[753,3844,748]],1587=>[[168,3596]],1588=>[[3590,3588],[3590],[0]],1589=>[[3596,3588]],1590=>[[760,3589],[3591,3589],[3592,3589],[3593,247,3559,3850],[3594,3589],[757,3589],[759,3589]],1591=>[[775],[762],[774],[145],[349]],1592=>[[778],[773]],1593=>[[778],[773]],1594=>[[779],[780]],1597=>[[3598,3597],[3598],[0]],1596=>[[3600,3597]],1598=>[[761,3600]],1601=>[[3602],[0]],1600=>[[3612,3601]],1602=>[[69,4484]],1613=>[[3614],[0]],1604=>[[3626],[0]],1605=>[[487],[0]],1607=>[[3725],[0]],1608=>[[3624],[0]],1881=>[[3559],[0]],1622=>[[3623,3622],[3623]],1611=>[[3828],[0]],1612=>[[4456],[3649],[3815,3613],[3808],[3741],[4377,3604],[754],[3615],[3616],[3617,3596],[3849,3596],[3605,753,3844,748],[3606,2651],[752,4435,3559,747],[320,3719,7,753,3589,3607,748],[32,3596],[3621],[52,753,3559,17,3838,3608,748],[51,3881,3622,3611,159],[94,753,3559,750,3838,748],[94,753,3559,621,4150,748],[128,753,4444,748],[626,753,4444,748],[247,3559,3850,778,3559]],1614=>[[4489,3559]],1615=>[[3692]],1616=>[[3698]],1617=>[[778],[773],[758]],1618=>[[247],[0]],2106=>[[4149],[0]],1620=>[[52,753,3559,21,586,843,3618,4463,17,113,4106,748]],1621=>[[3620]],1623=>[[3826,3827]],1624=>[[3625]],1625=>[[731]],1626=>[[3627],[3628]],1627=>[[766,4463]],1628=>[[767,4463]],1645=>[[143],[0]],1651=>[[3652],[0]],1655=>[[3656],[0]],1659=>[[3660],[0]],1664=>[[3665],[0]],1667=>[[3668],[0]],1670=>[[3671],[0]],1673=>[[3674],[0]],1676=>[[3677],[0]],1679=>[[3680],[0]],1682=>[[3683],[0]],1685=>[[3686],[0]],1687=>[[3688],[0]],1690=>[[3691],[0]],1649=>[[26,753,3645,3718,748,3651],[3653,753,3718,748,3655],[3657],[95,753,3717,775,748,3659],[95,753,3662,748,3664],[345,753,3645,3718,748,3667],[326,753,3645,3718,748,3670],[551,753,3718,748,3673],[632,753,3718,748,3676],[548,753,3718,748,3679],[635,753,3718,748,3682],[564,753,3645,3718,748,3685],[218,753,3645,3844,3646,3687,748,3690]],1650=>[[3705]],1652=>[[3650]],1653=>[[35],[36],[38]],1654=>[[3705]],1656=>[[3654]],1657=>[[3716]],1658=>[[3705]],1660=>[[3658]],1662=>[[3717,775],[3718],[143,3844]],1663=>[[3705]],1665=>[[3663]],1666=>[[3705]],1668=>[[3666]],1669=>[[3705]],1671=>[[3669]],1672=>[[3705]],1674=>[[3672]],1675=>[[3705]],1677=>[[3675]],1678=>[[3705]],1680=>[[3678]],1681=>[[3705]],1683=>[[3681]],1684=>[[3705]],1686=>[[3684]],1688=>[[499,4465]],1689=>[[3705]],1691=>[[3689]],1692=>[[672,753,3844,748]],1693=>[[3708],[0]],1697=>[[3712],[0]],1703=>[[3704],[0]],1698=>[[3699,4488,3705],[688,3855,3705],[3700,753,3559,3693,748,3697,3705],[3701,3854,3697,3705],[687,753,3559,750,3596,748,3703,3697,3705]],1699=>[[696],[694],[679],[678],[692]],1700=>[[686],[684]],1701=>[[681],[685]],1702=>[[191],[268]],1704=>[[203,3702]],1705=>[[691,3706]],1706=>[[4431],[2685]],1710=>[[3711],[0]],1708=>[[750,3709,3710]],1709=>[[4452],[754],[4435],[3816]],1711=>[[750,3559]],1712=>[[3713,689]],1713=>[[695],[232]],1715=>[[3705],[0]],1716=>[[667,753,3718,748,3715],[666,753,3718,750,3718,748,3715]],1718=>[[3717,3559]],1719=>[[3721],[753,3721,748]],1722=>[[3723,3722],[3723],[0]],1721=>[[4444,3722]],1723=>[[750,4444]],1726=>[[3727],[0]],1725=>[[251,41,346],[251,359,267,346,3726],[645,430,176]],1727=>[[645,430,176]],1742=>[[3743],[0]],2359=>[[4488],[0]],1744=>[[3745,3744],[3745]],1751=>[[3752],[0]],2030=>[[3775],[0]],1757=>[[3758],[0]],1761=>[[3762],[0]],1741=>[[60,753,3844,3742,748],[105,4359],[116,3854],[122,3854],[229,3854],[242,753,3559,750,3559,750,3559,750,3559,748],[247,753,3559,3744,748],[3750],[272,753,3559,750,3559,748],[343,3854],[350,3854],[478,753,3559,750,3559,748],[495,3854],[586,3854],[583,753,3559,3751,748],[3790],[618,4488],[626,3854],[656,3854],[3753,753,3559,750,3754,748],[100,4359],[108,4030],[3755,753,3559,750,247,3559,3850,748],[182,753,3850,203,3559,748],[213,753,3789,750,3559,748],[372,4030],[414,753,3589,251,3559,748],[3798],[569,4030],[3756,753,3852,750,3559,750,3559,748],[622,4359],[624,4030],[623,4030],[19,3854],[58,3854],[67,3853],[70,3854],[109,4488],[231,753,3559,750,3559,750,3559,748],[201,753,3559,750,3559,3757,748],[337,3854],[349,753,3559,750,3559,748],[3759],[3760],[429,3854],[457,753,3559,750,3559,748],[458,753,3559,750,3559,750,3559,748],[476,3854],[485,4488],[597,753,3559,750,3559,748],[640,753,3559,3761,748],[641,753,3559,17,60,748],[641,753,3559,3770,748],[3772]],1743=>[[621,4150]],1745=>[[750,3559]],1748=>[[3747],[0]],1747=>[[851,3838]],1750=>[[850,753,3596,750,4469,3748,3749,748]],1752=>[[750,3559]],1753=>[[5],[558]],1754=>[[3559],[247,3559,3850]],1755=>[[114],[115]],1756=>[[584],[585]],1758=>[[750,3559]],1759=>[[382,753,4469,748]],1760=>[[406,3854]],1762=>[[750,3559]],1768=>[[3764],[0]],1764=>[[17,60,4148]],1765=>[[3778]],1769=>[[3767],[0]],1767=>[[3765]],1770=>[[17,32,4148],[3768,3769],[750,4450,750,4450,750,4450]],1772=>[[3773],[211,753,3807,748],[279,3853],[351,3853],[352,3853],[353,3853],[411,753,3559,750,3559,748],[412,3853]],1773=>[[90,753,3559,750,3559,748]],1774=>[[3776],[0]],1775=>[[753,3774,748]],1776=>[[3777]],1777=>[[787]],1778=>[[274,3782]],1781=>[[3780,3781],[3780],[0]],1780=>[[750,3784]],1782=>[[4451,773,4451],[3784,3781]],1787=>[[3788],[0]],1784=>[[4451,3787]],1785=>[[18],[134]],1786=>[[476],[0]],1788=>[[3785,3786],[476]],1789=>[[116],[586],[113],[583]],1790=>[[595,753,3797,748]],1793=>[[3792],[0]],1792=>[[203,3559]],1797=>[[3559,3793],[269,3881,203,3559],[591,3881,203,3559],[43,3881,203,3559]],1798=>[[563,753,3559,3805,748]],1803=>[[3800],[0]],1800=>[[750,3559]],1804=>[[3802],[0]],1802=>[[200,3559]],1805=>[[750,3559,3803],[203,3559,3804]],1806=>[[3810],[0]],1808=>[[4432,753,3806,748],[4442,753,3807,748]],1811=>[[3812,3811],[3812],[0]],1810=>[[3814,3811]],1812=>[[750,3814]],1814=>[[3559,3813]],1815=>[[3816],[3819]],1816=>[[746,4484],[792]],1817=>[[4491],[0]],2445=>[[4449],[0]],1819=>[[745,3817,4484,4445]],1820=>[[3825],[128,4449]],1822=>[[4435,4445]],1824=>[[4485,4445]],1825=>[[3822],[3824]],1826=>[[642,3559]],1827=>[[582,3559]],1828=>[[154,3559]],2111=>[[4133],[0]],2116=>[[4141],[0]],1834=>[[249],[0]],2092=>[[4481],[0]],1838=>[[32,4111],[60,4111,4116],[4130,4111],[512,3834],[612,3834],[116],[586,4106],[113,4106],[126,4092],[656],[3839],[3840],[3842]],1839=>[[262]],1840=>[[4132]],1841=>[[4482],[0]],1842=>[[195,3841]],1845=>[[3846,3845],[3846],[0]],1844=>[[3559,3845]],1846=>[[750,3559]],1847=>[[60,506],[58]],1848=>[[371],[800]],1849=>[[771],[800]],1850=>[[3852],[3851]],1851=>[[494],[341],[342],[226],[228],[227],[119],[121],[120],[118],[655]],1852=>[[337],[495],[343],[229],[122],[640],[350],[429],[656]],1853=>[[753,3844,748]],1854=>[[753,3559,748]],1855=>[[753,3596,748]],1858=>[[3859,3858],[3859],[0]],1857=>[[3861,3858]],1859=>[[750,3861]],1861=>[[3559,4071]],1864=>[[3865,3864],[3865],[0]],1863=>[[3866,3864]],1865=>[[750,3866]],1866=>[[3559]],1867=>[[3868]],1868=>[[200,57,4472]],1869=>[[2004],[3870],[3871],[3884],[3889],[3890],[3896],[3897],[3923],[3922],[3963],[3966],[3964]],1870=>[[475,3559]],1871=>[[231,3873,159,231]],1874=>[[3875],[0]],1873=>[[3559,3876,3874]],1875=>[[155,3873],[154,3878]],1876=>[[582,3878]],1879=>[[3880,3879],[3880]],1878=>[[3879]],1880=>[[3869,755]],1885=>[[3886,3885],[3886]],1883=>[[3887],[0]],1884=>[[51,3881,3885,3883,159,51]],1886=>[[3826,3876]],1887=>[[154,3878]],1895=>[[4425],[0]],1889=>[[3891,3894,3895]],1890=>[[3894]],1891=>[[4424,749]],1892=>[[3902],[0]],1893=>[[3878],[0]],1894=>[[29,3892,3893,159]],1896=>[[3891,3897,3895]],1897=>[[3898],[3899],[3900]],1898=>[[294,3878,159,294]],1899=>[[644,3559,147,3878,159,644]],1900=>[[457,3878,613,3559,159,457]],1903=>[[3904,3903],[3904]],1902=>[[3903]],1904=>[[3905,755]],1905=>[[3908],[3911],[3916],[3921]],1909=>[[3910],[0]],1908=>[[127,4437,4117,4282,3909]],1910=>[[128,3559]],1911=>[[127,4435,83,200,3912]],1912=>[[4450],[3914]],1913=>[[627],[0]],1914=>[[526,3913,4469]],1918=>[[3919,3918],[3919],[0]],1916=>[[127,3917,219,200,3920,3918,3869]],1917=>[[92],[175],[605]],1919=>[[750,3920]],1920=>[[3912],[4435],[527],[3848,202],[525]],1921=>[[127,4435,106,200,2603]],1922=>[[260,4425]],1923=>[[271,4425]],1927=>[[3928],[0]],1925=>[[207,3927,138,3935]],1926=>[[540]],1928=>[[101],[3926]],1933=>[[3930,3933],[3930],[0]],1930=>[[750,3937]],1934=>[[3932,3934],[3932],[0]],1932=>[[750,3940]],1935=>[[3937,3933],[83,3936,3940,3934]],1936=>[[4456],[3815],[4442]],1937=>[[3938,763,3939]],1938=>[[3815],[4435]],1939=>[[377],[485]],1940=>[[3941,763,3942]],1941=>[[3815],[4435]],1942=>[[3943],[473]],1943=>[[64],[557],[87],[89],[88],[53],[492],[576],[73],[107],[336],[355]],1950=>[[3951],[0]],1945=>[[511,3946,3950]],1946=>[[4435],[3914]],1949=>[[3948,3949],[3948],[0]],1948=>[[750,3962]],1951=>[[506,3962,3949]],1955=>[[3956],[0]],1960=>[[3961],[0]],1954=>[[469,3955,3960]],1956=>[[4435],[3914]],1959=>[[3958,3959],[3958],[0]],1958=>[[750,3962]],1961=>[[506,3962,3959]],1962=>[[3943,763,3936]],1963=>[[387,4435]],1964=>[[66,4435]],1968=>[[3969],[0]],1966=>[[186,3968,4435,248,4437]],1967=>[[367],[0]],1969=>[[3967,203]],1973=>[[3974],[0]],1975=>[[3976],[0]],1972=>[[21,3559],[171,3559,3850,3973,3975]],1974=>[[542,3559]],1976=>[[160,3559]],1978=>[[4369,4007,3977]],1979=>[[3980],[4046]],1980=>[[3981]],1981=>[[62,3854]],1983=>[[4023,730]],2034=>[[4006],[0]],1993=>[[3994,4000,4073,4001],[205,3995,4002,4073,3988],[523,3995,4002,4073,3991],[4034,4004]],1994=>[[265],[236]],1996=>[[420,265],[609,3995]],1997=>[[3983]],2003=>[[3999],[0]],1999=>[[3997]],2004=>[[3996,4000,4073,4001],[199,265,4002,4061,4046],[3981,4003]],2005=>[[4435],[0]],2006=>[[86,4005]],2007=>[[4117,4022]],2018=>[[4009],[0]],2009=>[[209,12]],2019=>[[4011],[0]],2011=>[[637],[554]],2021=>[[4027,4021],[4027],[0]],2013=>[[4021]],2014=>[[4042,4014],[4042],[0]],2015=>[[4014]],2016=>[[4013],[4015]],2020=>[[4282,4018,17,3854,4019,4016]],2022=>[[4020],[4021]],2041=>[[420],[0]],2039=>[[265],[0]],2027=>[[4023,4479],[4028],[128,4031],[4032],[383,614,372,4030],[24],[501,128,627],[4041,265],[609,4039],[75,4469],[4281],[74,4037],[553,4038],[4033],[4035],[4036]],2028=>[[371,720]],2029=>[[3854]],2031=>[[4458],[372,4030],[4029]],2032=>[[4082]],2033=>[[707,4453]],2035=>[[4034,3981]],2036=>[[3983]],2037=>[[192],[152],[128]],2038=>[[142],[334],[128]],2042=>[[609,4039],[75,4465],[4040,376],[4041,265]],2043=>[[4440],[0]],2048=>[[4049],[0]],2056=>[[4057],[0]],2046=>[[443,4414,4043,4048,4056]],2047=>[[204],[402],[513]],2049=>[[320,4047]],2054=>[[4051],[0]],2051=>[[383,133,4058]],2055=>[[4053],[0]],2053=>[[383,614,4058]],2057=>[[383,614,4058,4054],[383,133,4058,4055]],2058=>[[4059],[506,4479],[373,3],[506,128]],2059=>[[471],[49]],2062=>[[4063,4062],[4063],[0]],2061=>[[753,4066,4062,748]],2063=>[[750,4066]],2066=>[[4435,4111,4071]],2069=>[[4070,4069],[4070],[0]],2068=>[[753,4072,4069,748]],2070=>[[750,4072]],2072=>[[4066],[3854,4071]],2073=>[[4074],[4075]],2074=>[[4068]],2075=>[[4061]],2076=>[[4077]],2077=>[[44],[488],[220]],2078=>[[4080],[4083]],2080=>[[264,4262,4450],[75,4469],[4081]],2081=>[[4082]],2082=>[[662],[661]],2083=>[[4084,4076]],2084=>[[621],[599]],2085=>[[4080],[645,401,4435]],2086=>[[4080]],2087=>[[4117,-1]],2103=>[[4136],[0]],2090=>[[4483],[0]],2146=>[[32],[0]],2126=>[[4127],[0]],2117=>[[4118,4111,4103],[4120,4090,4103],[4121,4092,4103],[37,4111],[4122],[4123,4133,4116],[60,4111,4116],[32,4111],[4124,4133,4146],[4130,4111,4146],[628,4133],[656,4111,4103],[116],[586,4106],[583,4106],[113,4106],[587],[39,4111],[4125],[293,628],[293,4126,4116],[589,4116],[580,4111,4116],[332,4116],[291,4116],[164,4460,4116],[506,4460,4116],[501],[4128],[4129]],2118=>[[249],[588],[516],[331],[31]],2131=>[[416],[0]],2120=>[[437],[146,4131]],2121=>[[195],[126],[378],[192]],2122=>[[42],[41]],2123=>[[60,633],[629]],2124=>[[358,629],[379],[361,629],[358,60,633],[361,633]],2125=>[[330],[290]],2127=>[[60,633],[629]],2128=>[[262]],2129=>[[212],[211],[411],[352],[279],[351],[412],[353]],2130=>[[361],[358,60]],2132=>[[437],[146,4131]],2133=>[[753,4134,748]],2134=>[[4453],[783]],2137=>[[4138,4137],[4138]],2136=>[[4137]],2138=>[[512],[612],[657]],2142=>[[4143],[0]],2141=>[[4145],[4147],[46],[3847,4150,4146],[32,4142]],2143=>[[3847,4150]],2145=>[[19,4146],[32,19]],2147=>[[606,4146],[32,606]],2148=>[[753,4451,748]],2149=>[[753,787,748]],2150=>[[4484],[32],[4151]],2151=>[[128]],2152=>[[4484],[4153],[4154]],2153=>[[128]],2154=>[[32]],2158=>[[4159,4158],[4159],[0]],2156=>[[4181,4158]],2159=>[[4157,4181]],2160=>[[4181,4160],[4181]],2161=>[[4160]],2175=>[[4416],[0]],2181=>[[163,4262,4404],[4184],[323,4262,4452],[344,4262,4452],[25,4262,4450],[406,4262,4463],[75,4262,4463],[4186],[4188],[24,4262,4452],[399,4262,4203],[4189,4262,4203],[4190,4262,4450],[132,4262,4450],[486,4262,4191],[608,4262,753,4175,748],[4212],[4206],[243,4262,4192],[112,139,4262,4465],[236,139,4262,4465],[572,4195,4435],[553,4196],[84,4262,4465],[264,4262,4450],[4197],[4199],[4201],[4202]],2182=>[[376],[4484]],2184=>[[721,4183,4182]],2186=>[[81,4262,4465]],2188=>[[158,4262,4465]],2189=>[[544],[545],[546]],2190=>[[61],[575]],2191=>[[128],[152],[192],[80],[442],[78]],2192=>[[373],[191],[268]],2194=>[[4262]],2195=>[[4194],[0]],2196=>[[142],[334]],2197=>[[543,592]],2199=>[[848,4262,4465]],2201=>[[849,4262,4465]],2202=>[[2350]],2203=>[[4450],[128]],2210=>[[128],[0]],2206=>[[4210,69,4262,4152]],2209=>[[4210,158,4262,4463]],2212=>[[4210,3847,4262,4150]],2217=>[[4218],[0]],2214=>[[4229],[0]],2215=>[[4237],[0]],2216=>[[405,45,4223,4217,4214,4215]],2218=>[[404,4451]],2227=>[[277],[0]],2230=>[[4234],[0]],2225=>[[4437],[0]],2223=>[[4227,265,4230,753,4225,748],[4227,220,753,3589,748],[4224,4226]],2224=>[[432],[280]],2226=>[[753,3589,748],[71,753,4225,748]],2232=>[[4233],[0]],2229=>[[561,45,4227,4231,4232]],2231=>[[220,753,3589,748],[265,4230,4440]],2233=>[[560,4451]],2234=>[[4235]],2235=>[[9,763,4451]],2238=>[[4239,4238],[4239],[0]],2237=>[[753,4243,4238,748]],2239=>[[750,4243]],2245=>[[4246],[0]],2266=>[[4263,4266],[4263],[0]],2250=>[[4251],[0]],2243=>[[405,4435,4245,4266,4250]],2244=>[[4269],[329]],2246=>[[626,273,581,4244],[626,251,4253]],2249=>[[4248,4249],[4248],[0]],2248=>[[750,4267]],2251=>[[753,4267,4249,748]],2254=>[[4255,4254],[4255],[0]],2253=>[[4269],[753,4269,4254,748]],2255=>[[750,4269]],2263=>[[572,4262,4435],[4257,163,4262,4404],[368,4262,4451],[4264,4262,4451],[4265,139,4262,4469],[75,4262,4469]],2264=>[[323],[344]],2265=>[[112],[236]],2267=>[[561,4484,4266]],2270=>[[4271,4270],[4271],[0]],2269=>[[753,4272,4270,748]],2271=>[[750,4272]],2272=>[[3589],[329]],2273=>[[130,763,4360]],2274=>[[231,174]],2275=>[[231,3848,174]],2278=>[[4279],[0]],2277=>[[4278,4280]],2279=>[[251],[397],[240]],2280=>[[4423,4283]],2281=>[[69,4152]],2283=>[[4117,4282]],2284=>[[753,4386,750,4386,748]],2287=>[[4288,4287],[4288],[0]],2286=>[[4394,4287]],2288=>[[750,4394]],2291=>[[4292,4291],[4292],[0]],2290=>[[4293,4291]],2292=>[[750,4293]],2293=>[[4377,4489,4294]],2294=>[[3559],[128]],2295=>[[3847,4150]],2296=>[[4299,4296],[4299]],2297=>[[71,4296]],2298=>[[392],[0]],2299=>[[579,45,4465],[4298,157,45,4465],[167,45,4465]],2300=>[[4302,4300],[4302]],2301=>[[278,4300]],2302=>[[4303,45,4465]],2303=>[[579],[541]],2306=>[[4307,4306],[4307],[0]],2305=>[[4360,4306]],2307=>[[750,4360]],2310=>[[4311,4310],[4311],[0]],2309=>[[4317,4310]],2311=>[[750,4317]],2314=>[[4315,4314],[4315],[0]],2313=>[[4333,4314]],2315=>[[750,4333]],2331=>[[4332],[0]],2317=>[[4360,4331]],2318=>[[406]],2328=>[[4320],[0]],2320=>[[4318]],2321=>[[45,4465]],2329=>[[4323],[0]],2323=>[[17,4466],[4321]],2326=>[[4325],[0]],2325=>[[645,4484]],2327=>[[4326,45,734,406]],2330=>[[45,4328,4465],[645,4484,4329],[4327]],2332=>[[230,4330]],2333=>[[4334,4350]],2334=>[[3118],[4360]],2342=>[[4336],[0]],2336=>[[645,4484]],2337=>[[734,406]],2338=>[[4337],[4465]],2345=>[[4341],[0]],2341=>[[17,4466,4344]],2349=>[[4347],[0]],2347=>[[4342,45,4338,4343,4344],[645,4484,4345]],2348=>[[4352]],2350=>[[230,4349],[4348],[0]],2351=>[[727,101,406]],2352=>[[141,728,406]],2353=>[[458,4465]],2357=>[[4358],[0]],2355=>[[4484,4357]],2356=>[[4484],[0]],2358=>[[746,4356],[792]],2360=>[[4355],[105,4359]],2361=>[[275,4463]],2362=>[[4361],[2765]],2363=>[[385],[380]],2364=>[[284],[375]],2365=>[[4366]],2366=>[[405,4440]],2368=>[[4449],[4442,4445]],2369=>[[4370],[4371]],2370=>[[4435]],2371=>[[4368]],2372=>[[4435]],2375=>[[4376,4375],[4376],[0]],2374=>[[753,4372,4375,748]],2376=>[[750,4372]],2377=>[[4368]],2378=>[[4377],[4382]],2379=>[[4435]],2380=>[[4368]],2383=>[[4384],[0]],2382=>[[4435,751,4383,775]],2384=>[[4435,751]],2385=>[[4435]],2386=>[[4435]],2387=>[[4442]],2388=>[[4442]],2389=>[[4442]],2390=>[[4442]],2391=>[[4442]],2392=>[[4442]],2393=>[[4442],[4449]],2394=>[[4442],[4449]],2395=>[[4435]],2396=>[[4435]],2397=>[[4435]],2398=>[[4435]],2399=>[[4442]],2400=>[[4442]],2401=>[[4435]],2402=>[[4484]],2403=>[[4484]],2404=>[[4484]],2405=>[[4442],[4449]],2406=>[[4386,4449]],2412=>[[4413],[0]],2408=>[[4435,4412]],2411=>[[4410],[0]],2410=>[[751,775]],2413=>[[751,775],[4449,4411]],2414=>[[4442],[4449]],2417=>[[4418,4417],[4418],[0]],2416=>[[4414,4417]],2418=>[[750,4414]],2421=>[[4422,4421],[4422],[0]],2420=>[[4408,4421]],2422=>[[750,4408]],2423=>[[4435]],2424=>[[4432],[4501]],2425=>[[4424]],2426=>[[4432],[4510]],2427=>[[4426]],2428=>[[4435]],2429=>[[4463]],2430=>[[4435]],2431=>[[4435]],2432=>[[4433],[4434]],2433=>[[793],[781]],2434=>[[784]],2435=>[[4432],[4493]],2438=>[[4439,4438],[4439],[0]],2437=>[[4435,4438]],2439=>[[750,4435]],2440=>[[753,4437,748]],2442=>[[4435,4445]],2446=>[[4447],[0]],2444=>[[4435,4446],[4448]],2447=>[[4449,4445]],2448=>[[4449,4449]],2449=>[[751,4435]],2450=>[[787],[786],[788],[791],[783],[785]],2451=>[[787],[786],[788],[791]],2452=>[[787],[788],[791],[783],[785]],2453=>[[787],[4454],[791],[788]],2454=>[[786]],2470=>[[794],[0]],2456=>[[4469],[4477],[4480],[4479],[4478],[4470,4457]],2457=>[[786],[782]],2458=>[[4456],[778,4450],[773,4450]],2461=>[[4462,4461],[4462],[0]],2460=>[[753,4465,4461,748]],2462=>[[750,4465]],2463=>[[790],[4464]],2464=>[[784]],2465=>[[4463],[786],[782]],2466=>[[4463],[4467]],2467=>[[786]],2468=>[[4463,4468],[4463],[0]],2469=>[[4471,4468]],2471=>[[4470,4463],[789]],2472=>[[4463]],2475=>[[4476,4475],[4476],[0]],2474=>[[4463,4475]],2476=>[[750,4463]],2477=>[[787],[788],[791],[783],[785]],2478=>[[596],[183]],2479=>[[376],[801]],2480=>[[116,4463],[586,4463],[583,4463]],2481=>[[4133],[4483]],2482=>[[4133]],2483=>[[753,787,750,787,748]],2484=>[[4435],[4463]],2485=>[[4432],[4514]],2486=>[[4426],[4463]],2487=>[[4453],[4432]],2488=>[[753,748]],2489=>[[763],[756]],2490=>[[658],[673],[214],[284],[502]],2491=>[[214,751],[284,751],[502,751]],2492=>[[658,751],[673,751],[214,751],[284,751],[502,751]],2493=>[[4497],[4498]],2494=>[[510]],2495=>[[714]],2496=>[[4501],[4516],[173],[4494],[4495]],2497=>[[4496]],2498=>[[4506],[4499],[4500],[4505],[4515]],2499=>[[173],[714],[510]],2500=>[[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]],2501=>[[4503],[4504]],2502=>[[4520],[170],[188],[369],[423],[427],[451],[459],[709],[565]],2503=>[[4502]],2504=>[[4506],[4505],[4515]],2505=>[[170],[188],[369],[423],[427],[451],[459],[709],[565]],2506=>[[4507],[4509]],2507=>[[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]],2508=>[[731],[741],[735],[738],[736],[733],[744],[740],[737],[734],[739],[742],[743],[583],[586]],2509=>[[4508]],2510=>[[4512],[4513]],2511=>[[4520],[4516]],2512=>[[4511]],2513=>[[4506],[4500],[4515]],2514=>[[4506],[4499],[4500],[4505]],2515=>[[214],[284],[658],[673],[502]],2516=>[[4517],[4518],[4519]],2517=>[[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]],2518=>[[510]],2519=>[[234]],2520=>[[4521],[4522],[4524],[4526],[4527]],2521=>[[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]],2522=>[[510]],2523=>[[99],[234],[206],[484],[487]],2524=>[[4523]],2525=>[[172],[177],[386],[565],[625],[646]],2526=>[[4525]],2527=>[[660]]]]; +return ['rules_offset'=>2000,'rules_names'=>['query','%f1','%f2','%f3','simpleStatement','%f4','%f5','%f6','%f7','alterStatement','%f8','%f9','%f10','%f11','alterInstance','%f12','%f13','%f14','%f15','%f16','%f17','%f18','%f19','%f20','%f21','%f22','%f23','%f24','%f25','%f26','%f27','alterDatabase','%f28','%f29','%f30','%f31','%f32','%f33','%f34','%f35','%f36','%f37','alterEvent','%f38','%f39','%f40','%f41','%f42','%f43','%f44','%f45','%f46','%f47','%f48','%f49','%f50','%f51','%f52','%f53','%f54','alterLogfileGroup','%f55','alterLogfileGroupOptions','%f56','%f57','%f58','alterLogfileGroupOption','alterServer','%f59','%f60','%f61','alterTable','%f62','%f63','%f64','%f65','%f66','alterTableActions','%f67','%f68','%f69','%f70','%f71','alterCommandList','%f72','%f73','%f74','alterCommandsModifierList','%f75','%f76','standaloneAlterCommands','%f77','%f78','%f79','%f80','%f81','%f82','%f83','%f84','%f85','%f86','%f87','%f88','%f89','%f90','alterPartition','%f91','%f92','%f93','%f94','%f95','%f96','alterList','%f97','%f98','%f99','%f100','alterCommandsModifier','%f101','%f102','%f103','%f104','%f105','%f106','%f107','%f108','alterListItem','%f109','%f110','%f111','%f112','%f113','%f114','%f115','%f116','%f117','%f118','%f119','%f120','%f121','%f122','%f123','%f124','%f125','%f126','%f127','%f128','%f129','%f130','%f131','%f132','%f133','%f134','place','restrict','%f135','%f136','alterOrderList','%f137','%f138','%f139','%f140','alterAlgorithmOption','%f141','%f142','alterLockOption','%f143','%f144','%f145','indexLockAndAlgorithm','withValidation','%f146','%f147','removePartitioning','allOrPartitionNameList','alterTablespace','%f148','%f149','%f150','%f151','%f152','%f153','%f154','%f155','%f156','%f157','%f158','%f159','%f160','%f161','%f162','alterUndoTablespace','%f163','%f164','undoTableSpaceOptions','%f165','%f166','%f167','undoTableSpaceOption','%f168','alterTablespaceOptions','%f169','%f170','%f171','%f172','alterTablespaceOption','%f173','%f174','changeTablespaceOption','%f175','%f176','%f177','alterView','%f178','viewTail','%f179','viewSelect','%f180','viewCheckOption','%f181','%f182','createStatement','%f183','%f184','%f185','%f186','%f187','%f188','createDatabase','createDatabaseOption','%f189','%f190','%f191','createTable','%f192','%f193','%f194','%f195','%f196','%f197','%f198','%f199','tableElementList','%f200','%f201','tableElement','%f202','%f203','duplicateAsQueryExpression','%f204','%f205','queryExpressionOrParens','%f206','createRoutine','%f207','%f208','%f209','%f210','createProcedure','%f211','%f212','%f213','%f214','%f215','%f216','%f217','%f218','%f219','%f220','%f221','createFunction','%f222','%f223','%f224','%f225','%f226','%f227','%f228','%f229','%f230','createUdf','%f231','%f232','routineCreateOption','%f233','routineAlterOptions','routineOption','%f234','%f235','%f236','createIndex','%f237','%f238','%f239','%f240','%f241','%f242','%f243','%f244','%f245','%f246','%f247','indexNameAndType','%f248','%f249','%f250','createIndexTarget','%f251','createLogfileGroup','%f252','%f253','logfileGroupOptions','%f254','%f255','%f256','logfileGroupOption','createServer','%f257','serverOptions','%f258','%f259','serverOption','%f260','%f261','createTablespace','%f262','%f263','%f264','createUndoTablespace','tsDataFileName','%f265','%f266','%f267','%f268','tsDataFile','%f269','tablespaceOptions','%f270','%f271','%f272','tablespaceOption','%f273','%f274','%f275','%f276','tsOptionInitialSize','%f277','tsOptionUndoRedoBufferSize','%f278','%f279','tsOptionAutoextendSize','%f280','tsOptionMaxSize','%f281','tsOptionExtentSize','%f282','tsOptionNodegroup','%f283','%f284','tsOptionEngine','%f285','tsOptionEngineAttribute','tsOptionWait','%f286','%f287','tsOptionComment','%f288','tsOptionFileblockSize','%f289','tsOptionEncryption','%f290','%f291','%f292','createView','%f293','viewReplaceOrAlgorithm','viewAlgorithm','%f294','viewSuid','%f295','%f296','%f297','createTrigger','%f298','%f299','%f300','%f301','%f302','triggerFollowsPrecedesClause','%f303','%f304','%f305','%f306','%f307','%f308','%f309','createEvent','%f310','%f311','%f312','%f313','%f314','%f315','%f316','%f317','%f318','%f319','%f320','createRole','%f321','%f322','%f323','createSpatialReference','srsAttribute','dropStatement','%f324','%f325','%f326','%f327','%f328','dropDatabase','%f329','dropEvent','%f330','dropFunction','%f331','dropProcedure','%f332','%f333','dropIndex','%f334','dropLogfileGroup','%f335','%f336','%f337','%f338','%f339','%f340','dropLogfileGroupOption','%f341','dropServer','%f342','%f343','%f344','dropTable','%f345','%f346','%f347','%f348','dropTableSpace','%f349','%f350','%f351','%f352','%f353','%f354','%f355','dropTrigger','%f356','%f357','dropView','%f358','%f359','%f360','dropRole','%f361','dropSpatialReference','%f362','dropUndoTablespace','%f363','renameTableStatement','%f364','%f365','%f366','renamePair','%f367','truncateTableStatement','importStatement','%f368','callStatement','%f369','%f370','%f371','%f372','%f373','deleteStatement','%f374','%f375','%f376','%f377','%f378','%f379','%f380','%f381','%f382','%f383','%f384','%f385','%f386','%f387','%f388','partitionDelete','%f389','deleteStatementOption','doStatement','%f390','%f391','%f392','handlerStatement','%f393','%f394','%f395','%f396','%f397','handlerReadOrScan','%f398','%f399','%f400','%f401','%f402','%f403','%f404','%f405','%f406','insertStatement','%f407','%f408','%f409','%f410','%f411','%f412','%f413','%f414','%f415','insertLockOption','%f416','insertFromConstructor','%f417','%f418','%f419','%f420','fields','%f421','%f422','insertValues','%f423','%f424','insertQueryExpression','%f425','%f426','valueList','%f427','%f428','%f429','%f430','values','%f431','%f432','%f433','%f434','%f435','valuesReference','insertUpdateList','%f436','%f437','%f438','%f439','%f440','%f441','%f442','%f443','loadStatement','%f444','%f445','%f446','%f447','dataOrXml','xmlRowsIdentifiedBy','%f448','%f449','%f450','loadDataFileTail','%f451','%f452','%f453','%f454','%f455','%f456','loadDataFileTargetList','%f457','fieldOrVariableList','%f458','%f459','%f460','%f461','%f462','%f463','%f464','replaceStatement','%f465','%f466','%f467','%f468','selectStatement','%f469','selectStatementWithInto','%f470','%f471','queryExpression','%f472','%f473','%f474','%f475','%f476','%f477','%f478','%f479','%f480','%f481','%f482','%f483','queryExpressionBody','%f484','%f485','%f486','%f487','%f488','%f489','queryTerm','%f490','%f491','%f492','%f493','%f494','%f495','%f496','queryExpressionParens','queryPrimary','%f497','%f498','%f499','%f500','%f501','%f502','%f503','%f504','%f505','querySpecification','%f506','%f507','%f508','subquery','querySpecOption','limitClause','simpleLimitClause','%f509','limitOptions','%f510','%f511','%f512','limitOption','%f513','intoClause','%f514','%f515','%f516','%f517','%f518','%f519','%f520','%f521','%f522','%f523','procedureAnalyseClause','%f524','%f525','%f526','%f527','%f528','havingClause','%f529','windowClause','%f530','%f531','windowDefinition','windowSpec','%f532','%f533','%f534','%f535','%f536','%f537','%f538','%f539','%f540','%f541','windowSpecDetails','%f542','%f543','%f544','%f545','%f546','%f547','%f548','windowFrameClause','windowFrameUnits','windowFrameExtent','windowFrameStart','windowFrameBetween','windowFrameBound','windowFrameExclusion','%f549','%f550','%f551','withClause','%f552','%f553','%f554','commonTableExpression','%f555','groupByClause','olapOption','%f556','orderClause','direction','fromClause','%f557','%f558','tableReferenceList','%f559','%f560','%f561','tableValueConstructor','%f562','%f563','explicitTable','%f564','rowValueExplicit','selectOption','%f565','%f566','%f567','lockingClauseList','%f568','%f569','lockingClause','%f570','%f571','%f572','%f573','%f574','%f575','lockStrengh','%f576','lockedRowAction','%f577','selectItemList','%f578','%f579','%f580','%f581','selectItem','%f582','selectAlias','%f583','whereClause','%f584','tableReference','%f585','%f586','%f587','%f588','escapedTableReference','%f589','joinedTable','%f590','%f591','%f592','%f593','%f594','naturalJoinType','%f595','%f596','innerJoinType','%f597','%f598','%f599','outerJoinType','%f600','tableFactor','%f601','%f602','%f603','%f604','singleTable','singleTableParens','%f605','%f606','%f607','derivedTable','%f608','%f609','%f610','%f611','%f612','%f613','tableReferenceListParens','%f614','%f615','tableFunction','%f616','columnsClause','%f617','%f618','%f619','%f620','%f621','jtColumn','%f622','%f623','%f624','%f625','%f626','onEmptyOrError','onEmpty','onError','jtOnResponse','setOperationOption','%f627','tableAlias','%f628','%f629','%f630','%f631','indexHintList','%f632','%f633','%f634','indexHint','indexHintType','keyOrIndex','%f635','constraintKeyType','indexHintClause','%f636','%f637','indexList','%f638','%f639','indexListElement','%f640','%f641','%f642','%f643','%f644','%f645','updateStatement','%f646','%f647','%f648','transactionOrLockingStatement','%f649','%f650','%f651','%f652','transactionStatement','%f653','%f654','%f655','%f656','%f657','%f658','%f659','beginWork','%f660','transactionCharacteristicList','%f661','%f662','transactionCharacteristic','%f663','%f664','%f665','savepointStatement','%f666','%f667','%f668','%f669','%f670','%f671','%f672','%f673','%f674','%f675','%f676','lockStatement','%f677','%f678','%f679','%f680','%f681','%f682','%f683','lockItem','%f684','%f685','lockOption','xaStatement','%f686','%f687','%f688','%f689','%f690','%f691','%f692','%f693','%f694','%f695','%f696','%f697','%f698','%f699','xaConvert','%f700','%f701','%f702','%f703','%f704','xid','%f705','%f706','%f707','%f708','%f709','%f710','replicationStatement','%f711','%f712','%f713','%f714','%f715','%f716','%f717','%f718','%f719','%f720','%f721','%f722','%f723','%f724','resetOption','%f725','masterResetOptions','%f726','%f727','%f728','%f729','replicationLoad','%f730','%f731','changeMaster','%f732','changeMasterOptions','%f733','%f734','masterOption','privilegeCheckDef','tablePrimaryKeyCheckDef','masterTlsCiphersuitesDef','masterFileDef','%f735','serverIdList','%f736','%f737','%f738','%f739','%f740','%f741','%f742','%f743','changeReplication','%f744','%f745','%f746','%f747','%f748','%f749','changeReplicationSourceOptions','%f750','%f751','replicationSourceOption','%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','%f784','%f785','filterDefinition','%f786','filterDbList','%f787','%f788','%f789','filterTableList','%f790','%f791','%f792','filterStringList','%f793','%f794','filterWildDbTableString','%f795','filterDbPairList','%f796','%f797','%f798','%f799','%f800','%f801','%f802','slave','%f803','%f804','%f805','slaveUntilOptions','%f806','%f807','%f808','%f809','%f810','%f811','slaveConnectionOptions','%f812','%f813','%f814','%f815','%f816','%f817','%f818','%f819','%f820','%f821','%f822','%f823','%f824','%f825','slaveThreadOptions','%f826','%f827','slaveThreadOption','groupReplication','%f828','preparedStatement','%f829','%f830','%f831','executeStatement','%f832','%f833','%f834','executeVarList','%f835','%f836','cloneStatement','%f837','%f838','%f839','%f840','%f841','%f842','%f843','%f844','%f845','dataDirSSL','%f846','ssl','accountManagementStatement','%f847','%f848','%f849','alterUser','%f850','%f851','%f852','alterUserTail','%f853','%f854','%f855','%f856','%f857','%f858','userFunction','createUser','%f859','%f860','createUserTail','%f861','%f862','%f863','%f864','%f865','%f866','%f867','%f868','%f869','defaultRoleClause','%f870','%f871','%f872','%f873','requireClause','%f874','%f875','%f876','connectOptions','%f877','%f878','accountLockPasswordExpireOptions','%f879','%f880','%f881','%f882','%f883','%f884','%f885','%f886','%f887','%f888','%f889','%f890','%f891','dropUser','%f892','%f893','%f894','grant','%f895','%f896','%f897','%f898','%f899','%f900','%f901','%f902','%f903','%f904','%f905','%f906','%f907','%f908','grantTargetList','%f909','%f910','grantOptions','%f911','%f912','%f913','exceptRoleList','withRoles','%f914','%f915','%f916','grantAs','versionedRequireClause','%f917','%f918','renameUser','%f919','%f920','%f921','%f922','revoke','%f923','%f924','%f925','%f926','%f927','%f928','%f929','%f930','%f931','%f932','%f933','%f934','%f935','%f936','%f937','%f938','onTypeTo','%f939','%f940','%f941','%f942','%f943','%f944','%f945','aclType','%f946','roleOrPrivilegesList','%f947','%f948','%f949','%f950','%f951','roleOrPrivilege','%f952','%f953','%f954','%f955','%f956','%f957','%f958','%f959','%f960','%f961','%f962','%f963','%f964','grantIdentifier','%f965','%f966','%f967','%f968','%f969','requireList','%f970','%f971','%f972','requireListElement','grantOption','%f973','setRole','%f974','%f975','%f976','%f977','%f978','roleList','%f979','%f980','%f981','role','%f982','%f983','%f984','%f985','%f986','%f987','%f988','%f989','%f990','tableAdministrationStatement','%f991','%f992','%f993','%f994','%f995','%f996','%f997','%f998','%f999','%f1000','%f1001','%f1002','histogram','%f1003','%f1004','%f1005','%f1006','checkOption','%f1007','repairType','%f1008','%f1009','installUninstallStatment','%f1010','%f1011','%f1012','%f1013','installOptionType','%f1014','installSetValue','%f1015','%f1016','installSetValueList','%f1017','%f1018','setStatement','%f1019','startOptionValueList','%f1020','%f1021','%f1022','%f1023','%f1024','%f1025','%f1026','%f1027','%f1028','%f1029','%f1030','%f1031','%f1032','%f1033','%f1034','%f1035','%f1036','transactionCharacteristics','%f1037','%f1038','%f1039','%f1040','transactionAccessMode','%f1041','isolationLevel','%f1042','%f1043','%f1044','optionValueListContinued','%f1045','%f1046','optionValueNoOptionType','%f1047','%f1048','%f1049','optionValue','%f1050','setSystemVariable','startOptionValueListFollowingOptionType','optionValueFollowingOptionType','setExprOrDefault','%f1051','%f1052','%f1053','showStatement','%f1054','%f1055','%f1056','%f1057','%f1058','%f1059','%f1060','%f1061','%f1062','%f1063','%f1064','%f1065','%f1066','%f1067','%f1068','%f1069','%f1070','%f1071','%f1072','%f1073','%f1074','%f1075','%f1076','%f1077','%f1078','%f1079','%f1080','%f1081','%f1082','%f1083','%f1084','%f1085','%f1086','%f1087','%f1088','%f1089','%f1090','%f1091','%f1092','%f1093','%f1094','%f1095','%f1096','%f1097','%f1098','%f1099','%f1100','%f1101','%f1102','%f1103','%f1104','%f1105','%f1106','%f1107','%f1108','%f1109','%f1110','%f1111','%f1112','%f1113','%f1114','%f1115','%f1116','%f1117','%f1118','%f1119','%f1120','%f1121','%f1122','showCommandType','%f1123','%f1124','nonBlocking','%f1125','%f1126','fromOrIn','inDb','profileType','%f1127','%f1128','%f1129','otherAdministrativeStatement','%f1130','%f1131','%f1132','%f1133','%f1134','%f1135','%f1136','%f1137','keyCacheListOrParts','%f1138','keyCacheList','%f1139','%f1140','%f1141','assignToKeycache','%f1142','assignToKeycachePartition','%f1143','cacheKeyList','keyUsageElement','%f1144','keyUsageList','%f1145','%f1146','%f1147','%f1148','flushOption','%f1149','%f1150','%f1151','logType','%f1152','flushTables','%f1153','%f1154','%f1155','%f1156','flushTablesOptions','%f1157','%f1158','%f1159','preloadTail','%f1160','%f1161','%f1162','preloadList','%f1163','%f1164','%f1165','%f1166','preloadKeys','%f1167','%f1168','adminPartition','resourceGroupManagement','%f1169','%f1170','%f1171','%f1172','createResourceGroup','%f1173','%f1174','%f1175','resourceGroupVcpuList','%f1176','%f1177','%f1178','%f1179','vcpuNumOrRange','%f1180','%f1181','%f1182','resourceGroupPriority','resourceGroupEnableDisable','%f1183','%f1184','%f1185','%f1186','alterResourceGroup','%f1187','setResourceGroup','%f1188','%f1189','%f1190','threadIdList','%f1191','%f1192','%f1193','%f1194','dropResourceGroup','utilityStatement','%f1195','%f1196','describeStatement','%f1197','%f1198','%f1199','%f1200','explainStatement','%f1201','%f1202','%f1203','%f1204','%f1205','%f1206','%f1207','%f1208','explainableStatement','%f1209','%f1210','%f1211','helpCommand','useCommand','restartServer','%f1212','expr','%f1213','%f1214','%f1215','%f1216','%f1217','%f1218','%f1219','%f1220','%f1221','%f1222','%f1223','boolPri','%f1224','%f1225','%f1226','compOp','%f1227','predicate','%f1228','%f1229','%f1230','%f1231','%f1232','%f1233','predicateOperations','%f1234','%f1235','%f1236','%f1237','bitExpr','%f1238','%f1239','%f1240','%f1241','%f1242','%f1243','simpleExpr','%f1244','%f1245','%f1246','%f1247','%f1248','%f1249','%f1250','%f1251','%f1252','%f1253','%f1254','%f1255','%f1256','%f1257','%f1258','%f1259','%f1260','%f1261','%f1262','%f1263','%f1264','%f1265','%f1266','%f1267','%f1268','%f1269','%f1270','arrayCast','%f1271','jsonOperator','%f1272','%f1273','%f1274','%f1275','%f1276','%f1277','%f1278','%f1279','%f1280','%f1281','%f1282','%f1283','%f1284','%f1285','%f1286','%f1287','%f1288','%f1289','%f1290','%f1291','%f1292','%f1293','sumExpr','%f1294','%f1295','%f1296','%f1297','%f1298','%f1299','%f1300','%f1301','%f1302','%f1303','%f1304','%f1305','%f1306','%f1307','%f1308','%f1309','%f1310','%f1311','%f1312','%f1313','%f1314','%f1315','%f1316','%f1317','%f1318','%f1319','%f1320','%f1321','%f1322','%f1323','%f1324','%f1325','%f1326','%f1327','%f1328','%f1329','%f1330','%f1331','%f1332','%f1333','%f1334','%f1335','groupingOperation','%f1336','%f1337','%f1338','%f1339','%f1340','windowFunctionCall','%f1341','%f1342','%f1343','%f1344','%f1345','%f1346','windowingClause','%f1347','%f1348','leadLagInfo','%f1349','%f1350','%f1351','nullTreatment','%f1352','%f1353','%f1354','jsonFunction','%f1355','inSumExpr','identListArg','%f1356','identList','%f1357','%f1358','%f1359','fulltextOptions','%f1360','%f1361','%f1362','%f1363','%f1364','%f1365','%f1366','%f1367','%f1368','%f1369','%f1370','%f1371','%f1372','%f1373','%f1374','runtimeFunctionCall','%f1375','%f1376','%f1377','%f1378','%f1379','%f1380','%f1381','%f1382','%f1383','%f1384','%f1385','%f1386','%f1387','%f1388','%f1389','%f1390','%f1391','%f1392','%f1393','%f1394','%f1395','%f1396','%f1397','%f1398','%f1399','%f1400','%f1401','%f1402','%f1403','%f1404','geometryFunction','%f1405','%f1406','timeFunctionParameters','fractionalPrecision','%f1407','weightStringLevels','%f1408','%f1409','%f1410','%f1411','%f1412','weightStringLevelListItem','%f1413','%f1414','%f1415','%f1416','dateTimeTtype','trimFunction','%f1417','%f1418','%f1419','%f1420','%f1421','%f1422','%f1423','substringFunction','%f1424','%f1425','%f1426','%f1427','%f1428','%f1429','%f1430','%f1431','%f1432','functionCall','%f1433','udfExprList','%f1434','%f1435','%f1436','udfExpr','variable','userVariable','%f1437','%f1438','systemVariable','internalVariableName','%f1439','%f1440','%f1441','%f1442','%f1443','whenExpression','thenExpression','elseExpression','%f1444','%f1445','%f1446','%f1447','%f1448','%f1449','%f1450','%f1451','%f1452','castType','%f1453','%f1454','%f1455','%f1456','%f1457','exprList','%f1458','%f1459','charset','notRule','not2Rule','interval','%f1460','intervalTimeStamp','exprListWithParentheses','exprWithParentheses','simpleExprWithParentheses','%f1461','orderList','%f1462','%f1463','%f1464','orderExpression','%f1465','groupList','%f1466','%f1467','groupingExpression','channel','%f1468','compoundStatement','returnStatement','ifStatement','%f1469','ifBody','%f1470','%f1471','thenStatement','%f1472','compoundStatementList','%f1473','%f1474','%f1475','%f1476','%f1477','caseStatement','%f1478','%f1479','elseStatement','%f1480','labeledBlock','unlabeledBlock','label','%f1481','%f1482','beginEndBlock','%f1483','labeledControl','unlabeledControl','loopBlock','whileDoBlock','repeatUntilBlock','%f1484','spDeclarations','%f1485','%f1486','spDeclaration','%f1487','%f1488','variableDeclaration','%f1489','%f1490','conditionDeclaration','spCondition','%f1491','sqlstate','%f1492','handlerDeclaration','%f1493','%f1494','%f1495','handlerCondition','cursorDeclaration','iterateStatement','leaveStatement','%f1496','getDiagnostics','%f1497','%f1498','%f1499','%f1500','%f1501','%f1502','%f1503','%f1504','%f1505','%f1506','signalAllowedExpr','statementInformationItem','%f1507','%f1508','conditionInformationItem','%f1509','%f1510','signalInformationItemName','%f1511','signalStatement','%f1512','%f1513','%f1514','%f1515','%f1516','%f1517','%f1518','%f1519','resignalStatement','%f1520','%f1521','%f1522','%f1523','%f1524','%f1525','%f1526','signalInformationItem','cursorOpen','cursorClose','%f1527','cursorFetch','%f1528','%f1529','%f1530','%f1531','%f1532','schedule','%f1533','%f1534','%f1535','%f1536','%f1537','columnDefinition','checkOrReferences','%f1538','checkConstraint','%f1539','constraintEnforcement','%f1540','%f1541','%f1542','%f1543','%f1544','%f1545','%f1546','%f1547','%f1548','tableConstraintDef','%f1549','%f1550','%f1551','%f1552','%f1553','%f1554','%f1555','%f1556','%f1557','%f1558','%f1559','%f1560','constraintName','fieldDefinition','%f1561','%f1562','%f1563','%f1564','%f1565','%f1566','%f1567','%f1568','%f1569','%f1570','%f1571','%f1572','%f1573','%f1574','%f1575','%f1576','%f1577','%f1578','%f1579','columnAttribute','%f1580','%f1581','%f1582','%f1583','%f1584','%f1585','%f1586','%f1587','%f1588','columnFormat','storageMedia','%f1589','%f1590','%f1591','gcolAttribute','%f1592','%f1593','%f1594','references','%f1595','%f1596','%f1597','%f1598','%f1599','%f1600','%f1601','%f1602','%f1603','%f1604','%f1605','deleteOption','%f1606','%f1607','keyList','%f1608','%f1609','%f1610','%f1611','keyPart','%f1612','keyListWithExpression','%f1613','%f1614','%f1615','keyPartOrExpression','keyListVariants','%f1616','%f1617','indexType','%f1618','indexOption','%f1619','commonIndexOption','%f1620','visibility','indexTypeClause','%f1621','fulltextIndexOption','spatialIndexOption','dataTypeDefinition','%f1622','%f1623','%f1624','%f1625','%f1626','%f1627','%f1628','%f1629','%f1630','%f1631','%f1632','%f1633','%f1634','%f1635','%f1636','%f1637','%f1638','%f1639','%f1640','%f1641','%f1642','%f1643','%f1644','%f1645','%f1646','%f1647','%f1648','%f1649','%f1650','dataType','%f1651','%f1652','%f1653','%f1654','%f1655','%f1656','%f1657','%f1658','%f1659','%f1660','%f1661','%f1662','nchar','%f1663','realType','fieldLength','%f1664','%f1665','fieldOptions','%f1666','%f1667','%f1668','%f1669','charsetWithOptBinary','%f1670','%f1671','%f1672','ascii','%f1673','unicode','wsNumCodepoints','typeDatetimePrecision','charsetName','%f1674','collationName','%f1675','%f1676','%f1677','createTableOptions','%f1678','%f1679','%f1680','%f1681','createTableOptionsSpaceSeparated','%f1682','%f1683','%f1684','%f1685','%f1686','%f1687','%f1688','%f1689','%f1690','%f1691','%f1692','%f1693','%f1694','%f1695','%f1696','%f1697','%f1698','%f1699','%f1700','createTableOption','%f1701','%f1702','%f1703','%f1704','%f1705','%f1706','%f1707','%f1708','%f1709','%f1710','%f1711','%f1712','%f1713','%f1714','%f1715','%f1716','%f1717','%f1718','%f1719','%f1720','%f1721','ternaryOption','%f1722','%f1723','defaultCollation','%f1724','%f1725','defaultEncryption','%f1726','%f1727','defaultCharset','%f1728','%f1729','%f1730','partitionClause','%f1731','%f1732','%f1733','%f1734','%f1735','%f1736','partitionTypeDef','%f1737','%f1738','%f1739','%f1740','%f1741','subPartitions','%f1742','%f1743','%f1744','%f1745','partitionKeyAlgorithm','%f1746','%f1747','partitionDefinitions','%f1748','%f1749','%f1750','%f1751','%f1752','partitionDefinition','%f1753','%f1754','%f1755','%f1756','%f1757','%f1758','%f1759','%f1760','%f1761','partitionValuesIn','%f1762','%f1763','%f1764','%f1765','%f1766','%f1767','%f1768','%f1769','%f1770','partitionOption','%f1771','%f1772','%f1773','subpartitionDefinition','%f1774','partitionValueItemListParen','%f1775','%f1776','partitionValueItem','definerClause','ifExists','ifNotExists','%f1777','procedureParameter','%f1778','%f1779','functionParameter','collate','%f1780','typeWithOptCollate','schemaIdentifierPair','%f1781','viewRefList','%f1782','%f1783','%f1784','updateList','%f1785','%f1786','updateElement','%f1787','charsetClause','%f1788','fieldsClause','%f1789','fieldTerm','%f1790','linesClause','lineTerm','%f1791','%f1792','userList','%f1793','%f1794','%f1795','createUserList','%f1796','%f1797','%f1798','alterUserList','%f1799','%f1800','%f1801','createUserEntry','%f1802','%f1803','%f1804','%f1805','%f1806','%f1807','%f1808','%f1809','%f1810','%f1811','%f1812','%f1813','%f1814','%f1815','%f1816','alterUserEntry','%f1817','%f1818','%f1819','%f1820','%f1821','%f1822','%f1823','%f1824','%f1825','%f1826','%f1827','%f1828','%f1829','%f1830','%f1831','%f1832','%f1833','retainCurrentPassword','discardOldPassword','replacePassword','%f1834','userIdentifierOrText','%f1835','%f1836','%f1837','%f1838','user','likeClause','likeOrWhere','onlineOption','noWriteToBinLog','usePartition','%f1839','%f1840','fieldIdentifier','columnName','%f1841','%f1842','columnInternalRef','%f1843','columnInternalRefList','%f1844','%f1845','columnRef','insertIdentifier','indexName','indexRef','%f1846','tableWild','%f1847','%f1848','schemaName','schemaRef','procedureName','procedureRef','functionName','functionRef','triggerName','triggerRef','viewName','viewRef','tablespaceName','tablespaceRef','logfileGroupName','logfileGroupRef','eventName','eventRef','udfName','serverName','serverRef','engineRef','tableName','filterTableRef','%f1849','tableRefWithWildcard','%f1850','%f1851','%f1852','%f1853','%f1854','tableRef','%f1855','tableRefList','%f1856','%f1857','%f1858','tableAliasRefList','%f1859','%f1860','parameterName','labelIdentifier','labelRef','roleIdentifier','roleRef','pluginRef','componentRef','resourceGroupRef','windowName','pureIdentifier','%f1861','%f1862','identifier','%f1863','identifierList','%f1864','%f1865','identifierListWithParentheses','%f1866','qualifiedIdentifier','%f1867','simpleIdentifier','%f1868','%f1869','%f1870','%f1871','dotIdentifier','ulong_number','real_ulong_number','ulonglong_number','real_ulonglong_number','%f1872','%f1873','literal','%f1874','signedLiteral','%f1875','stringList','%f1876','%f1877','textStringLiteral','%f1878','textString','textStringHash','%f1879','%f1880','textLiteral','%f1881','%f1882','textStringNoLinebreak','%f1883','textStringLiteralList','%f1884','%f1885','numLiteral','boolLiteral','nullLiteral','temporalLiteral','floatOptions','standardFloatOptions','precision','textOrIdentifier','lValueIdentifier','roleIdentifierOrText','sizeNumber','parentheses','equal','optionType','varIdentType','setVarIdentType','identifierKeyword','%f1886','%f1887','%f1888','%f1889','%f1890','identifierKeywordsAmbiguous1RolesAndLabels','identifierKeywordsAmbiguous2Labels','labelKeyword','%f1891','%f1892','%f1893','identifierKeywordsAmbiguous3Roles','identifierKeywordsUnambiguous','%f1894','%f1895','%f1896','roleKeyword','%f1897','%f1898','%f1899','lValueKeyword','identifierKeywordsAmbiguous4SystemVariables','roleOrIdentifierKeyword','%f1900','%f1901','%f1902','roleOrLabelKeyword','%f1903','%f1904','%f1905','%f1906','%f1907','%f1908','%f1909'],'grammar'=>[0=>[[-1],[2001,2003]],1=>[[2004],[2873]],2=>[[-1],[0]],3=>[[755,2002],[-1]],4=>[[2009],[2221],[2414],[2470],[2476],[2005],[2479],[2485],[2504],[2508],[2524],[2571],[2598],[2603],[2856],[2860],[2934],[3079],[2006],[3103],[3278],[3301],[3314],[3361],[2007],[3443],[3534],[2008],[3945],[3954]],5=>[[2477]],6=>[[3090]],7=>[[3498]],8=>[[3925]],9=>[[11,2013]],10=>[[2191]],12=>[[2285],[0]],13=>[[2071],[2031],[422,4388,2012],[206,4390,2012],[2212],[2042],[2175],[2010],[2060],[2067],[2014]],14=>[[2029]],15=>[[33]],16=>[[844],[2015]],19=>[[2018],[0]],18=>[[373,480,383,165]],20=>[[451,845,2019]],23=>[[2022],[0]],22=>[[373,480,383,165]],24=>[[451,845,200,57,4435,2023]],25=>[[156],[140]],26=>[[2025,844,846]],27=>[[451,847]],28=>[[482,2016,316,265],[2020],[2024],[2026],[2027]],29=>[[244,2028]],30=>[[4386],[0]],31=>[[109,2030,2034]],32=>[[615,112,139,357]],33=>[[2229,2033],[2229]],34=>[[2033],[2032]],391=>[[4273],[0]],43=>[[2044],[0]],46=>[[2047],[0]],48=>[[2049],[0]],53=>[[2054],[0]],55=>[[2056],[0]],57=>[[2058],[0]],42=>[[2391,170,4400,2043,2046,2048,2053,2055,2057]],44=>[[383,490,3972]],2023=>[[371],[0]],47=>[[383,79,4023,418]],49=>[[453,590,4435]],52=>[[2051],[0]],51=>[[383,514]],54=>[[156],[140,2052]],56=>[[75,4469]],58=>[[147,3869]],59=>[[2062],[0]],60=>[[288,217,4398,4,603,4469,2059]],64=>[[2065,2064],[2065],[0]],62=>[[2066,2064]],2157=>[[750],[0]],65=>[[4157,2066]],66=>[[2345],[2359],[2362]],67=>[[503,4403,2318]],427=>[[4363],[0]],73=>[[2074],[0]],70=>[[2077],[0]],71=>[[2427,2073,574,4414,2070]],72=>[[232]],74=>[[2072]],78=>[[2079],[0]],80=>[[2081],[0]],77=>[[2078,2090],[2083,2080],[4216],[2173]],79=>[[2087,750]],81=>[[4216],[2173]],84=>[[2085],[0]],83=>[[2087,2084],[2112]],85=>[[750,2112]],88=>[[2089,2088],[2089],[0]],87=>[[2117,2088]],89=>[[750,2117]],90=>[[141,572],[234,572],[2105],[2092]],91=>[[722],[723]],92=>[[2091]],1441=>[[4364],[0]],1273=>[[3296,3273],[3296],[0]],1277=>[[3298,3277],[3298],[0]],107=>[[2108],[0]],104=>[[2170],[0]],105=>[[4,405,3441,2106],[148,405,4437],[438,405,3441,2174],[388,405,3441,2174,3441],[14,405,3441,2174],[62,405,2174,3273],[455,405,3441,2174,3277],[67,405,3441,4451],[597,405,2174],[454,405,3441,2107],[172,405,4435,645,574,4414,2104],[2109],[2110]],106=>[[4237],[404,4451]],108=>[[4437,248,4237]],109=>[[141,405,2174,572]],110=>[[234,405,2174,572]],115=>[[2116,2115],[2116],[0]],112=>[[2113,2115]],113=>[[2126],[4161]],114=>[[2126],[2117],[4161]],116=>[[750,2114]],117=>[[2162],[2165],[2170]],136=>[[72],[0]],128=>[[2153],[0]],147=>[[2148],[0]],2282=>[[4281],[0]],126=>[[4,2136,2129],[4,3993],[55,2136,4372,4435,4007,2128],[348,2136,4372,4007,2128],[148,2138],[140,263],[156,263],[11,2136,4372,2142],[2143],[2144],[2145],[2146],[453,2147,4405],[2149],[94,590,3847,2151,4282],[198],[393,45,2157],[2152]],1977=>[[3979],[0]],129=>[[4435,4007,3977,2128],[753,2242,748]],130=>[[4372]],131=>[[4372],[0]],132=>[[2131]],133=>[[2130],[2132]],134=>[[62,4435]],135=>[[86,4435]],137=>[[2154],[0]],138=>[[2136,4372,2137],[199,265,2133],[420,265],[2840,4380],[2134],[2135]],139=>[[3854]],140=>[[2139],[4458]],141=>[[506,4082]],142=>[[506,128,2140],[148,128],[2141]],143=>[[11,236,4380,4082]],144=>[[11,62,4435,3983]],145=>[[11,86,4435,3983]],146=>[[453,72,4372,590,4435]],148=>[[590],[17]],149=>[[453,2840,4380,590,4379]],150=>[[128]],151=>[[2150],[4150]],152=>[[615,403]],153=>[[6,4435],[191]],154=>[[471],[49]],2071=>[[2724],[0]],159=>[[2160,2159],[2160],[0]],157=>[[4442,4071,2159]],160=>[[750,4442,4071]],2262=>[[763],[0]],162=>[[9,4262,2163]],163=>[[128],[4435]],165=>[[287,4262,2166]],166=>[[128],[4435]],167=>[[2165],[0]],168=>[[2162],[0]],169=>[[2162,2167],[2165,2168]],170=>[[2172]],171=>[[645],[646]],172=>[[2171,625]],173=>[[452,403]],174=>[[10],[4437]],175=>[[572,4396,2189]],176=>[[4],[148]],180=>[[2179,2180],[2179],[0]],179=>[[4157,2208]],184=>[[2182],[0]],182=>[[2208,2180]],183=>[[434],[436]],185=>[[55,111,4469,2184],[2183],[371,1]],186=>[[2185]],187=>[[2200]],188=>[[2200],[0]],189=>[[2176,111,4469,2188],[2186],[453,590,4435],[2187]],467=>[[2194],[0]],191=>[[605,572,4396,506,2192,2467]],192=>[[724],[725]],196=>[[2197,2196],[2197],[0]],194=>[[2198,2196]],197=>[[4157,2198]],198=>[[2359]],202=>[[2203,2202],[2203],[0]],200=>[[2205,2202]],203=>[[4157,2205]],205=>[[238,4262,4487],[2350],[2352],[2359],[2206],[2362],[2369]],206=>[[2361]],208=>[[238,4262,4487],[2350],[2352]],374=>[[2376],[0]],372=>[[2378],[0]],212=>[[2374,2391,2372,636,4394,2214]],1233=>[[4374],[0]],214=>[[3233,17,2216]],215=>[[2218],[0]],216=>[[2251,2215]],219=>[[2220],[0]],218=>[[645,2219,62,391]],220=>[[50],[284]],221=>[[97,2225]],222=>[[2408]],223=>[[2412]],224=>[[2328]],225=>[[2228],[2233],[2270],[2258],[2280],[2308],[2373],[2382],[2290],[2316],[2324],[2396],[2222],[2223],[2224]],1391=>[[4275],[0]],227=>[[2229,2227],[2229],[0]],228=>[[109,3391,4385,2227]],229=>[[4212],[4206],[2230]],230=>[[4209]],441=>[[577],[0]],233=>[[2441,574,3391,4405,2240]],236=>[[2235],[0]],235=>[[753,2242,748]],237=>[[4156],[0]],238=>[[4216],[0]],239=>[[2248],[0]],240=>[[275,4414],[753,275,4414,748],[2236,2237,2238,2239]],243=>[[2244,2243],[2244],[0]],242=>[[2245,2243]],244=>[[750,2245]],245=>[[3978],[3993]],249=>[[2250],[0]],762=>[[17],[0]],248=>[[2249,2762,2251]],250=>[[458],[232]],251=>[[2608],[2636]],252=>[[755],[0]],253=>[[97,2254,2252,-1]],254=>[[2258],[2270],[2280]],265=>[[2266],[0]],269=>[[2283,2269],[2283],[0]],258=>[[2391,422,2261,4387,753,2265,748,2269,3869]],260=>[[3391]],261=>[[2260]],264=>[[2263,2264],[2263],[0]],263=>[[750,4277]],266=>[[4277,2264]],277=>[[2278],[0]],270=>[[2391,206,2273,4389,753,2277,748,474,4283,2269,3869]],272=>[[3391]],273=>[[2272]],276=>[[2275,2276],[2275],[0]],275=>[[750,4280]],278=>[[4280,2276]],279=>[[8],[0]],280=>[[2279,206,4401,474,2281,520,4469]],281=>[[556],[249],[437],[126]],283=>[[2286],[4023,137]],284=>[[2283,2284],[2283]],285=>[[2284]],286=>[[75,4469],[267,537],[373,537],[90,537],[433,537,112],[347,537,112],[537,496,2287]],287=>[[130],[250]],428=>[[2169],[0]],290=>[[2427,2299,2428]],291=>[[4083],[0]],292=>[[4379,2291]],2000=>[[2302],[0]],294=>[[2292],[4000]],295=>[[609],[0]],2001=>[[4078,4001],[4078],[0]],1988=>[[4085,3988],[4085],[0]],1991=>[[4086,3991],[4086],[0]],299=>[[2295,236,2294,2306,4001],[205,236,4379,2306,3988],[523,236,4379,2306,3991]],2002=>[[4379],[0]],304=>[[2305],[0]],302=>[[4002,2304]],303=>[[621],[599]],305=>[[2303,4076]],306=>[[383,4414,4073]],307=>[[2311],[0]],308=>[[288,217,4397,4,2309,4469,2307]],309=>[[603],[440]],313=>[[2314,2313],[2314],[0]],311=>[[2315,2313]],314=>[[4157,2315]],315=>[[2345],[2347],[2356],[2359],[2362],[2365]],316=>[[503,4402,199,112,648,4484,2318]],319=>[[2320,2319],[2320],[0]],318=>[[390,753,2321,2319,748]],320=>[[750,2321]],321=>[[224,4469],[109,4469],[618,4469],[406,4469],[519,4469],[398,4469],[413,4450]],325=>[[2326],[0]],323=>[[2336],[0]],324=>[[572,4395,2329,2325,2323]],326=>[[620,288,217,4398]],328=>[[605,572,4395,4,2334,2467]],329=>[[2333],[4,2334]],332=>[[2331],[0]],331=>[[4,2334]],333=>[[2332]],334=>[[111,4469]],338=>[[2339,2338],[2339],[0]],336=>[[2340,2338]],339=>[[4157,2340]],340=>[[2345],[2350],[2352],[2354],[2356],[2359],[2341],[2362],[2365],[2342],[2343]],341=>[[2361]],342=>[[2367]],343=>[[2369]],345=>[[238,4262,4487]],347=>[[2348,4262,4487]],348=>[[604],[441]],350=>[[23,4262,4487]],352=>[[324,4262,4487]],354=>[[181,4262,4487]],356=>[[368,4262,4451]],2257=>[[553],[0]],359=>[[4257,163,4262,4404]],361=>[[848,4262,4463]],362=>[[2363]],363=>[[638],[374]],365=>[[75,4262,4469]],367=>[[189,4262,4487]],369=>[[158,4262,4463]],370=>[[2375],[0]],373=>[[2370,2391,2372,636,4393,2214]],375=>[[394,458,2374],[2376]],376=>[[9,763,2377]],377=>[[602],[335],[578]],378=>[[537,496,2379]],379=>[[130],[250]],381=>[[2388],[0]],382=>[[2391,594,2385,4391,2386,2387,383,4414,200,153,487,2381,3869]],384=>[[3391]],385=>[[2384]],386=>[[28],[6]],387=>[[242],[614],[133]],388=>[[2390]],389=>[[197],[415]],390=>[[2389,4484]],398=>[[2399],[0]],403=>[[2404],[0]],405=>[[2406],[0]],396=>[[2391,170,3391,4399,383,490,3972,2398,2403,2405,147,3869]],399=>[[383,79,4023,418]],402=>[[2401],[0]],401=>[[383,514]],404=>[[156],[140,2402]],406=>[[75,4469]],408=>[[659,3391,3264]],411=>[[2413,2411],[2413],[0]],412=>[[394,458,523,718,710,4453,2411],[523,718,710,3391,4453,2411]],413=>[[357,580,4472],[715,580,4472],[717,4472,230,45,4453],[716,580,4472]],414=>[[148,2418]],415=>[[2464]],416=>[[2466]],417=>[[2468]],418=>[[2420],[2422],[2424],[2426],[2429],[2431],[2440],[2444],[2449],[2457],[2460],[2415],[2416],[2417]],939=>[[4274],[0]],420=>[[109,2939,4386]],422=>[[170,2939,4400]],424=>[[206,2939,4390]],426=>[[422,2939,4388]],429=>[[2427,236,4380,383,4414,2428]],436=>[[2437],[0]],431=>[[288,217,4398,2436]],435=>[[2434,2435],[2434],[0]],434=>[[4157,2438]],437=>[[2438,2435]],438=>[[2362],[2359]],440=>[[503,2939,4403]],446=>[[2447],[0]],444=>[[2441,2445,2939,4416,2446]],445=>[[574],[571]],447=>[[471],[49]],454=>[[2455],[0]],449=>[[572,4396,2454]],453=>[[2452,2453],[2452],[0]],452=>[[4157,2438]],455=>[[2438,2453]],457=>[[594,2939,4392]],461=>[[2462],[0]],460=>[[636,2939,4286,2461]],462=>[[471],[49]],464=>[[659,2939,3264]],466=>[[523,718,710,2939,4453]],468=>[[605,572,4396,2467]],472=>[[2473,2472],[2473],[0]],470=>[[453,2471,2474,2472]],471=>[[574],[571]],473=>[[750,2474]],474=>[[4414,590,4405]],475=>[[574],[0]],476=>[[597,2475,4414]],477=>[[234,574,203,4474]],481=>[[2482],[0]],479=>[[48,4388,2481]],1807=>[[3844],[0]],482=>[[753,3807,748]],487=>[[2488],[0]],484=>[[2503,2484],[2503],[0]],485=>[[2487,133,2484,2500]],486=>[[2714]],488=>[[2486]],489=>[[2829]],493=>[[2491],[0]],491=>[[2489]],1415=>[[2765],[0]],494=>[[2501],[0]],1646=>[[2723],[0]],855=>[[2654],[0]],498=>[[4420,621,2728,3415],[4414,2493,2494,3415,3646,2855]],500=>[[203,2498],[4420,203,2728,3415]],501=>[[2502]],502=>[[405,753,4437,748]],503=>[[431],[295],[431],[232]],504=>[[147,2507]],505=>[[2756]],506=>[[3844]],507=>[[2505],[2506]],508=>[[219,2513]],1421=>[[2653],[0]],511=>[[66],[435,2514,3415,3421]],901=>[[2829],[0]],513=>[[4414,387,2901],[4435,2511]],514=>[[2515],[4435,2518]],515=>[[191],[367]],516=>[[191],[367],[419],[268]],517=>[[763],[769],[765],[768],[764]],518=>[[2516],[2517,753,2555,748]],519=>[[2534],[0]],852=>[[232],[0]],596=>[[248],[0]],791=>[[4365],[0]],523=>[[2562],[0]],524=>[[242,2519,2852,2596,4414,2791,2533,2523]],525=>[[2561]],531=>[[2527],[0]],527=>[[2525]],528=>[[2561]],532=>[[2530],[0]],530=>[[2528]],533=>[[2536,2531],[506,4290,2532],[2547]],534=>[[295],[131],[223]],538=>[[2539],[0]],536=>[[2538,2544]],546=>[[2541],[0]],539=>[[753,2546,748]],542=>[[2543,2542],[2543],[0]],541=>[[4378,2542]],543=>[[750,4378]],544=>[[2545,2550]],545=>[[626],[627]],547=>[[2251],[753,2546,748,2251]],736=>[[2555],[0]],552=>[[2553,2552],[2553],[0]],550=>[[753,2736,748,2552]],553=>[[750,753,2736,748]],558=>[[2559,2558],[2559],[0]],555=>[[2556,2558]],556=>[[3559],[128]],557=>[[3559],[128]],559=>[[750,2557]],561=>[[17,4435,3233]],562=>[[383,151,265,614,4290]],572=>[[2573],[0]],903=>[[284],[0]],574=>[[2575],[0]],667=>[[4295],[0]],568=>[[2577],[0]],668=>[[4297],[0]],669=>[[4301],[0]],571=>[[281,2576,2572,2903,237,4469,2574,248,574,4414,2791,2667,2568,2668,2669,2581]],573=>[[295],[82]],575=>[[458],[232]],576=>[[112],[653]],577=>[[484,230,45,4465]],583=>[[2584],[0]],579=>[[2588],[0]],585=>[[2586],[0]],581=>[[2583,2579,2585]],582=>[[278],[484]],584=>[[232,787,2582]],586=>[[506,4290]],587=>[[2590],[0]],588=>[[753,2587,748]],593=>[[2594,2593],[2594],[0]],590=>[[2591,2593]],591=>[[4377],[3816]],592=>[[4377],[3816]],594=>[[750,2592]],599=>[[2600],[0]],598=>[[458,2599,2596,4414,2791,2601]],600=>[[295],[131]],601=>[[2536],[506,4290],[2547]],635=>[[2742],[0]],603=>[[2608,2635],[2605]],605=>[[753,2605,748],[2608,2662,2635],[2608,2742,2662]],610=>[[2611],[0]],618=>[[2619],[0]],608=>[[2610,2616,2618]],609=>[[2714]],611=>[[2609]],616=>[[2621,3646,3421],[2636,3646,3421]],617=>[[2673]],619=>[[2617]],625=>[[2626,2625],[2626],[0]],621=>[[2628,2625]],622=>[[663]],623=>[[608],[2622]],631=>[[2827],[0]],626=>[[2623,2631,2628]],633=>[[2634,2633],[2634],[0]],628=>[[2629,2633]],629=>[[2637],[2636]],630=>[[2637],[2636]],632=>[[811,2631,2630]],634=>[[2632]],636=>[[753,2608,2635,748]],637=>[[2647],[2638],[2639]],638=>[[2732]],639=>[[2735]],640=>[[2738,2640],[2738],[0]],641=>[[2662],[0]],642=>[[2725],[0]],644=>[[2720],[0]],645=>[[2679],[0]],649=>[[2650],[0]],647=>[[497,2640,2756,2641,2642,3415,2644,2645,2649]],648=>[[2681]],650=>[[2648]],651=>[[2636]],652=>[[10],[143],[555],[223],[536],[531],[532],[534]],653=>[[276,2656]],654=>[[276,2660]],658=>[[2659],[0]],656=>[[2660,2658]],657=>[[750],[381]],659=>[[2657,2660]],660=>[[4435],[2661]],661=>[[754],[791],[788],[787]],662=>[[248,2671]],663=>[[4484],[3816]],664=>[[4484],[3816]],670=>[[2666,2670],[2666],[0]],666=>[[750,2664]],671=>[[396,4463,2667,2668,2669],[150,4463],[2663,2670]],677=>[[2678],[0]],673=>[[422,13,753,2677,748]],676=>[[2675],[0]],675=>[[750,787]],678=>[[787,2676]],679=>[[221,3559]],682=>[[2683,2682],[2683],[0]],681=>[[699,2684,2682]],683=>[[750,2684]],684=>[[4431,17,2685]],685=>[[753,2696,748]],695=>[[2704],[0]],697=>[[2698],[0]],699=>[[2700],[0]],692=>[[4431],[0]],701=>[[2702],[0]],696=>[[405,45,3857,3646,2695],[2697,2723,2695],[2699,3646,2704],[2692,2701,3646,2695]],698=>[[405,45,3857]],700=>[[405,45,3857]],702=>[[405,45,3857]],703=>[[2710],[0]],704=>[[2705,2706,2703]],705=>[[484],[432],[683]],706=>[[2707],[2708]],707=>[[698,693],[4452,693],[754,693],[247,3559,3850,693],[101,487]],708=>[[30,2709,15,2709]],709=>[[2707],[698,682],[4452,682],[754,682],[247,3559,3850,682]],710=>[[680,2711]],711=>[[101,487],[217],[697],[373,690]],712=>[[665],[0]],715=>[[2716,2715],[2716],[0]],714=>[[645,2712,2718,2715]],716=>[[750,2718]],718=>[[4435,3233,17,2651]],719=>[[2721],[0]],720=>[[217,45,3857,2719]],721=>[[645,481],[2722]],722=>[[645,99]],723=>[[393,45,3857]],724=>[[18],[134]],725=>[[203,2726]],726=>[[149],[2728]],729=>[[2730,2729],[2730],[0]],728=>[[2767,2729]],730=>[[750,2767]],733=>[[2734,2733],[2734],[0]],732=>[[626,2737,2733]],734=>[[750,2737]],735=>[[574,4414]],737=>[[487,753,2736,748]],738=>[[2652],[535],[2739],[2740]],739=>[[533]],740=>[[325,763,4451]],741=>[[2745,2741],[2745]],742=>[[2741]],747=>[[2748],[0]],750=>[[2751],[0]],745=>[[200,2752,2747,2750],[287,251,508,346]],746=>[[668,4420]],748=>[[2746]],749=>[[2754]],751=>[[2749]],752=>[[614],[2753]],753=>[[508]],754=>[[669,670],[671]],758=>[[2759,2758],[2759],[0]],756=>[[2757,2758]],757=>[[2761],[775]],759=>[[750,2761]],1813=>[[2763],[0]],761=>[[4382],[3559,3813]],763=>[[2762,2764]],764=>[[4435],[4463]],765=>[[643,3559]],771=>[[2774,2771],[2774],[0]],767=>[[2770,2771]],768=>[[4435]],769=>[[2768],[732]],770=>[[2789],[752,2769,2772,747]],772=>[[2789,2771]],775=>[[2776],[0]],774=>[[2783,2767,2775],[2787,2767,2777],[2780,2789]],776=>[[383,3559],[621,4440]],777=>[[383,3559],[621,4440]],778=>[[239],[0]],786=>[[395],[0]],780=>[[359,2778,261],[359,2781,2786,261]],781=>[[272],[478]],784=>[[2785],[0]],783=>[[2784,261],[555]],785=>[[239],[98]],787=>[[2788,2786,261]],788=>[[272],[478]],789=>[[2794],[2795],[2799],[2806],[2790]],790=>[[2809]],793=>[[2834],[0]],794=>[[4414,2791,2901,2793]],795=>[[753,2796,748]],796=>[[2794],[2795]],801=>[[2802],[0]],799=>[[2651,2901,2801],[2805]],800=>[[4374]],802=>[[2800]],805=>[[726,2651,2901,3233]],806=>[[753,2807,748]],807=>[[2728],[2806]],809=>[[701,753,3559,750,4463,2811,748,2901]],812=>[[2813,2812],[2813],[0]],811=>[[71,753,2817,2812,748]],813=>[[750,2817]],819=>[[2820],[0]],1605=>[[174],[0]],1749=>[[2823],[0]],817=>[[4435,200,703],[4435,4117,2819,3605,704,4463,3749],[702,704,4463,2811]],818=>[[4281]],820=>[[2818]],821=>[[2825],[0]],822=>[[2824],[0]],823=>[[2824,2821],[2825,2822]],824=>[[2826,383,700]],825=>[[2826,383,165]],826=>[[165],[376],[128,4463]],827=>[[143],[10]],831=>[[2832],[0]],829=>[[2831,4435]],830=>[[763]],832=>[[17],[2830]],833=>[[2838,2833],[2838]],834=>[[2833]],836=>[[2843],[0]],837=>[[2846],[0]],838=>[[2839,2840,2836,753,2846,748],[620,2840,2836,753,2837,748]],839=>[[198],[232]],840=>[[265],[236]],1995=>[[2840],[0]],842=>[[420,265],[609,3995]],843=>[[200,2844]],844=>[[261],[393,45],[217,45]],847=>[[2848,2847],[2848],[0]],846=>[[2849,2847]],848=>[[750,2849]],849=>[[4435],[420]],858=>[[2859],[0]],904=>[[295],[0]],856=>[[2858,614,2904,2852,2728,506,4290,3415,3646,2855]],857=>[[2714]],859=>[[2857]],860=>[[2865],[2882],[2894],[2906]],861=>[[2875],[0]],881=>[[647],[0]],867=>[[2868],[0]],870=>[[2871],[0]],865=>[[543,592,2861],[77,2881,2867,2870]],1101=>[[373],[0]],868=>[[15,3101,54]],871=>[[3101,450]],873=>[[29,2881]],876=>[[2877,2876],[2877],[0]],875=>[[2878,2876]],877=>[[750,2878]],878=>[[645,85,517],[2880]],879=>[[649],[386]],880=>[[435,2879]],882=>[[489,4435],[480,2881,2892],[450,489,4435]],890=>[[2885],[0]],885=>[[15,3101,54]],891=>[[2888],[0]],888=>[[3101,450]],889=>[[489],[0]],892=>[[590,2889,4435],[2890,2891]],896=>[[2897,2896],[2897],[0]],894=>[[287,2895,2902,2896],[2898],[611,2900]],895=>[[571],[574]],897=>[[750,2902]],898=>[[287,244,200,27]],899=>[[244]],900=>[[571],[574],[2899]],902=>[[4414,2901,2905]],905=>[[435,2903],[2904,649]],906=>[[651,2920]],907=>[[543],[29]],917=>[[2909],[0]],909=>[[261],[472]],912=>[[2911],[0]],911=>[[200,340]],918=>[[2914],[0]],914=>[[566,2912]],919=>[[2916],[0]],916=>[[384,407]],920=>[[2907,2927,2917],[159,2927,2918],[417,2927],[77,2927,2919],[480,2927],[439,2921]],921=>[[2925],[0]],924=>[[2923],[0]],923=>[[94,652]],925=>[[2924]],931=>[[2932],[0]],927=>[[4465,2931]],930=>[[2929],[0]],929=>[[750,4450]],932=>[[750,4465,2930]],937=>[[2938,2937],[2938],[0]],934=>[[428,2935,289,2936],[2959],[468,2949,2937],[2943],[3047],[2944],[2956],[2945]],935=>[[32],[316]],936=>[[590,4469],[28,3559]],938=>[[750,2949]],942=>[[2941],[0]],941=>[[2939,3820]],943=>[[468,658,2942]],944=>[[2979]],945=>[[3077]],946=>[[2951],[0]],1717=>[[10],[0]],1469=>[[3867],[0]],949=>[[316,2946],[2950],[514,3717,3469]],950=>[[430,47]],951=>[[2955]],952=>[[4451]],953=>[[4453]],954=>[[2952],[2953]],955=>[[590,2954]],956=>[[281,2957,203,316]],957=>[[112],[574,4414]],959=>[[55,316,590,2961,3469]],962=>[[2963,2962],[2963],[0]],961=>[[2964,2962]],963=>[[750,2964]],964=>[[300,763,4472],[729,763,4472],[297,763,4472],[318,763,4472],[303,763,4472],[304,763,4450],[298,763,4450],[305,763,4450],[299,763,4450],[314,763,4450],[308,763,4472],[307,763,4472],[317,763,4472],[309,763,4472],[738,763,2967],[310,763,4472],[313,763,4472],[315,763,4450],[311,763,4469],[312,763,4472],[712,763,4472],[713,763,4450],[319,763,4450],[233,763,2970],[735,763,4463],[736,763,4450],[296,763,4450],[737,763,2965],[739,763,4450],[742,763,2966],[2968]],965=>[[4355],[376]],966=>[[743],[383],[744]],967=>[[4472],[376]],968=>[[301,763,4472],[302,763,4452],[447,763,4472],[448,763,4450]],974=>[[2975],[0]],970=>[[753,2974,748]],973=>[[2972,2973],[2972],[0]],972=>[[750,4450]],975=>[[4450,2973]],980=>[[2981,2980],[2981],[0]],983=>[[2984],[0]],979=>[[55,459,522,590,2986,3469],[55,459,190,3024,2980,2983]],981=>[[750,3024]],982=>[[3867]],984=>[[2982]],987=>[[2988,2987],[2988],[0]],986=>[[2989,2987]],988=>[[750,2989]],989=>[[2990,763,4472],[2991,763,4472],[2992,763,4472],[2993,763,4472],[2994,763,4450],[2995,763,4472],[2996,763,4452],[2997,763,4450],[2998,763,4450],[2999,763,4450],[3000,763,4450],[817,763,4450],[3001,763,4450],[3002,763,4463],[3003,763,4450],[3004,763,4450],[3005,763,4472],[3006,763,4472],[3007,763,4472],[3008,763,4469],[3009,763,4472],[3010,763,4472],[3011,763,4472],[3012,763,4450],[3013,763,4472],[3014,763,2967],[3015,763,4472],[3016,763,4450],[729,763,4472],[233,763,2970],[841,763,4450],[737,763,2965],[739,763,4450],[742,763,2966],[842,763,4450],[447,763,4472],[448,763,4450]],990=>[[814],[297]],991=>[[820],[300]],992=>[[838],[318]],993=>[[823],[303]],994=>[[824],[304]],995=>[[821],[301]],996=>[[822],[302]],997=>[[813],[296]],998=>[[819],[319]],999=>[[816],[298]],1000=>[[826],[305]],1001=>[[818],[299]],1002=>[[815],[735]],1003=>[[839],[736]],1004=>[[827],[314]],1005=>[[828],[308]],1006=>[[829],[307]],1007=>[[830],[309]],1008=>[[832],[311]],1009=>[[833],[312]],1010=>[[834],[313]],1011=>[[831],[310]],1012=>[[835],[315]],1013=>[[837],[317]],1014=>[[836],[738]],1015=>[[825],[712]],1016=>[[840],[713]],1018=>[[3026],[0]],1020=>[[3030],[0]],1022=>[[3034],[0]],1023=>[[3039],[0]],1024=>[[460,763,753,3018,748],[461,763,753,3018,748],[462,763,753,3020,748],[463,763,753,3020,748],[464,763,753,3022,748],[465,763,753,3022,748],[466,763,753,3023,748]],1027=>[[3028,3027],[3028],[0]],1026=>[[4386,3027]],1028=>[[750,4386]],1031=>[[3032,3031],[3032],[0]],1030=>[[4406,3031]],1032=>[[750,4406]],1035=>[[3036,3035],[3036],[0]],1034=>[[3037,3035]],1036=>[[750,3037]],1037=>[[4472]],1040=>[[3041,3040],[3041],[0]],1039=>[[4284,3040]],1041=>[[750,4284]],1045=>[[3073],[0]],1048=>[[3049],[0]],1047=>[[543,514,3045,3048,3058,3469],[552,514,3045,3469]],1049=>[[613,3051]],1056=>[[3057,3056],[3057],[0]],1051=>[[3055,3056]],1052=>[[530],[528]],1053=>[[3052,763,4465]],1054=>[[529]],1055=>[[2968],[3053],[3054]],1057=>[[750,2968]],1058=>[[3071],[0]],1067=>[[3060],[0]],1060=>[[618,763,4465]],1068=>[[3062],[0]],1062=>[[406,763,4465]],1069=>[[3064],[0]],1064=>[[129,763,4465]],1070=>[[3066],[0]],1066=>[[409,763,4465]],1071=>[[3067,3068,3069,3070]],1074=>[[3075,3074],[3075],[0]],1073=>[[3076,3074]],1075=>[[750,3076]],1076=>[[449],[538]],1077=>[[3078,210]],1078=>[[543],[552]],1079=>[[417,4435,203,3080],[3083],[3081,417,4435]],1080=>[[4469],[3816]],1081=>[[123],[148]],1084=>[[3085],[0]],1083=>[[173,4435,3084]],1085=>[[621,3087]],1088=>[[3089,3088],[3089],[0]],1087=>[[3816,3088]],1089=>[[750,3816]],1090=>[[677,3097]],1096=>[[3092],[0]],1092=>[[200,459]],1093=>[[3100],[0]],1094=>[[244,203,4360,749,4450,230,45,4463,3093]],2183=>[[4489],[0]],1097=>[[284,112,139,4183,4463],[676,3096],[3094]],1099=>[[3102],[0]],1100=>[[3102],[112,139,4183,4463,3099]],1102=>[[467,3101,539]],1103=>[[3104],[3119],[3158],[3162],[3193],[3198],[3105]],1104=>[[3107]],1105=>[[3258]],1109=>[[3110],[0]],1107=>[[11,618,3109,3111]],1108=>[[4274]],1110=>[[3108]],1111=>[[3114],[3117,3122]],1112=>[[3118],[4360]],1113=>[[10],[369],[3264]],1114=>[[3112,128,659,3113]],1115=>[[4313]],1116=>[[4309]],1117=>[[3115],[3116]],1118=>[[618,4488]],1119=>[[97,618,3121,4309,3132,3122]],1120=>[[4275]],1121=>[[3120],[0]],1122=>[[3131],[0]],1123=>[[75],[812]],1124=>[[3123,4465]],1130=>[[3126],[0]],1126=>[[3124]],1127=>[[3137],[0]],1128=>[[3141],[0]],1129=>[[3144,3129],[3144],[0]],1131=>[[3127,3128,3129,3130]],1132=>[[3136],[0]],1135=>[[3134],[0]],1134=>[[128,659,3264]],1136=>[[3135]],1137=>[[467,3139]],1138=>[[539],[650],[369]],1139=>[[3251],[3138]],1142=>[[3143,3142],[3143]],1141=>[[645,3142]],1143=>[[322,4450],[327,4450],[321,4450],[328,4450]],1144=>[[2,3145],[406,3155],[740,3156],[741,4451]],1145=>[[287],[611]],1154=>[[3147],[0]],1147=>[[247,4451,122],[365],[128]],1148=>[[4451],[128]],1149=>[[4451,122],[128]],1152=>[[3151],[0]],1151=>[[128],[719]],1153=>[[467,101,3152]],1155=>[[177,3154],[705,3148],[706,247,3149],[3153]],1156=>[[4451],[698]],1160=>[[3161],[0]],1158=>[[148,618,3160,4305]],1159=>[[4274]],1161=>[[3159]],1162=>[[215,3176]],1165=>[[3164],[0]],1164=>[[645,660,391]],1166=>[[3225,590,4305,3165]],1210=>[[421],[0]],1168=>[[3225],[10,3210]],1175=>[[3170],[0]],1170=>[[645,215,391]],1218=>[[3223],[0]],1172=>[[3190],[0]],1173=>[[3180],[0]],1174=>[[3189],[0]],1176=>[[3166],[3168,383,3218,3245,590,3177,3172,3173,3174],[427,383,4360,590,3177,3175]],1177=>[[3178],[3179]],1178=>[[4309]],1179=>[[4305]],1180=>[[3182],[3183]],1181=>[[3256,3181],[3256]],1182=>[[645,3181]],1183=>[[645,215,391]],1184=>[[663,3264]],1185=>[[645,659,3187]],1186=>[[3184],[0]],1187=>[[3264],[10,3186],[369],[128]],1188=>[[3185],[0]],1189=>[[17,618,3188]],1190=>[[3191]],1191=>[[3137]],1194=>[[3195,3194],[3195],[0]],1193=>[[453,618,4360,590,4360,3194]],1195=>[[750,4360,590,4360]],1200=>[[3201],[0]],1213=>[[3214],[0]],1198=>[[477,3200,3211,3213]],1199=>[[4274]],1201=>[[3199]],1202=>[[3225,203,4305]],1203=>[[3215],[0]],1207=>[[3205],[0]],1205=>[[3203,203,4305]],1208=>[[383,3218,3245,3207]],1209=>[[3208],[750,215,391,203,4305]],1211=>[[3202],[3225,3215,203,4305],[10,3210,3209],[427,383,4360,203,4305]],1212=>[[232,610,618]],1214=>[[3212]],1215=>[[3217],[3222]],1217=>[[383,3218,3245]],1221=>[[3220],[0]],1220=>[[383,3218,3245]],1222=>[[3221]],1223=>[[574],[206],[422]],1226=>[[3227,3226],[3227],[0]],1225=>[[3231,3226]],1227=>[[750,3231]],1241=>[[3242],[0]],1230=>[[483],[0]],1231=>[[3235],[3236,3233],[3238],[3239],[215,391],[509,110],[97,3241],[287,571],[459,3243],[509,636],[11,3230]],1232=>[[792],[746,4484]],1234=>[[4486,3232],[4486,3233]],1235=>[[3234]],1236=>[[497],[242],[614],[443]],1237=>[[97],[148]],1238=>[[3237,659]],1239=>[[133],[616],[236],[148],[173],[451],[510],[423],[188],[427],[565],[170],[594]],1240=>[[483],[572],[618],[636]],1242=>[[577,571],[3240]],1243=>[[65],[514]],1246=>[[3247],[0]],1245=>[[775,3246],[4386,751,3249],[4386],[4414]],1247=>[[751,775]],1248=>[[4414]],1249=>[[775],[3248]],1253=>[[3254,3253],[3254],[0]],1251=>[[3255,3253]],1252=>[[15],[0]],1254=>[[3252,3255]],1255=>[[63,4465],[259,4465],[559,4465]],1256=>[[215,391],[322,4450],[327,4450],[321,4450],[328,4450]],1261=>[[3262],[0]],1258=>[[506,659,3264],[506,659,3259],[506,128,659,3260,590,3264],[506,659,10,3261]],1259=>[[369],[128]],1260=>[[3264],[369],[10]],1262=>[[663,3264]],1265=>[[3266,3265],[3266],[0]],1264=>[[3268,3265]],1266=>[[750,3268]],1269=>[[3270],[0]],1268=>[[4486,3269]],1270=>[[746,4484],[792]],1281=>[[3282],[0]],1285=>[[3286],[0]],1278=>[[14,3441,3279,4416,3281],[62,3283,4416,3273],[61,3284,4416,3285],[388,3441,3287,4416],[455,3441,3288,4416,3277]],1279=>[[574],[571]],1280=>[[3291]],1282=>[[3280]],1283=>[[574],[571]],1284=>[[574],[571]],1286=>[[431],[180]],1287=>[[574],[571]],1288=>[[574],[571]],1292=>[[3293],[0]],1294=>[[3295],[0]],1291=>[[614,674,383,4437,3292,3294],[148,674,383,4437]],1293=>[[645,787,675]],1295=>[[621,112,4463]],1296=>[[200,615],[3297]],1297=>[[431],[184],[333],[180],[56]],1298=>[[431],[180],[619]],1302=>[[3303],[0]],1304=>[[3305,3304],[3305],[0]],1301=>[[245,410,4435,520,4463],[245,664,4474,3302],[607,410,4428],[607,664,4429,3304]],1303=>[[506,3311]],1305=>[[750,4429]],1306=>[[214],[658]],1307=>[[3306],[0]],1308=>[[3307,3820,4489,3309]],1309=>[[383],[3559]],1312=>[[3313,3312],[3313],[0]],1311=>[[3308,3312]],1313=>[[750,3308]],1314=>[[506,3316]],1317=>[[3318],[0]],1316=>[[592,3334],[406,3317,4489,3325],[3331],[3348,3345],[4490,3355]],1318=>[[200,4360]],1319=>[[382,753,4465,748]],1320=>[[406,753,4465,748]],2343=>[[4353],[0]],2344=>[[4351],[0]],1325=>[[4465,4343,4344],[4465,4343,4344],[3319],[3320]],1328=>[[3327],[0]],1327=>[[200,4360]],1331=>[[406,3328,590,734,4343,4344]],1335=>[[3336],[0]],1337=>[[3338],[0]],1334=>[[3339,3335],[3341,3337]],1336=>[[750,3341]],1338=>[[750,3339]],1339=>[[435,3340]],1340=>[[649],[386]],1341=>[[258,274,3343]],1342=>[[76],[601]],1343=>[[456,435],[435,3342],[500]],1346=>[[3347,3346],[3347],[0]],1345=>[[3346]],1347=>[[750,3352]],1348=>[[3820,4489,3357],[4295],[3816,4489,3559],[3354,4489,3357],[356,3351]],1349=>[[128]],1351=>[[4489,3559],[4150,4282],[3349]],1352=>[[4490,3820,4489,3357],[3348]],1353=>[[4492],[0]],1354=>[[745,3353,3820]],1355=>[[3356,3345],[592,3334]],1356=>[[3820,4489,3357]],1357=>[[3559],[3358],[3360]],1358=>[[128],[383],[10],[32]],1359=>[[487],[710]],1360=>[[3359]],1361=>[[509,3430]],1362=>[[22]],1363=>[[4404],[10]],1364=>[[547],[354],[289]],1365=>[[203],[251]],1366=>[[32],[316]],1368=>[[225],[547,3434,3469]],1369=>[[33],[446]],1409=>[[3371],[0]],1371=>[[251,4465]],1410=>[[3373],[0]],1373=>[[203,4452]],1374=>[[180]],1413=>[[3376],[0]],1376=>[[3374]],1377=>[[236],[235],[263]],1378=>[[639],[166]],1381=>[[3380,3381],[3380],[0]],1380=>[[750,3439]],1419=>[[3383],[0]],1383=>[[3439,3381]],1420=>[[3385],[0]],1385=>[[200,430,787]],1386=>[[547],[631]],1387=>[[93]],1427=>[[3389],[0]],1389=>[[200,4360]],1390=>[[618,4360]],1392=>[[109,3391,4386],[170,4400],[206,4390],[422,4388],[574,4414],[594,4392],[636,4394],[3390]],1429=>[[4362],[0]],1406=>[[3431],[0]],1414=>[[3438],[0]],1432=>[[204],[0]],1422=>[[4490],[0]],1430=>[[3362],[110,3429],[3406,571,3414,3429],[3432,593,3414,3429],[169,3414,3429],[574,547,3414,3429],[387,571,3414,3429],[408],[163,3363,3364],[3406,71,3365,4414,3414,3429],[3366,289],[514,3368],[3369,169,3409,3410,3421,3469],[3413,3377,3437,4414,3414,3415],[4257,162],[95,753,775,748,3378],[639,3421],[166,3421],[426],[425,3419,3420,3421],[3422,3386,3429],[3432,424],[3847,3429],[70,3429],[3387],[421],[216,200,4360,621,4305],[216,3427],[316,547],[97,3392],[422,547,3429],[206,547,3429],[422,68,4388],[206,68,4390]],1431=>[[204],[3433]],1433=>[[180,3432]],1434=>[[3436],[0]],1435=>[[370],[0]],1436=>[[3435]],1437=>[[203],[251]],1438=>[[3437,4435]],1439=>[[40,255],[91,568],[400,185],[3440]],1440=>[[10],[96],[256],[334],[522],[567]],1449=>[[3450],[0]],1443=>[[33,4469],[47,236,3452,251,3444],[196,3441,3448],[266,3449,3559],[281,236,248,47,3485],[3451]],1444=>[[4435],[128]],1447=>[[3446,3447],[3446],[0]],1446=>[[750,3470]],1448=>[[3476],[3470,3447]],1450=>[[84],[430]],1451=>[[510]],1452=>[[3454],[3460]],1455=>[[3456,3455],[3456],[0]],1454=>[[3458,3455]],1456=>[[750,3458]],1492=>[[3462],[0]],1458=>[[4414,3492]],1460=>[[4414,405,753,2174,748,3492]],1461=>[[3465],[0]],1462=>[[2840,753,3461,748]],1463=>[[4435],[420]],1466=>[[3467,3466],[3467],[0]],1465=>[[3463,3466]],1467=>[[750,3463]],1468=>[[3474],[0]],1470=>[[3471],[3468,289],[445,289,3469],[3472],[3473]],1471=>[[136],[225],[421],[547],[617]],1472=>[[430,47]],1473=>[[389]],1474=>[[32],[163],[165],[208],[515]],1479=>[[3480],[0]],1476=>[[3477,3479]],1477=>[[571],[574]],1478=>[[3481],[0]],1480=>[[645,435,287],[4416,3478]],1481=>[[3482],[645,435,287]],1482=>[[200,179]],1486=>[[3487],[0]],1485=>[[4414,3497,3492,3486],[3489]],1487=>[[232,270]],1490=>[[3491,3490],[3491],[0]],1489=>[[3494,3490]],1491=>[[750,3494]],1495=>[[3496],[0]],1494=>[[4414,3492,3495]],1496=>[[232,270]],1497=>[[405,753,2174,748]],1498=>[[3503],[3522],[3524],[3533]],1518=>[[3507],[0]],1519=>[[3516],[0]],1520=>[[3517],[0]],1503=>[[97,709,217,4435,599,4183,3504,3518,3519,3520]],1504=>[[618],[710]],1509=>[[3510,3509],[3510],[0]],1507=>[[711,4183,3512,3509]],1510=>[[4157,3512]],1513=>[[3514],[0]],1512=>[[787,3513]],1514=>[[773,787]],1516=>[[708,4183,787]],1517=>[[156],[140]],1532=>[[198],[0]],1522=>[[11,709,217,4430,3518,3519,3520,3532]],1525=>[[3526],[0]],1524=>[[506,709,217,4435,3525]],1526=>[[200,3528]],1530=>[[3531,3530],[3531],[0]],1528=>[[4451,3530]],1531=>[[4157,4451]],1533=>[[148,709,217,4430,3532]],1534=>[[3542],[3537],[3555],[3556],[3535]],1535=>[[3557]],1539=>[[3540],[0]],1537=>[[3538,4414,3539]],1538=>[[178],[135],[134]],1540=>[[4465],[4377]],1549=>[[3550],[0]],1542=>[[3543,3549,3551]],1543=>[[178],[135],[134]],1544=>[[180]],1545=>[[404]],1546=>[[201,763,4484]],1547=>[[14,201,763,4484]],1548=>[[14]],1550=>[[3544],[3545],[3546],[3547],[3548]],1551=>[[2603],[3553],[3554]],1552=>[[2485],[2524],[2598],[2856]],1553=>[[3552]],1554=>[[200,84,4451]],1555=>[[222,4484]],1556=>[[620,4435]],1557=>[[714]],1558=>[[3567,3558],[3567],[0]],1559=>[[3561,3558]],1564=>[[3565],[0]],1561=>[[3571,3564],[3566]],1562=>[[596],[183],[610]],2040=>[[3848],[0]],1565=>[[257,4040,3562]],1566=>[[371,3559]],1567=>[[3568,3559],[654,3559],[3569,3559]],1568=>[[15],[770]],1569=>[[394],[772]],1570=>[[3573,3570],[3573],[0]],1571=>[[3577,3570]],1573=>[[257,4040,376],[3575,3574,2651],[3575,3577]],1574=>[[10],[16]],1575=>[[763],[777],[764],[765],[768],[769],[776]],1581=>[[3582],[0]],1577=>[[3589,3581]],1578=>[[668],[0]],1579=>[[733,3578,3855]],1582=>[[4040,3584],[3579],[521,275,3589]],1586=>[[3587],[0]],1584=>[[251,3585],[30,3589,15,3577],[275,3596,3586],[444,3589]],1585=>[[2651],[753,3844,748]],1587=>[[168,3596]],1588=>[[3590,3588],[3590],[0]],1589=>[[3596,3588]],1590=>[[760,3589],[3591,3589],[3592,3589],[3593,247,3559,3850],[3594,3589],[757,3589],[759,3589]],1591=>[[775],[762],[774],[145],[349]],1592=>[[778],[773]],1593=>[[778],[773]],1594=>[[779],[780]],1597=>[[3598,3597],[3598],[0]],1596=>[[3600,3597]],1598=>[[761,3600]],1601=>[[3602],[0]],1600=>[[3612,3601]],1602=>[[69,4484]],1613=>[[3614],[0]],1604=>[[487],[0]],1606=>[[3725],[0]],1607=>[[3624],[0]],1881=>[[3559],[0]],1622=>[[3623,3622],[3623]],1610=>[[3828],[0]],1611=>[[3626],[0]],1612=>[[4456],[3649],[3815,3613],[754],[3615],[3616],[3617,3596],[3849,3596],[3604,753,3844,748],[3605,2651],[752,4435,3559,747],[320,3719,7,753,3589,3606,748],[32,3596],[3621],[52,753,3559,17,3838,3607,748],[51,3881,3622,3610,159],[94,753,3559,750,3838,748],[94,753,3559,621,4150,748],[128,753,4444,748],[626,753,4444,748],[247,3559,3850,778,3559],[3808],[3741],[4377,3611]],1614=>[[4489,3559]],1615=>[[3692]],1616=>[[3698]],1617=>[[778],[773],[758]],1618=>[[247],[0]],2106=>[[4149],[0]],1620=>[[52,753,3559,21,586,843,3618,4463,17,113,4106,748]],1621=>[[3620]],1623=>[[3826,3827]],1624=>[[3625]],1625=>[[731]],1626=>[[3627],[3628]],1627=>[[766,4463]],1628=>[[767,4463]],1645=>[[143],[0]],1651=>[[3652],[0]],1655=>[[3656],[0]],1659=>[[3660],[0]],1664=>[[3665],[0]],1667=>[[3668],[0]],1670=>[[3671],[0]],1673=>[[3674],[0]],1676=>[[3677],[0]],1679=>[[3680],[0]],1682=>[[3683],[0]],1685=>[[3686],[0]],1687=>[[3688],[0]],1690=>[[3691],[0]],1649=>[[26,753,3645,3718,748,3651],[3653,753,3718,748,3655],[3657],[95,753,3717,775,748,3659],[95,753,3662,748,3664],[345,753,3645,3718,748,3667],[326,753,3645,3718,748,3670],[551,753,3718,748,3673],[632,753,3718,748,3676],[548,753,3718,748,3679],[635,753,3718,748,3682],[564,753,3645,3718,748,3685],[218,753,3645,3844,3646,3687,748,3690]],1650=>[[3705]],1652=>[[3650]],1653=>[[35],[36],[38]],1654=>[[3705]],1656=>[[3654]],1657=>[[3716]],1658=>[[3705]],1660=>[[3658]],1662=>[[3717,775],[3718],[143,3844]],1663=>[[3705]],1665=>[[3663]],1666=>[[3705]],1668=>[[3666]],1669=>[[3705]],1671=>[[3669]],1672=>[[3705]],1674=>[[3672]],1675=>[[3705]],1677=>[[3675]],1678=>[[3705]],1680=>[[3678]],1681=>[[3705]],1683=>[[3681]],1684=>[[3705]],1686=>[[3684]],1688=>[[499,4465]],1689=>[[3705]],1691=>[[3689]],1692=>[[672,753,3844,748]],1693=>[[3708],[0]],1697=>[[3712],[0]],1703=>[[3704],[0]],1698=>[[3699,4488,3705],[688,3855,3705],[3700,753,3559,3693,748,3697,3705],[3701,3854,3697,3705],[687,753,3559,750,3596,748,3703,3697,3705]],1699=>[[696],[694],[679],[678],[692]],1700=>[[686],[684]],1701=>[[681],[685]],1702=>[[191],[268]],1704=>[[203,3702]],1705=>[[691,3706]],1706=>[[4431],[2685]],1710=>[[3711],[0]],1708=>[[750,3709,3710]],1709=>[[4452],[754],[4435],[3816]],1711=>[[750,3559]],1712=>[[3713,689]],1713=>[[695],[232]],1715=>[[3705],[0]],1716=>[[667,753,3718,748,3715],[666,753,3718,750,3718,748,3715]],1718=>[[3717,3559]],1719=>[[3721],[753,3721,748]],1722=>[[3723,3722],[3723],[0]],1721=>[[4444,3722]],1723=>[[750,4444]],1726=>[[3727],[0]],1725=>[[251,41,346],[251,359,267,346,3726],[645,430,176]],1727=>[[645,430,176]],1742=>[[3743],[0]],2359=>[[4488],[0]],1744=>[[3745,3744],[3745]],1751=>[[3752],[0]],2030=>[[3775],[0]],1757=>[[3758],[0]],1761=>[[3762],[0]],1741=>[[60,753,3844,3742,748],[105,4359],[116,3854],[122,3854],[229,3854],[242,753,3559,750,3559,750,3559,750,3559,748],[247,753,3559,3744,748],[3750],[272,753,3559,750,3559,748],[343,3854],[350,3854],[478,753,3559,750,3559,748],[495,3854],[586,3854],[583,753,3559,3751,748],[3790],[618,4488],[626,3854],[656,3854],[3753,753,3559,750,3754,748],[100,4359],[108,4030],[3755,753,3559,750,247,3559,3850,748],[182,753,3850,203,3559,748],[213,753,3789,750,3559,748],[372,4030],[414,753,3589,251,3559,748],[3798],[569,4030],[3756,753,3852,750,3559,750,3559,748],[622,4359],[624,4030],[623,4030],[19,3854],[58,3854],[67,3853],[70,3854],[109,4488],[231,753,3559,750,3559,750,3559,748],[201,753,3559,750,3559,3757,748],[337,3854],[349,753,3559,750,3559,748],[3759],[3760],[429,3854],[457,753,3559,750,3559,748],[458,753,3559,750,3559,750,3559,748],[476,3854],[485,4488],[597,753,3559,750,3559,748],[640,753,3559,3761,748],[641,753,3559,17,60,748],[641,753,3559,3770,748],[3772]],1743=>[[621,4150]],1745=>[[750,3559]],1748=>[[3747],[0]],1747=>[[851,3838]],1750=>[[850,753,3596,750,4469,3748,3749,748]],1752=>[[750,3559]],1753=>[[5],[558]],1754=>[[3559],[247,3559,3850]],1755=>[[114],[115]],1756=>[[584],[585]],1758=>[[750,3559]],1759=>[[382,753,4469,748]],1760=>[[406,3854]],1762=>[[750,3559]],1768=>[[3764],[0]],1764=>[[17,60,4148]],1765=>[[3778]],1769=>[[3767],[0]],1767=>[[3765]],1770=>[[17,32,4148],[3768,3769],[750,4450,750,4450,750,4450]],1772=>[[3773],[211,753,3807,748],[279,3853],[351,3853],[352,3853],[353,3853],[411,753,3559,750,3559,748],[412,3853]],1773=>[[90,753,3559,750,3559,748]],1774=>[[3776],[0]],1775=>[[753,3774,748]],1776=>[[3777]],1777=>[[787]],1778=>[[274,3782]],1781=>[[3780,3781],[3780],[0]],1780=>[[750,3784]],1782=>[[4451,773,4451],[3784,3781]],1787=>[[3788],[0]],1784=>[[4451,3787]],1785=>[[18],[134]],1786=>[[476],[0]],1788=>[[3785,3786],[476]],1789=>[[116],[586],[113],[583]],1790=>[[595,753,3797,748]],1793=>[[3792],[0]],1792=>[[203,3559]],1797=>[[3559,3793],[269,3881,203,3559],[591,3881,203,3559],[43,3881,203,3559]],1798=>[[563,753,3559,3805,748]],1803=>[[3800],[0]],1800=>[[750,3559]],1804=>[[3802],[0]],1802=>[[200,3559]],1805=>[[750,3559,3803],[203,3559,3804]],1806=>[[3810],[0]],1808=>[[4432,753,3806,748],[4442,753,3807,748]],1811=>[[3812,3811],[3812],[0]],1810=>[[3814,3811]],1812=>[[750,3814]],1814=>[[3559,3813]],1815=>[[3816],[3819]],1816=>[[746,4484],[792]],1817=>[[4491],[0]],2445=>[[4449],[0]],1819=>[[745,3817,4484,4445]],1820=>[[3825],[128,4449]],1822=>[[4435,4445]],1824=>[[4485,4445]],1825=>[[3822],[3824]],1826=>[[642,3559]],1827=>[[582,3559]],1828=>[[154,3559]],2111=>[[4133],[0]],2116=>[[4141],[0]],1834=>[[249],[0]],2092=>[[4481],[0]],1838=>[[32,4111],[60,4111,4116],[4130,4111],[512,3834],[612,3834],[116],[586,4106],[113,4106],[126,4092],[656],[3839],[3840],[3842]],1839=>[[262]],1840=>[[4132]],1841=>[[4482],[0]],1842=>[[195,3841]],1845=>[[3846,3845],[3846],[0]],1844=>[[3559,3845]],1846=>[[750,3559]],1847=>[[60,506],[58]],1848=>[[371],[800]],1849=>[[771],[800]],1850=>[[3852],[3851]],1851=>[[494],[341],[342],[226],[228],[227],[119],[121],[120],[118],[655]],1852=>[[337],[495],[343],[229],[122],[640],[350],[429],[656]],1853=>[[753,3844,748]],1854=>[[753,3559,748]],1855=>[[753,3596,748]],1858=>[[3859,3858],[3859],[0]],1857=>[[3861,3858]],1859=>[[750,3861]],1861=>[[3559,4071]],1864=>[[3865,3864],[3865],[0]],1863=>[[3866,3864]],1865=>[[750,3866]],1866=>[[3559]],1867=>[[3868]],1868=>[[200,57,4472]],1869=>[[2004],[3870],[3871],[3884],[3889],[3890],[3896],[3897],[3923],[3922],[3963],[3966],[3964]],1870=>[[475,3559]],1871=>[[231,3873,159,231]],1874=>[[3875],[0]],1873=>[[3559,3876,3874]],1875=>[[155,3873],[154,3878]],1876=>[[582,3878]],1879=>[[3880,3879],[3880]],1878=>[[3879]],1880=>[[3869,755]],1885=>[[3886,3885],[3886]],1883=>[[3887],[0]],1884=>[[51,3881,3885,3883,159,51]],1886=>[[3826,3876]],1887=>[[154,3878]],1895=>[[4425],[0]],1889=>[[3891,3894,3895]],1890=>[[3894]],1891=>[[4424,749]],1892=>[[3902],[0]],1893=>[[3878],[0]],1894=>[[29,3892,3893,159]],1896=>[[3891,3897,3895]],1897=>[[3898],[3899],[3900]],1898=>[[294,3878,159,294]],1899=>[[644,3559,147,3878,159,644]],1900=>[[457,3878,613,3559,159,457]],1903=>[[3904,3903],[3904]],1902=>[[3903]],1904=>[[3905,755]],1905=>[[3908],[3911],[3916],[3921]],1909=>[[3910],[0]],1908=>[[127,4437,4117,4282,3909]],1910=>[[128,3559]],1911=>[[127,4435,83,200,3912]],1912=>[[4450],[3914]],1913=>[[627],[0]],1914=>[[526,3913,4469]],1918=>[[3919,3918],[3919],[0]],1916=>[[127,3917,219,200,3920,3918,3869]],1917=>[[92],[175],[605]],1919=>[[750,3920]],1920=>[[3912],[4435],[527],[3848,202],[525]],1921=>[[127,4435,106,200,2603]],1922=>[[260,4425]],1923=>[[271,4425]],1927=>[[3928],[0]],1925=>[[207,3927,138,3935]],1926=>[[540]],1928=>[[101],[3926]],1933=>[[3930,3933],[3930],[0]],1930=>[[750,3937]],1934=>[[3932,3934],[3932],[0]],1932=>[[750,3940]],1935=>[[3937,3933],[83,3936,3940,3934]],1936=>[[4456],[3815],[4442]],1937=>[[3938,763,3939]],1938=>[[3815],[4435]],1939=>[[377],[485]],1940=>[[3941,763,3942]],1941=>[[3815],[4435]],1942=>[[3943],[473]],1943=>[[64],[557],[87],[89],[88],[53],[492],[576],[73],[107],[336],[355]],1950=>[[3951],[0]],1945=>[[511,3946,3950]],1946=>[[4435],[3914]],1949=>[[3948,3949],[3948],[0]],1948=>[[750,3962]],1951=>[[506,3962,3949]],1955=>[[3956],[0]],1960=>[[3961],[0]],1954=>[[469,3955,3960]],1956=>[[4435],[3914]],1959=>[[3958,3959],[3958],[0]],1958=>[[750,3962]],1961=>[[506,3962,3959]],1962=>[[3943,763,3936]],1963=>[[387,4435]],1964=>[[66,4435]],1968=>[[3969],[0]],1966=>[[186,3968,4435,248,4437]],1967=>[[367],[0]],1969=>[[3967,203]],1973=>[[3974],[0]],1975=>[[3976],[0]],1972=>[[21,3559],[171,3559,3850,3973,3975]],1974=>[[542,3559]],1976=>[[160,3559]],1978=>[[4369,4007,3977]],1979=>[[3980],[4046]],1980=>[[3981]],1981=>[[62,3854]],1983=>[[4023,730]],2034=>[[4006],[0]],1993=>[[3994,4000,4073,4001],[205,3995,4002,4073,3988],[523,3995,4002,4073,3991],[4034,4004]],1994=>[[265],[236]],1996=>[[420,265],[609,3995]],1997=>[[3983]],2003=>[[3999],[0]],1999=>[[3997]],2004=>[[3996,4000,4073,4001],[199,265,4002,4061,4046],[3981,4003]],2005=>[[4435],[0]],2006=>[[86,4005]],2007=>[[4117,4022]],2018=>[[4009],[0]],2009=>[[209,12]],2019=>[[4011],[0]],2011=>[[637],[554]],2021=>[[4027,4021],[4027],[0]],2013=>[[4021]],2014=>[[4042,4014],[4042],[0]],2015=>[[4014]],2016=>[[4013],[4015]],2020=>[[4282,4018,17,3854,4019,4016]],2022=>[[4020],[4021]],2041=>[[420],[0]],2039=>[[265],[0]],2027=>[[4023,4479],[4028],[128,4031],[4032],[383,614,372,4030],[24],[501,128,627],[4041,265],[609,4039],[75,4469],[4281],[74,4037],[553,4038],[4033],[4035],[4036]],2028=>[[371,720]],2029=>[[3854]],2031=>[[4458],[372,4030],[4029]],2032=>[[4082]],2033=>[[707,4453]],2035=>[[4034,3981]],2036=>[[3983]],2037=>[[192],[152],[128]],2038=>[[142],[334],[128]],2042=>[[609,4039],[75,4465],[4040,376],[4041,265]],2043=>[[4440],[0]],2048=>[[4049],[0]],2056=>[[4057],[0]],2046=>[[443,4414,4043,4048,4056]],2047=>[[204],[402],[513]],2049=>[[320,4047]],2054=>[[4051],[0]],2051=>[[383,133,4058]],2055=>[[4053],[0]],2053=>[[383,614,4058]],2057=>[[383,614,4058,4054],[383,133,4058,4055]],2058=>[[4059],[506,4479],[373,3],[506,128]],2059=>[[471],[49]],2062=>[[4063,4062],[4063],[0]],2061=>[[753,4066,4062,748]],2063=>[[750,4066]],2066=>[[4435,4111,4071]],2069=>[[4070,4069],[4070],[0]],2068=>[[753,4072,4069,748]],2070=>[[750,4072]],2072=>[[4066],[3854,4071]],2073=>[[4074],[4075]],2074=>[[4068]],2075=>[[4061]],2076=>[[4077]],2077=>[[44],[488],[220]],2078=>[[4080],[4083]],2080=>[[264,4262,4450],[75,4469],[4081]],2081=>[[4082]],2082=>[[662],[661]],2083=>[[4084,4076]],2084=>[[621],[599]],2085=>[[4080],[645,401,4435]],2086=>[[4080]],2087=>[[4117,-1]],2103=>[[4136],[0]],2090=>[[4483],[0]],2146=>[[32],[0]],2126=>[[4127],[0]],2117=>[[4118,4111,4103],[4120,4090,4103],[4121,4092,4103],[37,4111],[4122],[4123,4133,4116],[60,4111,4116],[32,4111],[4124,4133,4146],[4130,4111,4146],[628,4133],[656,4111,4103],[116],[586,4106],[583,4106],[113,4106],[587],[39,4111],[4125],[293,628],[293,4126,4116],[589,4116],[580,4111,4116],[332,4116],[291,4116],[164,4460,4116],[506,4460,4116],[501],[4128],[4129]],2118=>[[249],[588],[516],[331],[31]],2131=>[[416],[0]],2120=>[[437],[146,4131]],2121=>[[195],[126],[378],[192]],2122=>[[42],[41]],2123=>[[60,633],[629]],2124=>[[358,629],[379],[361,629],[358,60,633],[361,633]],2125=>[[330],[290]],2127=>[[60,633],[629]],2128=>[[262]],2129=>[[212],[211],[411],[352],[279],[351],[412],[353]],2130=>[[361],[358,60]],2132=>[[437],[146,4131]],2133=>[[753,4134,748]],2134=>[[4453],[783]],2137=>[[4138,4137],[4138]],2136=>[[4137]],2138=>[[512],[612],[657]],2142=>[[4143],[0]],2141=>[[4145],[4147],[46],[3847,4150,4146],[32,4142]],2143=>[[3847,4150]],2145=>[[19,4146],[32,19]],2147=>[[606,4146],[32,606]],2148=>[[753,4451,748]],2149=>[[753,787,748]],2150=>[[4484],[32],[4151]],2151=>[[128]],2152=>[[4484],[4153],[4154]],2153=>[[128]],2154=>[[32]],2158=>[[4159,4158],[4159],[0]],2156=>[[4181,4158]],2159=>[[4157,4181]],2160=>[[4181,4160],[4181]],2161=>[[4160]],2175=>[[4416],[0]],2181=>[[163,4262,4404],[4184],[323,4262,4452],[344,4262,4452],[25,4262,4450],[406,4262,4463],[75,4262,4463],[4186],[4188],[24,4262,4452],[399,4262,4203],[4189,4262,4203],[4190,4262,4450],[132,4262,4450],[486,4262,4191],[608,4262,753,4175,748],[4212],[4206],[243,4262,4192],[112,139,4262,4465],[236,139,4262,4465],[572,4195,4435],[553,4196],[84,4262,4465],[264,4262,4450],[4197],[4199],[4201],[4202]],2182=>[[376],[4484]],2184=>[[721,4183,4182]],2186=>[[81,4262,4465]],2188=>[[158,4262,4465]],2189=>[[544],[545],[546]],2190=>[[61],[575]],2191=>[[128],[152],[192],[80],[442],[78]],2192=>[[373],[191],[268]],2194=>[[4262]],2195=>[[4194],[0]],2196=>[[142],[334]],2197=>[[543,592]],2199=>[[848,4262,4465]],2201=>[[849,4262,4465]],2202=>[[2350]],2203=>[[4450],[128]],2210=>[[128],[0]],2206=>[[4210,69,4262,4152]],2209=>[[4210,158,4262,4463]],2212=>[[4210,3847,4262,4150]],2217=>[[4218],[0]],2214=>[[4229],[0]],2215=>[[4237],[0]],2216=>[[405,45,4223,4217,4214,4215]],2218=>[[404,4451]],2227=>[[277],[0]],2230=>[[4234],[0]],2225=>[[4437],[0]],2223=>[[4227,265,4230,753,4225,748],[4227,220,753,3589,748],[4224,4226]],2224=>[[432],[280]],2226=>[[753,3589,748],[71,753,4225,748]],2232=>[[4233],[0]],2229=>[[561,45,4227,4231,4232]],2231=>[[220,753,3589,748],[265,4230,4440]],2233=>[[560,4451]],2234=>[[4235]],2235=>[[9,763,4451]],2238=>[[4239,4238],[4239],[0]],2237=>[[753,4243,4238,748]],2239=>[[750,4243]],2245=>[[4246],[0]],2266=>[[4263,4266],[4263],[0]],2250=>[[4251],[0]],2243=>[[405,4435,4245,4266,4250]],2244=>[[4269],[329]],2246=>[[626,273,581,4244],[626,251,4253]],2249=>[[4248,4249],[4248],[0]],2248=>[[750,4267]],2251=>[[753,4267,4249,748]],2254=>[[4255,4254],[4255],[0]],2253=>[[4269],[753,4269,4254,748]],2255=>[[750,4269]],2263=>[[572,4262,4435],[4257,163,4262,4404],[368,4262,4451],[4264,4262,4451],[4265,139,4262,4469],[75,4262,4469]],2264=>[[323],[344]],2265=>[[112],[236]],2267=>[[561,4484,4266]],2270=>[[4271,4270],[4271],[0]],2269=>[[753,4272,4270,748]],2271=>[[750,4272]],2272=>[[3589],[329]],2273=>[[130,763,4360]],2274=>[[231,174]],2275=>[[231,3848,174]],2278=>[[4279],[0]],2277=>[[4278,4280]],2279=>[[251],[397],[240]],2280=>[[4423,4283]],2281=>[[69,4152]],2283=>[[4117,4282]],2284=>[[753,4386,750,4386,748]],2287=>[[4288,4287],[4288],[0]],2286=>[[4394,4287]],2288=>[[750,4394]],2291=>[[4292,4291],[4292],[0]],2290=>[[4293,4291]],2292=>[[750,4293]],2293=>[[4377,4489,4294]],2294=>[[3559],[128]],2295=>[[3847,4150]],2296=>[[4299,4296],[4299]],2297=>[[71,4296]],2298=>[[392],[0]],2299=>[[579,45,4465],[4298,157,45,4465],[167,45,4465]],2300=>[[4302,4300],[4302]],2301=>[[278,4300]],2302=>[[4303,45,4465]],2303=>[[579],[541]],2306=>[[4307,4306],[4307],[0]],2305=>[[4360,4306]],2307=>[[750,4360]],2310=>[[4311,4310],[4311],[0]],2309=>[[4317,4310]],2311=>[[750,4317]],2314=>[[4315,4314],[4315],[0]],2313=>[[4333,4314]],2315=>[[750,4333]],2331=>[[4332],[0]],2317=>[[4360,4331]],2318=>[[406]],2328=>[[4320],[0]],2320=>[[4318]],2321=>[[45,4465]],2329=>[[4323],[0]],2323=>[[17,4466],[4321]],2326=>[[4325],[0]],2325=>[[645,4484]],2327=>[[4326,45,734,406]],2330=>[[45,4328,4465],[645,4484,4329],[4327]],2332=>[[230,4330]],2333=>[[4334,4350]],2334=>[[3118],[4360]],2342=>[[4336],[0]],2336=>[[645,4484]],2337=>[[734,406]],2338=>[[4337],[4465]],2345=>[[4341],[0]],2341=>[[17,4466,4344]],2349=>[[4347],[0]],2347=>[[4342,45,4338,4343,4344],[645,4484,4345]],2348=>[[4352]],2350=>[[230,4349],[4348],[0]],2351=>[[727,101,406]],2352=>[[141,728,406]],2353=>[[458,4465]],2357=>[[4358],[0]],2355=>[[4484,4357]],2356=>[[4484],[0]],2358=>[[746,4356],[792]],2360=>[[4355],[105,4359]],2361=>[[275,4463]],2362=>[[4361],[2765]],2363=>[[385],[380]],2364=>[[284],[375]],2365=>[[4366]],2366=>[[405,4440]],2368=>[[4449],[4442,4445]],2369=>[[4370],[4371]],2370=>[[4435]],2371=>[[4368]],2372=>[[4435]],2375=>[[4376,4375],[4376],[0]],2374=>[[753,4372,4375,748]],2376=>[[750,4372]],2377=>[[4368]],2378=>[[4377],[4382]],2379=>[[4435]],2380=>[[4368]],2383=>[[4384],[0]],2382=>[[4435,751,4383,775]],2384=>[[4435,751]],2385=>[[4435]],2386=>[[4435]],2387=>[[4442]],2388=>[[4442]],2389=>[[4442]],2390=>[[4442]],2391=>[[4442]],2392=>[[4442]],2393=>[[4442],[4449]],2394=>[[4442],[4449]],2395=>[[4435]],2396=>[[4435]],2397=>[[4435]],2398=>[[4435]],2399=>[[4442]],2400=>[[4442]],2401=>[[4435]],2402=>[[4484]],2403=>[[4484]],2404=>[[4484]],2405=>[[4442],[4449]],2406=>[[4386,4449]],2412=>[[4413],[0]],2408=>[[4435,4412]],2411=>[[4410],[0]],2410=>[[751,775]],2413=>[[751,775],[4449,4411]],2414=>[[4442],[4449]],2417=>[[4418,4417],[4418],[0]],2416=>[[4414,4417]],2418=>[[750,4414]],2421=>[[4422,4421],[4422],[0]],2420=>[[4408,4421]],2422=>[[750,4408]],2423=>[[4435]],2424=>[[4432],[4501]],2425=>[[4424]],2426=>[[4432],[4510]],2427=>[[4426]],2428=>[[4435]],2429=>[[4463]],2430=>[[4435]],2431=>[[4435]],2432=>[[4433],[4434]],2433=>[[793],[781]],2434=>[[784]],2435=>[[4432],[4493]],2438=>[[4439,4438],[4439],[0]],2437=>[[4435,4438]],2439=>[[750,4435]],2440=>[[753,4437,748]],2442=>[[4435,4445]],2446=>[[4447],[0]],2444=>[[4435,4446],[4448]],2447=>[[4449,4445]],2448=>[[4449,4449]],2449=>[[751,4435]],2450=>[[787],[786],[788],[791],[783],[785]],2451=>[[787],[786],[788],[791]],2452=>[[787],[788],[791],[783],[785]],2453=>[[787],[4454],[791],[788]],2454=>[[786]],2470=>[[794],[0]],2456=>[[4469],[4477],[4480],[4479],[4478],[4470,4457]],2457=>[[786],[782]],2458=>[[4456],[778,4450],[773,4450]],2461=>[[4462,4461],[4462],[0]],2460=>[[753,4465,4461,748]],2462=>[[750,4465]],2463=>[[790],[4464]],2464=>[[784]],2465=>[[4463],[786],[782]],2466=>[[4463],[4467]],2467=>[[786]],2468=>[[4463,4468],[4463],[0]],2469=>[[4471,4468]],2471=>[[4470,4463],[789]],2472=>[[4463]],2475=>[[4476,4475],[4476],[0]],2474=>[[4463,4475]],2476=>[[750,4463]],2477=>[[787],[788],[791],[783],[785]],2478=>[[596],[183]],2479=>[[376],[801]],2480=>[[116,4463],[586,4463],[583,4463]],2481=>[[4133],[4483]],2482=>[[4133]],2483=>[[753,787,750,787,748]],2484=>[[4435],[4463]],2485=>[[4432],[4514]],2486=>[[4426],[4463]],2487=>[[4453],[4432]],2488=>[[753,748]],2489=>[[763],[756]],2490=>[[658],[673],[214],[284],[502]],2491=>[[214,751],[284,751],[502,751]],2492=>[[658,751],[673,751],[214,751],[284,751],[502,751]],2493=>[[4497],[4498]],2494=>[[510]],2495=>[[714]],2496=>[[4501],[4516],[173],[4494],[4495]],2497=>[[4496]],2498=>[[4506],[4499],[4500],[4505],[4515]],2499=>[[173],[714],[510]],2500=>[[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]],2501=>[[4503],[4504]],2502=>[[4520],[170],[188],[369],[423],[427],[451],[459],[709],[565]],2503=>[[4502]],2504=>[[4506],[4505],[4515]],2505=>[[170],[188],[369],[423],[427],[451],[459],[709],[565]],2506=>[[4507],[4509]],2507=>[[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]],2508=>[[731],[741],[735],[738],[736],[733],[744],[740],[737],[734],[739],[742],[743],[583],[586]],2509=>[[4508]],2510=>[[4512],[4513]],2511=>[[4520],[4516]],2512=>[[4511]],2513=>[[4506],[4500],[4515]],2514=>[[4506],[4499],[4500],[4505]],2515=>[[214],[284],[658],[673],[502]],2516=>[[4517],[4518],[4519]],2517=>[[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]],2518=>[[510]],2519=>[[234]],2520=>[[4521],[4522],[4524],[4526],[4527]],2521=>[[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]],2522=>[[510]],2523=>[[99],[234],[206],[484],[487]],2524=>[[4523]],2525=>[[172],[177],[386],[565],[625],[646]],2526=>[[4525]],2527=>[[660]]]]; diff --git a/wp-includes/parser/class-wp-parser-node.php b/wp-includes/parser/class-wp-parser-node.php index b65ebd6..9d66233 100644 --- a/wp-includes/parser/class-wp-parser-node.php +++ b/wp-includes/parser/class-wp-parser-node.php @@ -15,7 +15,7 @@ class WP_Parser_Node { */ public $rule_id; public $rule_name; - public $children = array(); + private $children = array(); public function __construct( $rule_id, $rule_name ) { $this->rule_id = $rule_id; @@ -102,83 +102,169 @@ public function merge_fragment( $node ) { $this->children = array_merge( $this->children, $node->children ); } - public function has_child( $rule_name ) { + public function has_child(): bool { + return count( $this->children ) > 0; + } + + public function has_child_node( ?string $rule_name = null ): bool { foreach ( $this->children as $child ) { - if ( ( $child instanceof WP_Parser_Node && $child->rule_name === $rule_name ) ) { + if ( + $child instanceof WP_Parser_Node + && ( null === $rule_name || $child->rule_name === $rule_name ) + ) { return true; } } return false; } - public function has_token( $token_id = null ) { + public function has_child_token( ?int $token_id = null ): bool { foreach ( $this->children as $child ) { - if ( $child instanceof WP_MySQL_Token && ( - null === $token_id || - $child->type === $token_id - ) ) { + if ( + $child instanceof WP_Parser_Token + && ( null === $token_id || $child->id === $token_id ) + ) { return true; } } return false; } - public function get_token( $token_id = null ) { + + public function get_first_child() { + return $this->children[0] ?? null; + } + + public function get_first_child_node( ?string $rule_name = null ): ?WP_Parser_Node { foreach ( $this->children as $child ) { - if ( $child instanceof WP_MySQL_Token && ( - null === $token_id || - $child->type === $token_id - ) ) { + if ( + $child instanceof WP_Parser_Node + && ( null === $rule_name || $child->rule_name === $rule_name ) + ) { return $child; } } return null; } - public function get_child( $rule_name = null ) { + public function get_first_child_token( ?int $token_id = null ): ?WP_Parser_Token { foreach ( $this->children as $child ) { - if ( $child instanceof WP_Parser_Node && ( - $child->rule_name === $rule_name || - null === $rule_name - ) ) { + if ( + $child instanceof WP_Parser_Token + && ( null === $token_id || $child->id === $token_id ) + ) { + return $child; + } + } + return null; + } + + public function get_first_descendant_node( ?string $rule_name = null ): ?WP_Parser_Node { + $nodes = array( $this ); + while ( count( $nodes ) ) { + $node = array_shift( $nodes ); + $child = $node->get_first_child_node( $rule_name ); + if ( $child ) { return $child; } + $children = $node->get_child_nodes(); + if ( count( $children ) > 0 ) { + array_push( $nodes, ...$children ); + } } + return null; } - public function get_descendant( $rule_name ) { - $parse_trees = array( $this ); - while ( count( $parse_trees ) ) { - $parse_tree = array_pop( $parse_trees ); - if ( $parse_tree->rule_name === $rule_name ) { - return $parse_tree; + public function get_first_descendant_token( ?int $token_id = null ): ?WP_Parser_Token { + $nodes = array( $this ); + while ( count( $nodes ) ) { + $node = array_shift( $nodes ); + $child = $node->get_first_child_token( $token_id ); + if ( $child ) { + return $child; + } + $children = $node->get_child_nodes(); + if ( count( $children ) > 0 ) { + array_push( $nodes, ...$children ); } - array_push( $parse_trees, ...$parse_tree->get_children() ); } return null; } - public function get_descendants( $rule_name ) { - $parse_trees = array( $this ); + public function get_children(): array { + return $this->children; + } + + 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; + } + + 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; + } + + public function get_descendants(): array { + $nodes = array( $this ); $all_descendants = array(); - while ( count( $parse_trees ) ) { - $parse_tree = array_pop( $parse_trees ); - $all_descendants = array_merge( $all_descendants, $parse_tree->get_children( $rule_name ) ); - array_push( $parse_trees, ...$parse_tree->get_children() ); + while ( count( $nodes ) ) { + $node = array_shift( $nodes ); + $all_descendants = array_merge( $all_descendants, $node->get_children() ); + $children = $node->get_child_nodes(); + if ( count( $children ) > 0 ) { + array_push( $nodes, ...$children ); + } } return $all_descendants; } - public function get_children( $rule_name = null ) { - $matches = array(); - foreach ( $this->children as $child ) { - if ( $child instanceof WP_Parser_Node && ( - null === $rule_name || - $child->rule_name === $rule_name - ) ) { - $matches[] = $child; + public function get_descendant_nodes( ?string $rule_name = null ): array { + $nodes = array( $this ); + $all_descendants = array(); + while ( count( $nodes ) ) { + $node = array_shift( $nodes ); + $all_descendants = array_merge( $all_descendants, $node->get_child_nodes( $rule_name ) ); + $children = $node->get_child_nodes(); + if ( count( $children ) > 0 ) { + array_push( $nodes, ...$children ); } } - return $matches; + return $all_descendants; } + + public function get_descendant_tokens( ?int $token_id = null ): array { + $nodes = array( $this ); + $all_descendants = array(); + while ( count( $nodes ) ) { + $node = array_shift( $nodes ); + $all_descendants = array_merge( $all_descendants, $node->get_child_tokens( $token_id ) ); + $children = $node->get_child_nodes(); + if ( count( $children ) > 0 ) { + array_push( $nodes, ...$children ); + } + } + return $all_descendants; + } + + /* + * @TODO: Let's implement a more powerful AST-querying API. + * See: https://github.com/WordPress/sqlite-database-integration/pull/164#discussion_r1855230501 + */ } diff --git a/wp-includes/parser/class-wp-parser-token.php b/wp-includes/parser/class-wp-parser-token.php new file mode 100644 index 0000000..1148995 --- /dev/null +++ b/wp-includes/parser/class-wp-parser-token.php @@ -0,0 +1,36 @@ +id = $id; + $this->value = $value; + } +} diff --git a/wp-includes/parser/class-wp-parser.php b/wp-includes/parser/class-wp-parser.php index 088f3a2..f266cc7 100644 --- a/wp-includes/parser/class-wp-parser.php +++ b/wp-includes/parser/class-wp-parser.php @@ -37,7 +37,7 @@ private function parse_recursive( $rule_id ) { return true; } - if ( $this->tokens[ $this->position ]->type === $rule_id ) { + if ( $this->tokens[ $this->position ]->id === $rule_id ) { ++$this->position; return $this->tokens[ $this->position - 1 ]; } @@ -52,7 +52,7 @@ private function parse_recursive( $rule_id ) { // 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 ]->type; + $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 ] ) @@ -101,7 +101,7 @@ private function parse_recursive( $rule_id ) { // 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->type ) { + if ( $la && 'selectStatement' === $rule_name && WP_MySQL_Lexer::INTO_SYMBOL === $la->id ) { $branch_matches = false; } @@ -115,7 +115,7 @@ private function parse_recursive( $rule_id ) { return false; } - if ( 0 === count( $node->children ) ) { + if ( ! $node->has_child() ) { return true; } diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php new file mode 100644 index 0000000..d14a391 --- /dev/null +++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php @@ -0,0 +1,24 @@ +driver = $driver; + } + + public function getDriver(): WP_SQLite_Driver { + return $this->driver; + } +} diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php new file mode 100644 index 0000000..e97e388 --- /dev/null +++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php @@ -0,0 +1,2601 @@ += 3.37.0 due to the STRICT table support: + * https://www.sqlite.org/stricttables.html + */ + const MINIMUM_SQLITE_VERSION = '3.37.0'; + + /** + * The default timeout in seconds for SQLite to wait for a writable lock. + */ + const DEFAULT_SQLITE_TIMEOUT = 10; + + /** + * An identifier prefix for internal database objects. + * + * @TODO: Do not allow accessing objects with this prefix. + */ + const RESERVED_PREFIX = '_wp_sqlite_'; + + /** + * A map of MySQL tokens to SQLite data types. + * + * This is used to translate a MySQL data type to an SQLite data type. + */ + 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', + + // 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', + + // 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', + + // Binary data types: + WP_MySQL_Lexer::BINARY_SYMBOL => 'INTEGER', + 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', + + // 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', + + // SERIAL, SET, and JSON types are handled in the translation process. + ); + + /** + * A map of normalized MySQL data types to SQLite data types. + * + * This is used to generate SQLite CREATE TABLE statements from the MySQL + * INFORMATION_SCHEMA tables. They keys are MySQL data types normalized + * as they appear in the INFORMATION_SCHEMA. Values are SQLite data types. + */ + const DATA_TYPE_STRING_MAP = array( + // Numeric data types: + 'bit' => 'INTEGER', + 'bool' => 'INTEGER', + 'boolean' => 'INTEGER', + 'tinyint' => 'INTEGER', + 'smallint' => 'INTEGER', + 'mediumint' => 'INTEGER', + 'int' => 'INTEGER', + 'integer' => 'INTEGER', + 'bigint' => 'INTEGER', + 'float' => 'REAL', + 'double' => 'REAL', + 'real' => 'REAL', + 'decimal' => 'REAL', + 'dec' => 'REAL', + 'fixed' => 'REAL', + 'numeric' => 'REAL', + + // String data types: + 'char' => 'TEXT', + 'varchar' => 'TEXT', + 'nchar' => 'TEXT', + 'nvarchar' => 'TEXT', + 'tinytext' => 'TEXT', + 'text' => 'TEXT', + 'mediumtext' => 'TEXT', + 'longtext' => 'TEXT', + 'enum' => 'TEXT', + 'set' => 'TEXT', + 'json' => 'TEXT', + + // Date and time data types: + 'date' => 'TEXT', + 'time' => 'TEXT', + 'datetime' => 'TEXT', + 'timestamp' => 'TEXT', + 'year' => 'TEXT', + + // Binary data types: + 'binary' => 'INTEGER', + 'varbinary' => 'BLOB', + 'tinyblob' => 'BLOB', + 'blob' => 'BLOB', + 'mediumblob' => 'BLOB', + 'longblob' => 'BLOB', + + // Spatial data types: + 'geometry' => 'TEXT', + 'point' => 'TEXT', + 'linestring' => 'TEXT', + 'polygon' => 'TEXT', + 'multipoint' => 'TEXT', + 'multilinestring' => 'TEXT', + 'multipolygon' => 'TEXT', + 'geomcollection' => 'TEXT', + 'geometrycollection' => 'TEXT', + ); + + /** + * A map of MySQL to SQLite date format translation. + * + * It maps MySQL DATE_FORMAT() formats to SQLite STRFTIME() formats. + * + * For MySQL formats, see: + * https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_date-format + * + * For SQLite formats, see: + * https://www.sqlite.org/lang_datefunc.html + * https://strftime.org/ + */ + const MYSQL_DATE_FORMAT_TO_SQLITE_STRFTIME_MAP = array( + '%a' => '%D', + '%b' => '%M', + '%c' => '%n', + '%D' => '%jS', + '%d' => '%d', + '%e' => '%j', + '%H' => '%H', + '%h' => '%h', + '%I' => '%h', + '%i' => '%M', + '%j' => '%z', + '%k' => '%G', + '%l' => '%g', + '%M' => '%F', + '%m' => '%m', + '%p' => '%A', + '%r' => '%h:%i:%s %A', + '%S' => '%s', + '%s' => '%s', + '%T' => '%H:%i:%s', + '%U' => '%W', + '%u' => '%W', + '%V' => '%W', + '%v' => '%W', + '%W' => '%l', + '%w' => '%w', + '%X' => '%Y', + '%x' => '%o', + '%Y' => '%Y', + '%y' => '%y', + ); + + /** + * The SQLite engine version. + * + * This is a mysqli-like property that is needed to avoid a PHP warning in + * the WordPress health info. The "WP_Debug_Data::get_wp_database()" method + * calls "$wpdb->dbh->client_info" - a mysqli-specific abstraction leak. + * + * @TODO: This should be fixed in WordPress core. + * + * See: + * https://github.com/WordPress/wordpress-develop/blob/bcdca3f9925f1d3eca7b78d231837c0caf0c8c24/src/wp-admin/includes/class-wp-debug-data.php#L1579 + * + * @var string + */ + public $client_info; + + /** + * A MySQL query parser grammar. + * + * @var WP_Parser_Grammar + */ + private static $mysql_grammar; + + /** + * The database name. + * + * @var string + */ + private $db_name; + + /** + * An instance of the PDO object. + * + * @var PDO + */ + private $pdo; + + /** + * A service for managing MySQL INFORMATION_SCHEMA tables in SQLite. + * + * @var WP_SQLite_Information_Schema_Builder + */ + private $information_schema_builder; + + /** + * Last executed MySQL query. + * + * @var string + */ + private $last_mysql_query; + + /** + * A list of SQLite queries executed for the last MySQL query. + * + * @var array{ sql: string, params: array }[] + */ + private $last_sqlite_queries = array(); + + /** + * Results of the last emulated query. + * + * @var array|null + */ + private $last_result; + + /** + * Return value of the last emulated query. + * + * @var mixed + */ + private $last_return_value; + + /** + * Number of rows found by the last SQL_CALC_FOUND_ROW query. + * + * @var int + */ + private $last_sql_calc_found_rows = null; + + /** + * Transaction nesting level of the executed SQLite queries. + * + * @var int + */ + private $transaction_level = 0; + + /** + * The PDO fetch mode used for the emulated query. + * + * @var mixed + */ + private $pdo_fetch_mode; + + /** + * Constructor. + * + * Set up an SQLite connection and the MySQL-on-SQLite driver. + * + * @param array $options { + * An array of options. + * + * @type string $database Database name. + * The name of the emulated MySQL database. + * @type string|null $path Optional. SQLite database path. + * For in-memory database, use ':memory:'. + * Must be set when PDO instance is not provided. + * @type PDO|null $connection Optional. PDO instance with SQLite connection. + * If not provided, a new PDO instance will be created. + * @type int|null $timeout Optional. SQLite timeout in seconds. + * The time to wait for a writable lock. + * @type string|null $sqlite_journal_mode Optional. SQLite journal mode. + * } + * + * @throws WP_SQLite_Driver_Exception When the driver initialization fails. + */ + public function __construct( array $options ) { + // Database name. + if ( ! isset( $options['database'] ) || ! is_string( $options['database'] ) ) { + throw $this->new_driver_exception( 'Option "database" is required.' ); + } + $this->db_name = $options['database']; + + // Database connection. + if ( isset( $options['connection'] ) && $options['connection'] instanceof PDO ) { + $this->pdo = $options['connection']; + } + + // Create a PDO connection if it is not provided. + if ( ! $this->pdo ) { + if ( ! isset( $options['path'] ) || ! is_string( $options['path'] ) ) { + throw $this->new_driver_exception( + 'Option "path" is required when "connection" is not provided.' + ); + } + $path = $options['path']; + + try { + $this->pdo = new PDO( 'sqlite:' . $path ); + } catch ( PDOException $e ) { + $code = $e->getCode(); + throw $this->new_driver_exception( $e->getMessage(), is_int( $code ) ? $code : 0, $e ); + } + } + + // Throw exceptions on error. + $this->pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); + + // Configure SQLite timeout. + if ( isset( $options['timeout'] ) && is_int( $options['timeout'] ) ) { + $timeout = $options['timeout']; + } else { + $timeout = self::DEFAULT_SQLITE_TIMEOUT; + } + $this->pdo->setAttribute( PDO::ATTR_TIMEOUT, $timeout ); + + // Return all values (except null) as strings. + $this->pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); + + // Check the SQLite version. + $sqlite_version = $this->get_sqlite_version(); + if ( version_compare( $sqlite_version, self::MINIMUM_SQLITE_VERSION, '<' ) ) { + throw $this->new_driver_exception( + sprintf( + 'The SQLite version %s is not supported. Minimum required version is %s.', + $sqlite_version, + self::MINIMUM_SQLITE_VERSION + ) + ); + } + + // Load SQLite version to a property used by WordPress health info. + $this->client_info = $sqlite_version; + + // Enable foreign keys. By default, they are off. + $this->pdo->query( 'PRAGMA foreign_keys = ON' ); + + // Configure SQLite journal mode. + if ( + isset( $options['sqlite_journal_mode'] ) + && in_array( + $options['sqlite_journal_mode'], + array( 'DELETE', 'TRUNCATE', 'PERSIST', 'MEMORY', 'WAL', 'OFF' ), + true + ) + ) { + $this->pdo->query( 'PRAGMA journal_mode = ' . $options['sqlite_journal_mode'] ); + } + + // Register SQLite functions. + WP_SQLite_PDO_User_Defined_Functions::register_for( $this->pdo ); + + // Load MySQL grammar. + if ( null === self::$mysql_grammar ) { + self::$mysql_grammar = new WP_Parser_Grammar( require self::MYSQL_GRAMMAR_PATH ); + } + + // Initialize information schema builder. + $this->information_schema_builder = new WP_SQLite_Information_Schema_Builder( + $this->db_name, + array( $this, 'execute_sqlite_query' ) + ); + $this->information_schema_builder->ensure_information_schema_tables(); + } + + /** + * Get the PDO object. + * + * @return PDO + */ + public function get_pdo(): PDO { + return $this->pdo; + } + + /** + * Get the version of the SQLite engine. + * + * @return string SQLite engine version as a string. + */ + public function get_sqlite_version(): string { + return $this->pdo->query( 'SELECT SQLITE_VERSION()' )->fetchColumn(); + } + + /** + * Get the last executed MySQL query. + * + * @return string|null + */ + public function get_last_mysql_query(): ?string { + return $this->last_mysql_query; + } + + /** + * Get SQLite queries executed for the last MySQL query. + * + * @return array{ sql: string, params: array }[] + */ + public function get_last_sqlite_queries(): array { + return $this->last_sqlite_queries; + } + + /** + * Get the auto-increment value generated for the last query. + * + * @return int|string + */ + public function get_insert_id() { + $last_insert_id = $this->pdo->lastInsertId(); + if ( is_numeric( $last_insert_id ) ) { + $last_insert_id = (int) $last_insert_id; + } + return $last_insert_id; + } + + /** + * Translate and execute a MySQL query in SQLite. + * + * A single MySQL query can be translated into zero or more SQLite queries. + * + * @param string $query Full SQL statement string. + * @param int $fetch_mode PDO fetch mode. Default is PDO::FETCH_OBJ. + * @param array ...$fetch_mode_args Additional fetch mode arguments. + * + * @return mixed Return value, depending on the query type. + * + * @throws WP_SQLite_Driver_Exception When the query execution fails. + */ + public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mode_args ) { + $this->flush(); + $this->pdo_fetch_mode = $fetch_mode; + $this->last_mysql_query = $query; + + try { + // Parse the MySQL query. + $lexer = new WP_MySQL_Lexer( $query ); + $tokens = $lexer->remaining_tokens(); + + $parser = new WP_MySQL_Parser( self::$mysql_grammar, $tokens ); + $ast = $parser->parse(); + + if ( null === $ast ) { + throw $this->new_driver_exception( 'Failed to parse the MySQL query.' ); + } + + // Handle transaction commands. + + /* + * [GRAMMAR] + * beginWork: BEGIN_SYMBOL WORK_SYMBOL? + */ + $child = $ast->get_first_child(); + if ( $child instanceof WP_Parser_Node && 'beginWork' === $child->rule_name ) { + $this->begin_transaction(); + return true; + } + + if ( $child instanceof WP_Parser_Node && 'simpleStatement' === $child->rule_name ) { + /* + * [GRAMMAR] + * transactionOrLockingStatement: + * transactionStatement | savepointStatement | lockStatement | xaStatement + */ + $subchild = $child->get_first_child_node( 'transactionOrLockingStatement' ); + if ( null !== $subchild ) { + $tokens = $subchild->get_descendant_tokens(); + $token1 = $tokens[0]; + $token2 = $tokens[1] ?? null; + if ( + WP_MySQL_Lexer::START_SYMBOL === $token1->id + && WP_MySQL_Lexer::TRANSACTION_SYMBOL === $token2->id + ) { + $this->begin_transaction(); + return true; + } + + if ( + WP_MySQL_Lexer::BEGIN_SYMBOL === $token1->id + ) { + $this->begin_transaction(); + return true; + } + + if ( + WP_MySQL_Lexer::COMMIT_SYMBOL === $token1->id + ) { + $this->commit(); + return true; + } + + if ( + WP_MySQL_Lexer::ROLLBACK_SYMBOL === $token1->id + ) { + $this->rollback(); + return true; + } + } + } + + // Perform all the queries in a nested transaction. + $this->begin_transaction(); + $this->execute_mysql_query( $ast ); + $this->commit(); + return $this->last_return_value; + } catch ( Throwable $e ) { + try { + $this->rollback(); + } catch ( Throwable $rollback_exception ) { + // Ignore rollback errors. + } + $code = $e->getCode(); + throw $this->new_driver_exception( $e->getMessage(), is_int( $code ) ? $code : 0, $e ); + } + } + + /** + * Get results of the last query. + * + * @return mixed + */ + public function get_query_results() { + return $this->last_result; + } + + /** + * Get return value of the last query() function call. + * + * @return mixed + */ + public function get_last_return_value() { + return $this->last_return_value; + } + + /** + * Execute a query in SQLite. + * + * @param string $sql The query to execute. + * @param array $params The query parameters. + * @throws PDOException When the query execution fails. + * @return PDOStatement The PDO statement object. + */ + public function execute_sqlite_query( string $sql, array $params = array() ): PDOStatement { + $this->last_sqlite_queries[] = array( + 'sql' => $sql, + 'params' => $params, + ); + $stmt = $this->pdo->prepare( $sql ); + $stmt->execute( $params ); + return $stmt; + } + + /** + * Begin a new transaction or nested transaction. + */ + public function begin_transaction(): void { + if ( 0 === $this->transaction_level ) { + $this->execute_sqlite_query( 'BEGIN' ); + } else { + $this->execute_sqlite_query( 'SAVEPOINT LEVEL' . $this->transaction_level ); + } + ++$this->transaction_level; + } + + /** + * Commit the current transaction or nested transaction. + */ + public function commit(): void { + if ( 0 === $this->transaction_level ) { + return; + } + + --$this->transaction_level; + if ( 0 === $this->transaction_level ) { + $this->execute_sqlite_query( 'COMMIT' ); + } else { + $this->execute_sqlite_query( 'RELEASE SAVEPOINT LEVEL' . $this->transaction_level ); + } + } + + /** + * Rollback the current transaction or nested transaction. + */ + public function rollback(): void { + if ( 0 === $this->transaction_level ) { + return; + } + + --$this->transaction_level; + if ( 0 === $this->transaction_level ) { + $this->execute_sqlite_query( 'ROLLBACK' ); + } else { + $this->execute_sqlite_query( 'ROLLBACK TO SAVEPOINT LEVEL' . $this->transaction_level ); + } + } + + /** + * Translate and execute a MySQL query in SQLite. + * + * @param WP_Parser_Node $node The "query" AST node with "simpleStatement" child. + * @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 ) { + throw $this->new_driver_exception( + sprintf( 'Expected "query" node, got: "%s"', $node->rule_name ) + ); + } + + /* + * [GRAMMAR] + * query: + * EOF + * | (simpleStatement | beginWork) (SEMICOLON_SYMBOL EOF? | EOF) + */ + $children = $node->get_child_nodes(); + if ( count( $children ) !== 1 ) { + throw $this->new_driver_exception( + sprintf( 'Expected 1 child node, got: %d', count( $children ) ) + ); + } + + if ( 'simpleStatement' !== $children[0]->rule_name ) { + throw $this->new_driver_exception( + sprintf( 'Expected "simpleStatement" node, got: "%s"', $children[0]->rule_name ) + ); + } + + // Process the "simpleStatement" AST node. + $node = $children[0]->get_first_child_node(); + switch ( $node->rule_name ) { + case 'selectStatement': + $this->execute_select_statement( $node ); + break; + case 'insertStatement': + case 'replaceStatement': + $this->execute_insert_or_replace_statement( $node ); + break; + case 'updateStatement': + $this->execute_update_statement( $node ); + break; + case 'deleteStatement': + $this->execute_delete_statement( $node ); + break; + case 'createStatement': + $subtree = $node->get_first_child_node(); + switch ( $subtree->rule_name ) { + case 'createTable': + $this->execute_create_table_statement( $node ); + break; + default: + throw $this->new_not_supported_exception( + sprintf( + 'statement type: "%s" > "%s"', + $node->rule_name, + $subtree->rule_name + ) + ); + } + 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 + ) + ); + } + break; + case 'dropStatement': + $subtree = $node->get_first_child_node(); + switch ( $subtree->rule_name ) { + case 'dropTable': + $this->execute_drop_table_statement( $node ); + break; + default: + $query = $this->translate( $node ); + $this->execute_sqlite_query( $query ); + $this->set_result_from_affected_rows(); + } + break; + case 'setStatement': + /* + * It would be lovely to support at least SET autocommit, + * but I don't think that is even possible with SQLite. + */ + $this->last_result = 0; + break; + case 'showStatement': + $this->execute_show_statement( $node ); + break; + case 'utilityStatement': + $subtree = $node->get_first_child_node(); + switch ( $subtree->rule_name ) { + case 'describeStatement': + $this->execute_describe_statement( $subtree ); + break; + default: + throw $this->new_not_supported_exception( + sprintf( + 'statement type: "%s" > "%s"', + $node->rule_name, + $subtree->rule_name + ) + ); + } + break; + default: + throw $this->new_not_supported_exception( + sprintf( 'statement type: "%s"', $node->rule_name ) + ); + } + } + + /** + * Translate and execute a MySQL SELECT statement in SQLite. + * + * @param WP_Parser_Node $node The "selectStatement" AST node. + * @throws WP_SQLite_Driver_Exception When the query execution fails. + */ + private function execute_select_statement( WP_Parser_Node $node ): void { + /* + * [GRAMMAR] + * selectStatement: + * queryExpression lockingClauseList? + * | selectStatementWithInto + */ + + // 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 + ); + + // 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' ); + while ( true ) { + if ( $query_expr->has_child_node( 'limitClause' ) ) { + 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' ); + 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' ); + continue; + } + + break; + } + + // 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 ) ) { + $count_expr->append_child( $child ); + } + } + + // Get count of all the rows. + $result = $this->execute_sqlite_query( + 'SELECT COUNT(*) AS cnt FROM (' . $this->translate( $count_expr ) . ')' + ); + + $this->last_sql_calc_found_rows = $result->fetchColumn(); + } else { + $this->last_sql_calc_found_rows = null; + } + + // Execute the query. + $stmt = $this->execute_sqlite_query( $query ); + $this->set_results_from_fetched_data( + $stmt->fetchAll( $this->pdo_fetch_mode ) + ); + } + + /** + * Translate and execute a MySQL INSERT or REPLACE statement in SQLite. + * + * @param WP_Parser_Node $node The "insertStatement" or "replaceStatement" AST node. + * @throws WP_SQLite_Driver_Exception When the query execution fails. + */ + private function execute_insert_or_replace_statement( WP_Parser_Node $node ): void { + $parts = array(); + foreach ( $node->get_children() as $child ) { + if ( $child instanceof WP_MySQL_Token && WP_MySQL_Lexer::IGNORE_SYMBOL === $child->id ) { + // Translate "UPDATE IGNORE" to "UPDATE OR IGNORE". + $parts[] = 'OR IGNORE'; + } else { + $parts[] = $this->translate( $child ); + } + } + $query = implode( ' ', $parts ); + $this->execute_sqlite_query( $query ); + $this->set_result_from_affected_rows(); + } + + /** + * Translate and execute a MySQL UPDATE statement in SQLite. + * + * @param WP_Parser_Node $node The "updateStatement" AST node. + * @throws WP_SQLite_Driver_Exception When the query execution fails. + */ + 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' ); + + /* + * SQLite doesn't support UPDATE with ORDER BY/LIMIT. + * We need to use a subquery to emulate this behavior. + * + * For instance, the following query: + * UPDATE t SET c = 1 WHERE c = 2 LIMIT 1; + * Will be rewritten to: + * UPDATE t SET c = 1 WHERE rowid IN ( SELECT rowid FROM t WHERE c = 2 LIMIT 1 ); + */ + $where_subquery = null; + 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' ), + ) + ); + } + + // Iterate and translate the update statement children. + $parts = array(); + foreach ( $node->get_children() as $child ) { + if ( $child instanceof WP_MySQL_Token && WP_MySQL_Lexer::IGNORE_SYMBOL === $child->id ) { + // Translate "UPDATE IGNORE" to "UPDATE OR IGNORE". + $parts[] = 'OR IGNORE'; + } else { + $parts[] = $this->translate( $child ); + } + + // When using a subquery, skip WHERE, ORDER BY, and LIMIT. + if ( + null !== $where_subquery + && $child instanceof WP_Parser_Node + && 'updateList' === $child->rule_name + ) { + // We can stop here, as the update statement grammar is: + // ... updateList whereClause? orderClause? simpleLimitClause? + break; + } + } + + // Compose the update query. + $query = implode( ' ', $parts ); + if ( null !== $where_subquery ) { + $query .= ' WHERE rowid IN ( ' . $where_subquery . ' )'; + } + + $this->execute_sqlite_query( $query ); + $this->set_result_from_affected_rows(); + } + + /** + * Translate and execute a MySQL DELETE statement in SQLite. + * + * @param WP_Parser_Node $node The "deleteStatement" AST node. + * @throws WP_SQLite_Driver_Exception When the query execution fails. + */ + private function execute_delete_statement( WP_Parser_Node $node ): void { + /* + * Multi-table DELETE. + * + * MySQL supports multi-table DELETE statements that don't work in SQLite. + * These statements can have the following two flavours: + * 1. "DELETE t1, t2 FROM ... JOIN ... WHERE ..." + * 2. "DELETE FROM t1, t2 USING ... JOIN ... WHERE ..." + * + * 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' ); + 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 ) { + $table_aliases[] = $this->unquote_sqlite_identifier( + $this->translate( $alias_ref ) + ); + } + + // 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 ) { + $alias = $this->unquote_sqlite_identifier( + $this->translate( $single_table->get_first_child_node( 'tableAlias' ) ) + ); + $ref = $this->unquote_sqlite_identifier( + $this->translate( $single_table->get_first_child_node( 'tableRef' ) ) + ); + + $alias_map[ $alias ] = $ref; + } + + // 3. Compose the SELECT query to fetch ROWIDs to delete. + $where_clause = $node->get_first_child_node( 'whereClause' ); + if ( null !== $where_clause ) { + $where = $this->translate( $where_clause->get_first_child_node( 'expr' ) ); + } + + $select_list = array(); + foreach ( $table_aliases as $table ) { + $select_list[] = "\"$table\".rowid AS \"{$table}_rowid\""; + } + + $ids = $this->execute_sqlite_query( + sprintf( + 'SELECT %s FROM %s %s', + implode( ', ', $select_list ), + $this->translate( $table_ref_list ), + isset( $where ) ? "WHERE $where" : '' + ) + )->fetchAll( PDO::FETCH_ASSOC ); + + // 4. Execute DELETE statements for each table. + $rows = 0; + if ( count( $ids ) > 0 ) { + foreach ( $table_aliases as $table ) { + $this->execute_sqlite_query( + sprintf( + 'DELETE FROM %s AS %s WHERE rowid IN ( %s )', + $this->quote_sqlite_identifier( $alias_map[ $table ] ), + $this->quote_sqlite_identifier( $table ), + implode( ', ', array_column( $ids, "{$table}_rowid" ) ) + ) + ); + $this->set_result_from_affected_rows(); + $rows += $this->last_result; + } + } + + $this->set_result_from_affected_rows( $rows ); + return; + } + + // @TODO: Translate DELETE with JOIN to use a subquery. + + $query = $this->translate( $node ); + $this->execute_sqlite_query( $query ); + $this->set_result_from_affected_rows(); + } + + /** + * Translate and execute a MySQL CREATE TABLE statement in SQLite. + * + * @param WP_Parser_Node $node The "createStatement" AST node with "createTable" child. + * @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 and CREATE TABLE ... SELECT. + $is_temporary = $subnode->has_child_token( WP_MySQL_Lexer::TEMPORARY_SYMBOL ); + $element_list = $subnode->get_first_child_node( 'tableElementList' ); + if ( true === $is_temporary || null === $element_list ) { + $query = $this->translate( $node ) . ' STRICT'; + $this->execute_sqlite_query( $query ); + $this->set_result_from_affected_rows(); + return; + } + + // Get table name. + $table_name = $this->unquote_sqlite_identifier( + $this->translate( $subnode->get_first_child_node( 'tableName' ) ) + ); + + // Handle IF NOT EXISTS. + if ( $subnode->has_child_node( 'ifNotExists' ) ) { + $table_exists = $this->execute_sqlite_query( + 'SELECT 1 FROM _mysql_information_schema_tables WHERE table_schema = ? AND table_name = ?', + array( $this->db_name, $table_name ) + )->fetchColumn(); + + if ( $table_exists ) { + $this->set_result_from_affected_rows( 0 ); + return; + } + } + + // Save information to information schema tables. + $this->information_schema_builder->record_create_table( $node ); + + // Generate CREATE TABLE statement from the information schema tables. + $queries = $this->get_sqlite_create_table_statement( $table_name ); + $create_table_query = $queries[0]; + $constraint_queries = array_slice( $queries, 1 ); + + $this->execute_sqlite_query( $create_table_query ); + + foreach ( $constraint_queries as $query ) { + $this->execute_sqlite_query( $query ); + } + } + + /** + * Translate and execute a MySQL ALTER TABLE statement in SQLite. + * + * @param WP_Parser_Node $node The "alterStatement" AST node with "alterTable" child. + * @throws WP_SQLite_Driver_Exception When the query execution fails. + */ + private function execute_alter_table_statement( WP_Parser_Node $node ): void { + $table_name = $this->unquote_sqlite_identifier( + $this->translate( $node->get_first_descendant_node( 'tableRef' ) ) + ); + + // Save all column names from the original table. + $column_names = $this->execute_sqlite_query( + 'SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = ? AND table_name = ?', + array( $this->db_name, $table_name ) + )->fetchAll( PDO::FETCH_COLUMN ); + + // Preserve ROWIDs. + // This also addresses a special case when all original columns are dropped + // and there is nothing to copy. We'll always have at least the ROWID column. + array_unshift( $column_names, 'rowid' ); + + // Track column renames and removals. + $column_map = array_combine( $column_names, $column_names ); + foreach ( $node->get_descendant_nodes( 'alterListItem' ) 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( 'columnInternalRef' ) ); + if ( null !== $name ) { + $name = $this->unquote_sqlite_identifier( $name ); + unset( $column_map[ $name ] ); + } + break; + case WP_MySQL_Lexer::CHANGE_SYMBOL: + $old_name = $this->unquote_sqlite_identifier( + $this->translate( $action->get_first_child_node( 'columnInternalRef' ) ) + ); + $new_name = $this->unquote_sqlite_identifier( + $this->translate( $action->get_first_child_node( 'identifier' ) ) + ); + + $column_map[ $old_name ] = $new_name; + break; + case WP_MySQL_Lexer::RENAME_SYMBOL: + $column_ref = $action->get_first_child_node( 'columnInternalRef' ); + if ( null !== $column_ref ) { + $old_name = $this->unquote_sqlite_identifier( + $this->translate( $column_ref ) + ); + $new_name = $this->unquote_sqlite_identifier( + $this->translate( $action->get_first_child_node( 'identifier' ) ) + ); + + $column_map[ $old_name ] = $new_name; + } + break; + } + } + + $this->information_schema_builder->record_alter_table( $node ); + + /* + * See: + * https://www.sqlite.org/lang_altertable.html#making_other_kinds_of_table_schema_changes + */ + + // 1. If foreign key constraints are enabled, disable them. + $pragma_foreign_keys = $this->execute_sqlite_query( 'PRAGMA foreign_keys' )->fetchColumn(); + $this->execute_sqlite_query( 'PRAGMA foreign_keys = OFF' ); + + // 2. Create a new table with the new schema. + $tmp_table_name = self::RESERVED_PREFIX . "tmp_{$table_name}_" . uniqid(); + $quoted_table_name = $this->quote_sqlite_identifier( $table_name ); + $quoted_tmp_table_name = $this->quote_sqlite_identifier( $tmp_table_name ); + $queries = $this->get_sqlite_create_table_statement( $table_name, $tmp_table_name ); + $create_table_query = $queries[0]; + $constraint_queries = array_slice( $queries, 1 ); + $this->execute_sqlite_query( $create_table_query ); + + // 3. Copy data from the original table to the new table. + $this->execute_sqlite_query( + sprintf( + 'INSERT INTO %s (%s) SELECT %s FROM %s', + $quoted_tmp_table_name, + implode( + ', ', + array_map( array( $this, 'quote_sqlite_identifier' ), $column_map ) + ), + implode( + ', ', + array_map( array( $this, 'quote_sqlite_identifier' ), array_keys( $column_map ) ) + ), + $quoted_table_name + ) + ); + + // 4. Drop the original table. + $this->execute_sqlite_query( sprintf( 'DROP TABLE %s', $quoted_table_name ) ); + + // 5. Rename the new table to the original table name. + $this->execute_sqlite_query( + sprintf( + 'ALTER TABLE %s RENAME TO %s', + $quoted_tmp_table_name, + $quoted_table_name + ) + ); + + // 6. Reconstruct indexes, triggers, and views. + foreach ( $constraint_queries as $query ) { + $this->execute_sqlite_query( $query ); + } + + // 7. If foreign key constraints were enabled, verify and enable them. + if ( '1' === $pragma_foreign_keys ) { + $this->execute_sqlite_query( 'PRAGMA foreign_key_check' ); + $this->execute_sqlite_query( 'PRAGMA foreign_keys = ON' ); + } + + // @TODO: Triggers and views. + + // @TODO: Consider using a "fast path" for ALTER TABLE statements that + // consist only of operations that SQLite's ALTER TABLE supports. + } + + /** + * Translate and execute a MySQL DROP TABLE statement in SQLite. + * + * @param WP_Parser_Node $node The "dropStatement" AST node with "dropTable" child. + * @throws WP_SQLite_Driver_Exception When the query execution fails. + */ + private function execute_drop_table_statement( WP_Parser_Node $node ): void { + $child_node = $node->get_first_child_node(); + + // MySQL supports removing multiple tables in a single query DROP query. + // In SQLite, we need to execute each DROP TABLE statement separately. + $table_refs = $child_node->get_first_child_node( 'tableRefList' )->get_child_nodes(); + $is_temporary = $child_node->has_child_token( WP_MySQL_Lexer::TEMPORARY_SYMBOL ); + $queries = array(); + foreach ( $table_refs as $table_ref ) { + $parts = array(); + foreach ( $child_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 ) { + continue; + } + + // Replace table list with the current table reference. + if ( ! $is_token && 'tableRefList' === $child->rule_name ) { + // Add a "temp." schema prefix for temporary tables. + $prefix = $is_temporary ? '"temp".' : ''; + $part = $prefix . $this->translate( $table_ref ); + } else { + $part = $this->translate( $child ); + } + + if ( null !== $part ) { + $parts[] = $part; + } + } + $queries[] = 'DROP ' . implode( ' ', $parts ); + } + + foreach ( $queries as $query ) { + $this->execute_sqlite_query( $query ); + } + $this->information_schema_builder->record_drop_table( $node ); + } + + /** + * Translate and execute a MySQL SHOW statement in SQLite. + * + * @param WP_Parser_Node $node The "showStatement" 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::CREATE_SYMBOL: + if ( WP_MySQL_Lexer::TABLE_SYMBOL === $keyword2->id ) { + $table_name = $this->unquote_sqlite_identifier( + $this->translate( $node->get_first_child_node( 'tableRef' ) ) + ); + + $sql = $this->get_mysql_create_table_statement( $table_name ); + if ( null === $sql ) { + $this->set_results_from_fetched_data( array() ); + } else { + $this->set_results_from_fetched_data( + array( + (object) array( + 'Create Table' => $sql, + ), + ) + ); + } + return; + } + // Fall through to default. + case WP_MySQL_Lexer::INDEX_SYMBOL: + case WP_MySQL_Lexer::INDEXES_SYMBOL: + case WP_MySQL_Lexer::KEYS_SYMBOL: + $table_name = $this->unquote_sqlite_identifier( + $this->translate( $node->get_first_child_node( 'tableRef' ) ) + ); + $this->execute_show_index_statement( $table_name ); + break; + case WP_MySQL_Lexer::GRANTS_SYMBOL: + $this->set_results_from_fetched_data( + array( + (object) array( + 'Grants for root@localhost' => 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, SHUTDOWN, PROCESS, FILE, REFERENCES, INDEX, ALTER, SHOW DATABASES, SUPER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE USER, EVENT, TRIGGER, CREATE TABLESPACE, CREATE ROLE, DROP ROLE ON *.* TO `root`@`localhost` WITH GRANT OPTION', + ), + ) + ); + return; + case WP_MySQL_Lexer::TABLE_SYMBOL: + $this->execute_show_table_status_statement( $node ); + break; + case WP_MySQL_Lexer::TABLES_SYMBOL: + $this->execute_show_tables_statement( $node ); + break; + case WP_MySQL_Lexer::VARIABLES_SYMBOL: + $this->last_result = true; + return; + default: + throw $this->new_not_supported_exception( + sprintf( + 'statement type: "%s" > "%s"', + $node->rule_name, + $keyword1->value + ) + ); + } + } + + /** + * Translate and execute a MySQL SHOW INDEX statement in SQLite. + * + * @param string $table_name The table name to show indexes for. + */ + private function execute_show_index_statement( string $table_name ): void { + $index_info = $this->execute_sqlite_query( + ' + SELECT + TABLE_NAME AS `Table`, + NON_UNIQUE AS `Non_unique`, + INDEX_NAME AS `Key_name`, + SEQ_IN_INDEX AS `Seq_in_index`, + COLUMN_NAME AS `Column_name`, + COLLATION AS `Collation`, + CARDINALITY AS `Cardinality`, + SUB_PART AS `Sub_part`, + PACKED AS `Packed`, + NULLABLE AS `Null`, + INDEX_TYPE AS `Index_type`, + COMMENT AS `Comment`, + INDEX_COMMENT AS `Index_comment`, + IS_VISIBLE AS `Visible`, + EXPRESSION AS `Expression` + FROM _mysql_information_schema_statistics + WHERE table_schema = ? + AND table_name = ? + ORDER BY + INDEX_NAME = "PRIMARY" DESC, + INDEX_TYPE = "FULLTEXT" ASC, + SEQ_IN_INDEX + ', + array( $this->db_name, $table_name ) + )->fetchAll( PDO::FETCH_OBJ ); + + $this->set_results_from_fetched_data( $index_info ); + } + + /** + * Translate and execute a MySQL SHOW TABLE STATUS statement in SQLite. + * + * @param WP_Parser_Node $node The "showStatement" AST node. + * @throws WP_SQLite_Driver_Exception When the query execution fails. + */ + private function execute_show_table_status_statement( WP_Parser_Node $node ): void { + // FROM/IN database. + $in_db = $node->get_first_child_node( 'inDb' ); + if ( null === $in_db ) { + $database = $this->db_name; + } else { + $database = $this->unquote_sqlite_identifier( + $this->translate( $in_db->get_first_child_node( 'identifier' ) ) + ); + } + + // LIKE and WHERE clauses. + $like_or_where = $node->get_first_child_node( 'likeOrWhere' ); + if ( null !== $like_or_where ) { + $condition = $this->translate_show_like_or_where_condition( $like_or_where ); + } + + // Fetch table information. + $table_info = $this->execute_sqlite_query( + sprintf( + 'SELECT * FROM _mysql_information_schema_tables WHERE table_schema = ? %s', + $condition ?? '' + ), + array( $database ) + )->fetchAll( PDO::FETCH_ASSOC ); + + if ( false === $table_info ) { + $this->set_results_from_fetched_data( array() ); + } + + // Format the results. + $tables = array(); + foreach ( $table_info as $value ) { + $tables[] = (object) array( + 'Name' => $value['TABLE_NAME'], + 'Engine' => $value['ENGINE'], + 'Version' => $value['VERSION'], + 'Row_format' => $value['ROW_FORMAT'], + 'Rows' => $value['TABLE_ROWS'], + 'Avg_row_length' => $value['AVG_ROW_LENGTH'], + 'Data_length' => $value['DATA_LENGTH'], + 'Max_data_length' => $value['MAX_DATA_LENGTH'], + 'Index_length' => $value['INDEX_LENGTH'], + 'Data_free' => $value['DATA_FREE'], + 'Auto_increment' => $value['AUTO_INCREMENT'], + 'Create_time' => $value['CREATE_TIME'], + 'Update_time' => $value['UPDATE_TIME'], + 'Check_time' => $value['CHECK_TIME'], + 'Collation' => $value['TABLE_COLLATION'], + 'Checksum' => $value['CHECKSUM'], + 'Create_options' => $value['CREATE_OPTIONS'], + 'Comment' => $value['TABLE_COMMENT'], + ); + } + + $this->set_results_from_fetched_data( $tables ); + } + + /** + * Translate and execute a MySQL SHOW TABLES statement in SQLite. + * + * @param WP_Parser_Node $node The "showStatement" AST node. + * @throws WP_SQLite_Driver_Exception When the query execution fails. + */ + private function execute_show_tables_statement( WP_Parser_Node $node ): void { + // FROM/IN database. + $in_db = $node->get_first_child_node( 'inDb' ); + if ( null === $in_db ) { + $database = $this->db_name; + } else { + $database = $this->unquote_sqlite_identifier( + $this->translate( $in_db->get_first_child_node( 'identifier' ) ) + ); + } + + // LIKE and WHERE clauses. + $like_or_where = $node->get_first_child_node( 'likeOrWhere' ); + if ( null !== $like_or_where ) { + $condition = $this->translate_show_like_or_where_condition( $like_or_where ); + } + + // Fetch table information. + $table_info = $this->execute_sqlite_query( + sprintf( + 'SELECT * FROM _mysql_information_schema_tables WHERE table_schema = ? %s', + $condition ?? '' + ), + array( $database ) + )->fetchAll( PDO::FETCH_ASSOC ); + + if ( false === $table_info ) { + $this->set_results_from_fetched_data( array() ); + } + + // 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 ); + + // Format the results. + $tables = array(); + foreach ( $table_info as $value ) { + $table = array( + "Tables_in_$database" => $value['TABLE_NAME'], + ); + if ( true === $is_full ) { + $table['Table_type'] = $value['TABLE_TYPE']; + } + $tables[] = (object) $table; + } + + $this->set_results_from_fetched_data( $tables ); + } + + /** + * Translate and execute a MySQL DESCRIBE statement in SQLite. + * + * @param WP_Parser_Node $node The "describeStatement" AST node. + * @throws WP_SQLite_Driver_Exception When the query execution fails. + */ + private function execute_describe_statement( WP_Parser_Node $node ): void { + $table_name = $this->unquote_sqlite_identifier( + $this->translate( $node->get_first_child_node( 'tableRef' ) ) + ); + + $column_info = $this->execute_sqlite_query( + ' + SELECT + column_name AS `Field`, + column_type AS `Type`, + is_nullable AS `Null`, + column_key AS `Key`, + column_default AS `Default`, + extra AS Extra + FROM _mysql_information_schema_columns + WHERE table_schema = ? + AND table_name = ? + ', + array( $this->db_name, $table_name ) + )->fetchAll( PDO::FETCH_OBJ ); + + $this->set_results_from_fetched_data( $column_info ); + } + + /** + * Translate a MySQL AST node or token to an SQLite query fragment. + * + * @param WP_Parser_Node|WP_MySQL_Token $node The AST node to translate. + * @return string|null The translated query fragment. + * @throws WP_SQLite_Driver_Exception When the translation fails. + */ + private function translate( $node ): ?string { + if ( null === $node ) { + return null; + } + + if ( $node instanceof WP_MySQL_Token ) { + return $this->translate_token( $node ); + } + + if ( ! $node instanceof WP_Parser_Node ) { + throw $this->new_driver_exception( + sprintf( + 'Expected a WP_Parser_Node or WP_MySQL_Token instance, got: %s', + gettype( $node ) + ) + ); + } + + $rule_name = $node->rule_name; + switch ( $rule_name ) { + case 'querySpecification': + // Translate "HAVING ..." without "GROUP BY ..." to "GROUP BY 1 HAVING ...". + if ( $node->has_child_node( 'havingClause' ) && ! $node->has_child_node( 'groupByClause' ) ) { + $parts = array(); + foreach ( $node->get_children() as $child ) { + if ( $child instanceof WP_Parser_Node && 'havingClause' === $child->rule_name ) { + $parts[] = 'GROUP BY 1'; + } + $part = $this->translate( $child ); + if ( null !== $part ) { + $parts[] = $part; + } + } + return implode( ' ', $parts ); + } + return $this->translate_sequence( $node->get_children() ); + case 'qualifiedIdentifier': + case 'dotIdentifier': + return $this->translate_sequence( $node->get_children(), '' ); + case 'identifierKeyword': + return '`' . $this->translate( $node->get_first_child() ) . '`'; + case 'pureIdentifier': + return $this->translate_pure_identifier( $node ); + case 'textStringLiteral': + return $this->translate_string_literal( $node ); + case 'dataType': + case 'nchar': + $child = $node->get_first_child(); + if ( $child instanceof WP_Parser_Node ) { + return $this->translate( $child ); + } + + // 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 ) { + $child = $node->get_child_tokens()[1] ?? null; + } elseif ( WP_MySQL_Lexer::NATIONAL_SYMBOL === $child->id ) { + $child = $node->get_child_tokens()[1] ?? null; + } + + if ( null === $child ) { + throw $this->new_invalid_input_exception(); + } + + $type = self::DATA_TYPE_MAP[ $child->id ] ?? null; + if ( null !== $type ) { + return $type; + } + + // SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE. + if ( WP_MySQL_Lexer::SERIAL_SYMBOL === $child->id ) { + return 'INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE'; + } + + // @TODO: Handle SET and JSON. + throw $this->new_not_supported_exception( + sprintf( 'data type: %s', $child->value ) + ); + case 'fromClause': + // 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 ) ) { + return null; + } + return $this->translate_sequence( $node->get_children() ); + case 'insertUpdateList': + // Translate "ON DUPLICATE KEY UPDATE" to "ON CONFLICT DO UPDATE SET". + return sprintf( + 'ON CONFLICT DO UPDATE SET %s', + $this->translate( $node->get_first_child_node( 'updateList' ) ) + ); + case 'simpleExpr': + return $this->translate_simple_expr( $node ); + case 'predicateOperations': + $token = $node->get_first_child_token(); + if ( WP_MySQL_Lexer::LIKE_SYMBOL === $token->id ) { + return $this->translate_like( $node ); + } elseif ( WP_MySQL_Lexer::REGEXP_SYMBOL === $token->id ) { + return $this->translate_regexp_functions( $node ); + } + return $this->translate_sequence( $node->get_children() ); + case 'runtimeFunctionCall': + return $this->translate_runtime_function_call( $node ); + case 'functionCall': + return $this->translate_function_call( $node ); + case 'systemVariable': + // @TODO: Emulate some system variables, or use reasonable defaults. + // See: https://dev.mysql.com/doc/refman/8.4/en/server-system-variable-reference.html + // See: https://dev.mysql.com/doc/refman/8.4/en/server-system-variables.html + + // When we have no value, it's reasonable to use NULL. + return 'NULL'; + case 'castType': + // Translate "CAST(... AS BINARY)" to "CAST(... AS BLOB)". + if ( $node->has_child_token( WP_MySQL_Lexer::BINARY_SYMBOL ) ) { + return 'BLOB'; + } + return $this->translate_sequence( $node->get_children() ); + case 'defaultCollation': + // @TODO: Check and save in information schema. + return null; + case 'duplicateAsQueryExpression': + // @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': + return null; + default: + return $this->translate_sequence( $node->get_children() ); + } + } + + /** + * Translate a MySQL token to SQLite. + * + * @param WP_MySQL_Token $token The MySQL token to translate. + * @return string|null The translated value. + */ + private function translate_token( WP_MySQL_Token $token ): ?string { + switch ( $token->id ) { + case WP_MySQL_Lexer::EOF: + return null; + case WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL: + return 'AUTOINCREMENT'; + case WP_MySQL_Lexer::BINARY_SYMBOL: + /* + * There is no "BINARY expr" equivalent in SQLite. We look for the + * keyword from a higher level to respect it in particular cases + * (REGEXP, LIKE, etc.) and then remove it from the output here. + */ + return null; + case WP_MySQL_Lexer::SQL_CALC_FOUND_ROWS_SYMBOL: + /* + * The "SQL_CALC_FOUND_ROWS" keyword is implemented in the select + * statement translation and then removed from the output here. + */ + return null; + default: + return $token->value; + } + } + + /** + * Translate a sequence of MySQL AST nodes to SQLite. + * + * @param array $nodes The MySQL token to translate. + * @param string $separator The separator to use between fragments. + * @return string|null The translated value. + * @throws WP_SQLite_Driver_Exception When the translation fails. + */ + private function translate_sequence( array $nodes, string $separator = ' ' ): ?string { + $parts = array(); + foreach ( $nodes as $node ) { + if ( null === $node ) { + continue; + } + + $translated = $this->translate( $node ); + if ( null === $translated ) { + continue; + } + $parts[] = $translated; + } + if ( 0 === count( $parts ) ) { + return null; + } + return implode( $separator, $parts ); + } + + /** + * Translate a MySQL string literal to SQLite. + * + * @param WP_Parser_Node $node The "textStringLiteral" AST node. + * @return string The translated value. + */ + private function translate_string_literal( WP_Parser_Node $node ): string { + $token = $node->get_first_child_token(); + + /* + * 1. Remove bounding quotes. + */ + $quote = $token->value[0]; + $value = substr( $token->value, 1, -1 ); + + /* + * 2. 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 in cases + * where only one is used. To do so correctly, we need to: + * + * 1. Skip all double backslash patterns (as "\\" resolves to "\"). + * 2. Add an extra backslash when "\%" or "\_" follows right after. + * + * This may be related to: https://bugs.mysql.com/bug.php?id=84118 + */ + $value = preg_replace( '/(^|[^\\\\](?:\\\\{2}))*(\\\\[%_])/', '$1\\\\$2', $value ); + + /* + * 3. Unescape quotes within the string. + */ + $value = str_replace( $quote . $quote, $quote, $value ); + + /* + * 4. Unescape C-style escape sequences. + * + * MySQL string literals are represented using C-style encoded strings, + * but SQLite doesn't support such escaping. + * + * @TODO: Handle NO_BACKSLASH_ESCAPES SQL mode. + */ + $value = stripcslashes( $value ); + + /* + * 5. Translate datetime literals. + * + * Process only strings that could possibly represent a datetime + * literal ("YYYY-MM-DDTHH:MM:SS", "YYYY-MM-DDTHH:MM:SSZ", etc.). + */ + if ( strlen( $value ) >= 19 && is_numeric( $value[0] ) ) { + $value = $this->translate_datetime_literal( $value ); + } + + /* + * 6. Handle null characters. + * + * SQLite doesn't fully support null characters (\u0000) in strings. + * However, it can store them and read them, with some limitations. + * + * In PHP, null bytes are often produced by the serialize() function. + * Removing them would damage the serialized data. + * + * There is no way to store null bytes using a string literal, so we + * need to split the string and concatenate null bytes with its parts. + * This will convert literals will null bytes to expressions. + * + * Alternatively, we could replace string literals with parameters and + * pass them using prepared statements. However, that's not universally + * applicable for all string literals (e.g., in default column values). + * + * See: + * https://www.sqlite.org/nulinstr.html + */ + $parts = array(); + foreach ( explode( "\0", $value ) as $segment ) { + // Escape and quote each segment. + $parts[] = "'" . str_replace( "'", "''", $segment ) . "'"; + } + if ( count( $parts ) > 1 ) { + return '(' . implode( ' || CHAR(0) || ', $parts ) . ')'; + } + return $parts[0]; + } + + /** + * Translate a MySQL pure identifier to SQLite. + * + * @param WP_Parser_Node $node The "pureIdentifier" AST node. + * @return string The translated value. + */ + private function translate_pure_identifier( WP_Parser_Node $node ): string { + $token = $node->get_first_child_token(); + + if ( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT === $token->id ) { + $value = substr( $token->value, 1, -1 ); + $value = str_replace( '""', '"', $value ); + } elseif ( WP_MySQL_Lexer::BACK_TICK_QUOTED_ID === $token->id ) { + $value = substr( $token->value, 1, -1 ); + $value = str_replace( '``', '`', $value ); + } else { + $value = $token->value; + } + + return '`' . str_replace( '`', '``', $value ) . '`'; + } + + /** + * Translate a MySQL simple expression to SQLite. + * + * @param WP_Parser_Node $node The "simpleExpr" AST node. + * @return string The translated value. + * @throws WP_SQLite_Driver_Exception When the translation fails. + */ + private function translate_simple_expr( 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 ) { + return sprintf( + '`excluded`.%s', + $this->translate( $node->get_first_child_node( 'simpleIdentifier' ) ) + ); + } + + return $this->translate_sequence( $node->get_children() ); + } + + /** + * Translate a MySQL LIKE expression to SQLite. + * + * @param WP_Parser_Node $node The "predicateOperations" AST node. + * @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; + + if ( true === $is_binary ) { + $children = $node->get_children(); + return sprintf( + 'GLOB _helper_like_to_glob_pattern(%s)', + $this->translate( $children[1] ) + ); + } + + /* + * @TODO: Implement the ESCAPE '...' clause. + */ + + /* + * @TODO: Implement more correct LIKE behavior. + * + * While SQLite supports the LIKE operator, it seems to differ from the + * MySQL behavior in some ways: + * + * 1. In SQLite, LIKE is case-insensitive only for ASCII characters + * ('a' LIKE 'A' is TRUE but 'æ' LIKE 'Æ' is FALSE) + * 2. In MySQL, LIKE interprets some escape sequences. See the contents + * of the "_helper_like_to_glob_pattern" function. + * + * We'll probably need to overload the like() function: + * https://www.sqlite.org/lang_corefunc.html#like + */ + return $this->translate_sequence( $node->get_children() ) . " ESCAPE '\\'"; + } + + /** + * Translate MySQL REGEXP expression to SQLite. + * + * @param WP_Parser_Node $node The "predicateOperations" AST node. + * @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; + + /* + * If the query says REGEXP BINARY, the comparison is byte-by-byte + * and letter casing matters – lowercase and uppercase letters are + * represented using different byte codes. + * + * The REGEXP function can't be easily made to accept two + * parameters, so we'll have to use a hack to get around this. + * + * If the first character of the pattern is a null byte, we'll + * remove it and make the comparison case-sensitive. This should + * be reasonably safe since PHP does not allow null bytes in + * regular expressions anyway. + */ + if ( true === $is_binary ) { + return 'REGEXP CHAR(0) || ' . $this->translate( $node->get_first_child_node() ); + } + return 'REGEXP ' . $this->translate( $node->get_first_child_node() ); + } + + /** + * Translate a MySQL runtime function call to SQLite. + * + * @param WP_Parser_Node $node The "runtimeFunctionCall" AST node. + * @return string The translated value. + * @throws WP_SQLite_Driver_Exception When the translation fails. + */ + private function translate_runtime_function_call( WP_Parser_Node $node ): string { + $child = $node->get_first_child(); + if ( $child instanceof WP_Parser_Node ) { + return $this->translate( $child ); + } + + switch ( $child->id ) { + case WP_MySQL_Lexer::CURRENT_TIMESTAMP_SYMBOL: + case WP_MySQL_Lexer::NOW_SYMBOL: + /* + * 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: + $nodes = $node->get_child_nodes(); + $value = $this->translate( $nodes[1] ); + $unit = $this->translate( $nodes[2] ); + if ( 'WEEK' === $unit ) { + $unit = 'DAY'; + $value = 7 * $value; + } + return sprintf( + "DATETIME(%s, '%s' || %s || ' %s')", + $this->translate( $nodes[0] ), + WP_MySQL_Lexer::DATE_SUB_SYMBOL === $child->id ? '-' : '+', + $value, + $unit + ); + case WP_MySQL_Lexer::LEFT_SYMBOL: + $nodes = $node->get_child_nodes(); + return sprintf( + 'SUBSTRING(%s, 1, %s)', + $this->translate( $nodes[0] ), + $this->translate( $nodes[1] ) + ); + default: + return $this->translate_sequence( $node->get_children() ); + } + } + + /** + * Translate a MySQL function call to SQLite. + * + * @param WP_Parser_Node $node The "functionCall" AST node. + * @return string The translated value. + * @throws WP_SQLite_Driver_Exception When the translation fails. + */ + private function translate_function_call( WP_Parser_Node $node ): string { + $nodes = $node->get_child_nodes(); + $name = strtoupper( + $this->unquote_sqlite_identifier( $this->translate( $nodes[0] ) ) + ); + + $args = array(); + if ( isset( $nodes[1] ) ) { + foreach ( $nodes[1]->get_child_nodes() as $child ) { + $args[] = $this->translate( $child ); + } + } + + switch ( $name ) { + case 'DATE_FORMAT': + list ( $date, $mysql_format ) = $args; + + $format = strtr( $mysql_format, self::MYSQL_DATE_FORMAT_TO_SQLITE_STRFTIME_MAP ); + if ( ! $format ) { + throw $this->new_driver_exception( + sprintf( + 'Could not translate a DATE_FORMAT() format to STRFTIME format (%s)', + $mysql_format + ) + ); + } + + /* + * MySQL supports comparing strings and floats, e.g. + * + * > SELECT '00.42' = 0.4200 + * 1 + * + * SQLite does not support that. At the same time, + * WordPress likes to filter dates by comparing numeric + * outputs of DATE_FORMAT() to floats, e.g.: + * + * -- Filter by hour and minutes + * DATE_FORMAT( + * STR_TO_DATE('2014-10-21 00:42:29', '%Y-%m-%d %H:%i:%s'), + * '%H.%i' + * ) = 0.4200; + * + * Let's cast the STRFTIME() output to a float if + * the date format is typically used for string + * to float comparisons. + * + * In the future, let's update WordPress to avoid comparing + * strings and floats. + */ + $cast_to_float = "'%H.%i'" === $mysql_format; + if ( true === $cast_to_float ) { + return sprintf( 'CAST(STRFTIME(%s, %s) AS FLOAT)', $format, $date ); + } + return sprintf( 'STRFTIME(%s, %s)', $format, $date ); + case 'CHAR_LENGTH': + // @TODO LENGTH and CHAR_LENGTH aren't always the same in MySQL for utf8 characters. + return 'LENGTH(' . $args[0] . ')'; + case 'CONCAT': + return '(' . implode( ' || ', $args ) . ')'; + case 'FOUND_ROWS': + // @TODO: The following implementation with an alias assumes + // that the function is used in the SELECT field list. + // For compatibility with more complex use cases, it may + // be better to register it as a custom SQLite function. + $found_rows = $this->last_sql_calc_found_rows; + if ( null === $found_rows && is_array( $this->last_result ) ) { + $found_rows = count( $this->last_result ); + } + return sprintf( "(SELECT %d) AS 'FOUND_ROWS()'", $found_rows ); + default: + return $this->translate_sequence( $node->get_children() ); + } + } + + /** + * Translate a MySQL datetime literal to SQLite. + * + * @param string $value The MySQL datetime literal. + * @return string The translated value. + */ + private function translate_datetime_literal( string $value ): string { + /* + * The code below converts the date format to one preferred by SQLite. + * + * MySQL accepts ISO 8601 date strings: 'YYYY-MM-DDTHH:MM:SSZ' + * SQLite prefers a slightly different format: 'YYYY-MM-DD HH:MM:SS' + * + * SQLite date and time functions can understand the ISO 8601 notation, but + * lookups don't. To keep the lookups working, we need to store all dates + * in UTC without the "T" and "Z" characters. + * + * Caveat: It will adjust every string that matches the pattern, not just dates. + * + * In theory, we could only adjust semantic dates, e.g. the data inserted + * to a date column or compared against a date column. + * + * In practice, this is hard because dates are just text – SQLite has no separate + * datetime field. We'd need to cache the MySQL data type from the original + * CREATE TABLE query and then keep refreshing the cache after each ALTER TABLE query. + * + * That's a lot of complexity that's perhaps not worth it. Let's just convert + * everything for now. The regexp assumes "Z" is always at the end of the string, + * which is true in the unit test suite, but there could also be a timezone offset + * like "+00:00" or "+01:00". We could add support for that later if needed. + */ + if ( 1 === preg_match( '/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})Z$/', $value, $matches ) ) { + $value = $matches[1] . ' ' . $matches[2]; + } + + /* + * Mimic MySQL's behavior and truncate invalid dates. + * + * "2020-12-41 14:15:27" becomes "0000-00-00 00:00:00" + * + * WARNING: We have no idea whether the truncated value should + * be treated as a date in the first place. + * In SQLite dates are just strings. This could be a perfectly + * valid string that just happens to contain a date-like value. + * + * At the same time, WordPress seems to rely on MySQL's behavior + * and even tests for it in Tests_Post_wpInsertPost::test_insert_empty_post_date. + * Let's truncate the dates for now. + * + * In the future, let's update WordPress to do its own date validation + * and stop relying on this MySQL feature, + */ + if ( 1 === preg_match( '/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})$/', $value, $matches ) ) { + /* + * Calling strtotime("0000-00-00 00:00:00") in 32-bit environments triggers + * an "out of integer range" warning – let's avoid that call for the popular + * case of "zero" dates. + */ + if ( '0000-00-00 00:00:00' !== $value && false === strtotime( $value ) ) { + $value = '0000-00-00 00:00:00'; + } + } + return $value; + } + + /** + * Translate a MySQL SHOW LIKE ... or SHOW WHERE ... condition to SQLite. + * + * @param WP_Parser_Node $like_or_where The "likeOrWhere" AST node. + * @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_clause = $like_or_where->get_first_child_node( 'likeClause' ); + if ( null !== $like_clause ) { + $value = $this->translate( + $like_clause->get_first_child_node( 'textStringLiteral' ) + ); + return sprintf( "AND table_name LIKE %s ESCAPE '\\'", $value ); + } + + $where_clause = $like_or_where->get_first_child_node( 'whereClause' ); + if ( null !== $where_clause ) { + $value = $this->translate( + $where_clause->get_first_child_node( 'expr' ) + ); + return sprintf( 'AND %s', $value ); + } + + return ''; + } + + + /** + * Generate a SQLite CREATE TABLE statement from information schema data. + * + * @param string $table_name The name of the table to create. + * @param string|null $new_table_name Override the original table name for ALTER TABLE emulation. + * @return string[] Queries to create the table, indexes, and constraints. + * @throws WP_SQLite_Driver_Exception When the table information is missing. + */ + private function get_sqlite_create_table_statement( string $table_name, ?string $new_table_name = null ): array { + // 1. Get table info. + $table_info = $this->execute_sqlite_query( + " + SELECT * + FROM _mysql_information_schema_tables + WHERE table_type = 'BASE TABLE' + AND table_schema = ? + AND table_name = ? + ", + array( $this->db_name, $table_name ) + )->fetch( PDO::FETCH_ASSOC ); + + if ( false === $table_info ) { + throw $this->new_driver_exception( + sprintf( 'Table "%s" not found in information schema', $table_name ) + ); + } + + // 2. Get column info. + $column_info = $this->execute_sqlite_query( + 'SELECT * FROM _mysql_information_schema_columns WHERE table_schema = ? AND table_name = ?', + array( $this->db_name, $table_name ) + )->fetchAll( PDO::FETCH_ASSOC ); + + // 3. Get index info, grouped by index name. + $constraint_info = $this->execute_sqlite_query( + 'SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = ? AND table_name = ?', + array( $this->db_name, $table_name ) + )->fetchAll( PDO::FETCH_ASSOC ); + + $grouped_constraints = array(); + foreach ( $constraint_info as $constraint ) { + $name = $constraint['INDEX_NAME']; + $seq = $constraint['SEQ_IN_INDEX']; + $grouped_constraints[ $name ][ $seq ] = $constraint; + } + + // 4. Generate CREATE TABLE statement columns. + $rows = array(); + $on_update_queries = array(); + $has_autoincrement = false; + foreach ( $column_info as $column ) { + $query = ' '; + $query .= $this->quote_sqlite_identifier( $column['COLUMN_NAME'] ); + + $type = self::DATA_TYPE_STRING_MAP[ $column['DATA_TYPE'] ]; + + /* + * In SQLite, there is a PRIMARY KEY quirk for backward compatibility. + * This applies to ROWID tables and single-column primary keys only: + * 1. "INTEGER PRIMARY KEY" creates an alias of ROWID. + * 2. "INT PRIMARY KEY" will not alias of ROWID. + * + * Therefore, we want to: + * 1. Use "INT PRIMARY KEY" when we have a single-column integer + * PRIMARY KEY without AUTOINCREMENT (to avoid the ROWID alias). + * 2. Use "INTEGER PRIMARY KEY" otherwise. + * + * See: + * - https://www.sqlite.org/autoinc.html + * - https://www.sqlite.org/lang_createtable.html + */ + if ( + 'INTEGER' === $type + && 'PRI' === $column['COLUMN_KEY'] + && 'auto_increment' !== $column['EXTRA'] + && count( $grouped_constraints['PRIMARY'] ) === 1 + ) { + $type = 'INT'; + } + + $query .= ' ' . $type; + + // In MySQL, text fields are case-insensitive by default. + // COLLATE NOCASE emulates the same behavior in SQLite. + // @TODO: Respect the actual column and index collation. + if ( 'TEXT' === $type ) { + $query .= ' COLLATE NOCASE'; + } + if ( 'NO' === $column['IS_NULLABLE'] ) { + $query .= ' NOT NULL'; + } + if ( 'auto_increment' === $column['EXTRA'] ) { + $has_autoincrement = true; + $query .= ' PRIMARY KEY AUTOINCREMENT'; + } + if ( null !== $column['COLUMN_DEFAULT'] ) { + // @TODO: Handle defaults with expression values (DEFAULT_GENERATED). + + // Handle DEFAULT CURRENT_TIMESTAMP. This works only with timestamp + // and datetime columns. For other column types, it's just a string. + if ( + 'CURRENT_TIMESTAMP' === $column['COLUMN_DEFAULT'] + && ( 'timestamp' === $column['DATA_TYPE'] || 'datetime' === $column['DATA_TYPE'] ) + ) { + $query .= ' DEFAULT CURRENT_TIMESTAMP'; + } else { + $query .= ' DEFAULT ' . $this->pdo->quote( $column['COLUMN_DEFAULT'] ); + } + } + $rows[] = $query; + + if ( 'on update CURRENT_TIMESTAMP' === $column['EXTRA'] ) { + $on_update_queries[] = $this->get_column_on_update_trigger_query( + $table_name, + $column['COLUMN_NAME'] + ); + } + } + + // 5. Generate CREATE TABLE statement constraints, collect indexes. + $create_index_queries = array(); + foreach ( $grouped_constraints as $constraint ) { + ksort( $constraint ); + $info = $constraint[1]; + + if ( 'PRIMARY' === $info['INDEX_NAME'] ) { + if ( $has_autoincrement ) { + continue; + } + $query = ' PRIMARY KEY ('; + $query .= implode( + ', ', + array_map( + function ( $column ) { + return $this->quote_sqlite_identifier( $column['COLUMN_NAME'] ); + }, + $constraint + ) + ); + $query .= ')'; + $rows[] = $query; + } else { + $is_unique = '0' === $info['NON_UNIQUE']; + + // Prefix the original index name with the table name. + // This is to avoid conflicting index names in SQLite. + $index_name = $this->quote_sqlite_identifier( + $table_name . '__' . $info['INDEX_NAME'] + ); + + $query = sprintf( + 'CREATE %sINDEX %s ON %s (', + $is_unique ? 'UNIQUE ' : '', + $index_name, + $this->quote_sqlite_identifier( $table_name ) + ); + $query .= implode( + ', ', + array_map( + function ( $column ) { + return $this->quote_sqlite_identifier( $column['COLUMN_NAME'] ); + }, + $constraint + ) + ); + $query .= ')'; + + $create_index_queries[] = $query; + } + } + + // 6. Compose the CREATE TABLE statement. + $create_table_query = sprintf( + "CREATE TABLE %s (\n", + $this->quote_sqlite_identifier( $new_table_name ?? $table_name ) + ); + $create_table_query .= implode( ",\n", $rows ); + $create_table_query .= "\n) STRICT"; + return array_merge( array( $create_table_query ), $create_index_queries, $on_update_queries ); + } + + /** + * Generate a MySQL CREATE TABLE statement from information schema data. + * + * @param string $table_name The name of the table to create. + * @return string The CREATE TABLE statement. + */ + private function get_mysql_create_table_statement( string $table_name ): ?string { + // 1. Get table info. + $table_info = $this->execute_sqlite_query( + " + SELECT * + FROM _mysql_information_schema_tables + WHERE table_type = 'BASE TABLE' + AND table_schema = ? + AND table_name = ? + ", + array( $this->db_name, $table_name ) + )->fetch( PDO::FETCH_ASSOC ); + + if ( false === $table_info ) { + return null; + } + + // 2. Get column info. + $column_info = $this->execute_sqlite_query( + 'SELECT * FROM _mysql_information_schema_columns WHERE table_schema = ? AND table_name = ?', + array( $this->db_name, $table_name ) + )->fetchAll( PDO::FETCH_ASSOC ); + + // 3. Get index info, grouped by index name. + $constraint_info = $this->execute_sqlite_query( + 'SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = ? AND table_name = ?', + array( $this->db_name, $table_name ) + )->fetchAll( PDO::FETCH_ASSOC ); + + $grouped_constraints = array(); + foreach ( $constraint_info as $constraint ) { + $name = $constraint['INDEX_NAME']; + $seq = $constraint['SEQ_IN_INDEX']; + $grouped_constraints[ $name ][ $seq ] = $constraint; + } + + // 4. Generate CREATE TABLE statement columns. + $rows = array(); + foreach ( $column_info as $column ) { + $sql = ' '; + $sql .= $this->quote_mysql_identifier( $column['COLUMN_NAME'] ); + $sql .= ' ' . $column['COLUMN_TYPE']; + if ( 'NO' === $column['IS_NULLABLE'] ) { + $sql .= ' NOT NULL'; + } elseif ( 'timestamp' === $column['COLUMN_TYPE'] ) { + // Nullable "timestamp" columns dump NULL explicitly. + $sql .= ' NULL'; + } + if ( 'auto_increment' === $column['EXTRA'] ) { + $sql .= ' AUTO_INCREMENT'; + } + + // Handle DEFAULT CURRENT_TIMESTAMP. This works only with timestamp + // and datetime columns. For other column types, it's just a string. + if ( + 'CURRENT_TIMESTAMP' === $column['COLUMN_DEFAULT'] + && ( 'timestamp' === $column['DATA_TYPE'] || 'datetime' === $column['DATA_TYPE'] ) + ) { + $sql .= ' DEFAULT CURRENT_TIMESTAMP'; + } elseif ( null !== $column['COLUMN_DEFAULT'] ) { + $sql .= ' DEFAULT ' . $this->pdo->quote( $column['COLUMN_DEFAULT'] ); + } elseif ( 'YES' === $column['IS_NULLABLE'] ) { + $sql .= ' DEFAULT NULL'; + } + + // Handle ON UPDATE CURRENT_TIMESTAMP. + if ( str_contains( $column['EXTRA'], 'on update CURRENT_TIMESTAMP' ) ) { + $sql .= ' ON UPDATE CURRENT_TIMESTAMP'; + } + + $rows[] = $sql; + } + + // 4. Generate CREATE TABLE statement constraints, collect indexes. + foreach ( $grouped_constraints as $constraint ) { + ksort( $constraint ); + $info = $constraint[1]; + + if ( 'PRIMARY' === $info['INDEX_NAME'] ) { + $sql = ' PRIMARY KEY ('; + $sql .= implode( + ', ', + array_map( + function ( $column ) { + return $this->quote_mysql_identifier( $column['COLUMN_NAME'] ); + }, + $constraint + ) + ); + $sql .= ')'; + $rows[] = $sql; + } else { + $is_unique = '0' === $info['NON_UNIQUE']; + + $sql = sprintf( ' %sKEY ', $is_unique ? 'UNIQUE ' : '' ); + $sql .= $this->quote_mysql_identifier( $info['INDEX_NAME'] ); + $sql .= ' ('; + $sql .= implode( + ', ', + array_map( + function ( $column ) { + return $this->quote_mysql_identifier( $column['COLUMN_NAME'] ); + }, + $constraint + ) + ); + $sql .= ')'; + + $rows[] = $sql; + } + } + + // 5. Compose the CREATE TABLE statement. + $collation = $table_info['TABLE_COLLATION']; + $charset = substr( $collation, 0, strpos( $collation, '_' ) ); + + $sql = sprintf( "CREATE TABLE %s (\n", $this->quote_mysql_identifier( $table_name ) ); + $sql .= implode( ",\n", $rows ); + $sql .= "\n)"; + $sql .= sprintf( ' ENGINE=%s', $table_info['ENGINE'] ); + $sql .= sprintf( ' DEFAULT CHARSET=%s', $charset ); + $sql .= sprintf( ' COLLATE=%s', $collation ); + return $sql; + } + + + /** + * Get an SQLite query to emulate MySQL "ON UPDATE CURRENT_TIMESTAMP". + * + * In SQLite, "ON UPDATE CURRENT_TIMESTAMP" is not supported. We need to + * create a trigger to emulate this behavior. + * + * @param string $table The table name. + * @param string $column The column name. + */ + private function get_column_on_update_trigger_query( string $table, string $column ): string { + // The trigger wouldn't work for virtual and "WITHOUT ROWID" tables, + // but currently that can't happen as we're not creating such tables. + // See: https://www.sqlite.org/rowidtable.html + $trigger_name = self::RESERVED_PREFIX . "{$table}_{$column}_on_update"; + return " + CREATE TRIGGER \"$trigger_name\" + AFTER UPDATE ON \"$table\" + FOR EACH ROW + BEGIN + UPDATE \"$table\" SET \"$column\" = CURRENT_TIMESTAMP WHERE rowid = NEW.rowid; + END + "; + } + + /** + * Unquote a quoted SQLite identifier. + * + * Remove bounding quotes and replace escaped quotes with their values. + * + * @param string $quoted_identifier The quoted identifier value. + * @return string The unquoted identifier value. + */ + private function unquote_sqlite_identifier( string $quoted_identifier ): string { + $first_byte = $quoted_identifier[0] ?? null; + if ( '"' === $first_byte || '`' === $first_byte ) { + $unquoted = substr( $quoted_identifier, 1, -1 ); + } else { + $unquoted = $quoted_identifier; + } + return str_replace( $first_byte . $first_byte, $first_byte, $unquoted ); + } + + /** + * Quote an SQLite identifier. + * + * Wrap the identifier in backticks and escape backtick values within. + * + * @param string $unquoted_identifier The unquoted identifier value. + * @return string The quoted identifier value. + */ + private function quote_sqlite_identifier( string $unquoted_identifier ): string { + return '`' . str_replace( '`', '``', $unquoted_identifier ) . '`'; + } + + + /** + * Quote a MySQL identifier. + * + * Wrap the identifier in backticks and escape backtick values within. + * + * @param string $unquoted_identifier The unquoted identifier value. + * @return string The quoted identifier value. + */ + private function quote_mysql_identifier( string $unquoted_identifier ): string { + return '`' . str_replace( '`', '``', $unquoted_identifier ) . '`'; + } + + /** + * Clear the state of the driver. + */ + private function flush(): void { + $this->last_mysql_query = ''; + $this->last_sqlite_queries = array(); + $this->last_result = null; + $this->last_return_value = null; + } + + /** + * Set results of a query() call using fetched data. + * + * @param array $data The data to set. + */ + private function set_results_from_fetched_data( array $data ): void { + $this->last_result = $data; + $this->last_return_value = $this->last_result; + } + + /** + * Set results of a query() call using the number of affected rows. + * + * @param int|null $override Override the affected rows. + */ + private function set_result_from_affected_rows( int $override = null ): void { + /* + * SELECT CHANGES() is a workaround for the fact that $stmt->rowCount() + * returns "0" (zero) with the SQLite driver at all times. + * See: https://www.php.net/manual/en/pdostatement.rowcount.php + */ + if ( null === $override ) { + $affected_rows = (int) $this->execute_sqlite_query( 'SELECT CHANGES()' )->fetch()[0]; + } else { + $affected_rows = $override; + } + $this->last_result = $affected_rows; + $this->last_return_value = $affected_rows; + } + + /** + * Create a new SQLite driver exception. + * + * @param string $message The exception message. + * @param int $code The exception code. + * @param Throwable|null $previous The previous exception. + * @return WP_SQLite_Driver_Exception + */ + private function new_driver_exception( + string $message, + int $code = 0, + Throwable $previous = null + ): WP_SQLite_Driver_Exception { + return new WP_SQLite_Driver_Exception( $this, $message, $code, $previous ); + } + + /** + * Create a new invalid input exception. + * + * This exception can be used to mark cases that should never occur according + * to the MySQL grammar. It may serve as an assertion that should never fail. + * + * @return WP_SQLite_Driver_Exception + */ + private function new_invalid_input_exception(): WP_SQLite_Driver_Exception { + return new WP_SQLite_Driver_Exception( $this, 'MySQL query syntax error.' ); + } + + /** + * Create a new not supported exception. + * + * This exception can be used to mark MySQL constructs that are not supported. + * + * @param string $cause The cause, indicating which construct is not supported. + * @return WP_SQLite_Driver_Exception + */ + private function new_not_supported_exception( string $cause ): WP_SQLite_Driver_Exception { + return new WP_SQLite_Driver_Exception( + $this, + sprintf( 'MySQL query not supported. Cause: %s', $cause ) + ); + } +} diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php new file mode 100644 index 0000000..ac4322a --- /dev/null +++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php @@ -0,0 +1,1812 @@ + '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', + ); + + /** + * The default collation for each MySQL charset. + * This is needed as collation is not always specified in a query. + */ + const CHARSET_DEFAULT_COLLATION_MAP = array( + 'armscii8' => 'armscii8_general_ci', + 'ascii' => 'ascii_general_ci', + 'big5' => 'big5_chinese_ci', + 'binary' => 'binary', + 'cp1250' => 'cp1250_general_ci', + 'cp1251' => 'cp1251_general_ci', + 'cp1256' => 'cp1256_general_ci', + 'cp1257' => 'cp1257_general_ci', + 'cp850' => 'cp850_general_ci', + 'cp852' => 'cp852_general_ci', + 'cp866' => 'cp866_general_ci', + 'cp932' => 'cp932_japanese_ci', + 'dec8' => 'dec8_swedish_ci', + 'eucjpms' => 'eucjpms_japanese_ci', + 'euckr' => 'euckr_korean_ci', + 'gb18030' => 'gb18030_chinese_ci', + 'gb2312' => 'gb2312_chinese_ci', + 'gbk' => 'gbk_chinese_ci', + 'geostd8' => 'geostd8_general_ci', + 'greek' => 'greek_general_ci', + 'hebrew' => 'hebrew_general_ci', + 'hp8' => 'hp8_english_ci', + 'keybcs2' => 'keybcs2_general_ci', + 'koi8r' => 'koi8r_general_ci', + 'koi8u' => 'koi8u_general_ci', + 'latin1' => 'latin1_swedish_ci', + 'latin2' => 'latin2_general_ci', + 'latin5' => 'latin5_turkish_ci', + 'latin7' => 'latin7_general_ci', + 'macce' => 'macce_general_ci', + 'macroman' => 'macroman_general_ci', + 'sjis' => 'sjis_japanese_ci', + 'swe7' => 'swe7_swedish_ci', + 'tis620' => 'tis620_thai_ci', + 'ucs2' => 'ucs2_general_ci', + 'ujis' => 'ujis_japanese_ci', + 'utf16' => 'utf16_general_ci', + 'utf16le' => 'utf16le_general_ci', + 'utf32' => 'utf32_general_ci', + 'utf8' => 'utf8_general_ci', + 'utf8mb4' => 'utf8mb4_general_ci', // @TODO: From MySQL 8.0.1, this is utf8mb4_0900_ai_ci. + ); + + /** + * Maximum number of bytes per character for each charset. + * The map contains only multi-byte charsets. + * Charsets that are not included are single-byte. + */ + const CHARSET_MAX_BYTES_MAP = array( + 'big5' => 2, + 'cp932' => 2, + 'eucjpms' => 3, + 'euckr' => 2, + 'gb18030' => 4, + 'gb2312' => 2, + 'gbk' => 2, + 'sjis' => 2, + 'ucs2' => 2, + 'ujis' => 3, + 'utf16' => 4, + 'utf16le' => 4, + 'utf32' => 4, + 'utf8' => 3, + 'utf8mb4' => 4, + ); + + /** + * Database name. + * + * @TODO: Consider passing the database name as a parameter to each method. + * This would expose an API that could support multiple databases + * in the future. Alternatively, it could be a stateful property. + * + * @var string + */ + private $db_name; + + /** + * Query callback. + * + * @TODO: Consider extracting a part of the WP_SQLite_Driver class + * to a class like "WP_SQLite_Connection" and reuse it in both. + * + * @var callable(string, array): PDOStatement + */ + private $query_callback; + + /** + * Constructor. + * + * @param string $database Database name. + * @param callable(string, array): PDOStatement $query_callback A callback that executes an SQLite query. + */ + public function __construct( string $database, callable $query_callback ) { + $this->db_name = $database; + $this->query_callback = $query_callback; + } + + /** + * Ensure that the information schema tables exist in the SQLite + * database. Tables that are missing will be created. + */ + public function ensure_information_schema_tables(): void { + foreach ( self::CREATE_INFORMATION_SCHEMA_QUERIES as $query ) { + $this->query( $query ); + } + } + + /** + * Analyze CREATE TABLE statement and record data in the information schema. + * + * @param WP_Parser_Node $node The "createStatement" AST node with "createTable" child. + */ + public function record_create_table( WP_Parser_Node $node ): void { + $table_name = $this->get_value( $node->get_first_descendant_node( 'tableName' ) ); + $table_engine = $this->get_table_engine( $node ); + $table_row_format = 'MyISAM' === $table_engine ? 'FIXED' : 'DYNAMIC'; + $table_collation = $this->get_table_collation( $node ); + + // 1. Table. + $this->insert_values( + '_mysql_information_schema_tables', + array( + 'table_schema' => $this->db_name, + 'table_name' => $table_name, + 'table_type' => 'BASE TABLE', + 'engine' => $table_engine, + 'row_format' => $table_row_format, + 'table_collation' => $table_collation, + ) + ); + + // 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( 'columnName' ) ); + + // Column definition. + $column_data = $this->extract_column_data( + $table_name, + $column_name, + $column_node, + $column_position + ); + $this->insert_values( '_mysql_information_schema_columns', $column_data ); + + // Inline column constraint. + $column_constraint_data = $this->extract_column_constraint_data( + $table_name, + $column_name, + $column_node, + 'YES' === $column_data['is_nullable'] + ); + if ( null !== $column_constraint_data ) { + $this->insert_values( + '_mysql_information_schema_statistics', + $column_constraint_data + ); + } + + $column_position += 1; + } + + // 3. Constraints. + foreach ( $node->get_descendant_nodes( 'tableConstraintDef' ) as $constraint_node ) { + $this->record_add_constraint( $table_name, $constraint_node ); + } + } + + /** + * Analyze ALTER TABLE statement and record data in the information schema. + * + * @param WP_Parser_Node $node The "alterStatement" AST node with "alterTable" child. + */ + public function record_alter_table( WP_Parser_Node $node ): void { + $table_name = $this->get_value( $node->get_first_descendant_node( 'tableRef' ) ); + $actions = $node->get_descendant_nodes( 'alterListItem' ); + + foreach ( $actions as $action ) { + $first_token = $action->get_first_child_token(); + + // ADD + if ( WP_MySQL_Lexer::ADD_SYMBOL === $first_token->id ) { + // ADD [COLUMN] (...[, ...]) + $column_definitions = $action->get_descendant_nodes( 'columnDefinition' ); + if ( count( $column_definitions ) > 0 ) { + foreach ( $column_definitions as $column_definition ) { + $name = $this->get_value( $column_definition->get_first_child_node( 'identifier' ) ); + $this->record_add_column( $table_name, $name, $column_definition ); + } + continue; + } + + // ADD [COLUMN] ... + $field_definition = $action->get_first_descendant_node( 'fieldDefinition' ); + if ( null !== $field_definition ) { + $name = $this->get_value( $action->get_first_child_node( 'identifier' ) ); + $this->record_add_column( $table_name, $name, $field_definition ); + // @TODO: Handle FIRST/AFTER. + continue; + } + + // ADD CONSTRAINT. + $constraint = $action->get_first_descendant_node( 'tableConstraintDef' ); + if ( null !== $constraint ) { + $this->record_add_constraint( $table_name, $constraint ); + continue; + } + + throw new \Exception( sprintf( 'Unsupported ALTER TABLE ADD action: %s', $first_token->value ) ); + } + + // CHANGE [COLUMN] + if ( WP_MySQL_Lexer::CHANGE_SYMBOL === $first_token->id ) { + $old_name = $this->get_value( $action->get_first_child_node( 'columnInternalRef' ) ); + $new_name = $this->get_value( $action->get_first_child_node( 'identifier' ) ); + $this->record_change_column( + $table_name, + $old_name, + $new_name, + $action->get_first_descendant_node( 'fieldDefinition' ) + ); + continue; + } + + // MODIFY [COLUMN] + if ( WP_MySQL_Lexer::MODIFY_SYMBOL === $first_token->id ) { + $name = $this->get_value( $action->get_first_child_node( 'columnInternalRef' ) ); + $this->record_modify_column( + $table_name, + $name, + $action->get_first_descendant_node( 'fieldDefinition' ) + ); + continue; + } + + // DROP + if ( WP_MySQL_Lexer::DROP_SYMBOL === $first_token->id ) { + // DROP [COLUMN] + $column_ref = $action->get_first_child_node( 'columnInternalRef' ); + if ( null !== $column_ref ) { + $name = $this->get_value( $column_ref ); + $this->record_drop_column( $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( $table_name, $name ); + continue; + } + } + } + } + + /** + * Analyze DROP TABLE statement and record data in the information schema. + * + * @param WP_Parser_Node $node The "dropStatement" AST node with "dropTable" child. + */ + public function record_drop_table( WP_Parser_Node $node ): void { + $child_node = $node->get_first_child_node(); + if ( $child_node->has_child_token( WP_MySQL_Lexer::TEMPORARY_SYMBOL ) ) { + return; + } + + $table_refs = $child_node->get_first_child_node( 'tableRefList' )->get_child_nodes(); + foreach ( $table_refs as $table_ref ) { + $table_name = $this->get_value( $table_ref ); + $this->delete_values( + '_mysql_information_schema_tables', + array( + 'table_schema' => $this->db_name, + 'table_name' => $table_name, + ) + ); + $this->delete_values( + '_mysql_information_schema_columns', + array( + 'table_schema' => $this->db_name, + 'table_name' => $table_name, + ) + ); + $this->delete_values( + '_mysql_information_schema_statistics', + array( + 'table_schema' => $this->db_name, + 'table_name' => $table_name, + ) + ); + } + + // @TODO: RESTRICT vs. CASCADE + } + + /** + * Analyze ADD COLUMN definition and record data in the information schema. + * + * @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. + */ + private function record_add_column( string $table_name, string $column_name, WP_Parser_Node $node ): void { + $position = $this->query( + ' + SELECT MAX(ordinal_position) + FROM _mysql_information_schema_columns + WHERE table_schema = ? + AND table_name = ? + ', + array( $this->db_name, $table_name ) + )->fetchColumn(); + + $column_data = $this->extract_column_data( $table_name, $column_name, $node, (int) $position + 1 ); + $this->insert_values( '_mysql_information_schema_columns', $column_data ); + + $column_constraint_data = $this->extract_column_constraint_data( $table_name, $column_name, $node, true ); + if ( null !== $column_constraint_data ) { + $this->insert_values( '_mysql_information_schema_statistics', $column_constraint_data ); + } + } + + /** + * Analyze CHANGE COLUMN definition and record data in the information schema. + * + * @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. + */ + private function record_change_column( + string $table_name, + string $column_name, + string $new_column_name, + WP_Parser_Node $node + ): void { + $column_data = $this->extract_column_data( $table_name, $new_column_name, $node, 0 ); + $this->update_values( + '_mysql_information_schema_columns', + $column_data, + array( + 'table_schema' => $this->db_name, + 'table_name' => $table_name, + 'column_name' => $column_name, + ) + ); + + // Update column name in statistics, if it has changed. + if ( $new_column_name !== $column_name ) { + $this->update_values( + '_mysql_information_schema_statistics', + array( + 'column_name' => $new_column_name, + ), + array( + 'table_schema' => $this->db_name, + 'table_name' => $table_name, + 'column_name' => $column_name, + ) + ); + } + + // Handle inline constraints. When inline constraint is defined, MySQL + // always adds a new constraint rather than replacing an existing one. + $column_constraint_data = $this->extract_column_constraint_data( + $table_name, + $new_column_name, + $node, + 'YES' === $column_data['is_nullable'] + ); + if ( null !== $column_constraint_data ) { + $this->insert_values( '_mysql_information_schema_statistics', $column_constraint_data ); + $this->sync_column_key_info( $table_name ); + } + } + + /** + * Analyze MODIFY COLUMN definition and record data in the information schema. + * + * @param string $table_name The table name. + * @param string $column_name The column name. + * @param WP_Parser_Node $node The "fieldDefinition" AST node. + */ + private function record_modify_column( + string $table_name, + string $column_name, + WP_Parser_Node $node + ): void { + $this->record_change_column( $table_name, $column_name, $column_name, $node ); + } + + /** + * Record DROP COLUMN data in the information schema. + * + * @param string $table_name The table name. + * @param string $column_name The column name. + */ + private function record_drop_column( $table_name, $column_name ): void { + $this->delete_values( + '_mysql_information_schema_columns', + array( + 'table_schema' => $this->db_name, + 'table_name' => $table_name, + 'column_name' => $column_name, + ) + ); + + /** + * From MySQL documentation: + * + * If columns are dropped from a table, the columns are also removed + * from any index of which they are a part. If all columns that make up + * an index are dropped, the index is dropped as well. + * + * This means we need to remove the records from the STATISTICS table, + * renumber the SEQ_IN_INDEX values, and resync the column key info. + * + * See: + * - https://dev.mysql.com/doc/refman/8.4/en/alter-table.html + */ + $this->delete_values( + '_mysql_information_schema_statistics', + array( + 'table_schema' => $this->db_name, + 'table_name' => $table_name, + 'column_name' => $column_name, + ) + ); + + // @TODO: Renumber SEQ_IN_INDEX values. + + $this->sync_column_key_info( $table_name ); + } + + /** + * Record DROP INDEX data in the information schema. + * + * @param string $table_name The table name. + * @param string $index_name The index name. + */ + private function record_drop_index( string $table_name, string $index_name ): void { + $this->delete_values( + '_mysql_information_schema_statistics', + array( + 'table_schema' => $this->db_name, + 'table_name' => $table_name, + 'index_name' => $index_name, + ) + ); + $this->sync_column_key_info( $table_name ); + } + + /** + * Analyze ADD CONSTRAINT definition and record data in the information schema. + * + * @param string $table_name The table name. + * @param WP_Parser_Node $node The "tableConstraintDef" AST node. + */ + private function record_add_constraint( string $table_name, WP_Parser_Node $node ): void { + // Get first constraint 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(); + } + + if ( + WP_MySQL_Lexer::FOREIGN_SYMBOL === $keyword->id + || WP_MySQL_Lexer::CHECK_SYMBOL === $keyword->id + ) { + throw new \Exception( 'FOREIGN KEY and CHECK constraints are not supported yet.' ); + } + + // Get key parts. + $key_list = $node->get_first_child_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 index column names. + $key_part_column_names = array(); + foreach ( $key_parts as $key_part ) { + $key_part_column_names[] = $this->get_index_column_name( $key_part ); + } + + // Fetch column info. + $column_names = array_filter( $key_part_column_names ); + if ( count( $column_names ) > 0 ) { + $column_info = $this->query( + ' + SELECT column_name, data_type, is_nullable, character_maximum_length + FROM _mysql_information_schema_columns + WHERE table_schema = ? + AND table_name = ? + AND column_name IN (' . implode( ',', array_fill( 0, count( $column_names ), '?' ) ) . ') + ', + array_merge( array( $this->db_name, $table_name ), $column_names ) + )->fetchAll( + PDO::FETCH_ASSOC // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO + ); + } else { + $column_info = array(); + } + + $column_info_map = array_combine( + array_column( $column_info, 'COLUMN_NAME' ), + $column_info + ); + + // Get first index column data type (needed for index type). + $first_column_name = $this->get_index_column_name( $key_parts[0] ); + $first_column_type = $column_info_map[ $first_column_name ]['DATA_TYPE'] ?? null; + $has_spatial_column = null !== $first_column_type && $this->is_spatial_data_type( $first_column_type ); + + $non_unique = $this->get_index_non_unique( $keyword ); + $index_name = $this->get_index_name( $node ); + $index_type = $this->get_index_type( $node, $keyword, $has_spatial_column ); + + $seq_in_index = 1; + foreach ( $key_parts as $i => $key_part ) { + $column_name = $key_part_column_names[ $i ]; + $collation = $this->get_index_column_collation( $key_part, $index_type ); + if ( + 'PRIMARY' === $index_name + || 'NO' === $column_info_map[ $column_name ]['IS_NULLABLE'] + ) { + $nullable = ''; + } else { + $nullable = 'YES'; + } + + $sub_part = $this->get_index_column_sub_part( + $key_part, + $column_info_map[ $column_name ]['CHARACTER_MAXIMUM_LENGTH'], + $has_spatial_column + ); + + $this->insert_values( + '_mysql_information_schema_statistics', + array( + 'table_schema' => $this->db_name, + 'table_name' => $table_name, + 'non_unique' => $non_unique, + 'index_schema' => $this->db_name, + 'index_name' => $index_name, + 'seq_in_index' => $seq_in_index, + 'column_name' => $column_name, + 'collation' => $collation, + 'cardinality' => 0, // not implemented + 'sub_part' => $sub_part, + 'packed' => null, // not implemented + 'nullable' => $nullable, + 'index_type' => $index_type, + 'comment' => '', // not implemented + 'index_comment' => '', // @TODO + 'is_visible' => 'YES', // @TODO: Save actual visibility value. + 'expression' => null, // @TODO + ) + ); + + $seq_in_index += 1; + } + + $this->sync_column_key_info( $table_name ); + } + + /** + * Analyze "columnDefinition" or "fieldDefinition" 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 int $position The ordinal position of the column in the table. + * @return array Column data for the information schema. + */ + private function extract_column_data( string $table_name, string $column_name, WP_Parser_Node $node, int $position ): array { + $default = $this->get_column_default( $node ); + $nullable = $this->get_column_nullable( $node ); + $key = $this->get_column_key( $node ); + $extra = $this->get_column_extra( $node ); + $comment = $this->get_column_comment( $node ); + + list ( $data_type, $column_type ) = $this->get_column_data_types( $node ); + list ( $charset, $collation ) = $this->get_column_charset_and_collation( $node, $data_type ); + list ( $char_length, $octet_length ) = $this->get_column_lengths( $node, $data_type, $charset ); + list ( $precision, $scale ) = $this->get_column_numeric_attributes( $node, $data_type ); + $datetime_precision = $this->get_column_datetime_precision( $node, $data_type ); + $generation_expression = $this->get_column_generation_expression( $node ); + + return array( + 'table_schema' => $this->db_name, + 'table_name' => $table_name, + 'column_name' => $column_name, + 'ordinal_position' => $position, + 'column_default' => $default, + 'is_nullable' => $nullable, + 'data_type' => $data_type, + 'character_maximum_length' => $char_length, + 'character_octet_length' => $octet_length, + 'numeric_precision' => $precision, + 'numeric_scale' => $scale, + 'datetime_precision' => $datetime_precision, + 'character_set_name' => $charset, + 'collation_name' => $collation, + 'column_type' => $column_type, + 'column_key' => $key, + 'extra' => $extra, + 'privileges' => 'select,insert,update,references', + 'column_comment' => $comment, + 'generation_expression' => $generation_expression, + 'srs_id' => null, // not implemented + ); + } + + /** + * Analyze "columnDefinition" or "fieldDefinition" 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 bool $nullable Whether the column is nullable. + * @return array|null Constraint data for the information schema. + */ + private function extract_column_constraint_data( string $table_name, string $column_name, WP_Parser_Node $node, 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 ); + if ( $has_inline_primary_key || $has_inline_unique_key ) { + $index_name = $has_inline_primary_key ? 'PRIMARY' : $column_name; + return array( + 'table_schema' => $this->db_name, + 'table_name' => $table_name, + 'non_unique' => 0, + 'index_schema' => $this->db_name, + 'index_name' => $index_name, + 'seq_in_index' => 1, + 'column_name' => $column_name, + 'collation' => 'A', + 'cardinality' => 0, // not implemented + 'sub_part' => null, + 'packed' => null, // not implemented + 'nullable' => true === $nullable ? 'YES' : '', + 'index_type' => 'BTREE', + 'comment' => '', // not implemented + 'index_comment' => '', // @TODO + 'is_visible' => 'YES', // @TODO: Save actual visibility value. + 'expression' => null, // @TODO + ); + } + return null; + } + + /** + * Update column info from constraint data in the statistics table. + * + * When constraints are added or removed, we need to reflect the changes + * in the "COLUMN_KEY" and "IS_NULLABLE" columns of the "COLUMNS" table. + * + * A) COLUMN_KEY (priority from 1 to 4): + * 1. "PRI": Column is any component of a PRIMARY KEY. + * 2. "UNI": Column is the first column of a UNIQUE KEY. + * 3. "MUL": Column is the first column of a non-unique index. + * 4. "": Column is not indexed. + * + * B) IS_NULLABLE: In COLUMNS, "YES"/"NO". In STATISTICS, "YES"/"". + */ + private function sync_column_key_info( string $table_name ): void { + // @TODO: Consider listing only affected columns. + $this->query( + " + WITH s AS ( + SELECT + column_name, + CASE + WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' + WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' + WHEN MAX(seq_in_index = 1) THEN 'MUL' + ELSE '' + END AS column_key + FROM _mysql_information_schema_statistics + WHERE table_schema = ? + AND table_name = ? + GROUP BY column_name + ) + UPDATE _mysql_information_schema_columns AS c + SET + column_key = s.column_key, + is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) + FROM s + WHERE c.table_schema = ? + AND c.table_name = ? + AND s.column_name = c.column_name + ", + array( $this->db_name, $table_name, $this->db_name, $table_name ) + ); + } + + /** + * Extract table engine value from the "createStatement" AST node. + * + * @param WP_Parser_Node $node The "createStatement" AST node with "createTable" child. + * @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' ); + if ( null === $engine_node ) { + return 'InnoDB'; + } + + $engine = strtoupper( $this->get_value( $engine_node ) ); + if ( 'INNODB' === $engine ) { + return 'InnoDB'; + } elseif ( 'MYISAM' === $engine ) { + return 'MyISAM'; + } + return $engine; + } + + /** + * Extract table collation value from the "createStatement" AST node. + * + * @param WP_Parser_Node $node The "createStatement" AST node with "createTable" child. + * @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' ); + if ( null === $collate_node ) { + // @TODO: Use default DB collation or DB_CHARSET & DB_COLLATE. + return 'utf8mb4_general_ci'; + } + return strtolower( $this->get_value( $collate_node ) ); + } + + /** + * Extract column default value from the "columnDefinition" or "fieldDefinition" AST node. + * + * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" 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 ) ) { + $default_attr = $attr; + } + } + + if ( null === $default_attr ) { + return null; + } + + if ( $default_attr->has_child_token( WP_MySQL_Lexer::NOW_SYMBOL ) ) { + return 'CURRENT_TIMESTAMP'; + } + + if ( + $default_attr->has_child_node( 'signedLiteral' ) + && null !== $default_attr->get_first_descendant_node( 'nullLiteral' ) + ) { + return null; + } + + // @TODO: MySQL seems to normalize default values for numeric + // columns, such as 1.0 to 1, 1e3 to 1000, etc. + return $this->get_value( $default_attr->get_first_child_node() ); + } + + /** + * Extract column nullability from the "columnDefinition" or "fieldDefinition" AST node. + * + * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" 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 ) ) { + return 'NO'; + } + + foreach ( $node->get_descendant_nodes( 'columnAttribute' ) as $attr ) { + // PRIMARY KEY columns are always NOT NULL. + if ( $attr->has_child_token( WP_MySQL_Lexer::KEY_SYMBOL ) ) { + return 'NO'; + } + + // Check for NOT NULL attribute. + if ( + $attr->has_child_token( WP_MySQL_Lexer::NOT_SYMBOL ) + && $attr->has_child_node( 'nullLiteral' ) + ) { + return 'NO'; + } + } + return 'YES'; + } + + /** + * Extract column key info from the "columnDefinition" or "fieldDefinition" AST node. + * + * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" 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 ) + ) { + 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 ) ) { + return 'PRI'; + } + + // 2. UNI: Column has UNIQUE constraint. + if ( null !== $node->get_first_descendant_token( WP_MySQL_Lexer::UNIQUE_SYMBOL ) ) { + return 'UNI'; + } + + // 3. MUL: Column has INDEX. + if ( null !== $node->get_first_descendant_token( WP_MySQL_Lexer::INDEX_SYMBOL ) ) { + return 'MUL'; + } + + return ''; + } + + /** + * Extract column extra from the "columnDefinition" or "fieldDefinition" AST node. + * + * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" 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' ); + + // SERIAL + $data_type = $node->get_first_descendant_node( 'dataType' ); + if ( null !== $data_type->get_first_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL ) ) { + return 'auto_increment'; + } + + // Check whether DEFAULT value is an expression. + foreach ( $attributes as $attr ) { + if ( + $attr->has_child_token( WP_MySQL_Lexer::DEFAULT_SYMBOL ) + && $attr->has_child_node( 'exprWithParentheses' ) + ) { + $extras[] = 'DEFAULT_GENERATED'; + } + } + + foreach ( $attributes as $attr ) { + if ( $attr->has_child_token( WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL ) ) { + return 'auto_increment'; + } + if ( + $attr->has_child_token( WP_MySQL_Lexer::ON_SYMBOL ) + && $attr->has_child_token( WP_MySQL_Lexer::UPDATE_SYMBOL ) + ) { + $extras[] = 'on update CURRENT_TIMESTAMP'; + } + } + + if ( $node->get_first_descendant_token( WP_MySQL_Lexer::VIRTUAL_SYMBOL ) ) { + $extras[] = 'VIRTUAL GENERATED'; + } elseif ( $node->get_first_descendant_token( WP_MySQL_Lexer::STORED_SYMBOL ) ) { + $extras[] = 'STORED GENERATED'; + } + return implode( ' ', $extras ); + } + + /** + * Extract column comment from the "columnDefinition" or "fieldDefinition" AST node. + * + * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" 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' ) ); + } + } + return ''; + } + + /** + * Extract column data type from the "columnDefinition" or "fieldDefinition" AST node. + * + * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" 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 = $type_node->get_descendant_tokens(); + $token = $type[0]; + + // Normalize types. + if ( isset( self::TOKEN_TO_TYPE_MAP[ $token->id ] ) ) { + $type = self::TOKEN_TO_TYPE_MAP[ $token->id ]; + } elseif ( + // VARCHAR/NVARCHAR + // 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 ) + ) { + $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 + ) { + $type = 'char'; + } elseif ( + // LONG VARBINARY + WP_MySQL_Lexer::LONG_SYMBOL === $token->id + && isset( $type[1] ) && WP_MySQL_Lexer::VARBINARY_SYMBOL === $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 + ) { + $type = 'mediumtext'; + } elseif ( + // LONG VARCHAR + WP_MySQL_Lexer::LONG_SYMBOL === $token->id + && isset( $type[1] ) && WP_MySQL_Lexer::VARCHAR_SYMBOL === $type[1]->id + ) { + $type = 'mediumtext'; + } else { + throw new \RuntimeException( 'Unknown data type: ' . $token->value ); + } + + // 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' ); + 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' ); + if ( null !== $field_length ) { + if ( 'decimal' === $type || 'float' === $type || 'double' === $type ) { + $full_type .= rtrim( $this->get_value( $field_length ), ')' ) . ',0)'; + } else { + $full_type .= $this->get_value( $field_length ); + } + /* + * As of MySQL 8.0.17, the display width attribute is deprecated for + * integer types (tinyint, smallint, mediumint, int/integer, bigint) + * and is not stored anymore. However, it may be important for older + * versions and WP's dbDelta, so it is safer to keep it at the moment. + * @TODO: Investigate if it is important to keep this. + */ + } + + $precision = $type_node->get_first_descendant_node( 'precision' ); + if ( null !== $precision ) { + $full_type .= $this->get_value( $precision ); + } + + $datetime_precision = $type_node->get_first_descendant_node( 'typeDatetimePrecision' ); + 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 + ) { + $full_type .= '(1)'; // Add length for booleans. + } + + if ( null === $field_length && null === $precision ) { + if ( 'decimal' === $type ) { + $full_type .= '(10,0)'; // Add default precision for decimals. + } elseif ( 'char' === $type || 'bit' === $type || 'binary' === $type ) { + $full_type .= '(1)'; // Add default length for char, bit, binary. + } + } + + // 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 ) + ) { + $full_type .= ' unsigned'; + } + + // ZEROFILL. + if ( $type_node->get_first_descendant_token( WP_MySQL_Lexer::ZEROFILL_SYMBOL ) ) { + $full_type .= ' zerofill'; + } + + return array( $type, $full_type ); + } + + /** + * Extract column charset and collation from the "columnDefinition" or "fieldDefinition" AST node. + * + * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" 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. + */ + private function get_column_charset_and_collation( WP_Parser_Node $node, string $data_type ): array { + if ( ! ( + 'char' === $data_type + || 'varchar' === $data_type + || 'tinytext' === $data_type + || 'text' === $data_type + || 'mediumtext' === $data_type + || 'longtext' === $data_type + || 'enum' === $data_type + || 'set' === $data_type + ) ) { + return array( null, null ); + } + + $charset = null; + $collation = null; + $is_binary = false; + + // Charset. + $charset_node = $node->get_first_descendant_node( 'charsetWithOptBinary' ); + if ( null !== $charset_node ) { + $charset_name_node = $charset_node->get_first_child_node( 'charsetName' ); + if ( null !== $charset_name_node ) { + $charset = strtolower( $this->get_value( $charset_name_node ) ); + } elseif ( $charset_node->has_child_token( WP_MySQL_Lexer::ASCII_SYMBOL ) ) { + $charset = 'latin1'; + } elseif ( $charset_node->has_child_token( WP_MySQL_Lexer::UNICODE_SYMBOL ) ) { + $charset = 'ucs2'; + } elseif ( $charset_node->has_child_token( WP_MySQL_Lexer::BYTE_SYMBOL ) ) { + // @TODO: This changes varchar to varbinary. + } + + // @TODO: "DEFAULT" + + if ( $charset_node->has_child_token( WP_MySQL_Lexer::BINARY_SYMBOL ) ) { + $is_binary = true; + } + } else { + // National charsets (in MySQL, it's "utf8"). + $data_type_node = $node->get_first_descendant_node( 'dataType' ); + 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 ) + ) { + $charset = 'utf8'; + } + } + + // Normalize charset. + if ( 'utf8mb3' === $charset ) { + $charset = 'utf8'; + } + + // Collation. + $collation_node = $node->get_first_descendant_node( 'collationName' ); + if ( null !== $collation_node ) { + $collation = strtolower( $this->get_value( $collation_node ) ); + } + + // Defaults. + // @TODO: These are hardcoded now. We should get them from table/DB. + if ( null === $charset && null === $collation ) { + $charset = 'utf8mb4'; + // @TODO: "BINARY" (seems to change varchar to varbinary). + // @TODO: "DEFAULT" + } + + // If only one of charset/collation is set, the other one is derived. + if ( null === $collation ) { + if ( $is_binary ) { + $collation = $charset . '_bin'; + } elseif ( isset( self::CHARSET_DEFAULT_COLLATION_MAP[ $charset ] ) ) { + $collation = self::CHARSET_DEFAULT_COLLATION_MAP[ $charset ]; + } else { + $collation = $charset . '_general_ci'; + } + } elseif ( null === $charset ) { + $charset = substr( $collation, 0, strpos( $collation, '_' ) ); + } + + return array( $charset, $collation ); + } + + /** + * Extract column length info from the "columnDefinition" or "fieldDefinition" AST node. + * + * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" 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. + */ + private function get_column_lengths( WP_Parser_Node $node, string $data_type, ?string $charset ): array { + // Text and blob types. + if ( 'tinytext' === $data_type || 'tinyblob' === $data_type ) { + return array( 255, 255 ); + } elseif ( 'text' === $data_type || 'blob' === $data_type ) { + return array( 65535, 65535 ); + } elseif ( 'mediumtext' === $data_type || 'mediumblob' === $data_type ) { + return array( 16777215, 16777215 ); + } elseif ( 'longtext' === $data_type || 'longblob' === $data_type ) { + return array( 4294967295, 4294967295 ); + } + + // For CHAR, VARCHAR, BINARY, VARBINARY, we need to check the field length. + if ( + 'char' === $data_type + || 'binary' === $data_type + || 'varchar' === $data_type + || 'varbinary' === $data_type + ) { + $field_length = $node->get_first_descendant_node( 'fieldLength' ); + if ( null === $field_length ) { + $length = 1; + } else { + $length = (int) trim( $this->get_value( $field_length ), '()' ); + } + + if ( 'char' === $data_type || 'varchar' === $data_type ) { + $max_bytes_per_char = self::CHARSET_MAX_BYTES_MAP[ $charset ] ?? 1; + return array( $length, $max_bytes_per_char * $length ); + } else { + return array( $length, $length ); + } + } + + // 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' ); + $length = 0; + foreach ( $values as $value ) { + $length = max( $length, strlen( $this->get_value( $value ) ) ); + } + $max_bytes_per_char = self::CHARSET_MAX_BYTES_MAP[ $charset ] ?? 1; + return array( $length, $max_bytes_per_char * $length ); + } + + return array( null, null ); + } + + /** + * Extract column precision and scale from the "columnDefinition" or "fieldDefinition" AST node. + * + * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" 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. + */ + private function get_column_numeric_attributes( WP_Parser_Node $node, string $data_type ): array { + if ( 'tinyint' === $data_type ) { + return array( 3, 0 ); + } elseif ( 'smallint' === $data_type ) { + return array( 5, 0 ); + } elseif ( 'mediumint' === $data_type ) { + return array( 7, 0 ); + } elseif ( 'int' === $data_type ) { + return array( 10, 0 ); + } elseif ( 'bigint' === $data_type ) { + if ( null !== $node->get_first_descendant_token( WP_MySQL_Lexer::UNSIGNED_SYMBOL ) ) { + 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 ) ) { + return array( 20, 0 ); + } + + return array( 19, 0 ); + } + + // For bit columns, we need to check the precision. + if ( 'bit' === $data_type ) { + $field_length = $node->get_first_descendant_node( 'fieldLength' ); + if ( null === $field_length ) { + return array( 1, null ); + } + return array( (int) trim( $this->get_value( $field_length ), '()' ), null ); + } + + // For floating point numbers, we need to check the precision and scale. + $precision = null; + $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 ); + $precision = (int) $values[0]->value; + $scale = (int) $values[1]->value; + } + + if ( 'float' === $data_type ) { + return array( $precision ?? 12, $scale ); + } elseif ( 'double' === $data_type ) { + 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' ); + if ( null !== $field_length ) { + $precision = (int) trim( $this->get_value( $field_length ), '()' ); + } + } + return array( $precision ?? 10, $scale ?? 0 ); + } + + return array( null, null ); + } + + /** + * Extract column date/time precision from the "columnDefinition" or "fieldDefinition" AST node. + * + * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" 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' ); + if ( null === $precision ) { + return 0; + } else { + return (int) $this->get_value( $precision ); + } + } + return null; + } + + /** + * Extract column generation expression from the "columnDefinition" or "fieldDefinition" AST node. + * + * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" 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 ); + } + return ''; + } + + /** + * Extract index name from the "tableConstraintDef" AST node. + * + * @param WP_Parser_Node $node The "tableConstraintDef" AST node. + * @return string The index name as stored in information schema. + */ + private function get_index_name( WP_Parser_Node $node ): string { + if ( $node->get_first_descendant_token( WP_MySQL_Lexer::PRIMARY_SYMBOL ) ) { + return 'PRIMARY'; + } + + $name_node = $node->get_first_descendant_node( 'indexName' ); + if ( null === $name_node ) { + /* + * In MySQL, the default index name equals the first column name. + * For functional indexes, the string "functional_index" is used. + * If the name is already used, we need to append a number. + */ + $subnode = $node->get_first_child_node( 'keyListVariants' )->get_first_child_node(); + if ( 'exprWithParentheses' === $subnode->rule_name ) { + $name = 'functional_index'; + } else { + $name = $this->get_value( $subnode->get_first_descendant_node( 'identifier' ) ); + } + + // @TODO: Check if the name is already used. + return $name; + } + return $this->get_value( $name_node ); + } + + /** + * Extract index non-unique value from the "tableConstraintDef" 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 + ) { + return 0; + } + return 1; + } + + /** + * Extract index type from the "tableConstraintDef" AST node. + * + * @param WP_Parser_Node $node The "tableConstraintDef" 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. + */ + private function get_index_type( + WP_Parser_Node $node, + WP_MySQL_Token $token, + bool $has_spatial_column + ): string { + // Handle "USING ..." clause. + $index_type = $node->get_first_descendant_node( 'indexTypeClause' ); + if ( null !== $index_type ) { + $index_type = strtoupper( + $this->get_value( $index_type->get_first_child_node( 'indexType' ) ) + ); + if ( 'RTREE' === $index_type ) { + return 'SPATIAL'; + } elseif ( 'HASH' === $index_type ) { + // InnoDB uses BTREE even when HASH is specified. + return 'BTREE'; + } + return $index_type; + } + + // Derive index type from its definition. + if ( WP_MySQL_Lexer::FULLTEXT_SYMBOL === $token->id ) { + return 'FULLTEXT'; + } elseif ( WP_MySQL_Lexer::SPATIAL_SYMBOL === $token->id ) { + return 'SPATIAL'; + } + + // Spatial indexes are also derived from column data type. + if ( $has_spatial_column ) { + return 'SPATIAL'; + } + + return 'BTREE'; + } + + /** + * Extract index column name from the "keyPart" AST node. + * + * @param WP_Parser_Node $node The "keyPart" 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 ) { + return null; + } + return $this->get_value( $node->get_first_descendant_node( 'identifier' ) ); + } + + /** + * Extract index column name from the "keyPart" AST node. + * + * @param WP_Parser_Node $node The "keyPart" 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. + */ + private function get_index_column_collation( WP_Parser_Node $node, string $index_type ): ?string { + if ( 'FULLTEXT' === $index_type ) { + return null; + } + + $collate_node = $node->get_first_descendant_node( 'collationName' ); + if ( null === $collate_node ) { + return 'A'; + } + $collate = strtoupper( $this->get_value( $collate_node ) ); + return 'DESC' === $collate ? 'D' : 'A'; + } + + /** + * Extract index column sub-part value from the "keyPart" AST node. + * + * @param WP_Parser_Node $node The "keyPart" 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. + */ + private function get_index_column_sub_part( + WP_Parser_Node $node, + ?int $max_length, + bool $is_spatial + ): ?int { + $field_length = $node->get_first_descendant_node( 'fieldLength' ); + if ( null === $field_length ) { + if ( $is_spatial ) { + return 32; + } + return null; + } + + $value = (int) trim( $this->get_value( $field_length ), '()' ); + if ( null !== $max_length && $value >= $max_length ) { + return $max_length; + } + return $value; + } + + /** + * Determine whether the column data type is a spatial data type. + * + * @param string $data_type The column data type as stored in information schema. + * @return bool Whether the column data type is a spatial data type. + */ + private function is_spatial_data_type( string $data_type ): bool { + return 'geometry' === $data_type + || 'geomcollection' === $data_type + || 'point' === $data_type + || 'multipoint' === $data_type + || 'linestring' === $data_type + || 'multilinestring' === $data_type + || 'polygon' === $data_type + || 'multipolygon' === $data_type; + } + + /** + * This is a helper function to get the full unescaped value of a node. + * + * @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 + * etc. + * + * For saving "DEFAULT ..." in column definitions, we actually need to + * serialize the whole node, in the case of expressions. This may mean + * implementing an MySQL AST -> string printer. + * + * @param WP_Parser_Node $node The AST node that needs to be serialized. + * @return string The serialized value of the node. + */ + private function get_value( WP_Parser_Node $node ): string { + $full_value = ''; + foreach ( $node->get_children() as $child ) { + if ( $child instanceof WP_Parser_Node ) { + $value = $this->get_value( $child ); + } elseif ( WP_MySQL_Lexer::BACK_TICK_QUOTED_ID === $child->id ) { + $value = substr( $child->value, 1, -1 ); + $value = str_replace( '``', '`', $value ); + } elseif ( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT === $child->id ) { + $value = $child->value; + $value = substr( $value, 1, -1 ); + $value = str_replace( '\"', '"', $value ); + $value = str_replace( '""', '"', $value ); + } elseif ( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT === $child->id ) { + $value = $child->value; + $value = substr( $value, 1, -1 ); + $value = str_replace( '\"', '"', $value ); + $value = str_replace( '""', '"', $value ); + } else { + $value = $child->value; + } + $full_value .= $value; + } + return $full_value; + } + + /** + * Insert values into an SQLite table. + * + * @param string $table_name The name of the table. + * @param array $data The data to insert (key is column name, value is column value). + */ + private function insert_values( string $table_name, array $data ): void { + $this->query( + ' + INSERT INTO ' . $table_name . ' (' . implode( ', ', array_keys( $data ) ) . ') + VALUES (' . implode( ', ', array_fill( 0, count( $data ), '?' ) ) . ') + ', + array_values( $data ) + ); + } + + /** + * Update values in an SQLite table. + * + * @param string $table_name The name of the table. + * @param array $data The data to update (key is column name, value is column value). + * @param array $where The WHERE clause conditions (key is column name, value is column value). + */ + private function update_values( string $table_name, array $data, array $where ): void { + $set = array(); + foreach ( $data as $column => $value ) { + $set[] = $column . ' = ?'; + } + + $where_clause = array(); + foreach ( $where as $column => $value ) { + $where_clause[] = $column . ' = ?'; + } + + $this->query( + ' + UPDATE ' . $table_name . ' + SET ' . implode( ', ', $set ) . ' + WHERE ' . implode( ' AND ', $where_clause ) . ' + ', + array_merge( array_values( $data ), array_values( $where ) ) + ); + } + + /** + * Delete values from an SQLite table. + * + * @param string $table_name The name of the table. + * @param array $where The WHERE clause conditions (key is column name, value is column value). + */ + private function delete_values( string $table_name, array $where ): void { + $where_clause = array(); + foreach ( $where as $column => $value ) { + $where_clause[] = $column . ' = ?'; + } + + $this->query( + ' + DELETE FROM ' . $table_name . ' + WHERE ' . implode( ' AND ', $where_clause ) . ' + ', + array_values( $where ) + ); + } + + /** + * Execute an SQLite query. + * + * @param string $query The query to execute. + * @param array $params The query parameters. + * + * @return PDOStatement + */ + private function query( string $query, array $params = array() ) { + return ( $this->query_callback )( $query, $params ); + } +} diff --git a/wp-includes/sqlite/class-wp-sqlite-db.php b/wp-includes/sqlite/class-wp-sqlite-db.php index ed80e19..a958625 100644 --- a/wp-includes/sqlite/class-wp-sqlite-db.php +++ b/wp-includes/sqlite/class-wp-sqlite-db.php @@ -125,25 +125,22 @@ public function esc_like( $text ) { } /** - * Method to put out the error message. + * Prints SQL/DB error. * - * This overrides wpdb::print_error(), for we can't use the parent class method. - * - * @see wpdb::print_error() + * This overrides wpdb::print_error() while closely mirroring its implementation. * * @global array $EZSQL_ERROR Stores error information of query and error string. * * @param string $str The error to display. - * - * @return bool|void False if the showing of errors is disabled. + * @return void|false Void if the showing of errors is enabled, false if disabled. */ public function print_error( $str = '' ) { global $EZSQL_ERROR; if ( ! $str ) { - $err = $this->dbh->get_error_message() ? $this->dbh->get_error_message() : ''; - $str = empty( $err ) ? '' : $err[2]; + $str = $this->last_error; } + $EZSQL_ERROR[] = array( 'query' => $this->last_query, 'error_str' => $str, @@ -153,26 +150,32 @@ public function print_error( $str = '' ) { return false; } - wp_load_translations_early(); - $caller = $this->get_caller(); - $caller = $caller ? $caller : '(unknown)'; - - $error_str = sprintf( - 'WordPress database error %1$s for query %2$s made by %3$s', - $str, - $this->last_query, - $caller - ); + if ( $caller ) { + // Not translated, as this will only appear in the error log. + $error_str = sprintf( 'WordPress database error %1$s for query %2$s made by %3$s', $str, $this->last_query, $caller ); + } else { + $error_str = sprintf( 'WordPress database error %1$s for query %2$s', $str, $this->last_query ); + } error_log( $error_str ); + // Are we showing errors? if ( ! $this->show_errors ) { return false; } + wp_load_translations_early(); + + // If there is an error then take note of it. if ( is_multisite() ) { - $msg = "WordPress database error: [$str]\n{$this->last_query}\n"; + $msg = sprintf( + "%s [%s]\n%s\n", + __( 'WordPress database error:' ), + $str, + $this->last_query + ); + if ( defined( 'ERRORLOGFILE' ) ) { error_log( $msg, 3, ERRORLOGFILE ); } @@ -184,9 +187,10 @@ public function print_error( $str = '' ) { $query = htmlspecialchars( $this->last_query, ENT_QUOTES ); printf( - '

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

', + '

%s [%s]
%s

', + __( 'WordPress database error:' ), $str, - '' . $query . '' + $query ); } } @@ -229,8 +233,35 @@ public function db_connect( $allow_bail = true ) { if ( isset( $GLOBALS['@pdo'] ) ) { $pdo = $GLOBALS['@pdo']; } - $this->dbh = new WP_SQLite_Translator( $pdo ); - $this->last_error = $this->dbh->get_error_message(); + if ( defined( 'WP_SQLITE_AST_DRIVER' ) && WP_SQLITE_AST_DRIVER ) { + require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-grammar.php'; + require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser.php'; + require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-node.php'; + require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-token.php'; + require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-token.php'; + require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-lexer.php'; + require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-parser.php'; + require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver.php'; + require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php'; + require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php'; + $this->ensure_database_directory( FQDB ); + + try { + $this->dbh = new WP_SQLite_Driver( + array( + 'connection' => $pdo, + 'path' => FQDB, + 'database' => $this->dbname, + 'sqlite_journal_mode' => defined( 'SQLITE_JOURNAL_MODE' ) ? SQLITE_JOURNAL_MODE : null, + ) + ); + } catch ( Throwable $e ) { + $this->last_error = $this->format_error_message( $e ); + } + } else { + $this->dbh = new WP_SQLite_Translator( $pdo ); + $this->last_error = $this->dbh->get_error_message(); + } if ( $this->last_error ) { return false; } @@ -250,16 +281,17 @@ public function check_connection( $allow_bail = true ) { } /** - * Method to execute the query. + * Performs a database query. * - * This overrides wpdb::query(). In fact, this method does all the database - * access jobs. + * This overrides wpdb::query() while closely mirroring its implementation. * * @see wpdb::query() * * @param string $query Database query. * - * @return int|false Number of rows affected/selected or false on error + * @param string $query Database query. + * @return int|bool Boolean true for CREATE, ALTER, TRUNCATE and DROP queries. Number of rows + * affected/selected for all other queries. Boolean false on error. */ public function query( $query ) { if ( ! $this->ready ) { @@ -268,43 +300,108 @@ public function query( $query ) { $query = apply_filters( 'query', $query ); - $this->flush(); + if ( ! $query ) { + $this->insert_id = 0; + return false; + } - $this->func_call = "\$db->query(\"$query\")"; - $this->last_query = $query; + $this->flush(); - if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { - $this->timer_start(); - } + // Log how the function was called. + $this->func_call = "\$db->query(\"$query\")"; - $this->result = $this->dbh->query( $query ); - ++$this->num_queries; + // Keep track of the last query for debug. + $this->last_query = $query; - if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { - $this->queries[] = array( $query, $this->timer_stop(), $this->get_caller() ); - } + /* + * @TODO: WPDB uses "$this->check_current_query" to check table/column + * charset and strip all invalid characters from the query. + * This is an involved process that we can bypass for SQLite, + * if we simply strip all invalid UTF-8 characters from the query. + * + * To do so, mb_convert_encoding can be used with an optional + * fallback to a htmlspecialchars method. E.g.: + * https://github.com/nette/utils/blob/be534713c227aeef57ce1883fc17bc9f9e29eca2/src/Utils/Strings.php#L42 + */ + $this->_do_query( $query ); - $this->last_error = $this->dbh->get_error_message(); if ( $this->last_error ) { - $this->print_error( $this->last_error ); + // Clear insert_id on a subsequent failed insert. + if ( $this->insert_id && preg_match( '/^\s*(insert|replace)\s/i', $query ) ) { + $this->insert_id = 0; + } + + $this->print_error(); return false; } - if ( preg_match( '/^\\s*(set|create|alter|truncate|drop|optimize)\\s*/i', $query ) ) { - return $this->dbh->get_return_value(); - } + if ( preg_match( '/^\s*(create|alter|truncate|drop)\s/i', $query ) ) { + $return_val = true; + } elseif ( preg_match( '/^\s*(insert|delete|update|replace)\s/i', $query ) ) { + if ( $this->dbh instanceof WP_SQLite_Driver ) { + $this->rows_affected = $this->dbh->get_last_return_value(); + } else { + $this->rows_affected = $this->dbh->get_rows_affected(); + } - if ( preg_match( '/^\\s*(insert|delete|update|replace)\s/i', $query ) ) { - $this->rows_affected = $this->dbh->get_affected_rows(); + // Take note of the insert_id. if ( preg_match( '/^\s*(insert|replace)\s/i', $query ) ) { $this->insert_id = $this->dbh->get_insert_id(); } - return $this->rows_affected; + + // Return number of rows affected. + $return_val = $this->rows_affected; + } else { + $num_rows = 0; + + if ( is_array( $this->result ) ) { + $this->last_result = $this->result; + $num_rows = count( $this->result ); + } + + // Log and return the number of rows selected. + $this->num_rows = $num_rows; + $return_val = $num_rows; + } + + return $return_val; + } + + /** + * Internal function to perform the SQLite query call. + * + * This closely mirrors wpdb::_do_query(). + * + * @see wpdb::_do_query() + * + * @param string $query The query to run. + */ + private function _do_query( $query ) { + if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { + $this->timer_start(); + } + + try { + $this->result = $this->dbh->query( $query ); + } catch ( Throwable $e ) { + $this->last_error = $this->format_error_message( $e ); + } + + if ( $this->dbh instanceof WP_SQLite_Translator ) { + $this->last_error = $this->dbh->get_error_message(); } - $this->last_result = $this->dbh->get_query_results(); - $this->num_rows = $this->dbh->get_num_rows(); - return $this->num_rows; + ++$this->num_queries; + + if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { + $this->log_query( + $query, + $this->timer_stop(), + $this->get_caller(), + $this->time_start, + array() + ); + } } /** @@ -353,11 +450,95 @@ public function db_version() { } /** - * Retrieves full database server information. + * Returns the version of the SQLite engine. * - * @return string|false Server info on success, false on failure. + * @return string SQLite engine version as a string. */ public function db_server_info() { - return SQLite3::version()['versionString']; + return $this->dbh->get_sqlite_version(); + } + + /** + * Make sure the SQLite database directory exists and is writable. + * Create .htaccess and index.php files to prevent direct access. + * + * @param string $database_path The path to the SQLite database file. + */ + private function ensure_database_directory( string $database_path ) { + $dir = dirname( $database_path ); + + // Set the umask to 0000 to apply permissions exactly as specified. + // A non-zero umask affects new file and directory permissions. + $umask = umask( 0 ); + + // Ensure database directory. + if ( ! is_dir( $dir ) ) { + if ( ! @mkdir( $dir, 0700, true ) ) { + wp_die( sprintf( 'Failed to create database directory: %s', $dir ), 'Error!' ); + } + } + if ( ! is_writable( $dir ) ) { + wp_die( sprintf( 'Database directory is not writable: %s', $dir ), 'Error!' ); + } + + // Ensure .htaccess file to prevent direct access. + $path = $dir . DIRECTORY_SEPARATOR . '.htaccess'; + if ( ! is_file( $path ) ) { + $result = file_put_contents( $path, 'DENY FROM ALL', LOCK_EX ); + if ( false === $result ) { + wp_die( sprintf( 'Failed to create file: %s', $path ), 'Error!' ); + } + chmod( $path, 0600 ); + } + + // Ensure index.php file to prevent direct access. + $path = $dir . DIRECTORY_SEPARATOR . 'index.php'; + if ( ! is_file( $path ) ) { + $result = file_put_contents( $path, '', LOCK_EX ); + if ( false === $result ) { + wp_die( sprintf( 'Failed to create file: %s', $path ), 'Error!' ); + } + chmod( $path, 0600 ); + } + + // Restore the original umask value. + umask( $umask ); + } + + + /** + * Format SQLite driver error message. + * + * @return string + */ + private function format_error_message( Throwable $e ) { + $output = '
 
' . PHP_EOL; + + // Queries. + if ( $e instanceof WP_SQLite_Driver_Exception ) { + $driver = $e->getDriver(); + + $output .= '
' . PHP_EOL; + $output .= '

MySQL query:

' . PHP_EOL; + $output .= '

' . $driver->get_last_mysql_query() . '

' . PHP_EOL; + $output .= '

Queries made or created this session were:

' . PHP_EOL; + $output .= '
    ' . PHP_EOL; + foreach ( $driver->get_last_sqlite_queries() as $q ) { + $message = "Executing: {$q['sql']} | " . ( $q['params'] ? 'parameters: ' . implode( ', ', $q['params'] ) : '(no parameters)' ); + $output .= '
  1. ' . htmlspecialchars( $message ) . '
  2. ' . PHP_EOL; + } + $output .= '
' . PHP_EOL; + $output .= '
' . PHP_EOL; + } + + // Message. + $output .= '
' . PHP_EOL; + $output .= $e->getMessage() . PHP_EOL; + $output .= '
' . PHP_EOL; + + // Backtrace. + $output .= '

Backtrace:

' . PHP_EOL; + $output .= '
' . $e->getTraceAsString() . '
' . PHP_EOL; + return $output; } } diff --git a/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php b/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php index 6f0d83d..d14978b 100644 --- a/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php +++ b/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php @@ -23,19 +23,17 @@ class WP_SQLite_PDO_User_Defined_Functions { /** - * The class constructor - * - * Initializes the use defined functions to PDO object with PDO::sqliteCreateFunction(). + * Registers the user defined functions for SQLite to a PDO instance. + * The functions are registered using PDO::sqliteCreateFunction(). * * @param PDO $pdo The PDO object. */ - public function __construct( $pdo ) { - if ( ! $pdo ) { - wp_die( 'Database is not initialized.', 'Database Error' ); - } - foreach ( $this->functions as $f => $t ) { - $pdo->sqliteCreateFunction( $f, array( $this, $t ) ); + public static function register_for( PDO $pdo ): self { + $instance = new self(); + foreach ( $instance->functions as $f => $t ) { + $pdo->sqliteCreateFunction( $f, array( $instance, $t ) ); } + return $instance; } /** @@ -46,45 +44,48 @@ public function __construct( $pdo ) { * @var array */ private $functions = array( - 'month' => 'month', - 'monthnum' => 'month', - 'year' => 'year', - 'day' => 'day', - 'hour' => 'hour', - 'minute' => 'minute', - 'second' => 'second', - 'week' => 'week', - 'weekday' => 'weekday', - 'dayofweek' => 'dayofweek', - 'dayofmonth' => 'dayofmonth', - 'unix_timestamp' => 'unix_timestamp', - 'now' => 'now', - 'md5' => 'md5', - 'curdate' => 'curdate', - 'rand' => 'rand', - 'from_unixtime' => 'from_unixtime', - 'localtime' => 'now', - 'localtimestamp' => 'now', - 'isnull' => 'isnull', - 'if' => '_if', - 'regexp' => 'regexp', - 'field' => 'field', - 'log' => 'log', - 'least' => 'least', - 'greatest' => 'greatest', - 'get_lock' => 'get_lock', - 'release_lock' => 'release_lock', - 'ucase' => 'ucase', - 'lcase' => 'lcase', - 'unhex' => 'unhex', - 'inet_ntoa' => 'inet_ntoa', - 'inet_aton' => 'inet_aton', - 'datediff' => 'datediff', - 'locate' => 'locate', - 'utc_date' => 'utc_date', - 'utc_time' => 'utc_time', - 'utc_timestamp' => 'utc_timestamp', - 'version' => 'version', + 'month' => 'month', + 'monthnum' => 'month', + 'year' => 'year', + 'day' => 'day', + 'hour' => 'hour', + 'minute' => 'minute', + 'second' => 'second', + 'week' => 'week', + 'weekday' => 'weekday', + 'dayofweek' => 'dayofweek', + 'dayofmonth' => 'dayofmonth', + 'unix_timestamp' => 'unix_timestamp', + 'now' => 'now', + 'md5' => 'md5', + 'curdate' => 'curdate', + 'rand' => 'rand', + 'from_unixtime' => 'from_unixtime', + 'localtime' => 'now', + 'localtimestamp' => 'now', + 'isnull' => 'isnull', + 'if' => '_if', + 'regexp' => 'regexp', + 'field' => 'field', + 'log' => 'log', + 'least' => 'least', + 'greatest' => 'greatest', + 'get_lock' => 'get_lock', + 'release_lock' => 'release_lock', + 'ucase' => 'ucase', + 'lcase' => 'lcase', + 'unhex' => 'unhex', + 'inet_ntoa' => 'inet_ntoa', + 'inet_aton' => 'inet_aton', + 'datediff' => 'datediff', + 'locate' => 'locate', + 'utc_date' => 'utc_date', + 'utc_time' => 'utc_time', + 'utc_timestamp' => 'utc_timestamp', + 'version' => 'version', + + // Internal helper functions. + '_helper_like_to_glob_pattern' => '_helper_like_to_glob_pattern', ); /** @@ -759,4 +760,60 @@ public function utc_timestamp() { public function version() { return '5.5'; } + + /** + * A helper to covert LIKE pattern to a GLOB pattern for "LIKE BINARY" support. + + * @TODO: Some of the MySQL string specifics described below are likely to + * affect also other patterns than just "LIKE BINARY". We should + * consider applying some of the conversions more broadly. + * + * @param string $pattern + * @return string + */ + public function _helper_like_to_glob_pattern( $pattern ) { + if ( null === $pattern ) { + return null; + } + + /* + * 1. Escape characters that have special meaning in GLOB patterns. + * + * We need to: + * 1. Escape "]" as "[]]" to avoid interpreting "[...]" as a character class. + * 2. Escape "*" as "[*]" (must be after 1 to avoid being escaped). + * 3. Escape "?" as "[?]" (must be after 1 to avoid being escaped). + */ + $pattern = str_replace( ']', '[]]', $pattern ); + $pattern = str_replace( '*', '[*]', $pattern ); + $pattern = str_replace( '?', '[?]', $pattern ); + + /* + * 2. Convert LIKE wildcards to GLOB wildcards ("%" -> "*", "_" -> "?"). + * + * We need to convert them only when they don't follow any backslashes, + * or when they follow an even number of backslashes (as "\\" is "\"). + */ + $pattern = preg_replace( '/(^|[^\\\\](?:\\\\{2})*)%/', '$1*', $pattern ); + $pattern = preg_replace( '/(^|[^\\\\](?:\\\\{2})*)_/', '$1?', $pattern ); + + /* + * 3. Unescape LIKE escape sequences. + * + * While in MySQL LIKE patterns, a backslash is usually used to escape + * special characters ("%", "_", and "\"), it works with all characters. + * + * That is: + * SELECT '\\x' prints '\x', but LIKE '\\x' is equivalent to LIKE 'x'. + * + * This is true also for multi-byte characters: + * SELECT '\\©' prints '\©', but LIKE '\\©' is equivalent to LIKE '©'. + * + * However, the multi-byte behavior is likely to depend on the charset. + * For now, we'll assume UTF-8 and thus the "u" modifier for the regex. + */ + $pattern = preg_replace( '/\\\\(.)/u', '$1', $pattern ); + + return $pattern; + } } diff --git a/wp-includes/sqlite/class-wp-sqlite-translator.php b/wp-includes/sqlite/class-wp-sqlite-translator.php index 44996cd..c353668 100644 --- a/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -394,7 +394,7 @@ public function __construct( $pdo = null ) { } } - new WP_SQLite_PDO_User_Defined_Functions( $pdo ); + WP_SQLite_PDO_User_Defined_Functions::register_for( $pdo ); // MySQL data comes across stringified by default. $pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO @@ -410,7 +410,7 @@ public function __construct( $pdo = null ) { $this->pdo = $pdo; // Fixes a warning in the site-health screen. - $this->client_info = SQLite3::version()['versionString']; + $this->client_info = $this->get_sqlite_version(); register_shutdown_function( array( $this, '__destruct' ) ); @@ -474,6 +474,15 @@ public function get_pdo() { return $this->pdo; } + /** + * Get the version of the SQLite engine. + * + * @return string SQLite engine version as a string. + */ + public function get_sqlite_version(): string { + return $this->pdo->query( 'SELECT SQLITE_VERSION()' )->fetchColumn(); + } + /** * Method to return inserted row id. */