From a78ae21b2d4663a7643151ee4ec62dfcecaf7329 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Fri, 13 Nov 2026 14:05:44 +0000 Subject: [PATCH 1/2] Lowercase what an equals where is looking for once, not for every value --- src/Stache/Query/Builder.php | 19 ++++++ tests/Data/Entries/EntryQueryBuilderTest.php | 62 ++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/src/Stache/Query/Builder.php b/src/Stache/Query/Builder.php index 9ce2f15ffc3..cc3b8b8a771 100644 --- a/src/Stache/Query/Builder.php +++ b/src/Stache/Query/Builder.php @@ -11,6 +11,7 @@ abstract class Builder extends BaseBuilder { protected $store; protected $randomize = false; + private static $hasOwnEqualityTest = []; public function __construct(Store $store) { @@ -148,6 +149,13 @@ protected function getItems($keys) protected function filterWhereBasic($values, $where) { + if ($this->comparesEqualityAsStrings($where)) { + // What's being looked for is the same for every value, so it's lowercased once rather than for each of them. + $needle = strtolower($where['value'] ?? ''); + + return $values->filter(fn ($value) => strtolower($value ?? '') === $needle); + } + return $values->filter(function ($value) use ($where) { $method = 'filterTest'.$this->operators[$where['operator']]; @@ -155,6 +163,17 @@ protected function filterWhereBasic($values, $where) }); } + private function comparesEqualityAsStrings($where) + { + if ($where['operator'] !== '=' || ! (is_null($where['value']) || is_scalar($where['value']))) { + return false; + } + + // A builder with its own idea of what equal means gets to use it. + return ! (self::$hasOwnEqualityTest[static::class] + ??= (new \ReflectionMethod($this, 'filterTestEquals'))->getDeclaringClass()->getName() !== BaseBuilder::class); + } + protected function filterWhereIn($values, $where) { $lookup = array_flip(array_map($this->normalizeLookupValue(...), $where['values'])); diff --git a/tests/Data/Entries/EntryQueryBuilderTest.php b/tests/Data/Entries/EntryQueryBuilderTest.php index a31ec9b505d..fb9edfb2397 100644 --- a/tests/Data/Entries/EntryQueryBuilderTest.php +++ b/tests/Data/Entries/EntryQueryBuilderTest.php @@ -1700,6 +1700,68 @@ public function sorting_by_unsafe_method_does_not_invoke_it() $this->assertCount($count, Entry::all()); } + + #[Test] + #[DataProvider('equalsProvider')] + public function entries_are_found_by_what_a_value_equals($value, $expected) + { + Collection::make('posts')->save(); + collect([ + 'a' => 'Red', 'b' => 'red', 'c' => 'RED ', 'd' => null, 'e' => '', 'f' => true, 'g' => false, + 'h' => 1, 'i' => '1', 'j' => 0, 'k' => 1.5, 'l' => '1.5', 'm' => 'Ünï', + ])->each(fn ($thing, $id) => EntryFactory::id($id)->slug($id)->collection('posts')->data(['thing' => $thing])->create()); + + $this->assertEquals($expected, Entry::query()->where('thing', $value)->get()->map->id()->all()); + $this->assertEquals($expected, Entry::query()->where('thing', '=', $value)->get()->map->id()->all()); + } + + public static function equalsProvider() + { + return [ + 'string' => ['red', ['a', 'b']], + 'string in another case' => ['RED', ['a', 'b']], + 'string with a space' => ['red ', ['c']], + 'empty string' => ['', ['d', 'e', 'g']], + 'true' => [true, ['f', 'h', 'i']], + 'false' => [false, ['d', 'e', 'g']], + 'one' => [1, ['f', 'h', 'i']], + 'one as a string' => ['1', ['f', 'h', 'i']], + 'zero' => [0, ['j']], + 'float' => [1.5, ['k', 'l']], + 'accented' => ['Ünï', ['m']], + ]; + } + + #[Test] + public function a_date_is_found_by_the_date_it_equals() + { + Collection::make('posts')->dated(true)->save(); + EntryFactory::id('a')->slug('a')->collection('posts')->date('2021-11-15')->create(); + EntryFactory::id('b')->slug('b')->collection('posts')->date('2021-11-14')->create(); + + $this->assertEquals(['a'], Entry::query()->where('date', \Illuminate\Support\Carbon::parse('2021-11-15'))->get()->map->id()->all()); + $this->assertEquals(['a'], Entry::query()->where('date', '2021-11-15 00:00:00')->get()->map->id()->all()); + } + + #[Test] + public function a_query_builder_with_its_own_idea_of_equal_gets_to_use_it() + { + Collection::make('posts')->save(); + EntryFactory::id('a')->slug('a')->collection('posts')->data(['thing' => 'Red'])->create(); + EntryFactory::id('b')->slug('b')->collection('posts')->data(['thing' => 'red'])->create(); + + $query = new CaseSensitiveEntryQueryBuilder(app(\Statamic\Stache\Stache::class)->store('entries')); + + $this->assertEquals(['b'], $query->where('thing', 'red')->get()->map->id()->all()); + } +} + +class CaseSensitiveEntryQueryBuilder extends \Statamic\Stache\Query\EntryQueryBuilder +{ + protected function filterTestEquals($item, $value) + { + return $item === $value; + } } class CustomScope extends Scope From 530680ee8c97113e0f5c88da33c80f51349cc404 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Fri, 13 Nov 2026 14:10:35 +0000 Subject: [PATCH 2/2] Cover looking for null --- tests/Data/Entries/EntryQueryBuilderTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Data/Entries/EntryQueryBuilderTest.php b/tests/Data/Entries/EntryQueryBuilderTest.php index fb9edfb2397..00d1a88f6cf 100644 --- a/tests/Data/Entries/EntryQueryBuilderTest.php +++ b/tests/Data/Entries/EntryQueryBuilderTest.php @@ -1722,6 +1722,7 @@ public static function equalsProvider() 'string in another case' => ['RED', ['a', 'b']], 'string with a space' => ['red ', ['c']], 'empty string' => ['', ['d', 'e', 'g']], + 'null' => [null, ['d', 'e', 'g']], 'true' => [true, ['f', 'h', 'i']], 'false' => [false, ['d', 'e', 'g']], 'one' => [1, ['f', 'h', 'i']],