Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Order by rework #39

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
47 changes: 19 additions & 28 deletions src/Clauses/OrderByClause.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@

namespace WikibaseSolutions\CypherDSL\Clauses;

use WikibaseSolutions\CypherDSL\Property;
use WikibaseSolutions\CypherDSL\Types\AnyType;
use function array_map;
use WikibaseSolutions\CypherDSL\Order;
use WikibaseSolutions\CypherDSL\Traits\EscapeTrait;
use WikibaseSolutions\CypherDSL\Types\PropertyTypes\PropertyType;

/**
* This class represents an ORDER BY clause. This clause should always be preceded by a RETURN
Expand All @@ -35,9 +38,9 @@ class OrderByClause extends Clause
use EscapeTrait;

/**
* @var Property[] The expressions to include in the clause
* @var Order[] The expressions to include in the clause
*/
private array $properties = [];
private array $orderings = [];

/**
* @var bool
Expand All @@ -47,47 +50,36 @@ class OrderByClause extends Clause
/**
* Add a property to sort on.
*
* @param Property $property The additional property to sort on
* @param PropertyType $property The additional property to sort on.
* @param string|null $order The order of the property to appear. Null is equal to the default in Neo4J.
*
* @return OrderByClause
*/
public function addProperty(Property $property): self
public function addProperty(PropertyType $property, ?string $order = null): self
{
$this->properties[] = $property;
$this->orderings[] = new Order($property, $order);

return $this;
}

/**
* Returns the properties to order.
*
* @return Property[]
* @return AnyType[]
*/
public function getProperties(): array
{
return $this->properties;
}

/**
* Returns whether the ordering is in descending order.
*
* @return bool
*/
public function isDescending(): bool
{
return $this->descending;
return array_map(static fn (Order $o) => $o->getExpression(), $this->orderings);
}

/**
* Set to sort in a DESCENDING order.
* Returns the orderings.
*
* @param bool $descending
* @return OrderByClause
* @return Order[]
*/
public function setDescending(bool $descending = true): self
public function getOrderings(): array
{
$this->descending = $descending;

return $this;
return $this->orderings;
}

/**
Expand All @@ -103,9 +95,8 @@ protected function getClause(): string
*/
protected function getSubject(): string
{
$properties = array_map(fn (Property $property): string => $property->toQuery(), $this->properties);
$subject = implode(", ", $properties);
$properties = array_map(static fn ($x) => $x->toQuery(), $this->orderings);

return $this->descending ? sprintf("%s DESCENDING", $subject) : $subject;
return implode(", ", $properties);
}
}
100 changes: 100 additions & 0 deletions src/Order.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

/*
* Cypher DSL
* Copyright (C) 2021 Wikibase Solutions
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

namespace WikibaseSolutions\CypherDSL;

use function in_array;
use InvalidArgumentException;
use function strtoupper;
use WikibaseSolutions\CypherDSL\Types\AnyType;
use WikibaseSolutions\CypherDSL\Types\PropertyTypes\PropertyType;

/**
* Defines the order of an expression. Can only be used in an ORDER BY clause.
*
* @see https://neo4j.com/docs/cypher-manual/current/clauses/order-by/
* @note While the documentation online does not mention this, ORDER BY supports multiple directions in the same clause:
* - ORDER BY a ASC, b DESC
* is considered valid.
* This means it is impossible for the OrderBy clause to order all expressions individually, necessitating this class.
*/
class Order implements QueryConvertable
{
private PropertyType $expression;
/** @var string|null */
private ?string $ordering;

/**
* Order constructor.
*
* @param PropertyType $expression The expression to order by.
* @param string|null $ordering The order modifier. Must be null or a valid modifier ('ASC', 'ASCENDING', 'DESC', 'DESCENDING')
*/
public function __construct(PropertyType $expression, ?string $ordering = null)
{
$this->expression = $expression;
$this->setOrdering($ordering);
}

/**
* Returns the expression being ordered.
*
* @return AnyType
*/
public function getExpression(): AnyType
{
return $this->expression;
}

/**
* @return string|null
*/
public function getOrdering(): ?string
{
return $this->ordering;
}

public function setOrdering(?string $ordering): self
{
if ($ordering !== null) {
$ordering = strtoupper($ordering);
if (!in_array($ordering, ['ASC', 'DESC', 'ASCENDING', 'DESCENDING'])) {
throw new InvalidArgumentException('Ordering must be null, "ASC", "DESC", "ASCENDING" or "DESCENDING"');
}

$this->ordering = $ordering;
} else {
$this->ordering = null;
}

return $this;
}

public function toQuery(): string
{
$cypher = $this->getExpression()->toQuery();
if ($this->ordering) {
$cypher .= ' ' . $this->ordering;
}

return $cypher;
}
}
9 changes: 6 additions & 3 deletions src/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -475,16 +475,19 @@ public function optionalMatch($patterns): self
public function orderBy($properties, bool $descending = false): self
{
$orderByClause = new OrderByClause();
$orderByClause->setDescending($descending);

if (!is_array($properties)) {
$properties = [$properties];
}

foreach ($properties as $property) {
foreach ($properties as $i => $property) {
$this->assertClass('property', Property::class, $property);

$orderByClause->addProperty($property);
if ($descending && $i === count($properties) - 1) {
$orderByClause->addProperty($property, 'DESCENDING');
} else {
$orderByClause->addProperty($property);
}
}

$this->clauses[] = $orderByClause;
Expand Down
26 changes: 17 additions & 9 deletions tests/Unit/Clauses/OrderByClauseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use PHPUnit\Framework\TestCase;
use TypeError;
use WikibaseSolutions\CypherDSL\Clauses\OrderByClause;
use WikibaseSolutions\CypherDSL\Order;
use WikibaseSolutions\CypherDSL\Property;
use WikibaseSolutions\CypherDSL\Tests\Unit\TestHelper;
use WikibaseSolutions\CypherDSL\Types\AnyType;
Expand All @@ -41,7 +42,6 @@ public function testEmptyClause(): void

$this->assertSame("", $orderBy->toQuery());
$this->assertEquals([], $orderBy->getProperties());
$this->assertFalse($orderBy->isDescending());
}

public function testSingleProperty(): void
Expand All @@ -52,7 +52,6 @@ public function testSingleProperty(): void

$this->assertSame("ORDER BY a.a", $orderBy->toQuery());
$this->assertEquals([$property], $orderBy->getProperties());
$this->assertFalse($orderBy->isDescending());
}

