1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Yiisoft\Db\Oracle; |
||
6 | |||
7 | use JsonException; |
||
8 | use Yiisoft\Db\Exception\Exception; |
||
9 | use Yiisoft\Db\Exception\InvalidArgumentException; |
||
10 | use Yiisoft\Db\Exception\InvalidConfigException; |
||
11 | use Yiisoft\Db\Exception\NotSupportedException; |
||
12 | use Yiisoft\Db\Expression\Expression; |
||
13 | use Yiisoft\Db\Query\QueryInterface; |
||
14 | use Yiisoft\Db\QueryBuilder\AbstractDMLQueryBuilder; |
||
15 | |||
16 | use function array_map; |
||
17 | use function implode; |
||
18 | use function count; |
||
19 | |||
20 | /** |
||
21 | * Implements a DML (Data Manipulation Language) SQL statements for Oracle Server. |
||
22 | */ |
||
23 | final class DMLQueryBuilder extends AbstractDMLQueryBuilder |
||
24 | { |
||
25 | /** |
||
26 | * @throws Exception |
||
27 | * @throws InvalidArgumentException |
||
28 | * @throws InvalidConfigException |
||
29 | * @throws NotSupportedException |
||
30 | */ |
||
31 | 26 | public function batchInsert(string $table, array $columns, iterable $rows, array &$params = []): string |
|
32 | { |
||
33 | 26 | if (empty($rows)) { |
|
34 | 1 | return ''; |
|
35 | } |
||
36 | |||
37 | 25 | $columns = $this->extractColumnNames($rows, $columns); |
|
38 | 25 | $values = $this->prepareBatchInsertValues($table, $rows, $columns, $params); |
|
39 | |||
40 | 25 | if (empty($values)) { |
|
41 | 1 | return ''; |
|
42 | } |
||
43 | |||
44 | 24 | $tableAndColumns = ' INTO ' . $this->quoter->quoteTableName($table); |
|
45 | |||
46 | 24 | if (count($columns) > 0) { |
|
47 | 22 | $quotedColumnNames = array_map([$this->quoter, 'quoteColumnName'], $columns); |
|
48 | |||
49 | 22 | $tableAndColumns .= ' (' . implode(', ', $quotedColumnNames) . ')'; |
|
50 | } |
||
51 | |||
52 | 24 | $tableAndColumns .= ' VALUES '; |
|
53 | |||
54 | 24 | return 'INSERT ALL' . $tableAndColumns . implode($tableAndColumns, $values) . ' SELECT 1 FROM SYS.DUAL'; |
|
55 | } |
||
56 | |||
57 | /** |
||
58 | * @throws Exception |
||
59 | * @throws NotSupportedException |
||
60 | */ |
||
61 | 1 | public function insertWithReturningPks(string $table, QueryInterface|array $columns, array &$params = []): string |
|
62 | { |
||
63 | 1 | throw new NotSupportedException(__METHOD__ . ' is not supported by Oracle.'); |
|
64 | } |
||
65 | |||
66 | /** |
||
67 | * @link https://docs.oracle.com/cd/B28359_01/server.111/b28286/statements_9016.htm#SQLRF01606 |
||
68 | * |
||
69 | * @throws Exception |
||
70 | * @throws InvalidArgumentException |
||
71 | * @throws InvalidConfigException |
||
72 | * @throws JsonException |
||
73 | * @throws NotSupportedException |
||
74 | */ |
||
75 | 36 | public function upsert( |
|
76 | string $table, |
||
77 | QueryInterface|array $insertColumns, |
||
78 | array|bool $updateColumns, |
||
79 | array &$params = [] |
||
80 | ): string { |
||
81 | 36 | $constraints = []; |
|
82 | |||
83 | 36 | [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns( |
|
84 | 36 | $table, |
|
85 | 36 | $insertColumns, |
|
86 | 36 | $updateColumns, |
|
87 | 36 | $constraints |
|
88 | 36 | ); |
|
89 | |||
90 | 36 | if (empty($uniqueNames)) { |
|
91 | 2 | return $this->insert($table, $insertColumns, $params); |
|
92 | } |
||
93 | |||
94 | 34 | $onCondition = ['or']; |
|
95 | 34 | $quotedTableName = $this->quoter->quoteTableName($table); |
|
96 | |||
97 | 34 | foreach ($constraints as $constraint) { |
|
98 | 34 | $columnNames = (array) $constraint->getColumnNames(); |
|
99 | 34 | $constraintCondition = ['and']; |
|
100 | /** @psalm-var string[] $columnNames */ |
||
101 | 34 | foreach ($columnNames as $name) { |
|
102 | 34 | $quotedName = $this->quoter->quoteColumnName($name); |
|
103 | 34 | $constraintCondition[] = "$quotedTableName.$quotedName=\"EXCLUDED\".$quotedName"; |
|
104 | } |
||
105 | |||
106 | 34 | $onCondition[] = $constraintCondition; |
|
107 | } |
||
108 | |||
109 | 34 | $on = $this->queryBuilder->buildCondition($onCondition, $params); |
|
110 | |||
111 | 34 | [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params); |
|
112 | |||
113 | 34 | if (!empty($placeholders)) { |
|
114 | 21 | $usingSelectValues = []; |
|
115 | |||
116 | 21 | foreach ($insertNames as $index => $name) { |
|
117 | 21 | $usingSelectValues[$name] = new Expression($placeholders[$index]); |
|
118 | } |
||
119 | |||
120 | 21 | $values = $this->queryBuilder->buildSelect($usingSelectValues, $params) |
|
121 | 21 | . ' ' . $this->queryBuilder->buildFrom(['DUAL'], $params); |
|
122 | } |
||
123 | |||
124 | 34 | $insertValues = []; |
|
125 | 34 | $mergeSql = 'MERGE INTO ' . $quotedTableName . ' USING (' . $values . ') "EXCLUDED" ON (' . $on . ')'; |
|
126 | |||
127 | 34 | foreach ($insertNames as $quotedName) { |
|
128 | 34 | $insertValues[] = '"EXCLUDED".' . $quotedName; |
|
129 | } |
||
130 | |||
131 | 34 | $insertSql = 'INSERT (' . implode(', ', $insertNames) . ')' . ' VALUES (' . implode(', ', $insertValues) . ')'; |
|
132 | |||
133 | 34 | if ($updateColumns === false || $updateNames === []) { |
|
0 ignored issues
–
show
introduced
by
![]() |
|||
134 | /** there are no columns to update */ |
||
135 | 14 | return "$mergeSql WHEN NOT MATCHED THEN $insertSql"; |
|
136 | } |
||
137 | |||
138 | 20 | if ($updateColumns === true) { |
|
0 ignored issues
–
show
|
|||
139 | 10 | $updateColumns = []; |
|
140 | /** @psalm-var string[] $updateNames */ |
||
141 | 10 | foreach ($updateNames as $quotedName) { |
|
142 | 10 | $updateColumns[$quotedName] = new Expression('"EXCLUDED".' . $quotedName); |
|
143 | } |
||
144 | } |
||
145 | |||
146 | 20 | [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params); |
|
147 | 20 | $updateSql = 'UPDATE SET ' . implode(', ', $updates); |
|
148 | |||
149 | 20 | return "$mergeSql WHEN MATCHED THEN $updateSql WHEN NOT MATCHED THEN $insertSql"; |
|
150 | } |
||
151 | |||
152 | 77 | protected function prepareInsertValues(string $table, array|QueryInterface $columns, array $params = []): array |
|
153 | { |
||
154 | 77 | if (empty($columns)) { |
|
155 | 4 | $names = []; |
|
156 | 4 | $placeholders = []; |
|
157 | 4 | $tableSchema = $this->schema->getTableSchema($table); |
|
158 | |||
159 | 4 | if ($tableSchema !== null) { |
|
160 | 4 | if (!empty($tableSchema->getPrimaryKey())) { |
|
161 | 2 | $columns = $tableSchema->getPrimaryKey(); |
|
162 | } else { |
||
163 | 2 | $columns = [current($tableSchema->getColumns())->getName()]; |
|
164 | } |
||
165 | |||
166 | 4 | foreach ($columns as $name) { |
|
167 | 4 | $names[] = $this->quoter->quoteColumnName($name); |
|
168 | 4 | $placeholders[] = 'DEFAULT'; |
|
169 | } |
||
170 | } |
||
171 | |||
172 | 4 | return [$names, $placeholders, '', $params]; |
|
173 | } |
||
174 | |||
175 | 73 | return parent::prepareInsertValues($table, $columns, $params); |
|
176 | } |
||
177 | |||
178 | 5 | public function resetSequence(string $table, int|string $value = null): string |
|
179 | { |
||
180 | 5 | $tableSchema = $this->schema->getTableSchema($table); |
|
181 | |||
182 | 5 | if ($tableSchema === null) { |
|
183 | 1 | throw new InvalidArgumentException("Table not found: '$table'."); |
|
184 | } |
||
185 | |||
186 | 4 | $sequenceName = $tableSchema->getSequenceName(); |
|
187 | |||
188 | 4 | if ($sequenceName === null) { |
|
189 | 2 | throw new InvalidArgumentException("There is not sequence associated with table '$table'."); |
|
190 | } |
||
191 | |||
192 | 2 | if ($value === null && count($tableSchema->getPrimaryKey()) > 1) { |
|
193 | throw new InvalidArgumentException("Can't reset sequence for composite primary key in table: $table"); |
||
194 | } |
||
195 | |||
196 | /** |
||
197 | * Oracle needs at least many queries to reset a sequence (see adding transactions and/or use an alter method to |
||
198 | * avoid grant issue?) |
||
199 | */ |
||
200 | 2 | return 'declare |
|
201 | 2 | lastSeq number' . ($value !== null ? (' := ' . $value) : '') . '; |
|
202 | 2 | begin' . ($value === null ? ' |
|
203 | 2 | SELECT MAX("' . $tableSchema->getPrimaryKey()[0] . '") + 1 INTO lastSeq FROM "' . $tableSchema->getName() . '";' : '') . ' |
|
204 | if lastSeq IS NULL then lastSeq := 1; end if; |
||
205 | 2 | execute immediate \'DROP SEQUENCE "' . $sequenceName . '"\'; |
|
206 | 2 | execute immediate \'CREATE SEQUENCE "' . $sequenceName . '" START WITH \' || lastSeq || \' INCREMENT BY 1 NOMAXVALUE NOCACHE\'; |
|
207 | 2 | end;'; |
|
208 | } |
||
209 | } |
||
210 |