Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Drivers/MongoEntitySet.php
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,10 @@ protected function evaluateFilter(?Node $node)
case $node instanceof Node\Literal:
return $node->getValue()->get();

case $node instanceof Node\Property\Navigation\Count:
$node->notImplemented();

// no break
case $node instanceof Node\Property:
return '$'.$node->getValue()->getName();
}
Expand Down
54 changes: 54 additions & 0 deletions src/Drivers/SQL/SQLExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ public function evaluate(Node $node): void
$this->lambdaExpression($node);
break;

case $node instanceof Node\Property\Navigation\Count:
$this->navigationPropertyCountExpression($node);
break;

case $node instanceof Property:
$this->propertyExpression($node);
break;
Expand Down Expand Up @@ -1289,6 +1293,56 @@ protected function literalExpression(Literal $node): void
* @param Lambda $node Node
* @return void
*/
/**
* Expand a navigation property count expression into a correlated COUNT subquery
* @param Node\Property\Navigation\Count $node Node
* @return void
*/
protected function navigationPropertyCountExpression(Node\Property\Navigation\Count $node): void
{
/** @var NavigationProperty $navigationProperty */
$navigationProperty = $node->getValue();

/** @var NavigationBinding $navigationBinding */
$navigationBinding = $this->entitySet->getBindingByNavigationProperty($navigationProperty);

/** @var SQLEntitySet $targetSet */
$targetSet = $navigationBinding->getTarget();

/** @var ReferentialConstraint[] $constraints */
$constraints = $navigationBinding->getPath()->getConstraints()->all();

if (!$constraints) {
$node->notImplemented();
}

$this->pushStatement(
sprintf(
'( SELECT COUNT(*) FROM %s WHERE',
$targetSet->quoteSingleIdentifier($targetSet->getTable())
)
);

$first = true;

foreach ($constraints as $constraint) {
if (!$first) {
$this->pushStatement('AND');
}

$first = false;

$referencedField = $targetSet->propertyToExpression($constraint->getReferencedProperty());
$localField = $this->entitySet->propertyToExpression($constraint->getProperty());

$this->pushStatement(sprintf('%s = %s', $referencedField->getStatement(), $localField->getStatement()));
$this->pushParameters($referencedField->getParameters());
$this->pushParameters($localField->getParameters());
}

$this->pushStatement(')');
}

protected function lambdaExpression(Lambda $node): void
{
$driver = $this->entitySet->getDriver();
Expand Down
19 changes: 19 additions & 0 deletions src/Expression/Node/Property/Navigation/Count.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Flat3\Lodata\Expression\Node\Property\Navigation;

use Flat3\Lodata\Expression\Node\Property\Navigation;

/**
* Navigation property count
*
* Represents `$count` applied to a collection-valued navigation property within an
* expression, for example `passengers/$count` in a `$filter`. The node carries the
* navigation property it counts and is evaluated to the number of related entities.
* @package Flat3\Lodata\Expression\Node\Property\Navigation
*/
class Count extends Navigation
{
}
36 changes: 36 additions & 0 deletions src/Expression/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,42 @@ public function tokenizeTimeOfDay(): bool
return true;
}

/**
* Tokenize a navigation property count (e.g. `passengers/$count`)
* @return bool
*/
public function tokenizeNavigationPropertyCount(): bool
{
$currentResource = $this->getCurrentResource();

if (!$currentResource) {
return false;
}

$navigationProperties = $currentResource->getType()->getNavigationProperties();

$token = $this->lexer->with(function () use ($navigationProperties) {
$identifier = $this->lexer->identifier();
$this->lexer->char(Lexer::pathSeparator);
$this->lexer->literal('$count');

return $navigationProperties->get($identifier) ? $identifier : null;
});

if (!$token) {
return false;
}

$property = $navigationProperties->get($token);

$operand = new Node\Property\Navigation\Count($this);
$operand->setValue($property);
$this->operandStack[] = $operand;
$this->tokens[] = $operand;

return true;
}

