← All changes
|
wp-includes/database/sqlite/class-wp-mysql-on-sqlite.php
+116
-33
3.0.1
→
3.1.0
View file →
| @@ -711,8 +711,19 @@ | ||
| 711 | 711 | */ |
| 712 | 712 | private $in_transaction = false; |
| 713 | 713 | |
| 714 | 714 | /** |
| 715 | + * Names of active user savepoints, outermost first. | |
| 716 | + * | |
| 717 | + * MySQL replaces a savepoint with the same name, while SQLite only shadows it. | |
| 718 | + * Tracking normalized names prevents shadowed SQLite savepoints from becoming | |
| 719 | + * visible again and supports case-insensitive lookup. | |
| 720 | + * | |
| 721 | + * @var string[] | |
| 722 | + */ | |
| 723 | + private $savepoint_names = array(); | |
| 724 | + | |
| 725 | + /** | |
| 715 | 726 | * Whether a MySQL table lock is active. |
| 716 | 727 | * |
| 717 | 728 | * Set to "true" when a lock is acquired using the MySQL LOCK statement. |
| 718 | 729 | * Set to "false" when locks are released using the MySQL UNLOCK statement. |
| @@ -1181,13 +1192,18 @@ | ||
| 1181 | 1192 | $this->error_code = '00000'; |
| 1182 | 1193 | $this->error_info = array( '00000', null, null ); |
| 1183 | 1194 | return $stmt; |
| 1184 | 1195 | } catch ( Throwable $e ) { |
| 1185 | - try { | |
| 1186 | - $this->rollback_user_transaction(); | |
| 1187 | - $this->table_lock_active = false; | |
| 1188 | - } catch ( Throwable $rollback_exception ) { | |
| 1189 | - // Ignore rollback errors. | |
| 1196 | + // MySQL error 1305 reports a missing savepoint without ending the transaction. | |
| 1197 | + $preserves_transaction = $e instanceof WP_MySQL_On_SQLite_Exception | |
| 1198 | + && 1305 === ( $e->errorInfo[1] ?? null ); | |
| 1199 | + if ( ! $preserves_transaction ) { | |
| 1200 | + try { | |
| 1201 | + $this->rollback_user_transaction(); | |
| 1202 | + $this->table_lock_active = false; | |
| 1203 | + } catch ( Throwable $rollback_exception ) { | |
| 1204 | + // Ignore rollback errors. | |
| 1205 | + } | |
| 1190 | 1206 | } |
| 1191 | 1207 | if ( $e instanceof WP_SQLite_Information_Schema_Exception ) { |
| 1192 | 1208 | $e = $this->convert_information_schema_exception( $e ); |
| 1193 | 1209 | } |
| @@ -2091,9 +2107,10 @@ | ||
| 2091 | 2107 | * |
| 2092 | 2108 | * @see self::begin_wrapper_transaction() |
| 2093 | 2109 | */ |
| 2094 | 2110 | $this->connection->query( 'BEGIN IMMEDIATE' ); |
| 2095 | - $this->in_transaction = true; | |
| 2111 | + $this->in_transaction = true; | |
| 2112 | + $this->savepoint_names = array(); | |
| 2096 | 2113 | } |
| 2097 | 2114 | |
| 2098 | 2115 | /** |
| 2099 | 2116 | * Execute the "COMMIT" MySQL statement in SQLite. |
| @@ -2103,9 +2120,10 @@ | ||
| 2103 | 2120 | if ( ! $this->inTransaction() ) { |
| 2104 | 2121 | return; |
| 2105 | 2122 | } |
| 2106 | 2123 | $this->connection->query( 'COMMIT' ); |
| 2107 | - $this->in_transaction = false; | |
| 2124 | + $this->in_transaction = false; | |
| 2125 | + $this->savepoint_names = array(); | |
| 2108 | 2126 | } |
| 2109 | 2127 | |
| 2110 | 2128 | /** |
| 2111 | 2129 | * Execute the "ROLLBACK" MySQL statement in SQLite. |
| @@ -2115,9 +2133,10 @@ | ||
| 2115 | 2133 | if ( ! $this->inTransaction() ) { |
| 2116 | 2134 | return; |
| 2117 | 2135 | } |
| 2118 | 2136 | $this->connection->query( 'ROLLBACK' ); |
| 2119 | - $this->in_transaction = false; | |
| 2137 | + $this->in_transaction = false; | |
| 2138 | + $this->savepoint_names = array(); | |
| 2120 | 2139 | } |
| 2121 | 2140 | |
| 2122 | 2141 | /** |
| 2123 | 2142 | * Execute a MySQL transaction or locking statement in SQLite. |
| @@ -2145,8 +2164,11 @@ | ||
| 2145 | 2164 | |
| 2146 | 2165 | break; |
| 2147 | 2166 | case 'savepointStatement': |
| 2148 | 2167 | $savepoint_name = $this->translate( $subnode->get_first_child_node( 'identifier' ) ); |
| 2168 | + $savepoint_key = null === $savepoint_name | |
| 2169 | + ? null | |
| 2170 | + : strtolower( $this->unquote_sqlite_identifier( $savepoint_name ) ); | |
| 2149 | 2171 | |
| 2150 | 2172 | // ROLLBACK/ROLLBACK TO SAVEPOINT <identifier>. |
| 2151 | 2173 | if ( WP_MySQL_Lexer::ROLLBACK_SYMBOL === $token->id ) { |
| 2152 | 2174 | if ( null === $savepoint_name ) { |
| @@ -2151,9 +2173,15 @@ | ||
| 2151 | 2173 | if ( WP_MySQL_Lexer::ROLLBACK_SYMBOL === $token->id ) { |
| 2152 | 2174 | if ( null === $savepoint_name ) { |
| 2153 | 2175 | $this->rollback_user_transaction(); |
| 2154 | 2176 | } else { |
| 2177 | + // ROLLBACK TO keeps the named savepoint and deletes those created after it. | |
| 2178 | + $index = array_search( $savepoint_key, $this->savepoint_names, true ); | |
| 2179 | + if ( false === $index ) { | |
| 2180 | + throw $this->new_savepoint_does_not_exist_exception( $savepoint_name ); | |
| 2181 | + } | |
| 2155 | 2182 | $this->execute_sqlite_query( sprintf( 'ROLLBACK TO SAVEPOINT %s', $savepoint_name ) ); |
| 2183 | + array_splice( $this->savepoint_names, $index + 1 ); | |
| 2156 | 2184 | } |
| 2157 | 2185 | return; |
| 2158 | 2186 | } |
| 2159 | 2187 | |
| @@ -2158,15 +2186,38 @@ | ||
| 2158 | 2186 | } |
| 2159 | 2187 | |
| 2160 | 2188 | // SAVEPOINT. |
| 2161 | 2189 | if ( WP_MySQL_Lexer::SAVEPOINT_SYMBOL === $token->id ) { |
| 2190 | + // In MySQL with autocommit enabled, a standalone savepoint is discarded | |
| 2191 | + // immediately without starting a transaction. | |
| 2192 | + if ( ! $this->inTransaction() ) { | |
| 2193 | + return; | |
| 2194 | + } | |
| 2162 | 2195 | $this->execute_sqlite_query( sprintf( 'SAVEPOINT %s', $savepoint_name ) ); |
| 2196 | + | |
| 2197 | + /* | |
| 2198 | + * MySQL deletes an existing savepoint when its name is reused, while | |
| 2199 | + * SQLite keeps it on the stack, shadowed by the new one. Drop the old | |
| 2200 | + * name so that it can no longer be referenced. The shadowed SQLite | |
| 2201 | + * savepoint is harmless; it is discarded when the transaction ends. | |
| 2202 | + */ | |
| 2203 | + $index = array_search( $savepoint_key, $this->savepoint_names, true ); | |
| 2204 | + if ( false !== $index ) { | |
| 2205 | + array_splice( $this->savepoint_names, $index, 1 ); | |
| 2206 | + } | |
| 2207 | + $this->savepoint_names[] = $savepoint_key; | |
| 2163 | 2208 | return; |
| 2164 | 2209 | } |
| 2165 | 2210 | |
| 2166 | 2211 | // RELEASE SAVEPOINT. |
| 2167 | 2212 | if ( WP_MySQL_Lexer::RELEASE_SYMBOL === $token->id ) { |
| 2213 | + // RELEASE deletes the named savepoint and those created after it. | |
| 2214 | + $index = array_search( $savepoint_key, $this->savepoint_names, true ); | |
| 2215 | + if ( false === $index ) { | |
| 2216 | + throw $this->new_savepoint_does_not_exist_exception( $savepoint_name ); | |
| 2217 | + } | |
| 2168 | 2218 | $this->execute_sqlite_query( sprintf( 'RELEASE SAVEPOINT %s', $savepoint_name ) ); |
| 2219 | + array_splice( $this->savepoint_names, $index ); | |
| 2169 | 2220 | return; |
| 2170 | 2221 | } |
| 2171 | 2222 | |
| 2172 | 2223 | break; |
| @@ -4891,8 +4942,10 @@ | ||
| 4891 | 4942 | $tokens = $node->get_descendant_tokens(); |
| 4892 | 4943 | $is_binary = isset( $tokens[1] ) && WP_MySQL_Lexer::BINARY_SYMBOL === $tokens[1]->id; |
| 4893 | 4944 | |
| 4894 | 4945 | if ( true === $is_binary ) { |
| 4946 | + // TODO: GLOB can match different invalid UTF-8 bytes as equal. | |
| 4947 | + // Use byte-wise matching for full LIKE BINARY compatibility. | |
| 4895 | 4948 | $children = $node->get_children(); |
| 4896 | 4949 | return sprintf( |
| 4897 | 4950 | 'GLOB _helper_like_to_glob_pattern(%s)', |
| 4898 | 4951 | $this->translate( $children[1] ) |
| @@ -4967,8 +5020,17 @@ | ||
| 4967 | 5020 | return $this->translate( $child ); |
| 4968 | 5021 | } |
| 4969 | 5022 | |
| 4970 | 5023 | switch ( $child->id ) { |
| 5024 | + case WP_MySQL_Lexer::IF_SYMBOL: | |
| 5025 | + // CASE also supports SQLite versions before IIF() was added in 3.32.0. | |
| 5026 | + $nodes = $node->get_child_nodes(); | |
| 5027 | + return sprintf( | |
| 5028 | + 'CASE WHEN %s THEN %s ELSE %s END', | |
| 5029 | + $this->translate( $nodes[0] ), | |
| 5030 | + $this->translate( $nodes[1] ), | |
| 5031 | + $this->translate( $nodes[2] ) | |
| 5032 | + ); | |
| 4971 | 5033 | case WP_MySQL_Lexer::DATABASE_SYMBOL: |
| 4972 | 5034 | return $this->quote_sqlite_value( $this->db_name ); |
| 4973 | 5035 | case WP_MySQL_Lexer::CURRENT_TIMESTAMP_SYMBOL: |
| 4974 | 5036 | case WP_MySQL_Lexer::NOW_SYMBOL: |
| @@ -6251,8 +6313,22 @@ | ||
| 6251 | 6313 | | self::SQL_MODES['IGNORE_SPACE'] |
| 6252 | 6314 | | self::SQL_MODES['ONLY_FULL_GROUP_BY']; |
| 6253 | 6315 | } |
| 6254 | 6316 | |
| 6317 | + if ( ( $sql_modes & self::SQL_MODES['TRADITIONAL'] ) !== 0 ) { | |
| 6318 | + $sql_modes |= self::SQL_MODES['STRICT_TRANS_TABLES'] | |
| 6319 | + | self::SQL_MODES['STRICT_ALL_TABLES'] | |
| 6320 | + | self::SQL_MODES['NO_ZERO_IN_DATE'] | |
| 6321 | + | self::SQL_MODES['NO_ZERO_DATE'] | |
| 6322 | + | self::SQL_MODES['ERROR_FOR_DIVISION_BY_ZERO'] | |
| 6323 | + | self::SQL_MODES['NO_ENGINE_SUBSTITUTION']; | |
| 6324 | + | |
| 6325 | + // TRADITIONAL included NO_AUTO_CREATE_USER until its removal in MySQL 8.0.11. | |
| 6326 | + if ( ! $this->is_sql_mode_removed( 'NO_AUTO_CREATE_USER' ) ) { | |
| 6327 | + $sql_modes |= self::SQL_MODES['NO_AUTO_CREATE_USER']; | |
| 6328 | + } | |
| 6329 | + } | |
| 6330 | + | |
| 6255 | 6331 | $this->active_sql_modes = $sql_modes; |
| 6256 | 6332 | } |
| 6257 | 6333 | |
| 6258 | 6334 | /** |
| @@ -7319,8 +7395,19 @@ | ||
| 7319 | 7395 | $rows[] = $sql; |
| 7320 | 7396 | } |
| 7321 | 7397 | |
| 7322 | 7398 | // 7. Generate CREATE TABLE statement constraints, collect indexes. |
| 7399 | + $format_index_column = function ( $column ) { | |
| 7400 | + $definition = $this->quote_mysql_identifier( $column['COLUMN_NAME'] ); | |
| 7401 | + if ( null !== $column['SUB_PART'] ) { | |
| 7402 | + $definition .= sprintf( '(%d)', $column['SUB_PART'] ); | |
| 7403 | + } | |
| 7404 | + if ( 'D' === $column['COLLATION'] ) { | |
| 7405 | + $definition .= ' DESC'; | |
| 7406 | + } | |
| 7407 | + return $definition; | |
| 7408 | + }; | |
| 7409 | + | |
| 7323 | 7410 | foreach ( $grouped_constraints as $constraint ) { |
| 7324 | 7411 | ksort( $constraint ); |
| 7325 | 7412 | $info = $constraint[1]; |
| 7326 | 7413 | |
| @@ -7325,17 +7412,9 @@ | ||
| 7325 | 7412 | $info = $constraint[1]; |
| 7326 | 7413 | |
| 7327 | 7414 | if ( 'PRIMARY' === $info['INDEX_NAME'] ) { |
| 7328 | 7415 | $sql = ' PRIMARY KEY ('; |
| 7329 | - $sql .= implode( | |
| 7330 | - ', ', | |
| 7331 | - array_map( | |
| 7332 | - function ( $column ) { | |
| 7333 | - return $this->quote_mysql_identifier( $column['COLUMN_NAME'] ); | |
| 7334 | - }, | |
| 7335 | - $constraint | |
| 7336 | - ) | |
| 7337 | - ); | |
| 7416 | + $sql .= implode( ', ', array_map( $format_index_column, $constraint ) ); | |
| 7338 | 7417 | $sql .= ')'; |
| 7339 | 7418 | } else { |
| 7340 | 7419 | $is_unique = '0' === $info['NON_UNIQUE']; |
| 7341 | 7420 | |
| @@ -7346,24 +7425,9 @@ | ||
| 7346 | 7425 | 'SPATIAL' === $info['INDEX_TYPE'] ? 'SPATIAL ' : '' |
| 7347 | 7426 | ); |
| 7348 | 7427 | $sql .= $this->quote_mysql_identifier( $info['INDEX_NAME'] ); |
| 7349 | 7428 | $sql .= ' ('; |
| 7350 | - $sql .= implode( | |
| 7351 | - ', ', | |
| 7352 | - array_map( | |
| 7353 | - function ( $column ) { | |
| 7354 | - $definition = $this->quote_mysql_identifier( $column['COLUMN_NAME'] ); | |
| 7355 | - if ( null !== $column['SUB_PART'] ) { | |
| 7356 | - $definition .= sprintf( '(%d)', $column['SUB_PART'] ); | |
| 7357 | - } | |
| 7358 | - if ( 'D' === $column['COLLATION'] ) { | |
| 7359 | - $definition .= ' DESC'; | |
| 7360 | - } | |
| 7361 | - return $definition; | |
| 7362 | - }, | |
| 7363 | - $constraint | |
| 7364 | - ) | |
| 7365 | - ); | |
| 7429 | + $sql .= implode( ', ', array_map( $format_index_column, $constraint ) ); | |
| 7366 | 7430 | $sql .= ')'; |
| 7367 | 7431 | } |
| 7368 | 7432 | |
| 7369 | 7433 | if ( '' !== $info['INDEX_COMMENT'] ) { |
| @@ -7812,8 +7876,27 @@ | ||
| 7812 | 7876 | 'SQLSTATE[42S02]: Base table or view not found: 1146 ' . $driver_message, |
| 7813 | 7877 | '42S02', |
| 7814 | 7878 | $previous, |
| 7815 | 7879 | array( '42S02', 1146, $driver_message ) |
| 7880 | + ); | |
| 7881 | + } | |
| 7882 | + | |
| 7883 | + /** | |
| 7884 | + * Create a MySQL-compatible savepoint-not-found exception. | |
| 7885 | + * | |
| 7886 | + * @param string $savepoint_name The missing savepoint name, as an SQLite identifier. | |
| 7887 | + * @return WP_MySQL_On_SQLite_Exception | |
| 7888 | + */ | |
| 7889 | + private function new_savepoint_does_not_exist_exception( string $savepoint_name ): WP_MySQL_On_SQLite_Exception { | |
| 7890 | + $driver_message = sprintf( | |
| 7891 | + 'SAVEPOINT %s does not exist', | |
| 7892 | + $this->unquote_sqlite_identifier( $savepoint_name ) | |
| 7893 | + ); | |
| 7894 | + return $this->new_driver_exception( | |
| 7895 | + 'SQLSTATE[42000]: Syntax error or access violation: 1305 ' . $driver_message, | |
| 7896 | + '42000', | |
| 7897 | + null, | |
| 7898 | + array( '42000', 1305, $driver_message ) | |
| 7816 | 7899 | ); |
| 7817 | 7900 | } |
| 7818 | 7901 | |
| 7819 | 7902 | /** |