Skip to content

Commit 40f1bff

Browse files
committed
where field exists alias
1 parent 74e5ca0 commit 40f1bff

4 files changed

Lines changed: 127 additions & 12 deletions

File tree

src/Eloquent/Docs/ModelDocs.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,14 @@
8383
* @method static $this orWhereNestedObject($column, $query, $filterInnerHits = false, $options = [])
8484
* @method static $this orWhereNotNestedObject($column, $query, $filterInnerHits = false, $options = [])
8585
*-----------------------------------
86-
* @method static $this whereTermExists($column, $boolean = 'and', $not = false)
87-
* @method static $this whereNotTermExists($column)
88-
* @method static $this orWhereTermExists($column)
89-
* @method static $this orWhereNotTermsExists($column)
86+
* @method static $this whereFieldExists($column, $boolean = 'and', $not = false)
87+
* @method static $this whereFieldDoesntExist($column)
88+
* @method static $this orWhereFieldExists($column)
89+
* @method static $this orWhereFieldDoesntExist($column)
90+
* @method static $this whereTermExists($column, $boolean = 'and', $not = false) @deprecated use whereFieldExists
91+
* @method static $this whereNotTermExists($column) @deprecated use whereFieldDoesntExist
92+
* @method static $this orWhereTermExists($column) @deprecated use orWhereFieldExists
93+
* @method static $this orWhereNotTermsExists($column) @deprecated use orWhereFieldDoesntExist
9094
*-----------------------------------
9195
* @method static $this whereRaw($dsl, $bindings = [], $boolean = 'and', $options = [])
9296
*===========================================

src/Query/Concerns/BuildsFieldQueries.php

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,19 @@ public function orWhereNotExact($column, $value, $options = []): self
8181
}
8282

8383
// ----------------------------------------------------------------------
84-
// Term Exists Query - check if a field has an indexed value
84+
// Exists Query - check if a field has an indexed value
8585
// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-exists-query.html
86+
//
87+
// Named whereFieldExists (not whereExists) to avoid colliding with
88+
// Laravel's base Builder::whereExists(Closure $callback) — the SQL
89+
// EXISTS subquery clause, which has entirely different semantics.
90+
//
91+
// Works on top-level fields and on nested-mapped fields: the compiler
92+
// auto-wraps the exists clause in a nested query when the field's path
93+
// is mapped as nested.
8694
// ----------------------------------------------------------------------
8795

88-
public function whereTermExists($column, $boolean = 'and', $not = false): self
96+
public function whereFieldExists($column, $boolean = 'and', $not = false): self
8997
{
9098
$this->wheres[] = [
9199
'type' => 'Basic',
@@ -99,19 +107,52 @@ public function whereTermExists($column, $boolean = 'and', $not = false): self
99107
return $this;
100108
}
101109

102-
public function whereNotTermExists($column)
110+
public function whereFieldDoesntExist($column): self
111+
{
112+
return $this->whereFieldExists($column, 'and', true);
113+
}
114+
115+
public function orWhereFieldExists($column): self
116+
{
117+
return $this->whereFieldExists($column, 'or', false);
118+
}
119+
120+
public function orWhereFieldDoesntExist($column): self
121+
{
122+
return $this->whereFieldExists($column, 'or', true);
123+
}
124+
125+
/**
126+
* @deprecated Use whereFieldExists() instead. Will be removed in v6.
127+
*/
128+
public function whereTermExists($column, $boolean = 'and', $not = false): self
129+
{
130+
return $this->whereFieldExists($column, $boolean, $not);
131+
}
132+
133+
/**
134+
* @deprecated Use whereFieldDoesntExist() instead. Will be removed in v6.
135+
*/
136+
public function whereNotTermExists($column): self
103137
{
104-
return $this->whereTermExists($column, 'and', true);
138+
return $this->whereFieldExists($column, 'and', true);
105139
}
106140

107-
public function orWhereTermExists($column)
141+
/**
142+
* @deprecated Use orWhereFieldExists() instead. Will be removed in v6.
143+
*/
144+
public function orWhereTermExists($column): self
108145
{
109-
return $this->whereTermExists($column, 'or', false);
146+
return $this->whereFieldExists($column, 'or', false);
110147
}
111148

112-
public function orWhereNotTermsExists($column)
149+
/**
150+
* @deprecated Use orWhereFieldDoesntExist() instead. Will be removed in v6.
151+
* Note: the original method name had a typo ("Terms" plural).
152+
*/
153+
public function orWhereNotTermsExists($column): self
113154
{
114-
return $this->whereTermExists($column, 'or', true);
155+
return $this->whereFieldExists($column, 'or', true);
115156
}
116157

117158
// ----------------------------------------------------------------------

src/Query/Grammar/Concerns/CompilesWheres.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,16 @@ protected function compileWhereBasic(Builder $builder, array $where): array
118118

119119
if (is_null($value) || $where['operator'] == 'exists') {
120120
$query = DslFactory::exists($field, $options);
121+
122+
// Auto-wrap in nested when the field lives under a nested path
123+
// and we're not already inside a parent-field (nested closure) context.
124+
$insideNestedContext = ! empty($builder->options()->get('parentField'));
125+
if (! $insideNestedContext) {
126+
$nestedPath = $this->getNestedPath($field, $builder);
127+
if ($nestedPath) {
128+
$query = DslFactory::nested($nestedPath, ['query' => $query]);
129+
}
130+
}
121131
} elseif (in_array($where['operator'], ['like', 'not like'])) {
122132
$field = $this->getIndexableField($field, $builder);
123133
$wildcardValue = str_replace('%', '*', $value);

tests/QueryElasticsearchSpecificTest.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,66 @@
192192
Post::whereNestedObject('comments', 12345);
193193
})->throws(\PDPhilip\Elasticsearch\Exceptions\BuilderException::class);
194194

