PluginProbe
SQLite Database Integration / 3.1.0
SQLite Database Integration v3.1.0
3.1.0 3.0.2 3.0.1 trunk 2.1.13 2.1.14 2.1.15 2.1.16 2.2.0 2.2.1 2.2.10 2.2.11 2.2.12 2.2.13 2.2.14 2.2.15 2.2.16 2.2.17 2.2.18 2.2.19 2.2.2 2.2.20 2.2.21 2.2.22 2.2.23 All 33 releases
← 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 /**