Skip to content

Commit

Permalink
Support generated fields in the Insert command
Browse files Browse the repository at this point in the history
  • Loading branch information
roxblnfk committed Feb 2, 2024
1 parent b2f052b commit 7d3b728
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 12 deletions.
96 changes: 85 additions & 11 deletions src/Command/Database/Insert.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<non-empty-string, int> $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;
Expand All @@ -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
Expand All @@ -59,6 +71,7 @@ public function getStoreData(): array
public function execute(): void
{
$state = $this->state;
$returningFields = [];

if ($this->appendix !== []) {
$state->setData($this->appendix);
Expand All @@ -72,28 +85,65 @@ 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;

Check warning on line 91 in src/Command/Database/Insert.php

View check run for this annotation

Codecov / codecov/patch

src/Command/Database/Insert.php#L91

Added line #L91 was not covered by tests
}

$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 replace:
// $insert->returning(...\array_values($returning));
$insert->returning(\array_values($returning)[0]);

if ($insertID !== null && \count($this->primaryKeys) === 1) {
$fpk = $this->primaryKeys[0]; // first PK
if (!isset($data[$fpk])) {
$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);

Check warning on line 129 in src/Command/Database/Insert.php

View check run for this annotation

Codecov / codecov/patch

src/Command/Database/Insert.php#L129

Added line #L129 was not covered by tests
}
}
} 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();
Expand All @@ -103,4 +153,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;

Check warning on line 163 in src/Command/Database/Insert.php

View check run for this annotation

Codecov / codecov/patch

src/Command/Database/Insert.php#L163

Added line #L163 was not covered by tests
}

$data = $this->state->getData();

foreach ($this->generated as $field => $mode) {
if (($mode & (SchemaInterface::GENERATED_DB | SchemaInterface::GENERATED_PHP_INSERT)) === 0x0) {
continue;

Check warning on line 170 in src/Command/Database/Insert.php

View check run for this annotation

Codecov / codecov/patch

src/Command/Database/Insert.php#L170

Added line #L170 was not covered by tests
}

if (!isset($data[$field])) {
return true;
}
}

return true;

Check warning on line 178 in src/Command/Database/Insert.php

View check run for this annotation

Codecov / codecov/patch

src/Command/Database/Insert.php#L178

Added line #L178 was not covered by tests
}
}
4 changes: 4 additions & 0 deletions src/Mapper/DatabaseMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ abstract class DatabaseMapper implements MapperInterface
protected array $primaryKeys;
private ?TypecastInterface $typecast;
protected RelationMap $relationMap;
/** @var array<non-empty-string, int> */
private array $generatedFields;

public function __construct(
ORMInterface $orm,
Expand All @@ -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) {
Expand Down Expand Up @@ -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,
);
}

Expand Down
6 changes: 6 additions & 0 deletions src/SchemaInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case321;

use Cycle\ORM\Select;
use Cycle\ORM\Tests\Functional\Driver\Common\BaseTest;
use Cycle\ORM\Tests\Functional\Driver\Common\Integration\IntegrationTestTrait;
use Cycle\ORM\Tests\Traits\TableTrait;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
'id' => 'int',
],
Schema::SCHEMA => [],
Schema::GENERATED_FIELDS => [
'id' => Schema::GENERATED_DB, // autoincrement
],
],
'user2' => [
Schema::ENTITY => User2::class,
Expand All @@ -44,5 +47,8 @@
'id' => 'int',
],
Schema::SCHEMA => [],
Schema::GENERATED_FIELDS => [
'id' => Schema::GENERATED_DB, // autoincrement
],
],
];
3 changes: 3 additions & 0 deletions tests/ORM/Functional/Driver/Postgres/SerialColumnTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public function setUp(): void
SchemaInterface::COLUMNS => ['id', 'balance'],
SchemaInterface::SCHEMA => [],
SchemaInterface::RELATIONS => [],
SchemaInterface::GENERATED_FIELDS => [
'balance' => SchemaInterface::GENERATED_DB, // sequence
],
],
]));
}
Expand Down
5 changes: 5 additions & 0 deletions tests/ORM/Unit/Command/InsertCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ public function testCommandWithReturningInterfaceShouldUseIt()
->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])
Expand Down

0 comments on commit 7d3b728

Please sign in to comment.