├── Console └── Command │ └── Simulate.php ├── Framework └── DB │ └── Adapter │ └── Pdo │ └── Mysql.php ├── LICENSE.txt ├── README.md ├── Setup └── UpgradeSchema.php ├── etc ├── di.xml └── module.xml └── registration.php /Console/Command/Simulate.php: -------------------------------------------------------------------------------- 1 | _resource = $resource; 62 | $this->_appState = $appState; 63 | $this->_dir = $dir; 64 | $this->_deploymentConfig = $deploymentConfig; 65 | parent::__construct(); 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | protected function configure() 72 | { 73 | $this->setName('cadence:deadlockRetry:simulate') 74 | ->setDescription('Simulate a deadlock') 75 | ->setDefinition([]); 76 | } 77 | 78 | /** 79 | * Regenerate Url Rewrites 80 | * @param InputInterface $input 81 | * @param OutputInterface $output 82 | * @return void 83 | */ 84 | protected function execute(InputInterface $input, OutputInterface $output) 85 | { 86 | set_time_limit(0); 87 | $this->_appState->setAreaCode('adminhtml'); 88 | 89 | $output->writeln("Starting a transaction to force a deadlock..."); 90 | 91 | try { 92 | $this->_resource->getConnection()->beginTransaction(); 93 | 94 | $this->_resource->getConnection() 95 | ->fetchAll("select * from innodb_deadlock_maker where a = 1 FOR UPDATE;"); 96 | 97 | $cnxDetails = $this->_deploymentConfig->get(ConfigOptionsListConstants::CONFIG_PATH_DB_CONNECTIONS . '/default'); 98 | $dbh = new PDO('mysql:host=' . $cnxDetails['host'] . ';dbname=' . $cnxDetails['dbname'], $cnxDetails['username'], $cnxDetails['password']); 99 | 100 | $dbh->query("start transaction"); 101 | $dbh->query("SELECT * FROM innodb_deadlock_maker where a = 0 FOR UPDATE"); 102 | 103 | $this->_resource->getConnection() 104 | ->query("update innodb_deadlock_maker set a = 2 where a <> 1"); 105 | $dbh->query("UPDATE innodb_deadlock_maker set a = 3 where a <> 0"); 106 | 107 | $output->writeln("We're done! We intentionally don't commit this transaction."); 108 | } catch (\Throwable $e) { 109 | $output->writeln("Encountered exception:"); 110 | $output->writeln($e->getMessage()); 111 | } 112 | 113 | $output->writeln('Finished'); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Framework/DB/Adapter/Pdo/Mysql.php: -------------------------------------------------------------------------------- 1 | 'bool', 150 | Table::TYPE_SMALLINT => 'smallint', 151 | Table::TYPE_INTEGER => 'int', 152 | Table::TYPE_BIGINT => 'bigint', 153 | Table::TYPE_FLOAT => 'float', 154 | Table::TYPE_DECIMAL => 'decimal', 155 | Table::TYPE_NUMERIC => 'decimal', 156 | Table::TYPE_DATE => 'date', 157 | Table::TYPE_TIMESTAMP => 'timestamp', 158 | Table::TYPE_DATETIME => 'datetime', 159 | Table::TYPE_TEXT => 'text', 160 | Table::TYPE_BLOB => 'blob', 161 | Table::TYPE_VARBINARY => 'blob', 162 | ]; 163 | 164 | /** 165 | * All possible DDL statements 166 | * First 3 symbols for each statement 167 | * 168 | * @var string[] 169 | */ 170 | protected $_ddlRoutines = ['alt', 'cre', 'ren', 'dro', 'tru']; 171 | 172 | /** 173 | * Allowed interval units array 174 | * 175 | * @var array 176 | */ 177 | protected $_intervalUnits = [ 178 | self::INTERVAL_YEAR => 'YEAR', 179 | self::INTERVAL_MONTH => 'MONTH', 180 | self::INTERVAL_DAY => 'DAY', 181 | self::INTERVAL_HOUR => 'HOUR', 182 | self::INTERVAL_MINUTE => 'MINUTE', 183 | self::INTERVAL_SECOND => 'SECOND', 184 | ]; 185 | 186 | /** 187 | * Hook callback to modify queries. Mysql specific property, designed only for backwards compatibility. 188 | * 189 | * @var array|null 190 | */ 191 | protected $_queryHook = null; 192 | 193 | /** 194 | * @var String 195 | */ 196 | protected $string; 197 | 198 | /** 199 | * @var DateTime 200 | */ 201 | protected $dateTime; 202 | 203 | /** 204 | * @var SelectFactory 205 | * @since 100.1.0 206 | */ 207 | protected $selectFactory; 208 | 209 | /** 210 | * @var LoggerInterface 211 | */ 212 | protected $logger; 213 | 214 | /** 215 | * Map that links database error code to corresponding Magento exception 216 | * 217 | * @var \Zend_Db_Adapter_Exception[] 218 | */ 219 | private $exceptionMap; 220 | 221 | /** 222 | * @var QueryGenerator 223 | */ 224 | private $queryGenerator; 225 | 226 | /** 227 | * @var SerializerInterface 228 | */ 229 | private $serializer; 230 | 231 | /** 232 | * @var SchemaListener 233 | */ 234 | private $schemaListener; 235 | 236 | /** 237 | * Constructor 238 | * 239 | * @param StringUtils $string 240 | * @param DateTime $dateTime 241 | * @param LoggerInterface $logger 242 | * @param SelectFactory $selectFactory 243 | * @param array $config 244 | * @param SerializerInterface|null $serializer 245 | */ 246 | public function __construct( 247 | StringUtils $string, 248 | DateTime $dateTime, 249 | LoggerInterface $logger, 250 | SelectFactory $selectFactory, 251 | array $config = [], 252 | SerializerInterface $serializer = null 253 | ) { 254 | $this->string = $string; 255 | $this->dateTime = $dateTime; 256 | $this->logger = $logger; 257 | $this->selectFactory = $selectFactory; 258 | $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); 259 | $this->exceptionMap = [ 260 | // SQLSTATE[HY000]: General error: 2006 MySQL server has gone away 261 | 2006 => ConnectionException::class, 262 | // SQLSTATE[HY000]: General error: 2013 Lost connection to MySQL server during query 263 | 2013 => ConnectionException::class, 264 | // SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded 265 | 1205 => LockWaitException::class, 266 | // SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock 267 | 1213 => DeadlockException::class, 268 | // SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 269 | 1062 => DuplicateException::class, 270 | ]; 271 | try { 272 | parent::__construct($config); 273 | } catch (\Zend_Db_Adapter_Exception $e) { 274 | throw new \InvalidArgumentException($e->getMessage(), $e->getCode(), $e); 275 | } 276 | } 277 | 278 | /** 279 | * Begin new DB transaction for connection 280 | * 281 | * @return $this 282 | * @throws \Exception 283 | */ 284 | public function beginTransaction() 285 | { 286 | if ($this->_isRolledBack) { 287 | // phpcs:ignore Magento2.Exceptions.DirectThrow.FoundDirectThrow 288 | throw new \Exception(AdapterInterface::ERROR_ROLLBACK_INCOMPLETE_MESSAGE); 289 | } 290 | if ($this->_transactionLevel === 0) { 291 | $this->logger->startTimer(); 292 | parent::beginTransaction(); 293 | $this->logger->logStats(LoggerInterface::TYPE_TRANSACTION, 'BEGIN'); 294 | } 295 | ++$this->_transactionLevel; 296 | return $this; 297 | } 298 | 299 | /** 300 | * Commit DB transaction 301 | * 302 | * @return $this 303 | * @throws \Exception 304 | */ 305 | public function commit() 306 | { 307 | if ($this->_transactionLevel === 1 && !$this->_isRolledBack) { 308 | $this->logger->startTimer(); 309 | parent::commit(); 310 | $this->logger->logStats(LoggerInterface::TYPE_TRANSACTION, 'COMMIT'); 311 | } elseif ($this->_transactionLevel === 0) { 312 | // phpcs:ignore Magento2.Exceptions.DirectThrow.FoundDirectThrow 313 | throw new \Exception(AdapterInterface::ERROR_ASYMMETRIC_COMMIT_MESSAGE); 314 | } elseif ($this->_isRolledBack) { 315 | // phpcs:ignore Magento2.Exceptions.DirectThrow.FoundDirectThrow 316 | throw new \Exception(AdapterInterface::ERROR_ROLLBACK_INCOMPLETE_MESSAGE); 317 | } 318 | --$this->_transactionLevel; 319 | return $this; 320 | } 321 | 322 | /** 323 | * Rollback DB transaction 324 | * 325 | * @return $this 326 | * @throws \Exception 327 | */ 328 | public function rollBack() 329 | { 330 | if ($this->_transactionLevel === 1) { 331 | $this->logger->startTimer(); 332 | parent::rollBack(); 333 | $this->_isRolledBack = false; 334 | $this->logger->logStats(LoggerInterface::TYPE_TRANSACTION, 'ROLLBACK'); 335 | } elseif ($this->_transactionLevel === 0) { 336 | // phpcs:ignore Magento2.Exceptions.DirectThrow.FoundDirectThrow 337 | throw new \Exception(AdapterInterface::ERROR_ASYMMETRIC_ROLLBACK_MESSAGE); 338 | } else { 339 | $this->_isRolledBack = true; 340 | } 341 | --$this->_transactionLevel; 342 | return $this; 343 | } 344 | 345 | /** 346 | * Get adapter transaction level state. Return 0 if all transactions are complete 347 | * 348 | * @return int 349 | */ 350 | public function getTransactionLevel() 351 | { 352 | return $this->_transactionLevel; 353 | } 354 | 355 | /** 356 | * Convert date to DB format 357 | * 358 | * @param int|string|\DateTimeInterface $date 359 | * @return \Zend_Db_Expr 360 | */ 361 | public function convertDate($date) 362 | { 363 | return $this->formatDate($date, false); 364 | } 365 | 366 | /** 367 | * Convert date and time to DB format 368 | * 369 | * @param int|string|\DateTimeInterface $datetime 370 | * @return \Zend_Db_Expr 371 | */ 372 | public function convertDateTime($datetime) 373 | { 374 | return $this->formatDate($datetime, true); 375 | } 376 | 377 | /** 378 | * Creates a PDO object and connects to the database. 379 | * 380 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 381 | * @SuppressWarnings(PHPMD.NPathComplexity) 382 | * 383 | * @return void 384 | * @throws \Zend_Db_Adapter_Exception 385 | * @throws \Zend_Db_Statement_Exception 386 | */ 387 | protected function _connect() 388 | { 389 | if ($this->_connection) { 390 | return; 391 | } 392 | 393 | if (!extension_loaded('pdo_mysql')) { 394 | throw new \Zend_Db_Adapter_Exception('pdo_mysql extension is not installed'); 395 | } 396 | 397 | if (!isset($this->_config['host'])) { 398 | throw new \Zend_Db_Adapter_Exception('No host configured to connect'); 399 | } 400 | 401 | if (isset($this->_config['port'])) { 402 | throw new \Zend_Db_Adapter_Exception('Port must be configured within host parameter (like localhost:3306'); 403 | } 404 | 405 | unset($this->_config['port']); 406 | 407 | if (strpos($this->_config['host'], '/') !== false) { 408 | $this->_config['unix_socket'] = $this->_config['host']; 409 | unset($this->_config['host']); 410 | } elseif (strpos($this->_config['host'], ':') !== false) { 411 | list($this->_config['host'], $this->_config['port']) = explode(':', $this->_config['host']); 412 | } 413 | 414 | if (!isset($this->_config['driver_options'][\PDO::MYSQL_ATTR_MULTI_STATEMENTS])) { 415 | $this->_config['driver_options'][\PDO::MYSQL_ATTR_MULTI_STATEMENTS] = false; 416 | } 417 | 418 | $this->logger->startTimer(); 419 | parent::_connect(); 420 | $this->logger->logStats(LoggerInterface::TYPE_CONNECT, ''); 421 | 422 | /** @link http://bugs.mysql.com/bug.php?id=18551 */ 423 | $this->_connection->query("SET SQL_MODE=''"); 424 | 425 | // As we use default value CURRENT_TIMESTAMP for TIMESTAMP type columns we need to set GMT timezone 426 | $this->_connection->query("SET time_zone = '+00:00'"); 427 | 428 | if (isset($this->_config['initStatements'])) { 429 | $statements = $this->_splitMultiQuery($this->_config['initStatements']); 430 | foreach ($statements as $statement) { 431 | $this->_query($statement); 432 | } 433 | } 434 | 435 | if (!$this->_connectionFlagsSet) { 436 | $this->_connection->setAttribute(\PDO::ATTR_EMULATE_PREPARES, true); 437 | if (isset($this->_config['use_buffered_query']) && $this->_config['use_buffered_query'] === false) { 438 | $this->_connection->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); 439 | } else { 440 | $this->_connection->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true); 441 | } 442 | $this->_connectionFlagsSet = true; 443 | } 444 | } 445 | 446 | /** 447 | * Create new database connection 448 | * 449 | * @return \PDO 450 | */ 451 | private function createConnection() 452 | { 453 | $connection = new \PDO( 454 | $this->_dsn(), 455 | $this->_config['username'], 456 | $this->_config['password'], 457 | $this->_config['driver_options'] 458 | ); 459 | return $connection; 460 | } 461 | 462 | /** 463 | * Run RAW Query 464 | * 465 | * @param string $sql 466 | * @return \Zend_Db_Statement_Interface 467 | * @throws \PDOException 468 | */ 469 | public function rawQuery($sql) 470 | { 471 | try { 472 | $result = $this->query($sql); 473 | } catch (\Zend_Db_Statement_Exception $e) { 474 | // Convert to \PDOException to maintain backwards compatibility with usage of MySQL adapter 475 | $e = $e->getPrevious(); 476 | if (!($e instanceof \PDOException)) { 477 | $e = new \PDOException($e->getMessage(), $e->getCode()); 478 | } 479 | throw $e; 480 | } 481 | 482 | return $result; 483 | } 484 | 485 | /** 486 | * Run RAW query and Fetch First row 487 | * 488 | * @param string $sql 489 | * @param string|int $field 490 | * @return mixed|null 491 | */ 492 | public function rawFetchRow($sql, $field = null) 493 | { 494 | $result = $this->rawQuery($sql); 495 | if (!$result) { 496 | return false; 497 | } 498 | 499 | $row = $result->fetch(\PDO::FETCH_ASSOC); 500 | if (!$row) { 501 | return false; 502 | } 503 | 504 | if (empty($field)) { 505 | return $row; 506 | } else { 507 | return $row[$field] ?? false; 508 | } 509 | } 510 | 511 | /** 512 | * Check transaction level in case of DDL query 513 | * 514 | * @param string|\Magento\Framework\DB\Select $sql 515 | * @return void 516 | * @throws \Zend_Db_Adapter_Exception 517 | */ 518 | protected function _checkDdlTransaction($sql) 519 | { 520 | if ($this->getTransactionLevel() > 0) { 521 | $sql = ltrim(preg_replace('/\s+/', ' ', $sql)); 522 | $sqlMessage = explode(' ', $sql, 3); 523 | $startSql = strtolower(substr($sqlMessage[0], 0, 3)); 524 | if (in_array($startSql, $this->_ddlRoutines) && strcasecmp($sqlMessage[1], 'temporary') !== 0) { 525 | throw new ConnectionException(AdapterInterface::ERROR_DDL_MESSAGE, E_USER_ERROR); 526 | } 527 | } 528 | } 529 | 530 | /** 531 | * Special handling for PDO query(). 532 | * 533 | * All bind parameter names must begin with ':'. 534 | * 535 | * @param string|\Magento\Framework\DB\Select $sql The SQL statement with placeholders. 536 | * @param mixed $bind An array of data or data itself to bind to the placeholders. 537 | * @return \Zend_Db_Statement_Pdo|void 538 | * @throws \Zend_Db_Adapter_Exception To re-throw \PDOException. 539 | * @throws \Zend_Db_Statement_Exception 540 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 541 | */ 542 | protected function _query($sql, $bind = []) 543 | { 544 | $connectionErrors = [ 545 | 2006, // SQLSTATE[HY000]: General error: 2006 MySQL server has gone away 546 | 2013, // SQLSTATE[HY000]: General error: 2013 Lost connection to MySQL server during query 547 | ]; 548 | 549 | 550 | // *** Deadlock module edits: Start *** 551 | 552 | $deadlockErrors = [ 553 | 1205, // SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction 554 | 1213 // SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction 555 | ]; 556 | 557 | /** @var \Psr\Log\LoggerInterface $stringLogger */ 558 | $stringLogger = ObjectManager::getInstance()->get('Psr\Log\LoggerInterface'); 559 | 560 | // *** Deadlock module edits: End *** 561 | 562 | $triesCount = 0; 563 | do { 564 | $retry = false; 565 | $this->logger->startTimer(); 566 | try { 567 | $this->_checkDdlTransaction($sql); 568 | $this->_prepareQuery($sql, $bind); 569 | $result = parent::query($sql, $bind); 570 | $this->logger->logStats(LoggerInterface::TYPE_QUERY, $sql, $bind, $result); 571 | return $result; 572 | } catch (\Exception $e) { 573 | // Finalize broken query 574 | $profiler = $this->getProfiler(); 575 | if ($profiler instanceof Profiler) { 576 | /** @var Profiler $profiler */ 577 | $profiler->queryEndLast(); 578 | } 579 | 580 | /** @var $pdoException \PDOException */ 581 | $pdoException = null; 582 | if ($e instanceof \PDOException) { 583 | $pdoException = $e; 584 | } elseif (($e instanceof \Zend_Db_Statement_Exception) 585 | && ($e->getPrevious() instanceof \PDOException) 586 | ) { 587 | $pdoException = $e->getPrevious(); 588 | } 589 | 590 | // Check to reconnect 591 | if ($pdoException && $triesCount < self::MAX_CONNECTION_RETRIES 592 | && in_array($pdoException->errorInfo[1], $connectionErrors) 593 | ) { 594 | $retry = true; 595 | $triesCount++; 596 | $this->closeConnection(); 597 | 598 | /** 599 | * _connect() function does not allow port parameter, so put the port back with the host 600 | */ 601 | if (!empty($this->_config['port'])) { 602 | $this->_config['host'] = implode(':', [$this->_config['host'], $this->_config['port']]); 603 | unset($this->_config['port']); 604 | } 605 | 606 | $this->_connect(); 607 | 608 | // *** Deadlock module edits: Start *** 609 | 610 | } else if ($pdoException && $triesCount < self::MAX_DEADLOCK_RETRIES 611 | // Next check to see if we encountered a deadlock 612 | && in_array($pdoException->errorInfo[1], $deadlockErrors)){ 613 | $retry = true; 614 | $triesCount++; 615 | $sleepSeconds = pow($triesCount, 2); 616 | $stringLogger->critical("Encountered Deadlock Exception. Retrying in {$sleepSeconds} seconds. Full exception details below. " . $e->getMessage() . "\n" . $e->getTraceAsString()); 617 | sleep($sleepSeconds); 618 | 619 | // *** Deadlock module edits: End *** 620 | } 621 | 622 | if (!$retry) { 623 | $this->logger->logStats(LoggerInterface::TYPE_QUERY, $sql, $bind); 624 | $this->logger->critical($e); 625 | // rethrow custom exception if needed 626 | if ($pdoException && isset($this->exceptionMap[$pdoException->errorInfo[1]])) { 627 | $customExceptionClass = $this->exceptionMap[$pdoException->errorInfo[1]]; 628 | /** @var \Zend_Db_Adapter_Exception $customException */ 629 | $customException = new $customExceptionClass($e->getMessage(), $pdoException->errorInfo[1], $e); 630 | throw $customException; 631 | } 632 | throw $e; 633 | } 634 | } 635 | } while ($retry); 636 | } 637 | 638 | /** 639 | * Special handling for PDO query(). 640 | * 641 | * All bind parameter names must begin with ':'. 642 | * 643 | * @param string|\Magento\Framework\DB\Select $sql The SQL statement with placeholders. 644 | * @param mixed $bind An array of data or data itself to bind to the placeholders. 645 | * @return \Zend_Db_Statement_Pdo|void 646 | * @throws \Zend_Db_Adapter_Exception To re-throw \PDOException. 647 | * @throws LocalizedException In case multiple queries are attempted at once, to protect from SQL injection 648 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 649 | */ 650 | public function query($sql, $bind = []) 651 | { 652 | if (strpos(rtrim($sql, " \t\n\r\0;"), ';') !== false && count($this->_splitMultiQuery($sql)) > 1) { 653 | throw new \Magento\Framework\Exception\LocalizedException( 654 | new Phrase("Multiple queries can't be executed. Run a single query and try again.") 655 | ); 656 | } 657 | return $this->_query($sql, $bind); 658 | } 659 | 660 | /** 661 | * Allows multiple queries 662 | * 663 | * Allows multiple queries -- to safeguard against SQL injection, USE CAUTION and verify that input 664 | * cannot be tampered with. 665 | * Special handling for PDO query(). 666 | * All bind parameter names must begin with ':'. 667 | * 668 | * @param string|\Magento\Framework\DB\Select $sql The SQL statement with placeholders. 669 | * @param mixed $bind An array of data or data itself to bind to the placeholders. 670 | * @return \Zend_Db_Statement_Pdo|void 671 | * @throws \Zend_Db_Adapter_Exception To re-throw \PDOException. 672 | * @throws LocalizedException In case multiple queries are attempted at once, to protect from SQL injection 673 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 674 | * @deprecated 101.0.0 675 | */ 676 | public function multiQuery($sql, $bind = []) 677 | { 678 | return $this->_query($sql, $bind); 679 | } 680 | 681 | /** 682 | * Prepares SQL query by moving to bind all special parameters that can be confused with bind placeholders 683 | * (e.g. "foo:bar"). And also changes named bind to positional one, because underlying library has problems 684 | * with named binds. 685 | * 686 | * @param \Magento\Framework\DB\Select|string $sql 687 | * @param mixed $bind 688 | * @return $this 689 | */ 690 | protected function _prepareQuery(&$sql, &$bind = []) 691 | { 692 | $sql = (string) $sql; 693 | if (!is_array($bind)) { 694 | $bind = [$bind]; 695 | } 696 | 697 | // Mixed bind is not supported - so remember whether it is named bind, to normalize later if required 698 | $isNamedBind = false; 699 | if ($bind) { 700 | foreach ($bind as $k => $v) { 701 | if (!is_int($k)) { 702 | $isNamedBind = true; 703 | if ($k[0] != ':') { 704 | $bind[":{$k}"] = $v; 705 | unset($bind[$k]); 706 | } 707 | } 708 | } 709 | } 710 | 711 | // Special query hook 712 | if ($this->_queryHook) { 713 | $object = $this->_queryHook['object']; 714 | $method = $this->_queryHook['method']; 715 | $object->$method($sql, $bind); 716 | } 717 | 718 | return $this; 719 | } 720 | 721 | /** 722 | * Callback function for preparation of query and bind by regexp. 723 | * Checks query parameters for special symbols and moves such parameters to bind array as named ones. 724 | * This method writes to $_bindParams, where query bind parameters are kept. 725 | * This method requires further normalizing, if bind array is positional. 726 | * 727 | * @param string[] $matches 728 | * @return string 729 | */ 730 | public function proccessBindCallback($matches) 731 | { 732 | if (isset($matches[6]) && ( 733 | strpos($matches[6], "'") !== false || 734 | strpos($matches[6], ':') !== false || 735 | strpos($matches[6], '?') !== false) 736 | ) { 737 | $bindName = ':_mage_bind_var_' . (++$this->_bindIncrement); 738 | $this->_bindParams[$bindName] = $this->_unQuote($matches[6]); 739 | return ' ' . $bindName; 740 | } 741 | return $matches[0]; 742 | } 743 | 744 | /** 745 | * Unquote raw string (use for auto-bind) 746 | * 747 | * @param string $string 748 | * @return string 749 | */ 750 | protected function _unQuote($string) 751 | { 752 | $translate = [ 753 | "\\000" => "\000", 754 | "\\n" => "\n", 755 | "\\r" => "\r", 756 | "\\\\" => "\\", 757 | "\'" => "'", 758 | "\\\"" => "\"", 759 | "\\032" => "\032", 760 | ]; 761 | return strtr($string, $translate); 762 | } 763 | 764 | /** 765 | * Normalizes mixed positional-named bind to positional bind, and replaces named placeholders in query to 766 | * '?' placeholders. 767 | * 768 | * @param string $sql 769 | * @param array $bind 770 | * @return $this 771 | */ 772 | protected function _convertMixedBind(&$sql, &$bind) 773 | { 774 | $positions = []; 775 | $offset = 0; 776 | // get positions 777 | while (true) { 778 | $pos = strpos($sql, '?', $offset); 779 | if ($pos !== false) { 780 | $positions[] = $pos; 781 | $offset = ++$pos; 782 | } else { 783 | break; 784 | } 785 | } 786 | 787 | $bindResult = []; 788 | $map = []; 789 | foreach ($bind as $k => $v) { 790 | // positional 791 | if (is_int($k)) { 792 | if (!isset($positions[$k])) { 793 | continue; 794 | } 795 | $bindResult[$positions[$k]] = $v; 796 | } else { 797 | $offset = 0; 798 | while (true) { 799 | $pos = strpos($sql, $k, $offset); 800 | if ($pos === false) { 801 | break; 802 | } else { 803 | $offset = $pos + strlen($k); 804 | $bindResult[$pos] = $v; 805 | } 806 | } 807 | $map[$k] = '?'; 808 | } 809 | } 810 | 811 | ksort($bindResult); 812 | $bind = array_values($bindResult); 813 | $sql = strtr($sql, $map); 814 | 815 | return $this; 816 | } 817 | 818 | /** 819 | * Sets (removes) query hook. 820 | * 821 | * $hook must be either array with 'object' and 'method' entries, or null to remove hook. 822 | * Previous hook is returned. 823 | * 824 | * @param array $hook 825 | * @return array|null 826 | */ 827 | public function setQueryHook($hook) 828 | { 829 | $prev = $this->_queryHook; 830 | $this->_queryHook = $hook; 831 | return $prev; 832 | } 833 | 834 | /** 835 | * Split multi statement query 836 | * 837 | * @param string $sql 838 | * @return array 839 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 840 | * @SuppressWarnings(PHPMD.NPathComplexity) 841 | * @deprecated 100.1.2 842 | */ 843 | protected function _splitMultiQuery($sql) 844 | { 845 | $parts = preg_split( 846 | '#(;|\'|"|\\\\|//|--|\n|/\*|\*/)#', 847 | $sql, 848 | null, 849 | PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE 850 | ); 851 | 852 | $q = false; 853 | $c = false; 854 | $stmts = []; 855 | $s = ''; 856 | 857 | foreach ($parts as $i => $part) { 858 | // strings 859 | if (($part === "'" || $part === '"') && ($i === 0 || $parts[$i-1] !== '\\')) { 860 | if ($q === false) { 861 | $q = $part; 862 | } elseif ($q === $part) { 863 | $q = false; 864 | } 865 | } 866 | 867 | // single line comments 868 | if (($part === '//' || $part === '--') && ($i === 0 || $parts[$i-1] === "\n")) { 869 | $c = $part; 870 | } elseif ($part === "\n" && ($c === '//' || $c === '--')) { 871 | $c = false; 872 | } 873 | 874 | // multi line comments 875 | if ($part === '/*' && $c === false) { 876 | $c = '/*'; 877 | } elseif ($part === '*/' && $c === '/*') { 878 | $c = false; 879 | } 880 | 881 | // statements 882 | if ($part === ';' && $q === false && $c === false) { 883 | if (trim($s) !== '') { 884 | $stmts[] = trim($s); 885 | $s = ''; 886 | } 887 | } else { 888 | $s .= $part; 889 | } 890 | } 891 | if (trim($s) !== '') { 892 | $stmts[] = trim($s); 893 | } 894 | 895 | return $stmts; 896 | } 897 | 898 | /** 899 | * Drop the Foreign Key from table 900 | * 901 | * @param string $tableName 902 | * @param string $fkName 903 | * @param string $schemaName 904 | * @return $this 905 | */ 906 | public function dropForeignKey($tableName, $fkName, $schemaName = null) 907 | { 908 | $foreignKeys = $this->getForeignKeys($tableName, $schemaName); 909 | $fkName = strtoupper($fkName); 910 | if (substr($fkName, 0, 3) == 'FK_') { 911 | $fkName = substr($fkName, 3); 912 | } 913 | foreach ([$fkName, 'FK_' . $fkName] as $key) { 914 | if (isset($foreignKeys[$key])) { 915 | $sql = sprintf( 916 | 'ALTER TABLE %s DROP FOREIGN KEY %s', 917 | $this->quoteIdentifier($this->_getTableName($tableName, $schemaName)), 918 | $this->quoteIdentifier($foreignKeys[$key]['FK_NAME']) 919 | ); 920 | $this->resetDdlCache($tableName, $schemaName); 921 | $this->rawQuery($sql); 922 | $this->getSchemaListener()->dropForeignKey($tableName, $fkName); 923 | } 924 | } 925 | return $this; 926 | } 927 | 928 | /** 929 | * Prepare table before add constraint foreign key 930 | * 931 | * @param string $tableName 932 | * @param string $columnName 933 | * @param string $refTableName 934 | * @param string $refColumnName 935 | * @param string $onDelete 936 | * @return $this 937 | */ 938 | public function purgeOrphanRecords( 939 | $tableName, 940 | $columnName, 941 | $refTableName, 942 | $refColumnName, 943 | $onDelete = AdapterInterface::FK_ACTION_CASCADE 944 | ) { 945 | $onDelete = strtoupper($onDelete); 946 | if ($onDelete == AdapterInterface::FK_ACTION_CASCADE 947 | || $onDelete == AdapterInterface::FK_ACTION_RESTRICT 948 | ) { 949 | $sql = sprintf( 950 | "DELETE p.* FROM %s AS p LEFT JOIN %s AS r ON p.%s = r.%s WHERE r.%s IS NULL", 951 | $this->quoteIdentifier($tableName), 952 | $this->quoteIdentifier($refTableName), 953 | $this->quoteIdentifier($columnName), 954 | $this->quoteIdentifier($refColumnName), 955 | $this->quoteIdentifier($refColumnName) 956 | ); 957 | $this->rawQuery($sql); 958 | } elseif ($onDelete == AdapterInterface::FK_ACTION_SET_NULL) { 959 | $sql = sprintf( 960 | "UPDATE %s AS p LEFT JOIN %s AS r ON p.%s = r.%s SET p.%s = NULL WHERE r.%s IS NULL", 961 | $this->quoteIdentifier($tableName), 962 | $this->quoteIdentifier($refTableName), 963 | $this->quoteIdentifier($columnName), 964 | $this->quoteIdentifier($refColumnName), 965 | $this->quoteIdentifier($columnName), 966 | $this->quoteIdentifier($refColumnName) 967 | ); 968 | $this->rawQuery($sql); 969 | } 970 | 971 | return $this; 972 | } 973 | 974 | /** 975 | * Check does table column exist 976 | * 977 | * @param string $tableName 978 | * @param string $columnName 979 | * @param string $schemaName 980 | * @return bool 981 | */ 982 | public function tableColumnExists($tableName, $columnName, $schemaName = null) 983 | { 984 | $describe = $this->describeTable($tableName, $schemaName); 985 | foreach ($describe as $column) { 986 | if ($column['COLUMN_NAME'] == $columnName) { 987 | return true; 988 | } 989 | } 990 | return false; 991 | } 992 | 993 | /** 994 | * Adds new column to table. 995 | * 996 | * Generally $defintion must be array with column data to keep this call cross-DB compatible. 997 | * Using string as $definition is allowed only for concrete DB adapter. 998 | * Adds primary key if needed 999 | * 1000 | * @param string $tableName 1001 | * @param string $columnName 1002 | * @param array|string $definition string specific or universal array DB Server definition 1003 | * @param string $schemaName 1004 | * @return true|\Zend_Db_Statement_Pdo 1005 | * @throws \Zend_Db_Exception 1006 | */ 1007 | public function addColumn($tableName, $columnName, $definition, $schemaName = null) 1008 | { 1009 | $this->getSchemaListener()->addColumn($tableName, $columnName, $definition); 1010 | if ($this->tableColumnExists($tableName, $columnName, $schemaName)) { 1011 | return true; 1012 | } 1013 | 1014 | $primaryKey = ''; 1015 | if (is_array($definition)) { 1016 | $definition = array_change_key_case($definition, CASE_UPPER); 1017 | if (empty($definition['COMMENT'])) { 1018 | throw new \Zend_Db_Exception("Impossible to create a column without comment."); 1019 | } 1020 | if (!empty($definition['PRIMARY'])) { 1021 | $primaryKey = sprintf(', ADD PRIMARY KEY (%s)', $this->quoteIdentifier($columnName)); 1022 | } 1023 | $definition = $this->_getColumnDefinition($definition); 1024 | } 1025 | 1026 | $sql = sprintf( 1027 | 'ALTER TABLE %s ADD COLUMN %s %s %s', 1028 | $this->quoteIdentifier($this->_getTableName($tableName, $schemaName)), 1029 | $this->quoteIdentifier($columnName), 1030 | $definition, 1031 | $primaryKey 1032 | ); 1033 | 1034 | $result = $this->rawQuery($sql); 1035 | 1036 | $this->resetDdlCache($tableName, $schemaName); 1037 | 1038 | return $result; 1039 | } 1040 | 1041 | /** 1042 | * Delete table column 1043 | * 1044 | * @param string $tableName 1045 | * @param string $columnName 1046 | * @param string $schemaName 1047 | * @return true|\Zend_Db_Statement_Pdo 1048 | */ 1049 | public function dropColumn($tableName, $columnName, $schemaName = null) 1050 | { 1051 | if (!$this->tableColumnExists($tableName, $columnName, $schemaName)) { 1052 | return true; 1053 | } 1054 | $this->getSchemaListener()->dropColumn($tableName, $columnName); 1055 | $alterDrop = []; 1056 | 1057 | $foreignKeys = $this->getForeignKeys($tableName, $schemaName); 1058 | foreach ($foreignKeys as $fkProp) { 1059 | if ($fkProp['COLUMN_NAME'] == $columnName) { 1060 | $this->getSchemaListener()->dropForeignKey($tableName, $fkProp['FK_NAME']); 1061 | $alterDrop[] = 'DROP FOREIGN KEY ' . $this->quoteIdentifier($fkProp['FK_NAME']); 1062 | } 1063 | } 1064 | 1065 | /* drop index that after column removal would coincide with the existing index by indexed columns */ 1066 | foreach ($this->getIndexList($tableName, $schemaName) as $idxData) { 1067 | $idxColumns = $idxData['COLUMNS_LIST']; 1068 | $idxColumnKey = array_search($columnName, $idxColumns); 1069 | if ($idxColumnKey !== false) { 1070 | unset($idxColumns[$idxColumnKey]); 1071 | if (empty($idxColumns)) { 1072 | $this->getSchemaListener()->dropIndex($tableName, $idxData['KEY_NAME'], 'index'); 1073 | } 1074 | if ($idxColumns && $this->_getIndexByColumns($tableName, $idxColumns, $schemaName)) { 1075 | $this->dropIndex($tableName, $idxData['KEY_NAME'], $schemaName); 1076 | } 1077 | } 1078 | } 1079 | 1080 | $alterDrop[] = 'DROP COLUMN ' . $this->quoteIdentifier($columnName); 1081 | $sql = sprintf( 1082 | 'ALTER TABLE %s %s', 1083 | $this->quoteIdentifier($this->_getTableName($tableName, $schemaName)), 1084 | implode(', ', $alterDrop) 1085 | ); 1086 | 1087 | $result = $this->rawQuery($sql); 1088 | $this->resetDdlCache($tableName, $schemaName); 1089 | 1090 | return $result; 1091 | } 1092 | 1093 | /** 1094 | * Retrieve index information by indexed columns or return NULL, if there is no index for a column list 1095 | * 1096 | * @param string $tableName 1097 | * @param array $columns 1098 | * @param string|null $schemaName 1099 | * @return array|null 1100 | */ 1101 | protected function _getIndexByColumns($tableName, array $columns, $schemaName) 1102 | { 1103 | foreach ($this->getIndexList($tableName, $schemaName) as $idxData) { 1104 | if ($idxData['COLUMNS_LIST'] === $columns) { 1105 | return $idxData; 1106 | } 1107 | } 1108 | return null; 1109 | } 1110 | 1111 | /** 1112 | * Change the column name and definition 1113 | * 1114 | * For change definition of column - use modifyColumn 1115 | * 1116 | * @param string $tableName 1117 | * @param string $oldColumnName 1118 | * @param string $newColumnName 1119 | * @param array $definition 1120 | * @param boolean $flushData flush table statistic 1121 | * @param string $schemaName 1122 | * @return \Zend_Db_Statement_Pdo 1123 | * @throws \Zend_Db_Exception 1124 | */ 1125 | public function changeColumn( 1126 | $tableName, 1127 | $oldColumnName, 1128 | $newColumnName, 1129 | $definition, 1130 | $flushData = false, 1131 | $schemaName = null 1132 | ) { 1133 | $this->getSchemaListener()->changeColumn( 1134 | $tableName, 1135 | $oldColumnName, 1136 | $newColumnName, 1137 | $definition 1138 | ); 1139 | if (!$this->tableColumnExists($tableName, $oldColumnName, $schemaName)) { 1140 | throw new \Zend_Db_Exception( 1141 | sprintf( 1142 | 'Column "%s" does not exist in table "%s".', 1143 | $oldColumnName, 1144 | $tableName 1145 | ) 1146 | ); 1147 | } 1148 | 1149 | if (is_array($definition)) { 1150 | $definition = $this->_getColumnDefinition($definition); 1151 | } 1152 | 1153 | $sql = sprintf( 1154 | 'ALTER TABLE %s CHANGE COLUMN %s %s %s', 1155 | $this->quoteIdentifier($tableName), 1156 | $this->quoteIdentifier($oldColumnName), 1157 | $this->quoteIdentifier($newColumnName), 1158 | $definition 1159 | ); 1160 | 1161 | $result = $this->rawQuery($sql); 1162 | 1163 | if ($flushData) { 1164 | $this->showTableStatus($tableName, $schemaName); 1165 | } 1166 | $this->resetDdlCache($tableName, $schemaName); 1167 | 1168 | return $result; 1169 | } 1170 | 1171 | /** 1172 | * Modify the column definition 1173 | * 1174 | * @param string $tableName 1175 | * @param string $columnName 1176 | * @param array|string $definition 1177 | * @param boolean $flushData 1178 | * @param string $schemaName 1179 | * @return $this 1180 | * @throws \Zend_Db_Exception 1181 | */ 1182 | public function modifyColumn($tableName, $columnName, $definition, $flushData = false, $schemaName = null) 1183 | { 1184 | $this->getSchemaListener()->modifyColumn( 1185 | $tableName, 1186 | $columnName, 1187 | $definition 1188 | ); 1189 | if (!$this->tableColumnExists($tableName, $columnName, $schemaName)) { 1190 | throw new \Zend_Db_Exception(sprintf('Column "%s" does not exist in table "%s".', $columnName, $tableName)); 1191 | } 1192 | if (is_array($definition)) { 1193 | $definition = $this->_getColumnDefinition($definition); 1194 | } 1195 | 1196 | $sql = sprintf( 1197 | 'ALTER TABLE %s MODIFY COLUMN %s %s', 1198 | $this->quoteIdentifier($tableName), 1199 | $this->quoteIdentifier($columnName), 1200 | $definition 1201 | ); 1202 | 1203 | $this->rawQuery($sql); 1204 | if ($flushData) { 1205 | $this->showTableStatus($tableName, $schemaName); 1206 | } 1207 | $this->resetDdlCache($tableName, $schemaName); 1208 | 1209 | return $this; 1210 | } 1211 | 1212 | /** 1213 | * Show table status 1214 | * 1215 | * @param string $tableName 1216 | * @param string $schemaName 1217 | * @return mixed 1218 | */ 1219 | public function showTableStatus($tableName, $schemaName = null) 1220 | { 1221 | $fromDbName = null; 1222 | if ($schemaName !== null) { 1223 | $fromDbName = ' FROM ' . $this->quoteIdentifier($schemaName); 1224 | } 1225 | $query = sprintf('SHOW TABLE STATUS%s LIKE %s', $fromDbName, $this->quote($tableName)); 1226 | 1227 | return $this->rawFetchRow($query); 1228 | } 1229 | 1230 | /** 1231 | * Retrieve Create Table SQL 1232 | * 1233 | * @param string $tableName 1234 | * @param string $schemaName 1235 | * @return string 1236 | */ 1237 | public function getCreateTable($tableName, $schemaName = null) 1238 | { 1239 | $cacheKey = $this->_getTableName($tableName, $schemaName); 1240 | $ddl = $this->loadDdlCache($cacheKey, self::DDL_CREATE); 1241 | if ($ddl === false) { 1242 | $sql = 'SHOW CREATE TABLE ' . $this->quoteIdentifier($this->_getTableName($tableName, $schemaName)); 1243 | $ddl = $this->rawFetchRow($sql, 'Create Table'); 1244 | $this->saveDdlCache($cacheKey, self::DDL_CREATE, $ddl); 1245 | } 1246 | 1247 | return $ddl; 1248 | } 1249 | 1250 | /** 1251 | * Retrieve the foreign keys descriptions for a table. 1252 | * 1253 | * The return value is an associative array keyed by the UPPERCASE foreign key, 1254 | * as returned by the RDBMS. 1255 | * 1256 | * The value of each array element is an associative array 1257 | * with the following keys: 1258 | * 1259 | * FK_NAME => string; original foreign key name 1260 | * SCHEMA_NAME => string; name of database or schema 1261 | * TABLE_NAME => string; 1262 | * COLUMN_NAME => string; column name 1263 | * REF_SCHEMA_NAME => string; name of reference database or schema 1264 | * REF_TABLE_NAME => string; reference table name 1265 | * REF_COLUMN_NAME => string; reference column name 1266 | * ON_DELETE => string; action type on delete row 1267 | * 1268 | * @param string $tableName 1269 | * @param string $schemaName 1270 | * @return array 1271 | */ 1272 | public function getForeignKeys($tableName, $schemaName = null) 1273 | { 1274 | $cacheKey = $this->_getTableName($tableName, $schemaName); 1275 | $ddl = $this->loadDdlCache($cacheKey, self::DDL_FOREIGN_KEY); 1276 | if ($ddl === false) { 1277 | $ddl = []; 1278 | $createSql = $this->getCreateTable($tableName, $schemaName); 1279 | 1280 | // collect CONSTRAINT 1281 | $regExp = '#,\s+CONSTRAINT `([^`]*)` FOREIGN KEY ?\(`([^`]*)`\) ' 1282 | . 'REFERENCES (`([^`]*)`\.)?`([^`]*)` \(`([^`]*)`\)' 1283 | . '( ON DELETE (RESTRICT|CASCADE|SET NULL|NO ACTION))?' 1284 | . '( ON UPDATE (RESTRICT|CASCADE|SET NULL|NO ACTION))?#'; 1285 | $matches = []; 1286 | preg_match_all($regExp, $createSql, $matches, PREG_SET_ORDER); 1287 | foreach ($matches as $match) { 1288 | $ddl[strtoupper($match[1])] = [ 1289 | 'FK_NAME' => $match[1], 1290 | 'SCHEMA_NAME' => $schemaName, 1291 | 'TABLE_NAME' => $tableName, 1292 | 'COLUMN_NAME' => $match[2], 1293 | 'REF_SHEMA_NAME' => isset($match[4]) ? $match[4] : $schemaName, 1294 | 'REF_TABLE_NAME' => $match[5], 1295 | 'REF_COLUMN_NAME' => $match[6], 1296 | 'ON_DELETE' => isset($match[7]) ? $match[8] : '' 1297 | ]; 1298 | } 1299 | 1300 | $this->saveDdlCache($cacheKey, self::DDL_FOREIGN_KEY, $ddl); 1301 | } 1302 | 1303 | return $ddl; 1304 | } 1305 | 1306 | /** 1307 | * Retrieve the foreign keys tree for all tables 1308 | * 1309 | * @return array 1310 | */ 1311 | public function getForeignKeysTree() 1312 | { 1313 | $tree = []; 1314 | foreach ($this->listTables() as $table) { 1315 | foreach ($this->getForeignKeys($table) as $key) { 1316 | $tree[$table][$key['COLUMN_NAME']] = $key; 1317 | } 1318 | } 1319 | 1320 | return $tree; 1321 | } 1322 | 1323 | /** 1324 | * Modify tables, used for upgrade process 1325 | * 1326 | * Change columns definitions, reset foreign keys, change tables comments and engines. 1327 | * 1328 | * The value of each array element is an associative array 1329 | * with the following keys: 1330 | * 1331 | * columns => array; list of columns definitions 1332 | * comment => string; table comment 1333 | * engine => string; table engine 1334 | * 1335 | * @param array $tables 1336 | * @return $this 1337 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 1338 | * @SuppressWarnings(PHPMD.NPathComplexity) 1339 | * @SuppressWarnings(PHPMD.UnusedLocalVariable) 1340 | */ 1341 | public function modifyTables($tables) 1342 | { 1343 | $foreignKeys = $this->getForeignKeysTree(); 1344 | foreach ($tables as $table => $tableData) { 1345 | if (!$this->isTableExists($table)) { 1346 | continue; 1347 | } 1348 | foreach ($tableData['columns'] as $column => $columnDefinition) { 1349 | if (!$this->tableColumnExists($table, $column)) { 1350 | continue; 1351 | } 1352 | $droppedKeys = []; 1353 | foreach ($foreignKeys as $keyTable => $columns) { 1354 | foreach ($columns as $columnName => $keyOptions) { 1355 | if ($table == $keyOptions['REF_TABLE_NAME'] && $column == $keyOptions['REF_COLUMN_NAME']) { 1356 | $this->dropForeignKey($keyTable, $keyOptions['FK_NAME']); 1357 | $droppedKeys[] = $keyOptions; 1358 | } 1359 | } 1360 | } 1361 | 1362 | $this->modifyColumn($table, $column, $columnDefinition); 1363 | 1364 | foreach ($droppedKeys as $options) { 1365 | unset($columnDefinition['identity'], $columnDefinition['primary'], $columnDefinition['comment']); 1366 | 1367 | $onDelete = $options['ON_DELETE']; 1368 | 1369 | if ($onDelete == AdapterInterface::FK_ACTION_SET_NULL) { 1370 | $columnDefinition['nullable'] = true; 1371 | } 1372 | $this->modifyColumn($options['TABLE_NAME'], $options['COLUMN_NAME'], $columnDefinition); 1373 | $this->addForeignKey( 1374 | $options['FK_NAME'], 1375 | $options['TABLE_NAME'], 1376 | $options['COLUMN_NAME'], 1377 | $options['REF_TABLE_NAME'], 1378 | $options['REF_COLUMN_NAME'], 1379 | ($onDelete) ? $onDelete : AdapterInterface::FK_ACTION_NO_ACTION 1380 | ); 1381 | } 1382 | } 1383 | if (!empty($tableData['comment'])) { 1384 | $this->changeTableComment($table, $tableData['comment']); 1385 | } 1386 | if (!empty($tableData['engine'])) { 1387 | $this->changeTableEngine($table, $tableData['engine']); 1388 | } 1389 | } 1390 | 1391 | return $this; 1392 | } 1393 | 1394 | /** 1395 | * Retrieve table index information 1396 | * 1397 | * The return value is an associative array keyed by the UPPERCASE index key (except for primary key, 1398 | * that is always stored under 'PRIMARY' key) as returned by the RDBMS. 1399 | * 1400 | * The value of each array element is an associative array 1401 | * with the following keys: 1402 | * 1403 | * SCHEMA_NAME => string; name of database or schema 1404 | * TABLE_NAME => string; name of the table 1405 | * KEY_NAME => string; the original index name 1406 | * COLUMNS_LIST => array; array of index column names 1407 | * INDEX_TYPE => string; lowercase, create index type 1408 | * INDEX_METHOD => string; index method using 1409 | * type => string; see INDEX_TYPE 1410 | * fields => array; see COLUMNS_LIST 1411 | * 1412 | * @param string $tableName 1413 | * @param string $schemaName 1414 | * @return array|string|int 1415 | */ 1416 | public function getIndexList($tableName, $schemaName = null) 1417 | { 1418 | $cacheKey = $this->_getTableName($tableName, $schemaName); 1419 | $ddl = $this->loadDdlCache($cacheKey, self::DDL_INDEX); 1420 | if ($ddl === false) { 1421 | $ddl = []; 1422 | 1423 | $sql = sprintf( 1424 | 'SHOW INDEX FROM %s', 1425 | $this->quoteIdentifier($this->_getTableName($tableName, $schemaName)) 1426 | ); 1427 | foreach ($this->fetchAll($sql) as $row) { 1428 | $fieldKeyName = 'Key_name'; 1429 | $fieldNonUnique = 'Non_unique'; 1430 | $fieldColumn = 'Column_name'; 1431 | $fieldIndexType = 'Index_type'; 1432 | 1433 | if (strtolower($row[$fieldKeyName]) == AdapterInterface::INDEX_TYPE_PRIMARY) { 1434 | $indexType = AdapterInterface::INDEX_TYPE_PRIMARY; 1435 | } elseif ($row[$fieldNonUnique] == 0) { 1436 | $indexType = AdapterInterface::INDEX_TYPE_UNIQUE; 1437 | } elseif (strtolower($row[$fieldIndexType]) == AdapterInterface::INDEX_TYPE_FULLTEXT) { 1438 | $indexType = AdapterInterface::INDEX_TYPE_FULLTEXT; 1439 | } else { 1440 | $indexType = AdapterInterface::INDEX_TYPE_INDEX; 1441 | } 1442 | 1443 | $upperKeyName = strtoupper($row[$fieldKeyName]); 1444 | if (isset($ddl[$upperKeyName])) { 1445 | $ddl[$upperKeyName]['fields'][] = $row[$fieldColumn]; // for compatible 1446 | $ddl[$upperKeyName]['COLUMNS_LIST'][] = $row[$fieldColumn]; 1447 | } else { 1448 | $ddl[$upperKeyName] = [ 1449 | 'SCHEMA_NAME' => $schemaName, 1450 | 'TABLE_NAME' => $tableName, 1451 | 'KEY_NAME' => $row[$fieldKeyName], 1452 | 'COLUMNS_LIST' => [$row[$fieldColumn]], 1453 | 'INDEX_TYPE' => $indexType, 1454 | 'INDEX_METHOD' => $row[$fieldIndexType], 1455 | 'type' => strtolower($indexType), // for compatibility 1456 | 'fields' => [$row[$fieldColumn]], // for compatibility 1457 | ]; 1458 | } 1459 | } 1460 | $this->saveDdlCache($cacheKey, self::DDL_INDEX, $ddl); 1461 | } 1462 | 1463 | return $ddl; 1464 | } 1465 | 1466 | /** 1467 | * Remove duplicate entry for create key 1468 | * 1469 | * @param string $table 1470 | * @param array $fields 1471 | * @param string[] $ids 1472 | * @return $this 1473 | */ 1474 | protected function _removeDuplicateEntry($table, $fields, $ids) 1475 | { 1476 | $where = []; 1477 | $i = 0; 1478 | foreach ($fields as $field) { 1479 | $where[] = $this->quoteInto($field . '=?', $ids[$i++]); 1480 | } 1481 | 1482 | if (!$where) { 1483 | return $this; 1484 | } 1485 | $whereCond = implode(' AND ', $where); 1486 | $sql = sprintf('SELECT COUNT(*) as `cnt` FROM `%s` WHERE %s', $table, $whereCond); 1487 | 1488 | $cnt = $this->rawFetchRow($sql, 'cnt'); 1489 | if ($cnt > 1) { 1490 | $sql = sprintf( 1491 | 'DELETE FROM `%s` WHERE %s LIMIT %d', 1492 | $table, 1493 | $whereCond, 1494 | $cnt - 1 1495 | ); 1496 | $this->rawQuery($sql); 1497 | } 1498 | 1499 | return $this; 1500 | } 1501 | 1502 | /** 1503 | * Creates and returns a new \Magento\Framework\DB\Select object for this adapter. 1504 | * 1505 | * @return Select 1506 | */ 1507 | public function select() 1508 | { 1509 | return $this->selectFactory->create($this); 1510 | } 1511 | 1512 | /** 1513 | * Quotes a value and places into a piece of text at a placeholder. 1514 | * 1515 | * Method revrited for handle empty arrays in value param 1516 | * 1517 | * @param string $text The text with a placeholder. 1518 | * @param mixed $value The value to quote. 1519 | * @param string $type OPTIONAL SQL datatype 1520 | * @param integer $count OPTIONAL count of placeholders to replace 1521 | * @return string An SQL-safe quoted value placed into the orignal text. 1522 | */ 1523 | public function quoteInto($text, $value, $type = null, $count = null) 1524 | { 1525 | if (is_array($value) && empty($value)) { 1526 | $value = new \Zend_Db_Expr('NULL'); 1527 | } 1528 | 1529 | if ($value instanceof \DateTimeInterface) { 1530 | $value = $value->format('Y-m-d H:i:s'); 1531 | } 1532 | 1533 | return parent::quoteInto($text, $value, $type, $count); 1534 | } 1535 | 1536 | /** 1537 | * Retrieve ddl cache name 1538 | * 1539 | * @param string $tableName 1540 | * @param string $schemaName 1541 | * @return string 1542 | */ 1543 | protected function _getTableName($tableName, $schemaName = null) 1544 | { 1545 | return ($schemaName ? $schemaName . '.' : '') . $tableName; 1546 | } 1547 | 1548 | /** 1549 | * Retrieve Id for cache 1550 | * 1551 | * @param string $tableKey 1552 | * @param int $ddlType 1553 | * @return string 1554 | */ 1555 | protected function _getCacheId($tableKey, $ddlType) 1556 | { 1557 | return sprintf('%s_%s_%s', self::DDL_CACHE_PREFIX, $tableKey, $ddlType); 1558 | } 1559 | 1560 | /** 1561 | * Load DDL data from cache 1562 | * 1563 | * Return false if cache does not exists 1564 | * 1565 | * @param string $tableCacheKey the table cache key 1566 | * @param int $ddlType the DDL constant 1567 | * @return string|array|int|false 1568 | */ 1569 | public function loadDdlCache($tableCacheKey, $ddlType) 1570 | { 1571 | if (!$this->_isDdlCacheAllowed) { 1572 | return false; 1573 | } 1574 | if (isset($this->_ddlCache[$ddlType][$tableCacheKey])) { 1575 | return $this->_ddlCache[$ddlType][$tableCacheKey]; 1576 | } 1577 | 1578 | if ($this->_cacheAdapter) { 1579 | $cacheId = $this->_getCacheId($tableCacheKey, $ddlType); 1580 | $data = $this->_cacheAdapter->load($cacheId); 1581 | if ($data !== false) { 1582 | $data = $this->serializer->unserialize($data); 1583 | $this->_ddlCache[$ddlType][$tableCacheKey] = $data; 1584 | } 1585 | return $data; 1586 | } 1587 | 1588 | return false; 1589 | } 1590 | 1591 | /** 1592 | * Save DDL data into cache 1593 | * 1594 | * @param string $tableCacheKey 1595 | * @param int $ddlType 1596 | * @param array $data 1597 | * @return $this 1598 | */ 1599 | public function saveDdlCache($tableCacheKey, $ddlType, $data) 1600 | { 1601 | if (!$this->_isDdlCacheAllowed) { 1602 | return $this; 1603 | } 1604 | $this->_ddlCache[$ddlType][$tableCacheKey] = $data; 1605 | 1606 | if ($this->_cacheAdapter) { 1607 | $cacheId = $this->_getCacheId($tableCacheKey, $ddlType); 1608 | $data = $this->serializer->serialize($data); 1609 | $this->_cacheAdapter->save($data, $cacheId, [self::DDL_CACHE_TAG]); 1610 | } 1611 | 1612 | return $this; 1613 | } 1614 | 1615 | /** 1616 | * Reset cached DDL data from cache 1617 | * 1618 | * If table name is null - reset all cached DDL data 1619 | * 1620 | * @param string $tableName 1621 | * @param string $schemaName OPTIONAL 1622 | * @return $this 1623 | */ 1624 | public function resetDdlCache($tableName = null, $schemaName = null) 1625 | { 1626 | if (!$this->_isDdlCacheAllowed) { 1627 | return $this; 1628 | } 1629 | if ($tableName === null) { 1630 | $this->_ddlCache = []; 1631 | if ($this->_cacheAdapter) { 1632 | $this->_cacheAdapter->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::DDL_CACHE_TAG]); 1633 | } 1634 | } else { 1635 | $cacheKey = $this->_getTableName($tableName, $schemaName); 1636 | 1637 | $ddlTypes = [self::DDL_DESCRIBE, self::DDL_CREATE, self::DDL_INDEX, self::DDL_FOREIGN_KEY]; 1638 | foreach ($ddlTypes as $ddlType) { 1639 | unset($this->_ddlCache[$ddlType][$cacheKey]); 1640 | } 1641 | 1642 | if ($this->_cacheAdapter) { 1643 | foreach ($ddlTypes as $ddlType) { 1644 | $cacheId = $this->_getCacheId($cacheKey, $ddlType); 1645 | $this->_cacheAdapter->remove($cacheId); 1646 | } 1647 | } 1648 | } 1649 | 1650 | return $this; 1651 | } 1652 | 1653 | /** 1654 | * Disallow DDL caching 1655 | * 1656 | * @return $this 1657 | */ 1658 | public function disallowDdlCache() 1659 | { 1660 | $this->_isDdlCacheAllowed = false; 1661 | return $this; 1662 | } 1663 | 1664 | /** 1665 | * Allow DDL caching 1666 | * 1667 | * @return $this 1668 | */ 1669 | public function allowDdlCache() 1670 | { 1671 | $this->_isDdlCacheAllowed = true; 1672 | return $this; 1673 | } 1674 | 1675 | /** 1676 | * Returns the column descriptions for a table. 1677 | * 1678 | * The return value is an associative array keyed by the column name, 1679 | * as returned by the RDBMS. 1680 | * 1681 | * The value of each array element is an associative array 1682 | * with the following keys: 1683 | * 1684 | * SCHEMA_NAME => string; name of database or schema 1685 | * TABLE_NAME => string; 1686 | * COLUMN_NAME => string; column name 1687 | * COLUMN_POSITION => number; ordinal position of column in table 1688 | * DATA_TYPE => string; SQL datatype name of column 1689 | * DEFAULT => string; default expression of column, null if none 1690 | * NULLABLE => boolean; true if column can have nulls 1691 | * LENGTH => number; length of CHAR/VARCHAR 1692 | * SCALE => number; scale of NUMERIC/DECIMAL 1693 | * PRECISION => number; precision of NUMERIC/DECIMAL 1694 | * UNSIGNED => boolean; unsigned property of an integer type 1695 | * PRIMARY => boolean; true if column is part of the primary key 1696 | * PRIMARY_POSITION => integer; position of column in primary key 1697 | * IDENTITY => integer; true if column is auto-generated with unique values 1698 | * 1699 | * @param string $tableName 1700 | * @param string $schemaName OPTIONAL 1701 | * @return array 1702 | */ 1703 | public function describeTable($tableName, $schemaName = null) 1704 | { 1705 | $cacheKey = $this->_getTableName($tableName, $schemaName); 1706 | $ddl = $this->loadDdlCache($cacheKey, self::DDL_DESCRIBE); 1707 | if ($ddl === false) { 1708 | $ddl = parent::describeTable($tableName, $schemaName); 1709 | /** 1710 | * Remove bug in some MySQL versions, when int-column without default value is described as: 1711 | * having default empty string value 1712 | */ 1713 | $affected = ['tinyint', 'smallint', 'mediumint', 'int', 'bigint']; 1714 | foreach ($ddl as $key => $columnData) { 1715 | if (($columnData['DEFAULT'] === '') && (array_search($columnData['DATA_TYPE'], $affected) !== false)) { 1716 | $ddl[$key]['DEFAULT'] = null; 1717 | } 1718 | } 1719 | $this->saveDdlCache($cacheKey, self::DDL_DESCRIBE, $ddl); 1720 | } 1721 | 1722 | return $ddl; 1723 | } 1724 | 1725 | /** 1726 | * Format described column to definition, ready to be added to ddl table. 1727 | * 1728 | * Return array with keys: name, type, length, options, comment 1729 | * 1730 | * @param array $columnData 1731 | * @return array 1732 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 1733 | * @SuppressWarnings(PHPMD.NPathComplexity) 1734 | */ 1735 | public function getColumnCreateByDescribe($columnData) 1736 | { 1737 | $type = $this->_getColumnTypeByDdl($columnData); 1738 | $options = []; 1739 | 1740 | if ($columnData['IDENTITY'] === true) { 1741 | $options['identity'] = true; 1742 | } 1743 | if ($columnData['UNSIGNED'] === true) { 1744 | $options['unsigned'] = true; 1745 | } 1746 | if ($columnData['NULLABLE'] === false 1747 | && !($type == Table::TYPE_TEXT && strlen($columnData['DEFAULT']) != 0) 1748 | ) { 1749 | $options['nullable'] = false; 1750 | } 1751 | if ($columnData['PRIMARY'] === true) { 1752 | $options['primary'] = true; 1753 | } 1754 | if ($columnData['DEFAULT'] !== null && $type != Table::TYPE_TEXT) { 1755 | $options['default'] = $this->quote($columnData['DEFAULT']); 1756 | } 1757 | if (strlen($columnData['SCALE']) > 0) { 1758 | $options['scale'] = $columnData['SCALE']; 1759 | } 1760 | if (strlen($columnData['PRECISION']) > 0) { 1761 | $options['precision'] = $columnData['PRECISION']; 1762 | } 1763 | 1764 | $comment = $this->string->upperCaseWords($columnData['COLUMN_NAME'], '_', ' '); 1765 | 1766 | $result = [ 1767 | 'name' => $columnData['COLUMN_NAME'], 1768 | 'type' => $type, 1769 | 'length' => $columnData['LENGTH'], 1770 | 'options' => $options, 1771 | 'comment' => $comment, 1772 | ]; 1773 | 1774 | return $result; 1775 | } 1776 | 1777 | /** 1778 | * Create \Magento\Framework\DB\Ddl\Table object by data from describe table 1779 | * 1780 | * @param string $tableName 1781 | * @param string $newTableName 1782 | * @return Table 1783 | */ 1784 | public function createTableByDdl($tableName, $newTableName) 1785 | { 1786 | $describe = $this->describeTable($tableName); 1787 | $table = $this->newTable($newTableName) 1788 | ->setComment($this->string->upperCaseWords($newTableName, '_', ' ')); 1789 | 1790 | foreach ($describe as $columnData) { 1791 | $columnInfo = $this->getColumnCreateByDescribe($columnData); 1792 | 1793 | $table->addColumn( 1794 | $columnInfo['name'], 1795 | $columnInfo['type'], 1796 | $columnInfo['length'], 1797 | $columnInfo['options'], 1798 | $columnInfo['comment'] 1799 | ); 1800 | } 1801 | 1802 | $indexes = $this->getIndexList($tableName); 1803 | foreach ($indexes as $indexData) { 1804 | /** 1805 | * Do not create primary index - it is created with identity column. 1806 | * For reliability check both name and type, because these values can start to differ in future. 1807 | */ 1808 | if (($indexData['KEY_NAME'] == 'PRIMARY') 1809 | || ($indexData['INDEX_TYPE'] == AdapterInterface::INDEX_TYPE_PRIMARY) 1810 | ) { 1811 | continue; 1812 | } 1813 | 1814 | $fields = $indexData['COLUMNS_LIST']; 1815 | $options = ['type' => $indexData['INDEX_TYPE']]; 1816 | $table->addIndex($this->getIndexName($newTableName, $fields, $indexData['INDEX_TYPE']), $fields, $options); 1817 | } 1818 | 1819 | $foreignKeys = $this->getForeignKeys($tableName); 1820 | foreach ($foreignKeys as $keyData) { 1821 | $fkName = $this->getForeignKeyName( 1822 | $newTableName, 1823 | $keyData['COLUMN_NAME'], 1824 | $keyData['REF_TABLE_NAME'], 1825 | $keyData['REF_COLUMN_NAME'] 1826 | ); 1827 | $onDelete = $this->_getDdlAction($keyData['ON_DELETE']); 1828 | 1829 | $table->addForeignKey( 1830 | $fkName, 1831 | $keyData['COLUMN_NAME'], 1832 | $keyData['REF_TABLE_NAME'], 1833 | $keyData['REF_COLUMN_NAME'], 1834 | $onDelete 1835 | ); 1836 | } 1837 | 1838 | // Set additional options 1839 | $tableData = $this->showTableStatus($tableName); 1840 | $table->setOption('type', $tableData['Engine']); 1841 | 1842 | return $table; 1843 | } 1844 | 1845 | /** 1846 | * Modify the column definition by data from describe table 1847 | * 1848 | * @param string $tableName 1849 | * @param string $columnName 1850 | * @param array $definition 1851 | * @param boolean $flushData 1852 | * @param string $schemaName 1853 | * @return $this 1854 | */ 1855 | public function modifyColumnByDdl($tableName, $columnName, $definition, $flushData = false, $schemaName = null) 1856 | { 1857 | $definition = array_change_key_case($definition, CASE_UPPER); 1858 | $definition['COLUMN_TYPE'] = $this->_getColumnTypeByDdl($definition); 1859 | if (array_key_exists('DEFAULT', $definition) && $definition['DEFAULT'] === null) { 1860 | unset($definition['DEFAULT']); 1861 | } 1862 | 1863 | return $this->modifyColumn($tableName, $columnName, $definition, $flushData, $schemaName); 1864 | } 1865 | 1866 | /** 1867 | * Retrieve column data type by data from describe table 1868 | * 1869 | * @param array $column 1870 | * @return string 1871 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 1872 | */ 1873 | protected function _getColumnTypeByDdl($column) 1874 | { 1875 | switch ($column['DATA_TYPE']) { 1876 | case 'bool': 1877 | return Table::TYPE_BOOLEAN; 1878 | case 'tinytext': 1879 | case 'char': 1880 | case 'varchar': 1881 | case 'text': 1882 | case 'mediumtext': 1883 | case 'longtext': 1884 | return Table::TYPE_TEXT; 1885 | case 'blob': 1886 | case 'mediumblob': 1887 | case 'longblob': 1888 | return Table::TYPE_BLOB; 1889 | case 'tinyint': 1890 | case 'smallint': 1891 | return Table::TYPE_SMALLINT; 1892 | case 'mediumint': 1893 | case 'int': 1894 | return Table::TYPE_INTEGER; 1895 | case 'bigint': 1896 | return Table::TYPE_BIGINT; 1897 | case 'datetime': 1898 | return Table::TYPE_DATETIME; 1899 | case 'timestamp': 1900 | return Table::TYPE_TIMESTAMP; 1901 | case 'date': 1902 | return Table::TYPE_DATE; 1903 | case 'float': 1904 | return Table::TYPE_FLOAT; 1905 | case 'decimal': 1906 | case 'numeric': 1907 | return Table::TYPE_DECIMAL; 1908 | } 1909 | } 1910 | 1911 | /** 1912 | * Change table storage engine 1913 | * 1914 | * @param string $tableName 1915 | * @param string $engine 1916 | * @param string $schemaName 1917 | * @return \Zend_Db_Statement_Pdo 1918 | */ 1919 | public function changeTableEngine($tableName, $engine, $schemaName = null) 1920 | { 1921 | $table = $this->quoteIdentifier($this->_getTableName($tableName, $schemaName)); 1922 | $sql = sprintf('ALTER TABLE %s ENGINE=%s', $table, $engine); 1923 | 1924 | return $this->rawQuery($sql); 1925 | } 1926 | 1927 | /** 1928 | * Change table comment 1929 | * 1930 | * @param string $tableName 1931 | * @param string $comment 1932 | * @param string $schemaName 1933 | * @return \Zend_Db_Statement_Pdo 1934 | */ 1935 | public function changeTableComment($tableName, $comment, $schemaName = null) 1936 | { 1937 | $table = $this->quoteIdentifier($this->_getTableName($tableName, $schemaName)); 1938 | $sql = sprintf("ALTER TABLE %s COMMENT='%s'", $table, $comment); 1939 | 1940 | return $this->rawQuery($sql); 1941 | } 1942 | 1943 | /** 1944 | * Inserts a table row with specified data 1945 | * 1946 | * Special for Zero values to identity column 1947 | * 1948 | * @param string $table 1949 | * @param array $bind 1950 | * @return int The number of affected rows. 1951 | */ 1952 | public function insertForce($table, array $bind) 1953 | { 1954 | $this->rawQuery("SET @OLD_INSERT_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO'"); 1955 | $result = $this->insert($table, $bind); 1956 | $this->rawQuery("SET SQL_MODE=IFNULL(@OLD_INSERT_SQL_MODE,'')"); 1957 | 1958 | return $result; 1959 | } 1960 | 1961 | /** 1962 | * Inserts a table row with specified data. 1963 | * 1964 | * @param string $table The table to insert data into. 1965 | * @param array $data Column-value pairs or array of column-value pairs. 1966 | * @param array $fields update fields pairs or values 1967 | * @return int The number of affected rows. 1968 | * @throws \Zend_Db_Exception 1969 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 1970 | * @SuppressWarnings(PHPMD.NPathComplexity) 1971 | */ 1972 | public function insertOnDuplicate($table, array $data, array $fields = []) 1973 | { 1974 | // extract and quote col names from the array keys 1975 | $row = reset($data); // get first element from data array 1976 | $bind = []; // SQL bind array 1977 | $values = []; 1978 | 1979 | if (is_array($row)) { // Array of column-value pairs 1980 | $cols = array_keys($row); 1981 | foreach ($data as $row) { 1982 | if (array_diff($cols, array_keys($row))) { 1983 | throw new \Zend_Db_Exception('Invalid data for insert'); 1984 | } 1985 | $values[] = $this->_prepareInsertData($row, $bind); 1986 | } 1987 | unset($row); 1988 | } else { // Column-value pairs 1989 | $cols = array_keys($data); 1990 | $values[] = $this->_prepareInsertData($data, $bind); 1991 | } 1992 | 1993 | $updateFields = []; 1994 | if (empty($fields)) { 1995 | $fields = $cols; 1996 | } 1997 | 1998 | // prepare ON DUPLICATE KEY conditions 1999 | foreach ($fields as $k => $v) { 2000 | $field = $value = null; 2001 | if (!is_numeric($k)) { 2002 | $field = $this->quoteIdentifier($k); 2003 | if ($v instanceof \Zend_Db_Expr) { 2004 | $value = $v->__toString(); 2005 | } elseif ($v instanceof \Zend\Db\Sql\Expression) { 2006 | $value = $v->getExpression(); 2007 | } elseif (is_string($v)) { 2008 | $value = sprintf('VALUES(%s)', $this->quoteIdentifier($v)); 2009 | } elseif (is_numeric($v)) { 2010 | $value = $this->quoteInto('?', $v); 2011 | } 2012 | } elseif (is_string($v)) { 2013 | $value = sprintf('VALUES(%s)', $this->quoteIdentifier($v)); 2014 | $field = $this->quoteIdentifier($v); 2015 | } 2016 | 2017 | if ($field && is_string($value) && $value !== '') { 2018 | $updateFields[] = sprintf('%s = %s', $field, $value); 2019 | } 2020 | } 2021 | 2022 | $insertSql = $this->_getInsertSqlQuery($table, $cols, $values); 2023 | if ($updateFields) { 2024 | $insertSql .= ' ON DUPLICATE KEY UPDATE ' . implode(', ', $updateFields); 2025 | } 2026 | // execute the statement and return the number of affected rows 2027 | $stmt = $this->query($insertSql, array_values($bind)); 2028 | $result = $stmt->rowCount(); 2029 | 2030 | return $result; 2031 | } 2032 | 2033 | /** 2034 | * Inserts a table multiply rows with specified data. 2035 | * 2036 | * @param string|array|\Zend_Db_Expr $table The table to insert data into. 2037 | * @param array $data Column-value pairs or array of Column-value pairs. 2038 | * @return int The number of affected rows. 2039 | * @throws \Zend_Db_Exception 2040 | */ 2041 | public function insertMultiple($table, array $data) 2042 | { 2043 | $row = reset($data); 2044 | // support insert syntaxes 2045 | if (!is_array($row)) { 2046 | return $this->insert($table, $data); 2047 | } 2048 | 2049 | // validate data array 2050 | $cols = array_keys($row); 2051 | $insertArray = []; 2052 | foreach ($data as $row) { 2053 | $line = []; 2054 | if (array_diff($cols, array_keys($row))) { 2055 | throw new \Zend_Db_Exception('Invalid data for insert'); 2056 | } 2057 | foreach ($cols as $field) { 2058 | $line[] = $row[$field]; 2059 | } 2060 | $insertArray[] = $line; 2061 | } 2062 | unset($row); 2063 | 2064 | return $this->insertArray($table, $cols, $insertArray); 2065 | } 2066 | 2067 | /** 2068 | * Insert array into a table based on columns definition 2069 | * 2070 | * $data can be represented as: 2071 | * - arrays of values ordered according to columns in $columns array 2072 | * array( 2073 | * array('value1', 'value2'), 2074 | * array('value3', 'value4'), 2075 | * ) 2076 | * - array of values, if $columns contains only one column 2077 | * array('value1', 'value2') 2078 | * 2079 | * @param string $table 2080 | * @param string[] $columns 2081 | * @param array $data 2082 | * @param int $strategy 2083 | * @return int 2084 | * @throws \Zend_Db_Exception 2085 | */ 2086 | public function insertArray($table, array $columns, array $data, $strategy = 0) 2087 | { 2088 | $values = []; 2089 | $bind = []; 2090 | $columnsCount = count($columns); 2091 | foreach ($data as $row) { 2092 | if (is_array($row) && $columnsCount != count($row)) { 2093 | throw new \Zend_Db_Exception('Invalid data for insert'); 2094 | } 2095 | $values[] = $this->_prepareInsertData($row, $bind); 2096 | } 2097 | 2098 | switch ($strategy) { 2099 | case self::REPLACE: 2100 | $query = $this->_getReplaceSqlQuery($table, $columns, $values); 2101 | break; 2102 | default: 2103 | $query = $this->_getInsertSqlQuery($table, $columns, $values, $strategy); 2104 | } 2105 | 2106 | // execute the statement and return the number of affected rows 2107 | $stmt = $this->query($query, $bind); 2108 | $result = $stmt->rowCount(); 2109 | 2110 | return $result; 2111 | } 2112 | 2113 | /** 2114 | * Set cache adapter 2115 | * 2116 | * @param FrontendInterface $cacheAdapter 2117 | * @return $this 2118 | */ 2119 | public function setCacheAdapter(FrontendInterface $cacheAdapter) 2120 | { 2121 | $this->_cacheAdapter = $cacheAdapter; 2122 | return $this; 2123 | } 2124 | 2125 | /** 2126 | * Return new DDL Table object 2127 | * 2128 | * @param string $tableName the table name 2129 | * @param string $schemaName the database/schema name 2130 | * @return Table 2131 | */ 2132 | public function newTable($tableName = null, $schemaName = null) 2133 | { 2134 | $table = new Table(); 2135 | if ($tableName !== null) { 2136 | $table->setName($tableName); 2137 | } 2138 | if ($schemaName !== null) { 2139 | $table->setSchema($schemaName); 2140 | } 2141 | if (isset($this->_config['engine'])) { 2142 | $table->setOption('type', $this->_config['engine']); 2143 | } 2144 | 2145 | return $table; 2146 | } 2147 | 2148 | /** 2149 | * Create table 2150 | * 2151 | * @param Table $table 2152 | * @throws \Zend_Db_Exception 2153 | * @return \Zend_Db_Statement_Pdo 2154 | */ 2155 | public function createTable(Table $table) 2156 | { 2157 | $this->getSchemaListener()->createTable($table); 2158 | $columns = $table->getColumns(); 2159 | foreach ($columns as $columnEntry) { 2160 | if (empty($columnEntry['COMMENT'])) { 2161 | throw new \Zend_Db_Exception("Cannot create table without columns comments"); 2162 | } 2163 | } 2164 | 2165 | $sqlFragment = array_merge( 2166 | $this->_getColumnsDefinition($table), 2167 | $this->_getIndexesDefinition($table), 2168 | $this->_getForeignKeysDefinition($table) 2169 | ); 2170 | $tableOptions = $this->_getOptionsDefinition($table); 2171 | $sql = sprintf( 2172 | "CREATE TABLE IF NOT EXISTS %s (\n%s\n) %s", 2173 | $this->quoteIdentifier($table->getName()), 2174 | implode(",\n", $sqlFragment), 2175 | implode(" ", $tableOptions) 2176 | ); 2177 | 2178 | if ($this->getTransactionLevel() > 0) { 2179 | $result = $this->createConnection()->query($sql); 2180 | } else { 2181 | $result = $this->query($sql); 2182 | } 2183 | $this->resetDdlCache($table->getName(), $table->getSchema()); 2184 | 2185 | return $result; 2186 | } 2187 | 2188 | /** 2189 | * Create temporary table 2190 | * 2191 | * @param \Magento\Framework\DB\Ddl\Table $table 2192 | * @throws \Zend_Db_Exception 2193 | * @return \Zend_Db_Statement_Pdo|void 2194 | * @SuppressWarnings(PHPMD.UnusedLocalVariable) 2195 | */ 2196 | public function createTemporaryTable(\Magento\Framework\DB\Ddl\Table $table) 2197 | { 2198 | $sqlFragment = array_merge( 2199 | $this->_getColumnsDefinition($table), 2200 | $this->_getIndexesDefinition($table), 2201 | $this->_getForeignKeysDefinition($table) 2202 | ); 2203 | $tableOptions = $this->_getOptionsDefinition($table); 2204 | $sql = sprintf( 2205 | "CREATE TEMPORARY TABLE %s (\n%s\n) %s", 2206 | $this->quoteIdentifier($table->getName()), 2207 | implode(",\n", $sqlFragment), 2208 | implode(" ", $tableOptions) 2209 | ); 2210 | 2211 | return $this->query($sql); 2212 | } 2213 | 2214 | /** 2215 | * Create temporary table like 2216 | * 2217 | * @param string $temporaryTableName 2218 | * @param string $originTableName 2219 | * @param bool $ifNotExists 2220 | * @return \Zend_Db_Statement_Pdo 2221 | */ 2222 | public function createTemporaryTableLike($temporaryTableName, $originTableName, $ifNotExists = false) 2223 | { 2224 | $ifNotExistsSql = ($ifNotExists ? 'IF NOT EXISTS' : ''); 2225 | $temporaryTable = $this->quoteIdentifier($this->_getTableName($temporaryTableName)); 2226 | $originTable = $this->quoteIdentifier($this->_getTableName($originTableName)); 2227 | $sql = sprintf('CREATE TEMPORARY TABLE %s %s LIKE %s', $ifNotExistsSql, $temporaryTable, $originTable); 2228 | 2229 | return $this->query($sql); 2230 | } 2231 | 2232 | /** 2233 | * Rename several tables 2234 | * 2235 | * @param array $tablePairs array('oldName' => 'Name1', 'newName' => 'Name2') 2236 | * 2237 | * @return boolean 2238 | * @throws \Zend_Db_Exception 2239 | */ 2240 | public function renameTablesBatch(array $tablePairs) 2241 | { 2242 | if (count($tablePairs) == 0) { 2243 | throw new \Zend_Db_Exception('Please provide tables for rename'); 2244 | } 2245 | 2246 | $renamesList = []; 2247 | $tablesList = []; 2248 | foreach ($tablePairs as $pair) { 2249 | $oldTableName = $pair['oldName']; 2250 | $newTableName = $pair['newName']; 2251 | $renamesList[] = sprintf('%s TO %s', $oldTableName, $newTableName); 2252 | 2253 | $tablesList[$oldTableName] = $oldTableName; 2254 | $tablesList[$newTableName] = $newTableName; 2255 | } 2256 | 2257 | $query = sprintf('RENAME TABLE %s', implode(',', $renamesList)); 2258 | $this->query($query); 2259 | 2260 | foreach ($tablesList as $table) { 2261 | $this->resetDdlCache($table); 2262 | } 2263 | 2264 | return true; 2265 | } 2266 | 2267 | /** 2268 | * Retrieve columns and primary keys definition array for create table 2269 | * 2270 | * @param Table $table 2271 | * @return string[] 2272 | * @throws \Zend_Db_Exception 2273 | */ 2274 | protected function _getColumnsDefinition(Table $table) 2275 | { 2276 | $definition = []; 2277 | $primary = []; 2278 | $columns = $table->getColumns(); 2279 | if (empty($columns)) { 2280 | throw new \Zend_Db_Exception('Table columns are not defined'); 2281 | } 2282 | 2283 | foreach ($columns as $columnData) { 2284 | $columnDefinition = $this->_getColumnDefinition($columnData); 2285 | if ($columnData['PRIMARY']) { 2286 | $primary[$columnData['COLUMN_NAME']] = $columnData['PRIMARY_POSITION']; 2287 | } 2288 | 2289 | $definition[] = sprintf( 2290 | ' %s %s', 2291 | $this->quoteIdentifier($columnData['COLUMN_NAME']), 2292 | $columnDefinition 2293 | ); 2294 | } 2295 | 2296 | // PRIMARY KEY 2297 | if (!empty($primary)) { 2298 | asort($primary, SORT_NUMERIC); 2299 | $primary = array_map([$this, 'quoteIdentifier'], array_keys($primary)); 2300 | $definition[] = sprintf(' PRIMARY KEY (%s)', implode(', ', $primary)); 2301 | } 2302 | 2303 | return $definition; 2304 | } 2305 | 2306 | /** 2307 | * Retrieve table indexes definition array for create table 2308 | * 2309 | * @param Table $table 2310 | * @return string[] 2311 | */ 2312 | protected function _getIndexesDefinition(Table $table) 2313 | { 2314 | $definition = []; 2315 | $indexes = $table->getIndexes(); 2316 | foreach ($indexes as $indexData) { 2317 | if (!empty($indexData['TYPE'])) { 2318 | //Skipping not supported fulltext indexes for NDB 2319 | if (($indexData['TYPE'] == AdapterInterface::INDEX_TYPE_FULLTEXT) && $this->isNdb($table)) { 2320 | continue; 2321 | } 2322 | switch ($indexData['TYPE']) { 2323 | case AdapterInterface::INDEX_TYPE_PRIMARY: 2324 | $indexType = 'PRIMARY KEY'; 2325 | unset($indexData['INDEX_NAME']); 2326 | break; 2327 | default: 2328 | $indexType = strtoupper($indexData['TYPE']); 2329 | break; 2330 | } 2331 | } else { 2332 | $indexType = 'KEY'; 2333 | } 2334 | 2335 | $columns = []; 2336 | foreach ($indexData['COLUMNS'] as $columnData) { 2337 | $column = $this->quoteIdentifier($columnData['NAME']); 2338 | if (!empty($columnData['SIZE'])) { 2339 | $column .= sprintf('(%d)', $columnData['SIZE']); 2340 | } 2341 | $columns[] = $column; 2342 | } 2343 | $indexName = isset($indexData['INDEX_NAME']) ? $this->quoteIdentifier($indexData['INDEX_NAME']) : ''; 2344 | $definition[] = sprintf( 2345 | ' %s %s (%s)', 2346 | $indexType, 2347 | $indexName, 2348 | implode(', ', $columns) 2349 | ); 2350 | } 2351 | 2352 | return $definition; 2353 | } 2354 | 2355 | /** 2356 | * Check if NDB is used for table 2357 | * 2358 | * @param Table $table 2359 | * @return bool 2360 | */ 2361 | protected function isNdb(Table $table) 2362 | { 2363 | $engineType = strtolower($table->getOption('type')); 2364 | return $engineType == 'ndb' || $engineType == 'ndbcluster'; 2365 | } 2366 | 2367 | /** 2368 | * Retrieve table foreign keys definition array for create table 2369 | * 2370 | * @param Table $table 2371 | * @return string[] 2372 | */ 2373 | protected function _getForeignKeysDefinition(Table $table) 2374 | { 2375 | $definition = []; 2376 | $relations = $table->getForeignKeys(); 2377 | 2378 | if (!empty($relations)) { 2379 | foreach ($relations as $fkData) { 2380 | $onDelete = $this->_getDdlAction($fkData['ON_DELETE']); 2381 | $definition[] = sprintf( 2382 | ' CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s) ON DELETE %s', 2383 | $this->quoteIdentifier($fkData['FK_NAME']), 2384 | $this->quoteIdentifier($fkData['COLUMN_NAME']), 2385 | $this->quoteIdentifier($fkData['REF_TABLE_NAME']), 2386 | $this->quoteIdentifier($fkData['REF_COLUMN_NAME']), 2387 | $onDelete 2388 | ); 2389 | } 2390 | } 2391 | 2392 | return $definition; 2393 | } 2394 | 2395 | /** 2396 | * Retrieve table options definition array for create table 2397 | * 2398 | * @param Table $table 2399 | * @return string[] 2400 | * @throws \Zend_Db_Exception 2401 | */ 2402 | protected function _getOptionsDefinition(Table $table) 2403 | { 2404 | $definition = []; 2405 | $comment = $table->getComment(); 2406 | if (empty($comment)) { 2407 | throw new \Zend_Db_Exception('Comment for table is required and must be defined'); 2408 | } 2409 | $definition[] = $this->quoteInto('COMMENT=?', $comment); 2410 | 2411 | $tableProps = [ 2412 | 'type' => 'ENGINE=%s', 2413 | 'checksum' => 'CHECKSUM=%d', 2414 | 'auto_increment' => 'AUTO_INCREMENT=%d', 2415 | 'avg_row_length' => 'AVG_ROW_LENGTH=%d', 2416 | 'max_rows' => 'MAX_ROWS=%d', 2417 | 'min_rows' => 'MIN_ROWS=%d', 2418 | 'delay_key_write' => 'DELAY_KEY_WRITE=%d', 2419 | 'row_format' => 'row_format=%s', 2420 | 'charset' => 'charset=%s', 2421 | 'collate' => 'COLLATE=%s', 2422 | ]; 2423 | foreach ($tableProps as $key => $mask) { 2424 | $v = $table->getOption($key); 2425 | if ($v !== null) { 2426 | $definition[] = sprintf($mask, $v); 2427 | } 2428 | } 2429 | 2430 | return $definition; 2431 | } 2432 | 2433 | /** 2434 | * Get column definition from description 2435 | * 2436 | * @param array $options 2437 | * @param null|string $ddlType 2438 | * @return string 2439 | */ 2440 | public function getColumnDefinitionFromDescribe($options, $ddlType = null) 2441 | { 2442 | $columnInfo = $this->getColumnCreateByDescribe($options); 2443 | foreach ($columnInfo['options'] as $key => $value) { 2444 | $columnInfo[$key] = $value; 2445 | } 2446 | return $this->_getColumnDefinition($columnInfo, $ddlType); 2447 | } 2448 | 2449 | /** 2450 | * Retrieve column definition fragment 2451 | * 2452 | * @param array $options 2453 | * @param string $ddlType Table DDL Column type constant 2454 | * @throws \Magento\Framework\Exception\LocalizedException 2455 | * @return string 2456 | * @throws \Zend_Db_Exception 2457 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 2458 | * @SuppressWarnings(PHPMD.NPathComplexity) 2459 | * @SuppressWarnings(PHPMD.ExcessiveMethodLength) 2460 | * @SuppressWarnings(PHPMD.ExcessiveParameterList) 2461 | */ 2462 | protected function _getColumnDefinition($options, $ddlType = null) 2463 | { 2464 | // convert keys to uppercase 2465 | $options = array_change_key_case($options, CASE_UPPER); 2466 | $cType = null; 2467 | $cUnsigned = false; 2468 | $cNullable = true; 2469 | $cDefault = false; 2470 | $cIdentity = false; 2471 | 2472 | // detect and validate column type 2473 | if ($ddlType === null) { 2474 | $ddlType = $this->_getDdlType($options); 2475 | } 2476 | 2477 | if (empty($ddlType) || !isset($this->_ddlColumnTypes[$ddlType])) { 2478 | throw new \Zend_Db_Exception('Invalid column definition data'); 2479 | } 2480 | 2481 | // column size 2482 | $cType = $this->_ddlColumnTypes[$ddlType]; 2483 | switch ($ddlType) { 2484 | case Table::TYPE_SMALLINT: 2485 | case Table::TYPE_INTEGER: 2486 | case Table::TYPE_BIGINT: 2487 | if (!empty($options['UNSIGNED'])) { 2488 | $cUnsigned = true; 2489 | } 2490 | break; 2491 | case Table::TYPE_DECIMAL: 2492 | case Table::TYPE_FLOAT: 2493 | case Table::TYPE_NUMERIC: 2494 | $precision = 10; 2495 | $scale = 0; 2496 | $match = []; 2497 | if (!empty($options['LENGTH']) && preg_match('#^\(?(\d+),(\d+)\)?$#', $options['LENGTH'], $match)) { 2498 | $precision = $match[1]; 2499 | $scale = $match[2]; 2500 | } else { 2501 | if (isset($options['SCALE']) && is_numeric($options['SCALE'])) { 2502 | $scale = $options['SCALE']; 2503 | } 2504 | if (isset($options['PRECISION']) && is_numeric($options['PRECISION'])) { 2505 | $precision = $options['PRECISION']; 2506 | } 2507 | } 2508 | $cType .= sprintf('(%d,%d)', $precision, $scale); 2509 | if (!empty($options['UNSIGNED'])) { 2510 | $cUnsigned = true; 2511 | } 2512 | break; 2513 | case Table::TYPE_TEXT: 2514 | case Table::TYPE_BLOB: 2515 | case Table::TYPE_VARBINARY: 2516 | if (empty($options['LENGTH'])) { 2517 | $length = Table::DEFAULT_TEXT_SIZE; 2518 | } else { 2519 | $length = $this->_parseTextSize($options['LENGTH']); 2520 | } 2521 | if ($length <= 255) { 2522 | $cType = $ddlType == Table::TYPE_TEXT ? 'varchar' : 'varbinary'; 2523 | $cType = sprintf('%s(%d)', $cType, $length); 2524 | } elseif ($length > 255 && $length <= 65536) { 2525 | $cType = $ddlType == Table::TYPE_TEXT ? 'text' : 'blob'; 2526 | } elseif ($length > 65536 && $length <= 16777216) { 2527 | $cType = $ddlType == Table::TYPE_TEXT ? 'mediumtext' : 'mediumblob'; 2528 | } else { 2529 | $cType = $ddlType == Table::TYPE_TEXT ? 'longtext' : 'longblob'; 2530 | } 2531 | break; 2532 | } 2533 | 2534 | if (array_key_exists('DEFAULT', $options)) { 2535 | $cDefault = $options['DEFAULT']; 2536 | } 2537 | if (array_key_exists('NULLABLE', $options)) { 2538 | $cNullable = (bool)$options['NULLABLE']; 2539 | } 2540 | if (!empty($options['IDENTITY']) || !empty($options['AUTO_INCREMENT'])) { 2541 | $cIdentity = true; 2542 | } 2543 | 2544 | /* For cases when tables created from createTableByDdl() 2545 | * where default value can be quoted already. 2546 | * We need to avoid "double-quoting" here 2547 | */ 2548 | if ($cDefault !== null && is_string($cDefault) && strlen($cDefault)) { 2549 | $cDefault = str_replace("'", '', $cDefault); 2550 | } 2551 | 2552 | // prepare default value string 2553 | if ($ddlType == Table::TYPE_TIMESTAMP) { 2554 | if ($cDefault === null) { 2555 | $cDefault = new \Zend_Db_Expr('NULL'); 2556 | } elseif ($cDefault == Table::TIMESTAMP_INIT) { 2557 | $cDefault = new \Zend_Db_Expr('CURRENT_TIMESTAMP'); 2558 | } elseif ($cDefault == Table::TIMESTAMP_UPDATE) { 2559 | $cDefault = new \Zend_Db_Expr('0 ON UPDATE CURRENT_TIMESTAMP'); 2560 | } elseif ($cDefault == Table::TIMESTAMP_INIT_UPDATE) { 2561 | $cDefault = new \Zend_Db_Expr('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); 2562 | } elseif ($cNullable && !$cDefault) { 2563 | $cDefault = new \Zend_Db_Expr('NULL'); 2564 | } else { 2565 | $cDefault = false; 2566 | } 2567 | } elseif ($cDefault === null && $cNullable) { 2568 | $cDefault = new \Zend_Db_Expr('NULL'); 2569 | } 2570 | 2571 | if (empty($options['COMMENT'])) { 2572 | $comment = ''; 2573 | } else { 2574 | $comment = $options['COMMENT']; 2575 | } 2576 | 2577 | //set column position 2578 | $after = null; 2579 | if (!empty($options['AFTER'])) { 2580 | $after = $options['AFTER']; 2581 | } 2582 | 2583 | return sprintf( 2584 | '%s%s%s%s%s COMMENT %s %s', 2585 | $cType, 2586 | $cUnsigned ? ' UNSIGNED' : '', 2587 | $cNullable ? ' NULL' : ' NOT NULL', 2588 | $cDefault !== false ? $this->quoteInto(' default ?', $cDefault) : '', 2589 | $cIdentity ? ' auto_increment' : '', 2590 | $this->quote($comment), 2591 | $after ? 'AFTER ' . $this->quoteIdentifier($after) : '' 2592 | ); 2593 | } 2594 | 2595 | /** 2596 | * Drop table from database 2597 | * 2598 | * @param string $tableName 2599 | * @param string $schemaName 2600 | * @return true 2601 | */ 2602 | public function dropTable($tableName, $schemaName = null) 2603 | { 2604 | $table = $this->quoteIdentifier($this->_getTableName($tableName, $schemaName)); 2605 | $query = 'DROP TABLE IF EXISTS ' . $table; 2606 | if ($this->getTransactionLevel() > 0) { 2607 | $this->createConnection()->query($query); 2608 | } else { 2609 | $this->query($query); 2610 | } 2611 | $this->resetDdlCache($tableName, $schemaName); 2612 | $this->getSchemaListener()->dropTable($tableName); 2613 | return true; 2614 | } 2615 | 2616 | /** 2617 | * Drop temporary table from database 2618 | * 2619 | * @param string $tableName 2620 | * @param string $schemaName 2621 | * @return boolean 2622 | */ 2623 | public function dropTemporaryTable($tableName, $schemaName = null) 2624 | { 2625 | $table = $this->quoteIdentifier($this->_getTableName($tableName, $schemaName)); 2626 | $query = 'DROP TEMPORARY TABLE IF EXISTS ' . $table; 2627 | $this->query($query); 2628 | 2629 | return true; 2630 | } 2631 | 2632 | /** 2633 | * Truncate a table 2634 | * 2635 | * @param string $tableName 2636 | * @param string $schemaName 2637 | * @return $this 2638 | * @throws \Zend_Db_Exception 2639 | */ 2640 | public function truncateTable($tableName, $schemaName = null) 2641 | { 2642 | if (!$this->isTableExists($tableName, $schemaName)) { 2643 | throw new \Zend_Db_Exception(sprintf('Table "%s" does not exist', $tableName)); 2644 | } 2645 | 2646 | $table = $this->quoteIdentifier($this->_getTableName($tableName, $schemaName)); 2647 | $query = 'TRUNCATE TABLE ' . $table; 2648 | $this->query($query); 2649 | 2650 | return $this; 2651 | } 2652 | 2653 | /** 2654 | * Check is a table exists 2655 | * 2656 | * @param string $tableName 2657 | * @param string $schemaName 2658 | * @return bool 2659 | */ 2660 | public function isTableExists($tableName, $schemaName = null) 2661 | { 2662 | return $this->showTableStatus($tableName, $schemaName) !== false; 2663 | } 2664 | 2665 | /** 2666 | * Rename table 2667 | * 2668 | * @param string $oldTableName 2669 | * @param string $newTableName 2670 | * @param string $schemaName 2671 | * @return true 2672 | * @throws \Zend_Db_Exception 2673 | */ 2674 | public function renameTable($oldTableName, $newTableName, $schemaName = null) 2675 | { 2676 | if (!$this->isTableExists($oldTableName, $schemaName)) { 2677 | throw new \Zend_Db_Exception(sprintf('Table "%s" does not exist', $oldTableName)); 2678 | } 2679 | if ($this->isTableExists($newTableName, $schemaName)) { 2680 | throw new \Zend_Db_Exception(sprintf('Table "%s" already exists', $newTableName)); 2681 | } 2682 | $this->getSchemaListener()->renameTable($oldTableName, $newTableName); 2683 | $oldTable = $this->_getTableName($oldTableName, $schemaName); 2684 | $newTable = $this->_getTableName($newTableName, $schemaName); 2685 | 2686 | $query = sprintf('ALTER TABLE %s RENAME TO %s', $oldTable, $newTable); 2687 | 2688 | if ($this->getTransactionLevel() > 0) { 2689 | $this->createConnection()->query($query); 2690 | } else { 2691 | $this->query($query); 2692 | } 2693 | $this->resetDdlCache($oldTableName, $schemaName); 2694 | 2695 | return true; 2696 | } 2697 | 2698 | /** 2699 | * Add new index to table name 2700 | * 2701 | * @param string $tableName 2702 | * @param string $indexName 2703 | * @param string|array $fields the table column name or array of ones 2704 | * @param string $indexType the index type 2705 | * @param string $schemaName 2706 | * @return \Zend_Db_Statement_Interface 2707 | * @throws \Zend_Db_Exception 2708 | * @throws \Exception 2709 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 2710 | * @SuppressWarnings(PHPMD.NPathComplexity) 2711 | */ 2712 | public function addIndex( 2713 | $tableName, 2714 | $indexName, 2715 | $fields, 2716 | $indexType = AdapterInterface::INDEX_TYPE_INDEX, 2717 | $schemaName = null 2718 | ) { 2719 | $this->getSchemaListener()->addIndex( 2720 | $tableName, 2721 | $indexName, 2722 | $fields, 2723 | $indexType 2724 | ); 2725 | $columns = $this->describeTable($tableName, $schemaName); 2726 | $keyList = $this->getIndexList($tableName, $schemaName); 2727 | 2728 | $query = sprintf('ALTER TABLE %s', $this->quoteIdentifier($this->_getTableName($tableName, $schemaName))); 2729 | if (isset($keyList[strtoupper($indexName)])) { 2730 | if ($keyList[strtoupper($indexName)]['INDEX_TYPE'] == AdapterInterface::INDEX_TYPE_PRIMARY) { 2731 | $query .= ' DROP PRIMARY KEY,'; 2732 | } else { 2733 | $query .= sprintf(' DROP INDEX %s,', $this->quoteIdentifier($indexName)); 2734 | } 2735 | } 2736 | 2737 | if (!is_array($fields)) { 2738 | $fields = [$fields]; 2739 | } 2740 | 2741 | $fieldSql = []; 2742 | foreach ($fields as $field) { 2743 | if (!isset($columns[$field])) { 2744 | $msg = sprintf( 2745 | 'There is no field "%s" that you are trying to create an index on "%s"', 2746 | $field, 2747 | $tableName 2748 | ); 2749 | throw new \Zend_Db_Exception($msg); 2750 | } 2751 | $fieldSql[] = $this->quoteIdentifier($field); 2752 | } 2753 | $fieldSql = implode(',', $fieldSql); 2754 | 2755 | switch (strtolower($indexType)) { 2756 | case AdapterInterface::INDEX_TYPE_PRIMARY: 2757 | $condition = 'PRIMARY KEY'; 2758 | break; 2759 | case AdapterInterface::INDEX_TYPE_UNIQUE: 2760 | $condition = 'UNIQUE ' . $this->quoteIdentifier($indexName); 2761 | break; 2762 | case AdapterInterface::INDEX_TYPE_FULLTEXT: 2763 | $condition = 'FULLTEXT ' . $this->quoteIdentifier($indexName); 2764 | break; 2765 | default: 2766 | $condition = 'INDEX ' . $this->quoteIdentifier($indexName); 2767 | break; 2768 | } 2769 | 2770 | $query .= sprintf(' ADD %s (%s)', $condition, $fieldSql); 2771 | 2772 | $cycle = true; 2773 | while ($cycle === true) { 2774 | try { 2775 | $result = $this->rawQuery($query); 2776 | $cycle = false; 2777 | } catch (\Exception $e) { 2778 | if (in_array(strtolower($indexType), ['primary', 'unique'])) { 2779 | $match = []; 2780 | if (preg_match('#SQLSTATE\[23000\]: [^:]+: 1062[^\']+\'([\d-\.]+)\'#', $e->getMessage(), $match)) { 2781 | $ids = explode('-', $match[1]); 2782 | $this->_removeDuplicateEntry($tableName, $fields, $ids); 2783 | continue; 2784 | } 2785 | } 2786 | throw $e; 2787 | } 2788 | } 2789 | 2790 | $this->resetDdlCache($tableName, $schemaName); 2791 | 2792 | return $result; 2793 | } 2794 | 2795 | /** 2796 | * Drop the index from table 2797 | * 2798 | * @param string $tableName 2799 | * @param string $keyName 2800 | * @param string $schemaName 2801 | * @return true|\Zend_Db_Statement_Interface 2802 | */ 2803 | public function dropIndex($tableName, $keyName, $schemaName = null) 2804 | { 2805 | $indexList = $this->getIndexList($tableName, $schemaName); 2806 | $indexType = 'index'; 2807 | $keyName = strtoupper($keyName); 2808 | if (!isset($indexList[$keyName])) { 2809 | return true; 2810 | } 2811 | 2812 | if ($keyName == 'PRIMARY') { 2813 | $indexType = 'primary'; 2814 | $cond = 'DROP PRIMARY KEY'; 2815 | } else { 2816 | if (strpos($keyName, 'UNQ_') !== false) { 2817 | $indexType = 'unique'; 2818 | } 2819 | $cond = 'DROP KEY ' . $this->quoteIdentifier($indexList[$keyName]['KEY_NAME']); 2820 | } 2821 | 2822 | $sql = sprintf( 2823 | 'ALTER TABLE %s %s', 2824 | $this->quoteIdentifier($this->_getTableName($tableName, $schemaName)), 2825 | $cond 2826 | ); 2827 | $this->getSchemaListener()->dropIndex($tableName, $keyName, $indexType); 2828 | $this->resetDdlCache($tableName, $schemaName); 2829 | 2830 | return $this->rawQuery($sql); 2831 | } 2832 | 2833 | /** 2834 | * Add new Foreign Key to table 2835 | * 2836 | * If Foreign Key with same name is exist - it will be deleted 2837 | * 2838 | * @param string $fkName 2839 | * @param string $tableName 2840 | * @param string $columnName 2841 | * @param string $refTableName 2842 | * @param string $refColumnName 2843 | * @param string $onDelete 2844 | * @param bool $purge trying remove invalid data 2845 | * @param string $schemaName 2846 | * @param string $refSchemaName 2847 | * @return \Zend_Db_Statement_Interface 2848 | * @SuppressWarnings(PHPMD.ExcessiveParameterList) 2849 | */ 2850 | public function addForeignKey( 2851 | $fkName, 2852 | $tableName, 2853 | $columnName, 2854 | $refTableName, 2855 | $refColumnName, 2856 | $onDelete = AdapterInterface::FK_ACTION_CASCADE, 2857 | $purge = false, 2858 | $schemaName = null, 2859 | $refSchemaName = null 2860 | ) { 2861 | $this->dropForeignKey($tableName, $fkName, $schemaName); 2862 | 2863 | if ($purge) { 2864 | $this->purgeOrphanRecords($tableName, $columnName, $refTableName, $refColumnName, $onDelete); 2865 | } 2866 | 2867 | $query = sprintf( 2868 | 'ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)', 2869 | $this->quoteIdentifier($this->_getTableName($tableName, $schemaName)), 2870 | $this->quoteIdentifier($fkName), 2871 | $this->quoteIdentifier($columnName), 2872 | $this->quoteIdentifier($this->_getTableName($refTableName, $refSchemaName)), 2873 | $this->quoteIdentifier($refColumnName) 2874 | ); 2875 | 2876 | if ($onDelete !== null) { 2877 | $query .= ' ON DELETE ' . strtoupper($onDelete); 2878 | } 2879 | 2880 | $this->getSchemaListener()->addForeignKey( 2881 | $fkName, 2882 | $tableName, 2883 | $columnName, 2884 | $refTableName, 2885 | $refColumnName, 2886 | $onDelete 2887 | ); 2888 | 2889 | $result = $this->rawQuery($query); 2890 | $this->resetDdlCache($tableName); 2891 | return $result; 2892 | } 2893 | 2894 | /** 2895 | * Format Date to internal database date format 2896 | * 2897 | * @param int|string|\DateTimeInterface $date 2898 | * @param bool $includeTime 2899 | * @return \Zend_Db_Expr 2900 | */ 2901 | public function formatDate($date, $includeTime = true) 2902 | { 2903 | $date = $this->dateTime->formatDate($date, $includeTime); 2904 | 2905 | if ($date === null) { 2906 | return new \Zend_Db_Expr('NULL'); 2907 | } 2908 | 2909 | return new \Zend_Db_Expr($this->quote($date)); 2910 | } 2911 | 2912 | /** 2913 | * Run additional environment before setup 2914 | * 2915 | * @return $this 2916 | */ 2917 | public function startSetup() 2918 | { 2919 | $this->rawQuery("SET SQL_MODE=''"); 2920 | $this->rawQuery("SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0"); 2921 | $this->rawQuery("SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO'"); 2922 | 2923 | return $this; 2924 | } 2925 | 2926 | /** 2927 | * Run additional environment after setup 2928 | * 2929 | * @return $this 2930 | */ 2931 | public function endSetup() 2932 | { 2933 | $this->rawQuery("SET SQL_MODE=IFNULL(@OLD_SQL_MODE,'')"); 2934 | $this->rawQuery("SET FOREIGN_KEY_CHECKS=IF(@OLD_FOREIGN_KEY_CHECKS=0, 0, 1)"); 2935 | 2936 | return $this; 2937 | } 2938 | 2939 | /** 2940 | * Build SQL statement for condition 2941 | * 2942 | * If $condition integer or string - exact value will be filtered ('eq' condition) 2943 | * 2944 | * If $condition is array is - one of the following structures is expected: 2945 | * - array("from" => $fromValue, "to" => $toValue) 2946 | * - array("eq" => $equalValue) 2947 | * - array("neq" => $notEqualValue) 2948 | * - array("like" => $likeValue) 2949 | * - array("in" => array($inValues)) 2950 | * - array("nin" => array($notInValues)) 2951 | * - array("notnull" => $valueIsNotNull) 2952 | * - array("null" => $valueIsNull) 2953 | * - array("gt" => $greaterValue) 2954 | * - array("lt" => $lessValue) 2955 | * - array("gteq" => $greaterOrEqualValue) 2956 | * - array("lteq" => $lessOrEqualValue) 2957 | * - array("finset" => $valueInSet) 2958 | * - array("nfinset" => $valueNotInSet) 2959 | * - array("regexp" => $regularExpression) 2960 | * - array("seq" => $stringValue) 2961 | * - array("sneq" => $stringValue) 2962 | * 2963 | * If non matched - sequential array is expected and OR conditions 2964 | * will be built using above mentioned structure 2965 | * 2966 | * @param string $fieldName 2967 | * @param integer|string|array $condition 2968 | * @return string 2969 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 2970 | */ 2971 | public function prepareSqlCondition($fieldName, $condition) 2972 | { 2973 | $conditionKeyMap = [ 2974 | 'eq' => "{{fieldName}} = ?", 2975 | 'neq' => "{{fieldName}} != ?", 2976 | 'like' => "{{fieldName}} LIKE ?", 2977 | 'nlike' => "{{fieldName}} NOT LIKE ?", 2978 | 'in' => "{{fieldName}} IN(?)", 2979 | 'nin' => "{{fieldName}} NOT IN(?)", 2980 | 'is' => "{{fieldName}} IS ?", 2981 | 'notnull' => "{{fieldName}} IS NOT NULL", 2982 | 'null' => "{{fieldName}} IS NULL", 2983 | 'gt' => "{{fieldName}} > ?", 2984 | 'lt' => "{{fieldName}} < ?", 2985 | 'gteq' => "{{fieldName}} >= ?", 2986 | 'lteq' => "{{fieldName}} <= ?", 2987 | 'finset' => "FIND_IN_SET(?, {{fieldName}})", 2988 | 'nfinset' => "NOT FIND_IN_SET(?, {{fieldName}})", 2989 | 'regexp' => "{{fieldName}} REGEXP ?", 2990 | 'from' => "{{fieldName}} >= ?", 2991 | 'to' => "{{fieldName}} <= ?", 2992 | 'seq' => null, 2993 | 'sneq' => null, 2994 | 'ntoa' => "INET_NTOA({{fieldName}}) LIKE ?", 2995 | ]; 2996 | 2997 | $query = ''; 2998 | if (is_array($condition)) { 2999 | $key = key(array_intersect_key($condition, $conditionKeyMap)); 3000 | 3001 | if (isset($condition['from']) || isset($condition['to'])) { 3002 | if (isset($condition['from'])) { 3003 | $from = $this->_prepareSqlDateCondition($condition, 'from'); 3004 | $query = $this->_prepareQuotedSqlCondition($conditionKeyMap['from'], $from, $fieldName); 3005 | } 3006 | 3007 | if (isset($condition['to'])) { 3008 | $query .= empty($query) ? '' : ' AND '; 3009 | $to = $this->_prepareSqlDateCondition($condition, 'to'); 3010 | $query = $query . $this->_prepareQuotedSqlCondition($conditionKeyMap['to'], $to, $fieldName); 3011 | } 3012 | } elseif (array_key_exists($key, $conditionKeyMap)) { 3013 | $value = $condition[$key]; 3014 | if (($key == 'seq') || ($key == 'sneq')) { 3015 | $key = $this->_transformStringSqlCondition($key, $value); 3016 | } 3017 | if (($key == 'in' || $key == 'nin') && is_string($value)) { 3018 | $value = explode(',', $value); 3019 | } 3020 | $query = $this->_prepareQuotedSqlCondition($conditionKeyMap[$key], $value, $fieldName); 3021 | } else { 3022 | $queries = []; 3023 | foreach ($condition as $orCondition) { 3024 | $queries[] = sprintf('(%s)', $this->prepareSqlCondition($fieldName, $orCondition)); 3025 | } 3026 | 3027 | $query = sprintf('(%s)', implode(' OR ', $queries)); 3028 | } 3029 | } else { 3030 | $query = $this->_prepareQuotedSqlCondition($conditionKeyMap['eq'], (string)$condition, $fieldName); 3031 | } 3032 | 3033 | return $query; 3034 | } 3035 | 3036 | /** 3037 | * Prepare Sql condition 3038 | * 3039 | * @param string $text Condition value 3040 | * @param mixed $value 3041 | * @param string $fieldName 3042 | * @return string 3043 | */ 3044 | protected function _prepareQuotedSqlCondition($text, $value, $fieldName) 3045 | { 3046 | $sql = $this->quoteInto($text, $value); 3047 | $sql = str_replace('{{fieldName}}', $fieldName, $sql); 3048 | return $sql; 3049 | } 3050 | 3051 | /** 3052 | * Transforms sql condition key 'seq' / 'sneq' that is used for comparing string values to its analog: 3053 | * - 'null' / 'notnull' for empty strings 3054 | * - 'eq' / 'neq' for non-empty strings 3055 | * 3056 | * @param string $conditionKey 3057 | * @param mixed $value 3058 | * @return string 3059 | */ 3060 | protected function _transformStringSqlCondition($conditionKey, $value) 3061 | { 3062 | $value = (string) $value; 3063 | if ($value == '') { 3064 | return ($conditionKey == 'seq') ? 'null' : 'notnull'; 3065 | } else { 3066 | return ($conditionKey == 'seq') ? 'eq' : 'neq'; 3067 | } 3068 | } 3069 | 3070 | /** 3071 | * Prepare value for save in column 3072 | * 3073 | * Return converted to column data type value 3074 | * 3075 | * @param array $column the column describe array 3076 | * @param mixed $value 3077 | * @return mixed 3078 | * 3079 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 3080 | * @SuppressWarnings(PHPMD.NPathComplexity) 3081 | */ 3082 | public function prepareColumnValue(array $column, $value) 3083 | { 3084 | if ($value instanceof \Zend_Db_Expr) { 3085 | return $value; 3086 | } 3087 | if ($value instanceof Parameter) { 3088 | return $value; 3089 | } 3090 | 3091 | // return original value if invalid column describe data 3092 | if (!isset($column['DATA_TYPE'])) { 3093 | return $value; 3094 | } 3095 | 3096 | // return null 3097 | if ($value === null && $column['NULLABLE']) { 3098 | return null; 3099 | } 3100 | 3101 | switch ($column['DATA_TYPE']) { 3102 | case 'smallint': 3103 | case 'int': 3104 | $value = (int)$value; 3105 | break; 3106 | case 'bigint': 3107 | if (!is_integer($value)) { 3108 | $value = sprintf('%.0f', (float)$value); 3109 | } 3110 | break; 3111 | 3112 | case 'decimal': 3113 | $precision = 10; 3114 | $scale = 0; 3115 | if (isset($column['SCALE'])) { 3116 | $scale = $column['SCALE']; 3117 | } 3118 | if (isset($column['PRECISION'])) { 3119 | $precision = $column['PRECISION']; 3120 | } 3121 | $format = sprintf('%%%d.%dF', $precision - $scale, $scale); 3122 | $value = (float)sprintf($format, $value); 3123 | break; 3124 | 3125 | case 'float': 3126 | $value = (float)sprintf('%F', $value); 3127 | break; 3128 | 3129 | case 'date': 3130 | $value = $this->formatDate($value, false); 3131 | break; 3132 | case 'datetime': 3133 | case 'timestamp': 3134 | $value = $this->formatDate($value); 3135 | break; 3136 | 3137 | case 'varchar': 3138 | case 'mediumtext': 3139 | case 'text': 3140 | case 'longtext': 3141 | $value = (string)$value; 3142 | if ($column['NULLABLE'] && $value == '') { 3143 | $value = null; 3144 | } 3145 | break; 3146 | 3147 | case 'varbinary': 3148 | case 'mediumblob': 3149 | case 'blob': 3150 | case 'longblob': 3151 | // No special processing for MySQL is needed 3152 | break; 3153 | } 3154 | 3155 | return $value; 3156 | } 3157 | 3158 | /** 3159 | * Generate fragment of SQL, that check condition and return true or false value 3160 | * 3161 | * @param \Zend_Db_Expr|\Magento\Framework\DB\Select|string $expression 3162 | * @param string $true true value 3163 | * @param string $false false value 3164 | * @return \Zend_Db_Expr 3165 | */ 3166 | public function getCheckSql($expression, $true, $false) 3167 | { 3168 | if ($expression instanceof \Zend_Db_Expr || $expression instanceof \Zend_Db_Select) { 3169 | $expression = sprintf("IF((%s), %s, %s)", $expression, $true, $false); 3170 | } else { 3171 | $expression = sprintf("IF(%s, %s, %s)", $expression, $true, $false); 3172 | } 3173 | 3174 | return new \Zend_Db_Expr($expression); 3175 | } 3176 | 3177 | /** 3178 | * Returns valid IFNULL expression 3179 | * 3180 | * @param \Zend_Db_Expr|\Magento\Framework\DB\Select|string $expression 3181 | * @param string|int $value OPTIONAL. Applies when $expression is NULL 3182 | * @return \Zend_Db_Expr 3183 | */ 3184 | public function getIfNullSql($expression, $value = 0) 3185 | { 3186 | if ($expression instanceof \Zend_Db_Expr || $expression instanceof \Zend_Db_Select) { 3187 | $expression = sprintf("IFNULL((%s), %s)", $expression, $value); 3188 | } else { 3189 | $expression = sprintf("IFNULL(%s, %s)", $expression, $value); 3190 | } 3191 | 3192 | return new \Zend_Db_Expr($expression); 3193 | } 3194 | 3195 | /** 3196 | * Generates case SQL fragment 3197 | * 3198 | * Generate fragment of SQL, that check value against multiple condition cases 3199 | * and return different result depends on them 3200 | * 3201 | * @param string $valueName Name of value to check 3202 | * @param array $casesResults Cases and results 3203 | * @param string $defaultValue value to use if value doesn't confirm to any cases 3204 | * @return \Zend_Db_Expr 3205 | */ 3206 | public function getCaseSql($valueName, $casesResults, $defaultValue = null) 3207 | { 3208 | $expression = 'CASE ' . $valueName; 3209 | foreach ($casesResults as $case => $result) { 3210 | $expression .= ' WHEN ' . $case . ' THEN ' . $result; 3211 | } 3212 | if ($defaultValue !== null) { 3213 | $expression .= ' ELSE ' . $defaultValue; 3214 | } 3215 | $expression .= ' END'; 3216 | 3217 | return new \Zend_Db_Expr($expression); 3218 | } 3219 | 3220 | /** 3221 | * Generate fragment of SQL, that combine together (concatenate) the results from data array 3222 | * 3223 | * All arguments in data must be quoted 3224 | * 3225 | * @param string[] $data 3226 | * @param string $separator concatenate with separator 3227 | * @return \Zend_Db_Expr 3228 | */ 3229 | public function getConcatSql(array $data, $separator = null) 3230 | { 3231 | $format = empty($separator) ? 'CONCAT(%s)' : "CONCAT_WS('{$separator}', %s)"; 3232 | return new \Zend_Db_Expr(sprintf($format, implode(', ', $data))); 3233 | } 3234 | 3235 | /** 3236 | * Generate fragment of SQL that returns length of character string 3237 | * 3238 | * The string argument must be quoted 3239 | * 3240 | * @param string $string 3241 | * @return \Zend_Db_Expr 3242 | */ 3243 | public function getLengthSql($string) 3244 | { 3245 | return new \Zend_Db_Expr(sprintf('LENGTH(%s)', $string)); 3246 | } 3247 | 3248 | /** 3249 | * Generate least SQL fragment 3250 | * 3251 | * Generate fragment of SQL, that compare with two or more arguments, and returns the smallest 3252 | * (minimum-valued) argument 3253 | * All arguments in data must be quoted 3254 | * 3255 | * @param string[] $data 3256 | * @return \Zend_Db_Expr 3257 | */ 3258 | public function getLeastSql(array $data) 3259 | { 3260 | return new \Zend_Db_Expr(sprintf('LEAST(%s)', implode(', ', $data))); 3261 | } 3262 | 3263 | /** 3264 | * Generate greatest SQL fragment 3265 | * 3266 | * Generate fragment of SQL, that compare with two or more arguments, and returns the largest 3267 | * (maximum-valued) argument 3268 | * All arguments in data must be quoted 3269 | * 3270 | * @param string[] $data 3271 | * @return \Zend_Db_Expr 3272 | */ 3273 | public function getGreatestSql(array $data) 3274 | { 3275 | return new \Zend_Db_Expr(sprintf('GREATEST(%s)', implode(', ', $data))); 3276 | } 3277 | 3278 | /** 3279 | * Get Interval Unit SQL fragment 3280 | * 3281 | * @param int $interval 3282 | * @param string $unit 3283 | * @return string 3284 | * @throws \Zend_Db_Exception 3285 | */ 3286 | protected function _getIntervalUnitSql($interval, $unit) 3287 | { 3288 | if (!isset($this->_intervalUnits[$unit])) { 3289 | throw new \Zend_Db_Exception(sprintf('Undefined interval unit "%s" specified', $unit)); 3290 | } 3291 | 3292 | return sprintf('INTERVAL %d %s', $interval, $this->_intervalUnits[$unit]); 3293 | } 3294 | 3295 | /** 3296 | * Add time values (intervals) to a date value 3297 | * 3298 | * @see INTERVAL_* constants for $unit 3299 | * 3300 | * @param \Zend_Db_Expr|string $date quoted field name or SQL statement 3301 | * @param int $interval 3302 | * @param string $unit 3303 | * @return \Zend_Db_Expr 3304 | */ 3305 | public function getDateAddSql($date, $interval, $unit) 3306 | { 3307 | $expr = sprintf('DATE_ADD(%s, %s)', $date, $this->_getIntervalUnitSql($interval, $unit)); 3308 | return new \Zend_Db_Expr($expr); 3309 | } 3310 | 3311 | /** 3312 | * Subtract time values (intervals) to a date value 3313 | * 3314 | * @see INTERVAL_* constants for $expr 3315 | * 3316 | * @param \Zend_Db_Expr|string $date quoted field name or SQL statement 3317 | * @param int|string $interval 3318 | * @param string $unit 3319 | * @return \Zend_Db_Expr 3320 | */ 3321 | public function getDateSubSql($date, $interval, $unit) 3322 | { 3323 | $expr = sprintf('DATE_SUB(%s, %s)', $date, $this->_getIntervalUnitSql($interval, $unit)); 3324 | return new \Zend_Db_Expr($expr); 3325 | } 3326 | 3327 | /** 3328 | * Format date as specified 3329 | * 3330 | * Supported format Specifier 3331 | * 3332 | * %H Hour (00..23) 3333 | * %i Minutes, numeric (00..59) 3334 | * %s Seconds (00..59) 3335 | * %d Day of the month, numeric (00..31) 3336 | * %m Month, numeric (00..12) 3337 | * %Y Year, numeric, four digits 3338 | * 3339 | * @param string $date quoted date value or non quoted SQL statement(field) 3340 | * @param string $format 3341 | * @return \Zend_Db_Expr 3342 | */ 3343 | public function getDateFormatSql($date, $format) 3344 | { 3345 | $expr = sprintf("DATE_FORMAT(%s, '%s')", $date, $format); 3346 | return new \Zend_Db_Expr($expr); 3347 | } 3348 | 3349 | /** 3350 | * Extract the date part of a date or datetime expression 3351 | * 3352 | * @param \Zend_Db_Expr|string $date quoted field name or SQL statement 3353 | * @return \Zend_Db_Expr 3354 | */ 3355 | public function getDatePartSql($date) 3356 | { 3357 | return new \Zend_Db_Expr(sprintf('DATE(%s)', $date)); 3358 | } 3359 | 3360 | /** 3361 | * Prepare substring sql function 3362 | * 3363 | * @param \Zend_Db_Expr|string $stringExpression quoted field name or SQL statement 3364 | * @param int|string|\Zend_Db_Expr $pos 3365 | * @param int|string|\Zend_Db_Expr|null $len 3366 | * @return \Zend_Db_Expr 3367 | */ 3368 | public function getSubstringSql($stringExpression, $pos, $len = null) 3369 | { 3370 | if ($len === null) { 3371 | return new \Zend_Db_Expr(sprintf('SUBSTRING(%s, %s)', $stringExpression, $pos)); 3372 | } 3373 | return new \Zend_Db_Expr(sprintf('SUBSTRING(%s, %s, %s)', $stringExpression, $pos, $len)); 3374 | } 3375 | 3376 | /** 3377 | * Prepare standard deviation sql function 3378 | * 3379 | * @param \Zend_Db_Expr|string $expressionField quoted field name or SQL statement 3380 | * @return \Zend_Db_Expr 3381 | */ 3382 | public function getStandardDeviationSql($expressionField) 3383 | { 3384 | return new \Zend_Db_Expr(sprintf('STDDEV_SAMP(%s)', $expressionField)); 3385 | } 3386 | 3387 | /** 3388 | * Extract part of a date 3389 | * 3390 | * @see INTERVAL_* constants for $unit 3391 | * 3392 | * @param \Zend_Db_Expr|string $date quoted field name or SQL statement 3393 | * @param string $unit 3394 | * @return \Zend_Db_Expr 3395 | * @throws \Zend_Db_Exception 3396 | */ 3397 | public function getDateExtractSql($date, $unit) 3398 | { 3399 | if (!isset($this->_intervalUnits[$unit])) { 3400 | throw new \Zend_Db_Exception(sprintf('Undefined interval unit "%s" specified', $unit)); 3401 | } 3402 | 3403 | $expr = sprintf('EXTRACT(%s FROM %s)', $this->_intervalUnits[$unit], $date); 3404 | return new \Zend_Db_Expr($expr); 3405 | } 3406 | 3407 | /** 3408 | * Returns a compressed version of the table name if it is too long 3409 | * 3410 | * @param string $tableName 3411 | * @return string 3412 | * @codeCoverageIgnore 3413 | */ 3414 | public function getTableName($tableName) 3415 | { 3416 | return ExpressionConverter::shortenEntityName($tableName, 't_'); 3417 | } 3418 | 3419 | /** 3420 | * Build a trigger name based on table name and trigger details 3421 | * 3422 | * @param string $tableName The table which is the subject of the trigger 3423 | * @param string $time Either "before" or "after" 3424 | * @param string $event The DB level event which activates the trigger, i.e. "update" or "insert" 3425 | * @return string 3426 | * @codeCoverageIgnore 3427 | */ 3428 | public function getTriggerName($tableName, $time, $event) 3429 | { 3430 | $triggerName = 'trg_' . $tableName . '_' . $time . '_' . $event; 3431 | return ExpressionConverter::shortenEntityName($triggerName, 'trg_'); 3432 | } 3433 | 3434 | /** 3435 | * Retrieve valid index name 3436 | * 3437 | * Check index name length and allowed symbols 3438 | * 3439 | * @param string $tableName 3440 | * @param string|string[] $fields the columns list 3441 | * @param string $indexType 3442 | * @return string 3443 | */ 3444 | public function getIndexName($tableName, $fields, $indexType = '') 3445 | { 3446 | if (is_array($fields)) { 3447 | $fields = implode('_', $fields); 3448 | } 3449 | 3450 | switch (strtolower($indexType)) { 3451 | case AdapterInterface::INDEX_TYPE_UNIQUE: 3452 | $prefix = 'unq_'; 3453 | break; 3454 | case AdapterInterface::INDEX_TYPE_FULLTEXT: 3455 | $prefix = 'fti_'; 3456 | break; 3457 | case AdapterInterface::INDEX_TYPE_INDEX: 3458 | default: 3459 | $prefix = 'idx_'; 3460 | } 3461 | return strtoupper(ExpressionConverter::shortenEntityName($tableName . '_' . $fields, $prefix)); 3462 | } 3463 | 3464 | /** 3465 | * Retrieve valid foreign key name 3466 | * 3467 | * Check foreign key name length and allowed symbols 3468 | * 3469 | * @param string $priTableName 3470 | * @param string $priColumnName 3471 | * @param string $refTableName 3472 | * @param string $refColumnName 3473 | * @return string 3474 | * @codeCoverageIgnore 3475 | */ 3476 | public function getForeignKeyName($priTableName, $priColumnName, $refTableName, $refColumnName) 3477 | { 3478 | $fkName = sprintf('%s_%s_%s_%s', $priTableName, $priColumnName, $refTableName, $refColumnName); 3479 | return strtoupper(ExpressionConverter::shortenEntityName($fkName, 'fk_')); 3480 | } 3481 | 3482 | /** 3483 | * Stop updating indexes 3484 | * 3485 | * @param string $tableName 3486 | * @param string $schemaName 3487 | * @return $this 3488 | */ 3489 | public function disableTableKeys($tableName, $schemaName = null) 3490 | { 3491 | $tableName = $this->_getTableName($tableName, $schemaName); 3492 | $query = sprintf('ALTER TABLE %s DISABLE KEYS', $this->quoteIdentifier($tableName)); 3493 | $this->query($query); 3494 | 3495 | return $this; 3496 | } 3497 | 3498 | /** 3499 | * Re-create missing indexes 3500 | * 3501 | * @param string $tableName 3502 | * @param string $schemaName 3503 | * @return $this 3504 | */ 3505 | public function enableTableKeys($tableName, $schemaName = null) 3506 | { 3507 | $tableName = $this->_getTableName($tableName, $schemaName); 3508 | $query = sprintf('ALTER TABLE %s ENABLE KEYS', $this->quoteIdentifier($tableName)); 3509 | $this->query($query); 3510 | 3511 | return $this; 3512 | } 3513 | 3514 | /** 3515 | * Get insert from Select object query 3516 | * 3517 | * @param Select $select 3518 | * @param string $table insert into table 3519 | * @param array $fields 3520 | * @param int|false $mode 3521 | * @return string 3522 | */ 3523 | public function insertFromSelect(Select $select, $table, array $fields = [], $mode = false) 3524 | { 3525 | $query = $mode === self::REPLACE ? 'REPLACE' : 'INSERT'; 3526 | 3527 | if ($mode === self::INSERT_IGNORE) { 3528 | $query .= ' IGNORE'; 3529 | } 3530 | $query = sprintf('%s INTO %s', $query, $this->quoteIdentifier($table)); 3531 | if ($fields) { 3532 | $columns = array_map([$this, 'quoteIdentifier'], $fields); 3533 | $query = sprintf('%s (%s)', $query, join(', ', $columns)); 3534 | } 3535 | 3536 | $query = sprintf('%s %s', $query, $select->assemble()); 3537 | 3538 | if ($mode === self::INSERT_ON_DUPLICATE) { 3539 | $query .= $this->renderOnDuplicate($table, $fields); 3540 | } 3541 | 3542 | return $query; 3543 | } 3544 | 3545 | /** 3546 | * Render On Duplicate query part 3547 | * 3548 | * @param string $table 3549 | * @param array $fields 3550 | * @return string 3551 | */ 3552 | private function renderOnDuplicate($table, array $fields) 3553 | { 3554 | if (!$fields) { 3555 | $describe = $this->describeTable($table); 3556 | foreach ($describe as $column) { 3557 | if ($column['PRIMARY'] === false) { 3558 | $fields[] = $column['COLUMN_NAME']; 3559 | } 3560 | } 3561 | } 3562 | $update = []; 3563 | foreach ($fields as $field) { 3564 | $update[] = sprintf('%1$s = VALUES(%1$s)', $this->quoteIdentifier($field)); 3565 | } 3566 | 3567 | return count($update) ? ' ON DUPLICATE KEY UPDATE ' . join(', ', $update) : ''; 3568 | } 3569 | 3570 | /** 3571 | * Get insert queries in array for insert by range with step parameter 3572 | * 3573 | * @param string $rangeField 3574 | * @param \Magento\Framework\DB\Select $select 3575 | * @param int $stepCount 3576 | * @return \Magento\Framework\DB\Select[] 3577 | * @throws LocalizedException 3578 | * @deprecated 100.1.3 3579 | */ 3580 | public function selectsByRange($rangeField, \Magento\Framework\DB\Select $select, $stepCount = 100) 3581 | { 3582 | $iterator = $this->getQueryGenerator()->generate($rangeField, $select, $stepCount); 3583 | $queries = []; 3584 | foreach ($iterator as $query) { 3585 | $queries[] = $query; 3586 | } 3587 | return $queries; 3588 | } 3589 | 3590 | /** 3591 | * Get query generator 3592 | * 3593 | * @return QueryGenerator 3594 | * @deprecated 100.1.3 3595 | */ 3596 | private function getQueryGenerator() 3597 | { 3598 | if ($this->queryGenerator === null) { 3599 | $this->queryGenerator = \Magento\Framework\App\ObjectManager::getInstance()->create(QueryGenerator::class); 3600 | } 3601 | return $this->queryGenerator; 3602 | } 3603 | 3604 | /** 3605 | * Get update table query using select object for join and update 3606 | * 3607 | * @param Select $select 3608 | * @param string|array $table 3609 | * @return string 3610 | * @throws LocalizedException 3611 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 3612 | * @SuppressWarnings(PHPMD.NPathComplexity) 3613 | */ 3614 | public function updateFromSelect(Select $select, $table) 3615 | { 3616 | if (!is_array($table)) { 3617 | $table = [$table => $table]; 3618 | } 3619 | 3620 | // get table name and alias 3621 | $keys = array_keys($table); 3622 | $tableAlias = $keys[0]; 3623 | $tableName = $table[$keys[0]]; 3624 | 3625 | $query = sprintf('UPDATE %s', $this->quoteTableAs($tableName, $tableAlias)); 3626 | 3627 | // render JOIN conditions (FROM Part) 3628 | $joinConds = []; 3629 | foreach ($select->getPart(\Magento\Framework\DB\Select::FROM) as $correlationName => $joinProp) { 3630 | if ($joinProp['joinType'] == \Magento\Framework\DB\Select::FROM) { 3631 | $joinType = strtoupper(\Magento\Framework\DB\Select::INNER_JOIN); 3632 | } else { 3633 | $joinType = strtoupper($joinProp['joinType']); 3634 | } 3635 | $joinTable = ''; 3636 | if ($joinProp['schema'] !== null) { 3637 | $joinTable = sprintf('%s.', $this->quoteIdentifier($joinProp['schema'])); 3638 | } 3639 | $joinTable .= $this->quoteTableAs($joinProp['tableName'], $correlationName); 3640 | 3641 | $join = sprintf(' %s %s', $joinType, $joinTable); 3642 | 3643 | if (!empty($joinProp['joinCondition'])) { 3644 | $join = sprintf('%s ON %s', $join, $joinProp['joinCondition']); 3645 | } 3646 | 3647 | $joinConds[] = $join; 3648 | } 3649 | 3650 | if ($joinConds) { 3651 | $query = sprintf("%s\n%s", $query, implode("\n", $joinConds)); 3652 | } 3653 | 3654 | // render UPDATE SET 3655 | $columns = []; 3656 | foreach ($select->getPart(\Magento\Framework\DB\Select::COLUMNS) as $columnEntry) { 3657 | list($correlationName, $column, $alias) = $columnEntry; 3658 | if (empty($alias)) { 3659 | $alias = $column; 3660 | } 3661 | if (!$column instanceof \Zend_Db_Expr && !empty($correlationName)) { 3662 | $column = $this->quoteIdentifier([$correlationName, $column]); 3663 | } 3664 | $columns[] = sprintf('%s = %s', $this->quoteIdentifier([$tableAlias, $alias]), $column); 3665 | } 3666 | 3667 | if (!$columns) { 3668 | throw new LocalizedException( 3669 | new \Magento\Framework\Phrase('The columns for UPDATE statement are not defined') 3670 | ); 3671 | } 3672 | 3673 | $query = sprintf("%s\nSET %s", $query, implode(', ', $columns)); 3674 | 3675 | // render WHERE 3676 | $wherePart = $select->getPart(\Magento\Framework\DB\Select::WHERE); 3677 | if ($wherePart) { 3678 | $query = sprintf("%s\nWHERE %s", $query, implode(' ', $wherePart)); 3679 | } 3680 | 3681 | return $query; 3682 | } 3683 | 3684 | /** 3685 | * Get delete from select object query 3686 | * 3687 | * @param Select $select 3688 | * @param string $table the table name or alias used in select 3689 | * @return string 3690 | */ 3691 | public function deleteFromSelect(Select $select, $table) 3692 | { 3693 | $select = clone $select; 3694 | $select->reset(\Magento\Framework\DB\Select::DISTINCT); 3695 | $select->reset(\Magento\Framework\DB\Select::COLUMNS); 3696 | 3697 | $query = sprintf('DELETE %s %s', $this->quoteIdentifier($table), $select->assemble()); 3698 | 3699 | return $query; 3700 | } 3701 | 3702 | /** 3703 | * Calculate checksum for table or for group of tables 3704 | * 3705 | * @param array|string $tableNames array of tables names | table name 3706 | * @param string $schemaName schema name 3707 | * @return array 3708 | */ 3709 | public function getTablesChecksum($tableNames, $schemaName = null) 3710 | { 3711 | $result = []; 3712 | $tableNames = is_array($tableNames) ? $tableNames : [$tableNames]; 3713 | 3714 | foreach ($tableNames as $tableName) { 3715 | $query = 'CHECKSUM TABLE ' . $this->_getTableName($tableName, $schemaName); 3716 | $checkSumArray = $this->fetchRow($query); 3717 | $result[$tableName] = $checkSumArray['Checksum']; 3718 | } 3719 | 3720 | return $result; 3721 | } 3722 | 3723 | /** 3724 | * Check if the database support STRAIGHT JOIN 3725 | * 3726 | * @return true 3727 | */ 3728 | public function supportStraightJoin() 3729 | { 3730 | return true; 3731 | } 3732 | 3733 | /** 3734 | * Adds order by random to select object 3735 | * 3736 | * Possible using integer field for optimization 3737 | * 3738 | * @param Select $select 3739 | * @param string $field 3740 | * @return $this 3741 | */ 3742 | public function orderRand(Select $select, $field = null) 3743 | { 3744 | if ($field !== null) { 3745 | $expression = new \Zend_Db_Expr(sprintf('RAND() * %s', $this->quoteIdentifier($field))); 3746 | $select->columns(['mage_rand' => $expression]); 3747 | $spec = new \Zend_Db_Expr('mage_rand'); 3748 | } else { 3749 | $spec = new \Zend_Db_Expr('RAND()'); 3750 | } 3751 | $select->order($spec); 3752 | 3753 | return $this; 3754 | } 3755 | 3756 | /** 3757 | * Render SQL FOR UPDATE clause 3758 | * 3759 | * @param string $sql 3760 | * @return string 3761 | */ 3762 | public function forUpdate($sql) 3763 | { 3764 | return sprintf('%s FOR UPDATE', $sql); 3765 | } 3766 | 3767 | /** 3768 | * Prepare insert data 3769 | * 3770 | * @param mixed $row 3771 | * @param array $bind 3772 | * @return string 3773 | */ 3774 | protected function _prepareInsertData($row, &$bind) 3775 | { 3776 | $row = (array)$row; 3777 | $line = []; 3778 | foreach ($row as $value) { 3779 | if ($value instanceof \Zend_Db_Expr) { 3780 | $line[] = $value->__toString(); 3781 | } else { 3782 | $line[] = '?'; 3783 | $bind[] = $value; 3784 | } 3785 | } 3786 | $line = implode(', ', $line); 3787 | 3788 | return sprintf('(%s)', $line); 3789 | } 3790 | 3791 | /** 3792 | * Return insert sql query 3793 | * 3794 | * @param string $tableName 3795 | * @param array $columns 3796 | * @param array $values 3797 | * @param null|int $strategy 3798 | * @return string 3799 | */ 3800 | protected function _getInsertSqlQuery($tableName, array $columns, array $values, $strategy = null) 3801 | { 3802 | $tableName = $this->quoteIdentifier($tableName, true); 3803 | $columns = array_map([$this, 'quoteIdentifier'], $columns); 3804 | $columns = implode(',', $columns); 3805 | $values = implode(', ', $values); 3806 | $strategy = $strategy === self::INSERT_IGNORE ? 'IGNORE' : ''; 3807 | 3808 | $insertSql = sprintf('INSERT %s INTO %s (%s) VALUES %s', $strategy, $tableName, $columns, $values); 3809 | 3810 | return $insertSql; 3811 | } 3812 | 3813 | /** 3814 | * Return replace sql query 3815 | * 3816 | * @param string $tableName 3817 | * @param array $columns 3818 | * @param array $values 3819 | * @return string 3820 | * @since 101.0.0 3821 | */ 3822 | protected function _getReplaceSqlQuery($tableName, array $columns, array $values) 3823 | { 3824 | $tableName = $this->quoteIdentifier($tableName, true); 3825 | $columns = array_map([$this, 'quoteIdentifier'], $columns); 3826 | $columns = implode(',', $columns); 3827 | $values = implode(', ', $values); 3828 | 3829 | $replaceSql = sprintf('REPLACE INTO %s (%s) VALUES %s', $tableName, $columns, $values); 3830 | 3831 | return $replaceSql; 3832 | } 3833 | 3834 | /** 3835 | * Return ddl type 3836 | * 3837 | * @param array $options 3838 | * @return string 3839 | */ 3840 | protected function _getDdlType($options) 3841 | { 3842 | $ddlType = null; 3843 | if (isset($options['TYPE'])) { 3844 | $ddlType = $options['TYPE']; 3845 | } elseif (isset($options['COLUMN_TYPE'])) { 3846 | $ddlType = $options['COLUMN_TYPE']; 3847 | } 3848 | 3849 | return $ddlType; 3850 | } 3851 | 3852 | /** 3853 | * Return DDL action 3854 | * 3855 | * @param string $action 3856 | * @return string 3857 | */ 3858 | protected function _getDdlAction($action) 3859 | { 3860 | switch ($action) { 3861 | case AdapterInterface::FK_ACTION_CASCADE: 3862 | return Table::ACTION_CASCADE; 3863 | case AdapterInterface::FK_ACTION_SET_NULL: 3864 | return Table::ACTION_SET_NULL; 3865 | case AdapterInterface::FK_ACTION_RESTRICT: 3866 | return Table::ACTION_RESTRICT; 3867 | default: 3868 | return Table::ACTION_NO_ACTION; 3869 | } 3870 | } 3871 | 3872 | /** 3873 | * Prepare sql date condition 3874 | * 3875 | * @param array $condition 3876 | * @param string $key 3877 | * @return string 3878 | */ 3879 | protected function _prepareSqlDateCondition($condition, $key) 3880 | { 3881 | if (empty($condition['date'])) { 3882 | if (empty($condition['datetime'])) { 3883 | $result = $condition[$key]; 3884 | } else { 3885 | $result = $this->formatDate($condition[$key]); 3886 | } 3887 | } else { 3888 | $result = $this->formatDate($condition[$key]); 3889 | } 3890 | 3891 | return $result; 3892 | } 3893 | 3894 | /** 3895 | * Try to find installed primary key name, if not - formate new one. 3896 | * 3897 | * @param string $tableName Table name 3898 | * @param string $schemaName OPTIONAL 3899 | * @return string Primary Key name 3900 | */ 3901 | public function getPrimaryKeyName($tableName, $schemaName = null) 3902 | { 3903 | $indexes = $this->getIndexList($tableName, $schemaName); 3904 | if (isset($indexes['PRIMARY'])) { 3905 | return $indexes['PRIMARY']['KEY_NAME']; 3906 | } else { 3907 | return 'PK_' . strtoupper($tableName); 3908 | } 3909 | } 3910 | 3911 | /** 3912 | * Parse text size 3913 | * 3914 | * Returns max allowed size if value great it 3915 | * 3916 | * @param string|int $size 3917 | * @return int 3918 | */ 3919 | protected function _parseTextSize($size) 3920 | { 3921 | $size = trim($size); 3922 | $last = strtolower(substr($size, -1)); 3923 | 3924 | switch ($last) { 3925 | case 'k': 3926 | $size = (int)$size * 1024; 3927 | break; 3928 | case 'm': 3929 | $size = (int)$size * 1024 * 1024; 3930 | break; 3931 | case 'g': 3932 | $size = (int)$size * 1024 * 1024 * 1024; 3933 | break; 3934 | } 3935 | 3936 | if (empty($size)) { 3937 | return Table::DEFAULT_TEXT_SIZE; 3938 | } 3939 | if ($size >= Table::MAX_TEXT_SIZE) { 3940 | return Table::MAX_TEXT_SIZE; 3941 | } 3942 | 3943 | return (int)$size; 3944 | } 3945 | 3946 | /** 3947 | * Converts fetched blob into raw binary PHP data. 3948 | * 3949 | * The MySQL drivers do it nice, no processing required. 3950 | * 3951 | * @param mixed $value 3952 | * @return mixed 3953 | */ 3954 | public function decodeVarbinary($value) 3955 | { 3956 | return $value; 3957 | } 3958 | 3959 | /** 3960 | * Create trigger 3961 | * 3962 | * @param \Magento\Framework\DB\Ddl\Trigger $trigger 3963 | * @throws \Zend_Db_Exception 3964 | * @return \Zend_Db_Statement_Pdo 3965 | */ 3966 | public function createTrigger(\Magento\Framework\DB\Ddl\Trigger $trigger) 3967 | { 3968 | if (!$trigger->getStatements()) { 3969 | throw new \Zend_Db_Exception( 3970 | (string)new \Magento\Framework\Phrase( 3971 | 'Trigger %1 has not statements available', 3972 | [$trigger->getName()] 3973 | ) 3974 | ); 3975 | } 3976 | 3977 | $statements = implode("\n", $trigger->getStatements()); 3978 | 3979 | $sql = sprintf( 3980 | "CREATE TRIGGER %s %s %s ON %s FOR EACH ROW\nBEGIN\n%s\nEND", 3981 | $trigger->getName(), 3982 | $trigger->getTime(), 3983 | $trigger->getEvent(), 3984 | $trigger->getTable(), 3985 | $statements 3986 | ); 3987 | 3988 | return $this->multiQuery($sql); 3989 | } 3990 | 3991 | /** 3992 | * Drop trigger from database 3993 | * 3994 | * @param string $triggerName 3995 | * @param string|null $schemaName 3996 | * @return bool 3997 | * @throws \InvalidArgumentException 3998 | */ 3999 | public function dropTrigger($triggerName, $schemaName = null) 4000 | { 4001 | if (empty($triggerName)) { 4002 | throw new \InvalidArgumentException((string)new \Magento\Framework\Phrase('Trigger name is not defined')); 4003 | } 4004 | 4005 | $triggerName = ($schemaName ? $schemaName . '.' : '') . $triggerName; 4006 | 4007 | $sql = 'DROP TRIGGER IF EXISTS ' . $this->quoteIdentifier($triggerName); 4008 | $this->query($sql); 4009 | 4010 | return true; 4011 | } 4012 | 4013 | /** 4014 | * Check if all transactions have been committed 4015 | * 4016 | * @return void 4017 | */ 4018 | public function __destruct() 4019 | { 4020 | if ($this->_transactionLevel > 0) { 4021 | trigger_error('Some transactions have not been committed or rolled back', E_USER_ERROR); 4022 | } 4023 | } 4024 | 4025 | /** 4026 | * Retrieve tables list 4027 | * 4028 | * @param null|string $likeCondition 4029 | * @return array 4030 | */ 4031 | public function getTables($likeCondition = null) 4032 | { 4033 | $sql = ($likeCondition === null) ? 'SHOW TABLES' : sprintf("SHOW TABLES LIKE '%s'", $likeCondition); 4034 | $result = $this->query($sql); 4035 | $tables = []; 4036 | while ($row = $result->fetchColumn()) { 4037 | $tables[] = $row; 4038 | } 4039 | return $tables; 4040 | } 4041 | 4042 | /** 4043 | * Returns auto increment field if exists 4044 | * 4045 | * @param string $tableName 4046 | * @param string|null $schemaName 4047 | * @return string|bool 4048 | * @since 100.1.0 4049 | */ 4050 | public function getAutoIncrementField($tableName, $schemaName = null) 4051 | { 4052 | $indexName = $this->getPrimaryKeyName($tableName, $schemaName); 4053 | $indexes = $this->getIndexList($tableName); 4054 | if ($indexName && count($indexes[$indexName]['COLUMNS_LIST']) == 1) { 4055 | return current($indexes[$indexName]['COLUMNS_LIST']); 4056 | } 4057 | return false; 4058 | } 4059 | 4060 | /** 4061 | * Get schema Listener. 4062 | * 4063 | * Required to listen all DDL changes done by 3-rd party modules with old Install/UpgradeSchema scripts. 4064 | * 4065 | * @return SchemaListener 4066 | * @since 102.0.0 4067 | */ 4068 | public function getSchemaListener() 4069 | { 4070 | if ($this->schemaListener === null) { 4071 | $this->schemaListener = \Magento\Framework\App\ObjectManager::getInstance()->create(SchemaListener::class); 4072 | } 4073 | return $this->schemaListener; 4074 | } 4075 | } 4076 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Open Software License ("OSL") v. 3.0 3 | 4 | This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: 5 | 6 | Licensed under the Open Software License version 3.0 7 | 8 | 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: 9 | 10 | 1. to reproduce the Original Work in copies, either alone or as part of a collective work; 11 | 12 | 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; 13 | 14 | 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; 15 | 16 | 4. to perform the Original Work publicly; and 17 | 18 | 5. to display the Original Work publicly. 19 | 20 | 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. 21 | 22 | 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. 23 | 24 | 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. 25 | 26 | 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). 27 | 28 | 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. 29 | 30 | 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. 31 | 32 | 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. 33 | 34 | 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). 35 | 36 | 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. 37 | 38 | 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. 39 | 40 | 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. 41 | 42 | 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. 43 | 44 | 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 45 | 46 | 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. 47 | 48 | 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # magento2-deadlockRetry 2 | Module to retry Deadlock transactions in Magento 2 3 | 4 | See the full readme here: https://www.cadence-labs.com/2018/09/magento-2-deadlock-retry-module/ 5 | -------------------------------------------------------------------------------- /Setup/UpgradeSchema.php: -------------------------------------------------------------------------------- 1 | startSetup(); 20 | 21 | $quoteAddressTable = 'quote_address'; 22 | $quoteTable = 'quote'; 23 | $orderTable = 'sales_order'; 24 | $invoiceTable = 'sales_invoice'; 25 | $creditmemoTable = 'sales_creditmemo'; 26 | 27 | /** 28 | * @see https://www.xaprb.com/blog/2006/08/08/how-to-deliberately-cause-a-deadlock-in-mysql/ 29 | * This is a quick version of that article's suggested implementation 30 | */ 31 | $setup->getConnection()->query( 32 | "create table innodb_deadlock_maker(a int primary key) engine=innodb;" 33 | ); 34 | 35 | $setup->getConnection()->query( 36 | "insert into innodb_deadlock_maker(a) values(0), (1);" 37 | ); 38 | 39 | $setup->endSetup(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /etc/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Cadence\DeadlockRetry\Console\Command\Simulate 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2017 Oleg Koval 8 | * @license OSL-3.0, AFL-3.0 9 | */ 10 | \Magento\Framework\Component\ComponentRegistrar::register( 11 | \Magento\Framework\Component\ComponentRegistrar::MODULE, 12 | 'Cadence_DeadlockRetry', 13 | __DIR__ 14 | ); 15 | --------------------------------------------------------------------------------