packages/ztd-query-core/src/Shadow/Mutation/Row/InsertMutation.php

1<?php
2
3declare(strict_types=1);
4
5namespace ZtdQuery\Shadow\Mutation\Row;
6
7use ZtdQuery\Exception\DuplicateKeyException;
8use ZtdQuery\Schema\Key\CandidateKeySet;
9use ZtdQuery\Schema\TableDefinition;
10use ZtdQuery\Shadow\Mutation\ConflictSearch;
11use ZtdQuery\Shadow\Mutation\DataMutation;
12use ZtdQuery\Shadow\Mutation\RowConstraints;
13use ZtdQuery\Shadow\Mutation\UpsertExpression;
14use ZtdQuery\Shadow\ShadowStore;
15
16/**
17 * Applies INSERT result rows to the shadow store.
18 *
19 * @phpstan-import-type Row from TableDefinition
20 */
21final class InsertMutation implements DataMutation
22{
23    /**
24     * Target table to insert into.
25     *
26     * @var string
27     */
28    private string $tableName;
29
30    /**
31     * Whether to ignore duplicate key errors.
32     *
33     * @var bool
34     */
35    private bool $ignore;
36
37    /**
38     * Table definition for constraint validation.
39     *
40     * @var TableDefinition|null
41     */
42    private ?TableDefinition $tableDefinition;
43
44    /**
45     * Original SQL statement for exception messages.
46     *
47     * @var string
48     */
49    private string $sql;
50
51    /**
52     * Whether constraint validation is enabled.
53     *
54     * @var bool
55     */
56    private bool $validateConstraints;
57
58    private CandidateKeySet $candidateKeys;
59
60    private ?UpsertExpression $conflictPredicate;
61
62    private RowConstraints $constraints;
63
64    private ConflictSearch $conflicts;
65
66    /**
67     * @param string $tableName Target table.
68     * @param array<int, string> $primaryKeys Primary key columns.
69     * @param bool $ignore Whether to ignore duplicates (INSERT IGNORE).
70     * @param TableDefinition|null $tableDefinition Table definition for constraint validation.
71     * @param string $sql Original SQL statement for exception messages.
72     * @param bool $validateConstraints Whether to validate constraints.
73     * @param CandidateKeySet|null $candidateKeys Candidate keys used for duplicate detection.
74     * @param UpsertExpression|null $conflictPredicate Condition that controls candidate-key eligibility.
75     */
76    public function __construct(
77        string $tableName,
78        array $primaryKeys = [],
79        bool $ignore = false,
80        ?TableDefinition $tableDefinition = null,
81        string $sql = '',
82        bool $validateConstraints = false,
83        ?CandidateKeySet $candidateKeys = null,
84        ?UpsertExpression $conflictPredicate = null,
85    ) {
86        $this->tableName = $tableName;
87        $this->ignore = $ignore;
88        $this->tableDefinition = $tableDefinition;
89        $this->sql = $sql;
90        $this->validateConstraints = $validateConstraints;
91        $this->candidateKeys = $candidateKeys ?? CandidateKeySet::fromSchema($primaryKeys);
92        $this->conflictPredicate = $conflictPredicate;
93        $this->constraints = new RowConstraints($this->tableDefinition, $this->tableName, $this->sql);
94        $this->conflicts = new ConflictSearch($this->candidateKeys, $this->conflictPredicate, $this->tableName);
95    }
96
97    /**
98     * {@inheritDoc}
99     *
100     * @throws DuplicateKeyException When a row would collide with one already there on a candidate key
101     */
102    public function apply(ShadowStore $store, array $rows): void
103    {
104        $existingRows = $store->get($this->tableName);
105        $filteredRows = [];
106
107        foreach ($rows as $row) {
108            if ($this->validateConstraints && $this->tableDefinition !== null) {
109                $this->constraints->assertNoNullWhereNoneIsAllowed($row);
110            }
111
112            $conflict = $this->conflicts->of($row, $existingRows);
113            if ($conflict !== null) {
114                if ($this->ignore) {
115                    continue;
116                }
117
118                if ($this->validateConstraints) {
119                    throw new DuplicateKeyException(
120                        $this->sql,
121                        $this->tableName,
122                        $conflict->keyName,
123                        $conflict->values
124                    );
125                }
126            }
127
128            if ($this->validateConstraints && $this->tableDefinition !== null) {
129                $this->constraints->assertNoDuplicateUniqueKey($row, $existingRows);
130            }
131
132            $filteredRows[] = $row;
133            $existingRows[] = $row;
134        }
135
136        $store->insert($this->tableName, $filteredRows);
137    }
138
139    /**
140     * {@inheritDoc}
141     */
142    public function tableName(): string
143    {
144        return $this->tableName;
145    }
146
147}
148