Quellcode durchsuchen

Trim SQL whitespace before parenthesis (#8522)

Alexandre Alapetite vor 1 Monat
Ursprung
Commit
d64a6c2751
2 geänderte Dateien mit 91 neuen und 91 gelöschten Zeilen
  1. 1 1
      app/Models/EntryDAO.php
  2. 90 90
      tests/app/Models/SearchTest.php

+ 1 - 1
app/Models/EntryDAO.php

@@ -1260,7 +1260,7 @@ SQL;
 					$isOpen = true;
 				}
 				// Remove superfluous leading 'AND '
-				$search .= '(' . substr($sub_search, 4) . ')';
+				$search .= '(' . trim(substr($sub_search, 4)) . ')';
 			}
 		}
 

+ 90 - 90
tests/app/Models/SearchTest.php

@@ -297,7 +297,7 @@ final class SearchTest extends \PHPUnit\Framework\TestCase {
 				],
 				'search:First OR search:Second',
 				[
-					'((e.author LIKE ? )) OR ((e.title LIKE ? ))',
+					'((e.author LIKE ?)) OR ((e.title LIKE ?))',
 					['%Alice%', '%World%'],
 				],
 			],
@@ -310,7 +310,7 @@ final class SearchTest extends \PHPUnit\Framework\TestCase {
 				],
 				'S:0,1 OR S:2,3,5',
 				[
-					'((e.author LIKE ? )) OR ((e.title LIKE ? )) OR ((e.link LIKE ? )) OR ((e.author LIKE ? ))',
+					'((e.author LIKE ?)) OR ((e.title LIKE ?)) OR ((e.link LIKE ?)) OR ((e.author LIKE ?))',
 					['%Alice%', '%World%', '%Example%', '%Bob%'],
 				],
 			],
@@ -321,7 +321,7 @@ final class SearchTest extends \PHPUnit\Framework\TestCase {
 				],
 				'intitle:Hello S:0,1 date:2025-10',
 				[
-					'((e.title LIKE ? )) AND ((e.author LIKE ? )) OR ((e.title LIKE ? )) AND ((e.id >= ? AND e.id <= ? ))',
+					'((e.title LIKE ?)) AND ((e.author LIKE ?)) OR ((e.title LIKE ?)) AND ((e.id >= ? AND e.id <= ?))',
 					['%Hello%', '%Alice%', '%World%', strtotime('2025-10-01') . '000000', (strtotime('2025-11-01') - 1) . '000000'],
 				],
 			],
