diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 0f4683275..d6af72652 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -31,6 +31,15 @@ class SearchController extends Controller $this->middleware('auth'); } + /** + * Case-insensitive LIKE operator. PostgreSQL's LIKE is case-sensitive, so + * use ILIKE there to match MySQL's default case-insensitive behaviour. + */ + protected function likeOperator(): string + { + return config('database.default') === 'pgsql' ? 'ilike' : 'like'; + } + public function searchAPI(Request $request): JsonResponse { $this->validate($request, [ @@ -109,7 +118,7 @@ class SearchController extends Controller ->whereNull('in_reply_to_id') ->whereNull('reblog_of_id') ->whereProfileId(Auth::user()->profile_id) - ->where('caption', 'like', '%'.$tag.'%') + ->where('caption', $this->likeOperator(), '%'.$tag.'%') ->latest() ->limit(10) ->get(); @@ -140,7 +149,7 @@ class SearchController extends Controller $tokens = Cache::remember($key, $ttl, function () use ($tag) { $htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag; $hashtags = Hashtag::select('id', 'name', 'slug') - ->where('slug', 'like', '%'.$htag.'%') + ->where('slug', $this->likeOperator(), '%'.$htag.'%') ->whereHas('posts') ->limit(20) ->get(); @@ -170,7 +179,7 @@ class SearchController extends Controller // $tokens = Cache::remember($key, $ttl, function() use($tag) { $htag = Str::contains($tag, ',') == true ? explode(',', $tag) : [$tag]; $hashtags = Place::select('id', 'name', 'slug', 'country') - ->where('name', 'like', '%'.$htag[0].'%') + ->where('name', $this->likeOperator(), '%'.$htag[0].'%') ->paginate(20); $tags = []; if ($hashtags->count() > 0) { @@ -242,7 +251,7 @@ class SearchController extends Controller } $users = Profile::select('status', 'domain', 'username', 'name', 'id') ->whereNull('status') - ->where('username', 'like', '%'.$tag.'%') + ->where('username', $this->likeOperator(), '%'.$tag.'%') ->limit(20) ->orderBy('domain') ->get(); diff --git a/tests/Feature/Search/SearchOperatorTest.php b/tests/Feature/Search/SearchOperatorTest.php new file mode 100644 index 000000000..2b488d624 --- /dev/null +++ b/tests/Feature/Search/SearchOperatorTest.php @@ -0,0 +1,53 @@ + $driver]); + $controller = app(SearchController::class); + $m = new ReflectionMethod($controller, 'likeOperator'); + $m->setAccessible(true); + + return $m->invoke($controller); +} + +it('uses ILIKE on postgres', function () { + expect(likeOperatorFor('pgsql'))->toBe('ilike'); +}); + +it('uses LIKE on mysql', function () { + expect(likeOperatorFor('mysql'))->toBe('like'); +}); + +it('uses LIKE on sqlite', function () { + expect(likeOperatorFor('sqlite'))->toBe('like'); +}); + +it('search endpoint still responds on the default driver', function () { + $user = User::factory()->create(); + $user->refresh(); + + $this->actingAs($user) + ->getJson('/api/search?'.http_build_query([ + 'q' => 'photography', + 'src' => 'metro', + 'v' => 2, + 'scope' => 'hashtag', + ])) + ->assertOk(); +});