Skip to content

Commit 3fee3fc

Browse files
committed
Merge pull request #3 from moufmouf/1.0
Adding ability to detect inheritance relationships between tables
2 parents a54928b + 311c9f3 commit 3fee3fc

File tree

6 files changed

+487
-257
lines changed

6 files changed

+487
-257
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,28 @@ A **junction table** is a table:
4747
- that has **exactly 2 foreign keys**
4848
- that has **only 2 columns** (or **3 columns** if the one of those is an *autoincremented primary key*).
4949

50+
## Detecting inheritance relationship between tables
51+
52+
### About inheritance relationships
53+
54+
If a table "user" has a primary key that is also a foreign key pointing on table "contact", then table "user" is
55+
considered to be a child of table "contact". This is because you cannot create a row in "user" without having a row
56+
with the same ID in "contact".
57+
58+
Therefore, a "user" ID has to match a "contact", but a "contact" has not necessarily a "user" associated.
59+
60+
### Detecting inheritance relationships
61+
62+
You can use `SchemaAnalyzer` to detect parent / child relationships.
63+
64+
```php
65+
$parent = $schemaAnalyzer->getParentTable("user");
66+
// This will return the "contact" table (as a string)
67+
68+
$children = $schemaAnalyzer->getChildrenTables("contact");
69+
// This will return an array of tables whose parent is contact: ["user"]
70+
```
71+
5072
## Computing the shortest path between 2 tables
5173

5274
Following foreign keys, the `getShortestPath` function will try to find the shortest path between 2 tables.
@@ -56,6 +78,8 @@ Internals:
5678

5779
- Each foreign key has a *cost* of 1
5880
- Junction tables have a *cost* of 1.5, instead of 2 (one for each foreign key)
81+
- Foreign keys representing an inheritance relationship (i.e. foreign keys binding the primary keys of 2 tables)
82+
have a *cost* of 0.1
5983

6084
```php
6185
// $conn is the DBAL connection.

src/SchemaAnalyzer.php

Lines changed: 150 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
class SchemaAnalyzer
2323
{
2424
private static $WEIGHT_FK = 1;
25+
private static $WEIGHT_INHERITANCE_FK = 0.1;
2526
private static $WEIGHT_JOINTURE_TABLE = 1.5;
2627

2728
const WEIGHT_IMPORTANT = 0.75;
@@ -49,13 +50,15 @@ class SchemaAnalyzer
4950
private $cachePrefix;
5051

5152
/**
52-
* Nested arrays containing table => column => cost
53+
* Nested arrays containing table => column => cost.
54+
*
5355
* @var float[][]
5456
*/
5557
private $alteredCosts = [];
5658

5759
/**
58-
* Array containing table cost
60+
* Array containing table cost.
61+
*
5962
* @var float[]
6063
*/
6164
private $alteredTableCosts = [];
@@ -124,7 +127,11 @@ private function isJunctionTable(Table $table)
124127
return false;
125128
}
126129

127-
$pkColumns = $table->getPrimaryKeyColumns();
130+
if ($table->hasPrimaryKey()) {
131+
$pkColumns = $table->getPrimaryKeyColumns();
132+
} else {
133+
$pkColumns = [];
134+
}
128135

129136
if (count($pkColumns) == 1 && count($columns) == 2) {
130137
return false;
@@ -170,14 +177,9 @@ private function isJunctionTable(Table $table)
170177
*/
171178
public function getShortestPath($fromTable, $toTable)
172179
{
173-
$cacheKey = $this->cachePrefix.'_shortest_'.$fromTable.'```'.$toTable;
174-
$path = $this->cache->fetch($cacheKey);
175-
if ($path === false) {
176-
$path = $this->getShortestPathWithoutCache($fromTable, $toTable);
177-
$this->cache->save($cacheKey, $path);
178-
}
179-
180-
return $path;
180+
return $this->fromCache($this->cachePrefix.'_shortest_'.$fromTable.'```'.$toTable, function () use ($fromTable, $toTable) {
181+
return $this->getShortestPathWithoutCache($fromTable, $toTable);
182+
});
181183
}
182184

