Files
BESCMS/dayrui/System/Database/SQLite3/Connection.php
T

1356 lines
43 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* 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\BaseConnection;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\TableName;
use CodeIgniter\Exceptions\InvalidArgumentException;
use Exception;
use SQLite3;
use SQLite3Result;
use stdClass;
/**
* Connection for SQLite3
*
* @extends BaseConnection<SQLite3, SQLite3Result>
*/
class Connection extends BaseConnection
{
/**
* Database driver
*
* @var string
*/
public $DBDriver = 'SQLite3';
/**
* Identifier escape character
*
* @var string
*/
public $escapeChar = '"';
/**
* @var bool Enable Foreign Key constraint or not
*/
protected $foreignKeys = false;
/**
* The milliseconds to sleep when database is locked.
* 迅睿默认 10s,避免并发写时立刻抛 database is locked。
*
* @var int|null milliseconds
*
* @see https://www.php.net/manual/en/sqlite3.busytimeout
*/
protected $busyTimeout = 10000;
/**
* The setting of the "synchronous" flag
*
* @var int<0, 3>|null flag
*
* @see https://www.sqlite.org/pragma.html#pragma_synchronous
*/
protected ?int $synchronous = null;
/**
* @return void
*/
public function initialize()
{
parent::initialize();
if ($this->foreignKeys) {
$this->enableForeignKeyChecks();
}
$timeout = is_int($this->busyTimeout) ? $this->busyTimeout : 10000;
if ($timeout > 0 && $this->connID instanceof SQLite3) {
$this->connID->busyTimeout($timeout);
// 双保险:部分环境 busyTimeout() 对 PRAGMA 路径不生效
@$this->connID->exec('PRAGMA busy_timeout = ' . $timeout);
}
// WAL 提升并发读;内存库跳过
if ($this->connID instanceof SQLite3 && $this->database !== ':memory:') {
@$this->connID->exec('PRAGMA journal_mode = WAL');
@$this->connID->exec('PRAGMA synchronous = NORMAL');
}
if (is_int($this->synchronous)) {
if (! in_array($this->synchronous, [0, 1, 2, 3], true)) {
throw new InvalidArgumentException('Invalid synchronous value.');
}
$this->connID->exec('PRAGMA synchronous = ' . $this->synchronous);
}
}
/**
* Connect to the database.
*
* @return SQLite3
*
* @throws DatabaseException
*/
public function connect(bool $persistent = false)
{
if ($persistent && $this->DBDebug) {
throw new DatabaseException('SQLite3 doesn\'t support persistent connections.');
}
try {
if ($this->database !== ':memory:' && ! str_contains($this->database, DIRECTORY_SEPARATOR)) {
$this->database = WRITEPATH . $this->database;
}
$sqlite = (! isset($this->password) || $this->password !== '')
? new SQLite3($this->database)
: new SQLite3($this->database, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $this->password);
$sqlite->enableExceptions(true);
return $sqlite;
} catch (Exception $e) {
throw new DatabaseException('SQLite3 error: ' . $e->getMessage());
}
}
/**
* Keep or establish the connection if no queries have been sent for
* a length of time exceeding the server's idle timeout.
*
* @return void
*/
public function reconnect()
{
$this->close();
$this->initialize();
}
/**
* Close the database connection.
*
* @return void
*/
protected function _close()
{
$this->connID->close();
}
/**
* Select a specific database table to use.
*/
public function setDatabase(string $databaseName): bool
{
return false;
}
/**
* Returns a string containing the version of the database being used.
*/
public function getVersion(): string
{
if (isset($this->dataCache['version'])) {
return $this->dataCache['version'];
}
$version = SQLite3::version();
return $this->dataCache['version'] = $version['versionString'];
}
/**
* Execute the query
*
* @return false|SQLite3Result
*/
protected function execute(string $sql)
{
try {
return $this->isWriteType($sql)
? $this->connID->exec($sql)
: $this->connID->query($sql);
} catch (Exception $e) {
log_message('error', (string) $e);
if ($this->DBDebug) {
throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
}
}
return false;
}
/**
* Returns the total number of rows affected by this query.
*/
public function affectedRows(): int
{
return $this->connID->changes();
}
/**
* Platform-dependant string escape
*/
protected function _escapeString(string $str): string
{
if (! $this->connID instanceof SQLite3) {
$this->initialize();
}
return $this->connID->escapeString($str);
}
/**
* Generates the SQL for listing tables in a platform-dependent manner.
*
* @param string|null $tableName If $tableName is provided will return only this table if exists.
*/
protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string
{
if ((string) $tableName !== '') {
return 'SELECT "NAME" FROM "SQLITE'.'_'.'MASTER" WHERE "TYPE" = \'table\''
. ' AND "NAME" NOT LIKE \'sqlite!_%\' ESCAPE \'!\''
. ' AND "NAME" LIKE ' . $this->escape($tableName);
}
return 'SELECT "NAME" FROM "SQLITE'.'_'.'MASTER" WHERE "TYPE" = \'table\''
. ' AND "NAME" NOT LIKE \'sqlite!_%\' ESCAPE \'!\''
. (($prefixLimit && $this->DBPrefix !== '')
? ' AND "NAME" LIKE \'' . $this->escapeLikeString($this->DBPrefix) . '%\' ' . sprintf($this->likeEscapeStr, $this->likeEscapeChar)
: '');
}
/**
* Generates a platform-specific query string so that the column names can be fetched.
*
* @param string|TableName $table
*/
protected function _listColumns($table = ''): string
{
if ($table instanceof TableName) {
$tableName = $this->escapeIdentifier($table);
} else {
$tableName = $this->protectIdentifiers($table, true, null, false);
}
return 'PRAGMA TABLE_INFO(' . $tableName . ')';
}
/**
* @param string|TableName $tableName
*
* @return false|list<string>
*
* @throws DatabaseException
*/
public function getFieldNames($tableName)
{
$table = ($tableName instanceof TableName) ? $tableName->getTableName() : $tableName;
// Is there a cached result?
if (isset($this->dataCache['field_names'][$table])) {
return $this->dataCache['field_names'][$table];
}
if (! $this->connID instanceof SQLite3) {
$this->initialize();
}
$sql = $this->_listColumns($tableName);
$query = $this->query($sql);
$this->dataCache['field_names'][$table] = [];
foreach ($query->getResultArray() as $row) {
// Do we know from where to get the column's name?
if (! isset($key)) {
if (isset($row['column_name'])) {
$key = 'column_name';
} elseif (isset($row['COLUMN_NAME'])) {
$key = 'COLUMN_NAME';
} elseif (isset($row['name'])) {
$key = 'name';
} else {
// We have no other choice but to just get the first element's key.
$key = key($row);
}
}
$this->dataCache['field_names'][$table][] = $row[$key];
}
return $this->dataCache['field_names'][$table];
}
/**
* Returns an array of objects with field data
*
* @return list<stdClass>
*
* @throws DatabaseException
*/
protected function _fieldData(string $table): array
{
if (false === $query = $this->query('PRAGMA TABLE_INFO(' . $this->protectIdentifiers($table, true, null, false) . ')')) {
throw new DatabaseException(lang('Database.failGetFieldData'));
}
$query = $query->getResultObject();
if (empty($query)) {
return [];
}
$retVal = [];
for ($i = 0, $c = count($query); $i < $c; $i++) {
$retVal[$i] = new stdClass();
$retVal[$i]->name = $query[$i]->name;
$retVal[$i]->type = $query[$i]->type;
$retVal[$i]->max_length = null;
$retVal[$i]->nullable = isset($query[$i]->notnull) && ! (bool) $query[$i]->notnull;
$retVal[$i]->default = $query[$i]->dflt_value;
// "pk" (either zero for columns that are not part of the primary key,
// or the 1-based index of the column within the primary key).
// https://www.sqlite.org/pragma.html#pragma_table_info
$retVal[$i]->primary_key = ($query[$i]->pk === 0) ? 0 : 1;
}
return $retVal;
}
/**
* Returns an array of objects with index data
*
* @return array<string, stdClass>
*
* @throws DatabaseException
*/
protected function _indexData(string $table): array
{
$sql = "SELECT 'PRIMARY' as indexname, l.name as fieldname, 'PRIMARY' as indextype
FROM pragma_table_info(" . $this->escape(strtolower($table)) . ") as l
WHERE l.pk <> 0
UNION ALL
SELECT sqlite_master.name as indexname, ii.name as fieldname,
CASE
WHEN ti.pk <> 0 AND sqlite_master.name LIKE 'sqlite_autoindex_%' THEN 'PRIMARY'
WHEN sqlite_master.name LIKE 'sqlite_autoindex_%' THEN 'UNIQUE'
WHEN sqlite_master.sql LIKE '% UNIQUE %' THEN 'UNIQUE'
ELSE 'INDEX'
END as indextype
FROM sqlite_master
INNER JOIN pragma_index_xinfo(sqlite_master.name) ii ON ii.name IS NOT NULL
LEFT JOIN pragma_table_info(" . $this->escape(strtolower($table)) . ") ti ON ti.name = ii.name
WHERE sqlite_master.type='index' AND sqlite_master.tbl_name = " . $this->escape(strtolower($table)) . ' COLLATE NOCASE';
if (($query = $this->query($sql)) === false) {
throw new DatabaseException(lang('Database.failGetIndexData'));
}
$query = $query->getResultObject();
$tempVal = [];
foreach ($query as $row) {
if ($row->indextype === 'PRIMARY') {
$tempVal['PRIMARY']['indextype'] = $row->indextype;
$tempVal['PRIMARY']['indexname'] = $row->indexname;
$tempVal['PRIMARY']['fields'][$row->fieldname] = $row->fieldname;
} else {
$tempVal[$row->indexname]['indextype'] = $row->indextype;
$tempVal[$row->indexname]['indexname'] = $row->indexname;
$tempVal[$row->indexname]['fields'][$row->fieldname] = $row->fieldname;
}
}
$retVal = [];
foreach ($tempVal as $val) {
$obj = new stdClass();
$obj->name = $val['indexname'];
$obj->fields = array_values($val['fields']);
$obj->type = $val['indextype'];
$retVal[$obj->name] = $obj;
}
return $retVal;
}
/**
* Returns an array of objects with Foreign key data
*
* @return array<string, stdClass>
*/
protected function _foreignKeyData(string $table): array
{
if (! $this->supportsForeignKeys()) {
return [];
}
$query = $this->query("PRAGMA foreign_key_list({$table})")->getResult();
$indexes = [];
foreach ($query as $row) {
$indexes[$row->id]['constraint_name'] = null;
$indexes[$row->id]['table_name'] = $table;
$indexes[$row->id]['foreign_table_name'] = $row->table;
$indexes[$row->id]['column_name'][] = $row->from;
$indexes[$row->id]['foreign_column_name'][] = $row->to;
$indexes[$row->id]['on_delete'] = $row->on_delete;
$indexes[$row->id]['on_update'] = $row->on_update;
$indexes[$row->id]['match'] = $row->match;
}
return $this->foreignKeyDataToObjects($indexes);
}
/**
* Returns platform-specific SQL to disable foreign key checks.
*
* @return string
*/
protected function _disableForeignKeyChecks()
{
return 'PRAGMA foreign_keys = OFF';
}
/**
* Returns platform-specific SQL to enable foreign key checks.
*
* @return string
*/
protected function _enableForeignKeyChecks()
{
return 'PRAGMA foreign_keys = ON';
}
/**
* Returns the last error code and message.
* Must return this format: ['code' => string|int, 'message' => string]
* intval(code) === 0 means "no error".
*
* @return array<string, int|string>
*/
public function error(): array
{
return [
'code' => $this->connID->lastErrorCode(),
'message' => $this->connID->lastErrorMsg(),
];
}
/**
* Insert ID
*/
public function insertID(): int
{
return $this->connID->lastInsertRowID();
}
/**
* Begin Transaction
*/
protected function _transBegin(): bool
{
return $this->connID->exec('BEGIN TRANSACTION');
}
/**
* Commit Transaction
*/
protected function _transCommit(): bool
{
return $this->connID->exec('END TRANSACTION');
}
/**
* Rollback Transaction
*/
protected function _transRollback(): bool
{
return $this->connID->exec('ROLLBACK');
}
/**
* Checks to see if the current install supports Foreign Keys
* and has them enabled.
*/
public function supportsForeignKeys(): bool
{
$result = $this->simpleQuery('PRAGMA foreign_keys');
return (bool) $result;
}
// --------------------------------------------------------------------
// 迅睿 DDL API(与 MySQLi\Connection 同名,供 Fcms/Model/Table 等调用)
// --------------------------------------------------------------------
/**
* 拦截 MySQL 风格 ALTER ... CHANGE,改为重建表修改列
*
* {@inheritDoc}
*/
public function query(string $sql, $binds = null, bool $setEscapeFlags = true, string $queryClass = '')
{
if ($binds === null && is_string($sql)) {
$trimmed = ltrim($sql);
$id = '(?:[`"]?)([^\s`"]+)(?:[`"]?)';
if (preg_match(
'/^ALTER\s+TABLE\s+'.$id.'\s+CHANGE\s+'.$id.'\s+'.$id.'\s+(.+)$/is',
$trimmed,
$m
)) {
$def = trim($m[4]);
$def = preg_replace("/\s+COMMENT\s+'((?:''|[^'])*)'/i", '', $def) ?? $def;
$def = preg_replace('/\s+COMMENT\s+"((?:""|[^"])*)"/i', '', $def) ?? $def;
if (preg_match('/^(\S+(?:\([^)]*\))?)\s*(.*)$/s', trim($def), $d)) {
$rt = $this->editField($m[1], $m[2], $d[1], trim($d[2]), '');
return $rt !== false;
}
}
if (preg_match(
'/^ALTER\s+TABLE\s+'.$id.'\s+DROP(?:\s+COLUMN)?\s+'.$id.'\s*$/is',
$trimmed,
$m
)) {
$rt = $this->dropField($m[1], $m[2]);
return $rt !== false;
}
}
return parent::query($sql, $binds, $setEscapeFlags, $queryClass);
}
/**
* 规范化建表/DDL SQLInstall.sql 等以 MySQL 方言为源 → SQLite
*/
public function formatCreateSql($sql)
{
if ($sql === '' || $sql === null) {
return '';
}
$sql = trim($sql);
// DML 兼容
$sql = preg_replace('/\bREPLACE\s+INTO\b/i', 'INSERT OR REPLACE INTO', $sql) ?? $sql;
$sql = preg_replace('/\bINSERT\s+IGNORE\s+INTO\b/i', 'INSERT OR IGNORE INTO', $sql) ?? $sql;
// DROP TABLE 可直接用
if (preg_match('/^DROP\s+TABLE\b/i', $sql)) {
return $sql;
}
// ALTER TABLE ADD / DROP(去掉 COMMENT
if (preg_match('/^ALTER\s+TABLE\b/i', $sql)) {
$sql = preg_replace("/\s+COMMENT\s+'((?:''|[^'])*)'/i", '', $sql) ?? $sql;
$sql = preg_replace('/\s+COMMENT\s+"((?:""|[^"])*)"/i', '', $sql) ?? $sql;
// CHANGE 留给 query() 拦截重建
if (preg_match('/^ALTER\s+TABLE\s+[`]?([^`\s]+)[`]?\s+CHANGE\b/i', $sql)) {
return $sql;
}
// DROP COLUMN 留给 query() 走重建(兼容旧 SQLite
if (preg_match('/^ALTER\s+TABLE\s+[`]?([^`\s]+)[`]?\s+DROP(?:\s+COLUMN)?\s+/i', $sql)) {
return $sql;
}
// ADD 列类型映射
if (preg_match('/^(ALTER\s+TABLE\s+[`]?[^`\s]+[`]?\s+ADD(?:\s+COLUMN)?\s+[`]?[^`\s]+[`]?\s+)(.+)$/is', $sql, $m)) {
return $m[1] . $this->_xrNormalizeMysqlColumnLine($m[2]);
}
return $sql;
}
// CREATE TABLE
if (preg_match('/^CREATE\s+TABLE\b/i', $sql)) {
return $this->_xrFormatCreateTableSql($sql);
}
return $sql;
}
/**
* 生成「添加字段」SQL(表名占位符 {tablename}
*/
public function sqlAddField($name, $type, $info, $note = '')
{
$def = $this->_xrNormalizeMysqlColumnLine(trim($type . ' ' . $info));
// 表名用占位符,避免反引号在 SQLite 下偶发语法问题;执行时再 escapeIdentifiers
return 'ALTER TABLE {tablename} ADD COLUMN ' . $this->escapeIdentifiers($name) . ' ' . $def;
}
/**
* 生成「修改字段」SQL(MySQL CHANGE 形态;执行时由 query() 拦截并重建表)
*/
public function sqlEditField($name, $type, $info, $note = '')
{
$id = $this->escapeIdentifiers($name);
$def = $this->_xrNormalizeMysqlColumnLine(trim($type . ' ' . $info));
return 'ALTER TABLE {tablename} CHANGE ' . $id . ' ' . $id . ' ' . $def;
}
/**
* 生成「删除字段」SQL
*/
public function sqlDropField($name)
{
return 'ALTER TABLE {tablename} DROP COLUMN ' . $this->escapeIdentifiers($name);
}
/**
* 批量添加字段
*/
public function sqlAddFields(array $columns)
{
// SQLite 一条 ALTER 只能 ADD 一列,拆成多条由调用方逐条执行不现实;
// 返回第一条,其余由 sqlAddField 循环更稳妥。这里用分号拼接,simpleQuery/exec 可多语句。
$parts = [];
foreach ($columns as $col) {
if (empty($col['name'])) {
continue;
}
$parts[] = $this->sqlAddField(
$col['name'],
$col['type'] ?? '',
$col['info'] ?? '',
$col['note'] ?? ''
);
}
return $parts === [] ? '' : implode(";\n", $parts);
}
/**
* 批量删除字段
*/
public function sqlDropFields(array $names)
{
$parts = [];
foreach ($names as $name) {
if ($name === '' || $name === null) {
continue;
}
$parts[] = $this->sqlDropField($name);
}
return $parts === [] ? '' : implode(";\n", $parts);
}
/**
* 删除表
*/
public function dropTable($table, $ifExists = true)
{
$table = trim((string) $table, '`');
$sql = ($ifExists ? 'DROP TABLE IF EXISTS ' : 'DROP TABLE ')
. $this->escapeIdentifiers($table);
return $this->query($sql);
}
/**
* 修改字段(重建表)
*/
public function editField($table, $name, $type, $info, $note)
{
$table = trim((string) $table, '`"[]');
$name = trim((string) $name, '`"[]');
$type = $this->_xrMapMysqlTypeOnly((string) $type);
$info = (string) $info;
$null = stripos($info, 'NOT NULL') === false;
$field = [
'name' => $name,
'type' => $type,
'null' => $null,
];
if (preg_match('/DEFAULT\s+(NULL|\'(?:\'\'|[^\'])*\'|"(?:""|[^"])*"|\S+)/i', $info, $m)) {
$default = $m[1];
if (strtoupper($default) === 'NULL') {
$field['default'] = null;
} else {
$field['default'] = trim($default, "'\"");
}
}
try {
$forge = new Forge($this);
return (new Table($this, $forge))
->fromTable($table)
->modifyColumn([$field])
->run();
} catch (\Throwable $e) {
log_message('error', 'SQLite3 editField: ' . $e->getMessage());
return false;
}
}
/**
* 添加字段
*/
public function addField($table, $name, $type, $info, $note)
{
$table = trim((string) $table, '`"[]');
return $this->query(str_replace(
'{tablename}',
$this->escapeIdentifiers($table),
$this->sqlAddField($name, $type, $info, $note)
));
}
/**
* 删除字段(重建表,兼容旧版 SQLite)
*/
public function dropField($table, $name)
{
$table = trim((string) $table, '`"[]');
$name = trim((string) $name, '`"[]');
try {
$forge = new Forge($this);
return (new Table($this, $forge))
->fromTable($table)
->dropColumn($name)
->run();
} catch (\Throwable $e) {
log_message('error', 'SQLite3 dropField: ' . $e->getMessage());
return false;
}
}
/**
* 创建表
*
* @param array $fields ['字段名' => '类型及属性SQL'] 或 [0 => '完整列定义SQL']
* @param array $indexs ['PRIMARY KEY (`id`)', 'KEY `name` (`name`)', ...]
* @param string $note 表注释(SQLite 忽略,仅日志无关)
*/
public function createTable($table, $fields, $indexs, $note)
{
$table = trim((string) $table, '`"[]');
$lines = [];
$extraIndexes = [];
$pkInline = false;
if (is_array($fields)) {
foreach ($fields as $name => $def) {
if (is_int($name)) {
$line = $this->_xrNormalizeMysqlColumnLine((string) $def);
if (preg_match('/PRIMARY\s+KEY/i', $line) && preg_match('/AUTOINCREMENT/i', $line)) {
$pkInline = true;
}
$lines[] = $line;
} else {
$mapped = $this->_xrNormalizeMysqlColumnLine((string) $def);
if (preg_match('/PRIMARY\s+KEY/i', $mapped) && preg_match('/AUTOINCREMENT/i', $mapped)) {
$pkInline = true;
}
$lines[] = $this->escapeIdentifiers($name) . ' ' . $mapped;
}
}
}
if (is_array($indexs)) {
foreach ($indexs as $idx) {
$idx = trim((string) $idx);
if ($idx === '') {
continue;
}
if (preg_match('/^PRIMARY\s+KEY/i', $idx)) {
if (! $pkInline) {
// 去掉 MySQL 反引号
$idx = preg_replace('/[`"]/', '', $idx) ?? $idx;
$lines[] = $idx;
}
continue;
}
// UNIQUE KEY `uid` (`uid`) → UNIQUE (`uid`)
if (preg_match('/^UNIQUE(?:\s+KEY|\s+INDEX)?\s+(?:[`"]?\w+[`]?\s+)?\((.+)\)/i', $idx, $m)) {
$cols = preg_replace('/[`"]/', '', $this->_xrStripMysqlIndexPrefix($m[1])) ?? $m[1];
$lines[] = 'UNIQUE (' . $cols . ')';
continue;
}
// KEY `name` (`col`) / INDEX → 表外 CREATE INDEX(索引名加表前缀,SQLite 全局唯一)
if (preg_match('/^(?:KEY|INDEX)\s+(?:[`"]?(\w+)[`]?\s+)?\((.+)\)/i', $idx, $m)) {
$extraIndexes[] = [
'name' => $m[1] ?? '',
'cols' => preg_replace('/[`"]/', '', $this->_xrStripMysqlIndexPrefix($m[2])) ?? $m[2],
];
continue;
}
$lines[] = preg_replace('/[`"]/', '', $idx) ?? $idx;
}
}
if ($lines === []) {
log_message('error', 'SQLite3 createTable: empty fields for ' . $table);
return false;
}
$sql = 'CREATE TABLE IF NOT EXISTS ' . $this->escapeIdentifiers($table) . " (
" . implode(",\n", $lines) . '
)';
try {
$rt = $this->query($sql);
} catch (\Throwable $e) {
log_message('error', 'SQLite3 createTable: ' . $e->getMessage() . ' SQL: ' . $sql);
return false;
}
if ($rt === false) {
$err = $this->error();
log_message('error', 'SQLite3 createTable failed: ' . ($err['message'] ?? '') . ' SQL: ' . $sql);
return false;
}
// 建表后刷新表名缓存,避免 tableExists 误判
$this->resetDataCache();
$safeTable = preg_replace('/[^a-zA-Z0-9_]/', '_', $table) ?: 't';
foreach ($extraIndexes as $i => $ix) {
$base = $ix['name'] !== '' ? $ix['name'] : ('idx_' . $i);
$idxName = $safeTable . '_' . preg_replace('/[^a-zA-Z0-9_]/', '_', $base);
try {
$this->query(
'CREATE INDEX IF NOT EXISTS ' . $this->escapeIdentifiers($idxName)
. ' ON ' . $this->escapeIdentifiers($table) . ' (' . $ix['cols'] . ')'
);
} catch (\Throwable $e) {
log_message('error', 'SQLite3 createIndex: ' . $e->getMessage());
}
}
return $rt;
}
/**
* 获取建表 SQLCREATE TABLE IF NOT EXISTS ...
*/
public function createTableSql($table)
{
$table = trim((string) $table, '`');
$row = $this->query(
'SELECT sql FROM sqlite_master WHERE type=' . $this->escape('table')
. ' AND name=' . $this->escape($table) . ' COLLATE NOCASE'
)->getRowArray();
if (! $row || empty($row['sql'])) {
return '';
}
$sql = trim($row['sql']);
if (stripos($sql, 'IF NOT EXISTS') === false) {
$sql = preg_replace('/^CREATE\s+TABLE\b/i', 'CREATE TABLE IF NOT EXISTS', $sql) ?? $sql;
}
return $sql;
}
/**
* 获取表完整字段信息(兼容 SHOW FULL COLUMNS 结果键名)
*/
public function showFullColunms($table)
{
$table = trim((string) $table, '`');
$rows = $this->query(
'PRAGMA TABLE_INFO(' . $this->protectIdentifiers($table, true, null, false) . ')'
)->getResultArray();
$out = [];
foreach ($rows as $r) {
$out[] = [
'Field' => $r['name'] ?? '',
'Type' => $r['type'] ?? '',
'Collation' => null,
'Null' => empty($r['notnull']) ? 'YES' : 'NO',
'Key' => ! empty($r['pk']) ? 'PRI' : '',
'Default' => $r['dflt_value'] ?? null,
'Extra' => '',
'Privileges' => '',
'Comment' => '',
];
}
return $out;
}
/**
* 获取全部表状态(兼容 SHOW TABLE STATUS 常用键)
*/
public function showTableStatus()
{
$tables = $this->query(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
)->getResultArray();
$out = [];
foreach ($tables as $t) {
$name = $t['name'];
$cnt = $this->query('SELECT COUNT(*) AS c FROM ' . $this->escapeIdentifiers($name))->getRowArray();
$out[] = [
'Name' => $name,
'Engine' => 'SQLite3',
'Version' => null,
'Row_format' => null,
'Rows' => isset($cnt['c']) ? (int) $cnt['c'] : 0,
'Avg_row_length' => null,
'Data_length' => 0,
'Max_data_length' => null,
'Index_length' => 0,
'Data_free' => 0,
'Auto_increment' => null,
'Create_time' => null,
'Update_time' => null,
'Check_time' => null,
'Collation' => 'BINARY',
'Checksum' => null,
'Create_options' => null,
'Comment' => '',
];
}
return $out;
}
/**
* 修复表(SQLite 无对应操作,返回 true
*/
public function repairTable($table)
{
return true;
}
/**
* 优化表 → VACUUM(库级,忽略单表名)
*/
public function optimizeTable($table)
{
return $this->query('VACUUM');
}
/**
* 刷新表(无操作)
*/
public function flushTable($table)
{
return true;
}
/**
* 检查表 → integrity_check,返回兼容 Msg_text 的一行
*/
public function checkTable($table)
{
$row = $this->query('PRAGMA integrity_check')->getRowArray();
$msg = is_array($row) ? (string) reset($row) : 'unknown';
return [
'Table' => trim((string) $table, '`'),
'Op' => 'check',
'Msg_type' => strtolower($msg) === 'ok' ? 'status' : 'error',
'Msg_text' => $msg,
];
}
/**
* 修改表字符集(SQLite 固定 UTF-8,无操作)
*/
public function setTableCharset($table, $charset = 'utf8mb4', $collate = 'utf8mb4_unicode_ci')
{
return true;
}
// --------------------------------------------------------------------
// 迅睿 DDL 内部辅助
// --------------------------------------------------------------------
/**
* 将 MySQL CREATE TABLE 转为 SQLiteKEY 拆成后续 CREATE INDEX
*/
protected function _xrFormatCreateTableSql(string $sql): string
{
// 去掉表尾 ENGINE/CHARSET/COLLATE/COMMENT
$sql = preg_replace(
'/\)\s*ENGINE\s*=\s*\w+[^;]*$/i',
')',
$sql
) ?? $sql;
$sql = preg_replace('/\)\s*DEFAULT\s+CHARSET\s*=\s*\w+[^;]*$/i', ')', $sql) ?? $sql;
$sql = preg_replace('/\)\s*COMMENT\s*=\s*\'(?:\'\'|[^\'])*\'\s*$/i', ')', $sql) ?? $sql;
$sql = preg_replace('/\s+CHARACTER\s+SET\s+\w+/i', '', $sql) ?? $sql;
$sql = preg_replace('/\s+COLLATE\s+\w+/i', '', $sql) ?? $sql;
if (! preg_match('/^(CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+)([^\s(]+)(\s*\(.*\))\s*$/is', $sql, $m)) {
// 宽松:仍做列级替换
return $this->_xrNormalizeMysqlColumnLine($sql);
}
$prefix = $m[1];
$table = trim($m[2], '`"');
$body = trim($m[3]);
$body = substr($body, 1, -1); // 去括号
$parts = $this->_xrSplitSqlColumns($body);
$cols = [];
$idxs = [];
$pkInline = false;
foreach ($parts as $part) {
$part = trim($part);
if ($part === '') {
continue;
}
if (preg_match('/^PRIMARY\s+KEY/i', $part)) {
if (! $pkInline) {
$cols[] = $part;
}
continue;
}
if (preg_match('/^UNIQUE(?:\s+KEY|\s+INDEX)?\s+(?:[`"]?\w+[`]?\s+)?\((.+)\)/i', $part, $um)) {
$cols[] = 'UNIQUE (' . $this->_xrStripMysqlIndexPrefix($um[1]) . ')';
continue;
}
if (preg_match('/^(?:KEY|INDEX)\s+(?:[`"]?(\w+)[`]?\s+)?\((.+)\)/i', $part, $km)) {
$idxs[] = [
'name' => $km[1] ?? '',
'cols' => $this->_xrStripMysqlIndexPrefix($km[2]),
];
continue;
}
if (preg_match('/^CONSTRAINT\b/i', $part)) {
// 跳过复杂约束或保留 UNIQUE/PK 子句
if (preg_match('/PRIMARY\s+KEY/i', $part) || preg_match('/UNIQUE/i', $part)) {
$cols[] = $part;
}
continue;
}
// 普通列:`name` def 或 name def
if (preg_match('/^[`"]?(\w+)[`"]?\s+(.+)$/s', $part, $cm)) {
$mapped = $this->_xrNormalizeMysqlColumnLine($cm[2]);
if (preg_match('/PRIMARY\s+KEY/i', $mapped) && preg_match('/AUTOINCREMENT/i', $mapped)) {
$pkInline = true;
}
$cols[] = $this->escapeIdentifiers($cm[1]) . ' ' . $mapped;
} else {
$cols[] = $this->_xrNormalizeMysqlColumnLine($part);
}
}
$create = $prefix . $this->escapeIdentifiers($table) . " (
" . implode(",\n", $cols) . '
)';
foreach ($idxs as $i => $ix) {
$idxName = $ix['name'] !== '' ? $ix['name'] : ($table . '_idx_' . $i);
$idxName = preg_replace('/[^a-zA-Z0-9_]/', '_', $idxName);
$create .= ";\nCREATE INDEX IF NOT EXISTS " . $this->escapeIdentifiers($idxName)
. ' ON ' . $this->escapeIdentifiers($table) . ' (' . $ix['cols'] . ')';
}
return $create;
}
/**
* 去掉 MySQL 索引列前缀长度:`col`(191) / col(20) → col
* SQLite 会把 col(20) 解析成函数调用,导致 no such function: col
*/
protected function _xrStripMysqlIndexPrefix(string $cols): string
{
return preg_replace('/([`"\']?\w+[`"\']?)\s*\(\s*\d+\s*\)/', '$1', $cols) ?? $cols;
}
/**
* 按逗号拆分 CREATE 括号内定义(忽略括号内逗号)
*/
protected function _xrSplitSqlColumns(string $body): array
{
$parts = [];
$buf = '';
$depth = 0;
$len = strlen($body);
$quote = null;
for ($i = 0; $i < $len; $i++) {
$ch = $body[$i];
if ($quote !== null) {
$buf .= $ch;
if ($ch === $quote) {
// 处理 '' 转义
if ($ch === "'" && $i + 1 < $len && $body[$i + 1] === "'") {
$buf .= $body[++$i];
continue;
}
$quote = null;
}
continue;
}
if ($ch === "'" || $ch === '"' || $ch === '`') {
$quote = $ch;
$buf .= $ch;
continue;
}
if ($ch === '(') {
$depth++;
$buf .= $ch;
continue;
}
if ($ch === ')') {
$depth--;
$buf .= $ch;
continue;
}
if ($ch === ',' && $depth === 0) {
$parts[] = trim($buf);
$buf = '';
continue;
}
$buf .= $ch;
}
if (trim($buf) !== '') {
$parts[] = trim($buf);
}
return $parts;
}
/**
* 规范化单列 MySQL 定义片段为 SQLite
*/
protected function _xrNormalizeMysqlColumnLine(string $def): string
{
$def = preg_replace("/\s+COMMENT\s+'((?:''|[^'])*)'/i", '', $def) ?? $def;
$def = preg_replace('/\s+COMMENT\s+"((?:""|[^"])*)"/i', '', $def) ?? $def;
$def = preg_replace('/\bUNSIGNED\b/i', '', $def) ?? $def;
$def = preg_replace('/\bZEROFILL\b/i', '', $def) ?? $def;
$def = preg_replace('/\bCHARACTER\s+SET\s+\w+/i', '', $def) ?? $def;
$def = preg_replace('/\bCOLLATE\s+\w+/i', '', $def) ?? $def;
$def = preg_replace('/\b(LONGTEXT|MEDIUMTEXT|TINYTEXT)\b/i', 'TEXT', $def) ?? $def;
$def = preg_replace('/\bDOUBLE(?:\s*\([^)]*\))?/i', 'REAL', $def) ?? $def;
$def = preg_replace('/\bFLOAT(?:\s*\([^)]*\))?/i', 'REAL', $def) ?? $def;
$def = preg_replace('/\bDECIMAL(?:\s*\([^)]*\))?/i', 'NUMERIC', $def) ?? $def;
$def = preg_replace('/\bDATETIME\b/i', 'TEXT', $def) ?? $def;
$def = preg_replace('/\bTIMESTAMP\b/i', 'TEXT', $def) ?? $def;
$def = preg_replace('/\bENUM\s*\([^)]*\)/i', 'TEXT', $def) ?? $def;
$def = preg_replace('/\bSET\s*\([^)]*\)/i', 'TEXT', $def) ?? $def;
$hasAi = (bool) preg_match('/\bAUTO_INCREMENT\b/i', $def);
$def = preg_replace('/\bAUTO_INCREMENT\b/i', '', $def) ?? $def;
// int(N) / tinyint 等 → INTEGER
$def = preg_replace('/\b(?:TINY|SMALL|MEDIUM|BIG)?INT(?:EGER)?\s*\(\d+\)/i', 'INTEGER', $def) ?? $def;
$def = preg_replace('/\b(?:TINY|SMALL|MEDIUM|BIG)?INT\b/i', 'INTEGER', $def) ?? $def;
if ($hasAi) {
// SQLiteAUTOINCREMENT 仅用于 INTEGER PRIMARY KEY
return 'INTEGER PRIMARY KEY AUTOINCREMENT';
}
return trim(preg_replace('/\s+/', ' ', $def) ?? $def);
}
/**
* 仅映射类型名(不含属性)
*/
protected function _xrMapMysqlTypeOnly(string $type): string
{
$type = trim($type);
$tmp = $this->_xrNormalizeMysqlColumnLine($type);
if (preg_match('/^(\S+)/', $tmp, $m)) {
return $m[1];
}
return $type;
}
// --------------------------------------------------------------------
// 字段搜索 WHERE 表达式(覆盖 Model 默认 MySQL 写法)
// --------------------------------------------------------------------
/**
* FIND_IN_SET → SQLite 逗号包围 LIKE
*/
public function whereFindInSet($column, $value)
{
$column = (string) $column;
if (dr_is_numeric($value)) {
$safe = (string) (int) $value;
} else {
$safe = str_replace(["\\", "'"], ["\\\\", "''"], dr_safe_replace($value));
}
return "(',' || IFNULL(" . $column . ",'') || ',') LIKE '%," . $safe . ",%'";
}
/**
* JSON 包含 → SQLite LIKE 兼容
*/
public function whereJson($table, $name, $value)
{
if (strpos($name, '`') === false && strpos($name, '"') === false) {
$name = $table
? $this->escapeIdentifiers($table) . '.' . $this->escapeIdentifiers($name)
: $this->escapeIdentifiers($name);
}
return $name . ' LIKE \'%"' . $value . '"%\'';
}
// --------------------------------------------------------------------
// 聚合 SELECT(供 Model::select_agg / select_count / select_sum 中转)
// --------------------------------------------------------------------
/**
* 混合 SELECT'form_id, COUNT(*) AS cnt, SUM(price) AS total'
* SQLite 用双引号标识符(escapeChar="
*/
public function buildSelectAgg($select)
{
$parts = $this->_xrSplitSelectParts((string) $select);
$out = [];
foreach ($parts as $part) {
$part = trim($part);
if ($part === '') {
continue;
}
if (preg_match('/^(COUNT|SUM|AVG|MAX|MIN)\s*\(\s*(.+?)\s*\)\s*(?:AS\s+([`"]?[\w]+[`"]?))?\s*$/i', $part, $m)) {
$out[] = $this->buildSelectFunc(strtoupper($m[1]), trim($m[2]), isset($m[3]) ? trim($m[3], '`"[]') : '');
continue;
}
if (preg_match('/^(.+?)\s+AS\s+([`"]?[\w]+[`"]?)\s*$/i', $part, $m)) {
$out[] = $this->_xrQuoteSelectIdent(trim($m[1])) . ' AS ' . $this->escapeIdentifiers(trim($m[2], '`"[]'));
continue;
}
$out[] = $this->_xrQuoteSelectIdent($part);
}
return implode(', ', $out);
}
/**
* 单个聚合:COUNT/SUM/AVG/MAX/MIN
*/
public function buildSelectFunc($fn, $field, $alias = '')
{
$fn = strtoupper(trim((string) $fn));
$field = trim((string) $field);
$alias = trim((string) $alias, '`"[]');
if (! in_array($fn, ['COUNT', 'SUM', 'AVG', 'MAX', 'MIN'], true) || $field === '') {
return '';
}
$inner = ($field === '*') ? '*' : $this->_xrQuoteSelectIdent($field);
$sql = $fn . '(' . $inner . ')';
if ($alias !== '') {
$sql .= ' AS ' . $this->escapeIdentifiers($alias);
}
return $sql;
}
/**
* 列标识加引(table.field / *
*/
protected function _xrQuoteSelectIdent($expr)
{
$expr = trim((string) $expr);
if ($expr === '' || $expr === '*') {
return $expr === '*' ? '*' : '';
}
$expr = trim($expr, '`"[]');
if (strpos($expr, '.') !== false) {
$bits = explode('.', $expr);
foreach ($bits as &$b) {
$b = $this->escapeIdentifiers(trim($b, '`"[]'));
}
return implode('.', $bits);
}
return $this->escapeIdentifiers($expr);
}
/**
* 按逗号拆 SELECT(忽略括号/引号内逗号)
*/
protected function _xrSplitSelectParts($select)
{
$parts = [];
$buf = '';
$depth = 0;
$len = strlen($select);
$quote = null;
for ($i = 0; $i < $len; $i++) {
$ch = $select[$i];
if ($quote !== null) {
$buf .= $ch;
if ($ch === $quote) {
if (($ch === "'" || $ch === '"') && $i + 1 < $len && $select[$i + 1] === $ch) {
$buf .= $select[++$i];
continue;
}
$quote = null;
}
continue;
}
if ($ch === "'" || $ch === '"' || $ch === '`') {
$quote = $ch;
$buf .= $ch;
continue;
}
if ($ch === '(') {
$depth++;
$buf .= $ch;
continue;
}
if ($ch === ')') {
$depth = max(0, $depth - 1);
$buf .= $ch;
continue;
}
if ($ch === ',' && $depth === 0) {
$parts[] = $buf;
$buf = '';
continue;
}
$buf .= $ch;
}
if (trim($buf) !== '') {
$parts[] = $buf;
}
return $parts;
}
}