Skip to content

Commit

Permalink
Merge pull request #6 from telkins/descendants
Browse files Browse the repository at this point in the history
Added DAG ancestors scope, removed order/distinct...
  • Loading branch information
telkins authored Oct 28, 2019
2 parents cd2bb5d + 12b706b commit 2dc9958
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 38 deletions.
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,19 @@ class MyModel extends Model

This will allow you to easily access certain functionality from your model class.

To apply a scope that only includes models descending from the specified model ID:
To apply a scope that only includes models that are descendants of the specified model ID:
```php
$descendants = MyModel::dagDescendantsOf($myModel->id, 'my-source')->get();
```

An ID and source must be provided. You may optionally provide the following arguments:
* `$order`: This will order the results by the number of hops. This can be `'asc'` (default), `'desc'`, or something falsy for no ordering.
* `$distinct`: This determines whether or not the scope will return a distinct set of entries. Sometimes it's possible to have the same descendant appear via multiple paths. If it's desirable to get this in your result set multiple times, then pass `false`. It defaults to `true`.
An ID and source must be provided.

Likewise, to apply a scope that only includes models that are ancestors of the specified model ID:
```php
$descendants = MyModel::dagAncestorsOf($myModel->id, 'my-source')->get();
```

Again, an ID and source must be provided.

## Testing