@@ -465,157 +465,157 @@ final class SearchTest extends \PHPUnit\Framework\TestCase {
 		return [
 			[
 				'f:1 (f:2 OR f:3 OR f:4) (f:5 OR (f:6 OR f:7))',
-				' ((e.id_feed IN (?) )) AND ((e.id_feed IN (?) ) OR (e.id_feed IN (?) ) OR (e.id_feed IN (?) )) AND' .
-					' (((e.id_feed IN (?) )) OR ((e.id_feed IN (?) ) OR (e.id_feed IN (?) ))) ',
+				' ((e.id_feed IN (?))) AND ((e.id_feed IN (?)) OR (e.id_feed IN (?)) OR (e.id_feed IN (?))) AND' .
+					' (((e.id_feed IN (?))) OR ((e.id_feed IN (?)) OR (e.id_feed IN (?)))) ',
 				[1, 2, 3, 4, 5, 6, 7]
 			],
 			[
 				'c:1 OR c:2,3',
-				' (e.id_feed IN (SELECT f.id FROM `_feed` f WHERE f.category IN (?)) ) OR (e.id_feed IN (SELECT f.id FROM `_feed` f WHERE f.category IN (?,?)) ) ',
+				' (e.id_feed IN (SELECT f.id FROM `_feed` f WHERE f.category IN (?))) OR (e.id_feed IN (SELECT f.id FROM `_feed` f WHERE f.category IN (?,?))) ',
 				[1, 2, 3]
 			],
 			[
 				'#tag Hello OR (author:Alice inurl:example) OR (f:3 intitle:World) OR L:12',
-				" ((TRIM(e.tags) || ' #' LIKE ? AND (e.title LIKE ? OR e.content LIKE ?) )) OR ((e.author LIKE ? AND e.link LIKE ? )) OR" .
-					' ((e.id_feed IN (?) AND e.title LIKE ? )) OR ((e.id IN (SELECT et.id_entry FROM `_entrytag` et WHERE et.id_tag IN (?)) )) ',
+				" ((TRIM(e.tags) || ' #' LIKE ? AND (e.title LIKE ? OR e.content LIKE ?))) OR ((e.author LIKE ? AND e.link LIKE ?)) OR" .
+					' ((e.id_feed IN (?) AND e.title LIKE ?)) OR ((e.id IN (SELECT et.id_entry FROM `_entrytag` et WHERE et.id_tag IN (?)))) ',
 				['%tag #%', '%Hello%', '%Hello%', '%Alice%', '%example%', 3, '%World%', 12]
 			],
 			[
 				'#tag Hello (author:Alice inurl:example) (f:3 intitle:World) label:Bleu',
-				" ((TRIM(e.tags) || ' #' LIKE ? AND (e.title LIKE ? OR e.content LIKE ?) )) AND" .
-					' ((e.author LIKE ? AND e.link LIKE ? )) AND' .
-					' ((e.id_feed IN (?) AND e.title LIKE ? )) AND' .
-					' ((e.id IN (SELECT et.id_entry FROM `_entrytag` et, `_tag` t WHERE et.id_tag = t.id AND t.name IN (?)) )) ',
+				" ((TRIM(e.tags) || ' #' LIKE ? AND (e.title LIKE ? OR e.content LIKE ?))) AND" .
+					' ((e.author LIKE ? AND e.link LIKE ?)) AND' .
+					' ((e.id_feed IN (?) AND e.title LIKE ?)) AND' .
+					' ((e.id IN (SELECT et.id_entry FROM `_entrytag` et, `_tag` t WHERE et.id_tag = t.id AND t.name IN (?)))) ',
 				['%tag #%', '%Hello%', '%Hello%', '%Alice%', '%example%', 3, '%World%', 'Bleu']
 			],
 			[
 				'!((author:Alice intitle:hello) OR (author:Bob intitle:world))',
-				' NOT (((e.author LIKE ? AND e.title LIKE ? )) OR ((e.author LIKE ? AND e.title LIKE ? ))) ',
+				' NOT (((e.author LIKE ? AND e.title LIKE ?)) OR ((e.author LIKE ? AND e.title LIKE ?))) ',
 				['%Alice%', '%hello%', '%Bob%', '%world%'],
 			],
 			[
 				'(author:Alice intitle:hello) !(author:Bob intitle:world)',
-				' ((e.author LIKE ? AND e.title LIKE ? )) AND NOT ((e.author LIKE ? AND e.title LIKE ? )) ',
+				' ((e.author LIKE ? AND e.title LIKE ?)) AND NOT ((e.author LIKE ? AND e.title LIKE ?)) ',
 				['%Alice%', '%hello%', '%Bob%', '%world%'],
 			],
 			[
 				'intitle:"(test)"',
-				'(e.title LIKE ? )',
+				'(e.title LIKE ?)',
 				['%(test)%'],
 			],
 			[
 				'intitle:\'"hello world"\'',
-				'(e.title LIKE ? )',
+				'(e.title LIKE ?)',
 				['%"hello world"%'],
 			],
 			[
 				'intext:\'"hello world"\'',
-				'(e.content LIKE ? )',
+				'(e.content LIKE ?)',
 				['%"hello world"%'],
 			],
 			[
 				'(ab) OR (cd) OR (ef)',
-				'(((e.title LIKE ? OR e.content LIKE ?) )) OR (((e.title LIKE ? OR e.content LIKE ?) )) OR (((e.title LIKE ? OR e.content LIKE ?) ))',
+				'(((e.title LIKE ? OR e.content LIKE ?))) OR (((e.title LIKE ? OR e.content LIKE ?))) OR (((e.title LIKE ? OR e.content LIKE ?)))',
 				['%ab%', '%ab%', '%cd%', '%cd%', '%ef%', '%ef%'],
 			],
 			[
 				'("plain or text") OR (cd)',
-				'(((e.title LIKE ? OR e.content LIKE ?) )) OR (((e.title LIKE ? OR e.content LIKE ?) ))',
+				'(((e.title LIKE ? OR e.content LIKE ?))) OR (((e.title LIKE ? OR e.content LIKE ?)))',
 				['%plain or text%', '%plain or text%', '%cd%', '%cd%'],
 			],
 			[
 				'"plain or text" OR cd',
-				'((e.title LIKE ? OR e.content LIKE ?) ) OR ((e.title LIKE ? OR e.content LIKE ?) )',
+				'((e.title LIKE ? OR e.content LIKE ?)) OR ((e.title LIKE ? OR e.content LIKE ?))',
 				['%plain or text%', '%plain or text%', '%cd%', '%cd%'],
 			],
 			[
 				'"plain OR text" OR cd',
-				'((e.title LIKE ? OR e.content LIKE ?) ) OR ((e.title LIKE ? OR e.content LIKE ?) ) ',
+				'((e.title LIKE ? OR e.content LIKE ?)) OR ((e.title LIKE ? OR e.content LIKE ?)) ',
 				['%plain OR text%', '%plain OR text%', '%cd%', '%cd%'],
 			],
 			[
 				'ab OR cd OR (ef)',
-				'(((e.title LIKE ? OR e.content LIKE ?) )) OR (((e.title LIKE ? OR e.content LIKE ?) )) OR (((e.title LIKE ? OR e.content LIKE ?) )) ',
+				'(((e.title LIKE ? OR e.content LIKE ?))) OR (((e.title LIKE ? OR e.content LIKE ?))) OR (((e.title LIKE ? OR e.content LIKE ?))) ',
 				['%ab%', '%ab%', '%cd%', '%cd%', '%ef%', '%ef%'],
 			],
 			[
 				'ab OR cd OR ef',
-				'((e.title LIKE ? OR e.content LIKE ?) ) OR ((e.title LIKE ? OR e.content LIKE ?) ) OR ((e.title LIKE ? OR e.content LIKE ?) )',
+				'((e.title LIKE ? OR e.content LIKE ?)) OR ((e.title LIKE ? OR e.content LIKE ?)) OR ((e.title LIKE ? OR e.content LIKE ?))',
 				['%ab%', '%ab%', '%cd%', '%cd%', '%ef%', '%ef%'],
 			],
 			[
 				'(ab) cd OR ef OR (gh)',
-				'(((e.title LIKE ? OR e.content LIKE ?) )) AND (((e.title LIKE ? OR e.content LIKE ?) )) ' .
-					'OR (((e.title LIKE ? OR e.content LIKE ?) )) OR (((e.title LIKE ? OR e.content LIKE ?) ))',
+				'(((e.title LIKE ? OR e.content LIKE ?))) AND (((e.title LIKE ? OR e.content LIKE ?))) ' .
+					'OR (((e.title LIKE ? OR e.content LIKE ?))) OR (((e.title LIKE ? OR e.content LIKE ?)))',
 				['%ab%', '%ab%', '%cd%', '%cd%', '%ef%', '%ef%', '%gh%', '%gh%'],
 			],
 			[
 				'(ab) OR cd OR ef OR (gh)',
-				'(((e.title LIKE ? OR e.content LIKE ?) )) OR (((e.title LIKE ? OR e.content LIKE ?) )) ' .
-					'OR (((e.title LIKE ? OR e.content LIKE ?) )) OR (((e.title LIKE ? OR e.content LIKE ?) ))',
+				'(((e.title LIKE ? OR e.content LIKE ?))) OR (((e.title LIKE ? OR e.content LIKE ?))) ' .
+					'OR (((e.title LIKE ? OR e.content LIKE ?))) OR (((e.title LIKE ? OR e.content LIKE ?)))',
 				['%ab%', '%ab%', '%cd%', '%cd%', '%ef%', '%ef%', '%gh%', '%gh%'],
 			],
 			[
 				'ab OR (!(cd OR ef))',
-				'(((e.title LIKE ? OR e.content LIKE ?) )) OR (NOT (((e.title LIKE ? OR e.content LIKE ?) ) OR ((e.title LIKE ? OR e.content LIKE ?) )))',
+				'(((e.title LIKE ? OR e.content LIKE ?))) OR (NOT (((e.title LIKE ? OR e.content LIKE ?)) OR ((e.title LIKE ? OR e.content LIKE ?))))',
 				['%ab%', '%ab%', '%cd%', '%cd%', '%ef%', '%ef%'],
 			],
 			[
 				'ab !(cd OR ef)',
-				'(((e.title LIKE ? OR e.content LIKE ?) )) AND NOT (((e.title LIKE ? OR e.content LIKE ?) ) OR ((e.title LIKE ? OR e.content LIKE ?) ))',
+				'(((e.title LIKE ? OR e.content LIKE ?))) AND NOT (((e.title LIKE ? OR e.content LIKE ?)) OR ((e.title LIKE ? OR e.content LIKE ?)))',
 				['%ab%', '%ab%', '%cd%', '%cd%', '%ef%', '%ef%'],
 			],
 			[
 				'ab OR !(cd OR ef)',
-				'(((e.title LIKE ? OR e.content LIKE ?) )) OR NOT (((e.title LIKE ? OR e.content LIKE ?) ) OR ((e.title LIKE ? OR e.content LIKE ?) ))',
+				'(((e.title LIKE ? OR e.content LIKE ?))) OR NOT (((e.title LIKE ? OR e.content LIKE ?)) OR ((e.title LIKE ? OR e.content LIKE ?)))',
 				['%ab%', '%ab%', '%cd%', '%cd%', '%ef%', '%ef%'],
 			],
 			[
 				'(ab (!cd OR ef OR (gh))) OR !(ij OR kl)',
-				'((((e.title LIKE ? OR e.content LIKE ?) )) AND (((e.title NOT LIKE ? AND e.content NOT LIKE ? )) OR (((e.title LIKE ? OR e.content LIKE ?) )) ' .
-					'OR (((e.title LIKE ? OR e.content LIKE ?) )))) OR NOT (((e.title LIKE ? OR e.content LIKE ?) ) OR ((e.title LIKE ? OR e.content LIKE ?) ))',
+				'((((e.title LIKE ? OR e.content LIKE ?))) AND (((e.title NOT LIKE ? AND e.content NOT LIKE ?)) OR (((e.title LIKE ? OR e.content LIKE ?))) ' .
+					'OR (((e.title LIKE ? OR e.content LIKE ?))))) OR NOT (((e.title LIKE ? OR e.content LIKE ?)) OR ((e.title LIKE ? OR e.content LIKE ?)))',
 				['%ab%', '%ab%', '%cd%', '%cd%', '%ef%', '%ef%', '%gh%', '%gh%', '%ij%', '%ij%', '%kl%', '%kl%'],
 			],
 			[
 				'"ab" "cd" ("ef") intitle:"gh" !"ij" -"kl"',
-				'(((e.title LIKE ? OR e.content LIKE ?) AND (e.title LIKE ? OR e.content LIKE ?) )) AND (((e.title LIKE ? OR e.content LIKE ?) )) ' .
-					'AND ((e.title LIKE ? AND e.title NOT LIKE ? AND e.content NOT LIKE ? AND e.title NOT LIKE ? AND e.content NOT LIKE ? ))',
+				'(((e.title LIKE ? OR e.content LIKE ?) AND (e.title LIKE ? OR e.content LIKE ?))) AND (((e.title LIKE ? OR e.content LIKE ?))) ' .
+					'AND ((e.title LIKE ? AND e.title NOT LIKE ? AND e.content NOT LIKE ? AND e.title NOT LIKE ? AND e.content NOT LIKE ?))',
 				['%ab%', '%ab%', '%cd%', '%cd%', '%ef%', '%ef%', '%gh%', '%ij%', '%ij%', '%kl%', '%kl%']
 			],
 			[
 				'intitle:"é & \' è" intext:/<&>/ \'< & " >\'',
-				'(e.title LIKE ? AND e.content ~ ? AND (e.title LIKE ? OR e.content LIKE ?) )',
+				'(e.title LIKE ? AND e.content ~ ? AND (e.title LIKE ? OR e.content LIKE ?))',
 				['%é &amp; \' è%', '<&>', '%&lt; &amp; " &gt;%', '%&lt; &amp; " &gt;%']
 			],
 			[
 				'/^(ab|cd) [(] \\) (ef|gh)/',
-				'((e.title ~ ? OR e.content ~ ?) )',
+				'((e.title ~ ? OR e.content ~ ?))',
 				['^(ab|cd) [(] \\) (ef|gh)', '^(ab|cd) [(] \\) (ef|gh)']
 			],
 			[
 				'!/^(ab|cd)/',
-				'(NOT e.title ~ ? AND NOT e.content ~ ? )',
+				'(NOT e.title ~ ? AND NOT e.content ~ ?)',
 				['^(ab|cd)', '^(ab|cd)']
 			],
 			[
 				'intitle:/^(ab|cd)/',
-				'(e.title ~ ? )',
+				'(e.title ~ ?)',
 				['^(ab|cd)']
 			],
 			[
 				'intext:/^(ab|cd)/',
-				'(e.content ~ ? )',
+				'(e.content ~ ?)',
 				['^(ab|cd)']
 			],
 			[
 				'L:1 L:2',
 				'(e.id IN (SELECT et.id_entry FROM `_entrytag` et WHERE et.id_tag IN (?)) AND ' .
-					'e.id IN (SELECT et.id_entry FROM `_entrytag` et WHERE et.id_tag IN (?)) )',
+					'e.id IN (SELECT et.id_entry FROM `_entrytag` et WHERE et.id_tag IN (?)))',
 				[1, 2]
 			],
 			[
 				'L:1,2',
-				'(e.id IN (SELECT et.id_entry FROM `_entrytag` et WHERE et.id_tag IN (?,?)) )',
+				'(e.id IN (SELECT et.id_entry FROM `_entrytag` et WHERE et.id_tag IN (?,?)))',
 				[1, 2]
 			],
 		];
@@ -637,98 +637,98 @@ final class SearchTest extends \PHPUnit\Framework\TestCase {
 			// Basic date operator tests
 			[
 				'date:2007-03-01/2008-05-11',
-				'(e.id >= ? AND e.id <= ? )',
+				'(e.id >= ? AND e.id <= ?)',
 				[strtotime('2007-03-01T00:00:00Z') . '000000', strtotime('2008-05-11T23:59:59Z') . '000000'],
 			],
 			[
 				'date:2007-03-01/',
-				'(e.id >= ? )',
+				'(e.id >= ?)',
 				[strtotime('2007-03-01T00:00:00Z') . '000000'],
 			],
 			[
 				'date:/2008-05-11',
-				'(e.id <= ? )',
+				'(e.id <= ?)',
 				[strtotime('2008-05-11T23:59:59Z') . '000000'],
 			],
 			// Basic pubdate operator tests
 			[
 				'pubdate:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z',
-				'(e.date >= ? AND e.date <= ? )',
+				'(e.date >= ? AND e.date <= ?)',
 				[strtotime('2007-03-01T13:00:00Z'), strtotime('2008-05-11T15:30:00Z')],
 			],
 			[
 				'pubdate:2007-03-01/',
-				'(e.date >= ? )',
+				'(e.date >= ?)',
 				[strtotime('2007-03-01T00:00:00Z')],
 			],
 			[
 				'pubdate:/2008-05-11',
-				'(e.date <= ? )',
+				'(e.date <= ?)',
 				[strtotime('2008-05-11T23:59:59Z')],
 			],
 			// Basic userdate operator tests
 			[
 				'userdate:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z',
-				'(e.`lastUserModified` >= ? AND e.`lastUserModified` <= ? )',
+				'(e.`lastUserModified` >= ? AND e.`lastUserModified` <= ?)',
 				[strtotime('2007-03-01T13:00:00Z'), strtotime('2008-05-11T15:30:00Z')],
 			],
 			[
 				'userdate:2007-03-01/',
-				'(e.`lastUserModified` >= ? )',
+				'(e.`lastUserModified` >= ?)',
 				[strtotime('2007-03-01T00:00:00Z')],
 			],
 			[
 				'userdate:/2008-05-11',
-				'(e.`lastUserModified` <= ? )',
+				'(e.`lastUserModified` <= ?)',
 				[strtotime('2008-05-11T23:59:59Z')],
 			],
 			// Negative date operator tests
 			[
 				'-date:2007-03-01/2008-05-11',
-				'((e.id < ? OR e.id > ?) )',
+				'((e.id < ? OR e.id > ?))',
 				[strtotime('2007-03-01T00:00:00Z') . '000000', strtotime('2008-05-11T23:59:59Z') . '000000'],
 			],
 			[
 				'!pubdate:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z',
-				'((e.date < ? OR e.date > ?) )',
+				'((e.date < ? OR e.date > ?))',
 				[strtotime('2007-03-01T13:00:00Z'), strtotime('2008-05-11T15:30:00Z')],
 			],
 			[
 				'!userdate:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z',
-				'((e.`lastUserModified` < ? OR e.`lastUserModified` > ?) )',
+				'((e.`lastUserModified` < ? OR e.`lastUserModified` > ?))',
 				[strtotime('2007-03-01T13:00:00Z'), strtotime('2008-05-11T15:30:00Z')],
 			],
 			// Combined date operators
 			[
 				'date:2007-03-01/ pubdate:/2008-05-11',
-				'(e.id >= ? AND e.date <= ? )',
+				'(e.id >= ? AND e.date <= ?)',
 				[strtotime('2007-03-01T00:00:00Z') . '000000', strtotime('2008-05-11T23:59:59Z')],
 			],
 			[
 				'pubdate:2007-03-01/ userdate:/2008-05-11',
-				'(e.date >= ? AND e.`lastUserModified` <= ? )',
+				'(e.date >= ? AND e.`lastUserModified` <= ?)',
 				[strtotime('2007-03-01T00:00:00Z'), strtotime('2008-05-11T23:59:59Z')],
 			],
 			[
 				'date:2007-03-01/ userdate:2007-06-01/',
-				'(e.id >= ? AND e.`lastUserModified` >= ? )',
+				'(e.id >= ? AND e.`lastUserModified` >= ?)',
 				[strtotime('2007-03-01T00:00:00Z') . '000000', strtotime('2007-06-01T00:00:00Z')],
 			],
 			// Complex combinations with other operators
 			[
 				'intitle:test date:2007-03-01/ pubdate:/2008-05-11',
-				'(e.id >= ? AND e.date <= ? AND e.title LIKE ? )',
+				'(e.id >= ? AND e.date <= ? AND e.title LIKE ?)',
 				[strtotime('2007-03-01T00:00:00Z') . '000000', strtotime('2008-05-11T23:59:59Z'), '%test%'],
 			],
 			[
 				'author:john userdate:2007-03-01/2008-05-11',
-				'(e.`lastUserModified` >= ? AND e.`lastUserModified` <= ? AND e.author LIKE ? )',
+				'(e.`lastUserModified` >= ? AND e.`lastUserModified` <= ? AND e.author LIKE ?)',
 				[strtotime('2007-03-01T00:00:00Z'), strtotime('2008-05-11T23:59:59Z'), '%john%'],
 			],
 			// Mixed positive and negative date operators
 			[
 				'date:2007-03-01/ !pubdate:2008-01-01/2008-05-11',
-				'(e.id >= ? AND (e.date < ? OR e.date > ?) )',
+				'(e.id >= ? AND (e.date < ? OR e.date > ?))',
 				[strtotime('2007-03-01T00:00:00Z') . '000000', strtotime('2008-01-01T00:00:00Z'), strtotime('2008-05-11T23:59:59Z')],
 			],
 		];
@@ -749,87 +749,87 @@ final class SearchTest extends \PHPUnit\Framework\TestCase {
 		return [
 			[
 				'intitle:/^ab$/',
-				'(e.title ~ ? )',
+				'(e.title ~ ?)',
 				['^ab$']
 			],
 			[
 				'intitle:/^ab$/i',
-				'(e.title ~* ? )',
+				'(e.title ~* ?)',
 				['^ab$']
 			],
 			[
 				'intitle:/^ab$/m',
-				'(e.title ~ ? )',
+				'(e.title ~ ?)',
 				['(?m)^ab$']
 			],
 			[
 				'intitle:/^ab\\M/',
-				'(e.title ~ ? )',
+				'(e.title ~ ?)',
 				['^ab\\M']
 			],
 			[
 				'intext:/^ab\\M/',
-				'(e.content ~ ? )',
+				'(e.content ~ ?)',
 				['^ab\\M']
 			],
 			[
 				'intitle:/\\b\\d+/',
-				'(e.title ~ ? )',
+				'(e.title ~ ?)',
 				['\\y\\d+']
 			],
 			[
 				'author:/^ab$/',
-				"(REPLACE(e.author, ';', '\n') ~ ? )",
+				"(REPLACE(e.author, ';', '\n') ~ ?)",
 				['^ab$']
 			],
 			[
 				'inurl:/^ab$/',
-				'(e.link ~ ? )',
+				'(e.link ~ ?)',
 				['^ab$']
 			],
 			[
 				'/^ab$/',
-				'((e.title ~ ? OR e.content ~ ?) )',
+				'((e.title ~ ? OR e.content ~ ?))',
 				['^ab$', '^ab$']
 			],
 			[
 				'!/^ab$/',
-				'(NOT e.title ~ ? AND NOT e.content ~ ? )',
+				'(NOT e.title ~ ? AND NOT e.content ~ ?)',
 				['^ab$', '^ab$']
 			],
 			[
 				'#/^a(b|c)$/im',
-				"(REPLACE(REPLACE(e.tags, ' #', '#'), '#', '\n') ~* ? )",
+				"(REPLACE(REPLACE(e.tags, ' #', '#'), '#', '\n') ~* ?)",
 				['(?m)^a(b|c)$']
 			],
 			[	// Not a regex
 				'inurl:https://example.net/test/',
-				'(e.link LIKE ? )',
+				'(e.link LIKE ?)',
 				['%https://example.net/test/%']
 			],
 			[	// Not a regex
 				'https://example.net/test/',
-				'((e.title LIKE ? OR e.content LIKE ?) )',
+				'((e.title LIKE ? OR e.content LIKE ?))',
 				['%https://example.net/test/%', '%https://example.net/test/%']
 			],
 			[	// Not a regex
 				"author:'/u/Alice'",
-				"(e.author LIKE ? )",
+				"(e.author LIKE ?)",
 				['%/u/Alice%'],
 			],
 			[	// Regex with literal 'or'
 				'intitle:/^A or B/i',
-				'(e.title ~* ? )',
+				'(e.title ~* ?)',
 				['^A or B']
 			],
 			[	// Regex with literal 'OR'
 				'intitle:/^A B OR C D/i OR intitle:/^A B OR C D/i',
-				'(e.title ~* ? ) OR (e.title ~* ? )',
+				'(e.title ~* ?) OR (e.title ~* ?)',
 				['^A B OR C D', '^A B OR C D']
 			],
 			[	// Quote with literal 'OR'
 				'intitle:"A B OR C D" OR intitle:"E or F"',
-				'(e.title LIKE ? ) OR (e.title LIKE ? )',
+				'(e.title LIKE ?) OR (e.title LIKE ?)',
 				['%A B OR C D%', '%E or F%']
 			],
 		];
@@ -852,27 +852,27 @@ final class SearchTest extends \PHPUnit\Framework\TestCase {
 		return [
 			[
 				'intitle:/^ab$/',
-				"(e.title REGEXP ? )",
+				"(e.title REGEXP ?)",
 				['(?-i)^ab$']
 			],
 			[
 				'intitle:/^ab$/i',
-				"(e.title REGEXP ? )",
+				"(e.title REGEXP ?)",
 				['(?i)^ab$']
 			],
 			[
 				'intitle:/^ab$/m',
-				"(e.title REGEXP ? )",
+				"(e.title REGEXP ?)",
 				['(?-i)(?m)^ab$']
 			],
 			[
 				'intitle:/\\b\\d+/',
-				"(e.title REGEXP ? )",
+				"(e.title REGEXP ?)",
 				['(?-i)\\b\\d+']
 			],
 			[
 				'intext:/^ab$/m',
-				'(UNCOMPRESS(e.content_bin) REGEXP ?) )',
+				'(UNCOMPRESS(e.content_bin) REGEXP ?))',
 				['(?-i)(?m)^ab$']
 			],
 		];
@@ -895,22 +895,22 @@ final class SearchTest extends \PHPUnit\Framework\TestCase {
 		return [
 			[
 				'intitle:/^ab$/',
-				"(REGEXP_LIKE(e.title,?,'c') )",
+				"(REGEXP_LIKE(e.title,?,'c'))",
 				['^ab$']
 			],
 			[
 				'intitle:/^ab$/i',
-				"(REGEXP_LIKE(e.title,?,'i') )",
+				"(REGEXP_LIKE(e.title,?,'i'))",
 				['^ab$']
 			],
 			[
 				'intitle:/^ab$/m',
-				"(REGEXP_LIKE(e.title,?,'mc') )",
+				"(REGEXP_LIKE(e.title,?,'mc'))",
 				['^ab$']
 			],
 			[
 				'intext:/^ab$/m',
-				"(REGEXP_LIKE(UNCOMPRESS(e.content_bin),?,'mc')) )",
+				"(REGEXP_LIKE(UNCOMPRESS(e.content_bin),?,'mc')))",
 				['^ab$']
 			],
 		];
@@ -931,27 +931,27 @@ final class SearchTest extends \PHPUnit\Framework\TestCase {
 		return [
 			[
 				'intitle:/^ab$/',
-				"(e.title REGEXP ? )",
+				"(e.title REGEXP ?)",
 				['/^ab$/']
 			],
 			[
 				'intitle:/^ab$/i',
-				"(e.title REGEXP ? )",
+				"(e.title REGEXP ?)",
 				['/^ab$/i']
 			],
 			[
 				'intitle:/^ab$/m',
-				"(e.title REGEXP ? )",
+				"(e.title REGEXP ?)",
 				['/^ab$/m']
 			],
 			[
 				'intitle:/^ab\\b/',
-				'(e.title REGEXP ? )',
+				'(e.title REGEXP ?)',
 				['/^ab\\b/']
 			],
 			[
 				'intext:/^ab\\b/',
-				'(e.content REGEXP ? )',
+				'(e.content REGEXP ?)',
 				['/^ab\\b/']
 			],
 		];