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 7f610a4
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 11 deletions.
93 changes: 82 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,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();
Expand All @@ -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;
}
}
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
7 changes: 7 additions & 0 deletions src/SchemaInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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

0 comments on commit 7f610a4

Please sign in to comment.