From a92196b5b7bd30c9dda1740bbaa595cfd84f2f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 28 May 2024 22:23:51 +0200 Subject: [PATCH 01/19] fix "integer with autoincrement" introspect for SQLite --- src/Persistence/Sql/Sqlite/PlatformTrait.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Persistence/Sql/Sqlite/PlatformTrait.php b/src/Persistence/Sql/Sqlite/PlatformTrait.php index 26d0fa4ad..885c6c49c 100644 --- a/src/Persistence/Sql/Sqlite/PlatformTrait.php +++ b/src/Persistence/Sql/Sqlite/PlatformTrait.php @@ -4,11 +4,20 @@ namespace Atk4\Data\Persistence\Sql\Sqlite; +use Atk4\Data\Persistence\Sql\PlatformFixColumnCommentTypeHintTrait; use Doctrine\DBAL\Schema\ForeignKeyConstraint; use Doctrine\DBAL\Schema\TableDiff; trait PlatformTrait { + // remove (and related property) once https://github.com/doctrine/dbal/pull/6411 is merged and released + use PlatformFixColumnCommentTypeHintTrait; + + /** @var list */ + private $requireCommentHintTypes = [ + 'bigint', + ]; + public function __construct() { $this->disableSchemaEmulation(); // @phpstan-ignore method.deprecated From 32a3fea28f47e62efd664c404a1511f7030ae4ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Thu, 23 May 2024 14:08:56 +0200 Subject: [PATCH 02/19] improve migrator test --- tests/Schema/MigratorTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Schema/MigratorTest.php b/tests/Schema/MigratorTest.php index 19d8a2e35..12201dfb0 100644 --- a/tests/Schema/MigratorTest.php +++ b/tests/Schema/MigratorTest.php @@ -389,13 +389,16 @@ public function testIntrospectTableToModelSetPersistence(): void $this->createMigrator() ->table('t') ->id() + ->field('a') ->create(); $model = (new Migrator($this->getConnection()))->introspectTableToModel('t'); + self::assertSame(['id', 'a'], array_keys($model->getFields())); self::assertFalse($model->issetPersistence()); $model = $this->createMigrator()->introspectTableToModel('t'); self::assertTrue($model->issetPersistence()); + self::assertSame(['id', 'a'], array_keys($model->getFields())); } } From dd2bd9ade3719257f2f502cad4467c505512ca22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Fri, 24 May 2024 14:01:21 +0200 Subject: [PATCH 03/19] create "bigint" column for "integer" in Migrator --- src/Schema/Migrator.php | 8 ++++++-- tests/Schema/MigratorTest.php | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Schema/Migrator.php b/src/Schema/Migrator.php index 21f033564..d2ebf63c3 100644 --- a/src/Schema/Migrator.php +++ b/src/Schema/Migrator.php @@ -240,18 +240,22 @@ public function field(string $fieldName, array $options = []): self $refType = $options['ref_type'] ?? self::REF_TYPE_NONE; unset($options['ref_type']); + if ($type === 'integer') { + $type = 'bigint'; + } + $column = $this->table->addColumn($this->getDatabasePlatform()->quoteSingleIdentifier($fieldName), $type); if (($options['nullable'] ?? true) && $refType !== self::REF_TYPE_PRIMARY) { $column->setNotnull(false); } - if ($type === 'integer' && $refType !== self::REF_TYPE_NONE) { + if ($type === 'bigint' && $refType !== self::REF_TYPE_NONE) { $column->setUnsigned(true); } // TODO remove, hack for createForeignKey so ID columns are unsigned - if ($type === 'integer' && str_ends_with($fieldName, '_id')) { + if ($type === 'bigint' && str_ends_with($fieldName, '_id')) { $column->setUnsigned(true); } diff --git a/tests/Schema/MigratorTest.php b/tests/Schema/MigratorTest.php index 12201dfb0..fb38afd12 100644 --- a/tests/Schema/MigratorTest.php +++ b/tests/Schema/MigratorTest.php @@ -339,6 +339,10 @@ public function testIntrospectTableToModelBasic(): void ]; } + // "integer" DBAL type in Migrator is mapped to "bigint" + $expectedFields['id']['type'] = 'bigint'; + $expectedFields['bar']['type'] = 'bigint'; + // TODO fix DBAL column comment type hint // see PlatformFixColumnCommentTypeHintTrait trait used for MSSQL and Oracle platforms if ($this->getDatabasePlatform() instanceof SQLitePlatform) { From 44667097f21850d10d80f7704b680480f471e2d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Fri, 24 May 2024 15:27:16 +0200 Subject: [PATCH 04/19] fix "bigint" to int php type typecasting --- src/Persistence.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Persistence.php b/src/Persistence.php index 9e0eb20cf..3b2231dbb 100644 --- a/src/Persistence.php +++ b/src/Persistence.php @@ -507,6 +507,7 @@ protected function _typecastSaveField(Field $field, $value) } $res = Type::getType($field->type)->convertToDatabaseValue($value, $this->getDatabasePlatform()); + if (is_resource($res) && get_resource_type($res) === 'stream') { $res = stream_get_contents($res); } @@ -556,7 +557,10 @@ protected function _typecastLoadField(Field $field, $value) } $res = Type::getType($field->type)->convertToPHPValue($value, $this->getDatabasePlatform()); - if (is_resource($res) && get_resource_type($res) === 'stream') { + + if ($field->type === 'bigint' && $res === (string) (int) $res) { // once DBAL 3.x support is dropped, it should no longer be needed + $res = (int) $res; + } elseif (is_resource($res) && get_resource_type($res) === 'stream') { $res = stream_get_contents($res); } From 3547a174d5d84e56591a5ec67a3052aca270ab75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Fri, 24 May 2024 14:52:27 +0200 Subject: [PATCH 05/19] fix Field::required for smallint/bigint --- src/Field.php | 2 ++ tests/FieldTest.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Field.php b/src/Field.php index 76a247515..d13f16669 100644 --- a/src/Field.php +++ b/src/Field.php @@ -178,7 +178,9 @@ public function normalize($value) } break; + case 'smallint': case 'integer': + case 'bigint': case 'float': case 'decimal': case 'atk4_money': diff --git a/tests/FieldTest.php b/tests/FieldTest.php index d6c5d64a8..2e3be5b99 100644 --- a/tests/FieldTest.php +++ b/tests/FieldTest.php @@ -159,7 +159,9 @@ public function testRequiredNumericZeroException(string $type): void */ public static function provideRequiredNumericZeroExceptionCases(): iterable { + yield ['smallint']; yield ['integer']; + yield ['bigint']; yield ['float']; yield ['decimal']; yield ['atk4_money']; From 242be9b26a2ca32b28320d2fe93afc0ec177ee5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Fri, 24 May 2024 16:03:25 +0200 Subject: [PATCH 06/19] add smallint/bigint "Must be numeric" ex --- src/Persistence.php | 2 ++ tests/FieldTest.php | 49 ++++++++++++++++----------------------------- 2 files changed, 19 insertions(+), 32 deletions(-) diff --git a/src/Persistence.php b/src/Persistence.php index 3b2231dbb..7161140dd 100644 --- a/src/Persistence.php +++ b/src/Persistence.php @@ -361,7 +361,9 @@ private function _typecastPreField(Field $field, $value, bool $fromLoad) switch ($field->type) { case 'boolean': + case 'smallint': case 'integer': + case 'bigint': case 'float': case 'decimal': case 'atk4_money': diff --git a/tests/FieldTest.php b/tests/FieldTest.php index 2e3be5b99..e72e5a5c0 100644 --- a/tests/FieldTest.php +++ b/tests/FieldTest.php @@ -672,32 +672,14 @@ public function testNormalizeStringBoolException(): void $m->set('foo', false); } - public function testNormalizeIntegerNumericException(): void - { - $m = new Model(); - $m->addField('foo', ['type' => 'integer']); - $m = $m->createEntity(); - - $this->expectException(ValidationException::class); - $this->expectExceptionMessage('Must be numeric'); - $m->set('foo', '1x'); - } - - public function testNormalizeFloatNumericException(): void - { - $m = new Model(); - $m->addField('foo', ['type' => 'float']); - $m = $m->createEntity(); - - $this->expectException(ValidationException::class); - $this->expectExceptionMessage('Must be numeric'); - $m->set('foo', '1x'); - } - - public function testNormalizeAtk4MoneyNumericException(): void + /** + * @dataProvider provideNormalizeNumericExceptionCases + */ + #[DataProvider('provideNormalizeNumericExceptionCases')] + public function testNormalizeNumericException(string $type): void { $m = new Model(); - $m->addField('foo', ['type' => 'atk4_money']); + $m->addField('foo', ['type' => $type]); $m = $m->createEntity(); $this->expectException(ValidationException::class); @@ -705,15 +687,18 @@ public function testNormalizeAtk4MoneyNumericException(): void $m->set('foo', '1x'); } - public function testNormalizeBooleanNumericException(): void + /** + * @return iterable> + */ + public static function provideNormalizeNumericExceptionCases(): iterable { - $m = new Model(); - $m->addField('foo', ['type' => 'boolean']); - $m = $m->createEntity(); - - $this->expectException(ValidationException::class); - $this->expectExceptionMessage('Must be numeric'); - $m->set('foo', '1x'); + yield ['boolean']; + yield ['smallint']; + yield ['integer']; + yield ['bigint']; + yield ['float']; + yield ['decimal']; + yield ['atk4_money']; } public function testNormalizeDateException(): void From 7a7b26dbe986167745592b8fb80a7dedbacdcf49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Fri, 24 May 2024 15:50:47 +0200 Subject: [PATCH 07/19] add smallint/bigint cast to null --- src/Persistence.php | 47 ++++++++++++++++++++++++--------------- tests/TypecastingTest.php | 10 +++++++++ 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/Persistence.php b/src/Persistence.php index 7161140dd..489037c55 100644 --- a/src/Persistence.php +++ b/src/Persistence.php @@ -359,21 +359,37 @@ private function _typecastPreField(Field $field, $value, bool $fromLoad) break; } - switch ($field->type) { - case 'boolean': - case 'smallint': - case 'integer': - case 'bigint': - case 'float': - case 'decimal': - case 'atk4_money': - if ($value === '') { + if ($value === '') { + // TODO should be handled by DBAL types itself like "json" type already does + // https://github.com/doctrine/dbal/blob/4.0.2/src/Types/JsonType.php#L55 + switch ($field->type) { + case 'boolean': + case 'smallint': + case 'integer': + case 'bigint': + case 'float': + case 'decimal': + case 'atk4_money': + case 'object': $value = null; - } elseif (!is_numeric($value)) { - throw new Exception('Must be numeric'); - } - break; + break; + } + } else { + switch ($field->type) { + case 'boolean': + case 'smallint': + case 'integer': + case 'bigint': + case 'float': + case 'decimal': + case 'atk4_money': + if (!is_numeric($value)) { + throw new Exception('Must be numeric'); + } + + break; + } } } elseif ($value !== null) { switch ($field->type) { @@ -529,11 +545,6 @@ protected function _typecastLoadField(Field $field, $value) { $value = $this->_typecastPreField($field, $value, true); - // TODO casting optionally to null should be handled by type itself solely - if ($value === '' && in_array($field->type, ['boolean', 'integer', 'float', 'decimal', 'datetime', 'date', 'time', 'json', 'object'], true)) { - return null; - } - // native DBAL DT types have no microseconds support if (in_array($field->type, ['datetime', 'date', 'time'], true) && str_starts_with(get_class(Type::getType($field->type)), 'Doctrine\DBAL\Types\\')) { diff --git a/tests/TypecastingTest.php b/tests/TypecastingTest.php index 262f58368..12fc4b1e1 100644 --- a/tests/TypecastingTest.php +++ b/tests/TypecastingTest.php @@ -156,7 +156,9 @@ public function testEmptyValues(): void 'datetime' => '', 'time' => '', 'boolean' => '', + 'smallint' => '', 'integer' => '', + 'bigint' => '', 'money' => '', 'float' => '', 'decimal' => '', @@ -177,7 +179,9 @@ public function testEmptyValues(): void $m->addField('datetime', ['type' => 'datetime']); $m->addField('time', ['type' => 'time']); $m->addField('boolean', ['type' => 'boolean']); + $m->addField('smallint', ['type' => 'smallint']); $m->addField('integer', ['type' => 'integer']); + $m->addField('bigint', ['type' => 'bigint']); $m->addField('money', ['type' => 'atk4_money']); $m->addField('float', ['type' => 'float']); $m->addField('decimal', ['type' => 'decimal']); @@ -192,7 +196,9 @@ public function testEmptyValues(): void self::assertNull($mm->get('datetime')); self::assertNull($mm->get('time')); self::assertNull($mm->get('boolean')); + self::assertNull($mm->get('smallint')); self::assertNull($mm->get('integer')); + self::assertNull($mm->get('bigint')); self::assertNull($mm->get('money')); self::assertNull($mm->get('float')); self::assertNull($mm->get('decimal')); @@ -210,7 +216,9 @@ public function testEmptyValues(): void self::assertNull($mm->get('datetime')); self::assertNull($mm->get('time')); self::assertNull($mm->get('boolean')); + self::assertNull($mm->get('smallint')); self::assertNull($mm->get('integer')); + self::assertNull($mm->get('bigint')); self::assertNull($mm->get('money')); self::assertNull($mm->get('float')); self::assertNull($mm->get('decimal')); @@ -241,7 +249,9 @@ public function testEmptyValues(): void 'datetime' => null, 'time' => null, 'boolean' => null, + 'smallint' => null, 'integer' => null, + 'bigint' => null, 'money' => null, 'float' => null, 'decimal' => null, From 6418ba8daf434bfcd868a8b5c85132f8bf384737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Fri, 24 May 2024 16:04:23 +0200 Subject: [PATCH 08/19] add "Must be true" test --- tests/FieldTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/FieldTest.php b/tests/FieldTest.php index e72e5a5c0..5ca8904a0 100644 --- a/tests/FieldTest.php +++ b/tests/FieldTest.php @@ -167,6 +167,17 @@ public static function provideRequiredNumericZeroExceptionCases(): iterable yield ['atk4_money']; } + public function testRequiredBooleanZeroException(): void + { + $m = new Model(); + $m->addField('foo', ['type' => 'boolean', 'required' => true]); + $m = $m->createEntity(); + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Must be true'); + $m->set('foo', 0); + } + public function testNotNullableNullInsertException(): void { $this->setDb([ From 08ce766f7cf60f672dbd18dbc85399fdcaef0191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 28 May 2024 10:30:13 +0200 Subject: [PATCH 09/19] fix empty strong load DT types --- src/Persistence.php | 15 ++++++++++----- tests/TypecastingTest.php | 7 ------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/Persistence.php b/src/Persistence.php index 489037c55..97cf7f16a 100644 --- a/src/Persistence.php +++ b/src/Persistence.php @@ -340,7 +340,7 @@ public function typecastLoadRow(Model $model, array $row): array /** * @param mixed $value * - * @return ($value is scalar ? scalar : mixed) + * @return ($value is scalar ? scalar|null : mixed) */ private function _typecastPreField(Field $field, $value, bool $fromLoad) { @@ -370,6 +370,9 @@ private function _typecastPreField(Field $field, $value, bool $fromLoad) case 'float': case 'decimal': case 'atk4_money': + case 'datetime': + case 'date': + case 'time': case 'object': $value = null; @@ -505,8 +508,9 @@ protected function _typecastSaveField(Field $field, $value) } // native DBAL DT types have no microseconds support - if (in_array($field->type, ['datetime', 'date', 'time'], true) - && str_starts_with(get_class(Type::getType($field->type)), 'Doctrine\DBAL\Types\\')) { + if ($value !== null && in_array($field->type, ['datetime', 'date', 'time'], true) + && str_starts_with(get_class(Type::getType($field->type)), 'Doctrine\DBAL\Types\\') + ) { if ($value === '') { return null; } elseif (!$value instanceof \DateTimeInterface) { @@ -546,8 +550,9 @@ protected function _typecastLoadField(Field $field, $value) $value = $this->_typecastPreField($field, $value, true); // native DBAL DT types have no microseconds support - if (in_array($field->type, ['datetime', 'date', 'time'], true) - && str_starts_with(get_class(Type::getType($field->type)), 'Doctrine\DBAL\Types\\')) { + if ($value !== null && in_array($field->type, ['datetime', 'date', 'time'], true) + && str_starts_with(get_class(Type::getType($field->type)), 'Doctrine\DBAL\Types\\') + ) { $format = ['date' => 'Y-m-d', 'datetime' => 'Y-m-d H:i:s', 'time' => 'H:i:s'][$field->type]; if (str_contains($value, '.')) { // time possibly with microseconds, otherwise invalid format $format = preg_replace('~(?<=H:i:s)(?![. ]*u)~', '.u', $format); diff --git a/tests/TypecastingTest.php b/tests/TypecastingTest.php index 12fc4b1e1..3d4ab8424 100644 --- a/tests/TypecastingTest.php +++ b/tests/TypecastingTest.php @@ -147,7 +147,6 @@ public function testEmptyValues(): void $dbData = [ 'types' => [ - '_types' => ['date' => 'date', 'datetime' => 'datetime', 'time' => 'time', 'decimal' => 'decimal', 'json' => 'json'], 1 => $row = [ 'id' => 1, 'string' => '', @@ -230,13 +229,7 @@ public function testEmptyValues(): void $mm->save(); - unset($dbData['types']['_types']); $dbData['types'][1]['id'] = '1'; - $dbData['types'][1]['date'] = null; - $dbData['types'][1]['datetime'] = null; - $dbData['types'][1]['time'] = null; - $dbData['types'][1]['decimal'] = null; - $dbData['types'][1]['json'] = null; $dbData['types'] = [$dbData['types'][1]]; self::assertSame($fixEmptyStringForOracleFx($dbData['types']), $m->export(null, null, false)); From ee4293c33c02631c3073de704fcfe7a4086f0064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 28 May 2024 10:39:38 +0200 Subject: [PATCH 10/19] rm empty string to null save typecasting --- src/Persistence.php | 4 ---- tests/TypecastingTest.php | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Persistence.php b/src/Persistence.php index 97cf7f16a..de748d85d 100644 --- a/src/Persistence.php +++ b/src/Persistence.php @@ -503,10 +503,6 @@ protected function _typecastSaveField(Field $field, $value) { $value = $this->_typecastPreField($field, $value, false); - if (in_array($field->type, ['json', 'object'], true) && $value === '') { // TODO remove later - return null; - } - // native DBAL DT types have no microseconds support if ($value !== null && in_array($field->type, ['datetime', 'date', 'time'], true) && str_starts_with(get_class(Type::getType($field->type)), 'Doctrine\DBAL\Types\\') diff --git a/tests/TypecastingTest.php b/tests/TypecastingTest.php index 3d4ab8424..233eeab06 100644 --- a/tests/TypecastingTest.php +++ b/tests/TypecastingTest.php @@ -206,7 +206,8 @@ public function testEmptyValues(): void self::assertNull($mm->get('local-object')); unset($row['id']); - unset($row['local-object']); + $row['json'] = null; + $row['local-object'] = null; $mm->setMulti($row); self::assertSame($fixEmptyStringForOracleFx(''), $mm->get('string')); From 15c6d556f08262521dd2b163dffd107e09743774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 28 May 2024 10:50:29 +0200 Subject: [PATCH 11/19] minor variable name refactor --- src/Persistence/Static_.php | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Persistence/Static_.php b/src/Persistence/Static_.php index ee27d3408..b3223486e 100644 --- a/src/Persistence/Static_.php +++ b/src/Persistence/Static_.php @@ -44,28 +44,28 @@ public function __construct(array $data = []) } 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 (is_array($v)) { - $fieldType = 'json'; + $type = 'json'; } elseif (is_object($v)) { - $fieldType = 'object'; + $type = 'object'; } 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'; } } @@ -79,8 +79,8 @@ public function __construct(array $data = []) $defTypes = []; $keyOverride = []; $mustOverride = false; - foreach ($fieldTypes as $k => $fieldType) { - $defTypes[$k] = ['type' => $fieldType]; + foreach ($fieldTypes as $k => $type) { + $defTypes[$k] = ['type' => $type]; // id information present, use it instead if ($k === 'id') { From c5ee7e1db38de20f93f2b733a1ad98c1bd662690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 28 May 2024 10:58:52 +0200 Subject: [PATCH 12/19] fix whitespace/comma removal for smallint/bigint --- src/Persistence.php | 2 ++ tests/FieldTest.php | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/src/Persistence.php b/src/Persistence.php index de748d85d..b105c868a 100644 --- a/src/Persistence.php +++ b/src/Persistence.php @@ -347,7 +347,9 @@ private function _typecastPreField(Field $field, $value, bool $fromLoad) if (is_string($value)) { switch ($field->type) { case 'boolean': + case 'smallint': case 'integer': + case 'bigint': $value = preg_replace('~\s+|,~', '', $value); break; diff --git a/tests/FieldTest.php b/tests/FieldTest.php index 5ca8904a0..33fd7066a 100644 --- a/tests/FieldTest.php +++ b/tests/FieldTest.php @@ -613,7 +613,9 @@ public function testNormalize(): void $m->addField('string', ['type' => 'string']); $m->addField('text', ['type' => 'text']); + $m->addField('smallint', ['type' => 'smallint']); $m->addField('integer', ['type' => 'integer']); + $m->addField('bigint', ['type' => 'bigint']); $m->addField('money', ['type' => 'atk4_money']); $m->addField('float', ['type' => 'float']); $m->addField('boolean', ['type' => 'boolean']); @@ -645,9 +647,15 @@ public function testNormalize(): void self::assertSame("Two\nLines", $m->get('text')); // integer, money, float + $m->set('smallint', '12,345.67676767'); + self::assertSame(12345, $m->get('smallint')); + $m->set('integer', '12,345.67676767'); self::assertSame(12345, $m->get('integer')); + $m->set('bigint', '12,345.67676767'); + self::assertSame(12345, (int) $m->get('bigint')); // once DBAL 3.x support is dropped, the explicit cast should no longer be needed + $m->set('money', '12,345.67676767'); self::assertSame(12345.6768, $m->get('money')); From cddb296d0638d5c0de17424d563eaa5eda5f6070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 28 May 2024 11:07:08 +0200 Subject: [PATCH 13/19] fix "must be scalar" ex for smallint/bigint --- src/Persistence.php | 7 ++++++- tests/FieldTest.php | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/Persistence.php b/src/Persistence.php index b105c868a..58ec8bbac 100644 --- a/src/Persistence.php +++ b/src/Persistence.php @@ -400,12 +400,17 @@ private function _typecastPreField(Field $field, $value, bool $fromLoad) switch ($field->type) { case 'string': case 'text': + case 'boolean': + case 'smallint': case 'integer': + case 'bigint': case 'float': case 'decimal': case 'atk4_money': if (is_bool($value)) { - throw new Exception('Must not be bool type'); + if ($field->type !== 'boolean') { + throw new Exception('Must not be bool type'); + } } elseif (is_int($value)) { if ($fromLoad) { $value = (string) $value; diff --git a/tests/FieldTest.php b/tests/FieldTest.php index 33fd7066a..806a19554 100644 --- a/tests/FieldTest.php +++ b/tests/FieldTest.php @@ -680,6 +680,35 @@ public function testNormalizeStringArrayException(): void $m->set('foo', []); } + /** + * @dataProvider provideNormalizeNumericNonScalarExceptionCases + */ + #[DataProvider('provideNormalizeNumericNonScalarExceptionCases')] + public function testNormalizeNumericNonScalarException(string $type): void + { + $m = new Model(); + $m->addField('foo', ['type' => $type]); + $m = $m->createEntity(); + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Must be scalar'); + $m->set('foo', []); + } + + /** + * @return iterable> + */ + public static function provideNormalizeNumericNonScalarExceptionCases(): iterable + { + yield ['boolean']; + yield ['smallint']; + yield ['integer']; + yield ['bigint']; + yield ['float']; + yield ['decimal']; + yield ['atk4_money']; + } + public function testNormalizeStringBoolException(): void { $m = new Model(); From a9cbb9349fed946b09364ad04f02910f40967316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 29 May 2024 09:59:54 +0200 Subject: [PATCH 14/19] imply "bigint" from "int" php type --- src/Persistence/Static_.php | 2 +- src/Schema/TestCase.php | 4 ++-- tests/Persistence/StaticTest.php | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Persistence/Static_.php b/src/Persistence/Static_.php index b3223486e..de165d7cc 100644 --- a/src/Persistence/Static_.php +++ b/src/Persistence/Static_.php @@ -46,7 +46,7 @@ public function __construct(array $data = []) if (is_bool($v)) { $type = 'boolean'; } elseif (is_int($v)) { - $type = 'integer'; + $type = 'bigint'; } elseif (is_float($v)) { $type = 'float'; } elseif ($v instanceof \DateTimeInterface) { diff --git a/src/Schema/TestCase.php b/src/Schema/TestCase.php index 6855039c0..d913ac58a 100644 --- a/src/Schema/TestCase.php +++ b/src/Schema/TestCase.php @@ -327,7 +327,7 @@ public function setDb(array $dbData, bool $importData = true): void if (is_bool($v)) { $type = 'boolean'; } elseif (is_int($v)) { - $type = 'integer'; + $type = 'bigint'; } elseif (is_float($v)) { $type = 'float'; } elseif ($v !== null) { @@ -345,7 +345,7 @@ public function setDb(array $dbData, bool $importData = true): void } } if (!isset($fieldTypes[$idField])) { - $fieldTypes = array_merge([$idField => 'integer'], $fieldTypes); + $fieldTypes = array_merge([$idField => 'bigint'], $fieldTypes); } $model = new Model(null, ['table' => $tableName, 'idField' => $idField]); diff --git a/tests/Persistence/StaticTest.php b/tests/Persistence/StaticTest.php index abd885408..35b84e5c5 100644 --- a/tests/Persistence/StaticTest.php +++ b/tests/Persistence/StaticTest.php @@ -150,7 +150,7 @@ public function testFieldTypes(): void $m = new Model($p); self::assertSame('integer', $m->getField('id')->type); - self::assertSame('integer', $m->getField('test_int')->type); + self::assertSame('bigint', $m->getField('test_int')->type); self::assertSame('float', $m->getField('test_float')->type); // self::assertSame('datetime', $m->getField('test_date')->type); // self::assertSame('json', $m->getField('test_array')->type); @@ -168,7 +168,7 @@ public function testFieldTypesBasicInteger(): void $p = new Persistence\Static_([1 => 'hello', 'world']); $m = new Model($p); - self::assertSame('integer', $m->getField('id')->type); + self::assertSame('bigint', $m->getField('id')->type); self::assertSame('string', $m->getField('name')->type); } @@ -187,7 +187,7 @@ public function testFieldTypesIntegerNullFirst(): void $m = new Model($p); self::assertSame('integer', $m->getField('id')->type); - self::assertSame('integer', $m->getField('foo')->type); + self::assertSame('bigint', $m->getField('foo')->type); } public function testFieldTypesBasicIntegerNullFirst(): void @@ -195,8 +195,8 @@ public function testFieldTypesBasicIntegerNullFirst(): void $p = new Persistence\Static_([1 => null, 2 => 1, 3 => null]); $m = new Model($p); - self::assertSame('integer', $m->getField('id')->type); - self::assertSame('integer', $m->getField('name')->type); + self::assertSame('bigint', $m->getField('id')->type); + self::assertSame('bigint', $m->getField('name')->type); } } From 2e73861a9aee74094a3cd7d0bb54bbf5ae711aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 29 May 2024 10:26:20 +0200 Subject: [PATCH 15/19] create implicit Model ID field with "bigint" type --- docs/aggregates.md | 2 +- docs/joins.md | 4 ++-- docs/persistence.md | 2 +- docs/references.md | 4 ++-- docs/typecasting.md | 22 +++++++++++----------- src/Model.php | 2 +- src/Persistence/Array_.php | 6 ++++-- tests/JoinSqlTest.php | 6 +++--- tests/Persistence/StaticTest.php | 4 ++-- tests/ReferenceSqlTest.php | 16 ++++++++-------- tests/ReferenceTest.php | 12 ++++++------ 11 files changed, 41 insertions(+), 39 deletions(-) diff --git a/docs/aggregates.md b/docs/aggregates.md index 3bea1963c..293f640d6 100644 --- a/docs/aggregates.md +++ b/docs/aggregates.md @@ -25,7 +25,7 @@ in various ways to fine-tune aggregation. Below is one sample use: $aggregate = new AggregateModel($orders); $aggregate->addField('country'); $aggregate->setGroupBy(['country_id'], [ - 'count' => ['expr' => 'count(*)', 'type' => 'integer'], + 'count' => ['expr' => 'count(*)', 'type' => 'bigint'], 'total_amount' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], ], ); diff --git a/docs/joins.md b/docs/joins.md index 6d1e2a746..c8a5193f2 100644 --- a/docs/joins.md +++ b/docs/joins.md @@ -67,7 +67,7 @@ but you wouldn't want that adding a new user would create a new country: ``` $user->addField('username'); -$user->addField('country_id', ['type' => 'integer']); +$user->addField('country_id', ['type' => 'bigint']); $jCountry = $user->join('country', ['weak' => true, 'prefix' => 'country_']); $jCountry->addField('code'); $jCountry->addField('name'); @@ -110,7 +110,7 @@ $jCreditCard = $user->join('credit_card', [ 'prefix' => 'cc_', 'masterField' => 'default_credit_card_id', ]); -$jCreditCard->addField('integer'); // creates cc_number +$jCreditCard->addField('bigint'); // creates cc_number $jCreditCard->addField('name'); // creates cc_name ``` diff --git a/docs/persistence.md b/docs/persistence.md index 1f6a04c38..d77fa551f 100644 --- a/docs/persistence.md +++ b/docs/persistence.md @@ -664,7 +664,7 @@ $m->onHook(Model::HOOK_BEFORE_SAVE, function (Model $entity) { $arc = $this->withPersistence($entity->getApp()->archive_db); // add some audit fields - $arc->addField('original_id', ['type' => 'integer'])->set($this->getId()); + $arc->addField('original_id', ['type' => 'bigint'])->set($this->getId()); $arc->addField('saved_by')->set($this->getApp()->user); $arc->saveAndUnload(); diff --git a/docs/references.md b/docs/references.md index ff67d1511..2131932e6 100644 --- a/docs/references.md +++ b/docs/references.md @@ -364,7 +364,7 @@ select * from user where id in By passing options to hasOne() you can also differentiate field name: ``` -$o->addField('user_id', ['type' => 'integer']); +$o->addField('user_id', ['type' => 'bigint']); $o->hasOne('User', ['model' => $u, 'ourField' => 'user_id']); $o->load(1)->ref('User')['name']; @@ -492,7 +492,7 @@ No condition will be applied by default so it's all up to you: $m->addReference('Archive', ['model' => static function (Persistence $persistence) use ($m) { $archive = new $m(null, ['table' => $m->table . '_archive']); - $m->addField('original_id', ['type' => 'integer']); + $m->addField('original_id', ['type' => 'bigint']); if ($m->isLoaded())) { $archive->addCondition('original_id', $m->getId()); diff --git a/docs/typecasting.md b/docs/typecasting.md index 903e1041a..2d4c919b5 100644 --- a/docs/typecasting.md +++ b/docs/typecasting.md @@ -101,17 +101,17 @@ a different type. ### Supported types -- 'string' - for storing short strings, such as name of a person. Normalize will trim the value. -- 'text' - for storing long strings, suchas notes or description. Normalize will trim the value. -- 'boolean' - normalize will cast value to boolean. -- 'integer' - normalize will cast value to integer. -- 'atk4_money' - normalize will round value with 4 digits after dot. -- 'float' - normalize will cast value to float. -- 'date' - normalize will convert value to DateTime object. -- 'datetime' - normalize will convert value to DateTime object. -- 'time' - normalize will convert value to DateTime object. -- 'json' - no normalization by default -- 'object' - no normalization by default +- `string` - for storing short strings, such as name of a person. Normalize will trim the value. +- `text` - for storing long strings, suchas notes or description. Normalize will trim the value. +- `boolean` - normalize will cast value to boolean. +- `smallint`, `integer`, `bigint` - normalize will cast value to integer. +- `atk4_money` - normalize will round value with 4 digits after dot. +- `float` - normalize will cast value to float. +- `date` - normalize will convert value to DateTime object. +- `datetime` - normalize will convert value to DateTime object. +- `time` - normalize will convert value to DateTime object. +- `json` - no normalization by default +- `object` - no normalization by default ### Types and UI diff --git a/src/Model.php b/src/Model.php index 2e47e6eaa..7d9624d31 100644 --- a/src/Model.php +++ b/src/Model.php @@ -385,7 +385,7 @@ protected function init(): void if ($this->idField) { if (!$this->hasField($this->idField)) { - $this->addField($this->idField, ['type' => 'integer']); + $this->addField($this->idField, ['type' => 'bigint']); } $this->getIdField()->required = true; $this->getIdField()->system = true; diff --git a/src/Persistence/Array_.php b/src/Persistence/Array_.php index d4ab2ae52..2f6d3f1b6 100644 --- a/src/Persistence/Array_.php +++ b/src/Persistence/Array_.php @@ -306,10 +306,12 @@ public function generateNewId(Model $model) $type = $model->idField ? $model->getIdField()->type - : 'integer'; + : 'bigint'; switch ($type) { + case 'smallint': case 'integer': + case 'bigint': $nextId = ($this->maxSeenIdByTable[$model->table] ?? 0) + 1; $this->maxSeenIdByTable[$model->table] = $nextId; @@ -319,7 +321,7 @@ public function generateNewId(Model $model) break; default: - throw (new Exception('Unsupported id field type. Array supports type=integer or type=string only')) + throw (new Exception('Unsupported ID field type')) ->addMoreInfo('type', $type); } diff --git a/tests/JoinSqlTest.php b/tests/JoinSqlTest.php index 7315ba7d9..9443ac206 100644 --- a/tests/JoinSqlTest.php +++ b/tests/JoinSqlTest.php @@ -664,7 +664,7 @@ public function testJoinHasOneHasMany(): void // main user model joined to contact table $user = new Model($this->db, ['table' => 'user']); $user->addField('name'); - $user->addField('contact_id', ['type' => 'integer']); + $user->addField('contact_id', ['type' => 'bigint']); $j = $user->join('contact'); $this->createMigrator()->createForeignKey($j); @@ -689,7 +689,7 @@ public function testJoinHasOneHasMany(): void // hasMany token model (uses default ourField, theirField) $token = new Model($this->db, ['table' => 'token']); - $token->addField('user_id', ['type' => 'integer']); + $token->addField('user_id', ['type' => 'bigint']); $token->addField('token'); $refMany = $j->hasMany('Token', ['model' => $token]); // hasMany on JOIN (use default ourField, theirField) $this->createMigrator()->createForeignKey($refMany); @@ -702,7 +702,7 @@ public function testJoinHasOneHasMany(): void // hasMany email model (uses custom ourField, theirField) $email = new Model($this->db, ['table' => 'email']); - $email->addField('contact_id', ['type' => 'integer']); + $email->addField('contact_id', ['type' => 'bigint']); $email->addField('address'); $refMany = $j->hasMany('Email', ['model' => $email, 'ourField' => 'contact_id', 'theirField' => 'contact_id']); // hasMany on JOIN (use custom ourField, theirField) $this->createMigrator()->createForeignKey($refMany); diff --git a/tests/Persistence/StaticTest.php b/tests/Persistence/StaticTest.php index 35b84e5c5..3739a95a2 100644 --- a/tests/Persistence/StaticTest.php +++ b/tests/Persistence/StaticTest.php @@ -149,7 +149,7 @@ public function testFieldTypes(): void ]]); $m = new Model($p); - self::assertSame('integer', $m->getField('id')->type); + self::assertSame('bigint', $m->getField('id')->type); self::assertSame('bigint', $m->getField('test_int')->type); self::assertSame('float', $m->getField('test_float')->type); // self::assertSame('datetime', $m->getField('test_date')->type); @@ -186,7 +186,7 @@ public function testFieldTypesIntegerNullFirst(): void $p = new Persistence\Static_([1 => ['foo' => null], 2 => ['foo' => 1], 3 => ['foo' => null]]); $m = new Model($p); - self::assertSame('integer', $m->getField('id')->type); + self::assertSame('bigint', $m->getField('id')->type); self::assertSame('bigint', $m->getField('foo')->type); } diff --git a/tests/ReferenceSqlTest.php b/tests/ReferenceSqlTest.php index db33c6d65..2fa868b02 100644 --- a/tests/ReferenceSqlTest.php +++ b/tests/ReferenceSqlTest.php @@ -41,7 +41,7 @@ public function testBasic(): void $o = new Model($this->db, ['table' => 'order']); $o->addField('amount', ['type' => 'integer']); - $o->addField('user_id', ['type' => 'integer']); + $o->addField('user_id', ['type' => 'bigint']); $u->hasMany('Orders', ['model' => $o]); @@ -76,7 +76,7 @@ public function testRefLink(): void $o = new Model($this->db, ['table' => 'order']); $o->addField('amount'); - $o->addField('user_id', ['type' => 'integer']); + $o->addField('user_id', ['type' => 'bigint']); $u->hasMany('Orders', ['model' => $o]); @@ -261,7 +261,7 @@ public function testRelatedExpression(): void $i->addField('ref_no'); $l = new Model($this->db, ['table' => 'invoice_line']); - $l->addField('invoice_id', ['type' => 'integer']); + $l->addField('invoice_id', ['type' => 'bigint']); $l->addField('total_net'); $l->addField('total_vat'); $l->addField('total_gross'); @@ -454,7 +454,7 @@ public function testAggregateHasMany(): void $i->addField('ref_no'); $l = new Model($this->db, ['table' => 'invoice_line']); - $l->addField('invoice_id', ['type' => 'integer']); + $l->addField('invoice_id', ['type' => 'bigint']); $l->addField('total_net', ['type' => 'atk4_money']); $l->addField('total_vat', ['type' => 'atk4_money']); $l->addField('total_gross', ['type' => 'atk4_money']); @@ -524,7 +524,7 @@ public function testOtherAggregates(): void $l->addField('name'); $i = new Model($this->db, ['table' => 'item']); - $i->addField('list_id', ['type' => 'integer']); + $i->addField('list_id', ['type' => 'bigint']); $i->addField('name'); $i->addField('code'); @@ -581,7 +581,7 @@ protected function setupDbForTraversing(): Model $user = new Model($this->db, ['table' => 'user']); $user->addField('name'); - $user->addField('company_id', ['type' => 'integer']); + $user->addField('company_id', ['type' => 'bigint']); $company = new Model($this->db, ['table' => 'company']); $company->addField('name'); @@ -589,7 +589,7 @@ protected function setupDbForTraversing(): Model $user->hasOne('Company', ['model' => $company, 'ourField' => 'company_id', 'theirField' => 'id']); $order = new Model($this->db, ['table' => 'order']); - $order->addField('company_id', ['type' => 'integer']); + $order->addField('company_id', ['type' => 'bigint']); $order->addField('description'); $order->addField('amount', ['default' => 20, 'type' => 'float']); @@ -709,7 +709,7 @@ public function testHasOneIdFieldAsOurField(): void $s = (new Model($this->db, ['table' => 'stadium'])); $s->addField('name'); - $s->addField('player_id', ['type' => 'integer']); + $s->addField('player_id', ['type' => 'bigint']); $p = new Model($this->db, ['table' => 'player']); $p->addField('name'); diff --git a/tests/ReferenceTest.php b/tests/ReferenceTest.php index 5ca3c303c..975892930 100644 --- a/tests/ReferenceTest.php +++ b/tests/ReferenceTest.php @@ -22,7 +22,7 @@ public function testBasicReferences(): void $order = new Model(null, ['table' => 'order']); $order->addField('amount', ['default' => 20]); - $order->addField('user_id', ['type' => 'integer']); + $order->addField('user_id', ['type' => 'bigint']); $r1 = $user->getModel()->hasMany('Orders', ['model' => $order, 'caption' => 'My Orders']); $o = $user->ref('Orders')->createEntity(); @@ -33,7 +33,7 @@ public function testBasicReferences(): void $r2 = $user->getModel()->hasMany('BigOrders', ['model' => static function () { $m = new Model(null, ['table' => 'big_order']); $m->addField('amount', ['default' => 100]); - $m->addField('user_id', ['type' => 'integer']); + $m->addField('user_id', ['type' => 'bigint']); return $m; }]); @@ -59,7 +59,7 @@ public function testModelCaption(): void $order = new Model(null, ['table' => 'order']); $order->addField('amount', ['default' => 20]); - $order->addField('user_id', ['type' => 'integer']); + $order->addField('user_id', ['type' => 'bigint']); $user->getModel()->hasMany('Orders', ['model' => $order, 'caption' => 'My Orders']); @@ -127,7 +127,7 @@ public function testTheirFieldNameGuessTableWithSchema(): void { $user = new Model($this->db, ['table' => 'db1.user']); $order = new Model($this->db, ['table' => 'db2.orders']); - $order->addField('user_id', ['type' => 'integer']); + $order->addField('user_id', ['type' => 'bigint']); $user->hasMany('Orders', ['model' => $order, 'caption' => 'My Orders']); self::assertSame('user_id', $user->getReference('Orders')->getTheirFieldName()); @@ -186,7 +186,7 @@ protected function init(): void self::assertSame(['id' => 'date'], array_map(static fn (Field $f) => $f->type, $userModel->getFields())); self::assertSame(['id' => 'date'], array_map(static fn (Field $f) => $f->type, $order->ref('a')->getFields())); self::assertSame(['id' => 'date', 'x' => 'float'], array_map(static fn (Field $f) => $f->type, $order->ref('b')->getFields())); - self::assertSame(['id' => 'integer', 'user_id' => 'date', 'a' => 'date', 'b' => 'float'], array_map(static fn (Field $f) => $f->type, $order->getFields())); + self::assertSame(['id' => 'bigint', 'user_id' => 'date', 'a' => 'date', 'b' => 'float'], array_map(static fn (Field $f) => $f->type, $order->getFields())); } public function testRefTypeMismatchWithDisabledCheck(): void @@ -199,7 +199,7 @@ public function testRefTypeMismatchWithDisabledCheck(): void self::assertSame('user', $order->ref('placed_by')->table); self::assertSame('string', $order->getField('placed_by_user_id')->type); - self::assertSame('integer', $order->ref('placed_by')->getIdField()->type); + self::assertSame('bigint', $order->ref('placed_by')->getIdField()->type); } public function testCreateTheirModelMissingModelSeedException(): void From 9606f6de7ce6b411d818666a934bcde798931407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 29 May 2024 10:38:33 +0200 Subject: [PATCH 16/19] do not imply "bigint" from "integer" implicitly in Migrator --- src/Schema/Migrator.php | 10 +++------- tests/JoinSqlTest.php | 18 +++++++++--------- tests/Schema/MigratorTest.php | 4 ---- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/Schema/Migrator.php b/src/Schema/Migrator.php index d2ebf63c3..328bac475 100644 --- a/src/Schema/Migrator.php +++ b/src/Schema/Migrator.php @@ -240,22 +240,18 @@ public function field(string $fieldName, array $options = []): self $refType = $options['ref_type'] ?? self::REF_TYPE_NONE; unset($options['ref_type']); - if ($type === 'integer') { - $type = 'bigint'; - } - $column = $this->table->addColumn($this->getDatabasePlatform()->quoteSingleIdentifier($fieldName), $type); if (($options['nullable'] ?? true) && $refType !== self::REF_TYPE_PRIMARY) { $column->setNotnull(false); } - if ($type === 'bigint' && $refType !== self::REF_TYPE_NONE) { + if (in_array($type, ['smallint', 'integer', 'bigint'], true) && $refType !== self::REF_TYPE_NONE) { $column->setUnsigned(true); } // TODO remove, hack for createForeignKey so ID columns are unsigned - if ($type === 'bigint' && str_ends_with($fieldName, '_id')) { + if (in_array($type, ['smallint', 'integer', 'bigint'], true) && str_ends_with($fieldName, '_id')) { $column->setUnsigned(true); } @@ -279,7 +275,7 @@ public function field(string $fieldName, array $options = []): self public function id(string $name = 'id', array $options = []): self { $options = array_merge([ - 'type' => 'integer', + 'type' => 'bigint', 'ref_type' => self::REF_TYPE_PRIMARY, 'nullable' => false, ], $options); diff --git a/tests/JoinSqlTest.php b/tests/JoinSqlTest.php index 9443ac206..43d189fd0 100644 --- a/tests/JoinSqlTest.php +++ b/tests/JoinSqlTest.php @@ -107,14 +107,14 @@ public function testJoinSaving1(): void $user = new Model($this->db, ['table' => 'user']); $this->setDb([ 'user' => [ - '_types' => ['name' => 'string', 'contact_id' => 'integer'], + '_types' => ['name' => 'string', 'contact_id' => 'bigint'], ], 'contact' => [ '_types' => ['contact_phone' => 'string'], ], ]); - $user->addField('contact_id', ['type' => 'integer']); + $user->addField('contact_id', ['type' => 'bigint']); $user->addField('name'); $j = $user->join('contact'); $this->assertMigratorResolveRelation('user.contact_id', 'contact.id', $j); @@ -164,7 +164,7 @@ public function testJoinSaving2(): void '_types' => ['name' => 'string'], ], 'contact' => [ - '_types' => ['contact_phone' => 'string', 'test_id' => 'integer'], + '_types' => ['contact_phone' => 'string', 'test_id' => 'bigint'], ], ]); @@ -234,7 +234,7 @@ public function testJoinSaving3(): void $user = new Model($this->db, ['table' => 'user']); $this->setDb([ 'user' => [ - '_types' => ['name' => 'string', 'test_id' => 'integer'], + '_types' => ['name' => 'string', 'test_id' => 'bigint'], ], 'contact' => [ '_types' => ['contact_phone' => 'string'], @@ -315,7 +315,7 @@ public function testJoinUpdate(): void ]); $user = new Model($this->db, ['table' => 'user']); - $user->addField('contact_id', ['type' => 'integer']); + $user->addField('contact_id', ['type' => 'bigint']); $user->addField('name'); $j = $user->join('contact'); $this->createMigrator()->createForeignKey($j); @@ -410,7 +410,7 @@ public function testJoinDelete(): void ]); $user = new Model($this->db, ['table' => 'user']); - $user->addField('contact_id', ['type' => 'integer']); + $user->addField('contact_id', ['type' => 'bigint']); $user->addField('name'); $j = $user->join('contact'); $this->createMigrator()->createForeignKey($j); @@ -467,7 +467,7 @@ public function testDoubleSaveHook(): void '_types' => ['name' => 'string'], ], 'contact' => [ - '_types' => ['contact_phone' => 'string', 'test_id' => 'integer'], + '_types' => ['contact_phone' => 'string', 'test_id' => 'bigint'], ], ]); @@ -521,7 +521,7 @@ public function testDoubleJoin(): void ]); $user = new Model($this->db, ['table' => 'user']); - $user->addField('contact_id', ['type' => 'integer']); + $user->addField('contact_id', ['type' => 'bigint']); $user->addField('name'); $jContact = $user->join('contact'); $this->assertMigratorResolveRelation('user.contact_id', 'contact.id', $jContact); @@ -810,7 +810,7 @@ public function testJoinActualFieldNamesAndPrefix(): void ]); $user = new Model($this->db, ['table' => 'user']); - $user->addField('contact_id', ['type' => 'integer', 'actual' => $contactForeignIdFieldName]); + $user->addField('contact_id', ['type' => 'bigint', 'actual' => $contactForeignIdFieldName]); $user->addField('name', ['actual' => 'first_name']); // normal join $j = $user->join('contact', ['prefix' => 'j1_']); diff --git a/tests/Schema/MigratorTest.php b/tests/Schema/MigratorTest.php index fb38afd12..12201dfb0 100644 --- a/tests/Schema/MigratorTest.php +++ b/tests/Schema/MigratorTest.php @@ -339,10 +339,6 @@ public function testIntrospectTableToModelBasic(): void ]; } - // "integer" DBAL type in Migrator is mapped to "bigint" - $expectedFields['id']['type'] = 'bigint'; - $expectedFields['bar']['type'] = 'bigint'; - // TODO fix DBAL column comment type hint // see PlatformFixColumnCommentTypeHintTrait trait used for MSSQL and Oracle platforms if ($this->getDatabasePlatform() instanceof SQLitePlatform) { From c5b7b5aa16ac437541464e1707149c17bfb3de2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 29 May 2024 11:33:48 +0200 Subject: [PATCH 17/19] create implicit Join ID field with "bigint" type --- src/Model/Join.php | 2 +- src/Persistence/Sql/Join.php | 4 ++-- tests/JoinArrayTest.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Model/Join.php b/src/Model/Join.php index 679b8c840..d9ba26565 100644 --- a/src/Model/Join.php +++ b/src/Model/Join.php @@ -143,7 +143,7 @@ protected function createFakeForeignModel(): Model } } if ($fakeModel->idField !== $this->foreignField) { - $fakeModel->addField($this->foreignField, ['type' => 'integer']); + $fakeModel->addField($this->foreignField, ['type' => 'bigint']); } return $fakeModel; diff --git a/src/Persistence/Sql/Join.php b/src/Persistence/Sql/Join.php index 35dad6b38..3428f68a2 100644 --- a/src/Persistence/Sql/Join.php +++ b/src/Persistence/Sql/Join.php @@ -33,11 +33,11 @@ protected function init(): void // 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]); + $field = $owner->addField($this->masterField, ['type' => 'bigint', 'system' => true, 'readOnly' => true]); $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]); + $field = $owner->addField($this->foreignField, ['type' => 'bigint', 'system' => true, 'readOnly' => true, 'actual' => $this->masterField]); $this->foreignField = $field->shortName; // TODO this mutates the join! } } diff --git a/tests/JoinArrayTest.php b/tests/JoinArrayTest.php index dd3a370b6..4cacc4e51 100644 --- a/tests/JoinArrayTest.php +++ b/tests/JoinArrayTest.php @@ -91,7 +91,7 @@ public function testJoinSaving1(): void $db = new Persistence\Array_(['user' => [], 'contact' => []]); $user = new Model($db, ['table' => 'user']); $user->addField('name'); - $user->addField('contact_id', ['type' => 'integer']); + $user->addField('contact_id', ['type' => 'bigint']); $j = $user->join('contact'); $j->addField('contact_phone'); From 1d4534f15d41bbb35f2824f96e889793b1648a21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 29 May 2024 11:48:33 +0200 Subject: [PATCH 18/19] minor test improvements --- tests/JoinArrayTest.php | 12 ++++++------ tests/Model/Smbo/Transfer.php | 2 +- tests/ModelAggregateTest.php | 10 +++++----- tests/ModelNestedArrayTest.php | 2 +- tests/ModelNestedSqlTest.php | 2 +- tests/ReferenceTest.php | 4 ++-- tests/SmboTransferTest.php | 6 +++--- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/JoinArrayTest.php b/tests/JoinArrayTest.php index 4cacc4e51..9b472eecc 100644 --- a/tests/JoinArrayTest.php +++ b/tests/JoinArrayTest.php @@ -150,7 +150,7 @@ public function testJoinSaving2(): void $user->addField('name'); $j = $user->join('contact.test_id'); $j->addField('contact_phone'); - $j->addField('test_id', ['type' => 'integer']); + $j->addField('test_id', ['type' => 'bigint']); $user2 = $user->createEntity(); $user2->set('name', 'John'); @@ -209,7 +209,7 @@ public function testJoinSaving3(): void $db = new Persistence\Array_(['user' => [], 'contact' => []]); $user = new Model($db, ['table' => 'user']); $user->addField('name'); - $user->addField('test_id', ['type' => 'integer']); + $user->addField('test_id', ['type' => 'bigint']); $j = $user->join('contact', ['masterField' => 'test_id']); $j->addField('contact_phone'); @@ -268,7 +268,7 @@ public function testJoinLoading(): void ]); $user = new Model($db, ['table' => 'user']); - $user->addField('contact_id', ['type' => 'integer']); + $user->addField('contact_id', ['type' => 'bigint']); $user->addField('name'); $j = $user->join('contact'); $j->addField('contact_phone'); @@ -306,7 +306,7 @@ public function testJoinUpdate(): void ]); $user = new Model($db, ['table' => 'user']); - $user->addField('contact_id', ['type' => 'integer']); + $user->addField('contact_id', ['type' => 'bigint']); $user->addField('name'); $j = $user->join('contact'); $j->addField('contact_phone'); @@ -383,7 +383,7 @@ public function testJoinDelete(): void ]); $user = new Model($db, ['table' => 'user']); - $user->addField('contact_id', ['type' => 'integer']); + $user->addField('contact_id', ['type' => 'bigint']); $user->addField('name'); $j = $user->join('contact'); $j->addField('contact_phone'); @@ -420,7 +420,7 @@ public function testLoadMissingException(): void ]); $user = new Model($db, ['table' => 'user']); - $user->addField('contact_id', ['type' => 'integer']); + $user->addField('contact_id', ['type' => 'bigint']); $user->addField('name'); $j = $user->join('contact'); $j->addField('contact_phone'); diff --git a/tests/Model/Smbo/Transfer.php b/tests/Model/Smbo/Transfer.php index c1ae9fd0d..881b19d4e 100644 --- a/tests/Model/Smbo/Transfer.php +++ b/tests/Model/Smbo/Transfer.php @@ -23,7 +23,7 @@ protected function init(): void $this->addCondition('transfer_document_id', '!=', null); } - $this->addField('destination_account_id', ['type' => 'integer', 'neverPersist' => true]); + $this->addField('destination_account_id', ['type' => 'bigint', 'neverPersist' => true]); $this->onHookShort(self::HOOK_BEFORE_SAVE, function () { // only for new records and when destination_account_id is set diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 092acf76f..a338d4a97 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -61,7 +61,7 @@ public function testGroupBy(): void $aggregate = $this->createInvoiceAggregate(); $aggregate->setGroupBy(['client_id'], [ - 'c' => ['expr' => 'count(*)', 'type' => 'integer'], + 'c' => ['expr' => 'count(*)', 'type' => 'bigint'], ]); self::assertSameExportUnordered([ @@ -76,7 +76,7 @@ public function testGroupSelect(): void $aggregate->addField('client'); $aggregate->setGroupBy(['client_id'], [ - 'c' => ['expr' => 'count(*)', 'type' => 'integer'], + 'c' => ['expr' => 'count(*)', 'type' => 'bigint'], ]); self::fixAllNonAggregatedFieldsInGroupBy($aggregate); @@ -130,7 +130,7 @@ public function testGroupSelectExpression(): void $aggregate->setGroupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], - 'sum_hasone' => ['expr' => 'sum([order])', 'type' => 'integer'], + 'sum_hasone' => ['expr' => 'sum([order])', 'type' => 'bigint'], ]); self::fixAllNonAggregatedFieldsInGroupBy($aggregate); @@ -250,7 +250,7 @@ public function testGroupSelectRef(): void $aggregate->addField('client'); $aggregate->setGroupBy(['client_id'], [ - 'c' => ['expr' => 'count(*)', 'type' => 'integer'], + 'c' => ['expr' => 'count(*)', 'type' => 'bigint'], ]); self::fixAllNonAggregatedFieldsInGroupBy($aggregate); @@ -345,7 +345,7 @@ public function testAggregateFieldExpressionSql(): void $aggregate->setGroupBy([$aggregate->expr('{}', ['abc'])], [ 'xyz' => ['expr' => 'sum([amount])'], - 'sum_hasone' => ['expr' => 'sum([order])', 'type' => 'integer'], + 'sum_hasone' => ['expr' => 'sum([order])', 'type' => 'bigint'], ]); $this->assertSameSql( diff --git a/tests/ModelNestedArrayTest.php b/tests/ModelNestedArrayTest.php index e7da8a9b2..1e3ffea41 100644 --- a/tests/ModelNestedArrayTest.php +++ b/tests/ModelNestedArrayTest.php @@ -86,7 +86,7 @@ public function atomic(\Closure $fx) ]); $mInner->removeField('_id'); $mInner->idField = 'uid'; - $mInner->addField('uid', ['actual' => '_id', 'type' => 'integer']); + $mInner->addField('uid', ['actual' => '_id', 'type' => 'bigint']); $mInner->addField('name'); $mInner->addField('y', ['actual' => '_birthday', 'type' => 'date']); $mInner->addCondition('uid', '!=', 3); diff --git a/tests/ModelNestedSqlTest.php b/tests/ModelNestedSqlTest.php index fca1cc8c1..4f264b963 100644 --- a/tests/ModelNestedSqlTest.php +++ b/tests/ModelNestedSqlTest.php @@ -87,7 +87,7 @@ public function atomic(\Closure $fx) ]); $mInner->removeField('id'); $mInner->idField = 'uid'; - $mInner->addField('uid', ['actual' => '_id', 'type' => 'integer']); + $mInner->addField('uid', ['actual' => '_id', 'type' => 'bigint']); $mInner->addField('name'); $mInner->addField('y', ['actual' => '_birthday', 'type' => 'date']); $mInner->addCondition('uid', '!=', 3); diff --git a/tests/ReferenceTest.php b/tests/ReferenceTest.php index 975892930..58afc5891 100644 --- a/tests/ReferenceTest.php +++ b/tests/ReferenceTest.php @@ -92,7 +92,7 @@ public function testHasManyDuplicateNameException(): void { $user = new Model(null, ['table' => 'user']); $order = new Model(); - $order->addField('user_id', ['type' => 'integer']); + $order->addField('user_id', ['type' => 'bigint']); $user->hasMany('Orders', ['model' => $order]); @@ -247,7 +247,7 @@ protected function init(): void $this->addField('name'); $this->hasMany('orders', ['model' => [self::$orderModelClass], 'theirField' => 'user_id']) - ->addField('orders_count', ['type' => 'integer', 'aggregate' => 'count']); + ->addField('orders_count', ['type' => 'bigint', 'aggregate' => 'count']); $this->analysingOrderModelFields = $this->getReference('orders')->createAnalysingTheirModel()->getFields(); diff --git a/tests/SmboTransferTest.php b/tests/SmboTransferTest.php index 946e8450d..8a1c062e5 100644 --- a/tests/SmboTransferTest.php +++ b/tests/SmboTransferTest.php @@ -30,11 +30,11 @@ protected function setUp(): void $this->createMigrator()->table('payment') ->id() - ->field('document_id', ['type' => 'integer']) - ->field('account_id', ['type' => 'integer']) + ->field('document_id', ['type' => 'bigint']) + ->field('account_id', ['type' => 'bigint']) ->field('cheque_no') ->field('misc_payment', ['type' => 'boolean']) - ->field('transfer_document_id', ['type' => 'integer']) + ->field('transfer_document_id', ['type' => 'bigint']) ->create(); } From ef718fae22edade7dc93cc5a6e91332cfd21111b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 29 May 2024 12:44:02 +0200 Subject: [PATCH 19/19] improve docs about historical model reuse for load --- docs/design.md | 25 ++----------------------- docs/fields.md | 3 --- docs/model.md | 15 +-------------- 3 files changed, 3 insertions(+), 40 deletions(-) diff --git a/docs/design.md b/docs/design.md index b22f7a8ea..fae1fc2dc 100644 --- a/docs/design.md +++ b/docs/design.md @@ -374,26 +374,8 @@ $order = new Model_Order($db); $order = $order->load(10); ``` -In scenario above we loaded a specific record. Agile Data does not create a -separate object when loading, instead the same object is re-used. This is done -to preserve some memory. - -So in the code above `$order` is not created for the record, but it can load -any record from the DataSet. Think of it as a "window" into a large table of -Orders: - -``` -$sum = 0; -$order = new Model_Order($db); -$order = $order->load(10); -$sum += $order->get('amount'); - -$order = $order->load(11); -$sum += $order->get('amount'); - -$order = $order->load(13); -$sum += $order->get('amount'); -``` +In scenario above we loaded a specific record. Agile Data creates a separate +object/entity when loading by cloning the original object/model. You can iterate over the DataSet: @@ -404,9 +386,6 @@ foreach (new Model_Order($db) as $order) { } ``` -You must remember that the code above will only create a single object and -iterating it will simply make it load different values. - At this point, I'll jump ahead a bit and will show you an alternative code: ``` diff --git a/docs/fields.md b/docs/fields.md index 0e433da7d..9cd5b5e50 100644 --- a/docs/fields.md +++ b/docs/fields.md @@ -31,9 +31,6 @@ $model->set('name', 'John'); echo $model->get('name'); // John ``` -Just like you can reuse {php:class}`Model` to access multiple data records, -{php:class}`Field` object will be reused also. - ## Purpose of Field Implementation of Field in Agile Data is a very powerful and distinctive feature. diff --git a/docs/model.md b/docs/model.md index 306b6030e..c8a2d3027 100644 --- a/docs/model.md +++ b/docs/model.md @@ -216,20 +216,7 @@ $model->addField('name'); var_dump($model->getField('name')); ``` -Other persistence framework will use "properties", because individual objects may impact -performance. In ATK Data this is not an issue, because "Model" is re-usable: - -``` -foreach (new User($db) as $user) { - // will be the same object every time!! - var_dump($user->getField('name')); - - // this is also the same object every time!! - var_dump($user); -} -``` - -Instead, Field handles many very valuable operations which would otherwise fall on the +Field handles many very valuable operations which would otherwise fall on the shoulders of developer (Read more here {php:class}`Field`) :::{php:method} addField($name, $seed)