3
declare(strict_types=1);
6
* This file is part of CodeIgniter 4 framework.
8
* (c) CodeIgniter Foundation <admin@codeigniter.com>
10
* For the full copyright and license information, please view
11
* the LICENSE file that was distributed with this source code.
14
namespace CodeIgniter\Database\OCI8;
16
use CodeIgniter\Database\BaseBuilder;
17
use CodeIgniter\Database\Exceptions\DatabaseException;
18
use CodeIgniter\Database\RawSql;
23
class Builder extends BaseBuilder
26
* Identifier escape character
30
protected $escapeChar = '"';
33
* ORDER BY random keyword
37
protected $randomKeyword = [
38
'"DBMS_RANDOM"."RANDOM"',
44
* @used-by CI_DB_driver::count_all()
45
* @used-by BaseBuilder::count_all_results()
49
protected $countString = 'SELECT COUNT(1) ';
54
* If we use LIMIT, we'll add a field that will
55
* throw off num_fields later.
59
protected $limitUsed = false;
62
* A reference to the database connection.
69
* Generates a platform-specific insert string from the supplied data.
71
protected function _insertBatch(string $table, array $keys, array $values): string
73
$sql = $this->QBOptions['sql'] ?? '';
75
// if this is the first iteration of batch then we need to build skeleton sql
77
$insertKeys = implode(', ', $keys);
78
$hasPrimaryKey = in_array('PRIMARY', array_column($this->db->getIndexData($table), 'type'), true);
81
$sql = 'INSERT' . ($hasPrimaryKey ? '' : ' ALL') . ' INTO ' . $table . ' (' . $insertKeys . ")\n{:_table_:}";
83
$this->QBOptions['sql'] = $sql;
86
if (isset($this->QBOptions['setQueryAsData'])) {
87
$data = $this->QBOptions['setQueryAsData'];
90
" FROM DUAL UNION ALL\n",
92
static fn ($value) => 'SELECT ' . implode(', ', array_map(
93
static fn ($key, $index) => $index . ' ' . $key,
102
return str_replace('{:_table_:}', $data, $sql);
106
* Generates a platform-specific replace string from the supplied data
108
protected function _replace(string $table, array $keys, array $values): string
110
$fieldNames = array_map(static fn ($columnName) => trim($columnName, '"'), $keys);
112
$uniqueIndexes = array_filter($this->db->getIndexData($table), static function ($index) use ($fieldNames) {
113
$hasAllFields = count(array_intersect($index->fields, $fieldNames)) === count($index->fields);
115
return ($index->type === 'PRIMARY') && $hasAllFields;
117
$replaceableFields = array_filter($keys, static function ($columnName) use ($uniqueIndexes) {
118
foreach ($uniqueIndexes as $index) {
119
if (in_array(trim($columnName, '"'), $index->fields, true)) {
127
$sql = 'MERGE INTO ' . $table . "\n USING (SELECT ";
129
$sql .= implode(', ', array_map(static fn ($columnName, $value) => $value . ' ' . $columnName, $keys, $values));
131
$sql .= ' FROM DUAL) "_replace" ON ( ';
134
$onList[] = '1 != 1';
136
foreach ($uniqueIndexes as $index) {
137
$onList[] = '(' . implode(' AND ', array_map(static fn ($columnName) => $table . '."' . $columnName . '" = "_replace"."' . $columnName . '"', $index->fields)) . ')';
140
$sql .= implode(' OR ', $onList) . ') WHEN MATCHED THEN UPDATE SET ';
142
$sql .= implode(', ', array_map(static fn ($columnName) => $columnName . ' = "_replace".' . $columnName, $replaceableFields));
144
$sql .= ' WHEN NOT MATCHED THEN INSERT (' . implode(', ', $replaceableFields) . ') VALUES ';
146
return $sql . (' (' . implode(', ', array_map(static fn ($columnName) => '"_replace".' . $columnName, $replaceableFields)) . ')');
150
* Generates a platform-specific truncate string from the supplied data
152
* If the database does not support the truncate() command,
153
* then this method maps to 'DELETE FROM table'
155
protected function _truncate(string $table): string
157
return 'TRUNCATE TABLE ' . $table;
161
* Compiles a delete string and runs the query
163
* @param mixed $where
167
* @throws DatabaseException
169
public function delete($where = '', ?int $limit = null, bool $resetData = true)
171
if ($limit !== null && $limit !== 0) {
172
$this->QBLimit = $limit;
175
return parent::delete($where, null, $resetData);
179
* Generates a platform-specific delete string from the supplied data
181
protected function _delete(string $table): string
183
if ($this->QBLimit) {
184
$this->where('rownum <= ', $this->QBLimit, false);
185
$this->QBLimit = false;
188
return parent::_delete($table);
192
* Generates a platform-specific update string from the supplied data
194
protected function _update(string $table, array $values): string
198
foreach ($values as $key => $val) {
199
$valStr[] = $key . ' = ' . $val;
202
if ($this->QBLimit) {
203
$this->where('rownum <= ', $this->QBLimit, false);
206
return 'UPDATE ' . $this->compileIgnore('update') . $table . ' SET ' . implode(', ', $valStr)
207
. $this->compileWhereHaving('QBWhere')
208
. $this->compileOrderBy();
212
* Generates a platform-specific LIMIT clause.
214
protected function _limit(string $sql, bool $offsetIgnore = false): string
216
$offset = (int) ($offsetIgnore === false ? $this->QBOffset : 0);
217
if (version_compare($this->db->getVersion(), '12.1', '>=')) {
218
// OFFSET-FETCH can be used only with the ORDER BY clause
219
if (empty($this->QBOrderBy)) {
220
$sql .= ' ORDER BY 1';
223
return $sql . ' OFFSET ' . $offset . ' ROWS FETCH NEXT ' . $this->QBLimit . ' ROWS ONLY';
226
$this->limitUsed = true;
227
$limitTemplateQuery = 'SELECT * FROM (SELECT INNER_QUERY.*, ROWNUM RNUM FROM (%s) INNER_QUERY WHERE ROWNUM < %d)' . ($offset !== 0 ? ' WHERE RNUM >= %d' : '');
229
return sprintf($limitTemplateQuery, $sql, $offset + $this->QBLimit + 1, $offset);
233
* Resets the query builder values. Called by the get() function
235
protected function resetSelect()
237
$this->limitUsed = false;
238
parent::resetSelect();
242
* Generates a platform-specific batch update string from the supplied data
244
protected function _updateBatch(string $table, array $keys, array $values): string
246
$sql = $this->QBOptions['sql'] ?? '';
248
// if this is the first iteration of batch then we need to build skeleton sql
250
$constraints = $this->QBOptions['constraints'] ?? [];
252
if ($constraints === []) {
253
if ($this->db->DBDebug) {
254
throw new DatabaseException('You must specify a constraint to match on for batch updates.');
257
return ''; // @codeCoverageIgnore
260
$updateFields = $this->QBOptions['updateFields'] ??
261
$this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ??
264
$alias = $this->QBOptions['alias'] ?? '"_u"';
266
// Oracle doesn't support ignore on updates so we will use MERGE
267
$sql = 'MERGE INTO ' . $table . "\n";
269
$sql .= "USING (\n{:_table_:}";
271
$sql .= ') ' . $alias . "\n";
273
$sql .= 'ON (' . implode(
276
static fn ($key, $value) => (
277
($value instanceof RawSql && is_string($key))
279
$table . '.' . $key . ' = ' . $value
282
$value instanceof RawSql
286
$table . '.' . $value . ' = ' . $alias . '.' . $value
289
array_keys($constraints),
294
$sql .= "WHEN MATCHED THEN UPDATE\n";
301
static fn ($key, $value) => $table . '.' . $key . ($value instanceof RawSql ?
303
' = ' . $alias . '.' . $value),
304
array_keys($updateFields),
309
$this->QBOptions['sql'] = $sql;
312
if (isset($this->QBOptions['setQueryAsData'])) {
313
$data = $this->QBOptions['setQueryAsData'];
318
static fn ($value) => 'SELECT ' . implode(', ', array_map(
319
static fn ($key, $index) => $index . ' ' . $key,
328
return str_replace('{:_table_:}', $data, $sql);
332
* Generates a platform-specific upsertBatch string from the supplied data
334
* @throws DatabaseException
336
protected function _upsertBatch(string $table, array $keys, array $values): string
338
$sql = $this->QBOptions['sql'] ?? '';
340
// if this is the first iteration of batch then we need to build skeleton sql
342
$constraints = $this->QBOptions['constraints'] ?? [];
344
if (empty($constraints)) {
345
$fieldNames = array_map(static fn ($columnName) => trim($columnName, '"'), $keys);
347
$uniqueIndexes = array_filter($this->db->getIndexData($table), static function ($index) use ($fieldNames) {
348
$hasAllFields = count(array_intersect($index->fields, $fieldNames)) === count($index->fields);
350
return ($index->type === 'PRIMARY' || $index->type === 'UNIQUE') && $hasAllFields;
353
// only take first index
354
foreach ($uniqueIndexes as $index) {
355
$constraints = $index->fields;
359
$constraints = $this->onConstraint($constraints)->QBOptions['constraints'] ?? [];
362
if (empty($constraints)) {
363
if ($this->db->DBDebug) {
364
throw new DatabaseException('No constraint found for upsert.');
367
return ''; // @codeCoverageIgnore
370
$alias = $this->QBOptions['alias'] ?? '"_upsert"';
372
$updateFields = $this->QBOptions['updateFields'] ?? $this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ?? [];
374
$sql = 'MERGE INTO ' . $table . "\nUSING (\n{:_table_:}";
376
$sql .= ") {$alias}\nON (";
381
static fn ($key, $value) => (
382
($value instanceof RawSql && is_string($key))
384
$table . '.' . $key . ' = ' . $value
387
$value instanceof RawSql
391
$table . '.' . $value . ' = ' . $alias . '.' . $value
394
array_keys($constraints),
399
$sql .= "WHEN MATCHED THEN UPDATE SET\n";
404
static fn ($key, $value) => $key . ($value instanceof RawSql ?
406
" = {$alias}.{$value}"),
407
array_keys($updateFields),
412
$sql .= "\nWHEN NOT MATCHED THEN INSERT (" . implode(', ', $keys) . ")\nVALUES ";
415
. implode(', ', array_map(static fn ($columnName) => "{$alias}.{$columnName}", $keys))
418
$this->QBOptions['sql'] = $sql;
421
if (isset($this->QBOptions['setQueryAsData'])) {
422
$data = $this->QBOptions['setQueryAsData'];
425
" FROM DUAL UNION ALL\n",
427
static fn ($value) => 'SELECT ' . implode(', ', array_map(
428
static fn ($key, $index) => $index . ' ' . $key,
437
return str_replace('{:_table_:}', $data, $sql);
441
* Generates a platform-specific batch update string from the supplied data
443
protected function _deleteBatch(string $table, array $keys, array $values): string
445
$sql = $this->QBOptions['sql'] ?? '';
447
// if this is the first iteration of batch then we need to build skeleton sql
449
$constraints = $this->QBOptions['constraints'] ?? [];
451
if ($constraints === []) {
452
if ($this->db->DBDebug) {
453
throw new DatabaseException('You must specify a constraint to match on for batch deletes.'); // @codeCoverageIgnore
456
return ''; // @codeCoverageIgnore
459
$alias = $this->QBOptions['alias'] ?? '_u';
461
$sql = 'DELETE ' . $table . "\n";
463
$sql .= "WHERE EXISTS (SELECT * FROM (\n{:_table_:}";
465
$sql .= ') ' . $alias . "\n";
467
$sql .= 'WHERE ' . implode(
470
static fn ($key, $value) => (
471
$value instanceof RawSql ?
475
$table . '.' . $key . ' = ' . $alias . '.' . $value :
476
$table . '.' . $value . ' = ' . $alias . '.' . $value
479
array_keys($constraints),
484
// convert binds in where
485
foreach ($this->QBWhere as $key => $where) {
486
foreach ($this->binds as $field => $bind) {
487
$this->QBWhere[$key]['condition'] = str_replace(':' . $field . ':', $bind[0], $where['condition']);
491
$sql .= ' ' . str_replace(
494
$this->compileWhereHaving('QBWhere')
497
$this->QBOptions['sql'] = $sql;
500
if (isset($this->QBOptions['setQueryAsData'])) {
501
$data = $this->QBOptions['setQueryAsData'];
504
" FROM DUAL UNION ALL\n",
506
static fn ($value) => 'SELECT ' . implode(', ', array_map(
507
static fn ($key, $index) => $index . ' ' . $key,
516
return str_replace('{:_table_:}', $data, $sql);
520
* Gets column names from a select query
522
protected function fieldsFromQuery(string $sql): array
524
return $this->db->query('SELECT * FROM (' . $sql . ') "_u_" WHERE ROWNUM = 1')->getFieldNames();