/packages/Db/Db/Managers/MySqlQuery.class.php

https://bitbucket.org/alexamiryan/stingle · PHP · 857 lines · 562 code · 100 blank · 195 comment · 152 complexity · 4d4e9a6d5286703d78a1a8e8e3b2aeb5 MD5 · raw file

  1. <?php
  2. class MySqlQuery extends Model {
  3. protected $link = null;
  4. protected $instanceName = MySqlDbManager::DEFAULT_INSTANCE_NAME;
  5. protected $result = null;
  6. ////////counter vars/////////////
  7. protected $lastFetchType = null;
  8. protected $lastRecordPosition = 0;
  9. protected $lastFieldPosition = 0;
  10. protected $isTransactionStarted = false;
  11. ////////////////////////////////
  12. protected $logger = null;
  13. protected $log = false;
  14. protected $nonExitentTables = array();
  15. protected $isInstanceLocked = false;
  16. const FETCH_TYPE_RECORD = 'record';
  17. const FETCH_TYPE_FIELD = 'field';
  18. const RECORD_TYPE_ARRAY = 0;
  19. const RECORD_TYPE_ASSOC = 1;
  20. const RECORD_TYPE_OBJECT = 2;
  21. const LOGGER_NAME = 'MysqlQuery';
  22. /**
  23. * Class constructor
  24. *
  25. * @param Db_MySqlDatabase db
  26. *
  27. */
  28. public function __construct($instanceName = null, Logger $logger = null) {
  29. if($instanceName !== null){
  30. $this->instanceName = $instanceName;
  31. }
  32. if ($logger === null) {
  33. $this->setLogger(new SessionLogger());
  34. }
  35. else {
  36. $this->setLogger($logger);
  37. }
  38. $this->link = MySqlDbManager::getDbObject($instanceName)->getLink();
  39. }
  40. /**
  41. * Switch to RW endpoint and lock on it. This will disable switching
  42. * on RO endpoints if SELECT is executed
  43. */
  44. public function lockEndpoint(){
  45. $this->switchToRWEndpoint();
  46. $this->isInstanceLocked = true;
  47. }
  48. /**
  49. * Release endpoint lock enable switching to RO endpoints
  50. */
  51. public function unlockEndpoint(){
  52. $this->isInstanceLocked = false;
  53. }
  54. protected function chooseDbEndpoint($query){
  55. $type = (self::isSelect($query) ? MySqlDbManager::ENDPOINT_TYPE_RO : MySqlDbManager::ENDPOINT_TYPE_RW);
  56. $this->link = MySqlDbManager::getDbObject($this->instanceName, $type)->getLink();
  57. }
  58. protected function switchToRWEndpoint(){
  59. $this->link = MySqlDbManager::getDbObject($this->instanceName, MySqlDbManager::ENDPOINT_TYPE_RW)->getLink();
  60. }
  61. public static function isSelect($query) {
  62. // Default trim()'s mask plus left parentheses
  63. $ltrimMask = "( \t\n\r\0\x0B";
  64. return 'SELECT' === strtoupper(
  65. substr(
  66. ltrim($query, $ltrimMask), 0, 6
  67. )
  68. );
  69. }
  70. public function getInstanceName(){
  71. return $this->instanceName;
  72. }
  73. public function setInstanceName($instanceName){
  74. $this->instanceName = $instanceName;
  75. }
  76. public function setLogger(Logger $logger) {
  77. $this->logger = $logger;
  78. }
  79. public function setLogging($bool) {
  80. if (!is_bool($bool)) {
  81. return false;
  82. }
  83. $this->log = $bool;
  84. }
  85. public function getLogging() {
  86. return $this->log;
  87. }
  88. /**
  89. * Get current error messgae from database
  90. *
  91. * @return bool
  92. */
  93. public function errorMessage() {
  94. return $this->link->error;
  95. }
  96. /**
  97. * Get current error code from database
  98. *
  99. * @return unknown
  100. */
  101. public function errorCode() {
  102. return $this->link->errno;
  103. }
  104. /**
  105. * Execute SQL query
  106. *
  107. * @param string $sqlStatement
  108. * @return MysqlQuery
  109. */
  110. public function exec($sqlStatement) {
  111. if(!$this->isInstanceLocked && !$this->isTransactionStarted){
  112. $this->chooseDbEndpoint($sqlStatement);
  113. }
  114. if (empty($sqlStatement)) {
  115. throw new EmptyArgumentException();
  116. }
  117. if ($this->log) {
  118. $this->logger->log(static::LOGGER_NAME, $sqlStatement);
  119. }
  120. if (($this->result = $this->link->query($sqlStatement)) !== false) {
  121. $this->lastFetchType = null;
  122. $this->lastFieldPosition = 0;
  123. $this->lastRecordPosition = 0;
  124. return $this;
  125. }
  126. else {
  127. $errorCode = $this->errorCode();
  128. $errorMessage = $this->errorMessage();
  129. if ($errorCode == 1146) {
  130. preg_match("/Table \'.*?\.(.+?)\' doesn\'t exist/", $errorMessage, $matches);
  131. if (isset($matches[1])) {
  132. $nonExistantTableName = $matches[1];
  133. if (!in_array($nonExistantTableName, $this->nonExitentTables)) {
  134. $sqlFiles = Tbl::getPluginSQLFilePathsByTableName($nonExistantTableName);
  135. if ($sqlFiles !== false) {
  136. $this->startTransaction();
  137. foreach ($sqlFiles as $sqlFilePath) {
  138. self::executeSQLFile($sqlFilePath, ';');
  139. }
  140. if ($this->commit()) {
  141. array_push($this->nonExitentTables, $nonExistantTableName);
  142. return $this->exec($sqlStatement);
  143. }
  144. else {
  145. $this->rollBack();
  146. }
  147. }
  148. }
  149. }
  150. }
  151. throw new MySqlException("MySQL Error: $errorCode: $errorMessage in query `$sqlStatement`", $errorCode);
  152. }
  153. }
  154. /**
  155. * Rows affected by query
  156. *
  157. * @return bool
  158. */
  159. public function affected() {
  160. return $this->link->affected_rows;
  161. }
  162. /**
  163. * Get last insert id
  164. *
  165. * @return int $insert_id
  166. */
  167. public function getLastInsertId() {
  168. return $this->link->insert_id;
  169. }
  170. /**
  171. * Analog of mysql_num_rows()
  172. *
  173. * @return int $number
  174. */
  175. public function countRecords() {
  176. if ($this->result) {
  177. return $this->result->num_rows;
  178. }
  179. else {
  180. return false;
  181. }
  182. }
  183. /**
  184. * Analog of mysql_num_fields()
  185. *
  186. * @return int $number
  187. */
  188. public function countFields() {
  189. if ($this->result) {
  190. return $this->result->field_count;
  191. }
  192. else {
  193. return false;
  194. }
  195. }
  196. /**
  197. * Fetch one row and move cursor to nex row
  198. *
  199. * @param int $type (0-Normal Array, 1-Associative Array, 2-Object)
  200. * @return array
  201. */
  202. public function fetchRecord($type = self::RECORD_TYPE_ASSOC) {
  203. if ($this->countRecords() == 0) {
  204. return false;
  205. }
  206. if (!$this->result) {
  207. return array();
  208. }
  209. if ($this->lastFetchType != self::FETCH_TYPE_RECORD) {
  210. $this->result->data_seek($this->lastRecordPosition);
  211. $this->lastFetchType = self::FETCH_TYPE_RECORD;
  212. }
  213. else {
  214. ++$this->lastRecordPosition;
  215. }
  216. switch ($type) {
  217. case self::RECORD_TYPE_ARRAY:
  218. return $this->result->fetch_row();
  219. case self::RECORD_TYPE_ASSOC:
  220. return $this->result->fetch_assoc();
  221. case self::RECORD_TYPE_OBJECT:
  222. return $this->result->fetch_object();
  223. default:
  224. return array();
  225. }
  226. }
  227. /**
  228. * Fetch one field from row
  229. *
  230. * @param string $fieldName
  231. * @param int $isNumeric (false - $field_identifier is name of the field, true - $field_identifier is number of the field)
  232. * @return string
  233. */
  234. public function fetchField($fieldName, $isNumeric = false) {
  235. if ($this->countRecords() == 0) {
  236. return false;
  237. }
  238. if (!$this->result) {
  239. return false;
  240. }
  241. if ($this->lastFetchType != self::FETCH_TYPE_FIELD) {
  242. $this->result->data_seek($this->lastFieldPosition);
  243. $this->lastFetchType = self::FETCH_TYPE_FIELD;
  244. }
  245. else {
  246. ++$this->lastFieldPosition;
  247. }
  248. if ($isNumeric) {
  249. $record = $this->result->fetch_row();
  250. }
  251. else {
  252. $record = $this->result->fetch_assoc();
  253. }
  254. if ($record) {
  255. return $record[$fieldName];
  256. }
  257. else {
  258. return false;
  259. }
  260. }
  261. /**
  262. * Get array of the query
  263. *
  264. * @param int $offset
  265. * @param int $len
  266. * @param int $fieldsType (0-Normal,1-Assoc, 2-Object)
  267. * @param int $rowsType (0-Normal,1-Assoc, 2-Object)
  268. * @param string $rowsTypeField (name of the field to become index for Assoc $rowsType)
  269. * @return array
  270. */
  271. public function fetchRecords($offset = 0, $len = 0, $fieldsType = self::RECORD_TYPE_ASSOC, $rowsType = self::RECORD_TYPE_ARRAY, $rowsTypeField = null) {
  272. $returnArray = array();
  273. $counter = 0;
  274. $numRecords = $this->countRecords();
  275. if (abs($offset) > $numRecords || $numRecords == 0 || !$this->result) {
  276. return array();
  277. }
  278. if ($this->lastFetchType == self::FETCH_TYPE_FIELD) {
  279. $resultLastPosition = $this->lastFieldPosition;
  280. }
  281. elseif ($this->lastFetchType == self::FETCH_TYPE_RECORD) {
  282. $resultLastPosition = $this->lastRecordPosition;
  283. }
  284. else {
  285. $resultLastPosition = 0;
  286. }
  287. $flagToReverceArray = false;
  288. if ($len < 0) {
  289. $flagToReverceArray = true;
  290. }
  291. if ($len > 0) {
  292. if ($offset < 0) {
  293. $offset = $numRecords - abs($offset);
  294. }
  295. }
  296. elseif ($len < 0) {
  297. if ($offset > 0) {
  298. if (abs($len) > abs($offset)) {
  299. $len = abs($offset);
  300. $offset = 0;
  301. }
  302. else {
  303. $offset = $offset - abs($len) + 1;
  304. $len = abs($len);
  305. }
  306. }
  307. elseif ($offset < 0) {
  308. if (abs($len) > abs($offset)) {
  309. $len = $numRecords - abs($offset) + 1;
  310. $offset = 0;
  311. }
  312. else {
  313. $offset = $numRecords - abs($offset) - abs($len) + 1;
  314. $len = abs($len);
  315. }
  316. }
  317. }
  318. elseif ($len == 0) {
  319. $len = $numRecords;
  320. }
  321. $this->result->data_seek($offset);
  322. if ($rowsType == self::RECORD_TYPE_ARRAY) {
  323. if ($fieldsType == self::RECORD_TYPE_ARRAY) {
  324. while ($currentResult = $this->result->fetch_row()) {
  325. $returnArray[$counter] = $currentResult;
  326. $counter++;
  327. if ($counter == abs($len)) {
  328. break;
  329. }
  330. }
  331. }
  332. elseif ($fieldsType == self::RECORD_TYPE_ASSOC) {
  333. while ($currentResult = $this->result->fetch_assoc()) {
  334. $returnArray[$counter] = $currentResult;
  335. $counter++;
  336. if ($counter == abs($len)) {
  337. break;
  338. }
  339. }
  340. }
  341. elseif ($fieldsType == self::RECORD_TYPE_OBJECT) {
  342. while ($currentResult = $this->result->fetch_object()) {
  343. $returnArray[$counter] = $currentResult;
  344. $counter++;
  345. if ($counter == abs($len)) {
  346. break;
  347. }
  348. }
  349. }
  350. else {
  351. return array();
  352. }
  353. }
  354. elseif ($rowsType == self::RECORD_TYPE_ASSOC and ! empty($rowsTypeField)) {
  355. $numFieldType = 0;
  356. if ($fieldsType == self::RECORD_TYPE_ARRAY) {
  357. for ($i = 0; $i < $this->countFields(); $i++) {
  358. if ($this->fieldName($i) == $rowsTypeField) {
  359. $numFieldType = $i;
  360. break;
  361. }
  362. }
  363. while ($currentResult = $this->result->fetch_row()) {
  364. $returnArray[$currentResult[$numFieldType]] = $currentResult;
  365. $counter++;
  366. if ($counter == abs($len)) {
  367. break;
  368. }
  369. }
  370. }
  371. elseif ($fieldsType == self::RECORD_TYPE_ASSOC) {
  372. while ($currentResult = $this->result->fetch_assoc()) {
  373. $returnArray[$currentResult[$rowsTypeField]] = $currentResult;
  374. $counter++;
  375. if ($counter == abs($len)) {
  376. break;
  377. }
  378. }
  379. }
  380. elseif ($fieldsType == self::RECORD_TYPE_OBJECT) {
  381. while ($currentResult = $this->result->fetch_object()) {
  382. $returnArray[$currentResult->$rowsTypeField] = $currentResult;
  383. $counter++;
  384. if ($counter == abs($len)) {
  385. break;
  386. }
  387. }
  388. }
  389. }
  390. else {
  391. return array();
  392. }
  393. if ($resultLastPosition < $numRecords) {
  394. $this->result->data_seek($resultLastPosition);
  395. }
  396. if ($flagToReverceArray) {
  397. $returnArray = array_reverse($returnArray);
  398. }
  399. return $returnArray;
  400. }
  401. /**
  402. * Get array of only one field
  403. *
  404. * @param string $fieldName
  405. * @param bool $isNumeric (true-$field_identifier is numeric, false-$field_identifier is a name)
  406. * @param int $offset
  407. * @param int $len
  408. * @return array
  409. */
  410. public function fetchFields($fieldName, $isNumeric = false, $offset = 0, $len = 0) {
  411. $returnArray = array();
  412. $counter = 0;
  413. $numRecords = $this->countRecords();
  414. if (abs($offset) > $numRecords || $numRecords == 0 || !$this->result) {
  415. return array();
  416. }
  417. if ($this->lastFetchType == self::FETCH_TYPE_FIELD) {
  418. $resultLastPosition = $this->lastFieldPosition;
  419. }
  420. elseif ($this->lastFetchType == self::FETCH_TYPE_RECORD) {
  421. $resultLastPosition = $this->lastRecordPosition;
  422. }
  423. else {
  424. $resultLastPosition = 0;
  425. }
  426. $flagToReverceArray = false;
  427. if ($len < 0) {
  428. $flagToReverceArray = true;
  429. }
  430. if ($len > 0) {
  431. if ($offset < 0) {
  432. $offset = $numRecords - abs($offset);
  433. }
  434. }
  435. elseif ($len < 0) {
  436. if ($offset > 0) {
  437. if (abs($len) > abs($offset)) {
  438. $len = abs($offset);
  439. $offset = 0;
  440. }
  441. else {
  442. $offset = $offset - abs($len) + 1;
  443. $len = abs($len);
  444. }
  445. }
  446. elseif ($offset < 0) {
  447. if (abs($len) > abs($offset)) {
  448. $len = $numRecords - abs($offset) + 1;
  449. $offset = 0;
  450. }
  451. else {
  452. $offset = $numRecords - abs($offset) - abs($len) + 1;
  453. $len = abs($len);
  454. }
  455. }
  456. }
  457. elseif ($len == 0) {
  458. $len = $numRecords;
  459. }
  460. $this->result->data_seek($offset);
  461. if ($isNumeric == false) {
  462. while ($currentResult = $this->result->fetch_assoc()) {
  463. $returnArray[$counter] = $currentResult[$fieldName];
  464. $counter++;
  465. if ($counter == abs($len)) {
  466. break;
  467. }
  468. }
  469. }
  470. elseif ($isNumeric == true) {
  471. while ($currentResult = $this->result->fetch_row()) {
  472. $returnArray[$counter] = $currentResult[$fieldName];
  473. $counter++;
  474. if ($counter == abs($len)) {
  475. break;
  476. }
  477. }
  478. }
  479. else {
  480. return array();
  481. }
  482. if ($resultLastPosition < $numRecords) {
  483. $this->result->data_seek($resultLastPosition);
  484. }
  485. if ($flagToReverceArray) {
  486. $returnArray = array_reverse($returnArray);
  487. }
  488. return $returnArray;
  489. }
  490. /**
  491. * Get name of specified field
  492. *
  493. * @param int $offset
  494. * @return string
  495. */
  496. public function fieldName($offset) {
  497. return $this->fieldInfo($offset)->name;
  498. }
  499. /**
  500. * Get type of specified field
  501. *
  502. * @param int $offset
  503. * @return string
  504. */
  505. public function fieldType($offset) {
  506. return $this->fieldInfo($offset)->type;
  507. }
  508. /**
  509. * Get length of specified field
  510. *
  511. * @param int $offset
  512. * @return int
  513. */
  514. public function fieldLen($offset) {
  515. return $this->fieldInfo($offset)->length;
  516. }
  517. /**
  518. * Get flags of specified field
  519. *
  520. * @param int $offset
  521. * @return array
  522. */
  523. public function fieldFlags($offset) {
  524. return $this->fieldInfo($offset)->flags;
  525. }
  526. /**
  527. *
  528. * @param int $offset
  529. * @return mysqli_fetch_field_direct
  530. */
  531. public function fieldInfo($offset){
  532. if ($this->result and $offset >= 0 and $offset < $this->countFields()) {
  533. return $this->result->fetch_field_direct($offset);
  534. }
  535. return false;
  536. }
  537. /**
  538. * Get found rows count from select query which
  539. * uses SQL_CALC_FOUND_ROWS parameter
  540. *
  541. * @return integer
  542. */
  543. public function getFoundRowsCount() {
  544. $this->exec("SELECT FOUND_ROWS() AS `cnt`");
  545. return $this->fetchField('cnt');
  546. }
  547. public function escapeString($string){
  548. return $this->link->real_escape_string($string);
  549. }
  550. /**
  551. * Starts a new transaction
  552. *
  553. * @access public
  554. * @throws DB_Exception
  555. * @return boolean
  556. */
  557. public function startTransaction($withSnapshot = false, $name = NULL) {
  558. if ($this->isTransactionStarted) {
  559. return false;
  560. }
  561. $this->switchToRWEndpoint();
  562. if($name !== null){
  563. if($this->link->begin_transaction(($withSnapshot ? MYSQLI_TRANS_START_WITH_CONSISTENT_SNAPSHOT : NULL), $name)){
  564. $this->isTransactionStarted = true;
  565. return true;
  566. }
  567. }
  568. else{
  569. if($this->link->begin_transaction(($withSnapshot ? MYSQLI_TRANS_START_WITH_CONSISTENT_SNAPSHOT : NULL))){
  570. $this->isTransactionStarted = true;
  571. return true;
  572. }
  573. }
  574. return false;
  575. }
  576. /**
  577. * Commits a last started transaction
  578. *
  579. * @access public
  580. * @return boolean
  581. */
  582. public function commit($name = NULL) {
  583. if (!$this->isTransactionStarted) {
  584. return false;
  585. }
  586. $result = $this->link->commit(NULL, $name);
  587. $this->isTransactionStarted = false;
  588. return $result;
  589. }
  590. /**
  591. * Saves a last started transaction
  592. *
  593. * @param string $identifier savePoint identifier
  594. *
  595. * @access public
  596. * @return boolean
  597. */
  598. public function savePoint($identifier) {
  599. if (!$this->isTransactionStarted || empty($identifier)) {
  600. return false;
  601. }
  602. return $this->link->savepoint($identifier);
  603. }
  604. /**
  605. * Rolls back to last savePoint
  606. *
  607. * @param string $savepointIdentifier
  608. *
  609. * @access public
  610. * @return boolean
  611. */
  612. public function rollBack($savepointIdentifier = NULL) {
  613. if (!$this->isTransactionStarted) {
  614. return false;
  615. }
  616. $result = $this->link->rollback(NULL, $savepointIdentifier);
  617. $this->isTransactionStarted = false;
  618. return $result;
  619. }
  620. /**
  621. * Locks tables from given array
  622. *
  623. * @param array $tables
  624. *
  625. * @example $tables = Array("table_name_1" => "r", "table_name_2" => "w", "table_name_3" => "");
  626. *
  627. * or
  628. *
  629. * @param string $table
  630. * @param string $type
  631. *
  632. * @example $tables = "table", $type = "r" (READ)
  633. * @example $tables = "table", $type = "w" (WRITE)
  634. *
  635. * @access public
  636. * @return boolean
  637. */
  638. public function lockTables($tables, $type = "r") {
  639. $this->switchToRWEndpoint();
  640. if (empty($tables)) {
  641. return false;
  642. }
  643. $lockQuery = "LOCK TABLES ";
  644. if (is_array($tables)) {
  645. $lockQueriesArr = array();
  646. foreach ($tables as $table_name => $current_type) {
  647. $query .= $table_name . " ";
  648. if ($current_type == "w") {
  649. $query .= " WRITE";
  650. }
  651. else {
  652. $query .= " READ";
  653. }
  654. array_push($lockQueriesArr, $query);
  655. }
  656. $lockQuery .= implode(", ", $lockQueriesArr);
  657. }
  658. elseif (is_string($tables)) {
  659. $lockQuery .= $tables;
  660. if ($type == "w") {
  661. $lockQuery .= " WRITE";
  662. }
  663. else {
  664. $lockQuery .= " READ";
  665. }
  666. }
  667. return $this->link->query($lockQuery);
  668. }
  669. /**
  670. * Unlocks tables that were locked by current thread
  671. *
  672. * @access public
  673. * @return boolean
  674. */
  675. public function unlockTables() {
  676. $this->switchToRWEndpoint();
  677. return $this->link->query('UNLOCK TABLES');
  678. }
  679. /**
  680. * Drops table or tables
  681. *
  682. * @param array $tableName
  683. *
  684. * or
  685. *
  686. * @param string $tableName
  687. *
  688. * @access public
  689. * @return boolean
  690. */
  691. public function dropTables($tableName) {
  692. if (empty($tableName)) {
  693. return false;
  694. }
  695. $this->switchToRWEndpoint();
  696. $dropQuery = "DROP TABLE ";
  697. if (is_array($tableName)) {
  698. $dropQuery .= implode(",", $tableName);
  699. }
  700. elseif (is_string($tableName)) {
  701. $dropQuery .= $tableName;
  702. }
  703. return $this->link->query($dropQuery);
  704. }
  705. /**
  706. * Renames table
  707. *
  708. * @param string $oldName
  709. * @param string $newName
  710. *
  711. * @access public
  712. * @return boolean
  713. */
  714. public function renameTable($oldName, $newName) {
  715. $this->switchToRWEndpoint();
  716. if (!empty($oldName) && !empty($newName)) {
  717. return $this->link->query("RENAME TABLE $oldName TO $newName");
  718. }
  719. else {
  720. return false;
  721. }
  722. }
  723. public function executeSQLFile($file, $delimiter = ';') {
  724. $this->switchToRWEndpoint();
  725. $matches = array();
  726. $otherDelimiter = false;
  727. if (is_file($file) === true) {
  728. $file = fopen($file, 'r');
  729. if (is_resource($file) === true) {
  730. $query = array();
  731. while (feof($file) === false) {
  732. $query[] = fgets($file);
  733. if (preg_match('~' . preg_quote('delimiter', '~') . '\s*([^\s]+)$~iS', end($query), $matches) === 1) {
  734. //DELIMITER DIRECTIVE DETECTED
  735. array_pop($query); //WE DON'T NEED THIS LINE IN SQL QUERY
  736. if ($otherDelimiter = ( $matches[1] != $delimiter )) {
  737. }
  738. else {
  739. // THIS IS THE DEFAULT DELIMITER, DELETE THE LINE BEFORE THE LAST (THAT SHOULD BE THE NOT DEFAULT DELIMITER) AND WE SHOULD CLOSE THE STATEMENT
  740. array_pop($query);
  741. $query[] = $delimiter;
  742. }
  743. }
  744. if (!$otherDelimiter && preg_match('~' . preg_quote($delimiter, '~') . '\s*$~iS', end($query)) === 1) {
  745. $query = trim(implode('', $query));
  746. $this->exec($query);
  747. }
  748. if (is_string($query) === true) {
  749. $query = array();
  750. }
  751. }
  752. return fclose($file);
  753. }
  754. }
  755. return false;
  756. }
  757. }