Skip to content

Commit 0fb4fa5

Browse files
committed
Adding Ambiguity exception with nice message
1 parent fb08604 commit 0fb4fa5

File tree

2 files changed

+134
-7
lines changed

2 files changed

+134
-7
lines changed

src/SchemaAnalyzer.php

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Doctrine\DBAL\Schema\Table;
1111
use Fhaculty\Graph\Edge\Base;
1212
use Fhaculty\Graph\Graph;
13+
use Fhaculty\Graph\Vertex;
1314
use Graphp\Algorithms\ShortestPath\Dijkstra;
1415

1516
/**
@@ -172,8 +173,15 @@ private function getShortestPathWithoutCache($fromTable, $toTable)
172173
{
173174
$graph = $this->buildSchemaGraph();
174175

175-
$predecessors = MultiDijkstra::findShortestPaths($graph->getVertex($fromTable), $graph->getVertex($toTable));
176-
$edges = MultiDijkstra::getCheapestPathFromPredecesArray($graph->getVertex($fromTable), $graph->getVertex($toTable), $predecessors);
176+
try {
177+
$predecessors = MultiDijkstra::findShortestPaths($graph->getVertex($fromTable), $graph->getVertex($toTable));
178+
$edges = MultiDijkstra::getCheapestPathFromPredecesArray($graph->getVertex($fromTable), $graph->getVertex($toTable), $predecessors);
179+
} catch (MultiDijkstraAmbiguityException $e) {
180+
// If there is more than 1 short path, let's display this.
181+
$paths = MultiDijkstra::getAllPossiblePathsFromPredecesArray($graph->getVertex($fromTable), $graph->getVertex($toTable), $predecessors);
182+
$msg = $this->getAmbiguityExceptionMessage($paths, $graph->getVertex($fromTable), $graph->getVertex($toTable));
183+
throw new ShortestPathAmbiguityException($msg);
184+
}
177185

178186
$foreignKeys = [];
179187

@@ -262,4 +270,75 @@ private function getSchema() {
262270
return $this->schema;
263271
}
264272

273+
/**
274+
* Returns the full exception message when an ambiguity arises.
275+
*
276+
* @param Base[][] $paths
277+
* @param Vertex $startVertex
278+
*/
279+
private function getAmbiguityExceptionMessage(array $paths, Vertex $startVertex, Vertex $endVertex) {
280+
$textPaths = [];
281+
$i = 1;
282+
foreach ($paths as $path) {
283+
$textPaths[] = "Path ".$i.": ".$this->getTextualPath($path, $startVertex);
284+
$i++;
285+
}
286+
287+
$msg = sprintf("There are many possible shortest paths between table '%s' and table '%s'\n\n",
288+
$startVertex->getId(), $endVertex->getId());
289+
290+
$msg .= implode("\n\n", $textPaths);
291+
292+
return $msg;
293+
}
294+
295+
/**
296+
* Returns the textual representation of the path.
297+
*
298+
* @param Base[] $path
299+
* @param Vertex $startVertex
300+
*/
301+
private function getTextualPath(array $path, Vertex $startVertex) {
302+
$currentVertex = $startVertex;
303+
$currentTable = $currentVertex->getId();
304+
305+
$textPath = $currentTable;
306+
307+
foreach ($path as $edge) {
308+
/* @var $fk ForeignKeyConstraint */
309+
if ($fk = $edge->getAttribute('fk')) {
310+
if ($fk->getForeignTableName() == $currentTable) {
311+
$currentTable = $fk->getLocalTable()->getName();
312+
$isForward = false;
313+
} else {
314+
$currentTable = $fk->getForeignTableName();
315+
$isForward = true;
316+
}
317+
318+
$columns = implode(',', $fk->getLocalColumns());
319+
320+
$textPath .= " ".(!$isForward?"<":"");
321+
$textPath .= "--(".$columns.")--";
322+
$textPath .= ($isForward?">":"")." ";
323+
$textPath .= $currentTable;
324+
} elseif ($junctionTable = $edge->getAttribute('junction')) {
325+
/* @var $junctionTable Table */
326+
$junctionFks = array_values($junctionTable->getForeignKeys());
327+
// We need to order the 2 FKs. The first one is the one that has a common point with the current table.
328+
$fk = $junctionFks[0];
329+
if ($fk->getForeignTableName() == $currentTable) {
330+
$currentTable = $junctionFks[1]->getForeignTableName();
331+
} else {
332+
$currentTable = $fk->getForeignTableName();
333+
}
334+
$textPath .= " <=(".$junctionTable->getName().")=> ".$currentTable;
335+
} else {
336+
// @codeCoverageIgnoreStart
337+
throw new SchemaAnalyzerException('Unexpected edge. We should have a fk or a junction attribute.');
338+
// @codeCoverageIgnoreEnd
339+
}
340+
}
341+
342+
return $textPath;
343+
}
265344
}

