Skip to content

Commit bbbd38d

Browse files
committed
Adding cost modifiers
1 parent 70a05ab commit bbbd38d

File tree

3 files changed

+173
-2
lines changed

3 files changed

+173
-2
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,26 @@ $cache = new ApcCache();
8888
$schemaAnalyzer = new SchemaAnalyzer($conn->getSchemaManager(), $cache, "my_prefix");
8989
```
9090

91+
## Changing the cost of the foreign keys to alter the shortest path
92+
93+
If you are facing an ambiguity exception or if the shortest path simply does not suit you, you can alter the
94+
cost of the foreign keys.
95+
96+
```php
97+
$schemaAnalyzer->setForeignKeyCost($tableName, $columnName, $cost);
98+
```
99+
100+
The `$cost` can be any number. Remember that the default cost for a foreign key is **1**.
101+
102+
SchemaAnalyzer comes with a set of default constants to help you work with costs:
103+
104+
- `SchemaAnalyzer::WEIGHT_IMPORTANT` (0.75) for foreign keys that should be followed in priority
105+
- `SchemaAnalyzer::WEIGHT_IRRELEVANT` (2) for foreign keys that should be generally avoided
106+
- `SchemaAnalyzer::WEIGHT_IGNORE` (Infinity) for foreign keys that should never be used as part of the shortest path
107+
108+
Another option is to add a cost modifier to a table. This will alter the cost of all foreign keys pointing to or
109+
originating from this table.
110+
111+
```php
112+
$schemaAnalyzer->setTableCostModifier($tableName, $cost);
113+
```

src/SchemaAnalyzer.php

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ class SchemaAnalyzer
2424
private static $WEIGHT_FK = 1;
2525
private static $WEIGHT_JOINTURE_TABLE = 1.5;
2626

27+
const WEIGHT_IMPORTANT = 0.75;
28+
const WEIGHT_IRRELEVANT = 2;
29+
const WEIGHT_IGNORE = INF;
30+
2731
/**
2832
* @var AbstractSchemaManager
2933
*/
@@ -44,6 +48,18 @@ class SchemaAnalyzer
4448
*/
4549
private $cachePrefix;
4650

51+
/**
52+
* Nested arrays containing table => column => cost
53+
* @var float[][]
54+
*/
55+
private $alteredCosts = [];
56+
57+
/**
58+
* Array containing table cost
59+
* @var float[]
60+
*/
61+
private $alteredTableCosts = [];
62+
4763
/**
4864
* @param AbstractSchemaManager $schemaManager
4965
* @param Cache|null $cache The Doctrine cache service to use to cache results (optional)
@@ -239,7 +255,16 @@ private function buildSchemaGraph()
239255
foreach ($table->getForeignKeys() as $fk) {
240256
// Create an undirected edge, with weight = 1
241257
$edge = $graph->getVertex($table->getName())->createEdge($graph->getVertex($fk->getForeignTableName()));
242-
$edge->setWeight(self::$WEIGHT_FK);
258+
if (isset($this->alteredCosts[$fk->getLocalTable()->getName()][implode(',',$fk->getLocalColumns())])) {
259+
$cost = $this->alteredCosts[$fk->getLocalTable()->getName()][implode(',',$fk->getLocalColumns())];
260+
} else {
261+
$cost = self::$WEIGHT_FK;
262+
}
263+
if (isset($this->alteredTableCosts[$fk->getLocalTable()->getName()])) {
264+
$cost *= $this->alteredTableCosts[$fk->getLocalTable()->getName()];
265+
}
266+
267+
$edge->setWeight($cost);
243268
$edge->getAttributeBag()->setAttribute('fk', $fk);
244269
}
245270
}
@@ -252,7 +277,11 @@ private function buildSchemaGraph()
252277
}
253278

254279
$edge = $graph->getVertex($tables[0])->createEdge($graph->getVertex($tables[1]));
255-
$edge->setWeight(self::$WEIGHT_JOINTURE_TABLE);
280+
$cost = self::$WEIGHT_JOINTURE_TABLE;
281+
if (isset($this->alteredTableCosts[$junctionTable->getName()])) {
282+
$cost *= $this->alteredTableCosts[$junctionTable->getName()];
283+
}
284+
$edge->setWeight($cost);
256285
$edge->getAttributeBag()->setAttribute('junction', $junctionTable);
257286
}
258287

@@ -351,4 +380,45 @@ private function getTextualPath(array $path, Vertex $startVertex)
351380

352381
return $textPath;
353382
}
383+
384+
/**
385+
* Sets the cost of a foreign key.
386+
*
387+
* @param string $tableName
388+
* @param string $columnName
389+
* @param float $cost
390+
* @return $this
391+
*/
392+
public function setForeignKeyCost($tableName, $columnName, $cost) {
393+
$this->alteredCosts[$tableName][$columnName] = $cost;
394+
}
395+
396+
/**
397+
* Sets the cost modifier of a table.
398+
*
399+
* @param string $tableName
400+
* @param float $cost
401+
* @return $this
402+
*/
403+
public function setTableCostModifier($tableName, $cost) {
404+
$this->alteredTableCosts[$tableName] = $cost;
405+
}
406+
407+
/**
408+
* Sets the cost modifier of all tables at once.
409+
*
410+
* @param array<string, float> $tableCosts The key is the table name, the value is the cost modifier.
411+
*/
412+
public function setTableCostModifiers(array $tableCosts) {
413+
$this->alteredTableCosts = $tableCosts;
414+
}
415+
416+
/**
417+
* Sets the cost of all foreign keys at once.
418+
*
419+
* @param array<string, array<string, float>> $fkCosts First key is the table name, second key is the column name, the value is the cost.
420+
*/
421+
public function setForeignKeyCosts(array $fkCosts) {
422+
$this->alteredCosts = $fkCosts;
423+
}
354424
}

tests/SchemaAnalyzerTest.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,5 +333,83 @@ public function testAmbiguityExceptionWithNoJointure() {
333333
$this->assertTrue($exceptionTriggered);
334334
}
335335

336+
public function testAmbiguityExceptionWithNoJointureAndModifiedWeight() {
337+
$schema = $this->getBaseSchema();
338+
$right = $schema->getTable("right");
339+
$right->addColumn("role_id", "integer", array("unsigned" => true));
340+
$right->addForeignKeyConstraint($schema->getTable('role'), array("role_id"), array("id"), array("onUpdate" => "CASCADE"));
341+
342+
$right->addColumn("role_id2", "integer", array("unsigned" => true));
343+
$right->addForeignKeyConstraint($schema->getTable('role'), array("role_id2"), array("id"), array("onUpdate" => "CASCADE"));
344+
345+
$schemaAnalyzer = new SchemaAnalyzer(new StubSchemaManager($schema));
346+
$schemaAnalyzer->setForeignKeyCost('right', 'role_id', SchemaAnalyzer::WEIGHT_IMPORTANT);
347+
348+
$fks = $schemaAnalyzer->getShortestPath("role", "right");
349+
350+
$this->assertCount(1, $fks);
351+
$this->assertEquals("right", $fks[0]->getLocalTable()->getName());
352+
$this->assertEquals("role", $fks[0]->getForeignTableName());
353+
}
354+
355+
public function testAmbiguityExceptionWithJointureAndModifiedWeight() {
356+
$schema = $this->getBaseSchema();
357+
358+
$role_right = $schema->createTable("role_right");
359+
$role_right->addColumn("role_id", "integer", array("unsigned" => true));
360+
$role_right->addColumn("right_id", "integer", array("unsigned" => true));
361+
$role_right->addForeignKeyConstraint($schema->getTable('role'), array("role_id"), array("id"), array("onUpdate" => "CASCADE"));
362+
$role_right->addForeignKeyConstraint($schema->getTable('right'), array("right_id"), array("id"), array("onUpdate" => "CASCADE"));
363+
$role_right->setPrimaryKey(["role_id", "right_id"]);
364+
365+
$role_right2 = $schema->createTable("role_right2");
366+
$role_right2->addColumn("role_id", "integer", array("unsigned" => true));
367+
$role_right2->addColumn("right_id", "integer", array("unsigned" => true));
368+
$role_right2->addForeignKeyConstraint($schema->getTable('role'), array("role_id"), array("id"), array("onUpdate" => "CASCADE"));
369+
$role_right2->addForeignKeyConstraint($schema->getTable('right'), array("right_id"), array("id"), array("onUpdate" => "CASCADE"));
370+
$role_right2->setPrimaryKey(["role_id", "right_id"]);
371+
372+
$schemaAnalyzer = new SchemaAnalyzer(new StubSchemaManager($schema));
373+
$schemaAnalyzer->setTableCostModifier("role_right2", SchemaAnalyzer::WEIGHT_IRRELEVANT);
374+
375+
$fks = $schemaAnalyzer->getShortestPath("role", "right");
376+
377+
$this->assertCount(2, $fks);
378+
$this->assertEquals("role_right", $fks[0]->getLocalTable()->getName());
379+
$this->assertEquals("role", $fks[0]->getForeignTableName());
380+
$this->assertEquals("role_right", $fks[1]->getLocalTable()->getName());
381+
$this->assertEquals("right", $fks[1]->getForeignTableName());
382+
}
383+
384+
public function testAmbiguityExceptionWithJointureAndModifiedWeight2() {
385+
$schema = $this->getBaseSchema();
386+
387+
$role_right = $schema->createTable("role_right");
388+
$role_right->addColumn("role_id", "integer", array("unsigned" => true));
389+
$role_right->addColumn("right_id", "integer", array("unsigned" => true));
390+
$role_right->addForeignKeyConstraint($schema->getTable('role'), array("role_id"), array("id"), array("onUpdate" => "CASCADE"));
391+
$role_right->addForeignKeyConstraint($schema->getTable('right'), array("right_id"), array("id"), array("onUpdate" => "CASCADE"));
392+
$role_right->setPrimaryKey(["role_id", "right_id"]);
393+
394+
$role_right2 = $schema->createTable("role_right2");
395+
$role_right2->addColumn("role_id", "integer", array("unsigned" => true));
396+
$role_right2->addColumn("right_id", "integer", array("unsigned" => true));
397+
$role_right2->addForeignKeyConstraint($schema->getTable('role'), array("role_id"), array("id"), array("onUpdate" => "CASCADE"));
398+
$role_right2->addForeignKeyConstraint($schema->getTable('right'), array("right_id"), array("id"), array("onUpdate" => "CASCADE"));
399+
$role_right2->setPrimaryKey(["role_id", "right_id"]);
400+
401+
$schemaAnalyzer = new SchemaAnalyzer(new StubSchemaManager($schema));
402+
$schemaAnalyzer->setTableCostModifiers(["role_right2" => SchemaAnalyzer::WEIGHT_IRRELEVANT]);
403+
$schemaAnalyzer->setForeignKeyCosts([]);
404+
405+
$fks = $schemaAnalyzer->getShortestPath("role", "right");
406+
407+
$this->assertCount(2, $fks);
408+
$this->assertEquals("role_right", $fks[0]->getLocalTable()->getName());
409+
$this->assertEquals("role", $fks[0]->getForeignTableName());
410+
$this->assertEquals("role_right", $fks[1]->getLocalTable()->getName());
411+
$this->assertEquals("right", $fks[1]->getForeignTableName());
412+
}
413+
336414
}
337415

0 commit comments

Comments
 (0)