Skip to content

Commit daded02

Browse files
committed
Adding ability to detect inheritance relationships between tables
1 parent a54928b commit daded02

File tree

3 files changed

+179
-1
lines changed

3 files changed

+179
-1
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: 107 additions & 1 deletion
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;
@@ -124,7 +125,11 @@ private function isJunctionTable(Table $table)
124125
return false;
125126
}
126127

127-
$pkColumns = $table->getPrimaryKeyColumns();
128+
if ($table->hasPrimaryKey()) {
129+
$pkColumns = $table->getPrimaryKeyColumns();
130+
} else {
131+
$pkColumns = [];
132+
}
128133

129134
if (count($pkColumns) == 1 && count($columns) == 2) {
130135
return false;
@@ -257,6 +262,8 @@ private function buildSchemaGraph()
257262
$edge = $graph->getVertex($table->getName())->createEdge($graph->getVertex($fk->getForeignTableName()));
258263
if (isset($this->alteredCosts[$fk->getLocalTable()->getName()][implode(',',$fk->getLocalColumns())])) {
259264
$cost = $this->alteredCosts[$fk->getLocalTable()->getName()][implode(',',$fk->getLocalColumns())];
265+
} elseif ($this->isInheritanceRelationship($fk)) {
266+
$cost = self::$WEIGHT_INHERITANCE_FK;
260267
} else {
261268
$cost = self::$WEIGHT_FK;
262269
}
@@ -421,4 +428,103 @@ public function setTableCostModifiers(array $tableCosts) {
421428
public function setForeignKeyCosts(array $fkCosts) {
422429
$this->alteredCosts = $fkCosts;
423430
}
431+
432+
/**
433+
* Returns true if this foreign key represents an inheritance relationship,
434+
* i.e. if this foreign key is based on a primary key.
435+
* @param ForeignKeyConstraint $fk
436+
* @return true
437+
*/
438+
private function isInheritanceRelationship(ForeignKeyConstraint $fk) {
439+
if (!$fk->getLocalTable()->hasPrimaryKey()) {
440+
return false;
441+
}
442+
$fkColumnNames = $fk->getLocalColumns();
443+
$pkColumnNames = $fk->getLocalTable()->getPrimaryKeyColumns();
444+
445+
sort($fkColumnNames);
446+
sort($pkColumnNames);
447+
448+
return $fkColumnNames == $pkColumnNames;
449+
}
450+
451+
/**
452+
* If this table is pointing to a parent table (if its primary key is a foreign key pointing on another table),
453+
* this function will return the pointed table.
454+
* This function will return null if there is no parent table.
455+
*
456+
* @param string $tableName
457+
* @return string|null
458+
*/
459+
public function getParentTable($tableName) {
460+
$cacheKey = $this->cachePrefix.'_parent_'.$tableName;
461+
$parent = $this->cache->fetch($cacheKey);
462+
if ($parent === false) {
463+
$parent = $this->getParentTableWithoutCache($tableName);
464+
$this->cache->save($cacheKey, $parent);
465+
}
466+
467+
return $parent;
468+
}
469+
470+
/**
471+
* If this table is pointing to a parent table (if its primary key is a foreign key pointing on another table),
472+
* this function will return the pointed table.
473+
* This function will return null if there is no parent table.
474+
*
475+
* @param string $tableName
476+
* @return string|null
477+
*/
478+
private function getParentTableWithoutCache($tableName) {
479+
$table = $this->getSchema()->getTable($tableName);
480+
foreach ($table->getForeignKeys() as $fk) {
481+
if ($this->isInheritanceRelationship($fk)) {
482+
return $fk->getForeignTableName();
483+
}
484+
}
485+
return null;
486+
}
487+
488+
/**
489+
* If this table is pointed by children tables (if other child tables have a primary key that is also a
490+
* foreign key to this table), this function will return the list of child tables.
491+
* This function will return an empty array if there are no children tables.
492+
*
493+
* @param string $tableName
494+
* @return string[]
495+
*/
496+
public function getChildrenTables($tableName) {
497+
$cacheKey = $this->cachePrefix.'_children_'.$tableName;
498+
$parent = $this->cache->fetch($cacheKey);
499+
if ($parent === false) {
500+
$parent = $this->getChildrenTablesWithoutCache($tableName);
501+
$this->cache->save($cacheKey, $parent);
502+
}
503+
504+
return $parent;
505+
}
506+
507+
/**
508+
* If this table is pointed by children tables (if other child tables have a primary key that is also a
509+
* foreign key to this table), this function will return the list of child tables.
510+
* This function will return an empty array if there are no children tables.
511+
*
512+
* @param string $tableName
513+
* @return string[]
514+
*/
515+
private function getChildrenTablesWithoutCache($tableName) {
516+
$schema = $this->getSchema();
517+
$children = [];
518+
foreach ($schema->getTables() as $table) {
519+
if ($table->getName() === $tableName) {
520+
continue;
521+
}
522+
foreach ($table->getForeignKeys() as $fk) {
523+
if ($fk->getForeignTableName() === $tableName && $this->isInheritanceRelationship($fk)) {
524+
$children[] = $fk->getLocalTableName();
525+
}
526+
}
527+
}
528+
return $children;
529+
}
424530
}

tests/SchemaAnalyzerTest.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,24 @@ public function testJointureTableDetectionWith2ColumnsWith2FkOnOneCol() {
161161
$this->assertCount(0, $junctionTables);
162162
}
163163

164+
public function testJointureTableDetectionWith2ColumnsAndNoPrimaryKey() {
165+
$schema = $this->getBaseSchema();
166+
$schema->getTable('role')->addColumn('right_id', 'integer');
167+
168+
$role_right = $schema->createTable("role_right");
169+
$role_right->addColumn("role_id", "integer", array("unsigned" => true));
170+
$role_right->addColumn("right_id", "integer", array("unsigned" => true));
171+
172+
$role_right->addForeignKeyConstraint($schema->getTable('role'), array("role_id"), array("id"), array("onUpdate" => "CASCADE"));
173+
$role_right->addForeignKeyConstraint($schema->getTable('right'), array("right_id"), array("id"), array("onUpdate" => "CASCADE"));
174+
175+
$schemaAnalyzer = new SchemaAnalyzer(new StubSchemaManager($schema));
176+
$junctionTables = $schemaAnalyzer->detectJunctionTables();
177+
178+
$this->assertCount(1, $junctionTables);
179+
}
180+
181+
164182
public function testJointureTableDetectionWith3ColumnsWithPkIsFk() {
165183
$schema = $this->getBaseSchema();
166184

@@ -411,5 +429,35 @@ public function testAmbiguityExceptionWithJointureAndModifiedWeight2() {
411429
$this->assertEquals("right", $fks[1]->getForeignTableName());
412430
}
413431

432+
public function testInheritanceRelationship() {
433+
$schema = new Schema();
434+
$contact = $schema->createTable("contact");
435+
$contact->addColumn("id", "integer", array("unsigned" => true));
436+
$contact->addColumn("name", "string", array("length" => 32));
437+
$contact->setPrimaryKey(["id"]);
438+
439+
$user = $schema->createTable("user");
440+
$user->addColumn("id", "integer", array("unsigned" => true));
441+
$user->addColumn("contact_id", "integer", array("unsigned" => true));
442+
$user->addColumn("login", "string", array("length" => 32));
443+
$user->setPrimaryKey(["id"]);
444+
445+
$user->addForeignKeyConstraint($contact, array("id"), array("id"), array("onUpdate" => "CASCADE"));
446+
$user->addForeignKeyConstraint($contact, array("contact_id"), array("id"), array("onUpdate" => "CASCADE"));
447+
448+
$schemaAnalyzer = new SchemaAnalyzer(new StubSchemaManager($schema));
449+
450+
// No ambiguity exception should be thrown because we go through inheritance relationship first.
451+
$fks = $schemaAnalyzer->getShortestPath("user", "contact");
452+
453+
$this->assertCount(1, $fks);
454+
$this->assertEquals("id", $fks[0]->getLocalColumns()[0]);
455+
456+
$this->assertEquals('contact', $schemaAnalyzer->getParentTable('user'));
457+
$this->assertNull($schemaAnalyzer->getParentTable('contact'));
458+
459+
$this->assertEquals(['user'], $schemaAnalyzer->getChildrenTables('contact'));
460+
$this->assertEquals([], $schemaAnalyzer->getChildrenTables('user'));
461+
}
414462
}
415463

0 commit comments

Comments
 (0)