/**
* Tokenize a navigation property path
* @return bool
Expand Down
6 changes: 6 additions & 0 deletions src/Expression/Parser/Common.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ public static function evaluate(Node $node, ?Entity $entity = null): ?Primitive
}

switch (true) {
case $node instanceof Node\Property\Navigation\Count:
throw new NotImplementedException(
'unsupported_expression',
'This entity set does not support counting a navigation property within an expression'
);

// Deserialization
case $node instanceof Node\Property:
$propertyValue = $entity[$node->getValue()];
Expand Down
1 change: 1 addition & 0 deletions src/Expression/Parser/Filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ protected function findToken(): bool
$this->tokenizeDeclaredProperty() ||
$this->tokenizeComputedProperty() ||
$this->tokenizeOperator() ||
$this->tokenizeNavigationPropertyCount() ||
$this->tokenizeNavigationPropertyPath();
}

Expand Down
27 changes: 27 additions & 0 deletions tests/Filter/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,31 @@ public function test_filter_all()
->expand('MyPets')
);
}

public function test_filter_navigation_count_eq()
{
$this->assertJsonResponseSnapshot(
(new Request)
->path($this->entitySet)
->filter('MyPets/$count eq 0')
);
}

public function test_filter_navigation_count_gt()
{
$this->assertJsonResponseSnapshot(
(new Request)
->path($this->entitySet)
->filter('MyPets/$count gt 1')
);
}

public function test_filter_navigation_count_combined()
{
$this->assertJsonResponseSnapshot(
(new Request)
->path($this->entitySet)
->filter('MyPets/$count gt 1 or id eq 2')
);
}
}
15 changes: 15 additions & 0 deletions tests/Filter/MongoTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,19 @@ public function test_path_query_filter_search()
{
$this->markTestSkipped();
}

public function test_filter_navigation_count_eq()
{
$this->markTestSkipped();
}

public function test_filter_navigation_count_gt()
{
$this->markTestSkipped();
}

public function test_filter_navigation_count_combined()
{
$this->markTestSkipped();
}
}
4 changes: 4 additions & 0 deletions tests/Parser/Handlers/LoopbackEntitySet.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ public function commonExpression(Node $node): void
));
return;

case $node instanceof Node\Property\Navigation\Count:
$this->addCommon(sprintf('%s/$count', $node->getValue()));
return;

case $node instanceof Node\Property:
$this->addCommon($node->getValue());
return;
Expand Down
15 changes: 15 additions & 0 deletions tests/Parser/LambdaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,19 @@ public function test_5g()
{
$this->assertLambda("airports/any(d:d/name eq 'hello') and 1 eq 2 or airports/all(d:d/name eq 'hello')");
}

public function test_count_single_constraint()
{
$this->assertLambda('da/$count eq 1');
}

public function test_count_multiple_constraints()
{
$this->assertLambda('airports/$count gt 0');
}

public function test_count_logical_combination()
{
$this->assertLambda('da/$count ge 2 or airports/$count eq 0');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"@context": "http://localhost/odata/$metadata#Passengers",
"value": [
{
"id": 1,
"flight_id": 1,
"name": "Alpha",
"dob": "2000-01-01T04:04:04+00:00",
"age": 4,
"chips": true,
"dq": "2000-01-01",
"in_role": "P1DT0S",
"open_time": "05:05:05.000000",
"colour": "Green",
"sock_colours": "Green,Blue",
"emails": [
"alpha@example.com",
"alpha@beta.com"
]
},
{
"id": 2,
"flight_id": null,
"name": "Beta",
"dob": "2001-02-02T05:05:05+00:00",
"age": 3,
"chips": false,
"dq": "2001-02-02",
"in_role": "P2DT5H5M5.2999999999884S",
"open_time": null,
"colour": null,
"sock_colours": null,
"emails": []
},
{
"id": 3,
"flight_id": 1,
"name": "Gamma",
"dob": "2002-03-03T06:06:06+00:00",
"age": 2,
"chips": true,
"dq": "2002-03-03",
"in_role": "P4DT32M41S",
"open_time": "07:07:07.000000",
"colour": "Blue",
"sock_colours": "Red,Green,Blue",
"emails": [
"gamma@example.com"
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"@context": "http://localhost/odata/$metadata#Passengers",
"value": [
{
"id": 2,
"flight_id": null,
"name": "Beta",
"dob": "2001-02-02T05:05:05+00:00",
"age": 3,
"chips": false,
"dq": "2001-02-02",
"in_role": "P2DT5H5M5.2999999999884S",
"open_time": null,
"colour": null,
"sock_colours": null,
"emails": []
},
{
"id": 4,
"flight_id": null,
"name": "Delta",
"dob": null,
"age": null,
"chips": null,
"dq": null,
"in_role": "PT2M7S",
"open_time": null,
"colour": null,
"sock_colours": null,
"emails": []
},
{
"id": 5,
"flight_id": null,
"name": "Epsilon",
"dob": "2003-04-04T07:07:07+00:00",
"age": 2.4,
"chips": null,
"dq": "2003-04-04",
"in_role": "PT14M48.9S",
"open_time": "23:11:33.000000",
"colour": null,
"sock_colours": null,
"emails": []
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"@context": "http://localhost/odata/$metadata#Passengers",
"value": [
{
"id": 1,
"flight_id": 1,
"name": "Alpha",
"dob": "2000-01-01T04:04:04+00:00",
"age": 4,
"chips": true,
"dq": "2000-01-01",
"in_role": "P1DT0S",
"open_time": "05:05:05.000000",
"colour": "Green",
"sock_colours": "Green,Blue",
"emails": [
"alpha@example.com",
"alpha@beta.com"
]
},
{
"id": 3,
"flight_id": 1,
"name": "Gamma",
"dob": "2002-03-03T06:06:06+00:00",
"age": 2,
"chips": true,
"dq": "2002-03-03",
"in_role": "P4DT32M41S",
"open_time": "07:07:07.000000",
"colour": "Blue",
"sock_colours": "Red,Green,Blue",
"emails": [
"gamma@example.com"
]
}
]
}
Loading
Loading