dbh.class.php 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217
  1. <?php
  2. /**
  3. * ZenTaoPHP的dbh类。
  4. * The dbh class file of ZenTaoPHP framework.
  5. *
  6. * The author disclaims copyright to this source code. In place of
  7. * a legal notice, here is a blessing:
  8. *
  9. * May you do good and not evil.
  10. * May you find forgiveness for yourself and forgive others.
  11. * May you share freely, never taking more than you give.
  12. */
  13. /**
  14. * DBH类。
  15. * DBH, database handler.
  16. *
  17. * @package lib
  18. */
  19. class dbh
  20. {
  21. /**
  22. * Flag for database.
  23. *
  24. * @var string MASTER|SLAVE|BI
  25. * @access private
  26. */
  27. private $flag;
  28. /**
  29. * PDO.
  30. *
  31. * @var object
  32. * @access private
  33. */
  34. private $pdo;
  35. /**
  36. * PDO Statement.
  37. *
  38. * @var object
  39. * @access private
  40. */
  41. private $statement;
  42. /**
  43. * 应用配置信息。
  44. * The app config.
  45. *
  46. * @var object
  47. * @access private
  48. */
  49. private $config;
  50. /**
  51. * Database dbConfig.
  52. *
  53. * @var object
  54. * @access private
  55. */
  56. private $dbConfig;
  57. /**
  58. * The SQL string of last query.
  59. *
  60. * @var string
  61. * @access private
  62. */
  63. private $sql;
  64. /**
  65. * 主库还是从库的标记。
  66. * Flag for master or slave.
  67. *
  68. * @var array
  69. * @access public
  70. */
  71. public static $flags = [];
  72. /**
  73. * 触发SQL执行的文件和行号。
  74. * The file and line number that triggered the SQL execution.
  75. *
  76. * @var array
  77. * @access public
  78. */
  79. public static $traces = [];
  80. /**
  81. * 执行的请求,所有的查询都保存在该数组。
  82. * The queries executed. Every query will be saved in this array.
  83. *
  84. * @var array
  85. * @access public
  86. */
  87. public static $queries = [];
  88. /**
  89. * SQL执行时长。
  90. * The duration of SQL execution.
  91. *
  92. * @var array
  93. * @access public
  94. */
  95. public static $durations = [];
  96. /**
  97. * 数据库的标识符引号。
  98. * identifier quote character.
  99. *
  100. * @var string
  101. * @access public
  102. */
  103. public $iqchar = '`';
  104. /**
  105. * Constructor
  106. *
  107. * @param object $dbConfig
  108. * @param bool $setSchema
  109. * @param string $flag
  110. * @access public
  111. * @return void
  112. */
  113. public function __construct($dbConfig, $setSchema = true, $flag = 'MASTER')
  114. {
  115. global $config;
  116. $this->config = $config;
  117. $this->dbConfig = $dbConfig;
  118. $this->flag = $flag;
  119. $this->pdo = $this->pdoInit($setSchema);
  120. $queries = [];
  121. if(in_array($dbConfig->driver, $config->mysqlDriverList))
  122. {
  123. if($dbConfig->driver == 'mysql') $queries[] = "SET NAMES {$dbConfig->encoding}" . ($dbConfig->collation ? " COLLATE '{$dbConfig->collation}'" : '');
  124. if(isset($dbConfig->strictMode) && empty($dbConfig->strictMode)) $queries[] = "SET @@sql_mode= ''";
  125. }
  126. else
  127. {
  128. $this->iqchar = '"';
  129. if($setSchema)
  130. {
  131. if($dbConfig->driver == 'dm')
  132. {
  133. $queries[] = "SET SCHEMA {$dbConfig->name}";
  134. }
  135. elseif(in_array($dbConfig->driver, $config->pgsqlDriverList))
  136. {
  137. $schema = $dbConfig->schema ?? 'public';
  138. $queries[] = "SET SCHEMA '{$schema}'";
  139. }
  140. }
  141. }
  142. if(!empty($queries))
  143. {
  144. foreach($queries as $query) $this->rawQuery($query);
  145. }
  146. }
  147. /**
  148. * 获取PDO驱动名称。
  149. * Get pdo driver name.
  150. *
  151. * @access private
  152. * @return string
  153. */
  154. private function getPdoDriver()
  155. {
  156. if($this->dbConfig->driver == 'kingbase') return 'kdb';
  157. if(in_array($this->dbConfig->driver, $this->config->mysqlDriverList)) return 'mysql';
  158. if(in_array($this->dbConfig->driver, $this->config->pgsqlDriverList)) return 'pgsql';
  159. return $this->dbConfig->driver;
  160. }
  161. /**
  162. * 初始化PDO对象。
  163. * Init pdo.
  164. *
  165. * @param bool $setSchema
  166. * @access private
  167. * @return object
  168. */
  169. private function pdoInit($setSchema)
  170. {
  171. $driver = $this->getPdoDriver();
  172. $dsn = "{$driver}:host={$this->dbConfig->host};port={$this->dbConfig->port}";
  173. if($setSchema)
  174. {
  175. $dsn .= ";dbname={$this->dbConfig->name}";
  176. }
  177. elseif(in_array($this->dbConfig->driver, $this->config->pgsqlDriverList))
  178. {
  179. $dsn .= ";dbname={$this->dbConfig->driver}"; // default database
  180. }
  181. $password = helper::decryptPassword($this->dbConfig->password);
  182. $pdo = new PDO($dsn, $this->dbConfig->user, $password);
  183. $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
  184. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  185. return $pdo;
  186. }
  187. /**
  188. * Process PDO/SQL error.
  189. *
  190. * @param object $exception
  191. * @access public
  192. * @return void
  193. */
  194. public function sqlError($exception)
  195. {
  196. $newException = new PDOException($exception->getMessage() . " ,the sql is: '{$this->sql}'");
  197. $newException->errorInfo = $exception->errorInfo;
  198. throw $newException;
  199. }
  200. /**
  201. * Execute sql.
  202. *
  203. * @param string $sql
  204. * @access public
  205. * @return int|false
  206. */
  207. public function exec($sql)
  208. {
  209. return $this->executeSql($sql, __FUNCTION__);
  210. }
  211. /**
  212. * Query sql.
  213. *
  214. * @param string $sql
  215. * @see https://www.php.net/manual/en/pdo.query.php
  216. * @access public
  217. * @return PDOStatement|false
  218. */
  219. public function query($sql)
  220. {
  221. return $this->executeSql($sql, __FUNCTION__);
  222. }
  223. /**
  224. * Query raw sql.
  225. *
  226. * @param string $sql
  227. * @access public
  228. * @return PDOStatement|false
  229. */
  230. public function rawQuery($sql)
  231. {
  232. return $this->executeSql($sql, __FUNCTION__);
  233. }
  234. /**
  235. * 执行SQL语句并记录执行时间和调用栈信息。
  236. * Execute SQL and record the duration and trace info.
  237. *
  238. * @param string $sql
  239. * @param string $mode exec|query|rawQuery
  240. * @access private
  241. * @return mixed
  242. */
  243. private function executeSql($sql, $mode)
  244. {
  245. if($mode == 'exec' || $mode == 'query')
  246. {
  247. $sql = $this->formatSQL($sql);
  248. if(!$sql) return false;
  249. if($mode == 'exec' && !empty($this->dbConfig->enableSqlite)) $this->pushSqliteQueue($sql);
  250. }
  251. $result = false;
  252. $begin = microtime(true);
  253. $method = $mode == 'exec' ? 'exec' : 'query';
  254. try
  255. {
  256. $result = $this->pdo->$method($sql);
  257. }
  258. catch(PDOException $e)
  259. {
  260. $this->sqlError($e);
  261. }
  262. finally
  263. {
  264. dbh::$flags[] = $this->flag;
  265. dbh::$queries[] = dao::processKeywords($sql);
  266. dbh::$durations[] = round(microtime(true) - $begin, 6);
  267. if(!empty($this->config->debug))
  268. {
  269. $trace = $this->getTrace();
  270. if($trace) dbh::$traces[] = 'vim +' . $trace['line'] . ' ' . $trace['file'];
  271. }
  272. }
  273. return $result;
  274. }
  275. /**
  276. * 获取当前执行的 SQL 的调用栈信息。
  277. * Get the call stack information of the currently executed SQL.
  278. *
  279. * @access private
  280. * @return array
  281. */
  282. private function getTrace()
  283. {
  284. $traces = array_reverse(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS));
  285. foreach($traces as $key => $trace)
  286. {
  287. $class = $trace['class'] ?? '';
  288. $function = $trace['function'] ?? '';
  289. if($class == 'settingModel' && strpos(',getItem,getItems,setItem,setItems,updateItem,deleteItems,', ",$function,") !== false) return $trace;
  290. if($class == 'baseDAO' && strpos(',exec,fetch,fetchPairs,fetchGroup,explain,showTables,getTableEngines,descTable,', ",$function,") !== false) return $trace;
  291. if($class == 'baseDAO' && $function == 'getProfiles') return $traces[$key + 1];
  292. if($class == 'baseDAO' && $function == 'fetchAll')
  293. {
  294. if($traces[$key + 1]['class'] == 'baseDAO' && $traces[$key + 1]['function'] == 'extractSQLFields') return $traces[$key + 2];
  295. return $trace;
  296. }
  297. if($class == 'baseDAO' && stripos($function, 'findBy') === 0) return $trace;
  298. if($class == 'baseRouter' && $function == 'dbQuery') return $trace;
  299. if($class == 'dbh' && strpos('exec,query,rawQuery', $function) !== false) return $trace;
  300. }
  301. return [];
  302. }
  303. /**
  304. * Prepare a PDO statement.
  305. *
  306. * @param string $sql
  307. * @access public
  308. * @return PDOStatement|false
  309. */
  310. public function prepare($sql)
  311. {
  312. $this->sql = $sql;
  313. try
  314. {
  315. $this->statement = $this->pdo->prepare($sql);
  316. }
  317. catch(PDOException $e)
  318. {
  319. $this->sqlError($e);
  320. }
  321. return $this->statement;
  322. }
  323. /**
  324. * Prepare and execute a PDO statement.
  325. *
  326. * @param string $sql
  327. * @param array $params
  328. * @access public
  329. * @return PDOStatement|false
  330. */
  331. public function execute($sql, $params)
  332. {
  333. $this->statement = $this->prepare($sql);
  334. try
  335. {
  336. $this->statement->execute($params);
  337. }
  338. catch(PDOException $e)
  339. {
  340. $this->sqlError($e);
  341. }
  342. return $this->statement;
  343. }
  344. /**
  345. * Set attribute.
  346. *
  347. * @param int $attribute
  348. * @param mixed $value
  349. * @access public
  350. * @return bool
  351. */
  352. public function setAttribute($attribute, $value)
  353. {
  354. return $this->pdo->setAttribute($attribute, $value);
  355. }
  356. /**
  357. * Check db exits or not.
  358. *
  359. * @access public
  360. * @return bool
  361. */
  362. public function dbExists()
  363. {
  364. if($this->dbConfig->driver == 'dm')
  365. {
  366. $sql = "SELECT * FROM ALL_OBJECTS WHERE object_type='SCH' AND owner='{$this->dbConfig->name}'";
  367. return $this->rawQuery($sql)->fetch();
  368. }
  369. if(in_array($this->dbConfig->driver, $this->config->mysqlDriverList))
  370. {
  371. $sql = "SHOW DATABASES like '{$this->dbConfig->name}'";
  372. return $this->rawQuery($sql)->fetch();
  373. }
  374. if(in_array($this->dbConfig->driver, $this->config->pgsqlDriverList))
  375. {
  376. $sql = "SELECT * FROM pg_database WHERE datname ='{$this->dbConfig->name}'";
  377. return $this->rawQuery($sql)->fetch();
  378. }
  379. return false;
  380. }
  381. /**
  382. * Check table exist or not.
  383. *
  384. * @param string $tableName
  385. * @access public
  386. * @return void
  387. */
  388. public function tableExist($tableName)
  389. {
  390. $tableName = str_replace(array("'", '`'), "", $tableName);
  391. if($this->dbConfig->driver == 'dm')
  392. {
  393. $sql = "SELECT * FROM all_tables WHERE owner='{$this->dbConfig->name}' AND table_name='{$tableName}'";
  394. return $this->rawQuery($sql)->fetch();
  395. }
  396. if(in_array($this->dbConfig->driver, $this->config->mysqlDriverList))
  397. {
  398. $sql = "SHOW TABLES FROM {$this->dbConfig->name} like '{$tableName}'";
  399. return $this->rawQuery($sql)->fetch();
  400. }
  401. if(in_array($this->dbConfig->driver, $this->config->pgsqlDriverList))
  402. {
  403. $this->useDB($this->dbConfig->name);
  404. $sql = "SELECT * FROM information_schema.tables WHERE table_catalog = '{$this->dbConfig->name}' AND table_name='{$tableName}'";
  405. return $this->rawQuery($sql)->fetch();
  406. }
  407. return false;
  408. }
  409. /**
  410. * 获取数据库支持的字符集和排序规则。
  411. * Get the database charset and collation.
  412. *
  413. * @param string $database
  414. * @access public
  415. * @return array
  416. */
  417. public function getDatabaseCharsetAndCollation($database = '')
  418. {
  419. if($this->dbConfig->driver != 'mysql') return ['charset' => 'utf8', 'collation' => ''];
  420. if(empty($database)) $database = $this->dbConfig->name;
  421. $sql = "SELECT DEFAULT_CHARACTER_SET_NAME AS charset, DEFAULT_COLLATION_NAME AS collation FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = '{$database}';";
  422. $result = $this->rawQuery($sql)->fetch();
  423. if($result) return (array)$result;
  424. return $this->getServerCharsetAndCollation();
  425. }
  426. /**
  427. * 获取服务器支持的字符集和排序规则。
  428. * Get the server charset and collation.
  429. *
  430. * @access public
  431. * @return array
  432. */
  433. public function getServerCharsetAndCollation()
  434. {
  435. if($this->dbConfig->driver != 'mysql') return ['charset' => 'utf8', 'collation' => ''];
  436. $charsets = [];
  437. $statement = $this->rawQuery("SHOW CHARSET WHERE Charset LIKE 'utf8%';");
  438. while($charset = $statement->fetch(PDO::FETCH_ASSOC)) $charsets[$charset['Charset']] = ['charset' => $charset['Charset'], 'collation' => $charset['Default collation']];
  439. return $charsets['utf8mb4'] ?? $charsets['utf8mb3'] ?? ['charset' => 'utf8', 'collation' => 'utf8_general_ci'];
  440. }
  441. /**
  442. * Create database.
  443. *
  444. * @param string $version
  445. * @access public
  446. * @return PDOStatement|false
  447. */
  448. public function createDB($version)
  449. {
  450. if($this->dbConfig->driver == 'dm')
  451. {
  452. $createSchema = "CREATE SCHEMA {$this->dbConfig->name} AUTHORIZATION {$this->dbConfig->user}";
  453. return $this->rawQuery($createSchema);
  454. }
  455. if($this->dbConfig->driver == 'mysql')
  456. {
  457. $result = $this->getServerCharsetAndCollation();
  458. $sql = "CREATE DATABASE `{$this->dbConfig->name}` DEFAULT CHARACTER SET {$result['charset']} COLLATE {$result['collation']}";
  459. return $this->rawQuery($sql);
  460. }
  461. if($this->dbConfig->driver == 'oceanbase' || in_array($this->dbConfig->driver, $this->config->pgsqlDriverList))
  462. {
  463. $sql = "CREATE DATABASE {$this->dbConfig->name}";
  464. return $this->rawQuery($sql);
  465. }
  466. return false;
  467. }
  468. /**
  469. * Use database or schema.
  470. *
  471. * @param string $dbName
  472. * @access public
  473. * @return PDOStatement|false
  474. */
  475. public function useDB($dbName)
  476. {
  477. if($this->dbConfig->driver == 'dm') return $this->exec("SET SCHEMA {$dbName}");
  478. if(in_array($this->dbConfig->driver, $this->config->mysqlDriverList)) return $this->exec("USE {$dbName}");
  479. if(in_array($this->dbConfig->driver, $this->config->pgsqlDriverList))
  480. {
  481. $this->pdo = $this->pdoInit(true);
  482. $schema = $this->dbConfig->schema ?? 'public';
  483. return $this->exec("SET SCHEMA '{$schema}'");
  484. }
  485. return false;
  486. }
  487. /**
  488. * Format sql.
  489. *
  490. * @param string $sql
  491. * @access public
  492. * @return string
  493. */
  494. public function formatSQL($sql)
  495. {
  496. $this->sql = $sql;
  497. if($this->dbConfig->driver == 'dm') return $this->formatDmSQL($sql);
  498. if(in_array($this->dbConfig->driver, $this->config->pgsqlDriverList)) return $this->formatPgSQL($sql);
  499. return $sql;
  500. }
  501. /**
  502. * Format pgsql sql.
  503. *
  504. * @param string $sql
  505. * @access public
  506. * @return string
  507. */
  508. public function formatPgSQL($sql)
  509. {
  510. $sql = trim($sql);
  511. $sql = $this->formatFunction($sql);
  512. $sql = $this->processDmChangeColumn($sql);
  513. $sql = $this->processDmTableIndex($sql);
  514. $actionPos = strpos($sql, ' ');
  515. $action = strtoupper(substr($sql, 0, $actionPos));
  516. $setPos = 0;
  517. switch($action)
  518. {
  519. case 'SELECT':
  520. return $this->formatField($sql);
  521. case 'REPLACE':
  522. $result = $this->processReplace($sql);
  523. if($result != $sql) return $result;
  524. $sql = str_replace('REPLACE', 'INSERT', $sql);
  525. case 'INSERT':
  526. case 'UPDATE':
  527. $setPos = stripos($sql, ' VALUES');
  528. $sql = str_replace('0000-00-00', '1970-01-01', $sql);
  529. $sql = str_replace('00:00:00', '00:00:01', $sql);
  530. if(strpos($sql, "\\'") !== false) $sql = str_replace("\\'", "''''", $sql);
  531. if(strpos($sql, '\"') !== false) $sql = str_replace('\"', '"', $sql);
  532. if(strpos($sql, '\\\\') !== false) $sql = str_replace('\\\\', '\\', $sql);
  533. if(stripos($sql, 'CURDATE()')) $sql = str_replace('CURDATE()', 'CURRENT_DATE', $sql);
  534. break;
  535. case 'CREATE':
  536. if(stripos($sql, 'CREATE VIEW') === 0) $sql = str_replace('CREATE VIEW', 'CREATE OR REPLACE VIEW', $sql);
  537. if(stripos($sql, 'CREATE FUNCTION') === 0) return '';
  538. if(stripos($sql, 'CREATE OR REPLACE VIEW ') === 0)
  539. {
  540. $sql = $this->formatField($sql);
  541. return str_replace('CREATE OR REPLACE VIEW ', 'CREATE VIEW ', $sql);
  542. }
  543. elseif(stripos($sql, 'CREATE UNIQUE INDEX') === 0 || stripos($sql, 'CREATE INDEX') === 0)
  544. {
  545. preg_match('/ON\s+([^.`\s]+\.)?`([^\s`]+)`/', $sql, $matches);
  546. $tableName = isset($matches[2]) ? str_replace($this->dbConfig->prefix, '', $matches[2]) : '';
  547. $sql = preg_replace('/INDEX\ +\`/', 'INDEX `' . strtolower($tableName) . '_', $sql);
  548. }
  549. /* Remove FULLTEXT index. */
  550. if(stripos($sql, 'FULLTEXT'))
  551. {
  552. $pattern = '/,\s*FULLTEXT\s+KEY\s+`[^`]+`\s*\([^)]+\)\s*/i';
  553. $sql = preg_replace($pattern, '', $sql);
  554. }
  555. /* Remove comment. */
  556. $pattern = '/\s+COMMENT\s+[\'"].*?[\'"]\s*/i';
  557. $sql = preg_replace($pattern, '', $sql);
  558. $sql = $this->formatAttr($sql);
  559. case 'ALTER':
  560. $sql = $this->formatField($sql);
  561. $sql = $this->formatAttr($sql);
  562. return $sql;
  563. case 'SET':
  564. if(stripos($sql, 'SET SCHEMA') === 0) return $sql;
  565. case 'USE':
  566. return '';
  567. case 'DESC';
  568. $tableName = str_ireplace(array('DESC ', '`'), '', $sql);
  569. $tableName = trim($tableName);
  570. if(strpos($tableName, ' ') !== false) list($tableName, $columnName) = explode(' ', $tableName);
  571. $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'";
  572. if(!empty($columnName)) $sql .= " and COLUMN_NAME='$columnName'";
  573. return $sql;
  574. case 'DROP':
  575. $sql .= ' CASCADE';
  576. return $this->formatField($sql);
  577. }
  578. if($setPos <= 0) return $sql;
  579. $fields = substr($sql, 0, $setPos);
  580. $fields = $this->formatField($fields);
  581. return $fields . substr($sql, $setPos);
  582. }
  583. /**
  584. * Format dm sql.
  585. *
  586. * @param string $sql
  587. * @access public
  588. * @return string
  589. */
  590. public function formatDmSQL($sql)
  591. {
  592. $sql = trim($sql);
  593. $sql = $this->formatFunction($sql);
  594. $sql = $this->processDmChangeColumn($sql);
  595. $sql = $this->processDmTableIndex($sql);
  596. $actionPos = strpos($sql, ' ');
  597. $action = strtoupper(substr($sql, 0, $actionPos));
  598. $setPos = 0;
  599. switch($action)
  600. {
  601. case 'SELECT':
  602. return $this->formatField($sql);
  603. case 'REPLACE':
  604. $result = $this->processReplace($sql, 'dm');
  605. if($result != $sql) return $result;
  606. $sql = str_replace('REPLACE', 'INSERT', $sql);
  607. $action = 'INSERT';
  608. case 'INSERT':
  609. case 'UPDATE':
  610. $setPos = stripos($sql, ' VALUES');
  611. $sql = str_replace('0000-00-00', '1970-01-01', $sql);
  612. $sql = str_replace('00:00:00', '00:00:01', $sql);
  613. if(strpos($sql, "\\'") !== FALSE) $sql = str_replace("\\'", "''''", $sql);
  614. if(strpos($sql, '\"') !== FALSE) $sql = str_replace('\"', '"', $sql);
  615. if(strpos($sql, '\\\\') !== FALSE) $sql = str_replace('\\\\', '\\', $sql);
  616. break;
  617. case 'CREATE':
  618. if(stripos($sql, 'CREATE VIEW') === 0) $sql = str_replace('CREATE VIEW', 'CREATE OR REPLACE VIEW', $sql);
  619. if(stripos($sql, 'CREATE FUNCTION') === 0) return '';
  620. if(stripos($sql, 'CREATE OR REPLACE VIEW ') === 0)
  621. {
  622. // Modify if function.
  623. $fieldsBegin = stripos($sql, 'select');
  624. $fieldsEnd = stripos($sql, 'from');
  625. $fields = substr($sql, $fieldsBegin+6, $fieldsEnd-$fieldsBegin-6);
  626. $fieldList = preg_split("/,(?![^(]+\))/", $fields);
  627. foreach($fieldList as $key => $field)
  628. {
  629. $aliasPos = stripos($field, ' AS ');
  630. $subField = substr($field, 0, $aliasPos);
  631. if(stripos($field, 'SUM(') === 0) $subField = substr($subField, 4, -1);
  632. $fieldParts = preg_split("/\+(?![^(]+\))/", $subField);
  633. foreach($fieldParts as $pkey => $fieldPart)
  634. {
  635. $originField = trim($fieldPart);
  636. if(stripos($originField, 'if(') === false) continue;
  637. $fieldParts[$pkey] = $this->formatDmIfFunction($originField);
  638. }
  639. $fieldList[$key] = str_replace($subField, implode(' + ', $fieldParts), $field);
  640. }
  641. $fields = implode(',', $fieldList);
  642. $sql = substr($sql, 0, $fieldsBegin+6) . $fields . substr($sql, $fieldsEnd);
  643. return str_replace('CREATE OR REPLACE VIEW ', 'CREATE VIEW ', $sql);
  644. }
  645. elseif(stripos($sql, 'CREATE UNIQUE INDEX') === 0 || stripos($sql, 'CREATE INDEX') === 0)
  646. {
  647. preg_match('/ON\s+([^.`\s]+\.)?`([^\s`]+)`/', $sql, $matches);
  648. $tableName = isset($matches[2]) ? str_replace($this->dbConfig->prefix, '', $matches[2]) : '';
  649. $sql = preg_replace('/INDEX\ +\`/', 'INDEX `' . strtolower($tableName) . '_', $sql);
  650. }
  651. /* Remove comment. */
  652. $pattern = '/\s+COMMENT\s+[\'"].*?[\'"]\s*/i';
  653. $sql = preg_replace($pattern, '', $sql);
  654. case 'ALTER':
  655. $sql = $this->formatField($sql);
  656. $sql = $this->formatAttr($sql);
  657. return $sql;
  658. case 'SET':
  659. if(stripos($sql, 'SET SCHEMA') === 0) return $sql;
  660. case 'USE':
  661. return '';
  662. case 'DESC';
  663. $tableName = str_ireplace(array('DESC ', '`'), '', $sql);
  664. $tableName = trim($tableName);
  665. if(strpos($tableName, ' ') !== false) list($tableName, $columnName) = explode(' ', $tableName);
  666. $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'";
  667. if(!empty($columnName)) $sql .= " and COLUMN_NAME='$columnName'";
  668. return $sql;
  669. case 'DROP':
  670. return $this->formatField($sql);
  671. }
  672. if($setPos <= 0) return $sql;
  673. $fields = substr($sql, 0, $setPos);
  674. $fields = $this->formatField($fields);
  675. $sql = $fields . substr($sql, $setPos);
  676. /* DMDB must set IDENTITY_INSERT 'on' to insert id field. */
  677. if($action == 'INSERT' and stripos($fields, '"id"') !== FALSE)
  678. {
  679. $tableBegin = strpos($sql, '"' . $this->dbConfig->prefix);
  680. $tableEnd = strpos($sql, '"', $tableBegin + 1);
  681. $tableName = '' . $this->dbConfig->name . '."' . substr($sql, $tableBegin + 1, $tableEnd - $tableBegin - 1) . '"';
  682. return "SET IDENTITY_INSERT $tableName ON;" . $sql;
  683. }
  684. return $sql;
  685. }
  686. /**
  687. * Format dm table index.
  688. *
  689. * @param string $sql
  690. * @access public
  691. * @return string
  692. */
  693. public function processDmTableIndex($sql)
  694. {
  695. if(strpos($sql, 'DROP INDEX') === FALSE) return $sql;
  696. return preg_replace('/DROP INDEX `(\w+)` ON `zt_(\w+)`/', 'DROP INDEX IF EXISTS `$2_$1`', $sql);
  697. }
  698. /**
  699. * Format dm change column.
  700. *
  701. * @param string $sql
  702. * @access public
  703. * @return string
  704. */
  705. public function processDmChangeColumn($sql)
  706. {
  707. if(strpos($sql, 'CHANGE COLUMN') === FALSE) return $sql;
  708. return preg_replace('/ALTER TABLE `([^`]+)` CHANGE COLUMN `([^`]+)` `([^`]+)` (\w+)/', 'ALTER TABLE `$1` RENAME COLUMN `$2` TO `$3`;', $sql);
  709. }
  710. /**
  711. * Format field.
  712. *
  713. * @param string $sql
  714. * @access public
  715. * @return string
  716. */
  717. public function formatField($sql)
  718. {
  719. if($this->dbConfig->driver == 'dm' || in_array($this->dbConfig->driver, $this->config->pgsqlDriverList))
  720. {
  721. $sql = str_replace('`', '"', $sql);
  722. $sql = preg_replace('/(?<!\w)if\(/i', '"IF"(', $sql);
  723. }
  724. return $sql;
  725. }
  726. /**
  727. * Format function.
  728. *
  729. * @param string $sql
  730. * @access public
  731. * @return string
  732. */
  733. public function formatFunction($sql)
  734. {
  735. /* DATE convert to TO_CHAR. */
  736. if($this->dbConfig->driver == 'dm') return preg_replace("/\bDATE\(([^)]*)\)/", "TO_CHAR($1, 'yyyy-mm-dd')", $sql, -1);
  737. return $sql;
  738. }
  739. /**
  740. * Format if function of dmdb.
  741. *
  742. * @param string $field
  743. * @access private
  744. * @return string
  745. */
  746. public function formatDmIfFunction($field)
  747. {
  748. preg_match('/(?<![a-zA-Z\'"])\bif\s*\(.+\)/i', $field, $matches);
  749. if(empty($matches)) return $field;
  750. $if = $matches[0];
  751. if(substr_count($if, '(') == 1)
  752. {
  753. $pos = strpos($if, ')');
  754. $if = substr($if, 0, $pos+1);
  755. }
  756. /* fix sum(if(..., 1, 0)) , count(if(..., 1, 0)) */
  757. if(substr($if, strlen($if)-2) == '))' and (stripos($field, 'sum(') == 0 or stripos($field, 'count(') == 0)) $if = substr($if, 0, strlen($if)-1);
  758. $parts = explode(',', substr($if, 3, strlen($if)-4)); // remove 'if(' and ')'
  759. $case = 'CASE WHEN ' . implode(',', array_slice($parts, 0, count($parts)-2)) . ' THEN ' . $parts[count($parts)-2] . ' ELSE ' . $parts[count($parts)-1] . ' END';
  760. $field = str_ireplace($if, $case, $field);
  761. return $field;
  762. }
  763. /**
  764. * Format attribute of field.
  765. *
  766. * @param string $sql
  767. * @access public
  768. * @return string
  769. */
  770. public function formatAttr($sql)
  771. {
  772. if($this->dbConfig->driver == 'dm' || in_array($this->dbConfig->driver, $this->config->pgsqlDriverList))
  773. {
  774. $pos = stripos($sql, ' ENGINE');
  775. if($pos > 0) $sql = substr($sql, 0, $pos);
  776. $sql = preg_replace('/\(\ *\d+\ *\)/', '', $sql);
  777. $replace = array(
  778. " int " => ' integer ',
  779. " mediumint " => ' integer ',
  780. " smallint " => ' integer ',
  781. " tinyint " => ' integer ',
  782. " varchar " => ' varchar(255) ',
  783. " char " => ' varchar(255) ',
  784. " mediumtext " => ' text ',
  785. " mediumtext," => ' text,',
  786. " longtext " => ' text ',
  787. "COLLATE 'utf8_general_ci'" => ' ',
  788. " unsigned " => ' ',
  789. " zerofill " => ' ',
  790. "0000-00-00" => '1970-01-01',
  791. );
  792. if($this->dbConfig->driver == 'dm')
  793. {
  794. $sql = str_ireplace(' AUTO_INCREMENT', ' IDENTITY(1, 1)', $sql);
  795. }
  796. else
  797. {
  798. $sql = preg_replace('/(\s*`[^`]+`)\s+\K.+AUTO_INCREMENT(,)/i', ' serial,', $sql);
  799. $sql = str_ireplace(' DATETIME', ' TIMESTAMP', $sql);
  800. }
  801. $sql = preg_replace('/ enum[\_0-9a-z\,\'\"\( ]+\)+/i', ' varchar(255) ', $sql);
  802. $sql = str_ireplace(array_keys($replace), array_values($replace), $sql);
  803. $sql = preg_replace('/\,\s+key[\_\"0-9a-z ]+\(+[\,\_\"0-9a-z ]+\)+/i', '', $sql);
  804. $sql = preg_replace('/\,\s*(unique|fulltext)*\s+key[\_\"0-9a-z ]+\(+[\,\_\"0-9a-z ]+\)+/i', '', $sql);
  805. $sql = preg_replace('/ float\s*\(+[\,\_\"0-9a-z ]+\)+/i', ' float', $sql);
  806. /* Convert "date" datetime to "date" datetime(0) to fix bug 25725, dm database datetime default 6 */
  807. preg_match_all('/"[0-9a-zA-Z]+" datetime/', $sql, $datetimeMatch);
  808. if(!empty($datetimeMatch))
  809. {
  810. foreach($datetimeMatch[0] as $match) $sql = str_replace($match, $match . '(0)', $sql);
  811. }
  812. if(strpos($sql, "ALTER TABLE") === 0)
  813. {
  814. $sql = $this->convertAlterTableSql($sql);
  815. if(stripos($sql, "ADD") !== false)
  816. {
  817. // 使用正则表达式匹配并去除 "AFTER" 关键字及其后面的内容
  818. $pattern = "/\s+AFTER\s+.+$/i";
  819. $sql = preg_replace($pattern, "", $sql);
  820. }
  821. }
  822. }
  823. return $sql;
  824. }
  825. /**
  826. * Process replace into sql.
  827. *
  828. * @param mixed $sql
  829. * @param string $driver
  830. * @access public
  831. * @return void
  832. */
  833. public function processReplace($sql, $driver = 'pgsql')
  834. {
  835. // 解析REPLACE INTO语句,提取出表名、字段和值
  836. $matches = [];
  837. preg_match('/^REPLACE\s+INTO\s+`?([\w_]+)`?\s*\((.*)\)\s+VALUES\s*\(([^()]+)\)\s*$/i', $sql, $matches);
  838. if(empty($matches)) return $sql;
  839. $table_name = $matches[1];
  840. $columns = array_map('trim', explode(',', $matches[2]));
  841. $values = array_map('trim', explode(', ', $matches[3]));
  842. if($table_name == '' or $columns == '' or $values == '') return $sql;
  843. // 构造SELECT语句,查询数据是否存在
  844. $where = [];
  845. foreach($columns as $index => $column)
  846. {
  847. $value = trim($values[$index], "'");
  848. $column = trim($column, '`');
  849. $values[$index] = $value;
  850. $columns[$index] = $column;
  851. if($value != 'NULL') $where[] = "`$column` = '$value'";
  852. }
  853. $select_sql = "SELECT * FROM `$table_name` WHERE " . implode(' AND ', $where);
  854. $result = $this->query($select_sql);
  855. $result = $result->fetchAll();
  856. $sql = in_array('id', $columns) && $driver == 'dm' ? "SET IDENTITY_INSERT `$table_name` ON;" : '';
  857. if($result)
  858. {
  859. // 数据已存在,构造UPDATE语句并执行
  860. $set = [];
  861. $where = [];
  862. foreach ($columns as $index => $column) {
  863. $value = $values[$index];
  864. $set[] = $value == 'NULL' ? "`$column` = NULL" : "`$column` = '$value'";
  865. $where[] = $value == 'NULL' ? "`$column` IS NULL" : "`$column` = '$value'";
  866. }
  867. $sql .= "UPDATE `$table_name` SET " . implode(', ', $set) . " WHERE " . implode(' AND ', $where);
  868. }
  869. else
  870. {
  871. // 数据不存在,构造INSERT INTO语句并执行
  872. $selectColumn = array();
  873. $selectValue = array();
  874. foreach ($columns as $index => $column) {
  875. if($values[$index] == 'NULL') continue;
  876. $selectColumn[] .= "`$column`";
  877. $selectValue[] .= "'{$values[$index]}'";
  878. }
  879. $sql .= "INSERT INTO `$table_name` (" . implode(', ', $selectColumn) . ") VALUES (" . implode(', ', $selectValue) . ")";
  880. }
  881. return $sql;
  882. }
  883. /**
  884. * Convert alter table sql.
  885. *
  886. * @param mixed $sql
  887. * @access public
  888. * @return void
  889. */
  890. public function convertAlterTableSql($sql)
  891. {
  892. $pattern = '/ALTER TABLE "(.*?)" CHANGE "(.*?)" "(.*?)" (.*?)(?:;|$)/';
  893. preg_match($pattern, $sql, $matches);
  894. if(count($matches) != 5) return $sql;
  895. $tableName = $matches[1];
  896. $oldColumnName = $matches[2];
  897. $newColumnName = $matches[3];
  898. $params = str_replace("'", "''", $matches[4]);
  899. $sql = 'begin ';
  900. if($oldColumnName != $newColumnName) $sql .= "execute immediate 'ALTER TABLE $tableName ALTER " . '"' . $oldColumnName . '" RENAME TO "' . $newColumnName . '"' . "';";
  901. $sql .= "execute immediate 'ALTER TABLE $tableName MODIFY " . '"' . $newColumnName . '" ' . $params . "';";
  902. $sql .= 'end;';
  903. return $sql;
  904. }
  905. /**
  906. * Quote.
  907. *
  908. * @param string $string
  909. * @param int $parameter_type
  910. * @access public
  911. * @return string
  912. */
  913. public function quote($string, $parameter_type = PDO::PARAM_STR)
  914. {
  915. return $this->pdo->quote($string, $parameter_type);
  916. }
  917. /**
  918. * Get last insert id.
  919. *
  920. * @param string $name
  921. * @access public
  922. * @return int|false
  923. */
  924. public function lastInsertId($name = null)
  925. {
  926. $lastInsertID = $this->pdo->lastInsertId($name);
  927. return $lastInsertID !== false ? (int)$lastInsertID : false;
  928. }
  929. /**
  930. * Begin transaction.
  931. *
  932. * @access public
  933. * @return bool
  934. */
  935. public function beginTransaction()
  936. {
  937. return $this->pdo->inTransaction() ? false : $this->pdo->beginTransaction();
  938. }
  939. /**
  940. * Check in transaction or not.
  941. *
  942. * @access public
  943. * @return bool
  944. */
  945. public function inTransaction()
  946. {
  947. return $this->pdo->inTransaction();
  948. }
  949. /**
  950. * Roll back if transaction failed.
  951. *
  952. * @access public
  953. * @return bool
  954. */
  955. public function rollBack()
  956. {
  957. return $this->pdo->inTransaction() ? $this->pdo->rollBack() : false;
  958. }
  959. /**
  960. * Commit transaction.
  961. *
  962. * @access public
  963. * @return bool
  964. */
  965. public function commit()
  966. {
  967. return $this->pdo->inTransaction() ? $this->pdo->commit() : false;
  968. }
  969. /**
  970. * 将SQL语句保存到队列中。
  971. * Save sql to SQLite queue.
  972. *
  973. * @param string $sql
  974. * @access public
  975. * @return int|null
  976. */
  977. public function pushSqliteQueue($sql)
  978. {
  979. $allowedActions = array('insert', 'update', 'delete', 'replace');
  980. $sql = str_replace(array('\r', '\n'), ' ', trim($sql));
  981. $actionPos = strpos($sql, ' ');
  982. $action = strtolower(substr($sql, 0, $actionPos));
  983. if(!in_array($action, $allowedActions)) return null;
  984. foreach($this->dbConfig->sqliteBlacklist as $table)
  985. {
  986. $tableName = $this->dbConfig->prefix . $table;
  987. if(stripos($sql, $tableName) !== false) return null;
  988. }
  989. $table = TABLE_SQLITE_QUEUE;
  990. $sql = $this->quote($sql);
  991. $now = "now()";
  992. $action = $this->getLastActionID() + 1;
  993. $queue = "INSERT INTO $table SET `sql` = $sql, `addDate` = $now, `status` = 'wait', `action` = $action";
  994. $this->pdo->exec($queue);
  995. return $this->pdo->lastInsertId();
  996. }
  997. /**
  998. * 获取最后一条动态的id。
  999. * Get last action id.
  1000. *
  1001. * @access public
  1002. * @return int|false
  1003. */
  1004. public function getLastActionID()
  1005. {
  1006. $table = TABLE_ACTION;
  1007. $sql = "SELECT id FROM $table ORDER BY id desc limit 1";
  1008. $lastAction = $this->pdo->query($sql)->fetch();
  1009. return $lastAction ? (int)$lastAction->id : false;
  1010. }
  1011. /**
  1012. * 安装时检查数据库用户权限。
  1013. * Check user privilege.
  1014. *
  1015. * @access public
  1016. * @return string
  1017. */
  1018. public function checkUserPriv()
  1019. {
  1020. global $config;
  1021. if(!in_array($this->dbConfig->driver, $config->mysqlDriverList)) return '';
  1022. $dbName = $this->dbConfig->name;
  1023. $user = $this->dbConfig->user;
  1024. $host = ($this->dbConfig->host == 'localhost' || $this->dbConfig->host == '127.0.0.1') ? 'localhost' : '%';
  1025. $privPairs = array();
  1026. try
  1027. {
  1028. $privList = $this->pdo->query("SHOW GRANTS FOR {$user}@'{$host}';")->fetchAll(PDO::FETCH_COLUMN);
  1029. }
  1030. catch(Exception $e)
  1031. {
  1032. return '';
  1033. }
  1034. foreach($privList as $privSQL)
  1035. {
  1036. if(strpos($privSQL, '*.*') === false && strpos($privSQL, "`$dbName`.*") === false) continue; // 如果权限不是全局或者当前数据库的,跳过
  1037. if(!preg_match('/GRANT (.*) ON (.+) TO/', $privSQL, $matches)) continue;
  1038. $privs = explode(',', $matches[1]);
  1039. foreach($privs as $priv)
  1040. {
  1041. $priv = trim($priv);
  1042. $privPairs[$priv] = true;
  1043. }
  1044. }
  1045. if(isset($privPairs['ALL PRIVILEGES'])) return '';
  1046. // 禅道所需的权限
  1047. $requiredPrivs = array('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE', 'ALTER', 'INDEX', 'CREATE VIEW');
  1048. $missingPrivs = array_diff($requiredPrivs, array_keys($privPairs));
  1049. if(empty($missingPrivs)) return ''; // 所有权限都满足
  1050. $missingPrivsSQL = implode(', ', $missingPrivs);
  1051. return "GRANT {$missingPrivsSQL} ON `{$dbName}`.* TO {$user}@'{$host}';";
  1052. }
  1053. /**
  1054. * 获取数据库版本。
  1055. * Get version.
  1056. *
  1057. * @access public
  1058. * @return string
  1059. */
  1060. public function getVersion()
  1061. {
  1062. if(in_array($this->dbConfig->driver, $this->config->mysqlDriverList))
  1063. {
  1064. $sql = "SELECT VERSION() AS version";
  1065. return $this->rawQuery($sql)->fetch()->version;
  1066. }
  1067. return '';
  1068. }
  1069. }