Merge pull request #13076 from marcusmoore/fixes/allow-multi-column-search-in-sqlite

Dynamically adapt concatenation searches for MySQL and sqlite
This commit is contained in:
snipe 2023-05-23 16:27:12 -07:00 committed by GitHub
commit 2e5486a62e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 117 additions and 19 deletions

View file

@ -947,8 +947,10 @@ class Asset extends Depreciable
->orWhere('assets_users.first_name', 'LIKE', '%'.$term.'%')
->orWhere('assets_users.last_name', 'LIKE', '%'.$term.'%')
->orWhere('assets_users.username', 'LIKE', '%'.$term.'%')
->orWhereRaw('CONCAT('.DB::getTablePrefix().'assets_users.first_name," ",'.DB::getTablePrefix().'assets_users.last_name) LIKE ?', ["%$term%"]);
->orWhereMultipleColumns([
'assets_users.first_name',
'assets_users.last_name',
], $term);
}
/**
@ -1343,7 +1345,10 @@ class Asset extends Depreciable
})->orWhere(function ($query) use ($search) {
$query->where('assets_users.first_name', 'LIKE', '%'.$search.'%')
->orWhere('assets_users.last_name', 'LIKE', '%'.$search.'%')
->orWhereRaw('CONCAT('.DB::getTablePrefix().'assets_users.first_name," ",'.DB::getTablePrefix().'assets_users.last_name) LIKE ?', ["%$search%"])
->orWhereMultipleColumns([
'assets_users.first_name',
'assets_users.last_name',
], $search)
->orWhere('assets_users.username', 'LIKE', '%'.$search.'%')
->orWhere('assets_locations.name', 'LIKE', '%'.$search.'%')
->orWhere('assigned_assets.name', 'LIKE', '%'.$search.'%');

View file

@ -5,6 +5,7 @@ namespace App\Models\Traits;
use App\Models\Asset;
use App\Models\CustomField;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
/**
* This trait allows for cleaner searching of models,
@ -164,7 +165,13 @@ trait Searchable
}
// I put this here because I only want to add the concat one time in the end of the user relation search
if($relation == 'user') {
$query->orWhereRaw('CONCAT (users.first_name, " ", users.last_name) LIKE ?', ["%{$term}%"]);
$query->orWhereRaw(
$this->buildMultipleColumnSearch([
'users.first_name',
'users.last_name',
]),
["%{$term}%"]
);
}
});
}
@ -257,4 +264,37 @@ trait Searchable
return $related->getTable();
}
/**
* Builds a search string for either MySQL or sqlite by separating the provided columns with a space.
*
* @param array $columns Columns to include in search string.
* @return string
*/
private function buildMultipleColumnSearch(array $columns): string
{
$mappedColumns = collect($columns)->map(fn($column) => DB::getTablePrefix() . $column)->toArray();
$driver = config('database.connections.' . config('database.default') . '.driver');
if ($driver === 'sqlite') {
return implode("||' '||", $mappedColumns) . ' LIKE ?';
}
// Default to MySQL's concatenation method
return 'CONCAT(' . implode('," ",', $mappedColumns) . ') LIKE ?';
}
/**
* Search a string across multiple columns separated with a space.
*
* @param Builder $query
* @param array $columns - Columns to include in search string.
* @param $term
* @return Builder
*/
public function scopeOrWhereMultipleColumns($query, array $columns, $term)
{
return $query->orWhereRaw($this->buildMultipleColumnSearch($columns), ["%{$term}%"]);
}
}

View file

@ -644,14 +644,14 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
*/
public function scopeSimpleNameSearch($query, $search)
{
$query = $query->where('first_name', 'LIKE', '%'.$search.'%')
->orWhere('last_name', 'LIKE', '%'.$search.'%')
->orWhereRaw('CONCAT('.DB::getTablePrefix().'users.first_name," ",'.DB::getTablePrefix().'users.last_name) LIKE ?', ["%{$search}%"]);
return $query;
return $query->where('first_name', 'LIKE', '%' . $search . '%')
->orWhere('last_name', 'LIKE', '%' . $search . '%')
->orWhereMultipleColumns([
'users.first_name',
'users.last_name',
], $search);
}
/**
* Run additional, advanced searches.
*
@ -660,9 +660,11 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
* @return \Illuminate\Database\Eloquent\Builder
*/
public function advancedTextSearch(Builder $query, array $terms) {
foreach($terms as $term) {
$query = $query->orWhereRaw('CONCAT('.DB::getTablePrefix().'users.first_name," ",'.DB::getTablePrefix().'users.last_name) LIKE ?', ["%{$term}%"]);
$query->orWhereMultipleColumns([
'users.first_name',
'users.last_name',
], $term);
}
return $query;

View file

@ -38,22 +38,32 @@ class CustomFieldSeeder extends Seeder
[
'custom_field_id' => '1',
'custom_fieldset_id' => '1',
'order' => 0,
'required' => 0,
],
[
'custom_field_id' => '2',
'custom_fieldset_id' => '1',
'order' => 0,
'required' => 0,
],
[
'custom_field_id' => '3',
'custom_fieldset_id' => '2',
'order' => 0,
'required' => 0,
],
[
'custom_field_id' => '4',
'custom_fieldset_id' => '2',
'order' => 0,
'required' => 0,
],
[
'custom_field_id' => '5',
'custom_fieldset_id' => '2',
'order' => 0,
'required' => 0,
],
]);

View file

@ -30,6 +30,19 @@ class UsersForSelectListTest extends TestCase
->assertJson(fn(AssertableJson $json) => $json->has('results', 3)->etc());
}
public function testUsersCanBeSearchedByFirstAndLastName()
{
User::factory()->create(['first_name' => 'Luke', 'last_name' => 'Skywalker']);
Passport::actingAs(User::factory()->create());
$response = $this->getJson(route('api.users.selectlist', ['search' => 'luke sky']))->assertOk();
$results = collect($response->json('results'));
$this->assertEquals(1, $results->count());
$this->assertTrue($results->pluck('text')->contains(fn($text) => str_contains($text, 'Luke')));
}
public function testUsersScopedToCompanyWhenMultipleFullCompanySupportEnabled()
{
$this->settings->enableMultipleFullCompanySupport();

View file

@ -0,0 +1,28 @@
<?php
namespace Tests\Feature\Api\Users;
use App\Models\User;
use Laravel\Passport\Passport;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class UsersSearchTest extends TestCase
{
use InteractsWithSettings;
public function testCanSearchByUserFirstAndLastName()
{
User::factory()->create(['first_name' => 'Luke', 'last_name' => 'Skywalker']);
User::factory()->create(['first_name' => 'Darth', 'last_name' => 'Vader']);
Passport::actingAs(User::factory()->viewUsers()->create());
$response = $this->getJson(route('api.users.index', ['search' => 'luke sky']))->assertOk();
$results = collect($response->json('rows'));
$this->assertEquals(1, $results->count());
$this->assertTrue($results->pluck('name')->contains(fn($text) => str_contains($text, 'Luke')));
$this->assertFalse($results->pluck('name')->contains(fn($text) => str_contains($text, 'Darth')));
}
}