195+
it('whereFieldExists matches users with the field set', function () {
196+
expect(User::whereFieldExists('title')->count())->toBe(8);
197+
});
198+
199+
it('whereFieldDoesntExist matches users with the field missing', function () {
200+
expect(User::whereFieldDoesntExist('title')->count())->toBe(1);
201+
});
202+
203+
it('whereFieldExists is parity with deprecated whereTermExists', function () {
204+
$new = User::whereFieldExists('title')->get()->pluck('name')->sort()->values()->all();
205+
$old = User::whereTermExists('title')->get()->pluck('name')->sort()->values()->all();
206+
expect($new)->toBe($old);
207+
});
208+
209+
it('whereFieldExists auto-wraps in nested for nested-mapped fields', function () {
210+
$dsl = Post::whereFieldExists('comments.country')->toDsl();
211+
212+
$clause = $dsl['body']['query'];
213+
expect($clause)->toHaveKey('nested')
214+
->and($clause['nested']['path'])->toBe('comments')
215+
->and($clause['nested']['query'])->toBe(['exists' => ['field' => 'comments.country']]);
216+
});
217+
218+
it('whereFieldExists returns posts when nested field exists', function () {
219+
expect(Post::whereFieldExists('comments.country')->count())->toBe(4);
220+
});
221+
222+
it('whereFieldExists does not wrap when already inside a nested closure', function () {
223+
$dsl = Post::whereNestedObject('comments', function ($q) {
224+
$q->whereFieldExists('country');
225+
})->toDsl();
226+
227+
$nested = $dsl['body']['query']['nested'];
228+
expect($nested['path'])->toBe('comments')
229+
->and($nested['query'])->toBe(['exists' => ['field' => 'comments.country']]);
230+
});
231+
232+
it('whereFieldExists leaves top-level fields unwrapped', function () {
233+
$dsl = Post::whereFieldExists('title')->toDsl();
234+
expect($dsl['body']['query'])->toBe(['exists' => ['field' => 'title']]);
235+
});
236+
237+
it('whereNotNull on a nested field auto-wraps in nested', function () {
238+
$dsl = Post::whereNotNull('comments.country')->toDsl();
239+
240+
$clause = $dsl['body']['query'];
241+
expect($clause)->toHaveKey('nested')
242+
->and($clause['nested']['path'])->toBe('comments')
243+
->and($clause['nested']['query'])->toBe(['exists' => ['field' => 'comments.country']]);
244+
});
245+
246+
it('whereNull on a nested field wraps nested inside must_not', function () {
247+
$dsl = Post::whereNull('comments.country')->toDsl();
248+
249+
$mustNot = $dsl['body']['query']['bool']['must_not'][0];
250+
expect($mustNot)->toHaveKey('nested')
251+
->and($mustNot['nested']['path'])->toBe('comments')
252+
->and($mustNot['nested']['query'])->toBe(['exists' => ['field' => 'comments.country']]);
253+
});
254+
195255
it('can search with field boosting', function () {
196256
$users = User::search('John', 'best_fields', ['name' => 5, 'description' => 1])->get();
197257
expect($users)->toHaveCount(2)

0 commit comments

Comments
 (0)