From 0b9d78aa4121835bbbd80aef6c964aec220bc10e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Thu, 9 Feb 2023 09:47:14 +0100 Subject: [PATCH] Fix non-scalar value to be implied from definitive condition --- src/Field.php | 4 +- src/Model/Scope/Condition.php | 12 ++-- tests/ReferenceSqlTest.php | 123 ++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 8 deletions(-) diff --git a/src/Field.php b/src/Field.php index 44c41337dd..3f932c8318 100644 --- a/src/Field.php +++ b/src/Field.php @@ -411,7 +411,6 @@ public function useAlias(): bool public function getQueryArguments($operator, $value): array { $typecastField = $this; - $allowArray = true; if (in_array($operator, [ Scope\Condition::OPERATOR_LIKE, Scope\Condition::OPERATOR_NOT_LIKE, @@ -421,12 +420,11 @@ public function getQueryArguments($operator, $value): array $typecastField = new self(['type' => 'string']); $typecastField->setOwner(new Model($this->getOwner()->getPersistence(), ['table' => false])); $typecastField->shortName = $this->shortName; - $allowArray = false; } if ($value instanceof Persistence\Array_\Action) { // needed to pass hintable tests $v = $value; - } elseif (is_array($value) && $allowArray) { + } elseif (is_array($value) && in_array($operator, [Scope\Condition::OPERATOR_IN, Scope\Condition::OPERATOR_NOT_IN], true)) { $v = array_map(fn ($value) => $typecastField->typecastSaveField($value), $value); } else { $v = $typecastField->typecastSaveField($value); diff --git a/src/Model/Scope/Condition.php b/src/Model/Scope/Condition.php index 1ef6706e85..8735d468e9 100644 --- a/src/Model/Scope/Condition.php +++ b/src/Model/Scope/Condition.php @@ -8,6 +8,7 @@ use Atk4\Data\Exception; use Atk4\Data\Field; use Atk4\Data\Model; +use Atk4\Data\Persistence; use Atk4\Data\Persistence\Sql\Expression; use Atk4\Data\Persistence\Sql\Expressionable; use Atk4\Data\Persistence\Sql\Sqlite\Expression as SqliteExpression; @@ -145,11 +146,10 @@ protected function onChangeModel(): void { $model = $this->getModel(); if ($model !== null) { - // if we have a definitive scalar value for a field - // sets it as default value for field and locks it + // if we have a definitive equal condition set the value as default value for field // new records will automatically get this value assigned for the field - // @todo: consider this when condition is part of OR scope - if ($this->operator === self::OPERATOR_EQUALS && !is_object($this->value) && !is_array($this->value)) { + // TODO: consider this when condition is part of OR scope + if ($this->operator === self::OPERATOR_EQUALS && !$this->value instanceof Expressionable) { // key containing '/' means chained references and it is handled in toQueryArguments method $field = $this->key; if (is_string($field) && !str_contains($field, '/')) { @@ -161,7 +161,9 @@ protected function onChangeModel(): void // for now, do not set default at least for PK/ID if ($field instanceof Field && $field->shortName !== $field->getOwner()->idField) { $field->system = true; - $field->default = $this->value; + $fakePersistence = new Persistence\Array_(); + $valueCloned = $fakePersistence->typecastLoadField($field, $fakePersistence->typecastSaveField($field, $this->value)); + $field->default = $valueCloned; } } } diff --git a/tests/ReferenceSqlTest.php b/tests/ReferenceSqlTest.php index 19abfe064e..c6dcad7a9c 100644 --- a/tests/ReferenceSqlTest.php +++ b/tests/ReferenceSqlTest.php @@ -7,9 +7,11 @@ use Atk4\Data\Exception; use Atk4\Data\Model; use Atk4\Data\Schema\TestCase; +use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\DBAL\Platforms\SQLServerPlatform; +use Doctrine\DBAL\Types as DbalTypes; /** * Tests that condition is applied when traversing hasMany @@ -280,6 +282,127 @@ public function testRelatedExpression(): void ); } + public function testReferenceWithObjectId(): void + { + $this->setDb([ + 'file' => [ + 1 => ['id' => 1, 'name' => 'a.txt', 'parentDirectoryId' => null], + ['id' => 2, 'name' => 'u', 'parentDirectoryId' => null], + ['id' => 3, 'name' => 'v', 'parentDirectoryId' => 2], + ['id' => 4, 'name' => 'w', 'parentDirectoryId' => 2], + ['id' => 5, 'name' => 'b.txt', 'parentDirectoryId' => 2], + ['id' => 6, 'name' => 'c.txt', 'parentDirectoryId' => 3], + ['id' => 7, 'name' => 'd.txt', 'parentDirectoryId' => 2], + ['id' => 8, 'name' => 'e.txt', 'parentDirectoryId' => 4], + ], + ]); + + $integerWrappedType = new class() extends DbalTypes\Type { + public function getName(): string + { + return self::class; + } + + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string + { + return DbalTypes\Type::getType(DbalTypes\Types::INTEGER)->getSQLDeclaration($fieldDeclaration, $platform); + } + + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?int + { + if ($value === null) { + return null; + } + + return DbalTypes\Type::getType('integer')->convertToDatabaseValue($value->getId(), $platform); + } + + public function convertToPHPValue($value, AbstractPlatform $platform): ?object + { + if ($value === null) { + return null; + } + + return new class(DbalTypes\Type::getType('integer')->convertToPHPValue($value, $platform)) { + private int $id; + + public function __construct(int $id) + { + $this->id = $id; + } + + public function getId(): int + { + return $this->id; + } + }; + } + }; + + DbalTypes\Type::addType($integerWrappedType->getName(), get_class($integerWrappedType)); + try { + $file = new Model($this->db, ['table' => 'file']); + $file->getField('id')->type = $integerWrappedType->getName(); + $file->addField('name'); + $file->hasOne('parentDirectory', [ + 'model' => $file, + 'type' => $integerWrappedType->getName(), + 'ourField' => 'parentDirectoryId', + ]); + $file->hasMany('childFiles', [ + 'model' => $file, + 'theirField' => 'parentDirectoryId' + ]); + + $fileEntity = $file->loadBy('name', 'v')->ref('childFiles')->createEntity(); + $fileEntity->save(['name' => 'x']); + + $fileEntity = $fileEntity->ref('childFiles')->createEntity(); + $fileEntity->save(['name' => 'y.txt']); + + $createWrappedIntegerFx = function (int $v) use ($integerWrappedType): object { + return $integerWrappedType->convertToPHPValue($v, $this->getDatabasePlatform()); + }; + + static::{'assertEquals'}([ + ['id' => $createWrappedIntegerFx(10), 'name' => 'y.txt', 'parentDirectoryId' => $createWrappedIntegerFx(9)], + ], $fileEntity->getModel()->export()); + static::assertSame([], $fileEntity->ref('childFiles')->export()); + + $fileEntity = $fileEntity->ref('parentDirectory'); + static::{'assertEquals'}([ + ['id' => $createWrappedIntegerFx(9), 'name' => 'x', 'parentDirectoryId' => $createWrappedIntegerFx(3)], + ], $fileEntity->getModel()->export()); + static::{'assertEquals'}([ + ['id' => $createWrappedIntegerFx(10), 'name' => 'y.txt', 'parentDirectoryId' => $createWrappedIntegerFx(9)], + ], $fileEntity->ref('childFiles')->export()); + + $fileEntity = $fileEntity->ref('parentDirectory'); + static::{'assertEquals'}([ + ['id' => $createWrappedIntegerFx(3), 'name' => 'v', 'parentDirectoryId' => $createWrappedIntegerFx(2)], + ], $fileEntity->getModel()->export()); + static::{'assertEquals'}([ + ['id' => $createWrappedIntegerFx(6), 'name' => 'c.txt', 'parentDirectoryId' => $createWrappedIntegerFx(3)], + ['id' => $createWrappedIntegerFx(9), 'name' => 'x', 'parentDirectoryId' => $createWrappedIntegerFx(3)], + ], $fileEntity->ref('childFiles')->export()); + static::{'assertEquals'}([ + ['id' => $createWrappedIntegerFx(6), 'name' => 'c.txt', 'parentDirectoryId' => $createWrappedIntegerFx(3)], + ['id' => $createWrappedIntegerFx(8), 'name' => 'e.txt', 'parentDirectoryId' => $createWrappedIntegerFx(4)], + ['id' => $createWrappedIntegerFx(9), 'name' => 'x', 'parentDirectoryId' => $createWrappedIntegerFx(3)], + ], $fileEntity->ref('parentDirectory')->ref('childFiles')->ref('childFiles')->export()); + + $fileEntity = $fileEntity->ref('parentDirectory'); + static::{'assertEquals'}([ + ['id' => $createWrappedIntegerFx(2), 'name' => 'u', 'parentDirectoryId' => null], + ], $fileEntity->getModel()->export()); + } finally { + \Closure::bind(function () use ($integerWrappedType) { + $dbalTypeRegistry = DbalTypes\Type::getTypeRegistry(); + unset($dbalTypeRegistry->instances[$integerWrappedType->getName()]); + }, null, DbalTypes\TypeRegistry::class)(); + } + } + public function testAggregateHasMany(): void { $vat = 0.23;