Current Path : /var/www/www-root/data/webdav/www/www.monolith-realty.ru/bitrix/modules/main/lib/db/ |
Current File : /var/www/www-root/data/webdav/www/www.monolith-realty.ru/bitrix/modules/main/lib/db/connection.php |
<?php namespace Bitrix\Main\DB; use Bitrix\Main; use Bitrix\Main\ArgumentNullException; use Bitrix\Main\Data; use Bitrix\Main\Diag; use Bitrix\Main\ORM\Fields\ScalarField; /** * Class Connection * * Base abstract class for database connections. * @package Bitrix\Main\DB */ abstract class Connection extends Data\Connection { /** @var MysqliSqlHelper | PgsqlSqlHelper */ protected $sqlHelper; /** @var Diag\SqlTracker */ protected $sqlTracker; protected $trackSql = false; protected $version; protected $versionExpress; protected $host; protected $database; protected $login; protected $password; protected $initCommand = 0; protected $options = 0; protected $nodeId = 0; protected $utf8mb4 = array(); protected $tableColumnsCache = array(); protected $lastQueryResult; /** * @var bool Flag for static::query - if needed to execute query or just to collect it * @see $disabledQueryExecutingDump */ protected $queryExecutingEnabled = true; /** @var null|string[] Queries that were collected while Query Executing was Disabled */ protected $disabledQueryExecutingDump; const PERSISTENT = 1; const DEFERRED = 2; const INDEX_UNIQUE = 'UNIQUE'; const INDEX_FULLTEXT = 'FULLTEXT'; const INDEX_SPATIAL = 'SPATIAL'; /** * $configuration may contain following keys: * <ul> * <li>host * <li>database * <li>login * <li>password * <li>initCommand * <li>options * </ul> * * @param array $configuration Array of Name => Value pairs. */ public function __construct(array $configuration) { parent::__construct($configuration); $this->host = $configuration['host'] ?? ''; $this->database = $configuration['database'] ?? ''; $this->login = $configuration['login'] ?? ''; $this->password = $configuration['password'] ?? ''; $this->initCommand = $configuration['initCommand'] ?? ''; $this->options = intval($configuration['options'] ?? 2); $this->utf8mb4 = (isset($configuration['utf8mb4']) && is_array($configuration['utf8mb4'])? $configuration['utf8mb4'] : []); } /** * @deprecated Use getDatabase() * @return string */ public function getDbName() { return $this->getDatabase(); } /** * Returns database host. * * @return string */ public function getHost() { return $this->host; } /** * Returns database login. * * @return string */ public function getLogin() { return $this->login; } /** * Returns database password. * * @return string */ public function getPassword() { return $this->password; } /** * Returns database name. * * @return string */ public function getDatabase() { return $this->database; } /** * Temporary disables query executing. All queries being collected in disabledQueryExecutingDump * * @api * @see enableQueryExecuting * @see getDisabledQueryExecutingDump * * @return void */ public function disableQueryExecuting() { $this->queryExecutingEnabled = false; } /** * Enables query executing after it has been temporary disabled * * @api * @see disableQueryExecuting * * @return void */ public function enableQueryExecuting() { $this->queryExecutingEnabled = true; } /** * @api * @see disableQueryExecuting * * @return bool */ public function isQueryExecutingEnabled() { return $this->queryExecutingEnabled; } /** * Returns queries that were collected while Query Executing was disabled and clears the dump. * * @api * @see disableQueryExecuting * * @return null|string[] */ public function getDisabledQueryExecutingDump() { $dump = $this->disabledQueryExecutingDump; $this->disabledQueryExecutingDump = null; return $dump; } /********************************************************** * SqlHelper **********************************************************/ abstract protected function createSqlHelper(); /** * Returns database-depended SqlHelper object. * Creates new one on the first call per Connection object instance. * * @return MysqliSqlHelper | PgsqlSqlHelper */ public function getSqlHelper() { if ($this->sqlHelper == null) { $this->sqlHelper = $this->createSqlHelper(); } return $this->sqlHelper; } /*********************************************************** * Connection and disconnection ***********************************************************/ /** * Connects to the database. * * @return void */ public function connect() { $this->isConnected = false; if (!$this->isDeferred()) { parent::connect(); } } /** * Disconnects from the database. * * @return void */ public function disconnect() { if (!$this->isPersistent()) { parent::disconnect(); } } /** * Returns true if the connection is deferred. * @return bool */ public function isDeferred() { return (($this->options & self::DEFERRED) !== 0); } /** * Returns true if the connection is persistent. * @return bool */ public function isPersistent() { return (($this->options & self::PERSISTENT) !== 0); } /********************************************************* * Query *********************************************************/ /** * Executes a query against connected database. * Rises SqlQueryException on any database error. * <p> * When object $trackerQuery passed then calls its startQuery and finishQuery * methods before and after query execution. * * @param string $sql Sql query. * @param array|null $binds Array of binds. * @param Diag\SqlTrackerQuery|null $trackerQuery Debug collector object. * * @return resource * @throws SqlQueryException */ abstract protected function queryInternal($sql, array $binds = null, Diag\SqlTrackerQuery $trackerQuery = null); /** * Returns database-depended result of the query. * * @param resource $result Result of internal query function. * @param Diag\SqlTrackerQuery|null $trackerQuery Debug collector object. * * @return Result */ abstract protected function createResult($result, Diag\SqlTrackerQuery $trackerQuery = null); /** * Executes a query to the database. * * - query($sql) * - query($sql, $limit) * - query($sql, $offset, $limit) * - query($sql, $binds) * - query($sql, $binds, $limit) * - query($sql, $binds, $offset, $limit) * * @param string $sql Sql query. * @param array $binds Array of binds. * @param int $offset Offset the of the first row to return, starting from 0. * @param int $limit Limit rows count. * * @return Result * @throws SqlQueryException */ public function query($sql) { list($sql, $binds, $offset, $limit) = self::parseQueryFunctionArgs(func_get_args()); if($limit > 0) { $sql = $this->getSqlHelper()->getTopSql($sql, $limit, $offset); } $trackerQuery = null; if ($this->queryExecutingEnabled) { $connection = Main\Application::getInstance()->getConnectionPool()->getSlaveConnection($sql); if($connection === null) { $connection = $this; } if ($this->trackSql) { $trackerQuery = $this->sqlTracker->getNewTrackerQuery(); $trackerQuery->setNode($connection->getNodeId()); } $result = $connection->queryInternal($sql, $binds, $trackerQuery); } else { if ($this->disabledQueryExecutingDump === null) { $this->disabledQueryExecutingDump = array(); } $this->disabledQueryExecutingDump[] = $sql; $result = true; } return $this->createResult($result, $trackerQuery); } /** * Executes a query, fetches a row and returns single field value * from the first column of the result. * * @param string $sql Sql text. * @param array|null $binds Binding array. * * @return string|null * @throws SqlQueryException */ public function queryScalar($sql, array $binds = null) { $result = $this->query($sql, $binds, 0, 1); if ($row = $result->fetch()) { return array_shift($row); } return null; } /** * Executes a query without returning result, i.e. INSERT, UPDATE, DELETE * * @param string $sql Sql text. * @param array|null $binds Binding array. * * @return void * @throws SqlQueryException */ public function queryExecute($sql, array $binds = null) { $this->query($sql, $binds); } /** * Helper function for parameters handling. * * @param mixed $args Variable list of parameters. * * @return array * @throws ArgumentNullException */ protected static function parseQueryFunctionArgs($args) { /* * query($sql) * query($sql, $limit) * query($sql, $offset, $limit) * query($sql, $arBinds) * query($sql, $arBinds, $limit) * query($sql, $arBinds, $offset, $limit) */ $numArgs = count($args); if ($numArgs < 1) throw new ArgumentNullException("sql"); $binds = array(); $offset = 0; $limit = 0; if ($numArgs == 1) { $sql = $args[0]; } elseif ($numArgs == 2) { if (is_array($args[1])) list($sql, $binds) = $args; else list($sql, $limit) = $args; } elseif ($numArgs == 3) { if (is_array($args[1])) list($sql, $binds, $limit) = $args; else list($sql, $offset, $limit) = $args; } else { list($sql, $binds, $offset, $limit) = $args; } return array($sql, $binds, $offset, $limit); } /** * Adds row to table and returns ID of the added row. * <p> * $identity parameter must be null when table does not have autoincrement column. * * @param string $tableName Name of the table for insertion of new row. * @param array $data Array of columnName => Value pairs. * @param string $identity For Oracle only. * * @return integer * @throws SqlQueryException */ public function add($tableName, array $data, $identity = "ID") { $insert = $this->getSqlHelper()->prepareInsert($tableName, $data); $sql = "INSERT INTO ".$this->getSqlHelper()->quote($tableName)."(".$insert[0].") ". "VALUES (".$insert[1].")"; $this->queryExecute($sql); return $this->getInsertedId(); } /** * @param string $tableName * @param array $rows * @param string $identity * * @return int * @throws SqlQueryException */ public function addMulti($tableName, $rows, $identity = "ID") { $uniqueColumns = []; $inserts = []; // prepare data foreach ($rows as $data) { $insert = $this->getSqlHelper()->prepareInsert($tableName, $data, true); $inserts[] = $insert; // and get unique column names foreach ($insert[0] as $column) { $uniqueColumns[$column] = true; } } // prepare sql $sqlValues = []; foreach ($inserts as $insert) { $columns = array_flip($insert[0]); $values = $insert[1]; $finalValues = []; foreach (array_keys($uniqueColumns) as $column) { if (array_key_exists($column, $columns)) { // set real value $finalValues[] = $values[$columns[$column]]; } else { // set default $finalValues[] = 'DEFAULT'; } } $sqlValues[] = '('.join(', ', $finalValues).')'; } $sql = "INSERT INTO {$this->getSqlHelper()->quote($tableName)} (".join(', ', array_keys($uniqueColumns)).") ". "VALUES ".join(', ', $sqlValues); $this->queryExecute($sql); return $this->getInsertedId(); } /** * @return integer */ abstract public function getInsertedId(); /** * Parses the string containing multiple queries and executes the queries one by one. * Queries delimiter depends on database type. * @see SqlHelper->getQueryDelimiter * * @param string $sqlBatch String with queries, separated by database-specific delimiters. * @param bool $stopOnError Whether return after the first error. * @return array Array of errors or empty array on success. */ public function executeSqlBatch($sqlBatch, $stopOnError = false) { $result = []; foreach ($this->parseSqlBatch($sqlBatch) as $sql) { try { $this->queryExecute($sql); } catch (SqlException $ex) { $result[] = $ex->getMessage(); if ($stopOnError) { return $result; } } } return $result; } /** * Parses the text containing sqls into separate queries. * * @param string $sqlBatch * @return array */ public function parseSqlBatch($sqlBatch) { $delimiter = $this->getSqlHelper()->getQueryDelimiter(); $sqlBatch = trim($sqlBatch); $statements = []; $sql = ""; do { if (preg_match("%^(.*?)(['\"`#]|--|\\$\\$|".$delimiter.")%is", $sqlBatch, $match)) { //Found string start if ($match[2] == "\"" || $match[2] == "'" || $match[2] == "`") { $sqlBatch = mb_substr($sqlBatch, mb_strlen($match[0])); $sql .= $match[0]; //find a quote not preceded by \ if (preg_match("%^(.*?)(?<!\\\\)".$match[2]."%s", $sqlBatch, $stringMatch)) { $sqlBatch = mb_substr($sqlBatch, mb_strlen($stringMatch[0])); $sql .= $stringMatch[0]; } else { //String foll beyond end of file $sql .= $sqlBatch; $sqlBatch = ""; } } //Comment found elseif ($match[2] == "#" || $match[2] == "--") { //Take that was before comment as part of sql $sqlBatch = mb_substr($sqlBatch, mb_strlen($match[1])); $sql .= $match[1]; //And cut the rest $p = mb_strpos($sqlBatch, "\n"); if ($p === false) { $p1 = mb_strpos($sqlBatch, "\r"); if ($p1 === false) { $sqlBatch = ""; } elseif ($p < $p1) { $sqlBatch = mb_substr($sqlBatch, $p); } else { $sqlBatch = mb_substr($sqlBatch, $p1); } } else { $sqlBatch = mb_substr($sqlBatch, $p); } } //$$ plpgsql body elseif ($match[2] == '$$') { //Take that was before delimiter as part of sql $sqlBatch = mb_substr($sqlBatch, mb_strlen($match[0])); //Including $$ $sql .= $match[0]; //Find closing $$ $p = mb_strpos($sqlBatch, '$$'); if ($p === false) { $sql .= $sqlBatch; $sqlBatch = ''; } else { $sql .= mb_substr($sqlBatch, 0, $p + 2); $sqlBatch = mb_substr($sqlBatch, $p + 2); } } //Delimiter! else { //Take that was before delimiter as part of sql $sqlBatch = mb_substr($sqlBatch, mb_strlen($match[0])); $sql .= $match[1]; //Delimiter must be followed by whitespace if (preg_match("%^[\n\r\t ]%", $sqlBatch)) { $sql = trim($sql); if (!empty($sql)) { $statements[] = str_replace("\r\n", "\n", $sql); $sql = ""; } } //It was not delimiter! elseif (!empty($sqlBatch)) { $sql .= $match[2]; } } } else //End of file is our delimiter { $sql .= $sqlBatch; $sqlBatch = ""; } } while (!empty($sqlBatch)); $sql = trim($sql, " \t\n\r"); if (!empty($sql)) { $statements[] = str_replace("\r\n", "\n", $sql); } return $statements; } /** * Returns affected rows count from last executed query. * * @return integer */ abstract public function getAffectedRowsCount(); /********************************************************* * DDL *********************************************************/ /** * Checks if a table exists. * * @param string $tableName The table name. * * @return boolean */ abstract public function isTableExists($tableName); /** * Checks if an index exists. * Actual columns in the index may differ from requested. * $columns may present a "prefix" of actual index columns. * * @param string $tableName A table name. * @param array $columns An array of columns in the index. * * @return boolean * @throws SqlQueryException */ abstract public function isIndexExists($tableName, array $columns); /** * Returns the name of an index. * * @param string $tableName A table name. * @param array $columns An array of columns in the index. * @param bool $strict The flag indicating that the columns in the index must exactly match the columns in the $arColumns parameter. * * @return string|null Name of the index or null if the index doesn't exist. */ abstract public function getIndexName($tableName, array $columns, $strict = false); /** * Returns fields objects according to the columns of a table. * Table must exist. * * @param string $tableName The table name. * * @return ScalarField[] An array of objects with columns information. * @throws SqlQueryException */ abstract public function getTableFields($tableName); /** * @param string $tableName Name of the new table. * @param ScalarField[] $fields Array with columns descriptions. * @param string[] $primary Array with primary key column names. * @param string[] $autoincrement Which columns will be auto incremented ones. * * @return void * @throws SqlQueryException */ abstract public function createTable($tableName, $fields, $primary = array(), $autoincrement = array()); /** * Creates primary index on column(s) * @api * * @param string $tableName Name of the table. * @param string|string[] $columnNames Name of the column or array of column names to be included into the index. * * @return Result * @throws SqlQueryException */ public function createPrimaryIndex($tableName, $columnNames) { if (!is_array($columnNames)) { $columnNames = array($columnNames); } foreach ($columnNames as &$columnName) { $columnName = $this->getSqlHelper()->quote($columnName); } $sql = 'ALTER TABLE '.$this->getSqlHelper()->quote($tableName).' ADD PRIMARY KEY('.join(', ', $columnNames).')'; return $this->query($sql); } /** * Creates index on column(s) * @api * * @param string $tableName Name of the table. * @param string $indexName Name of the new index. * @param string|string[] $columnNames Name of the column or array of column names to be included into the index. * * @return Result * @throws SqlQueryException */ public function createIndex($tableName, $indexName, $columnNames) { if (!is_array($columnNames)) { $columnNames = array($columnNames); } $sqlHelper = $this->getSqlHelper(); foreach ($columnNames as &$columnName) { $columnName = $sqlHelper->quote($columnName); } unset($columnName); $sql = 'CREATE INDEX '.$sqlHelper->quote($indexName).' ON '.$sqlHelper->quote($tableName).' ('.join(', ', $columnNames).')'; return $this->query($sql); } /** * Returns an object for the single column according to the column type. * * @param string $tableName Name of the table. * @param string $columnName Name of the column. * * @return ScalarField | null * @throws SqlQueryException */ public function getTableField($tableName, $columnName) { $tableFields = $this->getTableFields($tableName); return ($tableFields[$columnName] ?? null); } /** * Truncates all table data. * * @param string $tableName Name of the table. * @return Result */ public function truncateTable($tableName) { return $this->query('TRUNCATE TABLE '.$this->getSqlHelper()->quote($tableName)); } /** * Renames the table. Renamed table must exist and new name must not be occupied by any database object. * * @param string $currentName Old name of the table. * @param string $newName New name of the table. * * @return void * @throws SqlQueryException */ abstract public function renameTable($currentName, $newName); /** * Drops a column. This column must exist and must be not the part of primary constraint. * and must be not the last one in the table. * * @param string $tableName Name of the table to which column will be dropped. * @param string $columnName Name of the column to be dropped. * * @return void * @throws SqlQueryException */ public function dropColumn($tableName, $columnName) { $this->query('ALTER TABLE '.$this->getSqlHelper()->quote($tableName).' DROP COLUMN '.$this->getSqlHelper()->quote($columnName)); } /** * Drops the table. * * @param string $tableName Name of the table to be dropped. * * @return void * @throws SqlQueryException */ abstract public function dropTable($tableName); /********************************************************* * Transaction *********************************************************/ /** * Starts new database transaction. * * @return void * @throws SqlQueryException */ abstract public function startTransaction(); /** * Commits started database transaction. * * @return void * @throws SqlQueryException */ abstract public function commitTransaction(); /** * Rollbacks started database transaction. * * @return void * @throws SqlQueryException */ abstract public function rollbackTransaction(); /********************************************************* * Global named lock *********************************************************/ /** * Sets a global named lock. Currently only Mysql is supported. * @param string $name The lock name. * @param int $timeout * @return bool */ public function lock($name, $timeout = 0) { return true; } /** * Releases a global named lock. Currently only Mysql is supported. * @param string $name The lock name. * @return bool */ public function unlock($name) { return true; } /********************************************************* * Tracker *********************************************************/ /** * Starts collecting information about all queries executed. * * @param boolean $reset Clears all previously collected information when set to true. * * @return Diag\SqlTracker */ public function startTracker($reset = false) { if ($this->sqlTracker == null) $this->sqlTracker = new Diag\SqlTracker(); if ($reset) $this->sqlTracker->reset(); $this->trackSql = true; return $this->sqlTracker; } /** * Stops collecting information about all queries executed. * * @return void */ public function stopTracker() { $this->trackSql = false; } /** * Returns an object with information about queries executed. * or null if no tracking was started. * * @return null|Diag\SqlTracker */ public function getTracker() { return $this->sqlTracker; } /** * Sets new sql tracker. * * @param null|Diag\SqlTracker $sqlTracker New tracker. * * @return void */ public function setTracker(Diag\SqlTracker $sqlTracker = null) { $this->sqlTracker = $sqlTracker; } /********************************************************* * Type, version, cache, etc. *********************************************************/ /** * Returns database type. * <ul> * <li> mysql * <li> oracle * <li> mssql * </ul> * * @return string */ abstract public function getType(); /** * Returns connected database version. * Version presented in array of two elements. * - First (with index 0) is database version. * - Second (with index 1) is true when light/express version of database is used. * * @return array * @throws SqlQueryException */ abstract public function getVersion(); /** * Returns error message of last failed database operation. * * @return string */ abstract public function getErrorMessage(); /** * Clears all internal caches which may be used by some dictionary functions. * * @return void */ public function clearCaches() { $this->tableColumnsCache = array(); } /** * Sets connection node identifier. * * @param string $nodeId Node identifier. * @return void */ public function setNodeId($nodeId) { $this->nodeId = $nodeId; } /** * Returns connection node identifier. * * @return string|null */ public function getNodeId() { return $this->nodeId; } protected function afterConnected() { if(isset($this->configuration["include_after_connected"]) && $this->configuration["include_after_connected"] <> '') { include($this->configuration["include_after_connected"]); } } /** * Returns utfmb4 flag for the specific table/column. * * @param string|null $table * @param string|null $column * @return bool */ public function isUtf8mb4($table = null, $column = null) { if(isset($this->utf8mb4["global"]) && $this->utf8mb4["global"] === true) { return true; } if($table !== null && isset($this->utf8mb4["tables"][$table]) && $this->utf8mb4["tables"][$table] === true) { return true; } if($table !== null && $column !== null && isset($this->utf8mb4["tables"][$table][$column]) && $this->utf8mb4["tables"][$table][$column] === true) { return true; } return false; } protected static function findIndex(array $indexes, array $columns, $strict) { $columnsList = implode(",", $columns); foreach ($indexes as $indexName => $indexColumns) { ksort($indexColumns); $indexColumnList = implode(",", $indexColumns); if ($strict) { if ($indexColumnList === $columnsList) { return $indexName; } } else { if (str_starts_with($indexColumnList, $columnsList)) { return $indexName; } } } return null; } }