Use whereLike for search so MySQL works alongside PostgreSQL (#302)
Seven search call sites used the `ilike` operator, which only PostgreSQL understands. On MySQL they raise a syntax error, so post, asset, label, signature and workspace-member search — plus the MCP list-posts tool — were unusable on an engine `config/database.php` has always supported and the docs advertise. Replace them with `whereLike($column, $value)`, which the query grammars translate per driver: PostgresGrammar emits `ilike` and MySqlGrammar emits `like`. The generated SQL on PostgreSQL is therefore unchanged. Verified by running the full suite on both engines: PostgreSQL 16 3888 passed, 0 failed MySQL 8.0.46 one pre-existing failure fixed, none introduced Also adds case-insensitivity assertions to the five affected suites that lacked them, and search coverage for ListPostsTool, which had none. Note for MySQL installs: `like` is case-insensitive by virtue of the column collation, not the operator. Under the default `utf8mb4_unicode_ci` it is also accent-insensitive, so a search for "cafe" matches a stored "café" — PostgreSQL's `ilike` does not. That difference comes from the collation rather than this change. A `_bin` or `_cs` collation would make search case-sensitive on both. Co-authored-by: Paulo Castellano <paulo@castellanos.llc>
This commit is contained in:
parent
02e44b9785
commit
6e10e394a9
12 changed files with 108 additions and 7 deletions
|
|
@ -27,7 +27,7 @@ public static function query(Workspace $workspace, ?string $search = null, ?stri
|
|||
->where('mediable_type', Relation::getMorphAlias(Workspace::class))
|
||||
->where('mediable_id', $workspace->id)
|
||||
->where('collection', 'assets')
|
||||
->when(filled($search), fn (Builder $query) => $query->where('original_filename', 'ilike', '%'.trim($search).'%'))
|
||||
->when(filled($search), fn (Builder $query) => $query->whereLike('original_filename', '%'.trim($search).'%'))
|
||||
->when(filled($type), fn (Builder $query) => $query->where('type', $type))
|
||||
->latest()
|
||||
->orderByDesc('id');
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public function search(Request $request): AnonymousResourceCollection
|
|||
$type = $request->input('type');
|
||||
|
||||
$assets = $workspace->getMedia('assets')
|
||||
->when($term !== '', fn ($query) => $query->where('original_filename', 'ilike', '%'.$term.'%'))
|
||||
->when($term !== '', fn ($query) => $query->whereLike('original_filename', '%'.$term.'%'))
|
||||
->when(in_array($type, ['image', 'video'], true), fn ($query) => $query->where('type', $type))
|
||||
->latest()
|
||||
->paginate(config('app.pagination.default'));
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ public function index(Request $request, ?string $status = null): Response|Redire
|
|||
}
|
||||
|
||||
if ($search = $request->input('search')) {
|
||||
$query->where('content', 'ilike', "%{$search}%");
|
||||
$query->whereLike('content', "%{$search}%");
|
||||
}
|
||||
|
||||
$labelIds = $request->collect('labels')
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ public function searchMembers(Request $request): AnonymousResourceCollection
|
|||
|
||||
$members = $workspace->members()
|
||||
->where('users.id', '!=', $request->user()->id)
|
||||
->when($term !== '', fn ($query) => $query->where('users.name', 'ilike', '%'.$term.'%'))
|
||||
->when($term !== '', fn ($query) => $query->whereLike('users.name', '%'.$term.'%'))
|
||||
->orderBy('users.name')
|
||||
->limit(50)
|
||||
->get(['users.id', 'users.name', 'users.email']);
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public function index(Request $request): Response|RedirectResponse
|
|||
$this->authorize('createPost', $workspace);
|
||||
|
||||
$labels = $workspace->labels()
|
||||
->when($request->input('search'), fn ($query, $search) => $query->where('name', 'ilike', "%{$search}%"))
|
||||
->when($request->input('search'), fn ($query, $search) => $query->whereLike('name', "%{$search}%"))
|
||||
->latest()
|
||||
->paginate(config('app.pagination.default'));
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public function index(Request $request): Response|RedirectResponse
|
|||
$this->authorize('createPost', $workspace);
|
||||
|
||||
$signatures = $workspace->signatures()
|
||||
->when($request->input('search'), fn ($query, $search) => $query->where('name', 'ilike', "%{$search}%"))
|
||||
->when($request->input('search'), fn ($query, $search) => $query->whereLike('name', "%{$search}%"))
|
||||
->latest()
|
||||
->paginate(config('app.pagination.default'));
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ public function handle(Request $request): ResponseFactory
|
|||
};
|
||||
|
||||
if ($search = data_get($validated, 'search')) {
|
||||
$query->where('content', 'ilike', '%'.$search.'%');
|
||||
$query->whereLike('content', '%'.$search.'%');
|
||||
}
|
||||
|
||||
$posts = $query->latest('scheduled_at')
|
||||
|
|
|
|||
|
|
@ -66,6 +66,25 @@
|
|||
->assertJsonPath('data.0.original_filename', 'campaign-hero.jpg');
|
||||
});
|
||||
|
||||
test('filters assets by filename search case-insensitively', function () {
|
||||
Media::factory()->assets()->create([
|
||||
'mediable_type' => (new Workspace)->getMorphClass(),
|
||||
'mediable_id' => $this->workspace->id,
|
||||
'original_filename' => 'CAMPAIGN-Hero.jpg',
|
||||
]);
|
||||
Media::factory()->assets()->create([
|
||||
'mediable_type' => (new Workspace)->getMorphClass(),
|
||||
'mediable_id' => $this->workspace->id,
|
||||
'original_filename' => 'office-shot.jpg',
|
||||
]);
|
||||
|
||||
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
||||
->getJson(route('api.assets.index', ['search' => 'campaign-hero']))
|
||||
->assertOk()
|
||||
->assertJsonCount(1, 'data')
|
||||
->assertJsonPath('data.0.original_filename', 'CAMPAIGN-Hero.jpg');
|
||||
});
|
||||
|
||||
test('paginates assets with the application page size', function () {
|
||||
$perPage = (int) config('app.pagination.default');
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,18 @@
|
|||
$response->assertJsonPath('data.0.id', $matching->id);
|
||||
});
|
||||
|
||||
test('assets search matches filenames case-insensitively', function () {
|
||||
$matching = $this->workspace->addMedia(UploadedFile::fake()->image('VACATION-Beach.jpg'), 'assets');
|
||||
$this->workspace->addMedia(UploadedFile::fake()->image('office-shot.jpg'), 'assets');
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->getJson(route('app.assets.search', ['search' => 'vacation']));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonCount(1, 'data');
|
||||
$response->assertJsonPath('data.0.id', $matching->id);
|
||||
});
|
||||
|
||||
test('assets search filters by type', function () {
|
||||
$this->workspace->addMedia(UploadedFile::fake()->image('photo.jpg'), 'assets');
|
||||
$this->workspace->addMedia(UploadedFile::fake()->create('clip.mp4', 100, 'video/mp4'), 'assets');
|
||||
|
|
|
|||
|
|
@ -65,6 +65,50 @@
|
|||
});
|
||||
});
|
||||
|
||||
test('list posts filters by content search', function () {
|
||||
$matching = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'content' => 'Hello marketing world',
|
||||
]);
|
||||
Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'content' => 'Something else entirely',
|
||||
]);
|
||||
|
||||
$response = TryPostServer::actingAs($this->user)
|
||||
->tool(ListPostsTool::class, ['search' => 'marketing']);
|
||||
|
||||
$response->assertOk()
|
||||
->assertStructuredContent(function (AssertableJson $json) use ($matching) {
|
||||
$json->has('posts', 1, function (AssertableJson $post) use ($matching) {
|
||||
$post->where('id', $matching->id)->etc();
|
||||
})->etc();
|
||||
});
|
||||
});
|
||||
|
||||
test('list posts search is case insensitive', function () {
|
||||
Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'content' => 'MARKETING CAMPAIGN',
|
||||
]);
|
||||
Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'content' => 'Something else entirely',
|
||||
]);
|
||||
|
||||
$response = TryPostServer::actingAs($this->user)
|
||||
->tool(ListPostsTool::class, ['search' => 'marketing']);
|
||||
|
||||
$response->assertOk()
|
||||
->assertStructuredContent(function (AssertableJson $json) {
|
||||
$json->has('posts', 1)->etc();
|
||||
});
|
||||
});
|
||||
|
||||
test('get post returns PostResource shape', function () {
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
|
|
|
|||
|
|
@ -166,6 +166,19 @@
|
|||
);
|
||||
});
|
||||
|
||||
test('labels index search is case insensitive', function () {
|
||||
WorkspaceLabel::factory()->create(['workspace_id' => $this->workspace->id, 'name' => 'IMPORTANT']);
|
||||
WorkspaceLabel::factory()->create(['workspace_id' => $this->workspace->id, 'name' => 'Urgent']);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('app.labels.index', ['search' => 'important']));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->has('labels.data', 1)
|
||||
->where('filters.search', 'important')
|
||||
);
|
||||
});
|
||||
|
||||
test('labels index returns all when no search query', function () {
|
||||
WorkspaceLabel::factory()->count(3)->create(['workspace_id' => $this->workspace->id]);
|
||||
|
||||
|
|
|
|||
|
|
@ -157,6 +157,19 @@
|
|||
);
|
||||
});
|
||||
|
||||
test('signatures index search is case insensitive', function () {
|
||||
WorkspaceSignature::factory()->create(['workspace_id' => $this->workspace->id, 'name' => 'MARKETING']);
|
||||
WorkspaceSignature::factory()->create(['workspace_id' => $this->workspace->id, 'name' => 'Travel']);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('app.signatures.index', ['search' => 'marketing']));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->has('signatures.data', 1)
|
||||
->where('filters.search', 'marketing')
|
||||
);
|
||||
});
|
||||
|
||||
test('signatures index returns all when no search query', function () {
|
||||
WorkspaceSignature::factory()->count(3)->create(['workspace_id' => $this->workspace->id]);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue