From 564944cafde0564b96c670efa3b64aa9cb26f904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 2 Jan 2022 22:05:02 +0100 Subject: [PATCH 01/17] fix/use non-aliased field name for update/delete where --- src/Persistence/Sql.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Persistence/Sql.php b/src/Persistence/Sql.php index 202af53db..577a637a8 100644 --- a/src/Persistence/Sql.php +++ b/src/Persistence/Sql.php @@ -589,7 +589,7 @@ public function update(Model $model, $id, array $data): void // only apply fields that has been modified $update->setMulti($this->typecastSaveRow($model, $data)); - $update->where($model->getField($model->id_field), $id); + $update->where($model->getField($model->id_field)->getPersistenceName(), $id); $st = null; try { @@ -635,7 +635,7 @@ public function delete(Model $model, $id): void $delete = $this->initQuery($model); $delete->mode('delete'); - $delete->where($model->getField($model->id_field), $id); + $delete->where($model->getField($model->id_field)->getPersistenceName(), $id); $model->hook(self::HOOK_BEFORE_DELETE_QUERY, [$delete]); try { From 735332297f2d752cea1e9dbfb090e1712f5d2356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 4 Jan 2022 05:04:42 +0100 Subject: [PATCH 02/17] fix id typecasting in SQL persistence --- src/Persistence/Sql.php | 68 +++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 23 deletions(-) diff --git a/src/Persistence/Sql.php b/src/Persistence/Sql.php index 577a637a8..5e632bd1c 100644 --- a/src/Persistence/Sql.php +++ b/src/Persistence/Sql.php @@ -136,10 +136,7 @@ public function add(Model $model, array $defaults = []): void // When we work without table, we can't have any IDs if ($model->table === false) { $model->removeField($model->id_field); - $model->addExpression($model->id_field, '1'); - //} else { - // SQL databases use ID of int by default - //$m->getField($m->id_field)->type = 'integer'; + $model->addExpression($model->id_field, '-1'); } } @@ -514,14 +511,22 @@ public function tryLoad(Model $model, $id): ?array */ public function insert(Model $model, array $data): string { + if ($model->id_field) { + $dataId = $data[$model->id_field] ?? null; + if ($dataId === null) { + unset($data[$model->id_field]); + } + } else { + $dataId = null; + } + + $dataRaw = $this->typecastSaveRow($model, $data); + unset($data); + $insert = $this->initQuery($model); $insert->mode('insert'); - if ($model->id_field && ($data[$model->id_field] ?? null) === null) { - unset($data[$model->id_field]); - } - - $insert->setMulti($this->typecastSaveRow($model, $data)); + $insert->setMulti($dataRaw); $st = null; try { @@ -533,10 +538,14 @@ public function insert(Model $model, array $data): string ->addMoreInfo('scope', $model->getModel(true)->scope()->toWords()); } - if ($model->id_field && ($data[$model->id_field] ?? null) !== null) { - $id = (string) $data[$model->id_field]; + if ($model->id_field) { + if ($dataId !== null) { + $id = (string) $dataId; + } else { + $id = $this->lastInsertId($model); + } } else { - $id = $this->lastInsertId($model); + $id = ''; } $model->hook(self::HOOK_AFTER_INSERT_QUERY, [$insert, $st]); @@ -580,38 +589,48 @@ public function prepareIterator(Model $model): \Traversable */ public function update(Model $model, $id, array $data): void { - if (!$model->id_field) { + $idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null; + unset($id); + + $dataId = $data[$model->id_field] ?? null; + + if (!$model->id_field || $idRaw === null || (array_key_exists($model->id_field, $data) && $dataId === null)) { throw new Exception('id_field of a model is not set. Unable to update record.'); } + $dataRaw = $this->typecastSaveRow($model, $data); + unset($data); + + if (count($dataRaw) === 0) { + return; + } + $update = $this->initQuery($model); $update->mode('update'); // only apply fields that has been modified - $update->setMulti($this->typecastSaveRow($model, $data)); - $update->where($model->getField($model->id_field)->getPersistenceName(), $id); + $update->setMulti($dataRaw); + $update->where($model->getField($model->id_field)->getPersistenceName(), $idRaw); $st = null; try { $model->hook(self::HOOK_BEFORE_UPDATE_QUERY, [$update]); - if ($data) { - $st = $update->execute(); - } + $st = $update->execute(); } catch (SqlException $e) { throw (new Exception('Unable to update due to query error', 0, $e)) ->addMoreInfo('model', $model) ->addMoreInfo('scope', $model->getModel(true)->scope()->toWords()); } - if (isset($data[$model->id_field]) && $model->getDirtyRef()[$model->id_field]) { + if ($dataId !== null && $model->getDirtyRef()[$model->id_field]) { // ID was changed - $model->setId($data[$model->id_field]); + $model->setId($dataId); } $model->hook(self::HOOK_AFTER_UPDATE_QUERY, [$update, $st]); // if any rows were updated in database, and we had expressions, reload - if ($model->reload_after_save === true && (!$st || $st->rowCount())) { + if ($model->reload_after_save === true && $st->rowCount()) { $d = $model->getDirtyRef(); $model->reload(); \Closure::bind(function () use ($model) { @@ -629,13 +648,16 @@ public function update(Model $model, $id, array $data): void */ public function delete(Model $model, $id): void { - if (!$model->id_field) { + $idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null; + unset($id); + + if (!$model->id_field || $idRaw === null) { throw new Exception('id_field of a model is not set. Unable to delete record.'); } $delete = $this->initQuery($model); $delete->mode('delete'); - $delete->where($model->getField($model->id_field)->getPersistenceName(), $id); + $delete->where($model->getField($model->id_field)->getPersistenceName(), $idRaw); $model->hook(self::HOOK_BEFORE_DELETE_QUERY, [$delete]); try { From e0af1c5da4c074387097e1d76a8eda954887dd4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 4 Jan 2022 18:44:33 +0100 Subject: [PATCH 03/17] dedup/impl insert/update/delete methods in main Persistence --- phpstan.neon.dist | 3 - src/Persistence.php | 83 ++++++++++++++++++++++ src/Persistence/Array_/Join.php | 4 +- src/Persistence/Sql.php | 122 +++++++++++--------------------- 4 files changed, 126 insertions(+), 86 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index d5ac358ab..3adbf85c1 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -45,11 +45,8 @@ parameters: # for src/Field/SqlExpressionField.php - '~^Call to an undefined method Atk4\\Data\\Model::expr\(\)\.$~' # for src/Model.php - - '~^Call to an undefined method Atk4\\Data\\Persistence::update\(\)\.$~' - - '~^Call to an undefined method Atk4\\Data\\Persistence::insert\(\)\.$~' - '~^Call to an undefined method Atk4\\Data\\Persistence::export\(\)\.$~' - '~^Call to an undefined method Atk4\\Data\\Persistence::prepareIterator\(\)\.$~' - - '~^Call to an undefined method Atk4\\Data\\Persistence::delete\(\)\.$~' - '~^Call to an undefined method Atk4\\Data\\Persistence::action\(\)\.$~' # for src/Model/ReferencesTrait.php (in context of class Atk4\Data\Model) - '~^Call to an undefined method Atk4\\Data\\Reference::refLink\(\)\.$~' diff --git a/src/Persistence.php b/src/Persistence.php index 63768e5d9..518ed5cf1 100644 --- a/src/Persistence.php +++ b/src/Persistence.php @@ -147,6 +147,89 @@ public function load(Model $model, $id): array return $data; } + /** + * Inserts record in database and returns new record ID. + * + * @return mixed + */ + public function insert(Model $model, array $data) + { + if ($model->id_field && array_key_exists($model->id_field, $data) && $data[$model->id_field] === null) { + unset($data[$model->id_field]); + } + + $dataRaw = $this->typecastSaveRow($model, $data); + unset($data); + + $idRaw = $this->insertRaw($model, $dataRaw); + $id = $model->id_field ? $this->typecastLoadField($model->getField($model->id_field), $idRaw) : new \stdClass(); + + return $id; + } + + /** + * @return mixed + */ + protected function insertRaw(Model $model, array $dataRaw) + { + throw new Exception('Insert is not supported.'); + } + + /** + * Updates record in database. + * + * @param mixed $id + */ + public function update(Model $model, $id, array $data): void + { + $idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null; + unset($id); + if ($idRaw === null || (array_key_exists($model->id_field, $data) && $data[$model->id_field] === null)) { + throw new Exception('Model id_field is not set. Unable to update record.'); + } + + $dataRaw = $this->typecastSaveRow($model, $data); + unset($data); + + if (count($dataRaw) === 0) { + return; + } + + $this->updateRaw($model, $idRaw, $dataRaw); + } + + /** + * @param mixed $idRaw + */ + protected function updateRaw(Model $model, $idRaw, array $dataRaw): void + { + throw new Exception('Update is not supported.'); + } + + /** + * Deletes record from database. + * + * @param mixed $id + */ + public function delete(Model $model, $id): void + { + $idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null; + unset($id); + if ($idRaw === null) { + throw new Exception('Model id_field is not set. Unable to delete record.'); + } + + $this->deleteRaw($model, $idRaw); + } + + /** + * @param mixed $idRaw + */ + protected function deleteRaw(Model $model, $idRaw): void + { + throw new Exception('Delete is not supported.'); + } + /** * Will convert one row of data from native PHP types into * persistence types. This will also take care of the "actual" diff --git a/src/Persistence/Array_/Join.php b/src/Persistence/Array_/Join.php index 9aab16a2b..4d2fcd3db 100644 --- a/src/Persistence/Array_/Join.php +++ b/src/Persistence/Array_/Join.php @@ -114,11 +114,11 @@ public function beforeUpdate(Model $entity, array &$data): void $persistence = $this->persistence ?: $this->getOwner()->persistence; + // @phpstan-ignore-next-line TODO this cannot work, Persistence::update() returns void $this->setId($entity, $persistence->update( $this->makeFakeModelWithForeignTable(), $this->getId($entity), - $this->getAndUnsetSaveBuffer($entity), - $this->foreign_table + $this->getAndUnsetSaveBuffer($entity) )); } diff --git a/src/Persistence/Sql.php b/src/Persistence/Sql.php index 5e632bd1c..1f0a4c9bd 100644 --- a/src/Persistence/Sql.php +++ b/src/Persistence/Sql.php @@ -37,6 +37,8 @@ class Sql extends Persistence public const HOOK_AFTER_UPDATE_QUERY = self::class . '@afterUpdateQuery'; /** @const string */ public const HOOK_BEFORE_DELETE_QUERY = self::class . '@beforeDeleteQuery'; + /** @const string */ + public const HOOK_AFTER_DELETE_QUERY = self::class . '@afterDeleteQuery'; /** @var Connection Connection object. */ public $connection; @@ -506,53 +508,6 @@ public function tryLoad(Model $model, $id): ?array return $data; } - /** - * Inserts record in database and returns new record ID. - */ - public function insert(Model $model, array $data): string - { - if ($model->id_field) { - $dataId = $data[$model->id_field] ?? null; - if ($dataId === null) { - unset($data[$model->id_field]); - } - } else { - $dataId = null; - } - - $dataRaw = $this->typecastSaveRow($model, $data); - unset($data); - - $insert = $this->initQuery($model); - $insert->mode('insert'); - - $insert->setMulti($dataRaw); - - $st = null; - try { - $model->hook(self::HOOK_BEFORE_INSERT_QUERY, [$insert]); - $st = $insert->execute(); - } catch (SqlException $e) { - throw (new Exception('Unable to execute insert query', 0, $e)) - ->addMoreInfo('model', $model) - ->addMoreInfo('scope', $model->getModel(true)->scope()->toWords()); - } - - if ($model->id_field) { - if ($dataId !== null) { - $id = (string) $dataId; - } else { - $id = $this->lastInsertId($model); - } - } else { - $id = ''; - } - - $model->hook(self::HOOK_AFTER_INSERT_QUERY, [$insert, $st]); - - return $id; - } - /** * Export all DataSet. */ @@ -582,29 +537,39 @@ public function prepareIterator(Model $model): \Traversable } } - /** - * Updates record in database. - * - * @param mixed $id - */ - public function update(Model $model, $id, array $data): void + protected function insertRaw(Model $model, array $dataRaw) { - $idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null; - unset($id); + $insert = $this->initQuery($model); + $insert->mode('insert'); - $dataId = $data[$model->id_field] ?? null; + $insert->setMulti($dataRaw); - if (!$model->id_field || $idRaw === null || (array_key_exists($model->id_field, $data) && $dataId === null)) { - throw new Exception('id_field of a model is not set. Unable to update record.'); + $st = null; + try { + $model->hook(self::HOOK_BEFORE_INSERT_QUERY, [$insert]); + $st = $insert->execute(); + } catch (SqlException $e) { + throw (new Exception('Unable to execute insert query', 0, $e)) + ->addMoreInfo('model', $model) + ->addMoreInfo('scope', $model->getModel(true)->scope()->toWords()); } - $dataRaw = $this->typecastSaveRow($model, $data); - unset($data); - - if (count($dataRaw) === 0) { - return; + if ($model->id_field) { + $idRaw = $dataRaw[$model->getField($model->id_field)->getPersistenceName()] ?? null; + if ($idRaw === null) { + $idRaw = $this->lastInsertId($model); + } + } else { + $idRaw = ''; } + $model->hook(self::HOOK_AFTER_INSERT_QUERY, [$insert, $st]); + + return $idRaw; + } + + protected function updateRaw(Model $model, $idRaw, array $dataRaw): void + { $update = $this->initQuery($model); $update->mode('update'); @@ -612,9 +577,10 @@ public function update(Model $model, $id, array $data): void $update->setMulti($dataRaw); $update->where($model->getField($model->id_field)->getPersistenceName(), $idRaw); + $model->hook(self::HOOK_BEFORE_UPDATE_QUERY, [$update]); + $st = null; try { - $model->hook(self::HOOK_BEFORE_UPDATE_QUERY, [$update]); $st = $update->execute(); } catch (SqlException $e) { throw (new Exception('Unable to update due to query error', 0, $e)) @@ -622,9 +588,13 @@ public function update(Model $model, $id, array $data): void ->addMoreInfo('scope', $model->getModel(true)->scope()->toWords()); } - if ($dataId !== null && $model->getDirtyRef()[$model->id_field]) { - // ID was changed - $model->setId($dataId); + if ($model->id_field) { + $newIdRaw = $dataRaw[$model->getField($model->id_field)->getPersistenceName()] ?? null; + if ($newIdRaw !== null && $model->getDirtyRef()[$model->id_field]) { + // ID was changed + // TODO this cannot work with entity + $model->setId($this->typecastLoadField($model->getField($model->id_field), $newIdRaw)); + } } $model->hook(self::HOOK_AFTER_UPDATE_QUERY, [$update, $st]); @@ -641,32 +611,22 @@ public function update(Model $model, $id, array $data): void } } - /** - * Deletes record from database. - * - * @param mixed $id - */ - public function delete(Model $model, $id): void + protected function deleteRaw(Model $model, $idRaw): void { - $idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null; - unset($id); - - if (!$model->id_field || $idRaw === null) { - throw new Exception('id_field of a model is not set. Unable to delete record.'); - } - $delete = $this->initQuery($model); $delete->mode('delete'); $delete->where($model->getField($model->id_field)->getPersistenceName(), $idRaw); $model->hook(self::HOOK_BEFORE_DELETE_QUERY, [$delete]); try { - $delete->execute(); + $st = $delete->execute(); } catch (SqlException $e) { throw (new Exception('Unable to delete due to query error', 0, $e)) ->addMoreInfo('model', $model) ->addMoreInfo('scope', $model->getModel(true)->scope()->toWords()); } + + $model->hook(self::HOOK_AFTER_DELETE_QUERY, [$delete, $st]); } public function getFieldSqlExpression(Field $field, Expression $expression): Expression From af5333b0838430459c4db058d12bdd9fd56a051d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 4 Jan 2022 19:49:45 +0100 Subject: [PATCH 04/17] impl raw write methods --- src/Persistence/Array_.php | 38 ++++++++------------------------------ src/Persistence/Csv.php | 27 +++++---------------------- 2 files changed, 13 insertions(+), 52 deletions(-) diff --git a/src/Persistence/Array_.php b/src/Persistence/Array_.php index 62f897d47..cda1734a7 100644 --- a/src/Persistence/Array_.php +++ b/src/Persistence/Array_.php @@ -222,51 +222,29 @@ public function tryLoad(Model $model, $id): ?array return $this->typecastLoadRow($model, $this->filterRowDataOnlyModelFields($model, $row->getData())); } - /** - * Inserts record in data array and returns new record ID. - * - * @return mixed - */ - public function insert(Model $model, array $data) + protected function insertRaw(Model $model, array $dataRaw) { $this->seedData($model); - if ($model->id_field && ($data[$model->id_field] ?? null) === null) { - unset($data[$model->id_field]); - } - $data = $this->typecastSaveRow($model, $data); - - $id = $data[$model->id_field] ?? $this->generateNewId($model); + $idRaw = $dataRaw[$model->id_field] ?? $this->generateNewId($model); - $this->saveRow($model, $data, $id); + $this->saveRow($model, $dataRaw, $idRaw); - return $id; + return $idRaw; } - /** - * Updates record in data array and returns record ID. - * - * @param mixed $id - */ - public function update(Model $model, $id, array $data): void + protected function updateRaw(Model $model, $idRaw, array $dataRaw): void { $table = $this->seedDataAndGetTable($model); - $data = $this->typecastSaveRow($model, $data); - - $this->saveRow($model, array_merge($this->filterRowDataOnlyModelFields($model, $table->getRowById($model, $id)->getData()), $data), $id); + $this->saveRow($model, array_merge($this->filterRowDataOnlyModelFields($model, $table->getRowById($model, $idRaw)->getData()), $dataRaw), $idRaw); } - /** - * Deletes record in data array. - * - * @param mixed $id - */ - public function delete(Model $model, $id): void + protected function deleteRaw(Model $model, $idRaw): void { $table = $this->seedDataAndGetTable($model); - $table->deleteRow($table->getRowById($model, $id)); + $table->deleteRow($table->getRowById($model, $idRaw)); } /** diff --git a/src/Persistence/Csv.php b/src/Persistence/Csv.php index 095429ff2..54f560d51 100644 --- a/src/Persistence/Csv.php +++ b/src/Persistence/Csv.php @@ -261,12 +261,7 @@ public function prepareIterator(Model $model): \Traversable } } - /** - * Inserts record in data array and returns new record ID. - * - * @return mixed - */ - public function insert(Model $model, array $data) + protected function insertRaw(Model $model, array $dataRaw) { if (!$this->mode) { $this->mode = 'w'; @@ -274,8 +269,6 @@ public function insert(Model $model, array $data) throw new Exception('Currently reading records, so writing is not possible.'); } - $data = $this->typecastSaveRow($model, $data); - if (!$this->handle) { $this->saveHeader($model->getModel(true)); } @@ -283,30 +276,20 @@ public function insert(Model $model, array $data) $line = []; foreach ($this->header as $name) { - $line[] = $data[$name]; + $line[] = $dataRaw[$name]; } $this->putLine($line); - return $model->id_field ? $data[$model->id_field] : null; + return $model->id_field ? $dataRaw[$model->id_field] : null; } - /** - * Updates record in data array and returns record ID. - * - * @param mixed $id - */ - public function update(Model $model, $id, array $data): void + protected function updateRaw(Model $model, $idRaw, array $dataRaw): void { throw new Exception('Updating records is not supported in CSV persistence.'); } - /** - * Deletes record in data array. - * - * @param mixed $id - */ - public function delete(Model $model, $id): void + protected function deleteRaw(Model $model, $idRaw): void { throw new Exception('Deleting records is not supported in CSV persistence.'); } From 9dd61ba981eed83b594d1994b766a8bf200eb4aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 9 Jan 2022 17:44:11 +0100 Subject: [PATCH 05/17] add support for model nesting for SQL --- src/Model.php | 2 +- src/Persistence/Sql.php | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Model.php b/src/Model.php index f9e748f8f..cc069e953 100644 --- a/src/Model.php +++ b/src/Model.php @@ -120,7 +120,7 @@ class Model implements \IteratorAggregate * model normally lives. The interpretation of the table will be decoded * by persistence driver. * - * @var string|false + * @var string|self|false */ public $table; diff --git a/src/Persistence/Sql.php b/src/Persistence/Sql.php index 1f0a4c9bd..d98ce2508 100644 --- a/src/Persistence/Sql.php +++ b/src/Persistence/Sql.php @@ -206,7 +206,10 @@ public function initQuery(Model $model): Query $query = $model->persistence_data['dsql'] = $this->dsql(); if ($model->table) { - $query->table($model->table, $model->table_alias ?? null); + $query->table( + is_object($model->table) ? $model->table->action('select') : $model->table, + $model->table_alias ?? (is_object($model->table) ? '__inner__' : null) + ); } // add With cursors @@ -636,7 +639,7 @@ public function getFieldSqlExpression(Field $field, Expression $expression): Exp $prop = [ $field->hasJoin() ? ($field->getJoin()->foreign_alias ?: $field->getJoin()->short_name) - : ($field->getOwner()->table_alias ?: $field->getOwner()->table), + : ($field->getOwner()->table_alias ?: (is_object($field->getOwner()->table) ? '__inner__' : $field->getOwner()->table)), $field->getPersistenceName(), ]; } else { @@ -658,6 +661,10 @@ public function getFieldSqlExpression(Field $field, Expression $expression): Exp public function lastInsertId(Model $model): string { + if (is_object($model->table)) { + return $model->table->persistence->lastInsertId($model->table); + } + // PostgreSQL and Oracle DBAL platforms use sequence internally for PK autoincrement, // use default name if not set explicitly $sequenceName = null; From ddc2db3d05641a2b67d197bfc637d4c034ff03b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 9 Jan 2022 17:49:01 +0100 Subject: [PATCH 06/17] fix join/hasMany --- src/Model/Join.php | 16 +++++++++++++--- src/Reference/HasMany.php | 11 ++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/Model/Join.php b/src/Model/Join.php index 959d0847b..f5da760d3 100644 --- a/src/Model/Join.php +++ b/src/Model/Join.php @@ -182,6 +182,15 @@ static function (Model $entity) use ($name): self { ); } + private function getModelTableString(Model $model): string + { + if (is_object($model->table)) { + return $this->getModelTableString($model->table); + } + + return $model->table; + } + /** * Will use either foreign_alias or create #join_. */ @@ -204,7 +213,8 @@ protected function init(): void if ($this->reverse === true) { if ($this->master_field && $this->master_field !== $id_field) { // TODO not implemented yet, see https://github.com/atk4/data/issues/803 throw (new Exception('Joining tables on non-id fields is not implemented yet')) - ->addMoreInfo('condition', $this->getOwner()->table . '.' . $this->master_field . ' = ' . $this->foreign_table . '.' . $this->foreign_field); + ->addMoreInfo('master_field', $this->master_field) + ->addMoreInfo('id_field', $this->id_field); } if (!$this->master_field) { @@ -212,7 +222,7 @@ protected function init(): void } if (!$this->foreign_field) { - $this->foreign_field = $this->getOwner()->table . '_' . $id_field; + $this->foreign_field = $this->getModelTableString($this->getOwner()) . '_' . $id_field; } } else { $this->reverse = false; @@ -310,7 +320,7 @@ public function hasMany(string $link, array $defaults = []) { $defaults = array_merge([ 'our_field' => $this->id_field, - 'their_field' => $this->getOwner()->table . '_' . $this->id_field, + 'their_field' => $this->getModelTableString($this->getOwner()) . '_' . $this->id_field, ], $defaults); return $this->getOwner()->hasMany($link, $defaults); diff --git a/src/Reference/HasMany.php b/src/Reference/HasMany.php index 5a8c1d2c6..131709bab 100644 --- a/src/Reference/HasMany.php +++ b/src/Reference/HasMany.php @@ -11,6 +11,15 @@ class HasMany extends Reference { + private function getModelTableString(Model $model): string + { + if (is_object($model->table)) { + return $this->getModelTableString($model->table); + } + + return $model->table; + } + public function getTheirFieldName(): string { if ($this->their_field) { @@ -20,7 +29,7 @@ public function getTheirFieldName(): string // this is pure guess, verify if such field exist, otherwise throw // TODO probably remove completely in the future $ourModel = $this->getOurModel(null); - $theirFieldName = $ourModel->table . '_' . $ourModel->id_field; + $theirFieldName = $this->getModelTableString($ourModel) . '_' . $ourModel->id_field; if (!$this->createTheirModel()->hasField($theirFieldName)) { throw (new Exception('Their model does not contain fallback field')) ->addMoreInfo('their_fallback_field', $theirFieldName); From bcd74c5d7bb0d022e73644ad82490e22dba4d551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 4 Jan 2022 15:55:15 +0100 Subject: [PATCH 07/17] impl write queries using nested Models --- src/Persistence.php | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/Persistence.php b/src/Persistence.php index 518ed5cf1..1a32b1e5f 100644 --- a/src/Persistence.php +++ b/src/Persistence.php @@ -116,6 +116,18 @@ public function getDatabasePlatform(): Platforms\AbstractPlatform return new Persistence\GenericPlatform(); } + private function assertSameIdField(Model $model): void + { + $modelIdFieldName = $model->getField($model->id_field)->getPersistenceName(); + $tableIdFieldName = $model->table->id_field; + + if ($modelIdFieldName !== $tableIdFieldName) { + throw (new Exception('Table model with different ID field persistence name is not supported')) + ->addMoreInfo('model_id_field', $modelIdFieldName) + ->addMoreInfo('table_id_field', $tableIdFieldName); + } + } + /** * Tries to load data record, but will not fail if record can't be loaded. * @@ -161,6 +173,12 @@ public function insert(Model $model, array $data) $dataRaw = $this->typecastSaveRow($model, $data); unset($data); + if (is_object($model->table)) { + $this->assertSameIdField($model); + + return $model->table->insert($model->table->persistence->typecastLoadRow($model->table, $dataRaw)); + } + $idRaw = $this->insertRaw($model, $dataRaw); $id = $model->id_field ? $this->typecastLoadField($model->getField($model->id_field), $idRaw) : new \stdClass(); @@ -183,7 +201,6 @@ protected function insertRaw(Model $model, array $dataRaw) public function update(Model $model, $id, array $data): void { $idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null; - unset($id); if ($idRaw === null || (array_key_exists($model->id_field, $data) && $data[$model->id_field] === null)) { throw new Exception('Model id_field is not set. Unable to update record.'); } @@ -195,6 +212,16 @@ public function update(Model $model, $id, array $data): void return; } + if (is_object($model->table)) { + $this->assertSameIdField($model); + + $model->table->load($id)->save($model->table->persistence->typecastLoadRow($model->table, $dataRaw)); + + return; + } + + unset($id); + $this->updateRaw($model, $idRaw, $dataRaw); } @@ -214,11 +241,20 @@ protected function updateRaw(Model $model, $idRaw, array $dataRaw): void public function delete(Model $model, $id): void { $idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null; - unset($id); if ($idRaw === null) { throw new Exception('Model id_field is not set. Unable to delete record.'); } + if (is_object($model->table)) { + $this->assertSameIdField($model); + + $model->table->delete($id); + + return; + } + + unset($id); + $this->deleteRaw($model, $idRaw); } From a4438ea0a8ae3c9ac95313b3b0a2a98de25a304e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 9 Jan 2022 17:59:21 +0100 Subject: [PATCH 08/17] use "_tm" as default alias for model-in-model table --- src/Persistence/Sql.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Persistence/Sql.php b/src/Persistence/Sql.php index d98ce2508..d0d36ffdd 100644 --- a/src/Persistence/Sql.php +++ b/src/Persistence/Sql.php @@ -208,7 +208,7 @@ public function initQuery(Model $model): Query if ($model->table) { $query->table( is_object($model->table) ? $model->table->action('select') : $model->table, - $model->table_alias ?? (is_object($model->table) ? '__inner__' : null) + $model->table_alias ?? (is_object($model->table) ? '_tm' : null) ); } @@ -639,7 +639,7 @@ public function getFieldSqlExpression(Field $field, Expression $expression): Exp $prop = [ $field->hasJoin() ? ($field->getJoin()->foreign_alias ?: $field->getJoin()->short_name) - : ($field->getOwner()->table_alias ?: (is_object($field->getOwner()->table) ? '__inner__' : $field->getOwner()->table)), + : ($field->getOwner()->table_alias ?? (is_object($field->getOwner()->table) ? '_tm' : $field->getOwner()->table)), $field->getPersistenceName(), ]; } else { From 79b2743ea601068544d75aaaed0a5537a4ce91eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 9 Jan 2022 23:44:05 +0100 Subject: [PATCH 09/17] drop unneeded composer requirements --- composer.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/composer.json b/composer.json index 5d5f6c1f4..c12ef94f1 100644 --- a/composer.json +++ b/composer.json @@ -35,16 +35,12 @@ "homepage": "https://github.com/atk4/data", "require": { "php": ">=7.4 <8.2", - "ext-intl": "*", - "ext-pdo": "*", "atk4/core": "dev-develop", "doctrine/dbal": "^2.13.5 || ^3.2", "mvorisek/atk4-hintable": "~1.7.1" }, "require-release": { "php": ">=7.4 <8.2", - "ext-intl": "*", - "ext-pdo": "*", "atk4/core": "~3.2.0", "doctrine/dbal": "^2.13.5 || ^3.2", "mvorisek/atk4-hintable": "~1.7.1" From abfaabe6011ad54ae03d29b258eb3e7e86af5aed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 11 Jan 2022 08:59:52 +0100 Subject: [PATCH 10/17] do not skip WITH test for MariaDB --- tests/WithTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/WithTest.php b/tests/WithTest.php index bcc285ea1..14db20558 100644 --- a/tests/WithTest.php +++ b/tests/WithTest.php @@ -50,7 +50,7 @@ public function testWith(): void if ($this->getDatabasePlatform() instanceof MySQLPlatform) { $serverVersion = $this->db->connection->connection()->getWrappedConnection()->getServerVersion(); - if (str_starts_with($serverVersion, '5.')) { + if (preg_match('~^5\.(?!5\.5-.+?-MariaDB)~', $serverVersion)) { $this->markTestIncomplete('MySQL Server 5.x does not support WITH clause'); } } From 12c2c2a08681cf38fae5f6f3b901978f5a1f479f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 11 Jan 2022 13:09:32 +0100 Subject: [PATCH 11/17] add model-in-model tests incl. hooks --- src/Model.php | 10 +- tests/ModelNestedTest.php | 238 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+), 6 deletions(-) create mode 100644 tests/ModelNestedTest.php diff --git a/src/Model.php b/src/Model.php index cc069e953..78a473c50 100644 --- a/src/Model.php +++ b/src/Model.php @@ -1616,10 +1616,8 @@ public function save(array $data = []) * This is a temporary method to avoid code duplication, but insert / import should * be implemented differently. */ - protected function _rawInsert(array $row): void + protected function _insert(array $row): void { - $this->unload(); - // Find any row values that do not correspond to fields, and they may correspond to // references instead $refs = []; @@ -1668,10 +1666,10 @@ protected function _rawInsert(array $row): void */ public function insert(array $row) { - $model = $this->createEntity(); - $model->_rawInsert($row); + $entity = $this->createEntity(); + $entity->_insert($row); - return $this->id_field ? $model->getId() : null; + return $this->id_field ? $entity->getId() : null; } /** diff --git a/tests/ModelNestedTest.php b/tests/ModelNestedTest.php new file mode 100644 index 000000000..076647c53 --- /dev/null +++ b/tests/ModelNestedTest.php @@ -0,0 +1,238 @@ +setDb([ + 'user' => [ + ['id' => 1, 'name' => 'John', 'birthday_date' => '1980-2-1'], + ['id' => 2, 'name' => 'Sue', 'birthday_date' => '2005-4-3'], + ], + ]); + } + + /** @var array */ + public $hookLog = []; + + protected function createTestModel(): Model + { + $mWithLoggingClass = get_class(new class() extends Model { + /** @var \WeakReference */ + protected $testCaseWeakRef; + /** @var string */ + protected $testModelAlias; + + public function hook(string $spot, array $args = [], HookBreaker &$brokenBy = null) + { + if (!str_starts_with($spot, '__atk__method__') && $spot !== Model::HOOK_NORMALIZE) { + $convertValueToLogFx = function ($v) use (&$convertValueToLogFx) { + if (is_array($v)) { + return array_map($convertValueToLogFx, $v); + } elseif (is_scalar($v) || $v === null) { + return $v; + } elseif ($v instanceof self) { + return $this->testModelAlias; + } + + $res = preg_replace('~(?<=^Atk4\\\\Data\\\\Persistence\\\\Sql\\\\)\w+\\\\(?=\w+$)~', '', get_debug_type($v)); + if (Connection::isComposerDbal2x() && $res === 'Doctrine\DBAL\Statement') { + $res = DbalResult::class; + } + + return $res; + }; + + $this->testCaseWeakRef->get()->hookLog[] = [$convertValueToLogFx($this), $spot, $convertValueToLogFx($args)]; + } + + return parent::hook($spot, $args, $brokenBy); + } + }); + + $mInner = new $mWithLoggingClass($this->db, [ + 'testCaseWeakRef' => \WeakReference::create($this), + 'testModelAlias' => 'inner', + 'table' => 'user', + ]); + $mInner->addField('name'); + $mInner->addField('y', ['actual' => 'birthday_date', 'type' => 'date']); + + $m = new $mWithLoggingClass($this->db, [ + 'testCaseWeakRef' => \WeakReference::create($this), + 'testModelAlias' => 'main', + 'table' => $mInner, + ]); + $m->addField('name'); + $m->addField('birthday', ['actual' => 'y', 'type' => 'date']); + + return $m; + } + + public function testSelectSql(): void + { + $m = $this->createTestModel(); + $m->table->setOrder('name', 'desc'); + $m->table->setLimit(5); + $m->setOrder('birthday'); + + $this->assertSame( + ($this->db->connection->dsql()) + ->table( + ($this->db->connection->dsql()) + ->field('id') + ->field('name') + ->field('birthday_date', 'y') + ->table('user') + ->order('name', true) + ->limit(5), + '_tm' + ) + ->field('id') + ->field('name') + ->field('y', 'birthday') + ->order('y') + ->render()[0], + $m->action('select')->render()[0] + ); + + $this->assertSame([ + ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ], $this->hookLog); + } + + public function testSelectExport(): void + { + $m = $this->createTestModel(); + + $this->assertSameExportUnordered([ + ['id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['id' => 2, 'name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], + ], $m->export()); + + $this->assertSame([ + ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ], $this->hookLog); + } + + public function testInsert(): void + { + $m = $this->createTestModel(); + + $m->createEntity()->setMulti([ + 'name' => 'Karl', + 'birthday' => new \DateTime('2000-6-1'), + ])->save(); + + $this->assertSame([ + ['main', Model::HOOK_VALIDATE, ['save']], + ['main', Model::HOOK_BEFORE_SAVE, [false]], + ['main', Model::HOOK_BEFORE_INSERT, [['id' => null, 'name' => 'Karl', 'birthday' => \DateTime::class]]], + ['inner', Model::HOOK_VALIDATE, ['save']], + ['inner', Model::HOOK_BEFORE_SAVE, [false]], + ['inner', Model::HOOK_BEFORE_INSERT, [['id' => null, 'name' => 'Karl', 'y' => \DateTime::class]]], + ['inner', Persistence\Sql::HOOK_BEFORE_INSERT_QUERY, [Query::class]], + ['inner', Persistence\Sql::HOOK_AFTER_INSERT_QUERY, [Query::class, DbalResult::class]], + ['inner', Model::HOOK_AFTER_INSERT, []], + ['inner', Model::HOOK_AFTER_SAVE, [false]], + ['main', Model::HOOK_AFTER_INSERT, []], + ['main', Model::HOOK_BEFORE_UNLOAD, []], + ['main', Model::HOOK_AFTER_UNLOAD, []], + ['main', Model::HOOK_BEFORE_LOAD, [3]], + ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ['main', Model::HOOK_AFTER_LOAD, []], + ['main', Model::HOOK_AFTER_SAVE, [false]], + ], $this->hookLog); + + $this->assertSameExportUnordered([ + ['id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['id' => 2, 'name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], + ['id' => 3, 'name' => 'Karl', 'birthday' => new \DateTime('2000-6-1')], + ], $m->export()); + } + + public function testUpdate(): void + { + $m = $this->createTestModel(); + + $m->load(2)->setMulti([ + 'name' => 'Susan', + 'birthday' => new \DateTime('2020-10-10'), + ])->save(); + + $this->assertSame([ + ['main', Model::HOOK_BEFORE_LOAD, [2]], + ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ['main', Model::HOOK_AFTER_LOAD, []], + ['main', Model::HOOK_VALIDATE, ['save']], + ['main', Model::HOOK_BEFORE_SAVE, [true]], + ['main', Model::HOOK_BEFORE_UPDATE, [['name' => 'Susan', 'birthday' => \DateTime::class]]], + ['inner', Model::HOOK_BEFORE_LOAD, [2]], + ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ['inner', Model::HOOK_AFTER_LOAD, []], + ['inner', Model::HOOK_VALIDATE, ['save']], + ['inner', Model::HOOK_BEFORE_SAVE, [true]], + ['inner', Model::HOOK_BEFORE_UPDATE, [['name' => 'Susan', 'y' => \DateTime::class]]], + ['inner', Persistence\Sql::HOOK_BEFORE_UPDATE_QUERY, [Query::class]], + ['inner', Persistence\Sql::HOOK_AFTER_UPDATE_QUERY, [Query::class, DbalResult::class]], + ['inner', Model::HOOK_AFTER_UPDATE, [['name' => 'Susan', 'y' => \DateTime::class]]], + ['inner', Model::HOOK_AFTER_SAVE, [true]], + ['main', Model::HOOK_AFTER_UPDATE, [['name' => 'Susan', 'birthday' => \DateTime::class]]], + ['main', Model::HOOK_AFTER_SAVE, [true]], + ], $this->hookLog); + + $this->assertSameExportUnordered([ + ['id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['id' => 2, 'name' => 'Susan', 'birthday' => new \DateTime('2020-10-10')], + ], $m->export()); + } + + public function testDelete(): void + { + $m = $this->createTestModel(); + + $m->load(2)->delete(); + + $this->assertSame([ + ['main', Model::HOOK_BEFORE_LOAD, [2]], + ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ['main', Model::HOOK_AFTER_LOAD, []], + ['main', Model::HOOK_BEFORE_DELETE, []], + ['inner', Model::HOOK_BEFORE_LOAD, [2]], + ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ['inner', Model::HOOK_AFTER_LOAD, []], + ['inner', Model::HOOK_BEFORE_DELETE, []], + ['inner', Persistence\Sql::HOOK_BEFORE_DELETE_QUERY, [Query::class]], + ['inner', Persistence\Sql::HOOK_AFTER_DELETE_QUERY, [Query::class, DbalResult::class]], + ['inner', Model::HOOK_AFTER_DELETE, []], + ['inner', Model::HOOK_BEFORE_UNLOAD, []], + ['inner', Model::HOOK_AFTER_UNLOAD, []], + ['main', Model::HOOK_AFTER_DELETE, []], + ['main', Model::HOOK_BEFORE_UNLOAD, []], + ['main', Model::HOOK_AFTER_UNLOAD, []], + ], $this->hookLog); + + $this->assertSameExportUnordered([ + ['id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ], $m->export()); + } +} From aab0bf9c13aa812e7e75b1000957cfbc21e44509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 11 Jan 2022 14:01:30 +0100 Subject: [PATCH 12/17] test with "_id" ID column name --- src/Schema/TestCase.php | 10 ++++++---- tests/ModelNestedTest.php | 38 ++++++++++++++++++++++---------------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/Schema/TestCase.php b/src/Schema/TestCase.php index f4acf933c..fab30ef3f 100644 --- a/src/Schema/TestCase.php +++ b/src/Schema/TestCase.php @@ -255,11 +255,13 @@ public function setDb(array $dbData, bool $importData = true): void } $first_row = current($data); + $idColumnName = null; if ($first_row) { - $migrator->id('id'); + $idColumnName = isset($first_row['_id']) ? '_id' : 'id'; + $migrator->id($idColumnName); foreach ($first_row as $field => $row) { - if ($field === 'id') { + if ($field === $idColumnName) { continue; } @@ -292,8 +294,8 @@ public function setDb(array $dbData, bool $importData = true): void $query->table($tableName); $query->setMulti($row); - if (!isset($row['id']) && $hasId) { - $query->set('id', $id); + if (!isset($row[$idColumnName]) && $hasId) { + $query->set($idColumnName, $id); } $query->mode('insert')->execute(); diff --git a/tests/ModelNestedTest.php b/tests/ModelNestedTest.php index 076647c53..bff936f74 100644 --- a/tests/ModelNestedTest.php +++ b/tests/ModelNestedTest.php @@ -20,8 +20,8 @@ protected function setUp(): void $this->setDb([ 'user' => [ - ['id' => 1, 'name' => 'John', 'birthday_date' => '1980-2-1'], - ['id' => 2, 'name' => 'Sue', 'birthday_date' => '2005-4-3'], + ['_id' => 1, 'name' => 'John', '_birthday' => '1980-2-1'], + ['_id' => 2, 'name' => 'Sue', '_birthday' => '2005-4-3'], ], ]); } @@ -69,14 +69,20 @@ public function hook(string $spot, array $args = [], HookBreaker &$brokenBy = nu 'testModelAlias' => 'inner', 'table' => 'user', ]); + $mInner->removeField('id'); + $mInner->addField('_id', ['type' => 'integer']); + $mInner->id_field = '_id'; $mInner->addField('name'); - $mInner->addField('y', ['actual' => 'birthday_date', 'type' => 'date']); + $mInner->addField('y', ['actual' => '_birthday', 'type' => 'date']); $m = new $mWithLoggingClass($this->db, [ 'testCaseWeakRef' => \WeakReference::create($this), 'testModelAlias' => 'main', 'table' => $mInner, ]); + $m->removeField('id'); + $m->addField('_id', ['type' => 'integer']); + $m->id_field = '_id'; $m->addField('name'); $m->addField('birthday', ['actual' => 'y', 'type' => 'date']); @@ -94,15 +100,15 @@ public function testSelectSql(): void ($this->db->connection->dsql()) ->table( ($this->db->connection->dsql()) - ->field('id') + ->field('_id') ->field('name') - ->field('birthday_date', 'y') + ->field('_birthday', 'y') ->table('user') ->order('name', true) ->limit(5), '_tm' ) - ->field('id') + ->field('_id') ->field('name') ->field('y', 'birthday') ->order('y') @@ -121,8 +127,8 @@ public function testSelectExport(): void $m = $this->createTestModel(); $this->assertSameExportUnordered([ - ['id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], - ['id' => 2, 'name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], + ['_id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['_id' => 2, 'name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], ], $m->export()); $this->assertSame([ @@ -143,10 +149,10 @@ public function testInsert(): void $this->assertSame([ ['main', Model::HOOK_VALIDATE, ['save']], ['main', Model::HOOK_BEFORE_SAVE, [false]], - ['main', Model::HOOK_BEFORE_INSERT, [['id' => null, 'name' => 'Karl', 'birthday' => \DateTime::class]]], + ['main', Model::HOOK_BEFORE_INSERT, [['_id' => null, 'name' => 'Karl', 'birthday' => \DateTime::class]]], ['inner', Model::HOOK_VALIDATE, ['save']], ['inner', Model::HOOK_BEFORE_SAVE, [false]], - ['inner', Model::HOOK_BEFORE_INSERT, [['id' => null, 'name' => 'Karl', 'y' => \DateTime::class]]], + ['inner', Model::HOOK_BEFORE_INSERT, [['_id' => null, 'name' => 'Karl', 'y' => \DateTime::class]]], ['inner', Persistence\Sql::HOOK_BEFORE_INSERT_QUERY, [Query::class]], ['inner', Persistence\Sql::HOOK_AFTER_INSERT_QUERY, [Query::class, DbalResult::class]], ['inner', Model::HOOK_AFTER_INSERT, []], @@ -162,9 +168,9 @@ public function testInsert(): void ], $this->hookLog); $this->assertSameExportUnordered([ - ['id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], - ['id' => 2, 'name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], - ['id' => 3, 'name' => 'Karl', 'birthday' => new \DateTime('2000-6-1')], + ['_id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['_id' => 2, 'name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], + ['_id' => 3, 'name' => 'Karl', 'birthday' => new \DateTime('2000-6-1')], ], $m->export()); } @@ -200,8 +206,8 @@ public function testUpdate(): void ], $this->hookLog); $this->assertSameExportUnordered([ - ['id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], - ['id' => 2, 'name' => 'Susan', 'birthday' => new \DateTime('2020-10-10')], + ['_id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['_id' => 2, 'name' => 'Susan', 'birthday' => new \DateTime('2020-10-10')], ], $m->export()); } @@ -232,7 +238,7 @@ public function testDelete(): void ], $this->hookLog); $this->assertSameExportUnordered([ - ['id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['_id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], ], $m->export()); } } From 1a96086ef8161ec0bf633a9e8db2ad0d8aea9191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 11 Jan 2022 23:58:19 +0100 Subject: [PATCH 13/17] fix different ID column across nested models --- src/Field.php | 2 +- src/Model.php | 13 ------- src/Persistence.php | 51 ++++++++++++++------------ src/Persistence/Sql.php | 7 ++-- tests/ModelNestedTest.php | 76 ++++++++++++++++++++------------------- 5 files changed, 74 insertions(+), 75 deletions(-) diff --git a/src/Field.php b/src/Field.php index 0d780016b..19164ac3c 100644 --- a/src/Field.php +++ b/src/Field.php @@ -499,7 +499,7 @@ public function __debugInfo(): array ]; foreach ([ - 'system', 'never_persist', 'never_save', 'read_only', 'ui', 'joinName', + 'actual', 'system', 'never_persist', 'never_save', 'read_only', 'ui', 'joinName', ] as $key) { if ($this->{$key} !== null) { $arr[$key] = $this->{$key}; diff --git a/src/Model.php b/src/Model.php index 78a473c50..bd1bed894 100644 --- a/src/Model.php +++ b/src/Model.php @@ -1612,10 +1612,6 @@ public function save(array $data = []) }); } - /** - * This is a temporary method to avoid code duplication, but insert / import should - * be implemented differently. - */ protected function _insert(array $row): void { // Find any row values that do not correspond to fields, and they may correspond to @@ -1658,10 +1654,6 @@ protected function _insert(array $row): void } /** - * Faster method to add data, that does not modify active record. - * - * Will be further optimized in the future. - * * @return mixed */ public function insert(array $row) @@ -1673,11 +1665,6 @@ public function insert(array $row) } /** - * Even more faster method to add data, does not modify your - * current record and will not return anything. - * - * Will be further optimized in the future. - * * @return $this */ public function import(array $rows) diff --git a/src/Persistence.php b/src/Persistence.php index 1a32b1e5f..0eed8e4ed 100644 --- a/src/Persistence.php +++ b/src/Persistence.php @@ -116,18 +116,6 @@ public function getDatabasePlatform(): Platforms\AbstractPlatform return new Persistence\GenericPlatform(); } - private function assertSameIdField(Model $model): void - { - $modelIdFieldName = $model->getField($model->id_field)->getPersistenceName(); - $tableIdFieldName = $model->table->id_field; - - if ($modelIdFieldName !== $tableIdFieldName) { - throw (new Exception('Table model with different ID field persistence name is not supported')) - ->addMoreInfo('model_id_field', $modelIdFieldName) - ->addMoreInfo('table_id_field', $tableIdFieldName); - } - } - /** * Tries to load data record, but will not fail if record can't be loaded. * @@ -174,13 +162,28 @@ public function insert(Model $model, array $data) unset($data); if (is_object($model->table)) { - $this->assertSameIdField($model); + $innerInsertId = $model->table->insert($this->typecastLoadRow($model->table, $dataRaw)); + if (!$model->id_field) { + return false; + } - return $model->table->insert($model->table->persistence->typecastLoadRow($model->table, $dataRaw)); + $idField = $model->getField($model->id_field); + $insertId = $this->typecastLoadField( + $idField, + $idField->getPersistenceName() === $model->table->id_field + ? $this->typecastSaveField($model->table->getField($model->table->id_field), $innerInsertId) + : $dataRaw[$idField->getPersistenceName()] + ); + + return $insertId; } $idRaw = $this->insertRaw($model, $dataRaw); - $id = $model->id_field ? $this->typecastLoadField($model->getField($model->id_field), $idRaw) : new \stdClass(); + if (!$model->id_field) { + return false; + } + + $id = $this->typecastLoadField($model->getField($model->id_field), $idRaw); return $id; } @@ -201,6 +204,7 @@ protected function insertRaw(Model $model, array $dataRaw) public function update(Model $model, $id, array $data): void { $idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null; + unset($id); if ($idRaw === null || (array_key_exists($model->id_field, $data) && $data[$model->id_field] === null)) { throw new Exception('Model id_field is not set. Unable to update record.'); } @@ -213,15 +217,15 @@ public function update(Model $model, $id, array $data): void } if (is_object($model->table)) { - $this->assertSameIdField($model); + $idPersistenceName = $model->getField($model->id_field)->getPersistenceName(); + $innerId = $this->typecastLoadField($model->table->getField($idPersistenceName), $idRaw); + $innerModel = $model->table->loadBy($idPersistenceName, $innerId); - $model->table->load($id)->save($model->table->persistence->typecastLoadRow($model->table, $dataRaw)); + $innerModel->save($this->typecastLoadRow($model->table, $dataRaw)); return; } - unset($id); - $this->updateRaw($model, $idRaw, $dataRaw); } @@ -241,20 +245,21 @@ protected function updateRaw(Model $model, $idRaw, array $dataRaw): void public function delete(Model $model, $id): void { $idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null; + unset($id); if ($idRaw === null) { throw new Exception('Model id_field is not set. Unable to delete record.'); } if (is_object($model->table)) { - $this->assertSameIdField($model); + $idPersistenceName = $model->getField($model->id_field)->getPersistenceName(); + $innerId = $this->typecastLoadField($model->table->getField($idPersistenceName), $idRaw); + $innerModel = $model->table->loadBy($idPersistenceName, $innerId); - $model->table->delete($id); + $innerModel->delete(); return; } - unset($id); - $this->deleteRaw($model, $idRaw); } diff --git a/src/Persistence/Sql.php b/src/Persistence/Sql.php index d0d36ffdd..e5bd19f70 100644 --- a/src/Persistence/Sql.php +++ b/src/Persistence/Sql.php @@ -422,7 +422,8 @@ public function action(Model $model, string $type, array $args = []) $this->setLimitOrder($model, $query); if ($model->isEntity() && $model->isLoaded()) { - $query->where($model->getField($model->id_field), $model->getId()); + $idRaw = $this->typecastSaveField($model->getField($model->id_field), $model->getId()); + $query->where($model->getField($model->id_field), $idRaw); } return $query; @@ -478,7 +479,9 @@ public function tryLoad(Model $model, $id): ?array throw (new Exception('Unable to load field by "id" when Model->id_field is not defined.')) ->addMoreInfo('id', $id); } - $query->where($model->getField($model->id_field), $id); + + $idRaw = $this->typecastSaveField($model->getField($model->id_field), $id); + $query->where($model->getField($model->id_field), $idRaw); } $query->limit($id === self::ID_LOAD_ANY ? 1 : 2); diff --git a/tests/ModelNestedTest.php b/tests/ModelNestedTest.php index bff936f74..309583226 100644 --- a/tests/ModelNestedTest.php +++ b/tests/ModelNestedTest.php @@ -20,8 +20,8 @@ protected function setUp(): void $this->setDb([ 'user' => [ - ['_id' => 1, 'name' => 'John', '_birthday' => '1980-2-1'], - ['_id' => 2, 'name' => 'Sue', '_birthday' => '2005-4-3'], + ['_id' => 1, 'name' => 'John', '_birthday' => '1980-02-01'], + ['_id' => 2, 'name' => 'Sue', '_birthday' => '2005-04-03'], ], ]); } @@ -70,8 +70,8 @@ public function hook(string $spot, array $args = [], HookBreaker &$brokenBy = nu 'table' => 'user', ]); $mInner->removeField('id'); - $mInner->addField('_id', ['type' => 'integer']); - $mInner->id_field = '_id'; + $mInner->id_field = 'uid'; + $mInner->addField('uid', ['actual' => '_id', 'type' => 'integer']); $mInner->addField('name'); $mInner->addField('y', ['actual' => '_birthday', 'type' => 'date']); @@ -81,8 +81,7 @@ public function hook(string $spot, array $args = [], HookBreaker &$brokenBy = nu 'table' => $mInner, ]); $m->removeField('id'); - $m->addField('_id', ['type' => 'integer']); - $m->id_field = '_id'; + $m->id_field = 'birthday'; $m->addField('name'); $m->addField('birthday', ['actual' => 'y', 'type' => 'date']); @@ -100,7 +99,7 @@ public function testSelectSql(): void ($this->db->connection->dsql()) ->table( ($this->db->connection->dsql()) - ->field('_id') + ->field('_id', 'uid') ->field('name') ->field('_birthday', 'y') ->table('user') @@ -108,7 +107,6 @@ public function testSelectSql(): void ->limit(5), '_tm' ) - ->field('_id') ->field('name') ->field('y', 'birthday') ->order('y') @@ -127,8 +125,8 @@ public function testSelectExport(): void $m = $this->createTestModel(); $this->assertSameExportUnordered([ - ['_id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], - ['_id' => 2, 'name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], + ['name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], ], $m->export()); $this->assertSame([ @@ -141,18 +139,19 @@ public function testInsert(): void { $m = $this->createTestModel(); - $m->createEntity()->setMulti([ - 'name' => 'Karl', - 'birthday' => new \DateTime('2000-6-1'), - ])->save(); + $entity = $m->createEntity() + ->setMulti([ + 'name' => 'Karl', + 'birthday' => new \DateTime('2000-6-1'), + ])->save(); $this->assertSame([ ['main', Model::HOOK_VALIDATE, ['save']], ['main', Model::HOOK_BEFORE_SAVE, [false]], - ['main', Model::HOOK_BEFORE_INSERT, [['_id' => null, 'name' => 'Karl', 'birthday' => \DateTime::class]]], + ['main', Model::HOOK_BEFORE_INSERT, [['name' => 'Karl', 'birthday' => \DateTime::class]]], ['inner', Model::HOOK_VALIDATE, ['save']], ['inner', Model::HOOK_BEFORE_SAVE, [false]], - ['inner', Model::HOOK_BEFORE_INSERT, [['_id' => null, 'name' => 'Karl', 'y' => \DateTime::class]]], + ['inner', Model::HOOK_BEFORE_INSERT, [['uid' => null, 'name' => 'Karl', 'y' => \DateTime::class]]], ['inner', Persistence\Sql::HOOK_BEFORE_INSERT_QUERY, [Query::class]], ['inner', Persistence\Sql::HOOK_AFTER_INSERT_QUERY, [Query::class, DbalResult::class]], ['inner', Model::HOOK_AFTER_INSERT, []], @@ -160,17 +159,20 @@ public function testInsert(): void ['main', Model::HOOK_AFTER_INSERT, []], ['main', Model::HOOK_BEFORE_UNLOAD, []], ['main', Model::HOOK_AFTER_UNLOAD, []], - ['main', Model::HOOK_BEFORE_LOAD, [3]], + ['main', Model::HOOK_BEFORE_LOAD, [\DateTime::class]], ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['main', Model::HOOK_AFTER_LOAD, []], ['main', Model::HOOK_AFTER_SAVE, [false]], ], $this->hookLog); + $this->assertSame(3, $m->table->loadBy('name', 'Karl')->getId()); + $this->assertSameExportUnordered([[new \DateTime('2000-6-1')]], [[$entity->getId()]]); + $this->assertSameExportUnordered([ - ['_id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], - ['_id' => 2, 'name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], - ['_id' => 3, 'name' => 'Karl', 'birthday' => new \DateTime('2000-6-1')], + ['name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], + ['name' => 'Karl', 'birthday' => new \DateTime('2000-6-1')], ], $m->export()); } @@ -178,36 +180,37 @@ public function testUpdate(): void { $m = $this->createTestModel(); - $m->load(2)->setMulti([ - 'name' => 'Susan', - 'birthday' => new \DateTime('2020-10-10'), - ])->save(); + $m->load(new \DateTime('2005-4-3')) + ->setMulti([ + 'name' => 'Susan', + ])->save(); $this->assertSame([ - ['main', Model::HOOK_BEFORE_LOAD, [2]], + ['main', Model::HOOK_BEFORE_LOAD, [\DateTime::class]], ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['main', Model::HOOK_AFTER_LOAD, []], + ['main', Model::HOOK_VALIDATE, ['save']], ['main', Model::HOOK_BEFORE_SAVE, [true]], - ['main', Model::HOOK_BEFORE_UPDATE, [['name' => 'Susan', 'birthday' => \DateTime::class]]], - ['inner', Model::HOOK_BEFORE_LOAD, [2]], + ['main', Model::HOOK_BEFORE_UPDATE, [['name' => 'Susan']]], + ['inner', Model::HOOK_BEFORE_LOAD, [null]], ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['inner', Model::HOOK_AFTER_LOAD, []], ['inner', Model::HOOK_VALIDATE, ['save']], ['inner', Model::HOOK_BEFORE_SAVE, [true]], - ['inner', Model::HOOK_BEFORE_UPDATE, [['name' => 'Susan', 'y' => \DateTime::class]]], + ['inner', Model::HOOK_BEFORE_UPDATE, [['name' => 'Susan']]], ['inner', Persistence\Sql::HOOK_BEFORE_UPDATE_QUERY, [Query::class]], ['inner', Persistence\Sql::HOOK_AFTER_UPDATE_QUERY, [Query::class, DbalResult::class]], - ['inner', Model::HOOK_AFTER_UPDATE, [['name' => 'Susan', 'y' => \DateTime::class]]], + ['inner', Model::HOOK_AFTER_UPDATE, [['name' => 'Susan']]], ['inner', Model::HOOK_AFTER_SAVE, [true]], - ['main', Model::HOOK_AFTER_UPDATE, [['name' => 'Susan', 'birthday' => \DateTime::class]]], + ['main', Model::HOOK_AFTER_UPDATE, [['name' => 'Susan']]], ['main', Model::HOOK_AFTER_SAVE, [true]], ], $this->hookLog); $this->assertSameExportUnordered([ - ['_id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], - ['_id' => 2, 'name' => 'Susan', 'birthday' => new \DateTime('2020-10-10')], + ['name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['name' => 'Susan', 'birthday' => new \DateTime('2005-4-3')], ], $m->export()); } @@ -215,15 +218,16 @@ public function testDelete(): void { $m = $this->createTestModel(); - $m->load(2)->delete(); + $m->delete(new \DateTime('2005-4-3')); $this->assertSame([ - ['main', Model::HOOK_BEFORE_LOAD, [2]], + ['main', Model::HOOK_BEFORE_LOAD, [\DateTime::class]], ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['main', Model::HOOK_AFTER_LOAD, []], + ['main', Model::HOOK_BEFORE_DELETE, []], - ['inner', Model::HOOK_BEFORE_LOAD, [2]], + ['inner', Model::HOOK_BEFORE_LOAD, [null]], ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['inner', Model::HOOK_AFTER_LOAD, []], ['inner', Model::HOOK_BEFORE_DELETE, []], @@ -238,7 +242,7 @@ public function testDelete(): void ], $this->hookLog); $this->assertSameExportUnordered([ - ['_id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['name' => 'John', 'birthday' => new \DateTime('1980-2-1')], ], $m->export()); } } From d48a0379d6833b23b768bd358f0f676cefb2cfe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 12 Jan 2022 00:50:26 +0100 Subject: [PATCH 14/17] assert nested transactions --- tests/ModelNestedTest.php | 65 ++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/tests/ModelNestedTest.php b/tests/ModelNestedTest.php index 309583226..128d2c234 100644 --- a/tests/ModelNestedTest.php +++ b/tests/ModelNestedTest.php @@ -37,31 +37,48 @@ protected function createTestModel(): Model /** @var string */ protected $testModelAlias; + /** + * @param mixed $v + * + * @return mixed + */ + protected function convertValueToLog($v) + { + if (is_array($v)) { + return array_map(fn ($v) => $this->convertValueToLog($v), $v); + } elseif (is_scalar($v) || $v === null) { + return $v; + } elseif ($v instanceof self) { + return $this->testModelAlias; + } + + $res = preg_replace('~(?<=^Atk4\\\\Data\\\\Persistence\\\\Sql\\\\)\w+\\\\(?=\w+$)~', '', get_debug_type($v)); + if (Connection::isComposerDbal2x() && $res === 'Doctrine\DBAL\Statement') { + $res = DbalResult::class; + } + + return $res; + } + public function hook(string $spot, array $args = [], HookBreaker &$brokenBy = null) { if (!str_starts_with($spot, '__atk__method__') && $spot !== Model::HOOK_NORMALIZE) { - $convertValueToLogFx = function ($v) use (&$convertValueToLogFx) { - if (is_array($v)) { - return array_map($convertValueToLogFx, $v); - } elseif (is_scalar($v) || $v === null) { - return $v; - } elseif ($v instanceof self) { - return $this->testModelAlias; - } - - $res = preg_replace('~(?<=^Atk4\\\\Data\\\\Persistence\\\\Sql\\\\)\w+\\\\(?=\w+$)~', '', get_debug_type($v)); - if (Connection::isComposerDbal2x() && $res === 'Doctrine\DBAL\Statement') { - $res = DbalResult::class; - } - - return $res; - }; - - $this->testCaseWeakRef->get()->hookLog[] = [$convertValueToLogFx($this), $spot, $convertValueToLogFx($args)]; + $this->testCaseWeakRef->get()->hookLog[] = [$this->convertValueToLog($this), $spot, $this->convertValueToLog($args)]; } return parent::hook($spot, $args, $brokenBy); } + + public function atomic(\Closure $fx) + { + $this->testCaseWeakRef->get()->hookLog[] = [$this->convertValueToLog($this), '>>>']; + + $res = parent::atomic($fx); + + $this->testCaseWeakRef->get()->hookLog[] = [$this->convertValueToLog($this), '<<<']; + + return $res; + } }); $mInner = new $mWithLoggingClass($this->db, [ @@ -146,9 +163,11 @@ public function testInsert(): void ])->save(); $this->assertSame([ + ['main', '>>>'], ['main', Model::HOOK_VALIDATE, ['save']], ['main', Model::HOOK_BEFORE_SAVE, [false]], ['main', Model::HOOK_BEFORE_INSERT, [['name' => 'Karl', 'birthday' => \DateTime::class]]], + ['inner', '>>>'], ['inner', Model::HOOK_VALIDATE, ['save']], ['inner', Model::HOOK_BEFORE_SAVE, [false]], ['inner', Model::HOOK_BEFORE_INSERT, [['uid' => null, 'name' => 'Karl', 'y' => \DateTime::class]]], @@ -156,6 +175,7 @@ public function testInsert(): void ['inner', Persistence\Sql::HOOK_AFTER_INSERT_QUERY, [Query::class, DbalResult::class]], ['inner', Model::HOOK_AFTER_INSERT, []], ['inner', Model::HOOK_AFTER_SAVE, [false]], + ['inner', '<<<'], ['main', Model::HOOK_AFTER_INSERT, []], ['main', Model::HOOK_BEFORE_UNLOAD, []], ['main', Model::HOOK_AFTER_UNLOAD, []], @@ -164,6 +184,7 @@ public function testInsert(): void ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['main', Model::HOOK_AFTER_LOAD, []], ['main', Model::HOOK_AFTER_SAVE, [false]], + ['main', '<<<'], ], $this->hookLog); $this->assertSame(3, $m->table->loadBy('name', 'Karl')->getId()); @@ -191,12 +212,14 @@ public function testUpdate(): void ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['main', Model::HOOK_AFTER_LOAD, []], + ['main', '>>>'], ['main', Model::HOOK_VALIDATE, ['save']], ['main', Model::HOOK_BEFORE_SAVE, [true]], ['main', Model::HOOK_BEFORE_UPDATE, [['name' => 'Susan']]], ['inner', Model::HOOK_BEFORE_LOAD, [null]], ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['inner', Model::HOOK_AFTER_LOAD, []], + ['inner', '>>>'], ['inner', Model::HOOK_VALIDATE, ['save']], ['inner', Model::HOOK_BEFORE_SAVE, [true]], ['inner', Model::HOOK_BEFORE_UPDATE, [['name' => 'Susan']]], @@ -204,8 +227,10 @@ public function testUpdate(): void ['inner', Persistence\Sql::HOOK_AFTER_UPDATE_QUERY, [Query::class, DbalResult::class]], ['inner', Model::HOOK_AFTER_UPDATE, [['name' => 'Susan']]], ['inner', Model::HOOK_AFTER_SAVE, [true]], + ['inner', '<<<'], ['main', Model::HOOK_AFTER_UPDATE, [['name' => 'Susan']]], ['main', Model::HOOK_AFTER_SAVE, [true]], + ['main', '<<<'], ], $this->hookLog); $this->assertSameExportUnordered([ @@ -226,17 +251,21 @@ public function testDelete(): void ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['main', Model::HOOK_AFTER_LOAD, []], + ['main', '>>>'], ['main', Model::HOOK_BEFORE_DELETE, []], ['inner', Model::HOOK_BEFORE_LOAD, [null]], ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['inner', Model::HOOK_AFTER_LOAD, []], + ['inner', '>>>'], ['inner', Model::HOOK_BEFORE_DELETE, []], ['inner', Persistence\Sql::HOOK_BEFORE_DELETE_QUERY, [Query::class]], ['inner', Persistence\Sql::HOOK_AFTER_DELETE_QUERY, [Query::class, DbalResult::class]], ['inner', Model::HOOK_AFTER_DELETE, []], + ['inner', '<<<'], ['inner', Model::HOOK_BEFORE_UNLOAD, []], ['inner', Model::HOOK_AFTER_UNLOAD, []], ['main', Model::HOOK_AFTER_DELETE, []], + ['main', '<<<'], ['main', Model::HOOK_BEFORE_UNLOAD, []], ['main', Model::HOOK_AFTER_UNLOAD, []], ], $this->hookLog); From 9d7a89f06925ea11d7d2ee970b00e335181e205f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 12 Jan 2022 11:52:34 +0100 Subject: [PATCH 15/17] add one duplicate but filtered out inner record --- src/Persistence/Sql.php | 4 ---- tests/ModelNestedTest.php | 7 +++++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Persistence/Sql.php b/src/Persistence/Sql.php index e5bd19f70..a6c84a3da 100644 --- a/src/Persistence/Sql.php +++ b/src/Persistence/Sql.php @@ -664,10 +664,6 @@ public function getFieldSqlExpression(Field $field, Expression $expression): Exp public function lastInsertId(Model $model): string { - if (is_object($model->table)) { - return $model->table->persistence->lastInsertId($model->table); - } - // PostgreSQL and Oracle DBAL platforms use sequence internally for PK autoincrement, // use default name if not set explicitly $sequenceName = null; diff --git a/tests/ModelNestedTest.php b/tests/ModelNestedTest.php index 128d2c234..bb7b2ddf2 100644 --- a/tests/ModelNestedTest.php +++ b/tests/ModelNestedTest.php @@ -22,6 +22,7 @@ protected function setUp(): void 'user' => [ ['_id' => 1, 'name' => 'John', '_birthday' => '1980-02-01'], ['_id' => 2, 'name' => 'Sue', '_birthday' => '2005-04-03'], + ['_id' => 3, 'name' => 'Veronica', '_birthday' => '2005-04-03'], ], ]); } @@ -91,6 +92,7 @@ public function atomic(\Closure $fx) $mInner->addField('uid', ['actual' => '_id', 'type' => 'integer']); $mInner->addField('name'); $mInner->addField('y', ['actual' => '_birthday', 'type' => 'date']); + $mInner->addCondition('uid', '!=', 3); $m = new $mWithLoggingClass($this->db, [ 'testCaseWeakRef' => \WeakReference::create($this), @@ -116,10 +118,11 @@ public function testSelectSql(): void ($this->db->connection->dsql()) ->table( ($this->db->connection->dsql()) + ->table('user') ->field('_id', 'uid') ->field('name') ->field('_birthday', 'y') - ->table('user') + ->where('_id', '!=', 3) ->order('name', true) ->limit(5), '_tm' @@ -187,7 +190,7 @@ public function testInsert(): void ['main', '<<<'], ], $this->hookLog); - $this->assertSame(3, $m->table->loadBy('name', 'Karl')->getId()); + $this->assertSame(4, $m->table->loadBy('name', 'Karl')->getId()); $this->assertSameExportUnordered([[new \DateTime('2000-6-1')]], [[$entity->getId()]]); $this->assertSameExportUnordered([ From 6fbfd27b67d7847486f465c477ccd0465c6d3c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 12 Jan 2022 12:03:47 +0100 Subject: [PATCH 16/17] rm duplicate/unneeded CSV methods --- src/Persistence/Csv.php | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/Persistence/Csv.php b/src/Persistence/Csv.php index 54f560d51..04b54fada 100644 --- a/src/Persistence/Csv.php +++ b/src/Persistence/Csv.php @@ -284,26 +284,6 @@ protected function insertRaw(Model $model, array $dataRaw) return $model->id_field ? $dataRaw[$model->id_field] : null; } - protected function updateRaw(Model $model, $idRaw, array $dataRaw): void - { - throw new Exception('Updating records is not supported in CSV persistence.'); - } - - protected function deleteRaw(Model $model, $idRaw): void - { - throw new Exception('Deleting records is not supported in CSV persistence.'); - } - - /** - * Generates new record ID. - * - * @return string - */ - public function generateNewId(Model $model) - { - throw new Exception('Not implemented'); - } - /** * Export all DataSet. */ From 44cf43efc475e7e89fe3a6785ba4c86cb8a46496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 12 Jan 2022 12:26:43 +0100 Subject: [PATCH 17/17] rm unneeded bool cmp val --- src/Persistence/Sql.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Persistence/Sql.php b/src/Persistence/Sql.php index a6c84a3da..70adfa514 100644 --- a/src/Persistence/Sql.php +++ b/src/Persistence/Sql.php @@ -606,7 +606,7 @@ protected function updateRaw(Model $model, $idRaw, array $dataRaw): void $model->hook(self::HOOK_AFTER_UPDATE_QUERY, [$update, $st]); // if any rows were updated in database, and we had expressions, reload - if ($model->reload_after_save === true && $st->rowCount()) { + if ($model->reload_after_save && $st->rowCount()) { $d = $model->getDirtyRef(); $model->reload(); \Closure::bind(function () use ($model) {