183185
/**
@@ -255,8 +257,10 @@ private function buildSchemaGraph()
255257
foreach ($table->getForeignKeys() as $fk) {
256258
// Create an undirected edge, with weight = 1
257259
$edge = $graph->getVertex($table->getName())->createEdge($graph->getVertex($fk->getForeignTableName()));
258-
if (isset($this->alteredCosts[$fk->getLocalTable()->getName()][implode(',',$fk->getLocalColumns())])) {
259-
$cost = $this->alteredCosts[$fk->getLocalTable()->getName()][implode(',',$fk->getLocalColumns())];
260+
if (isset($this->alteredCosts[$fk->getLocalTable()->getName()][implode(',', $fk->getLocalColumns())])) {
261+
$cost = $this->alteredCosts[$fk->getLocalTable()->getName()][implode(',', $fk->getLocalColumns())];
262+
} elseif ($this->isInheritanceRelationship($fk)) {
263+
$cost = self::$WEIGHT_INHERITANCE_FK;
260264
} else {
261265
$cost = self::$WEIGHT_FK;
262266
}
@@ -386,21 +390,25 @@ private function getTextualPath(array $path, Vertex $startVertex)
386390
*
387391
* @param string $tableName
388392
* @param string $columnName
389-
* @param float $cost
393+
* @param float $cost
394+
*
390395
* @return $this
391396
*/
392-
public function setForeignKeyCost($tableName, $columnName, $cost) {
397+
public function setForeignKeyCost($tableName, $columnName, $cost)
398+
{
393399
$this->alteredCosts[$tableName][$columnName] = $cost;
394400
}
395401

396402
/**
397403
* Sets the cost modifier of a table.
398404
*
399405
* @param string $tableName
400-
* @param float $cost
406+
* @param float $cost
407+
*
401408
* @return $this
402409
*/
403-
public function setTableCostModifier($tableName, $cost) {
410+
public function setTableCostModifier($tableName, $cost)
411+
{
404412
$this->alteredTableCosts[$tableName] = $cost;
405413
}
406414

@@ -409,7 +417,8 @@ public function setTableCostModifier($tableName, $cost) {
409417
*
410418
* @param array<string, float> $tableCosts The key is the table name, the value is the cost modifier.
411419
*/
412-
public function setTableCostModifiers(array $tableCosts) {
420+
public function setTableCostModifiers(array $tableCosts)
421+
{
413422
$this->alteredTableCosts = $tableCosts;
414423
}
415424

@@ -418,7 +427,129 @@ public function setTableCostModifiers(array $tableCosts) {
418427
*
419428
* @param array<string, array<string, float>> $fkCosts First key is the table name, second key is the column name, the value is the cost.
420429
*/
421-
public function setForeignKeyCosts(array $fkCosts) {
430+
public function setForeignKeyCosts(array $fkCosts)
431+
{
422432
$this->alteredCosts = $fkCosts;
423433
}
434+
435+
/**
436+
* Returns true if this foreign key represents an inheritance relationship,
437+
* i.e. if this foreign key is based on a primary key.
438+
*
439+
* @param ForeignKeyConstraint $fk
440+
*
441+
* @return true
442+
*/
443+
private function isInheritanceRelationship(ForeignKeyConstraint $fk)
444+
{
445+
if (!$fk->getLocalTable()->hasPrimaryKey()) {
446+
return false;
447+
}
448+
$fkColumnNames = $fk->getLocalColumns();
449+
$pkColumnNames = $fk->getLocalTable()->getPrimaryKeyColumns();
450+
451+
sort($fkColumnNames);
452+
sort($pkColumnNames);
453+
454+
return $fkColumnNames == $pkColumnNames;
455+
}
456+
457+
/**
458+
* If this table is pointing to a parent table (if its primary key is a foreign key pointing on another table),
459+
* this function will return the pointed table.
460+
* This function will return null if there is no parent table.
461+
*
462+
* @param string $tableName
463+
*
464+
* @return string|null
465+
*/
466+
public function getParentTable($tableName)
467+
{
468+
return $this->fromCache($this->cachePrefix.'_parent_'.$tableName, function () use ($tableName) {
469+
return $this->getParentTableWithoutCache($tableName);
470+
});
471+
}
472+
473+
/**
474+
* If this table is pointing to a parent table (if its primary key is a foreign key pointing on another table),
475+
* this function will return the pointed table.
476+
* This function will return null if there is no parent table.
477+
*
478+
* @param string $tableName
479+
*
480+
* @return string|null
481+
*/
482+
private function getParentTableWithoutCache($tableName)
483+
{
484+
$table = $this->getSchema()->getTable($tableName);
485+
foreach ($table->getForeignKeys() as $fk) {
486+
if ($this->isInheritanceRelationship($fk)) {
487+
return $fk->getForeignTableName();
488+
}
489+
}
490+
491+
return;
492+
}
493+
494+
/**
495+
* If this table is pointed by children tables (if other child tables have a primary key that is also a
496+
* foreign key to this table), this function will return the list of child tables.
497+
* This function will return an empty array if there are no children tables.
498+
*
499+
* @param string $tableName
500+
*
501+
* @return string[]
502+
*/
503+
public function getChildrenTables($tableName)
504+
{
505+
return $this->fromCache($this->cachePrefix.'_children_'.$tableName, function () use ($tableName) {
506+
return $this->getChildrenTablesWithoutCache($tableName);
507+
});
508+
}
509+
510+
/**
511+
* If this table is pointed by children tables (if other child tables have a primary key that is also a
512+
* foreign key to this table), this function will return the list of child tables.
513+
* This function will return an empty array if there are no children tables.
514+
*
515+
* @param string $tableName
516+
*
517+
* @return string[]
518+
*/
519+
private function getChildrenTablesWithoutCache($tableName)
520+
{
521+
$schema = $this->getSchema();
522+
$children = [];
523+
foreach ($schema->getTables() as $table) {
524+
if ($table->getName() === $tableName) {
525+
continue;
526+
}
527+
foreach ($table->getForeignKeys() as $fk) {
528+
if ($fk->getForeignTableName() === $tableName && $this->isInheritanceRelationship($fk)) {
529+
$children[] = $fk->getLocalTableName();
530+
}
531+
}
532+
}
533+
534+
return $children;
535+
}
536+
537+
/**
538+
* Returns an item from cache or computes it using $closure and puts it in cache.
539+
*
540+
* @param string $key
541+
* @param callable $closure
542+
*
543+
* @return mixed
544+
*/
545+
private function fromCache($key, callable $closure)
546+
{
547+
$item = $this->cache->fetch($key);
548+
if ($item === false) {
549+
$item = $closure();
550+
$this->cache->save($key, $item);
551+
}
552+
553+
return $item;
554+
}
424555
}

0 commit comments

Comments
 (0)