Skip to content

Commit

Permalink
Merge pull request #7 from telkins/children
Browse files Browse the repository at this point in the history
Can constrain "generations" returned when using IsDagManaged scopes
  • Loading branch information
telkins authored Jan 7, 2020
2 parents 2dc9958 + 33105c3 commit 3cf270c
Show file tree
Hide file tree
Showing 4 changed files with 281 additions and 7 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ Homestead.json
.rocketeer/

composer.lock
.phpunit.result.cache

14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,23 @@ 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();
$ancestors = MyModel::dagAncestorsOf($myModel->id, 'my-source')->get();
```

Again, an ID and source must be provided.

Both of the aforementioned methods also allow the caller to constrain the results based on the number of hops. So, if you want to get the immediate children of the specified model ID, then you could do the following:
```php
$descendants = MyModel::dagDescendantsOf($myModel->id, 'my-source', 0)->get();
```

And, of course, in order to get the parents and grandparents of the specified model ID, you could do the following:
```php
$ancestors = MyModel::dagAncestorsOf($myModel->id, 'my-source', 1)->get();
```

Not providing the `$maxHops` parameter means that all descendants or ancestors will be returned.

## Testing

```bash
Expand Down
17 changes: 11 additions & 6 deletions src/Models/Traits/IsDagManaged.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ trait IsDagManaged
* @param int $modelId
* @param string $source
*/
public function scopeDagDescendantsOf($query, int $modelId, string $source)
public function scopeDagDescendantsOf($query, int $modelId, string $source, ?int $maxHops = null)
{
$this->scopeDagRelationsOf($query, $modelId, $source, true);
$this->scopeDagRelationsOf($query, $modelId, $source, true, $maxHops);
}

/**
Expand All @@ -23,9 +23,9 @@ public function scopeDagDescendantsOf($query, int $modelId, string $source)
* @param int $modelId
* @param string $source
*/
public function scopeDagAncestorsOf($query, int $modelId, string $source)
public function scopeDagAncestorsOf($query, int $modelId, string $source, ?int $maxHops = null)
{
$this->scopeDagRelationsOf($query, $modelId, $source, false);
$this->scopeDagRelationsOf($query, $modelId, $source, false, $maxHops);
}