Expand Down
47 changes: 32 additions & 15 deletions src/Models/Traits/IsDagManaged.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,46 @@ trait IsDagManaged
* Scope a query to only include models descending from the specified model ID.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param int $modelId
* @param int $modelId
* @param string $source
* @param string $order
* @param bool $distinct
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeDagDescendantsOf($query, int $modelId, string $source, string $order = 'asc', bool $distinct = true)
public function scopeDagDescendantsOf($query, int $modelId, string $source)
{
$query->whereIn($this->getQualifiedKeyName(), function ($query) use ($modelId, $source, $order, $distinct) {
if ($distinct) {
$query->distinct();
}
$this->scopeDagRelationsOf($query, $modelId, $source, true);
}

/**
* Scope a query to only include models that are ancestors of the specified model ID.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param int $modelId
* @param string $source
*/
public function scopeDagAncestorsOf($query, int $modelId, string $source)
{
$this->scopeDagRelationsOf($query, $modelId, $source, false);
}

$query->select('dag_edges.start_vertex')
/**
* Scope a query to only include models that are relations of (descendants or ancestors) of the specified model ID.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param int $modelId
* @param string $source
* @param bool. $down
*/
public function scopeDagRelationsOf($query, int $modelId, string $source, bool $down)
{
$query->whereIn($this->getQualifiedKeyName(), function ($query) use ($modelId, $source, $down) {
$selectField = $down ? 'start_vertex' : 'end_vertex';
$whereField = $down ? 'end_vertex' : 'start_vertex';

$query->select("dag_edges.{$selectField}")
->from('dag_edges')
->where([
['dag_edges.end_vertex', $modelId],
["dag_edges.{$whereField}", $modelId],
['dag_edges.source', $source],
]);

if ($order && in_array($order, ['asc', 'desc'])) {
$query->orderBy('dag_edges.hops', $order);
}
});
}
}
10 changes: 3 additions & 7 deletions tests/DagManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,21 @@

use Telkins\Dag\Models\DagEdge;
use Illuminate\Support\Collection;
use Telkins\Dag\Tests\Support\CreatesEdges;
use Telkins\Dag\Exceptions\TooManyHopsException;
use Telkins\Dag\Exceptions\CircularReferenceException;

class DagManagerTest extends TestCase
{
use CreatesEdges;

protected $a = 1;
protected $b = 2;
protected $c = 3;
protected $d = 4;
protected $e = 5;
protected $f = 6;

protected $source = 'test-source';

protected function createEdge(int $startVertex, int $endVertex, string $source = null)
{
return dag()->createEdge($startVertex, $endVertex, ($source ?? $this->source));
}

protected function assertExpectedEdge(DagEdge $actual, int $startVertex, int $endVertex, int $hops, string $source = null)
{
$this->assertSame($startVertex, $actual->start_vertex);
Expand Down
183 changes: 171 additions & 12 deletions tests/IsDagManagedTraitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,24 @@
namespace Telkins\Dag\Tests;

use Telkins\Dag\Tests\Support\TestModel;
use Telkins\Dag\Tests\Support\CreatesEdges;

class IsDagManagedTraitTest extends TestCase
{
protected $source = 'test-source';

protected function createEdge(int $startVertex, int $endVertex, string $source = null)
{
return dag()->createEdge($startVertex, $endVertex, ($source ?? $this->source));
}
use CreatesEdges;

/**
* Tests: A
* |
* B <-- get "related" descendants of this entry
* B <-- get descendants of this entry
* |
* C
* |
* D
*
* @test
*/
public function it_can_get_dag_descendants_from_a_simple_chain()
public function it_can_get_descendants_from_a_simple_chain()
{
/**
* Arrange/Given:
Expand Down Expand Up @@ -63,15 +59,15 @@ public function it_can_get_dag_descendants_from_a_simple_chain()
/**
* Tests: A
* / \
* B C <-- get "related" descendants of entry "B"
* B C <-- get descendants of entry "B"
* | \ |
* D E
* \ /
* F
*
* @test
*/
public function it_can_get_dag_descendants_from_a_complex_box_diamond_part_i()
public function it_can_get_descendants_from_a_complex_box_diamond_part_i()
{
/**
* Arrange/Given:
Expand Down Expand Up @@ -122,15 +118,15 @@ public function it_can_get_dag_descendants_from_a_complex_box_diamond_part_i()
/**
* Tests: A
* / \
* B C <-- get "related" descendants of entry "C"
* B C <-- get descendants of entry "C"
* | \ |
* D E
* \ /
* F
*
* @test
*/
public function it_can_get_dag_descendants_from_a_complex_box_diamond_part_ii()
public function it_can_get_descendants_from_a_complex_box_diamond_part_ii()
{
/**
* Arrange/Given:
Expand Down Expand Up @@ -175,4 +171,167 @@ public function it_can_get_dag_descendants_from_a_complex_box_diamond_part_ii()
$this->assertSame($e->id, $results->shift()->id);
$this->assertSame($f->id, $results->shift()->id);
}

/**
* Tests: A
* |
* B
* |
* C <-- get ancestors of this entry
* |
* D
*
* @test
*/
public function it_can_get_ancestors_from_a_simple_chain()
{
/**
* Arrange/Given:
* - we have the following test models:
* - a - d
* - we have the following dag edge(s) in place:
* - B -> A
* - C -> B
* - D -> C
*/
$a = TestModel::create(['name' => 'a']);
$b = TestModel::create(['name' => 'b']);
$c = TestModel::create(['name' => 'c']);
$d = TestModel::create(['name' => 'd']);
$this->createEdge($b->id, $a->id);
$this->createEdge($c->id, $b->id);
$this->createEdge($d->id, $c->id);

/**
* Act/When:
* - we attempt to get DAG ancestors of C
*/
$results = TestModel::dagAncestorsOf($c->id, $this->source)->get();

/**
* Assert/Then:
* - we have a collection with the following entries:
* - A (from B -> A)
* - B (from C -> B)
*/
$this->assertCount(2, $results);
$this->assertSame($a->id, $results->shift()->id);
$this->assertSame($b->id, $results->shift()->id);
}

/**
* Tests: A
* / \
* B C
* | \ |
* D E <-- get ancestors of entry "E"
* \ /
* F
*
* @test
*/
public function it_can_get_ancestors_from_a_complex_box_diamond_part_i()
{
/**
* Arrange/Given:
* - we have the following test models:
* - a - f
* - we have the following dag edge(s) in place:
* - B -> A
* - D -> B
* - E -> B
* - C -> A
* - E -> C
* - F -> D
* - F -> E
*/
$a = TestModel::create(['name' => 'a']);
$b = TestModel::create(['name' => 'b']);
$c = TestModel::create(['name' => 'c']);
$d = TestModel::create(['name' => 'd']);
$e = TestModel::create(['name' => 'e']);
$f = TestModel::create(['name' => 'f']);
$this->createEdge($b->id, $a->id);
$this->createEdge($d->id, $b->id);
$this->createEdge($e->id, $b->id);
$this->createEdge($c->id, $a->id);
$this->createEdge($e->id, $c->id);
$this->createEdge($f->id, $d->id);
$this->createEdge($f->id, $e->id);

/**
* Act/When:
* - we attempt to get DAG ancestors of E
*/
$results = TestModel::dagAncestorsOf($e->id, $this->source)->get();

/**
* Assert/Then:
* - we have a collection with the following entries:
* - A (from E -> B -> A *and/or* E -> C -> A)
* - B (from E -> B)
* - C (from E -> C)
*/
$this->assertCount(3, $results);
$this->assertSame($a->id, $results->shift()->id);
$this->assertSame($b->id, $results->shift()->id);
$this->assertSame($c->id, $results->shift()->id);
}

/**
* Tests: A
* / \
* B C
* | \ |
* D E <-- get ancestors of entry "D"
* \ /
* F
*
* @test
*/
public function it_can_get_ancestors_from_a_complex_box_diamond_part_ii()
{
/**
* Arrange/Given:
* - we have the following test models:
* - a - f
* - we have the following dag edge(s) in place:
* - B -> A
* - D -> B
* - E -> B
* - C -> A
* - E -> C
* - F -> D
* - F -> E
*/
$a = TestModel::create(['name' => 'a']);
$b = TestModel::create(['name' => 'b']);
$c = TestModel::create(['name' => 'c']);
$d = TestModel::create(['name' => 'd']);
$e = TestModel::create(['name' => 'e']);
$f = TestModel::create(['name' => 'f']);
$this->createEdge($b->id, $a->id);
$this->createEdge($d->id, $b->id);
$this->createEdge($e->id, $b->id);
$this->createEdge($c->id, $a->id);
$this->createEdge($e->id, $c->id);
$this->createEdge($f->id, $d->id);
$this->createEdge($f->id, $e->id);

/**
* Act/When:
* - we attempt to get DAG ancestors of D
*/
$results = TestModel::dagAncestorsOf($d->id, $this->source)->get();

/**
* Assert/Then:
* - we have a collection with the following entries:
* - A (from D -> B -> A)
* - B (from D -> B)
*/
$this->assertCount(2, $results);
$this->assertSame($a->id, $results->shift()->id);
$this->assertSame($b->id, $results->shift()->id);
}
}
13 changes: 13 additions & 0 deletions tests/Support/CreatesEdges.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Telkins\Dag\Tests\Support;

trait CreatesEdges
{
protected $source = 'test-source';

protected function createEdge(int $startVertex, int $endVertex, string $source = null)
{
return dag()->createEdge($startVertex, $endVertex, ($source ?? $this->source));
}
}

0 comments on commit 2dc9958

Please sign in to comment.