Skip to content

Commit

Permalink
Add Migrator::introspectTableToModel() method (#1218)
Browse files Browse the repository at this point in the history
  • Loading branch information
mvorisek authored May 21, 2024
1 parent 3bc5dce commit 5257612
Show file tree
Hide file tree
Showing 11 changed files with 200 additions and 115 deletions.
2 changes: 1 addition & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ parameters:
path: '*'
identifier: class.nameCase
message: '~^Class Doctrine\\DBAL\\(Platforms\\SqlitePlatform|Schema\\SqliteSchemaManager) referenced with incorrect case: Doctrine\\DBAL\\(Platforms\\SQLitePlatform|Schema\\SQLiteSchemaManager)\.$~'
count: 21
count: 23

# TODO these rules are generated, this ignores should be fixed in the code
# for src/Schema/TestCase.php
Expand Down
6 changes: 5 additions & 1 deletion src/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,11 @@ protected function init(): void
$this->_init();

if ($this->idField) {
$this->addField($this->idField, ['type' => 'integer', 'required' => true, 'system' => true]);
if (!$this->hasField($this->idField)) {
$this->addField($this->idField, ['type' => 'integer']);
}
$this->getIdField()->required = true;
$this->getIdField()->system = true;

$this->initEntityIdHooks();

Expand Down
8 changes: 4 additions & 4 deletions src/Persistence/Sql/Join.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ protected function init(): void
{
parent::init();

// TODO thus mutates the owner model!
// TODO this mutates the owner model!
$this->getOwner()->persistenceData['use_table_prefixes'] = true;

// our short name will be unique
Expand All @@ -30,15 +30,15 @@ protected function init(): void
$this->foreignAlias = ($this->getOwner()->tableAlias ?? '') . '_' . (str_starts_with($this->shortName, '#join-') ? substr($this->shortName, 6) : $this->shortName);
}

// TODO thus mutates the owner model/joins!
// TODO this mutates the owner model/joins!
if (!$this->reverse && !$this->getOwner()->hasField($this->masterField)) {
$owner = $this->hasJoin() ? $this->getJoin() : $this->getOwner();
$field = $owner->addField($this->masterField, ['type' => 'integer', 'system' => true, 'readOnly' => true]);
$this->masterField = $field->shortName; // TODO thus mutates the join!
$this->masterField = $field->shortName; // TODO this mutates the join!
} elseif ($this->reverse && !$this->getOwner()->hasField($this->foreignField) && $this->hasJoin()) {
$owner = $this->getJoin();
$field = $owner->addField($this->foreignField, ['type' => 'integer', 'system' => true, 'readOnly' => true, 'actual' => $this->masterField]);
$this->foreignField = $field->shortName; // TODO thus mutates the join!
$this->foreignField = $field->shortName; // TODO this mutates the join!
}
}

Expand Down
24 changes: 24 additions & 0 deletions src/Schema/Migrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use Doctrine\DBAL\Schema\Identifier;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Types\Type;

class Migrator
{
Expand Down Expand Up @@ -404,6 +405,29 @@ protected function fixTableNameForListMethod(string $tableName): string
return $platform->quoteSingleIdentifier($tableName);
}

public function introspectTableToModel(string $tableName): Model
{
$columns = $this->createSchemaManager()->listTableColumns($this->fixTableNameForListMethod($tableName));

$indexes = $this->createSchemaManager()->listTableIndexes($this->fixTableNameForListMethod($tableName));
$primaryIndexes = array_filter($indexes, static fn ($v) => $v->isPrimary() && count($v->getColumns()) === 1);
if (count($primaryIndexes) !== 1) {
throw (new Exception('Table must contain exactly one primary key'))
->addMoreInfo('table', $tableName);
}
$idFieldName = reset($primaryIndexes)->getUnquotedColumns()[0];

$model = new Model(null, ['table' => $tableName, 'idField' => $idFieldName]);
foreach ($columns as $column) {
$model->addField($column->getName(), [
'type' => Type::getTypeRegistry()->lookupName($column->getType()), // TODO simplify once https://github.com/doctrine/dbal/pull/6130 is merged
'nullable' => !$column->getNotnull(),
]);
}

return $model;
}

/**
* @param list<Field> $fields
*/
Expand Down
100 changes: 38 additions & 62 deletions src/Schema/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -308,76 +308,69 @@ public function createMigrator(?Model $model = null): Migrator
}

