diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b37d88f4..6ef221ed8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # CHANGELOG -v2.6.0 (22.12.2023) +v2.7.0 (08.02.2024) +-------------------- +- Add Generated Fields option into ORM Schema by @roxblnfk (#462) + +v2.6.1 (04.01.2024) +-------------------- +- Fix compatibility with PHP 8.3 by @msmakouz (#454) + +- v2.6.0 (22.12.2023) -------------------- - Add support for `loophp/collection` v7 by @msmakouz (#448) - Fix wrong adding table prefix on joins by @msmakouz (#447) diff --git a/composer.json b/composer.json index bdedd2250..eae93bb4d 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ "require": { "php": ">=8.0", "ext-pdo": "*", - "cycle/database": "^2.6", + "cycle/database": "^2.8.1", "doctrine/instantiator": "^1.3.1 || ^2.0", "spiral/core": "^2.8 || ^3.0" }, @@ -51,8 +51,7 @@ "phpunit/phpunit": "^9.5", "ramsey/uuid": "^4.0", "spiral/tokenizer": "^2.8 || ^3.0", - "vimeo/psalm": "5.21", - "buggregator/trap": "^1.4" + "vimeo/psalm": "5.21" }, "autoload": { "psr-4": { diff --git a/src/Command/Database/Insert.php b/src/Command/Database/Insert.php index f35352f75..2e3fe6b55 100644 --- a/src/Command/Database/Insert.php +++ b/src/Command/Database/Insert.php @@ -11,6 +11,7 @@ use Cycle\ORM\Command\Traits\MapperTrait; use Cycle\ORM\Heap\State; use Cycle\ORM\MapperInterface; +use Cycle\ORM\Schema\GeneratedField; /** * Insert data into associated table and provide lastInsertID promise. @@ -20,14 +21,20 @@ final class Insert extends StoreCommand use ErrorTrait; use MapperTrait; + /** + * @param non-empty-string $table + * @param string[] $primaryKeys + * @param non-empty-string|null $pkColumn + * @param array $generated + */ public function __construct( DatabaseInterface $db, string $table, State $state, ?MapperInterface $mapper, - /** @var string[] */ private array $primaryKeys = [], - private ?string $pkColumn = null + private ?string $pkColumn = null, + private array $generated = [], ) { parent::__construct($db, $table, $state); $this->mapper = $mapper; @@ -40,7 +47,12 @@ public function isReady(): bool public function hasData(): bool { - return $this->columns !== [] || $this->state->getData() !== []; + return match (true) { + $this->columns !== [], + $this->state->getData() !== [], + $this->hasGeneratedFields() => true, + default => false, + }; } public function getStoreData(): array @@ -59,6 +71,7 @@ public function getStoreData(): array public function execute(): void { $state = $this->state; + $returningFields = []; if ($this->appendix !== []) { $state->setData($this->appendix); @@ -72,25 +85,59 @@ public function execute(): void unset($uncasted[$key]); } } + // unset db-generated fields if they are null + foreach ($this->generated as $column => $mode) { + if (($mode & GeneratedField::ON_INSERT) === 0x0) { + continue; + } + + $returningFields[$column] = $mode; + if (!isset($uncasted[$column])) { + unset($uncasted[$column]); + } + } $uncasted = $this->prepareData($uncasted); $insert = $this->db ->insert($this->table) ->values(\array_merge($this->columns, $uncasted)); - if ($this->pkColumn !== null && $insert instanceof ReturningInterface) { - $insert->returning($this->pkColumn); + if ($this->pkColumn !== null && $returningFields === []) { + $returningFields[$this->primaryKeys[0]] ??= $this->pkColumn; } - $insertID = $insert->run(); + if ($insert instanceof ReturningInterface && $returningFields !== []) { + // Map generated fields to columns + $returning = $this->mapper->mapColumns($returningFields); + // Array of [field name => column name] + $returning = \array_combine(\array_keys($returningFields), \array_keys($returning)); - if ($insertID !== null && \count($this->primaryKeys) === 1) { - $fpk = $this->primaryKeys[0]; // first PK - if (!isset($data[$fpk])) { + $insert->returning(...\array_values($returning)); + + $insertID = $insert->run(); + + if (\count($returning) === 1) { + $field = \array_key_first($returning); $state->register( - $fpk, - $this->mapper === null ? $insertID : $this->mapper->cast([$fpk => $insertID])[$fpk] + $field, + $this->mapper === null ? $insertID : $this->mapper->cast([$field => $insertID])[$field], ); + } else { + foreach ($this->mapper->cast($insertID) as $field => $value) { + $state->register($field, $value); + } + } + } else { + $insertID = $insert->run(); + + if ($insertID !== null && \count($this->primaryKeys) === 1) { + $fpk = $this->primaryKeys[0]; // first PK + if (!isset($data[$fpk])) { + $state->register( + $fpk, + $this->mapper === null ? $insertID : $this->mapper->cast([$fpk => $insertID])[$fpk] + ); + } } } @@ -103,4 +150,28 @@ public function register(string $key, mixed $value): void { $this->state->register($key, $value); } + + /** + * Has fields that weren't provided but will be generated by the database or PHP. + */ + private function hasGeneratedFields(): bool + { + if ($this->generated === []) { + return false; + } + + $data = $this->state->getData(); + + foreach ($this->generated as $field => $mode) { + if (($mode & (GeneratedField::ON_INSERT | GeneratedField::BEFORE_INSERT)) === 0x0) { + continue; + } + + if (!isset($data[$field])) { + return true; + } + } + + return false; + } } diff --git a/src/Heap/State.php b/src/Heap/State.php index 743da9938..05338e6c4 100644 --- a/src/Heap/State.php +++ b/src/Heap/State.php @@ -163,7 +163,7 @@ public function getChanges(): array public function getValue(string $key): mixed { - return array_key_exists($key, $this->data) ? $this->data[$key] : ($this->transactionData[$key] ?? null); + return \array_key_exists($key, $this->data) ? $this->data[$key] : ($this->transactionData[$key] ?? null); } public function hasValue(string $key, bool $allowNull = true): bool @@ -171,7 +171,7 @@ public function hasValue(string $key, bool $allowNull = true): bool if (!$allowNull) { return isset($this->data[$key]) || isset($this->transactionData[$key]); } - return array_key_exists($key, $this->data) || array_key_exists($key, $this->transactionData); + return \array_key_exists($key, $this->data) || \array_key_exists($key, $this->transactionData); } public function register(string $key, mixed $value): void diff --git a/src/Heap/Traits/WaitFieldTrait.php b/src/Heap/Traits/WaitFieldTrait.php index 695b6be19..b985c1d92 100644 --- a/src/Heap/Traits/WaitFieldTrait.php +++ b/src/Heap/Traits/WaitFieldTrait.php @@ -18,7 +18,7 @@ public function waitField(string $key, bool $required = true): void public function getWaitingFields(bool $requiredOnly = false): array { - return array_keys($requiredOnly ? array_filter($this->waitingFields) : $this->waitingFields); + return \array_keys($requiredOnly ? \array_filter($this->waitingFields) : $this->waitingFields); } /** diff --git a/src/Mapper/DatabaseMapper.php b/src/Mapper/DatabaseMapper.php index e640aa6b6..167698e6e 100644 --- a/src/Mapper/DatabaseMapper.php +++ b/src/Mapper/DatabaseMapper.php @@ -39,6 +39,8 @@ abstract class DatabaseMapper implements MapperInterface protected array $primaryKeys; private ?TypecastInterface $typecast; protected RelationMap $relationMap; + /** @var array */ + private array $generatedFields; public function __construct( ORMInterface $orm, @@ -53,6 +55,7 @@ public function __construct( $this->columns[\is_int($property) ? $column : $property] = $column; } + $this->generatedFields = $schema->define($role, SchemaInterface::GENERATED_FIELDS) ?? []; // Parent's fields $parent = $schema->define($role, SchemaInterface::PARENT); while ($parent !== null) { @@ -128,6 +131,7 @@ public function queueCreate(object $entity, Node $node, State $state): CommandIn $this, $this->primaryKeys, \count($this->primaryColumns) === 1 ? $this->primaryColumns[0] : null, + $this->generatedFields, ); } diff --git a/src/Relation/BelongsTo.php b/src/Relation/BelongsTo.php index 014071a54..0517ecc0b 100644 --- a/src/Relation/BelongsTo.php +++ b/src/Relation/BelongsTo.php @@ -179,7 +179,7 @@ private function checkNullValuePossibility(Tuple $tuple): bool } if ($tuple->status < Tuple::STATUS_PREPROCESSED - && array_intersect($this->innerKeys, $tuple->state->getWaitingFields(false)) !== [] + && \array_intersect($this->innerKeys, $tuple->state->getWaitingFields(false)) !== [] ) { return true; } diff --git a/src/Relation/ManyToMany.php b/src/Relation/ManyToMany.php index 21d304092..2296ce0d9 100644 --- a/src/Relation/ManyToMany.php +++ b/src/Relation/ManyToMany.php @@ -363,8 +363,6 @@ protected function newLink(Pool $pool, Tuple $tuple, PivotedStorage $storage, ob foreach ($this->throughInnerKeys as $i => $pInnerKey) { $pTuple->state->register($pInnerKey, $tuple->state->getTransactionData()[$this->innerKeys[$i]] ?? null); - - // $rState->forward($this->outerKeys[$i], $pState, $this->throughOuterKeys[$i]); } if ($this->inversion === null) { diff --git a/src/Relation/RefersTo.php b/src/Relation/RefersTo.php index a150fdee8..9858cd806 100644 --- a/src/Relation/RefersTo.php +++ b/src/Relation/RefersTo.php @@ -98,7 +98,7 @@ public function queue(Pool $pool, Tuple $tuple): void if ($rTuple->status === Tuple::STATUS_PROCESSED || ($rTuple->status > Tuple::STATUS_PREPARING && $rTuple->state->getStatus() !== node::NEW - && array_intersect($this->outerKeys, $rTuple->state->getWaitingFields()) === []) + && \array_intersect($this->outerKeys, $rTuple->state->getWaitingFields()) === []) ) { $this->pullValues($tuple->state, $rTuple->state); $node->setRelation($this->getName(), $related); diff --git a/src/Schema/GeneratedField.php b/src/Schema/GeneratedField.php new file mode 100644 index 000000000..26eee49b2 --- /dev/null +++ b/src/Schema/GeneratedField.php @@ -0,0 +1,38 @@ + generating type] /** * Return all roles defined within the schema. diff --git a/src/Transaction/Runner.php b/src/Transaction/Runner.php index 224396df8..2083f15ec 100644 --- a/src/Transaction/Runner.php +++ b/src/Transaction/Runner.php @@ -75,7 +75,7 @@ public function complete(): void { if ($this->mode === self::MODE_OPEN_TRANSACTION) { // Commit all of the open and normalized database transactions - foreach (array_reverse($this->drivers) as $driver) { + foreach (\array_reverse($this->drivers) as $driver) { /** @var DriverInterface $driver */ $driver->commitTransaction(); } @@ -94,14 +94,14 @@ public function rollback(): void { if ($this->mode === self::MODE_OPEN_TRANSACTION) { // Close all open and normalized database transactions - foreach (array_reverse($this->drivers) as $driver) { + foreach (\array_reverse($this->drivers) as $driver) { /** @var DriverInterface $driver */ $driver->rollbackTransaction(); } } // Close all of external types of transactions (revert changes) - foreach (array_reverse($this->executed) as $command) { + foreach (\array_reverse($this->executed) as $command) { if ($command instanceof RollbackMethodInterface) { $command->rollBack(); } @@ -145,7 +145,7 @@ private function prepareTransaction(DriverInterface $driver): void if ($this->mode === self::MODE_CONTINUE_TRANSACTION) { if ($driver->getTransactionLevel() === 0) { - throw new RunnerException(sprintf( + throw new RunnerException(\sprintf( 'The `%s` driver connection has no opened transaction.', $driver->getType() )); diff --git a/src/Transaction/UnitOfWork.php b/src/Transaction/UnitOfWork.php index 9ebbab7f3..2582392e8 100644 --- a/src/Transaction/UnitOfWork.php +++ b/src/Transaction/UnitOfWork.php @@ -163,7 +163,7 @@ private function syncHeap(): void $node = $tuple->node; // marked as being deleted and has no external claims (GC like approach) - if (in_array($node->getStatus(), [Node::DELETED, Node::SCHEDULED_DELETE], true)) { + if (\in_array($node->getStatus(), [Node::DELETED, Node::SCHEDULED_DELETE], true)) { $heap->detach($e); continue; } @@ -174,7 +174,7 @@ private function syncHeap(): void $heap->attach($e, $node, $indexProvider->getIndexes($role)); if ($tuple->persist !== null) { - $syncData = array_udiff_assoc( + $syncData = \array_udiff_assoc( $tuple->state->getData(), $tuple->persist->getData(), [Node::class, 'compare'] @@ -265,7 +265,7 @@ private function resolveSlaveRelations(Tuple $tuple, RelationMap $map): int if (!$map->hasSlaves()) { return self::RELATIONS_RESOLVED; } - $changedFields = array_keys($tuple->state->getChanges()); + $changedFields = \array_keys($tuple->state->getChanges()); // Attach children to pool $transactData = $tuple->state->getTransactionData(); @@ -281,15 +281,15 @@ private function resolveSlaveRelations(Tuple $tuple, RelationMap $map): int } $innerKeys = $relation->getInnerKeys(); - $isWaitingKeys = array_intersect($innerKeys, $tuple->state->getWaitingFields(true)) !== []; - $hasChangedKeys = array_intersect($innerKeys, $changedFields) !== []; + $isWaitingKeys = \array_intersect($innerKeys, $tuple->state->getWaitingFields(true)) !== []; + $hasChangedKeys = \array_intersect($innerKeys, $changedFields) !== []; if ($relationStatus === RelationInterface::STATUS_PREPARE) { $relData ??= $tuple->mapper->fetchRelations($tuple->entity); $relation->prepare( $this->pool, $tuple, $relData[$name] ?? null, - $isWaitingKeys || $hasChangedKeys + $isWaitingKeys || $hasChangedKeys, ); $relationStatus = $tuple->state->getRelationStatus($relation->getName()); } @@ -298,9 +298,9 @@ private function resolveSlaveRelations(Tuple $tuple, RelationMap $map): int && $relationStatus !== RelationInterface::STATUS_RESOLVED && !$isWaitingKeys && !$hasChangedKeys - && \count(array_intersect($innerKeys, array_keys($transactData))) === \count($innerKeys) + && \count(\array_intersect($innerKeys, \array_keys($transactData))) === \count($innerKeys) ) { - $child ??= $tuple->state->getRelation($name); + // $child ??= $tuple->state->getRelation($name); $relation->queue($this->pool, $tuple); $relationStatus = $tuple->state->getRelationStatus($relation->getName()); } @@ -316,7 +316,7 @@ private function resolveSelfWithEmbedded(Tuple $tuple, RelationMap $map, bool $h if (!$map->hasEmbedded() && !$tuple->state->hasChanges()) { $tuple->status = !$hasDeferredRelations ? Tuple::STATUS_PROCESSED - : max(Tuple::STATUS_DEFERRED, $tuple->status); + : \max(Tuple::STATUS_DEFERRED, $tuple->status); return; } @@ -351,14 +351,13 @@ private function resolveSelfWithEmbedded(Tuple $tuple, RelationMap $map, bool $h $tuple->status = $tuple->status === Tuple::STATUS_PREPROCESSED || !$hasDeferredRelations ? Tuple::STATUS_PROCESSED - : max(Tuple::STATUS_DEFERRED, $tuple->status); + : \max(Tuple::STATUS_DEFERRED, $tuple->status); } private function resolveRelations(Tuple $tuple): void { $map = $this->orm->getRelationMap($tuple->node->getRole()); - // Dependency relations $result = $tuple->task === Tuple::TASK_STORE ? $this->resolveMasterRelations($tuple, $map) : $this->resolveSlaveRelations($tuple, $map); @@ -368,8 +367,8 @@ private function resolveRelations(Tuple $tuple): void // Self if ($deferred && $tuple->status < Tuple::STATUS_PROPOSED) { $tuple->status = Tuple::STATUS_DEFERRED; - // $this->pool->attachTuple($tuple); } + if ($isDependenciesResolved) { if ($tuple->task === Tuple::TASK_STORE) { $this->resolveSelfWithEmbedded($tuple, $map, $deferred); @@ -383,7 +382,6 @@ private function resolveRelations(Tuple $tuple): void } if ($tuple->cascade) { - // Slave relations relations $tuple->task === Tuple::TASK_STORE ? $this->resolveSlaveRelations($tuple, $map) : $this->resolveMasterRelations($tuple, $map); diff --git a/tests/ORM/Functional/Driver/Common/BaseTest.php b/tests/ORM/Functional/Driver/Common/BaseTest.php index ba446ae32..633f31329 100644 --- a/tests/ORM/Functional/Driver/Common/BaseTest.php +++ b/tests/ORM/Functional/Driver/Common/BaseTest.php @@ -97,7 +97,8 @@ public function setUp(): void null, new DoctrineCollectionFactory() ))->withCollectionFactory('array', new ArrayCollectionFactory()), - new Schema([]) + new Schema([]), + $this->getCommandGenerator(), ); } @@ -400,4 +401,9 @@ protected function applyDriverOptions(DriverConfig $config, array $options): voi $config->options['withDatetimeMicroseconds'] = $options['withDatetimeMicroseconds']; } } + + protected function getCommandGenerator(): ?Transaction\CommandGeneratorInterface + { + return null; + } } diff --git a/tests/ORM/Functional/Driver/Common/GeneratedColumnTest.php b/tests/ORM/Functional/Driver/Common/GeneratedColumnTest.php new file mode 100644 index 000000000..8f8778978 --- /dev/null +++ b/tests/ORM/Functional/Driver/Common/GeneratedColumnTest.php @@ -0,0 +1,131 @@ +createTables(); + + $this->orm = $this->withSchema(new Schema([ + User::class => [ + SchemaInterface::ROLE => 'user', + SchemaInterface::MAPPER => Mapper::class, + SchemaInterface::DATABASE => 'default', + SchemaInterface::TABLE => 'user', + SchemaInterface::PRIMARY_KEY => 'id', + SchemaInterface::COLUMNS => ['id', 'balance'], + SchemaInterface::SCHEMA => [], + SchemaInterface::RELATIONS => [], + SchemaInterface::GENERATED_FIELDS => [ + 'balance' => GeneratedField::ON_INSERT, // sequence + ], + ], + Document::class => [ + SchemaInterface::ROLE => 'profile', + SchemaInterface::MAPPER => Mapper::class, + SchemaInterface::DATABASE => 'default', + SchemaInterface::TABLE => 'document', + SchemaInterface::PRIMARY_KEY => 'id', + SchemaInterface::COLUMNS => ['id', 'body', 'created_at', 'updated_at'], + SchemaInterface::SCHEMA => [], + SchemaInterface::RELATIONS => [], + SchemaInterface::TYPECAST => [ + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ], + SchemaInterface::GENERATED_FIELDS => [ + 'id' => GeneratedField::ON_INSERT, + 'body' => GeneratedField::ON_INSERT, + 'created_at' => GeneratedField::ON_INSERT, + 'updated_at' => GeneratedField::BEFORE_INSERT | GeneratedField::BEFORE_UPDATE, + ], + ], + ])); + } + + public function testPersist(): void + { + $u = new User(); + $u->id = Uuid::uuid4()->toString(); + + $this->save($u); + + $this->assertNotNull($u->balance); + + $this->orm->getHeap()->clean(); + + $s = (new Select($this->orm, User::class))->wherePK($u->id)->fetchOne(); + + $this->assertSame($u->balance, $s->balance); + } + + public function testPersistMultipleSerial(): void + { + $d1 = new Document(); + + $d2 = new Document(); + $d2->body = 213; + $d2->created_at = $d2->updated_at = new DateTimeImmutable('2020-01-01'); + + $d3 = new Document(); + $d3->created_at = $d3->updated_at = new DateTimeImmutable('2020-01-01'); + + + $this->save($d1, $d2, $d3); + + $this->assertEquals(1, $d1->id); + $this->assertEquals(1, $d1->body); + $this->assertNotSame('2020-01-01', $d1->created_at->format('Y-m-d')); + $this->assertEquals(2, $d2->id); + $this->assertEquals(213, $d2->body); + $this->assertSame('2020-01-01', $d2->created_at->format('Y-m-d')); + $this->assertEquals(3, $d3->id); + $this->assertEquals(2, $d3->body); + $this->assertSame('2020-01-01', $d3->created_at->format('Y-m-d')); + } + + protected function getCommandGenerator(): ?Transaction\CommandGeneratorInterface + { + return new class () extends Transaction\CommandGenerator { + protected function storeEntity(ORMInterface $orm, Transaction\Tuple $tuple, bool $isNew): ?CommandInterface + { + /** @var CommandInterface|null $command */ + $command = parent::storeEntity($orm, $tuple, $isNew); + + if ($command !== null && $tuple->entity instanceof Document && empty($tuple->entity->updated_at)) { + $now = new DateTimeImmutable(); + $tuple->state->register('updated_at', $now); + $tuple->entity->updated_at = $now; + } + + return $command; + } + }; + } + + abstract public function createTables(): void; +} diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case321/CaseTest.php b/tests/ORM/Functional/Driver/Common/Integration/Case321/CaseTest.php new file mode 100644 index 000000000..9f72f6722 --- /dev/null +++ b/tests/ORM/Functional/Driver/Common/Integration/Case321/CaseTest.php @@ -0,0 +1,93 @@ +makeTables(); + + $this->loadSchema(__DIR__ . '/schema.php'); + } + + public function test1(): void + { + $user = new Entity\User1(); + + // Store changes and calc write queries + $this->captureWriteQueries(); + $this->save($user); + + // Check write queries count + $this->assertNumWrites(1); + } + + public function test2(): void + { + $user = new Entity\User2(); + + // Store changes and calc write queries + $this->captureWriteQueries(); + $this->save($user); + + // Check write queries count + $this->assertNumWrites(1); + } + + public function test3(): void + { + $user = new Entity\User3(); + + $this->captureWriteQueries(); + $this->save($user); + + // ORM won't detect any values to store + // because the id field isn't marked as autogenerated + $this->assertNumWrites(0); + } + + public function test4(): void + { + $user = new Entity\User4(); + + $this->save($user); + + // ORM won't detect any values to store + // because the id field isn't marked as autogenerated + $this->assertNumWrites(0); + } + + private function makeTables(): void + { + // Make tables + $this->makeTable('user1', [ + 'id' => 'primary', // autoincrement + ]); + + $this->makeTable('user2', [ + 'id' => 'primary', // autoincrement + ]); + + $this->makeTable('user3', [ + 'id' => 'primary', // autoincrement + ]); + + $this->logger->display(); + $this->makeTable('user4', [ + 'id' => 'primary', + 'counter' => 'int', + ]); + } +} diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case321/Entity/User1.php b/tests/ORM/Functional/Driver/Common/Integration/Case321/Entity/User1.php new file mode 100644 index 000000000..292e31c5f --- /dev/null +++ b/tests/ORM/Functional/Driver/Common/Integration/Case321/Entity/User1.php @@ -0,0 +1,10 @@ + [ + Schema::ENTITY => User1::class, + Schema::MAPPER => Mapper::class, + Schema::SOURCE => Source::class, + Schema::DATABASE => 'default', + Schema::TABLE => 'user1', + Schema::PRIMARY_KEY => ['id'], + Schema::FIND_BY_KEYS => ['id'], + Schema::COLUMNS => [ + 'id' => 'id', + ], + Schema::RELATIONS => [], + Schema::SCOPE => null, + Schema::TYPECAST => [ + 'id' => 'int', + ], + Schema::SCHEMA => [], + Schema::GENERATED_FIELDS => [ + 'id' => GeneratedField::ON_INSERT, // autoincrement + ], + ], + 'user2' => [ + Schema::ENTITY => User2::class, + Schema::MAPPER => Mapper::class, + Schema::SOURCE => Source::class, + Schema::DATABASE => 'default', + Schema::TABLE => 'user2', + Schema::PRIMARY_KEY => ['id'], + Schema::FIND_BY_KEYS => ['id'], + Schema::COLUMNS => [ + 'id' => 'id', + ], + Schema::RELATIONS => [], + Schema::SCOPE => null, + Schema::TYPECAST => [ + 'id' => 'int', + ], + Schema::SCHEMA => [], + Schema::GENERATED_FIELDS => [ + 'id' => GeneratedField::ON_INSERT, // autoincrement + ], + ], + 'user3' => [ + Schema::ENTITY => User3::class, + Schema::MAPPER => Mapper::class, + Schema::SOURCE => Source::class, + Schema::DATABASE => 'default', + Schema::TABLE => 'user3', + Schema::PRIMARY_KEY => ['id'], + Schema::FIND_BY_KEYS => ['id'], + Schema::COLUMNS => [ + 'id' => 'id', + ], + Schema::RELATIONS => [], + Schema::SCOPE => null, + Schema::TYPECAST => [ + 'id' => 'int', + ], + Schema::SCHEMA => [], + Schema::GENERATED_FIELDS => [], + ], + 'user4' => [ + Schema::ENTITY => User4::class, + Schema::MAPPER => Mapper::class, + Schema::SOURCE => Source::class, + Schema::DATABASE => 'default', + Schema::TABLE => 'user4', + Schema::PRIMARY_KEY => ['id'], + Schema::COLUMNS => [ + 'id' => 'id', + 'counter' => 'counter', + ], + Schema::TYPECAST => [ + 'id' => 'int', + 'counter' => 'counter', + ], + Schema::GENERATED_FIELDS => [ + 'counter' => GeneratedField::BEFORE_UPDATE, + ], + ], +]; diff --git a/tests/ORM/Functional/Driver/Common/Relation/Embedded/EmbeddedRelationTest.php b/tests/ORM/Functional/Driver/Common/Relation/Embedded/EmbeddedRelationTest.php index 87014cfb6..e02efd44a 100644 --- a/tests/ORM/Functional/Driver/Common/Relation/Embedded/EmbeddedRelationTest.php +++ b/tests/ORM/Functional/Driver/Common/Relation/Embedded/EmbeddedRelationTest.php @@ -138,7 +138,7 @@ public function testCreateUserWithEmbedded(): void $this->save($u); $this->assertNumWrites(1); - $this->assertSame(3, $u->id); + $this->assertSame(3, (int)$u->id); $selector = new Select($this->orm->withHeap(new Heap()), User::class); $u2 = $selector->load('credentials')->wherePK($u->id)->fetchOne(); diff --git a/tests/ORM/Functional/Driver/Common/Typecast/TypecastTest.php b/tests/ORM/Functional/Driver/Common/Typecast/TypecastTest.php index f34d14787..9051a1462 100644 --- a/tests/ORM/Functional/Driver/Common/Typecast/TypecastTest.php +++ b/tests/ORM/Functional/Driver/Common/Typecast/TypecastTest.php @@ -143,7 +143,7 @@ public function testAIIdTypecastingOnInsert(): void $this->save($user); $this->assertNotNull($user->id); - $this->assertIsInt($user->id->value); + $this->assertIsNumeric($user->id->value); } // ORM::make() diff --git a/tests/ORM/Functional/Driver/MySQL/Integration/Case321/CaseTest.php b/tests/ORM/Functional/Driver/MySQL/Integration/Case321/CaseTest.php new file mode 100644 index 000000000..7b3318f01 --- /dev/null +++ b/tests/ORM/Functional/Driver/MySQL/Integration/Case321/CaseTest.php @@ -0,0 +1,17 @@ +getDatabase()->table('user')->getSchema(); + $schema->column('id')->type('uuid'); + $schema->column('balance')->type('serial')->nullable(false); + $schema->save(); + + $this->getDatabase()->table('user')->insertMultiple( + ['id'], + [ + [Uuid::uuid4()->toString()], + [Uuid::uuid4()->toString()], + [Uuid::uuid4()->toString()], + ] + ); + + $schema = $this->getDatabase()->table('document')->getSchema(); + $schema->column('id')->primary(); + $schema->column('body')->type('serial')->nullable(false); + $schema->column('created_at')->type('datetime')->nullable(false)->defaultValue(AbstractColumn::DATETIME_NOW); + $schema->column('updated_at')->type('datetime')->nullable(false); + $schema->save(); + } +} diff --git a/tests/ORM/Functional/Driver/Postgres/Integration/Case321/CaseTest.php b/tests/ORM/Functional/Driver/Postgres/Integration/Case321/CaseTest.php new file mode 100644 index 000000000..0b591e540 --- /dev/null +++ b/tests/ORM/Functional/Driver/Postgres/Integration/Case321/CaseTest.php @@ -0,0 +1,17 @@ +getDatabase()->query('DROP SEQUENCE IF EXISTS testSequence1;'); + $this->getDatabase()->query('DROP SEQUENCE IF EXISTS testSequence2;'); + $this->getDatabase()->query('CREATE SEQUENCE testSequence1 START WITH 1;'); + $this->getDatabase()->query('CREATE SEQUENCE testSequence2 START WITH 1;'); + + $schema = $this->getDatabase()->table('user')->getSchema(); + $schema->column('id')->type('uuid'); + $schema->column('balance')->type('int')->nullable(false) + ->defaultValue(new Fragment('NEXT VALUE FOR testSequence1')); + $schema->save(); + + $this->getDatabase()->table('user')->insertMultiple( + ['id'], + [ + [Uuid::uuid4()->toString()], + [Uuid::uuid4()->toString()], + [Uuid::uuid4()->toString()], + ] + ); + + $schema = $this->getDatabase()->table('document')->getSchema(); + $schema->column('id')->primary(); + $schema->column('body')->type('int')->nullable(false) + ->defaultValue(new Fragment('NEXT VALUE FOR testSequence2')); + $schema->column('created_at')->type('datetime')->nullable(false)->defaultValue(AbstractColumn::DATETIME_NOW); + $schema->column('updated_at')->type('datetime')->nullable(false); + $schema->save(); + } +} diff --git a/tests/ORM/Functional/Driver/SQLServer/Integration/Case321/CaseTest.php b/tests/ORM/Functional/Driver/SQLServer/Integration/Case321/CaseTest.php new file mode 100644 index 000000000..fe4fbffb4 --- /dev/null +++ b/tests/ORM/Functional/Driver/SQLServer/Integration/Case321/CaseTest.php @@ -0,0 +1,17 @@ +with(['foo' => 'baaar']) ->andReturn(['baz' => 'bar']); + $this->mapper->shouldReceive('mapColumns') + ->once() + ->with(['id' => 'foo_id']) + ->andReturn(['foo_id' => 'foo_id']); + $this->mapper->shouldReceive('cast') ->once() ->with(['id' => 234]) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 5ccbab2b9..e6f3f0030 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -25,7 +25,10 @@ user: 'root', password: 'root', ), - queryCache: true + queryCache: true, + options: [ + 'logQueryParameters' => true, + ], ), 'postgres' => new Config\PostgresDriverConfig( connection: new Config\Postgres\TcpConnectionConfig( @@ -37,16 +40,20 @@ ), schema: 'public', queryCache: true, + options: [ + 'logQueryParameters' => true, + ], ), 'sqlserver' => new Config\SQLServerDriverConfig( - connection: new Config\SQLServer\TcpConnectionConfig( - database: 'tempdb', - host: '127.0.0.1', - port: 11433, + connection: new Config\SQLServer\DsnConnectionConfig( + 'sqlsrv:Server=127.0.0.1,11433;Database=tempdb;TrustServerCertificate=true', user: 'SA', password: 'SSpaSS__1' ), - queryCache: true + queryCache: true, + options: [ + 'logQueryParameters' => true, + ], ), ];