* * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace CodeIgniter\Database\SQLite3; use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\RawSql; use InvalidArgumentException; /** * Builder for SQLite3 */ class Builder extends BaseBuilder { /** * Default installs of SQLite typically do not * support limiting delete clauses. * * @var bool */ protected $canLimitDeletes = false; /** * Default installs of SQLite do no support * limiting update queries in combo with WHERE. * * @var bool */ protected $canLimitWhereUpdates = false; /** * ORDER BY random keyword * * @var array */ protected $randomKeyword = [ 'RANDOM()', ]; /** * @var array */ protected $supportedIgnoreStatements = [ 'insert' => 'OR IGNORE', ]; /** * Replace statement * * Generates a platform-specific replace string from the supplied data */ protected function _replace(string $table, array $keys, array $values): string { return 'INSERT OR ' . parent::_replace($table, $keys, $values); } /** * Generates a platform-specific truncate string from the supplied data * * If the database does not support the TRUNCATE statement, * then this method maps to 'DELETE FROM table' */ protected function _truncate(string $table): string { return 'DELETE FROM ' . $table; } /** * Generates a platform-specific batch update string from the supplied data */ protected function _updateBatch(string $table, array $keys, array $values): string { if (version_compare($this->db->getVersion(), '3.33.0') >= 0) { return parent::_updateBatch($table, $keys, $values); } $constraints = $this->QBOptions['constraints'] ?? []; if ($constraints === []) { if ($this->db->DBDebug) { throw new DatabaseException('You must specify a constraint to match on for batch updates.'); } return ''; // @codeCoverageIgnore } if (count($constraints) > 1 || isset($this->QBOptions['setQueryAsData']) || (current($constraints) instanceof RawSql)) { throw new DatabaseException('You are trying to use a feature which requires SQLite version 3.33 or higher.'); } $index = current($constraints); $ids = []; $final = []; foreach ($values as $val) { $val = array_combine($keys, $val); $ids[] = $val[$index]; foreach (array_keys($val) as $field) { if ($field !== $index) { $final[$field][] = 'WHEN ' . $index . ' = ' . $val[$index] . ' THEN ' . $val[$field]; } } } $cases = ''; foreach ($final as $k => $v) { $cases .= $k . " = CASE \n" . implode("\n", $v) . "\n" . 'ELSE ' . $k . ' END, '; } $this->where($index . ' IN(' . implode(',', $ids) . ')', null, false); return 'UPDATE ' . $this->compileIgnore('update') . $table . ' SET ' . substr($cases, 0, -2) . $this->compileWhereHaving('QBWhere'); } /** * Generates a platform-specific upsertBatch string from the supplied data * * @throws DatabaseException */ protected function _upsertBatch(string $table, array $keys, array $values): string { $sql = $this->QBOptions['sql'] ?? ''; // if this is the first iteration of batch then we need to build skeleton sql if ($sql === '') { $constraints = $this->QBOptions['constraints'] ?? []; if (empty($constraints)) { $fieldNames = array_map(static fn ($columnName) => trim($columnName, '`'), $keys); $allIndexes = array_filter($this->db->getIndexData($table), static function ($index) use ($fieldNames) { $hasAllFields = count(array_intersect($index->fields, $fieldNames)) === count($index->fields); return ($index->type === 'PRIMARY' || $index->type === 'UNIQUE') && $hasAllFields; }); foreach (array_map(static fn ($index) => $index->fields, $allIndexes) as $index) { $constraints[] = current($index); break; } $constraints = $this->onConstraint($constraints)->QBOptions['constraints'] ?? []; } if (empty($constraints)) { if ($this->db->DBDebug) { throw new DatabaseException('No constraint found for upsert.'); } return ''; // @codeCoverageIgnore } $alias = $this->QBOptions['alias'] ?? '`excluded`'; if (strtolower($alias) !== '`excluded`') { throw new InvalidArgumentException('SQLite alias is always named "excluded". A custom alias cannot be used.'); } $updateFields = $this->QBOptions['updateFields'] ?? $this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ?? []; $sql = 'INSERT INTO ' . $table . ' ('; $sql .= implode(', ', array_map(static fn ($columnName) => $columnName, $keys)); $sql .= ")\n"; $sql .= '{:_table_:}'; $sql .= 'ON CONFLICT(' . implode(',', $constraints) . ")\n"; $sql .= "DO UPDATE SET\n"; $sql .= implode( ",\n", array_map( static fn ($key, $value) => $key . ($value instanceof RawSql ? " = {$value}" : " = {$alias}.{$value}"), array_keys($updateFields), $updateFields ) ); $this->QBOptions['sql'] = $sql; } if (isset($this->QBOptions['setQueryAsData'])) { $hasWhere = stripos($this->QBOptions['setQueryAsData'], 'WHERE') > 0; $data = $this->QBOptions['setQueryAsData'] . ($hasWhere ? '' : "\nWHERE 1 = 1\n"); } else { $data = 'VALUES ' . implode(', ', $this->formatValues($values)) . "\n"; } return str_replace('{:_table_:}', $data, $sql); } /** * Generates a platform-specific batch update string from the supplied data */ protected function _deleteBatch(string $table, array $keys, array $values): string { $sql = $this->QBOptions['sql'] ?? ''; // if this is the first iteration of batch then we need to build skeleton sql if ($sql === '') { $constraints = $this->QBOptions['constraints'] ?? []; if ($constraints === []) { if ($this->db->DBDebug) { throw new DatabaseException('You must specify a constraint to match on for batch deletes.'); // @codeCoverageIgnore } return ''; // @codeCoverageIgnore } $sql = 'DELETE FROM ' . $table . "\n"; if (current($constraints) instanceof RawSql && $this->db->DBDebug) { throw new DatabaseException('You cannot use RawSql for constraint in SQLite.'); // @codeCoverageIgnore } if (is_string(current(array_keys($constraints)))) { $concat1 = implode(' || ', array_keys($constraints)); $concat2 = implode(' || ', array_values($constraints)); } else { $concat1 = implode(' || ', $constraints); $concat2 = $concat1; } $sql .= "WHERE {$concat1} IN (SELECT {$concat2} FROM (\n{:_table_:}))"; // where is not supported if ($this->QBWhere !== [] && $this->db->DBDebug) { throw new DatabaseException('You cannot use WHERE with SQLite.'); // @codeCoverageIgnore } $this->QBOptions['sql'] = $sql; } if (isset($this->QBOptions['setQueryAsData'])) { $data = $this->QBOptions['setQueryAsData']; } else { $data = implode( " UNION ALL\n", array_map( static fn ($value) => 'SELECT ' . implode(', ', array_map( static fn ($key, $index) => $index . ' ' . $key, $keys, $value )), $values ) ) . "\n"; } return str_replace('{:_table_:}', $data, $sql); } }