packages/ztd-query-postgres/src/Rewrite/Transformer/Merge/RowActions.php
1<?php
2
3declare(strict_types=1);
4
5namespace ZtdQuery\Platform\Postgres\Rewrite\Transformer\Merge;
6
7use ZtdQuery\Exception\UnsupportedSqlException;
8use ZtdQuery\Platform\Postgres\Rewrite\Transformer\InsertRowRenderer;
9use ZtdQuery\Platform\Postgres\Rewrite\Transformer\InsertSelectRenderer;
10use ZtdQuery\Platform\Postgres\Sql\Merge\PgSqlMergeActionKind;
11use ZtdQuery\Platform\Postgres\Sql\Merge\PgSqlMergeClause;
12use ZtdQuery\Platform\Postgres\Sql\Merge\PgSqlMergeMatchKind;
13use ZtdQuery\Platform\Postgres\Sql\Merge\PgSqlMergeStatement;
14use ZtdQuery\Platform\Postgres\Sql\PgSqlIdentifierQuoter;
15use ZtdQuery\Rewrite\ShadowIdentityAllocator;
16
17/**
18 * Row actions operations for PostgreSQL merge.
19 *
20 * @visibility root
21 */
22final class RowActions
23{
24 private readonly InsertSelectRenderer $insertSelectRenderer;
25 private readonly PgSqlIdentifierQuoter $quoter;
26 private readonly InsertRowRenderer $rowRenderer;
27
28 /**
29 * Supplies the dependencies used by this RowActions.
30 */
31 public function __construct(InsertSelectRenderer $insertSelectRenderer, PgSqlIdentifierQuoter $quoter, InsertRowRenderer $rowRenderer)
32 {
33 $this->insertSelectRenderer = $insertSelectRenderer;
34 $this->quoter = $quoter;
35 $this->rowRenderer = $rowRenderer;
36 }
37
38 /**
39 * @param list<string> $columns
40 * @param list<string> $effectiveConditions
41 */
42 public function unchangedRows(
43 PgSqlMergeStatement $statement,
44 array $columns,
45 array $effectiveConditions,
46 ): string {
47 $qualifier = $this->quoter->quote($statement->targetAlias);
48 $selects = [];
49 foreach ($columns as $column) {
50 $quoted = $this->quoter->quote($column);
51 $selects[] = $qualifier . '.' . $quoted . ' AS ' . $quoted;
52 }
53
54 $modifications = [];
55 foreach ($statement->clauses as $index => $clause) {
56 if ($clause->matchKind !== PgSqlMergeMatchKind::Matched
57 || !in_array($clause->actionKind, [PgSqlMergeActionKind::Update, PgSqlMergeActionKind::Delete], true)
58 ) {
59 continue;
60 }
61 $modifications[] = 'EXISTS (SELECT 1 FROM ' . $statement->sourceSql
62 . ' WHERE (' . $statement->joinConditionSql . ') AND (' . $effectiveConditions[$index] . '))';
63 }
64
65 $sql = 'SELECT ' . implode(', ', $selects)
66 . ' FROM ' . $statement->targetSql . ' AS ' . $qualifier;
67 if ($modifications !== []) {
68 $sql .= ' WHERE NOT (' . implode(' OR ', $modifications) . ')';
69 }
70
71 return $sql;
72 }
73
74 /**
75 * @param list<string> $columns
76 * @param array<string, string> $defaults
77 * @throws UnsupportedSqlException
78 */
79 public function updatedRows(
80 string $sql,
81 PgSqlMergeStatement $statement,
82 PgSqlMergeClause $clause,
83 array $columns,
84 array $defaults,
85 string $effectiveCondition,
86 ): string {
87 foreach (array_keys($clause->assignments) as $column) {
88 if (!in_array($column, $columns, true)) {
89 throw new UnsupportedSqlException($sql, 'MERGE UPDATE references an unknown target column');
90 }
91 }
92
93 $qualifier = $this->quoter->quote($statement->targetAlias);
94 $selects = [];
95 foreach ($columns as $column) {
96 $quoted = $this->quoter->quote($column);
97 $expression = $clause->assignments[$column] ?? null;
98 if ($expression === null) {
99 $expression = $qualifier . '.' . $quoted;
100 } elseif (strcasecmp($expression, 'DEFAULT') === 0) {
101 $expression = $defaults[$column] ?? 'NULL';
102 }
103 $selects[] = $expression . ' AS ' . $quoted;
104 }
105
106 return 'SELECT ' . implode(', ', $selects)
107 . ' FROM ' . $statement->targetSql . ' AS ' . $qualifier
108 . ' JOIN ' . $statement->sourceSql
109 . ' ON (' . $statement->joinConditionSql . ')'
110 . ' WHERE ' . $effectiveCondition;
111 }
112
113 /**
114 * @template T
115 * @param list<string> $columns
116 * @param array<string, string> $defaults
117 * @param array<string, \ZtdQuery\Schema\Key\IdentityGenerationStrategy> $identityStrategies
118 * @param array<int, array<string, T>> $existingRows
119 * @throws UnsupportedSqlException
120 */
121 public function insertedRows(
122 string $sql,
123 PgSqlMergeStatement $statement,
124 PgSqlMergeClause $clause,
125 array $columns,
126 array $defaults,
127 array $identityStrategies,
128 array $existingRows,
129 string $effectiveCondition,
130 ): string {
131 if ($clause->insertColumns !== []) {
132 $sourceColumns = $clause->insertColumns;
133 } elseif ($clause->insertValues === []) {
134 $sourceColumns = [];
135 } else {
136 $sourceColumns = $columns;
137 }
138 foreach ($sourceColumns as $column) {
139 if (!in_array($column, $columns, true)) {
140 throw new UnsupportedSqlException($sql, 'MERGE INSERT references an unknown target column');
141 }
142 }
143
144 if (count($sourceColumns) !== count($clause->insertValues)) {
145 throw new UnsupportedSqlException($sql, 'MERGE INSERT values count does not match column count');
146 }
147 $providedExpressions = $this->rowRenderer->providedExpressions($sourceColumns, $clause->insertValues);
148 $generatedStarts = (new ShadowIdentityAllocator())->allocateSelectStarts(
149 $statement->targetTable,
150 $identityStrategies,
151 array_keys($providedExpressions),
152 $existingRows,
153 );
154 foreach ($generatedStarts as $column => $start) {
155 $providedExpressions[$column] = $this->insertSelectRenderer->renderGeneratedIdentity($start);
156 }
157 $projected = $this->rowRenderer->render($columns, $providedExpressions, $defaults);
158
159 $selects = [];
160 foreach ($projected as $column => $expression) {
161 $selects[] = $expression . ' AS ' . $this->quoter->quote($column);
162 }
163 $targetAlias = $this->quoter->quote($statement->targetAlias);
164
165 return 'SELECT ' . implode(', ', $selects)
166 . ' FROM ' . $statement->sourceSql
167 . ' WHERE NOT EXISTS (SELECT 1 FROM ' . $statement->targetSql . ' AS ' . $targetAlias
168 . ' WHERE ' . $statement->joinConditionSql . ')'
169 . ' AND (' . $effectiveCondition . ')';
170 }
171 /**
172 * Projects UPDATE and INSERT branches using their ordered effective conditions.
173 * @template T
174 * @param list<string> $columns
175 * @param array<string, string> $defaults
176 * @param array<string, \ZtdQuery\Schema\Key\IdentityGenerationStrategy> $identityStrategies
177 * @param array<int, array<string, T>> $existingRows
178 * @param array<int, string> $effectiveConditions
179 * @return list<string>
180 */
181 public function modifiedRows(string $sql, PgSqlMergeStatement $statement, array $columns, array $defaults, array $identityStrategies, array $existingRows, array $effectiveConditions): array
182 {
183 $parts = [];
184 foreach ($statement->clauses as $index => $clause) {
185 $effective = $effectiveConditions[$index];
186 if ($clause->actionKind === PgSqlMergeActionKind::Update) {
187 $parts[] = $this->updatedRows($sql, $statement, $clause, $columns, $defaults, $effective);
188 }
189 if ($clause->actionKind === PgSqlMergeActionKind::Insert) {
190 $parts[] = $this->insertedRows(
191 $sql,
192 $statement,
193 $clause,
194 $columns,
195 $defaults,
196 $identityStrategies,
197 $existingRows,
198 $effective,
199 );
200 }
201 }
202
203 return $parts;
204 }
205}
206