Skip to content
This repository was archived by the owner on Jun 2, 2025. It is now read-only.

Commit 6e5a8f5

Browse files
committed
Implement support for NO_BACKSLASH_ESCAPES SQL mode
1 parent 931c82b commit 6e5a8f5

File tree

4 files changed

+293
-9
lines changed

4 files changed

+293
-9
lines changed

tests/WP_SQLite_Driver_Tests.php

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4703,4 +4703,233 @@ public function testAlterTableDuplicateKeyNameWithUnique(): void {
47034703
$this->assertSame( "SQLSTATE[42000]: Syntax error or access violation: 1061 Duplicate key name 'idx'", $exception->getMessage() );
47044704
$this->assertSame( '42S21', $exception->getCode() );
47054705
}
4706+
4707+
public function testNoBackslashEscapesSqlMode(): void {
4708+
$backslash = chr( 92 );
4709+
4710+
$query = "SELECT
4711+
'''' AS value_1,
4712+
'{$backslash}\"' AS value_2,
4713+
'{$backslash}0' AS value_3,
4714+
'{$backslash}n' AS value_4,
4715+
'{$backslash}r' AS value_5,
4716+
'{$backslash}t' AS value_6,
4717+
'{$backslash}b' AS value_7,
4718+
'{$backslash}{$backslash}' AS value_8,
4719+
'🙂' AS value_9,
4720+
'{$backslash}🙂' AS value_10,
4721+
'{$backslash}%' AS value_11,
4722+
'{$backslash}_' AS value_12
4723+
";
4724+
4725+
// With NO_BACKSLASH_ESCAPES disabled:
4726+
$this->assertQuery( "SET SESSION sql_mode = ''" );
4727+
$result = $this->assertQuery( $query );
4728+
$this->assertSame( chr( 39 ), $result[0]->value_1 ); // single quote
4729+
$this->assertSame( chr( 34 ), $result[0]->value_2 ); // double quote
4730+
$this->assertSame( chr( 0 ), $result[0]->value_3 ); // ASCII NULL
4731+
$this->assertSame( chr( 10 ), $result[0]->value_4 ); // newline
4732+
$this->assertSame( chr( 13 ), $result[0]->value_5 ); // carriage return
4733+
$this->assertSame( chr( 9 ), $result[0]->value_6 ); // tab
4734+
$this->assertSame( chr( 8 ), $result[0]->value_7 ); // backspace
4735+
$this->assertSame( chr( 92 ), $result[0]->value_8 ); // backslash
4736+
$this->assertSame( '🙂', $result[0]->value_9 );
4737+
$this->assertSame( '🙂', $result[0]->value_10 );
4738+
4739+
// Characters "%" and "_" follow special escaping rules. Escape sequences
4740+
// "\%" and "\_" preserve the backslash so it can be used in some contexts.
4741+
$this->assertSame( $backslash . '%', $result[0]->value_11 );
4742+
$this->assertSame( $backslash . '_', $result[0]->value_12 );
4743+
4744+
// With NO_BACKSLASH_ESCAPES enabled:
4745+
$this->assertQuery( "SET SESSION sql_mode = 'NO_BACKSLASH_ESCAPES'" );
4746+
$result = $this->assertQuery( $query );
4747+
$this->assertSame( "'", $result[0]->value_1 );
4748+
$this->assertSame( $backslash . '"', $result[0]->value_2 );
4749+
$this->assertSame( $backslash . '0', $result[0]->value_3 );
4750+
$this->assertSame( $backslash . 'n', $result[0]->value_4 );
4751+
$this->assertSame( $backslash . 'r', $result[0]->value_5 );
4752+
$this->assertSame( $backslash . 't', $result[0]->value_6 );
4753+
$this->assertSame( $backslash . 'b', $result[0]->value_7 );
4754+
$this->assertSame( $backslash . $backslash, $result[0]->value_8 );
4755+
$this->assertSame( '🙂', $result[0]->value_9 );
4756+
$this->assertSame( $backslash . '🙂', $result[0]->value_10 );
4757+
$this->assertSame( $backslash . '%', $result[0]->value_11 );
4758+
$this->assertSame( $backslash . '_', $result[0]->value_12 );
4759+
}
4760+
4761+
public function testNoBackslashEscapesSqlModeWithPatternMatching(): void {
4762+
$backslash = chr( 92 );
4763+
4764+
$this->assertQuery( 'CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, value TEXT)' );
4765+
$this->assertQuery( "INSERT INTO t (value) VALUES ('abc')" );
4766+
$this->assertQuery( "INSERT INTO t (value) VALUES ('abc_')" );
4767+
$this->assertQuery( "INSERT INTO t (value) VALUES ('abc%')" );
4768+
$this->assertQuery( "INSERT INTO t (value) VALUES ('abc{$backslash}{$backslash}x')" ); // abc\x
4769+
4770+
/*
4771+
* 1. With NO_BACKSLASH_ESCAPES disabled:
4772+
*
4773+
* Backslashes serve as special escape characters on two levels:
4774+
*
4775+
* 1. In MySQL string literals.
4776+
* 2. In LIKE patterns.
4777+
*
4778+
* Additionally, "\_" and "\%" sequences preserve the backslash in MySQL
4779+
* string literals, making them equivalent to "\\_" and "\\%" sequences.
4780+
*
4781+
* Here's what that does to some escape sequences:
4782+
*
4783+
* "\_"
4784+
* 1) String literal resolves to: "\_" sequence
4785+
* 2) Pattern matching resolves to: "_" character
4786+
*
4787+
* "\\_"
4788+
* 1) String literal resolves to: "\_" sequence
4789+
* 2) Pattern matching resolves to: "_" character
4790+
*
4791+
* "\\\_"
4792+
* 1) String literal resolves to: "\\_" sequence
4793+
* 2) Pattern matching resolves to: "\" character + "_" wildcard
4794+
*
4795+
* "\\\\_"
4796+
* 1) String literal resolves to: "\\_" sequence
4797+
* 2) Pattern matching resolves to: "\" character + "_" wildcard
4798+
*
4799+
* The same rules applies to the "%" character.
4800+
*/
4801+
$this->assertQuery( "SET SESSION sql_mode = ''" );
4802+
4803+
// A "_" = a wildcard:
4804+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc_' ORDER BY id" );
4805+
$this->assertCount( 2, $result );
4806+
$this->assertSame( 'abc_', $result[0]->value );
4807+
$this->assertSame( 'abc%', $result[1]->value );
4808+
4809+
// A "\_" sequence = the "_" character:
4810+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc{$backslash}_'" );
4811+
$this->assertCount( 1, $result );
4812+
$this->assertSame( 'abc_', $result[0]->value );
4813+
4814+
// A "\\_" sequence = the "_" character:
4815+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc{$backslash}{$backslash}_'" );
4816+
$this->assertCount( 1, $result );
4817+
$this->assertSame( 'abc_', $result[0]->value );
4818+
4819+
// A "\\\_" sequence = the "\" character and a wildcard:
4820+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc{$backslash}{$backslash}{$backslash}_'" );
4821+
$this->assertCount( 1, $result );
4822+
$this->assertSame( "abc{$backslash}x", $result[0]->value );
4823+
4824+
// A "\\\\_" sequence = the "\" character and a wildcard:
4825+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc{$backslash}{$backslash}{$backslash}{$backslash}_'" );
4826+
$this->assertCount( 1, $result );
4827+
$this->assertSame( "abc{$backslash}x", $result[0]->value );
4828+
4829+
// A "\\\\\_" sequence = the "\" character and the "_" character:
4830+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc{$backslash}{$backslash}{$backslash}{$backslash}{$backslash}_'" );
4831+
$this->assertCount( 0, $result );
4832+
4833+
// A "%" = a wildcard:
4834+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc%' ORDER BY id" );
4835+
$this->assertCount( 4, $result );
4836+
$this->assertSame( 'abc', $result[0]->value );
4837+
$this->assertSame( 'abc_', $result[1]->value );
4838+
$this->assertSame( 'abc%', $result[2]->value );
4839+
$this->assertSame( "abc{$backslash}x", $result[3]->value );
4840+
4841+
// A "\%" sequence = the "%" character:
4842+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc{$backslash}%'" );
4843+
$this->assertCount( 1, $result );
4844+
$this->assertSame( 'abc%', $result[0]->value );
4845+
4846+
// A "\\%" sequence = the "%" character:
4847+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc{$backslash}{$backslash}%'" );
4848+
$this->assertCount( 1, $result );
4849+
$this->assertSame( 'abc%', $result[0]->value );
4850+
4851+
// A "\\\%" sequence = the "\" character and a wildcard:
4852+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc{$backslash}{$backslash}{$backslash}%'" );
4853+
$this->assertCount( 1, $result );
4854+
$this->assertSame( "abc{$backslash}x", $result[0]->value );
4855+
4856+
// A "\\\\%" sequence = the "\" character and a wildcard:
4857+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc{$backslash}{$backslash}{$backslash}{$backslash}%'" );
4858+
$this->assertCount( 1, $result );
4859+
$this->assertSame( "abc{$backslash}x", $result[0]->value );
4860+
4861+
// A "\\\\\%" sequence = the "\" character and the "%" character:
4862+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc{$backslash}{$backslash}{$backslash}{$backslash}{$backslash}%'" );
4863+
$this->assertCount( 0, $result );
4864+
4865+
// A "\x" sequence = the "x" character:
4866+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc{$backslash}x'" );
4867+
$this->assertCount( 0, $result );
4868+
4869+
// A "\\x" sequence = the "x" character:
4870+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc{$backslash}{$backslash}x'" );
4871+
$this->assertCount( 0, $result );
4872+
4873+
// A "\\\x" sequence = the "x" character:
4874+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc{$backslash}{$backslash}{$backslash}x'" );
4875+
$this->assertCount( 0, $result );
4876+
4877+
// A "\\\\x" sequence = the "\" character and the "x" character:
4878+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc{$backslash}{$backslash}{$backslash}{$backslash}x'" );
4879+
$this->assertCount( 1, $result );
4880+
$this->assertSame( "abc{$backslash}x", $result[0]->value );
4881+
4882+
/*
4883+
* 2. With NO_BACKSLASH_ESCAPES enabled:
4884+
*
4885+
* Backslashes don't serve as special escape characters at all:
4886+
*
4887+
* 1. No special meaning in MySQL string literals.
4888+
* 2. No special meaning in LIKE patterns.
4889+
* This can be overriden using the "ESCAPE ..." clause of the LIKE
4890+
* expression. This is not implemented in the SQLite driver yet.
4891+
*/
4892+
$this->assertQuery( "SET SESSION sql_mode = 'NO_BACKSLASH_ESCAPES'" );
4893+
4894+
// A "_" = a wildcard:
4895+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc_' ORDER BY id" );
4896+
$this->assertCount( 2, $result );
4897+
$this->assertSame( 'abc_', $result[0]->value );
4898+
$this->assertSame( 'abc%', $result[1]->value );
4899+
4900+
// A "\_" sequence = the "\" character and a wildcard:
4901+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc{$backslash}_'" );
4902+
$this->assertCount( 1, $result );
4903+
$this->assertSame( "abc{$backslash}x", $result[0]->value );
4904+
4905+
// A "\\_" sequence = two "\" characters and a wildcard:
4906+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc{$backslash}{$backslash}_'" );
4907+
$this->assertCount( 0, $result );
4908+
4909+
// A "%" = a wildcard:
4910+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc%' ORDER BY id" );
4911+
$this->assertCount( 4, $result );
4912+
$this->assertSame( 'abc', $result[0]->value );
4913+
$this->assertSame( 'abc_', $result[1]->value );
4914+
$this->assertSame( 'abc%', $result[2]->value );
4915+
$this->assertSame( "abc{$backslash}x", $result[3]->value );
4916+
4917+
// A "\%" sequence = the "\" character and a wildcard.
4918+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc{$backslash}%'" );
4919+
$this->assertCount( 1, $result );
4920+
$this->assertSame( "abc{$backslash}x", $result[0]->value );
4921+
4922+
// A "\\%" sequence = two "\" characters and a wildcard.
4923+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc{$backslash}{$backslash}%'" );
4924+
$this->assertCount( 0, $result );
4925+
4926+
// A "\x" sequence = the "\" and the "x" character.
4927+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc{$backslash}x'" );
4928+
$this->assertCount( 1, $result );
4929+
$this->assertSame( "abc{$backslash}x", $result[0]->value );
4930+
4931+
// A "\\x" sequence = two "\" characters and the "x" character.
4932+
$result = $this->assertQuery( "SELECT value FROM t WHERE value LIKE 'abc{$backslash}{$backslash}x'" );
4933+
$this->assertCount( 0, $result );
4934+
}
47064935
}

wp-includes/mysql/class-wp-mysql-lexer.php

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2130,7 +2130,7 @@ class WP_MySQL_Lexer {
21302130
*
21312131
* @var int
21322132
*/
2133-
private $sql_modes;
2133+
private $sql_modes = 0;
21342134

21352135
/**
21362136
* How many bytes from the original SQL payload have been read and tokenized.
@@ -2181,16 +2181,28 @@ class WP_MySQL_Lexer {
21812181
/**
21822182
* @param string $sql The SQL payload to tokenize.
21832183
* @param int $mysql_version The version of the MySQL server that the SQL payload is intended for.
2184-
* @param int $sql_modes The SQL modes that should be considered active during tokenization.
2184+
* @param string[] $sql_modes The SQL modes that should be considered active during tokenization.
21852185
*/
21862186
public function __construct(
21872187
string $sql,
21882188
int $mysql_version = 80038,
2189-
int $sql_modes = 0
2189+
array $sql_modes = array()
21902190
) {
21912191
$this->sql = $sql;
21922192
$this->mysql_version = $mysql_version;
2193-
$this->sql_modes = $sql_modes;
2193+
2194+
foreach ( $sql_modes as $sql_mode ) {
2195+
$sql_mode = strtoupper( $sql_mode );
2196+
if ( 'HIGH_NOT_PRECEDENCE' === $sql_mode ) {
2197+
$this->sql_modes |= self::SQL_MODE_HIGH_NOT_PRECEDENCE;
2198+
} elseif ( 'PIPES_AS_CONCAT' === $sql_mode ) {
2199+
$this->sql_modes |= self::SQL_MODE_PIPES_AS_CONCAT;
2200+
} elseif ( 'IGNORE_SPACE' === $sql_mode ) {
2201+
$this->sql_modes |= self::SQL_MODE_IGNORE_SPACE;
2202+
} elseif ( 'NO_BACKSLASH_ESCAPES' === $sql_mode ) {
2203+
$this->sql_modes |= self::SQL_MODE_NO_BACKSLASH_ESCAPES;
2204+
}
2205+
}
21942206
}
21952207

21962208
/**
@@ -2251,7 +2263,8 @@ public function get_token(): ?WP_MySQL_Token {
22512263
$this->token_type,
22522264
$this->token_starts_at,
22532265
$this->bytes_already_read - $this->token_starts_at,
2254-
$this->sql
2266+
$this->sql,
2267+
$this->is_sql_mode_active( self::SQL_MODE_NO_BACKSLASH_ESCAPES )
22552268
);
22562269
}
22572270

wp-includes/mysql/class-wp-mysql-token.php

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,33 @@
77
* and consumed by WP_MySQL_Parser during the parsing process.
88
*/
99
class WP_MySQL_Token extends WP_Parser_Token {
10+
/**
11+
* Whether the NO_BACKSLASH_ESCAPES SQL mode is enabled.
12+
*
13+
* @var bool
14+
*/
15+
private $sql_mode_no_backslash_escapes_enabled;
16+
17+
/**
18+
* Constructor.
19+
*
20+
* @param int $id Token type.
21+
* @param int $start Byte offset in the input where the token begins.
22+
* @param int $length Byte length of the token in the input.
23+
* @param string $input Input bytes from which the token was parsed.
24+
* @param bool $sql_mode_no_backslash_escapes_enabled Whether the NO_BACKSLASH_ESCAPES SQL mode is enabled.
25+
*/
26+
public function __construct(
27+
int $id,
28+
int $start,
29+
int $length,
30+
string $input,
31+
bool $sql_mode_no_backslash_escapes_enabled
32+
) {
33+
parent::__construct( $id, $start, $length, $input );
34+
$this->sql_mode_no_backslash_escapes_enabled = $sql_mode_no_backslash_escapes_enabled;
35+
}
36+
1037
/**
1138
* Get the name of the token.
1239
*
@@ -40,6 +67,15 @@ public function get_value(): string {
4067
$quote = $value[0];
4168
$value = substr( $value, 1, -1 );
4269

70+
/*
71+
* When the NO_BACKSLASH_ESCAPES SQL mode is enabled, we only need to
72+
* handle escaped bounding quotes, as the other characters preserve
73+
* their literal values.
74+
*/
75+
if ( $this->sql_mode_no_backslash_escapes_enabled ) {
76+
return str_replace( $quote . $quote, $quote, $value );
77+
}
78+
4379
/**
4480
* Unescape MySQL escape sequences.
4581
*
@@ -58,8 +94,6 @@ public function get_value(): string {
5894
* Despite looking similar, these rules are different from the C-style
5995
* string escaping, so we cannot use "strip(c)slashes()" in this case.
6096
*
61-
* @TODO: Handle NO_BACKSLASH_ESCAPES SQL mode.
62-
*
6397
* See: https://dev.mysql.com/doc/refman/8.4/en/string-literals.html
6498
*/
6599
$backslash = chr( 92 );

wp-includes/sqlite-ast/class-wp-sqlite-driver.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,11 @@ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mo
691691
* @return WP_MySQL_Parser A parser initialized for the MySQL query.
692692
*/
693693
public function create_parser( string $query ): WP_MySQL_Parser {
694-
$lexer = new WP_MySQL_Lexer( $query );
694+
$lexer = new WP_MySQL_Lexer(
695+
$query,
696+
80038,
697+
$this->active_sql_modes
698+
);
695699
$tokens = $lexer->remaining_tokens();
696700
return new WP_MySQL_Parser( self::$mysql_grammar, $tokens );
697701
}
@@ -2508,7 +2512,11 @@ private function translate_like( WP_Parser_Node $node ): string {
25082512
* We'll probably need to overload the like() function:
25092513
* https://www.sqlite.org/lang_corefunc.html#like
25102514
*/
2511-
return $this->translate_sequence( $node->get_children() ) . " ESCAPE '\\'";
2515+
$statement = $this->translate_sequence( $node->get_children() );
2516+
if ( $this->is_sql_mode_active( 'NO_BACKSLASH_ESCAPES' ) ) {
2517+
return $statement;
2518+
}
2519+
return $statement . " ESCAPE '\\'";
25122520
}
25132521

25142522
/**

0 commit comments

Comments
 (0)