tests/SchemaAnalyzerTest.php

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -260,9 +260,6 @@ public function testCache() {
260260
$this->assertTrue($r1 === $r2);
261261
}
262262

263-
/**
264-
* @expectedException \Mouf\Database\SchemaAnalyzer\ShortestPathAmbiguityException
265-
*/
266263
public function testAmbiguityException() {
267264
$schema = $this->getBaseSchema();
268265

@@ -280,9 +277,60 @@ public function testAmbiguityException() {
280277
$role_right2->addForeignKeyConstraint($schema->getTable('right'), array("right_id"), array("id"), array("onUpdate" => "CASCADE"));
281278
$role_right2->setPrimaryKey(["role_id", "right_id"]);
282279

283-
$schemaAnalyzer = new SchemaAnalyzer($schema);
280+
$schemaAnalyzer = new SchemaAnalyzer(new StubSchemaManager($schema));
281+
282+
$exceptionTriggered = false;
283+
try {
284+
$schemaAnalyzer->getShortestPath("role", "right");
285+
} catch (ShortestPathAmbiguityException $e) {
286+
$this->assertContains("role <=(role_right)=> right", $e->getMessage());
287+
$this->assertContains("role <=(role_right2)=> right", $e->getMessage());
288+
$exceptionTriggered = true;
289+
}
290+
$this->assertTrue($exceptionTriggered);
291+
292+
$exceptionTriggered = false;
293+
try {
294+
$schemaAnalyzer->getShortestPath("right", "role");
295+
} catch (ShortestPathAmbiguityException $e) {
296+
$this->assertContains("right <=(role_right)=> role", $e->getMessage());
297+
$this->assertContains("right <=(role_right2)=> role", $e->getMessage());
298+
$exceptionTriggered = true;
299+
}
300+
$this->assertTrue($exceptionTriggered);
301+
302+
}
284303

285-
$schemaAnalyzer->getShortestPath("role", "right");
304+
public function testAmbiguityExceptionWithNoJointure() {
305+
$schema = $this->getBaseSchema();
306+
$right = $schema->getTable("right");
307+
$right->addColumn("role_id", "integer", array("unsigned" => true));
308+
$right->addForeignKeyConstraint($schema->getTable('role'), array("role_id"), array("id"), array("onUpdate" => "CASCADE"));
309+
310+
$right->addColumn("role_id2", "integer", array("unsigned" => true));
311+
$right->addForeignKeyConstraint($schema->getTable('role'), array("role_id2"), array("id"), array("onUpdate" => "CASCADE"));
312+
313+
$schemaAnalyzer = new SchemaAnalyzer(new StubSchemaManager($schema));
314+
315+
$exceptionTriggered = false;
316+
try {
317+
$schemaAnalyzer->getShortestPath("role", "right");
318+
} catch (ShortestPathAmbiguityException $e) {
319+
$this->assertContains("role <--(role_id)-- right", $e->getMessage());
320+
$this->assertContains("role <--(role_id2)-- right", $e->getMessage());
321+
$exceptionTriggered = true;
322+
}
323+
$this->assertTrue($exceptionTriggered);
324+
325+
$exceptionTriggered = false;
326+
try {
327+
$schemaAnalyzer->getShortestPath("right", "role");
328+
} catch (ShortestPathAmbiguityException $e) {
329+
$this->assertContains("right --(role_id)--> role", $e->getMessage());
330+
$this->assertContains("right --(role_id2)--> role", $e->getMessage());
331+
$exceptionTriggered = true;
332+
}
333+
$this->assertTrue($exceptionTriggered);
286334
}
287335

288336
}

0 commit comments

Comments
 (0)