@@ -425,6 +425,18 @@ class WP_SQLite_Driver {
425425 'STRICT_TRANS_TABLES ' ,
426426 );
427427
428+ /**
429+ * A name-to-value map of MySQL user variables.
430+ *
431+ * MySQL user variables are session-specific, so we can store them in-memory.
432+ *
433+ * See:
434+ * https://dev.mysql.com/doc/refman/8.4/en/user-variables.html
435+ *
436+ * @var array<string, string>
437+ */
438+ private $ user_variables = array ();
439+
428440 /**
429441 * Constructor.
430442 *
@@ -2109,11 +2121,19 @@ private function execute_set_statement( WP_Parser_Node $node ): void {
21092121 || 'setSystemVariable ' === $ part ->rule_name
21102122 )
21112123 ) {
2124+ // Set a system variable.
21122125 array_shift ( $ definition ); // Remove the '='.
21132126 $ value = array_shift ( $ definition );
21142127 $ this ->execute_set_system_variable_statement ( $ part , $ value , $ default_type );
2128+ } elseif (
2129+ $ part instanceof WP_Parser_Node
2130+ && 'userVariable ' === $ part ->rule_name
2131+ ) {
2132+ // Set a user variable.
2133+ array_shift ( $ definition ); // Remove the '='.
2134+ $ value = array_shift ( $ definition );
2135+ $ this ->execute_set_user_variable_statement ( $ part , $ value );
21152136 } else {
2116- // TODO: Support user variables (in-memory or a temporary table).
21172137 throw $ this ->new_not_supported_exception (
21182138 sprintf ( 'SET statement: %s ' , $ node ->rule_name )
21192139 );
@@ -2176,6 +2196,26 @@ private function execute_set_system_variable_statement(
21762196 // TODO: Handle GLOBAL, PERSIST, and PERSIST_ONLY types.
21772197 }
21782198
2199+ /**
2200+ * Translate and execute a MySQL SET statement for user variables.
2201+ *
2202+ * @param WP_Parser_Node $user_variable The "userVariable" AST node.
2203+ * @param WP_Parser_Node $expr The "expr" AST node.
2204+ * @throws WP_SQLite_Driver_Exception When the query execution fails.
2205+ */
2206+ private function execute_set_user_variable_statement (
2207+ WP_Parser_Node $ user_variable ,
2208+ WP_Parser_Node $ expr
2209+ ): void {
2210+ $ name = $ this ->unquote_sqlite_identifier (
2211+ $ this ->translate ( $ user_variable ->get_first_child () )
2212+ );
2213+ $ name = strtolower ( substr ( $ name , 1 ) ); // Remove '@', normalize case.
2214+ $ value = $ this ->evaluate_expression ( $ expr );
2215+
2216+ $ this ->user_variables [ $ name ] = $ value ;
2217+ }
2218+
21792219 /**
21802220 * Translate and execute a MySQL administration statement in SQLite.
21812221 *
@@ -2258,6 +2298,33 @@ private function execute_administration_statement( WP_Parser_Node $node ): void
22582298 $ this ->set_results_from_fetched_data ( $ results );
22592299 }
22602300
2301+ /**
2302+ * Evaluate an expression and return the value, preserving its type.
2303+ *
2304+ * This is used to support expressions in SET statements for MySQL variables.
2305+ *
2306+ * @param WP_Parser_Node $node The "expr" AST node.
2307+ * @return mixed The value of the expression.
2308+ */
2309+ public function evaluate_expression ( WP_Parser_Node $ node ) {
2310+ // To support expressions, we'll use a SQLite query.
2311+ $ stmt = $ this ->execute_sqlite_query (
2312+ sprintf ( 'SELECT %s ' , $ this ->translate ( $ node ) )
2313+ );
2314+
2315+ // MySQL variables are typed, so we need to preserve the value type.
2316+ $ value = $ stmt ->fetchColumn ();
2317+ $ type = $ stmt ->getColumnMeta ( 0 )['native_type ' ];
2318+ if ( 'null ' === $ type ) {
2319+ return null ;
2320+ } elseif ( 'integer ' === $ type ) {
2321+ return (int ) $ value ;
2322+ } elseif ( 'double ' === $ type ) {
2323+ return (float ) $ value ;
2324+ }
2325+ return $ value ;
2326+ }
2327+
22612328 /**
22622329 * Translate a MySQL AST node or token to an SQLite query fragment.
22632330 *
@@ -2428,6 +2495,17 @@ private function translate( $node ): ?string {
24282495 // See: https://dev.mysql.com/doc/refman/8.4/en/server-system-variable-reference.html
24292496 // See: https://dev.mysql.com/doc/refman/8.4/en/server-system-variables.html
24302497 return $ value ;
2498+ case 'userVariable ' :
2499+ $ name = $ this ->unquote_sqlite_identifier ( $ this ->translate ( $ node ->get_first_child () ) );
2500+ $ name = strtolower ( substr ( $ name , 1 ) ); // Remove '@', normalize case.
2501+ $ value = $ this ->user_variables [ $ name ] ?? null ;
2502+ if ( null === $ value ) {
2503+ return 'NULL ' ;
2504+ }
2505+ if ( is_string ( $ value ) ) {
2506+ return $ this ->connection ->quote ( $ value );
2507+ }
2508+ return (string ) $ value ;
24312509 case 'castType ' :
24322510 // Translate "CAST(... AS BINARY)" to "CAST(... AS BLOB)".
24332511 if ( $ node ->has_child_token ( WP_MySQL_Lexer::BINARY_SYMBOL ) ) {
0 commit comments