Skip to content

Commit 6fe8d18

Browse files
committed
whereNestedFieldExists
1 parent ebce8e9 commit 6fe8d18

4 files changed

Lines changed: 82 additions & 0 deletions

File tree

src/Eloquent/Docs/ModelDocs.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@
8282
* @method static $this whereNotNestedObject($column, $query, $filterInnerHits = false, $options = [])
8383
* @method static $this orWhereNestedObject($column, $query, $filterInnerHits = false, $options = [])
8484
* @method static $this orWhereNotNestedObject($column, $query, $filterInnerHits = false, $options = [])
85+
* @method static $this whereNestedFieldExists(string $path, string $boolean = 'and', bool $not = false)
86+
* @method static $this whereNestedFieldDoesntExist(string $path)
87+
* @method static $this orWhereNestedFieldExists(string $path)
88+
* @method static $this orWhereNestedFieldDoesntExist(string $path)
8589
*-----------------------------------
8690
* @method static $this whereFieldExists($column, $boolean = 'and', $not = false)
8791
* @method static $this whereFieldDoesntExist($column)

src/Query/Concerns/BuildsNestedQueries.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,43 @@ public function filterNested($column, $query, $options = [])
130130
return $this;
131131
}
132132

133+
// ----------------------------------------------------------------------
134+
// Nested Field Exists
135+
// Use when you want "this nested path has at least one document" without
136+
// any inner conditions. Explicit, no mapping lookup, no detection cost.
137+
// ----------------------------------------------------------------------
138+
139+
public function whereNestedFieldExists(string $path, string $boolean = 'and', bool $not = false): self
140+
{
141+
// Stored as 'path' (not 'column') to bypass the where-loop's automatic
142+
// parent-field prepending — the path here is the nested path itself,
143+
// not a field inside a nested context.
144+
$this->wheres[] = [
145+
'type' => 'NestedFieldExists',
146+
'path' => $path,
147+
'boolean' => $boolean,
148+
'not' => $not,
149+
'options' => [],
150+
];
151+
152+
return $this;
153+
}
154+
155+
public function whereNestedFieldDoesntExist(string $path): self
156+
{
157+
return $this->whereNestedFieldExists($path, 'and', true);
158+
}
159+
160+
public function orWhereNestedFieldExists(string $path): self
161+
{
162+
return $this->whereNestedFieldExists($path, 'or', false);
163+
}
164+
165+
public function orWhereNestedFieldDoesntExist(string $path): self
166+
{
167+
return $this->whereNestedFieldExists($path, 'or', true);
168+
}
169+
133170
/**
134171
* Order by a field within nested objects.
135172
*/

src/Query/Grammar/Concerns/CompilesWheres.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,18 @@ protected function compileWhereNested(Builder $builder, array $where): array
287287
return reset($compiled);
288288
}
289289

290+
/**
291+
* Nested field exists - "this nested path has at least one document".
292+
* No mapping lookup; the path is whatever the caller passed.
293+
*/
294+
protected function compileWhereNestedFieldExists(Builder $builder, array $where): array
295+
{
296+
$path = $where['path'];
297+
$query = DslFactory::nested($path, ['query' => DslFactory::exists($path)]);
298+
299+
return $this->applyOptionsToClause($query, $where);
300+
}
301+
290302
/**
291303
* Nested object query - query nested document type
292304
*/

tests/QueryElasticsearchSpecificTest.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,35 @@
253253
->and($mustNot['nested']['query'])->toBe(['exists' => ['field' => 'comments.country']]);
254254
});
255255

256+
it('whereNestedFieldExists emits nested+exists for the path itself', function () {
257+
$dsl = Post::whereNestedFieldExists('comments')->toDsl();
258+
259+
$clause = $dsl['body']['query'];
260+
expect($clause)->toHaveKey('nested')
261+
->and($clause['nested']['path'])->toBe('comments')
262+
->and($clause['nested']['query'])->toBe(['exists' => ['field' => 'comments']]);
263+
});
264+
265+
it('whereNestedFieldExists matches posts that have nested entries', function () {
266+
expect(Post::whereNestedFieldExists('comments')->count())->toBe(4);
267+
});
268+
269+
it('whereNestedFieldDoesntExist wraps nested inside must_not', function () {
270+
$dsl = Post::whereNestedFieldDoesntExist('comments')->toDsl();
271+
272+
$mustNot = $dsl['body']['query']['bool']['must_not'][0];
273+
expect($mustNot)->toHaveKey('nested')
274+
->and($mustNot['nested']['path'])->toBe('comments')
275+
->and($mustNot['nested']['query'])->toBe(['exists' => ['field' => 'comments']]);
276+
});
277+
278+
it('orWhereNestedFieldExists composes with other clauses', function () {
279+
$dsl = Post::where('status', 99)->orWhereNestedFieldExists('comments')->toDsl();
280+
281+
$should = $dsl['body']['query']['bool']['should'] ?? null;
282+
expect($should)->not->toBeNull();
283+
});
284+
256285
it('can search with field boosting', function () {
257286
$users = User::search('John', 'best_fields', ['name' => 5, 'description' => 1])->get();
258287
expect($users)->toHaveCount(2)

0 commit comments

Comments
 (0)