/**
* @param array<string, array<int|'_', array<string, mixed>>> $dbData
* @param array<string, array<int|'_types', array<string, mixed>>> $dbData
*/
public function setDb(array $dbData, bool $importData = true): void
{
foreach ($dbData as $tableName => $data) {
$migrator = $this->createMigrator()->table($tableName);
$idField = $data['_types']['_idField'] ?? 'id';
unset($data['_types']['_idField']);

$fieldTypes = [];
$fieldTypes = $data['_types'] ?? [];
unset($data['_types']);
foreach ($data as $row) {
foreach ($row as $k => $v) {
if (isset($fieldTypes[$k])) {
continue;
}

if (is_bool($v)) {
$fieldType = 'boolean';
$type = 'boolean';
} elseif (is_int($v)) {
$fieldType = 'integer';
$type = 'integer';
} elseif (is_float($v)) {
$fieldType = 'float';
$type = 'float';
} elseif ($v instanceof \DateTimeInterface) {
$fieldType = 'datetime';
$type = 'datetime';
} elseif ($v !== null) {
$fieldType = 'string';
$type = 'string';
} else {
$fieldType = null;
$type = null;
}

$fieldTypes[$k] = $fieldType;
$fieldTypes[$k] = $type;
}
}
foreach ($fieldTypes as $k => $fieldType) {
if ($fieldType === null) {
foreach ($fieldTypes as $k => $type) {
if ($type === null) {
$fieldTypes[$k] = 'string';
}
}
$idColumnName = isset($fieldTypes['_id']) ? '_id' : 'id';

// create table
$migrator->id($idColumnName, isset($fieldTypes[$idColumnName]) ? ['type' => $fieldTypes[$idColumnName]] : []);
foreach ($fieldTypes as $k => $fieldType) {
if ($k === $idColumnName) {
continue;
}
if (!isset($fieldTypes[$idField])) {
$fieldTypes = array_merge([$idField => 'integer'], $fieldTypes);
}

$migrator->field($k, ['type' => $fieldType]);
$model = new Model(null, ['table' => $tableName, 'idField' => $idField]);
foreach ($fieldTypes as $k => $type) {
$model->addField($k, ['type' => $type]);
}
$model->setPersistence($this->db);

// create table
$migrator = $this->createMigrator($model);
$migrator->create();

// import data
if ($importData) {
$this->db->atomic(function () use ($tableName, $data, $idColumnName) {
$hasId = array_key_first($data) !== 0;

if (array_key_first($data) !== 0) {
foreach ($data as $id => $row) {
$query = $this->db->dsql();
if ($id === '_') {
continue;
}

$query->table($tableName);
$query->setMulti($row);

if (!isset($row[$idColumnName]) && $hasId) {
$query->set($idColumnName, $id);
if (!isset($row[$idField])) {
$data[$id][$idField] = $id;
}

$query->mode('insert')->executeStatement();
}
});
}

$model->import($data);
}
}
}
Expand All @@ -401,35 +394,18 @@ public function getDb(?array $tableNames = null, bool $noId = false): array

$resAll = [];
foreach ($tableNames as $table) {
$query = $this->db->dsql();
$rows = $query->table($table)->getRows();

$res = [];
$idColumnName = null;
foreach ($rows as $row) {
if ($idColumnName === null) {
$idColumnName = isset($row['_id']) ? '_id' : 'id';
}

foreach ($row as $k => $v) {
if (preg_match('~(?:^|_)id$~', $k) && $v === (string) (int) $v) {
$row[$k] = (int) $v;
}
}

if ($noId) {
unset($row[$idColumnName]);
$res[] = $row;
} else {
$res[$row[$idColumnName]] = $row;
}
}
$model = $this->createMigrator()->introspectTableToModel($table);
$model->setPersistence($this->db);

if (!$noId) {
ksort($res);
$model->setOrder($model->idField);
}

$resAll[$table] = $res;
$data = $noId
? $model->export(array_diff(array_keys($model->getFields()), [$model->idField]))
: $model->export(null, $model->idField);

$resAll[$table] = $data;
}

return $resAll;
Expand Down
20 changes: 10 additions & 10 deletions tests/JoinSqlTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,10 @@ public function testJoinSaving1(): void
$user = new Model($this->db, ['table' => 'user']);
$this->setDb([
'user' => [
'_' => ['id' => 1, 'name' => 'John', 'contact_id' => 1],
'_types' => ['name' => 'string', 'contact_id' => 'integer'],
],
'contact' => [
'_' => ['id' => 1, 'contact_phone' => '+123'],
'_types' => ['contact_phone' => 'string'],
],
]);

Expand Down Expand Up @@ -161,10 +161,10 @@ public function testJoinSaving2(): void
{
$this->setDb([
'user' => [
'_' => ['id' => 1, 'name' => 'John'],
'_types' => ['name' => 'string'],
],
'contact' => [
'_' => ['id' => 1, 'contact_phone' => '+123', 'test_id' => 0],
'_types' => ['contact_phone' => 'string', 'test_id' => 'integer'],
],
]);

Expand Down Expand Up @@ -234,10 +234,10 @@ public function testJoinSaving3(): void
$user = new Model($this->db, ['table' => 'user']);
$this->setDb([
'user' => [
'_' => ['id' => 1, 'name' => 'John', 'test_id' => 0],
'_types' => ['name' => 'string', 'test_id' => 'integer'],
],
'contact' => [
'_' => ['id' => 1, 'contact_phone' => '+123'],
'_types' => ['contact_phone' => 'string'],
],
]);

Expand Down Expand Up @@ -464,10 +464,10 @@ public function testDoubleSaveHook(): void
{
$this->setDb([
'user' => [
'_' => ['id' => 1, 'name' => 'John'],
'_types' => ['name' => 'string'],
],
'contact' => [
'_' => ['id' => 1, 'contact_phone' => '+123', 'test_id' => 0],
'_types' => ['contact_phone' => 'string', 'test_id' => 'integer'],
],
]);

Expand Down Expand Up @@ -828,7 +828,7 @@ public function testJoinActualFieldNamesAndPrefix(): void
$j2->allowDangerousForeignTableUpdate = true;
$user2->save();

self::{'assertEquals'}([
self::assertSame([
'user' => [
1 => ['id' => 1, 'first_name' => 'John 2', $contactForeignIdFieldName => 1],
['id' => 2, 'first_name' => 'Peter', $contactForeignIdFieldName => 1],
Expand All @@ -851,7 +851,7 @@ public function testJoinActualFieldNamesAndPrefix(): void
$user3->set('j2_salary', 222);
$user3->save();

self::{'assertEquals'}([
self::assertSame([
'user' => [
1 => ['id' => 1, 'first_name' => 'John 2', $contactForeignIdFieldName => 1],
['id' => 2, 'first_name' => 'Peter', $contactForeignIdFieldName => 1],
Expand Down
Loading

0 comments on commit 5257612

Please sign in to comment.