/**
Expand All @@ -36,9 +36,13 @@ public function scopeDagAncestorsOf($query, int $modelId, string $source)
* @param string $source
* @param bool. $down
*/
public function scopeDagRelationsOf($query, int $modelId, string $source, bool $down)
public function scopeDagRelationsOf($query, int $modelId, string $source, bool $down, ?int $maxHops = null)
{
$query->whereIn($this->getQualifiedKeyName(), function ($query) use ($modelId, $source, $down) {
$maxHopsConfig = config('laravel-dag-manager.max_hops');
$maxHops = $maxHops ?? $maxHopsConfig;
$maxHops = min($maxHops, $maxHopsConfig);

$query->whereIn($this->getQualifiedKeyName(), function ($query) use ($modelId, $source, $maxHops, $down) {
$selectField = $down ? 'start_vertex' : 'end_vertex';
$whereField = $down ? 'end_vertex' : 'start_vertex';

Expand All @@ -47,6 +51,7 @@ public function scopeDagRelationsOf($query, int $modelId, string $source, bool $
->where([
["dag_edges.{$whereField}", $modelId],
['dag_edges.source', $source],
['dag_edges.hops', '<=', $maxHops],
]);
});
}
Expand Down
255 changes: 255 additions & 0 deletions tests/IsDagManagedTraitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -334,4 +334,259 @@ public function it_can_get_ancestors_from_a_complex_box_diamond_part_ii()
$this->assertSame($a->id, $results->shift()->id);
$this->assertSame($b->id, $results->shift()->id);
}

/**
* Tests: A <-- get descendants of this entry
* |
* B
* |
* C
* |
* D
*
* @test
* @dataProvider provideMaxHopsForSimpleChain
*/
public function it_can_get_descendants_from_a_simple_chain_constrained_by_max_hops($maxHops, $expectedNames)
{
/**
* 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 descendants of A, constrained by $maxHops
*/
$results = TestModel::dagDescendantsOf($a->id, $this->source, $maxHops)->get();

/**
* Assert/Then:
* - we have a collection with the expected number of entries
* - each of the expected names can be found in the results
*/
$this->assertCount(count($expectedNames), $results);
collect($expectedNames)->each(function ($expectedName) use ($results) {
$this->assertTrue(in_array($expectedName, $results->pluck('name')->all()));
});
}

public function provideMaxHopsForSimpleChain()
{
return [
[0, ['b']],
[1, ['b', 'c']],
[2, ['b', 'c', 'd']],
[3, ['b', 'c', 'd']],
[null, ['b', 'c', 'd']],
];
}

/**
* Tests: A <-- get descendants of entry "B"
* / \
* B C
* | \ |
* D E
* \ /
* F
*
* @test
* @dataProvider provideMaxHopsForComplexBoxDiamond
*/
public function it_can_get_descendants_from_a_complex_box_diamond_constrained_by_max_hops($maxHops, $expectedNames)
{
/**
* 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 descendants from A, constrained by $maxHops
*/
$results = TestModel::dagDescendantsOf($a->id, $this->source, $maxHops)->get();

/**
* Assert/Then:
* - we have a collection with the expected number of entries
* - each of the expected names can be found in the results
*/
$this->assertCount(count($expectedNames), $results);
collect($expectedNames)->each(function ($expectedName) use ($results) {
$this->assertTrue(in_array($expectedName, $results->pluck('name')->all()));
});
}

public function provideMaxHopsForComplexBoxDiamond()
{
return [
[0, ['b', 'c']],
[1, ['b', 'c', 'd', 'e']],
[2, ['b', 'c', 'd', 'e', 'f']],
[3, ['b', 'c', 'd', 'e', 'f']],
[null, ['b', 'c', 'd', 'e', 'f']],
];
}

/**
* Tests: A
* |
* B
* |
* C
* |
* D <-- get ancestors of this entry
*
* @test
* @dataProvider provideMaxHopsForSimpleChainUp
*/
public function it_can_get_ancestors_from_a_simple_chain_constrained_by_max_hops($maxHops, $expectedNames)
{
/**
* 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 D, constrained by $maxHops
*/
$results = TestModel::dagAncestorsOf($d->id, $this->source, $maxHops)->get();

/**
* Assert/Then:
* - we have a collection with the expected number of entries
* - each of the expected names can be found in the results
*/
$this->assertCount(count($expectedNames), $results);
collect($expectedNames)->each(function ($expectedName) use ($results) {
$this->assertTrue(in_array($expectedName, $results->pluck('name')->all()));
});
}

public function provideMaxHopsForSimpleChainUp()
{
return [
[0, ['c']],
[1, ['c', 'b']],
[2, ['c', 'b', 'a']],
[null, ['c', 'b', 'a']],
];
}

/**
* Tests: A
* / \
* B C
* | \ |
* D E
* \ /
* F <-- get ancestors of entry "F"
*
* @test
* @dataProvider provideMaxHopsForComplexBoxDiamondUp
*/
public function it_can_get_ancestors_from_a_complex_box_diamond_constrained_by_max_hops($maxHops, $expectedNames)
{
/**
* 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 F, constrained by $maxHops
*/
$results = TestModel::dagAncestorsOf($f->id, $this->source, $maxHops)->get();

/**
* Assert/Then:
* - we have a collection with the expected number of entries
* - each of the expected names can be found in the results
*/
$this->assertCount(count($expectedNames), $results);
collect($expectedNames)->each(function ($expectedName) use ($results) {
$this->assertTrue(in_array($expectedName, $results->pluck('name')->all()));
});
}

public function provideMaxHopsForComplexBoxDiamondUp()
{
return [
[0, ['e', 'd']],
[1, ['e', 'd', 'b', 'c']],
[2, ['e', 'd', 'b', 'c', 'a']],
[3, ['e', 'd', 'b', 'c', 'a']],
[null, ['e', 'd', 'b', 'c', 'a']],
];
}
}

0 comments on commit 3cf270c

Please sign in to comment.