diff --git a/src/Command/Database/Insert.php b/src/Command/Database/Insert.php index f35352f7..00f4e324 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\SchemaInterface; /** * 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,28 +85,62 @@ public function execute(): void unset($uncasted[$key]); } } + // unset db-generated fields if they are null + foreach ($this->generated as $column => $mode) { + if (($mode & SchemaInterface::GENERATED_DB) === 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)); + // TODO remove: + $returning = \array_slice($returning, 0, 1); // only one returning field is supported - 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 { + // todo multiple returning + } + } 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] + ); + } } } + $state->updateTransactionData(); parent::execute(); @@ -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 & (SchemaInterface::GENERATED_DB | SchemaInterface::GENERATED_PHP_INSERT)) === 0x0) { + continue; + } + + if (!isset($data[$field])) { + return true; + } + } + + return true; + } } diff --git a/src/Mapper/DatabaseMapper.php b/src/Mapper/DatabaseMapper.php index e640aa6b..167698e6 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/SchemaInterface.php b/src/SchemaInterface.php index 835ab022..7ef10358 100644 --- a/src/SchemaInterface.php +++ b/src/SchemaInterface.php @@ -31,6 +31,13 @@ interface SchemaInterface public const DISCRIMINATOR = 17; // Discriminator column name for single table inheritance public const LISTENERS = 18; public const TYPECAST_HANDLER = 19; // Typecast handler definition that implements TypecastInterface + public const GENERATED_FIELDS = 20; // List of generated fields [field => generating type] + + + public const GENERATED_DB = 1; // sequences and others + public const GENERATED_PHP_INSERT = 2; // generated by PHP code on insert like uuid + public const GENERATED_PHP_UPDATE = 4; // generated by PHP code on update like time + /** * Return all roles defined within the schema. diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case321/schema.php b/tests/ORM/Functional/Driver/Common/Integration/Case321/schema.php index e4862582..a473fbf7 100644 --- a/tests/ORM/Functional/Driver/Common/Integration/Case321/schema.php +++ b/tests/ORM/Functional/Driver/Common/Integration/Case321/schema.php @@ -26,6 +26,9 @@ 'id' => 'int', ], Schema::SCHEMA => [], + Schema::GENERATED_FIELDS => [ + 'id' => Schema::GENERATED_DB, // autoincrement + ], ], 'user2' => [ Schema::ENTITY => User2::class, @@ -44,5 +47,8 @@ 'id' => 'int', ], Schema::SCHEMA => [], + Schema::GENERATED_FIELDS => [ + 'id' => Schema::GENERATED_DB, // autoincrement + ], ], ]; diff --git a/tests/ORM/Functional/Driver/Postgres/SerialColumnTest.php b/tests/ORM/Functional/Driver/Postgres/SerialColumnTest.php index 7afd3346..f5bad23a 100644 --- a/tests/ORM/Functional/Driver/Postgres/SerialColumnTest.php +++ b/tests/ORM/Functional/Driver/Postgres/SerialColumnTest.php @@ -50,6 +50,9 @@ public function setUp(): void SchemaInterface::COLUMNS => ['id', 'balance'], SchemaInterface::SCHEMA => [], SchemaInterface::RELATIONS => [], + SchemaInterface::GENERATED_FIELDS => [ + 'balance' => SchemaInterface::GENERATED_DB, // sequence + ], ], ])); }