driver; if($config->driver == 'oceanbase') $driver = 'mysql'; // Oceanbase driver alias mysql. $dsn = "{$driver}:host={$config->host};port={$config->port}"; if($setSchema) $dsn .= ";dbname={$config->name}"; $password = helper::decryptPassword($config->password); $pdo = new PDO($dsn, $config->user, $password); $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $queries = []; /* Mysql driver include mysql and oceanbase. */ if($driver == 'mysql') { $queries[] = "SET NAMES {$config->encoding}"; if(isset($config->strictMode) && empty($config->strictMode)) $queries[] = "SET @@sql_mode= ''"; } else if($setSchema) { $queries[] = "SET SCHEMA {$config->name}"; } if(!empty($queries)) { foreach($queries as $query) { $pdo->exec($query); dbh::$traces[] = 'vim +' . (__LINE__ - 1) . ' ' . __FILE__; dbh::$queries[] = "[$flag] " . $query; } } $this->pdo = $pdo; $this->config = $config; $this->flag = $flag; } /** * Process PDO/SQL error. * * @param object $exception * @access public * @return void */ public function sqlError(object $exception) { $newException = new PDOException($exception->getMessage() . " ,the sql is: '{$this->sql}'"); $newException->errorInfo = $exception->errorInfo; throw $newException; } /** * Execute sql. * * @param string $sql * @access public * @return PDOStatement|false */ public function exec($sql) { $this->trace(); $sql = $this->formatSQL($sql); if(!$sql) return true; if(!empty($this->config->enableSqlite)) $this->pushSqliteQueue($sql); try { dbh::$queries[] = "[$this->flag] " . dao::processKeywords($sql); return $this->pdo->exec($sql); } catch(PDOException $e) { $this->sqlError($e); } } /** * Query sql. * * @param string $sql * @see https://www.php.net/manual/en/pdo.query.php * @access public * @return PDOStatement|false */ public function query($sql) { $this->trace(); $sql = $this->formatSQL($sql); try { dbh::$queries[] = "[$this->flag] " . dao::processKeywords($sql); return $this->pdo->query($sql); } catch(PDOException $e) { $this->sqlError($e); } } /** * Prepare a PDO statement. * * @param string $sql * @access public * @return PDOStatement|false */ public function prepare($sql) { $this->sql = $sql; try { $this->statement = $this->pdo->prepare($sql); } catch(PDOException $e) { $this->sqlError($e); } return $this->statement; } /** * Prepare and execute a PDO statement. * * @param string $sql * @param array $params * @access public * @return PDOStatement|false */ public function execute($sql, $params) { $this->trace(); $this->statement = $this->prepare($sql); try { $this->statement->execute($params); } catch(PDOException $e) { $this->sqlError($e); } return $this->statement; } /** * Query raw sql. * * @param string $sql * @access public * @return PDOStatement|false */ public function rawQuery($sql) { $this->trace(); try { dbh::$queries[] = "[$this->flag] " . dao::processKeywords($sql); return $this->pdo->query($sql); } catch(PDOException $e) { $this->sqlError($e); } } /** * 记录当前执行的 SQL 的文件和行号。 * Record the file and line number of the currently executed SQL. * * @access private * @return bool */ private function trace() { global $config; if(empty($config->debug) || $config->debug < 3) return false; $trace = $this->getTrace(); if(empty($trace)) return false; $file = $trace['file']; $line = $trace['line']; dbh::$traces[] = "vim +$line $file"; return true; } /** * 获取当前执行的 SQL 的调用栈信息。 * Get the call stack information of the currently executed SQL. * * @access private * @return array */ private function getTrace() { $traces = array_reverse(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)); foreach($traces as $key => $trace) { $class = $trace['class'] ?? ''; $function = $trace['function'] ?? ''; if($class == 'settingModel' && strpos(',getItem,getItems,setItem,setItems,updateItem,deleteItems,', ",$function,") !== false) return $trace; if($class == 'baseDAO' && strpos(',exec,fetch,fetchPairs,fetchGroup,explain,showTables,getTableEngines,descTable,', ",$function,") !== false) return $trace; if($class == 'baseDAO' && $function == 'getProfiles') return $traces[$key + 1]; if($class == 'baseDAO' && $function == 'fetchAll') { if($traces[$key + 1]['class'] == 'baseDAO' && $traces[$key + 1]['function'] == 'extractSQLFields') return $traces[$key + 2]; return $trace; } if($class == 'baseDAO' && stripos($function, 'findBy') === 0) return $trace; if($class == 'baseRouter' && $function == 'dbQuery') return $trace; if($class == 'dbh' && strpos('exec,query,rawQuery', $function) !== false) return $trace; } return []; } /** * Set attribute. * * @param int $attribute * @param mixed $value * @access public * @return bool */ public function setAttribute($attribute, $value) { return $this->pdo->setAttribute($attribute, $value); } /** * Check db exits or not. * * @access public * @return bool */ public function dbExists() { switch($this->config->driver) { case 'oceanbase': case 'mysql': $sql = "SHOW DATABASES like '{$this->config->name}'"; break; case 'dm': $sql = "SELECT * FROM ALL_OBJECTS WHERE object_type='SCH' AND owner='{$this->config->name}'"; break; default: $sql = ''; } return $this->rawQuery($sql)->fetch(); } /** * Check table exits or not. * * @param string $tableName * @access public * @return void */ public function tableExits($tableName) { $tableName = str_replace(array("'", '`'), "", $tableName); $sql = "SHOW TABLES FROM {$this->config->name} like '{$tableName}'"; switch($this->config->driver) { case 'oceanbase': case 'mysql': $sql = "SHOW TABLES FROM {$this->config->name} like '{$tableName}'"; break; case 'dm': $sql = "SELECT * FROM all_tables WHERE owner='{$this->config->name}' AND table_name='{$tableName}'"; break; default: $sql = ''; } return $this->rawQuery($sql)->fetch(); } /** * Create database. * * @param string $version * @access public * @return PDOStatement|false */ public function createDB($version) { switch($this->config->driver) { case 'oceanbase': $sql = "CREATE DATABASE `{$this->config->name}`"; return $this->rawQuery($sql); case 'mysql': $sql = "CREATE DATABASE `{$this->config->name}`"; if(version_compare($version, '5.6', '>=')) { $sql .= " DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"; } elseif(version_compare($version, '4.1', '>=')) { $sql .= " DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci"; } return $this->rawQuery($sql); case 'dm': /* $tableSpace = strtoupper($this->config->name); $res = $this->rawQuery("SELECT * FROM dba_data_files WHERE TABLESPACE_NAME = '$tableSpace'")->fetchAll(); if(empty($res)) { $createTableSpace = "CREATE TABLESPACE $tableSpace DATAFILE '{$this->config->name}.DBF' size 150 AUTOEXTEND ON"; $createUser = "CREATE USER {$this->config->name} IDENTIFIED by {$this->config->password} DEFAULT TABLESPACE {$this->config->name} DEFAULT INDEX TABLESPACE {$this->config->name}"; $this->rawQuery($createTableSpace); $this->rawQuery($createUser); } */ $createSchema = "CREATE SCHEMA {$this->config->name} AUTHORIZATION {$this->config->user}"; return $this->rawQuery($createSchema); default: return false; } } /** * Use database or schema. * * @param string $dbName * @access public * @return PDOStatement|false */ public function useDB($dbName) { switch($this->config->driver) { case 'oceanbase': case 'mysql': return $this->exec("USE {$this->config->name}"); case 'dm': return $this->exec("SET SCHEMA {$this->config->name}"); default: return false; } } /** * Format sql. * * @param string $sql * @access public * @return string */ public function formatSQL($sql) { $this->sql = $sql; switch($this->config->driver) { case 'dm': return $this->formatDmSQL($sql); return $sql; default: return $sql; } } /** * Format dm sql. * * @param string $sql * @access public * @return string */ public function formatDmSQL($sql) { $sql = trim($sql); $sql = $this->formatFunction($sql); $sql = $this->processDmChangeColumn($sql); $sql = $this->processDmTableIndex($sql); $actionPos = strpos($sql, ' '); $action = strtoupper(substr($sql, 0, $actionPos)); $setPos = 0; switch($action) { case 'SELECT': return $this->formatField($sql); case 'REPLACE': $result = $this->processReplace($sql); if($result != $sql) return $result; $sql = str_replace('REPLACE', 'INSERT', $sql); $action = 'INSERT'; case 'INSERT': case 'UPDATE': $setPos = stripos($sql, ' VALUES'); $sql = str_replace('0000-00-00', '1970-01-01', $sql); $sql = str_replace('00:00:00', '00:00:01', $sql); if(strpos($sql, "\\'") !== FALSE) $sql = str_replace("\\'", "''''", $sql); if(strpos($sql, '\"') !== FALSE) $sql = str_replace('\"', '"', $sql); if(strpos($sql, '\\\\') !== FALSE) $sql = str_replace('\\\\', '\\', $sql); break; case 'CREATE': if(stripos($sql, 'CREATE VIEW') === 0) $sql = str_replace('CREATE VIEW', 'CREATE OR REPLACE VIEW', $sql); if(stripos($sql, 'CREATE FUNCTION') === 0) return ''; if(stripos($sql, 'CREATE OR REPLACE VIEW ') === 0) { // Modify if function. $fieldsBegin = stripos($sql, 'select'); $fieldsEnd = stripos($sql, 'from'); $fields = substr($sql, $fieldsBegin+6, $fieldsEnd-$fieldsBegin-6); $fieldList = preg_split("/,(?![^(]+\))/", $fields); foreach($fieldList as $key => $field) { $aliasPos = stripos($field, ' AS '); $subField = substr($field, 0, $aliasPos); if(stripos($field, 'SUM(') === 0) $subField = substr($subField, 4, -1); $fieldParts = preg_split("/\+(?![^(]+\))/", $subField); foreach($fieldParts as $pkey => $fieldPart) { $originField = trim($fieldPart); if(stripos($originField, 'if(') === false) continue; $fieldParts[$pkey] = $this->formatDmIfFunction($originField); } $fieldList[$key] = str_replace($subField, implode(' + ', $fieldParts), $field); } $fields = implode(',', $fieldList); $sql = substr($sql, 0, $fieldsBegin+6) . $fields . substr($sql, $fieldsEnd); return str_replace('CREATE OR REPLACE VIEW ', 'CREATE VIEW ', $sql); } elseif(stripos($sql, 'CREATE UNIQUE INDEX') === 0 || stripos($sql, 'CREATE INDEX') === 0) { preg_match('/ON\s+([^.`\s]+\.)?`([^\s`]+)`/', $sql, $matches); $tableName = isset($matches[2]) ? str_replace($this->config->prefix, '', $matches[2]) : ''; $sql = preg_replace('/INDEX\ +\`/', 'INDEX `' . strtolower($tableName) . '_', $sql); } case 'ALTER': $sql = $this->formatField($sql); $sql = $this->formatAttr($sql); return $sql; case 'SET': if(stripos($sql, 'SET SCHEMA') === 0) return $sql; case 'USE': return ''; case 'DESC'; $tableName = str_ireplace(array('DESC ', '`'), '', $sql); $tableName = trim($tableName); if(strpos($tableName, ' ') !== false) list($tableName, $columnName) = explode(' ', $tableName); $sql = "select COLUMN_NAME as Field, DATA_TYPE as `Type`, DATA_LENGTH as Length, DATA_DEFAULT as `Default`, NULLABLE as `Null` from all_tab_columns where Table_Name='$tableName'"; if(!empty($columnName)) $sql .= " and COLUMN_NAME='$columnName'"; return $sql; case 'DROP': return $this->formatField($sql); } if($setPos <= 0) return $sql; $fields = substr($sql, 0, $setPos); $fields = $this->formatField($fields); $sql = $fields . substr($sql, $setPos); /* DMDB must set IDENTITY_INSERT 'on' to insert id field. */ if($action == 'INSERT' and stripos($fields, '"id"') !== FALSE) { $tableBegin = strpos($sql, '"' . $this->config->prefix); $tableEnd = strpos($sql, '"', $tableBegin + 1); $tableName = '' . $this->config->name . '."' . substr($sql, $tableBegin + 1, $tableEnd - $tableBegin - 1) . '"'; return "SET IDENTITY_INSERT $tableName ON;" . $sql; } return $sql; } /** * Format dm table index. * * @param string $sql * @access public * @return string */ public function processDmTableIndex($sql) { if(strpos($sql, 'DROP INDEX') === FALSE) return $sql; return preg_replace('/DROP INDEX `(\w+)` ON `zt_(\w+)`/', 'DROP INDEX IF EXISTS `$2_$1`', $sql); } /** * Format dm change column. * * @param string $sql * @access public * @return string */ public function processDmChangeColumn($sql) { if(strpos($sql, 'CHANGE COLUMN') === FALSE) return $sql; return preg_replace('/ALTER TABLE `([^`]+)` CHANGE COLUMN `([^`]+)` `([^`]+)` (\w+)/', 'ALTER TABLE `$1` RENAME COLUMN `$2` TO `$3`;', $sql); } /** * Format field. * * @param string $sql * @access public * @return string */ public function formatField($sql) { switch($this->config->driver) { case 'dm': $sql = str_replace('`', '"', $sql); return $sql; default: return $sql; } } /** * Format function. * * @param string $sql * @access public * @return string */ public function formatFunction($sql) { switch($this->config->driver) { case 'dm': /* DATE convert to TO_CHAR. */ $sql = preg_replace("/\bDATE\(([^)]*)\)/", "TO_CHAR($1, 'yyyy-mm-dd')", $sql, -1); return $sql; default: return $sql; } } /** * Format if function of dmdb. * * @param string $field * @access private * @return string */ public function formatDmIfFunction($field) { preg_match('/if\(.+\)+/i', $field, $matches); $if = $matches[0]; if(substr_count($if, '(') == 1) { $pos = strpos($if, ')'); $if = substr($if, 0, $pos+1); } /* fix sum(if(..., 1, 0)) , count(if(..., 1, 0)) */ if(substr($if, strlen($if)-2) == '))' and (stripos($field, 'sum(') == 0 or stripos($field, 'count(') == 0)) $if = substr($if, 0, strlen($if)-1); $parts = explode(',', substr($if, 3, strlen($if)-4)); // remove 'if(' and ')' $case = 'CASE WHEN ' . implode(',', array_slice($parts, 0, count($parts)-2)) . ' THEN ' . $parts[count($parts)-2] . ' ELSE ' . $parts[count($parts)-1] . ' END'; $field = str_ireplace($if, $case, $field); return $field; } /** * Format attribute of field. * * @param string $sql * @access public * @return string */ public function formatAttr($sql) { switch($this->config->driver) { case 'dm': $pos = stripos($sql, ' ENGINE'); if($pos > 0) $sql = substr($sql, 0, $pos); $sql = preg_replace('/\(\ *\d+\ *\)/', '', $sql); $replace = array( " AUTO_INCREMENT" => ' IDENTITY(1, 1)', " int " => ' integer ', " mediumint " => ' integer ', " smallint " => ' integer ', " tinyint " => ' integer ', " varchar " => ' varchar(255) ', " char " => ' varchar(255) ', " mediumtext " => ' text ', " mediumtext," => ' text,', " longtext " => ' text ', "COLLATE 'utf8_general_ci'" => ' ', " unsigned " => ' ', " zerofill " => ' ', "0000-00-00" => '1970-01-01', ); $sql = preg_replace('/ enum[\_0-9a-z\,\'\"\( ]+\)+/i', ' varchar(255) ', $sql); $sql = str_ireplace(array_keys($replace), array_values($replace), $sql); $sql = preg_replace('/\,\s+key[\_\"0-9a-z ]+\(+[\,\_\"0-9a-z ]+\)+/i', '', $sql); $sql = preg_replace('/\,\s*(unique|fulltext)*\s+key[\_\"0-9a-z ]+\(+[\,\_\"0-9a-z ]+\)+/i', '', $sql); $sql = preg_replace('/ float\s*\(+[\,\_\"0-9a-z ]+\)+/i', ' float', $sql); /* Convert "date" datetime to "date" datetime(0) to fix bug 25725, dm database datetime default 6 */ preg_match_all('/"[0-9a-zA-Z]+" datetime/', $sql, $datetimeMatch); if(!empty($datetimeMatch)) { foreach($datetimeMatch[0] as $match) $sql = str_replace($match, $match . '(0)', $sql); } if(strpos($sql, "ALTER TABLE") === 0) { $sql = $this->convertAlterTableSql($sql); if(stripos($sql, "ADD") !== false) { // 使用正则表达式匹配并去除 "AFTER" 关键字及其后面的内容 $pattern = "/\s+AFTER\s+.+$/i"; $sql = preg_replace($pattern, "", $sql); } } } return $sql; } /** * Process replace into sql. * * @param mixed $sql * @access public * @return void */ public function processReplace($sql) { // 解析REPLACE INTO语句,提取出表名、字段和值 $matches = []; preg_match('/^REPLACE\s+INTO\s+`?([\w_]+)`?\s*\((.*)\)\s+VALUES\s*\(([^()]+)\)\s*$/i', $sql, $matches); if(empty($matches)) return $sql; $table_name = $matches[1]; $columns = array_map('trim', explode(',', $matches[2])); $values = array_map('trim', explode(', ', $matches[3])); if($table_name == '' or $columns == '' or $values == '') return $sql; // 构造SELECT语句,查询数据是否存在 $where = []; foreach ($columns as $index => $column) { $value = trim($values[$index], "'"); $column = trim($column, '`'); $values[$index] = $value; $columns[$index] = $column; $where[] = "`$column` = '$value'"; } $select_sql = "SELECT * FROM `$table_name` WHERE " . implode(' AND ', $where); $result = $this->query($select_sql); $result = $result->fetchAll(); $sql = in_array('id', $columns) ? "SET IDENTITY_INSERT `$table_name` ON;" : ''; if($result) { // 数据已存在,构造UPDATE语句并执行 $set = []; $where = []; foreach ($columns as $index => $column) { $value = $values[$index]; $set[] = "`$column` = '$value'"; $where[] = "`$column` = '$value'"; } $sql .= "UPDATE `$table_name` SET " . implode(', ', $set) . " WHERE " . implode(' AND ', $where); } else { // 数据不存在,构造INSERT INTO语句并执行 $selectColumn = array(); $selectValue = array(); foreach ($columns as $index => $column) { $selectColumn[] .= "`$column`"; $selectValue[] .= "'{$values[$index]}'"; } $sql .= "INSERT INTO `$table_name` (" . implode(', ', $selectColumn) . ") VALUES (" . implode(', ', $selectValue) . ")"; } return $sql; } /** * Convert alter table sql. * * @param mixed $sql * @access public * @return void */ public function convertAlterTableSql($sql) { /* If table has datas and sql no default values defined, add default ''/0. */ if(strpos($sql, "NOT NULL") !== false && strpos($sql, "DEFAULT") === false) { $default = ''; if(strpos($sql, "integer") !== false) $default = 0; $sql = str_replace("NOT NULL", "NOT NULL DEFAULT '" . $default ."'", $sql); } $pattern = '/ALTER TABLE "(.*?)" CHANGE "(.*?)" "(.*?)" (.*?)(?:;|$)/'; preg_match($pattern, $sql, $matches); if(count($matches) != 5) return $sql; $tableName = $matches[1]; $oldColumnName = $matches[2]; $newColumnName = $matches[3]; $params = str_replace("'", "''", $matches[4]); $sql = 'begin '; if($oldColumnName != $newColumnName) $sql .= "execute immediate 'ALTER TABLE $tableName ALTER " . '"' . $oldColumnName . '" RENAME TO "' . $newColumnName . '"' . "';"; $sql .= "execute immediate 'ALTER TABLE $tableName MODIFY " . '"' . $newColumnName . '" ' . $params . "';"; $sql .= 'end;'; return $sql; } /** * Quote. * * @param string $string * @param int $parameter_type * @access public * @return string */ public function quote($string, $parameter_type = PDO::PARAM_STR) { return $this->pdo->quote($string, $parameter_type); } /** * Get last insert id. * * @param string $name * @access public * @return int|false */ public function lastInsertId($name = null) { $lastInsertID = $this->pdo->lastInsertId($name); return $lastInsertID !== false ? (int)$lastInsertID : false; } /** * Begin transaction. * * @access public * @return bool */ public function beginTransaction() { return $this->pdo->beginTransaction(); } /** * Check in transaction or not. * * @access public * @return bool */ public function inTransaction() { return $this->pdo->inTransaction(); } /** * Roll back if transaction failed. * * @access public * @return bool */ public function rollBack() { return $this->pdo->rollBack(); } /** * Commit transaction. * * @access public * @return bool */ public function commit() { return $this->pdo->commit(); } /** * 将SQL语句保存到队列中。 * Save sql to SQLite queue. * * @param string $sql * @access public * @return int|null */ public function pushSqliteQueue(string $sql): int|null { $allowedActions = array('insert', 'update', 'delete', 'replace'); $sql = str_replace(array('\r', '\n'), ' ', trim($sql)); $actionPos = strpos($sql, ' '); $action = strtolower(substr($sql, 0, $actionPos)); if(!in_array($action, $allowedActions)) return null; foreach($this->config->sqliteBlacklist as $table) { $tableName = $this->config->prefix . $table; if(stripos($sql, $tableName) !== false) return null; } $table = TABLE_SQLITE_QUEUE; $sql = $this->quote($sql); $now = "now()"; $action = $this->getLastActionID() + 1; $queue = "INSERT INTO $table SET `sql` = $sql, `addDate` = $now, `status` = 'wait', `action` = $action"; $this->pdo->exec($queue); return $this->pdo->lastInsertId(); } /** * 获取最后一条动态的id。 * Get last action id. * * @access public * @return int|false */ public function getLastActionID(): int|false { $table = TABLE_ACTION; $sql = "SELECT id FROM $table ORDER BY id desc limit 1"; $lastAction = $this->pdo->query($sql)->fetch(); return $lastAction ? (int)$lastAction->id : false; } /** * 安装时检查数据库用户权限。 * Check user privilege. * * @access public * @return string */ public function checkUserPriv(): string { global $config; if(!in_array($this->config->driver, $config->mysqlDriverList)) return ''; $dbName = $this->config->name; $user = $this->config->user; $host = ($this->config->host == 'localhost' || $this->config->host == '127.0.0.1') ? 'localhost' : '%'; $privPairs = array(); try { $privList = $this->pdo->query("SHOW GRANTS FOR {$user}@'{$host}';")->fetchAll(PDO::FETCH_COLUMN); } catch(Exception $e) { return ''; } foreach($privList as $privSQL) { if(strpos($privSQL, '*.*') === false && strpos($privSQL, "`$dbName`.*") === false) continue; // 如果权限不是全局或者当前数据库的,跳过 if(!preg_match('/GRANT (.*) ON (.+) TO/', $privSQL, $matches)) continue; $privs = explode(',', $matches[1]); foreach($privs as $priv) { $priv = trim($priv); $privPairs[$priv] = true; } } if(isset($privPairs['ALL PRIVILEGES'])) return ''; // 禅道所需的权限 $requiredPrivs = array('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE', 'ALTER', 'INDEX', 'CREATE VIEW'); $missingPrivs = array_diff($requiredPrivs, array_keys($privPairs)); if(empty($missingPrivs)) return ''; // 所有权限都满足 $missingPrivsSQL = implode(', ', $missingPrivs); return "GRANT {$missingPrivsSQL} ON `{$dbName}`.* TO {$user}@'{$host}';"; } /** * 获取数据库版本。 * Get version. * * @access public * @return string */ public function getVersion(): string { switch($this->config->driver) { case 'oceanbase': case 'mysql': $sql = "SELECT version() AS version"; break; case 'dm': default: $sql = ''; } if(empty($sql)) return ''; return $this->rawQuery($sql)->fetch()->version; } }