public function testMultipleProperties(): void
Expand All @@ -66,19 +65,16 @@ public function testMultipleProperties(): void

$this->assertSame("ORDER BY a.a, a.b", $orderBy->toQuery());
$this->assertEquals([$propertyA, $propertyB], $orderBy->getProperties());
$this->assertFalse($orderBy->isDescending());
}

public function testSinglePropertyDesc(): void
{
$orderBy = new OrderByClause();
$property = $this->getQueryConvertableMock(Property::class, "a.a");
$orderBy->addProperty($property);
$orderBy->setDescending();
$orderBy->addProperty($property, 'DESCENDING');

$this->assertSame("ORDER BY a.a DESCENDING", $orderBy->toQuery());
$this->assertEquals([$property], $orderBy->getProperties());
$this->assertTrue($orderBy->isDescending());
}

public function testMultiplePropertiesDesc(): void
Expand All @@ -88,12 +84,24 @@ public function testMultiplePropertiesDesc(): void
$propertyB = $this->getQueryConvertableMock(Property::class, "a.b");

$orderBy->addProperty($propertyA);
$orderBy->addProperty($propertyB);
$orderBy->setDescending();
$orderBy->addProperty($propertyB, 'DESCENDING');

$this->assertSame("ORDER BY a.a, a.b DESCENDING", $orderBy->toQuery());
$this->assertEquals([$propertyA, $propertyB], $orderBy->getProperties());
$this->assertTrue($orderBy->isDescending());
}

public function testMultiplePropertiesMixed(): void
{
$orderBy = new OrderByClause();
$propertyA = $this->getQueryConvertableMock(Property::class, "a.a");
$propertyB = $this->getQueryConvertableMock(Property::class, "a.b");

$orderBy->addProperty($propertyA, 'ASC');
$orderBy->addProperty($propertyB, 'DESCENDING');

$this->assertSame("ORDER BY a.a ASC, a.b DESCENDING", $orderBy->toQuery());
$this->assertEquals([$propertyA, $propertyB], $orderBy->getProperties());
$this->assertEquals([new Order($propertyA, 'asc'), new Order($propertyB, 'descending')], $orderBy->getOrderings());
}

/**
Expand Down
55 changes: 55 additions & 0 deletions tests/Unit/OrderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace WikibaseSolutions\CypherDSL\Tests\Unit;

use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use WikibaseSolutions\CypherDSL\Order;
use WikibaseSolutions\CypherDSL\RawExpression;

class OrderTest extends TestCase
{
use TestHelper;

public function testBasicOrder(): void
{
$order = new Order($this->getQueryConvertableMock(RawExpression::class, 'x'));

$this->assertEquals('x', $order->toQuery());
$this->assertNull($order->getOrdering());
}

public function testBasicOrderDescending(): void
{
$order = new Order($this->getQueryConvertableMock(RawExpression::class, 'x'), 'desc');

$this->assertEquals('x DESC', $order->toQuery());
$this->assertEquals('DESC', $order->getOrdering());
}

public function testBasicOrderAscending(): void
{
$order = new Order($this->getQueryConvertableMock(RawExpression::class, 'x'), 'asc');

$this->assertEquals('x ASC', $order->toQuery());
$this->assertEquals('ASC', $order->getOrdering());
}

public function testBasicOrderChange(): void
{
$order = new Order($this->getQueryConvertableMock(RawExpression::class, 'x'), 'asc');

$order->setOrdering(null);

$this->assertEquals('x', $order->toQuery());
$this->assertNull($order->getOrdering());
}

public function testOrderFalse(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectErrorMessage('Ordering must be null, "ASC", "DESC", "ASCENDING" or "DESCENDING"');

new Order($this->getQueryConvertableMock(RawExpression::class, 'x'), 'ascc');
}
}