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/sqlhelper.php |
<?php namespace Bitrix\Main\DB; use Bitrix\Main; use Bitrix\Main\Type; use Bitrix\Main\ORM; abstract class SqlHelper { /** @var Connection $connection */ protected $connection; protected $idCache; /** * @param Connection $connection Database connection. */ public function __construct(Connection $connection) { $this->connection = $connection; } /** * Returns an identificator escaping left character. * * @return string */ public function getLeftQuote() { return ''; } /** * Returns an identificator escaping right character. * * @return string */ public function getRightQuote() { return ''; } /** * Returns maximum length of an alias in a select statement * * @return integer */ abstract public function getAliasLength(); /** * Returns quoted identifier. * <p> * For example Title become : * - `Title` for MySQL * - "TITLE" for Oracle * - [Title] for Ms SQL * <p> * @param string $identifier Table or Column name. * * @return string * @see SqlHelper::getLeftQuote * @see SqlHelper::getRightQuote */ public function quote($identifier) { if (empty($this->idCache[$identifier])) { // security unshielding $quotedIdentifier = str_replace([$this->getLeftQuote(), $this->getRightQuote()], '', $identifier); // shield [[database.]tablename.]columnname if (str_contains($quotedIdentifier, '.')) { $quotedIdentifier = str_replace('.', $this->getRightQuote() . '.' . $this->getLeftQuote(), $quotedIdentifier); } // shield general borders $this->idCache[$identifier] = $this->getLeftQuote() . $quotedIdentifier . $this->getRightQuote(); } return $this->idCache[$identifier]; } /** * Returns database specific query delimiter for batch processing. * * @return string */ abstract public function getQueryDelimiter(); /** * Escapes special characters in a string for use in an SQL statement. * * @param string $value Value to be escaped. * @param integer $maxLength Limits string length if set. * * @return string */ abstract public function forSql($value, $maxLength = 0); /** * Returns binary safe data representation. * * @param string $value Value to be encoded. * * @return string */ public function convertToDbBinary($value) { return "'" . $this->forSql($value) . "'"; } /** * Returns function for getting current time. * * @return string */ abstract public function getCurrentDateTimeFunction(); /** * Returns function for getting current date without time part. * * @return string */ abstract public function getCurrentDateFunction(); /** * Returns function for adding seconds time interval to $from. * <p> * If $from is null or omitted, then current time is used. * <p> * $seconds and $from parameters are SQL unsafe. * * @param integer $seconds How many seconds to add. * @param integer $from Datetime database field of expression. * * @return string */ abstract public function addSecondsToDateTime($seconds, $from = null); /** * Returns function for adding days time interval to $from. * <p> * If $from is null or omitted, then current time is used. * <p> * $days and $from parameters are SQL unsafe. * * @abstract * @param integer $days How many days to add. * @param integer $from Datetime database field of expression. * * @return string */ public function addDaysToDateTime($days, $from = null) { throw new Main\NotImplementedException('Method should be implemented in a child class.'); } /** * Returns function cast $value to datetime database type. * <p> * $value parameter is SQL unsafe. * * @param string $value Database field or expression to cast. * * @return string */ abstract public function getDatetimeToDateFunction($value); /** * Returns database expression for converting $field value according the $format. * <p> * Following format parts converted: * - YYYY A full numeric representation of a year, 4 digits * - MMMM A full textual representation of a month, such as January or March * - MM Numeric representation of a month, with leading zeros * - MI Minutes with leading zeros * - M A short textual representation of a month, three letters * - DD Day of the month, 2 digits with leading zeros * - HH 24-hour format of an hour with leading zeros * - H 24-hour format of an hour without leading zeros * - GG 12-hour format of an hour with leading zeros * - G 12-hour format of an hour without leading zeros * - SS Seconds with leading zeros * - TT AM or PM * - T AM or PM * - W Day of the week (0=Sunday ... 6=Saturday) * <p> * $field parameter is SQL unsafe. * * @param string $format Format string. * @param string $field Database field or expression. * * @return string */ abstract public function formatDate($format, $field = null); /** * Returns function for getting part of string. * <p> * If length is null or omitted, the substring starting * from start until the end of the string will be returned. * <p> * $str and $from parameters are SQL unsafe. * * @param string $str Database field or expression. * @param integer $from Start position. * @param integer $length Maximum length. * * @return string */ public function getSubstrFunction($str, $from, $length = null) { $sql = 'SUBSTR('.$str.', '.$from; if (!is_null($length)) $sql .= ', '.$length; return $sql.')'; } /** * Returns function for concatenating database fields or expressions. * <p> * All parameters are SQL unsafe. * * @param string $field,... Database fields or expressions. * * @return string */ abstract public function getConcatFunction(); /** * Returns function for testing database field or expressions * against NULL value. When it is NULL then $result will be returned. * <p> * All parameters are SQL unsafe. * * @param string $expression Database field or expression for NULL test. * @param string $result Database field or expression to return when $expression is NULL. * * @return string */ abstract public function getIsNullFunction($expression, $result); /** * Returns function for getting length of database field or expression. * <p> * $field parameter is SQL unsafe. * * @param string $field Database field or expression. * * @return string */ abstract public function getLengthFunction($field); /** * Returns function for converting string value into datetime. * $value must be in YYYY-MM-DD HH:MI:SS format. * <p> * $value parameter is SQL unsafe. * * @param string $value String in YYYY-MM-DD HH:MI:SS format. * * @return string * @see SqlHelper::formatDate */ abstract public function getCharToDateFunction($value); /** * Returns function for converting database field or expression into string. * <p> * Result string will be in YYYY-MM-DD HH:MI:SS format. * <p> * $fieldName parameter is SQL unsafe. * * @param string $fieldName Database field or expression. * * @return string * @see SqlHelper::formatDate */ abstract public function getDateToCharFunction($fieldName); /** * Returns CAST expression for converting field or expression into string * * @param string $fieldName * * @return string */ abstract public function castToChar($fieldName); /** * Returns expression for text field being used in group or order * @see \Bitrix\Main\ORM\Query\Query::buildGroup * @see \Bitrix\Main\ORM\Query\Query::buildOrder * * @param string $fieldName * * @return string */ abstract public function softCastTextToChar($fieldName); /** * Transforms Sql according to $limit and $offset limitations. * <p> * You must specify $limit when $offset is set. * * @param string $sql Sql text. * @param integer $limit Maximum number of rows to return. * @param integer $offset Offset of the first row to return, starting from 0. * * @return string * @throws Main\ArgumentException */ abstract public function getTopSql($sql, $limit, $offset = 0); /** * Builds the strings for the SQL INSERT command for the given table. * * @param string $tableName A table name. * @param array $fields Array("column" => $value)[]. * * @param bool $returnAsArray * * @return array (columnList, valueList, binds) */ public function prepareInsert($tableName, array $fields, $returnAsArray = false) { $columns = array(); $values = array(); $tableFields = $this->connection->getTableFields($tableName); // one registry $tableFields = array_change_key_case($tableFields, CASE_UPPER); $fields = array_change_key_case($fields, CASE_UPPER); foreach ($fields as $columnName => $value) { if (isset($tableFields[$columnName])) { $columns[] = $this->quote($columnName); $values[] = $this->convertToDb($value, $tableFields[$columnName]); } else { trigger_error("Column `{$columnName}` is not found in the `{$tableName}` table", E_USER_WARNING); } } $binds = $this->prepareBinds($tableFields, $fields); return array( $returnAsArray ? $columns : implode(", ", $columns), $returnAsArray ? $values : implode(", ", $values), $binds ); } /** * Builds the strings for the SQL UPDATE command for the given table. * * @param string $tableName A table name. * @param array $fields Array("column" => $value)[]. * * @return array (update, binds) */ public function prepareUpdate($tableName, array $fields) { $update = array(); $tableFields = $this->connection->getTableFields($tableName); // one registry $tableFields = array_change_key_case($tableFields, CASE_UPPER); $fields = array_change_key_case($fields, CASE_UPPER); foreach ($fields as $columnName => $value) { if (isset($tableFields[$columnName])) { $update[] = $this->quote($columnName).' = '.$this->convertToDb($value, $tableFields[$columnName]); } else { trigger_error("Column `{$columnName}` is not found in the `{$tableName}` table", E_USER_WARNING); } } $binds = $this->prepareBinds($tableFields, $fields); return array( implode(", ", $update), $binds ); } /** * Builds the strings for the SQL MERGE command for the given table. * * @param string $tableName A table name. * @param array $primaryFields Array("column")[] Primary key columns list. * @param array $insertFields Array("column" => $value)[] What to insert. * @param array $updateFields Array("column" => $value)[] How to update. * * @return array (merge) */ abstract public function prepareMerge($tableName, array $primaryFields, array $insertFields, array $updateFields); /** * Performs additional processing of CLOB fields. * * @param ORM\Fields\ScalarField[] $tableFields Table fields. * @param array $fields Data fields. * * @return array */ protected function prepareBinds(array $tableFields, array $fields) { return array(); } /** * Builds the string for the SQL assignment operation of the given column. * * @param string $tableName A table name. * @param string $columnName A column name. * @param string $value A value to assign. * * @return string */ public function prepareAssignment($tableName, $columnName, $value) { $tableField = $this->connection->getTableField($tableName, $columnName); return $this->quote($columnName).' = '.$this->convertToDb($value, $tableField); } /** * Converts values to the string according to the column type to use it in a SQL query. * * @param mixed $value Value to be converted. * @param ORM\Fields\IReadable | null $field Type "source". * * @return string Value to write to column. */ public function convertToDb($value, ORM\Fields\IReadable $field = null) { if ($value === null) { return "NULL"; } if ($value instanceof SqlExpression) { return $value->compile(); } if (is_a($field, '\Bitrix\Main\ORM\Fields\StringField')) { $size = $field->getSize(); if ($size) { $value = mb_substr($value, 0, $size); } } if($field instanceof ORM\Fields\IReadable) { $result = $field->convertValueToDb($value); } else { $result = $this->convertToDbString($value); } return $result; } /** * Returns $value converted to a type according to $field type. * <p> * For example if $field is Entity\DatetimeField then returned value will be the instance of Type\DateTime. * * @param mixed $value Value to be converted. * @param ORM\Fields\IReadable $field Type "source". * * @return mixed */ public function convertFromDb($value, ORM\Fields\IReadable $field) { return $field->convertValueFromDb($value); } /** * Converts value to the string according to the data type to use it in a SQL query. * * @param mixed $value Value to be converted. * @param int $size Size in bytes. * * @return int Value to write to column. */ public function convertToDbInteger($value, $size = 8) { $value = intval($value); if ($size == 2) { $value = max(-32768, min(+32767, $value)); } elseif ($size == 4) { $value = max(-2147483648, min(+2147483647, $value)); } return $value; } /** * @param $value * * @return int */ public function convertFromDbInteger($value) { return intval($value); } /** * Converts value to the string according to the data type to use it in a SQL query. * * @param mixed $value Value to be converted. * @param int|null $scale Precise to round float value. * * @return string Value to write to column. */ public function convertToDbFloat($value, $scale = null) { $value = doubleval($value); if(!is_finite($value)) { $value = 0; } return $scale !== null ? "'".round($value, $scale)."'" : "'".$value."'"; } /** * @param $value * @param int $scale * * @return float */ public function convertFromDbFloat($value, $scale = null) { $value = doubleval($value); return $scale !== null ? round($value, $scale) : $value; } /** * Converts value to the string according to the data type to use it in a SQL query. * * @param mixed $value Value to be converted. * @param int|null $length Maximum acceptable length of the value * * @return string Value to write to column. */ public function convertToDbString($value, $length = null) { return "'".$this->forSql($value, $length)."'"; } /** * @param string $value * @param int $length * * @return string */ public function convertFromDbString($value, $length = null) { if ($length > 0) { $value = mb_substr($value, 0, $length); } return strval($value); } /** * Converts value to the string according to the data type to use it in a SQL query. * * @param mixed $value Value to be converted. * * @return string Value to write to column. */ public function convertToDbText($value) { return $this->convertToDbString($value); } /** * @param $value * * @return string */ public function convertFromDbText($value) { return $this->convertFromDbString($value); } /** * Converts value to the string according to the data type to use it in a SQL query. * * @param mixed $value Value to be converted. * * @return string Value to write to column. * @throws Main\ArgumentTypeException */ public function convertToDbDate($value) { if (empty($value)) { return "NULL"; } elseif($value instanceof Type\Date) { return $this->getCharToDateFunction($value->format("Y-m-d")); } else { throw new Main\ArgumentTypeException('value', '\Bitrix\Main\Type\Date'); } } /** * @param $value * * @return Type\Date * @throws Main\ObjectException */ public function convertFromDbDate($value) { return new Type\Date($value); } /** * Converts value to the string according to the data type to use it in a SQL query. * * @param mixed $value Value to be converted. * * @return string Value to write to column. * @throws Main\ArgumentTypeException */ public function convertToDbDateTime($value) { if (empty($value)) { return "NULL"; } elseif($value instanceof Type\Date) { if($value instanceof Type\DateTime) { $value = clone($value); $value->setDefaultTimeZone(); } return $this->getCharToDateFunction($value->format("Y-m-d H:i:s")); } else { throw new Main\ArgumentTypeException('value', '\Bitrix\Main\Type\Date'); } } /** * @param $value * * @return Type\DateTime * @throws Main\ObjectException */ public function convertFromDbDateTime($value) { return new Type\DateTime($value); } /** * @deprecated * Converts string into \Bitrix\Main\Type\DateTime object. * <p> * Helper function. * * @param string $value Value fetched. * * @return null|\Bitrix\Main\Type\DateTime * @see SqlHelper::getConverter */ public function convertDatetimeField($value) { return $this->convertFromDbDateTime($value); } /** * @deprecated * Converts string into \Bitrix\Main\Type\Date object. * <p> * Helper function. * * @param string $value Value fetched. * * @return null|\Bitrix\Main\Type\Date * @see SqlHelper::getConverter */ public function convertDateField($value) { return $this->convertFromDbDate($value); } /** * Returns callback to be called for a field value on fetch. * Used for soft conversion. For strict results @see ORM\Query\Result::setStrictValueConverters() * * @param ORM\Fields\ScalarField $field Type "source". * * @return false|callback */ public function getConverter(ORM\Fields\ScalarField $field) { return false; } /** * Returns a column type according to ScalarField object. * * @param \Bitrix\Main\ORM\Fields\ScalarField $field Type "source". * * @return string */ abstract public function getColumnTypeByField(ORM\Fields\ScalarField $field); /** * Returns instance of a descendant from Entity\ScalarField * that matches database type. * * @param string $name Database column name. * @param mixed $type Database specific type. * @param array | null $parameters Additional information. * * @return \Bitrix\Main\ORM\Fields\ScalarField */ abstract public function getFieldByColumnType($name, $type, array $parameters = null); /** * Returns ascending order specifier for ORDER BY clause. * * @return string */ public function getAscendingOrder() { return 'ASC'; } /** * Returns descending order specifier for ORDER BY clause. * * @return string */ public function getDescendingOrder() { return 'DESC'; } /** * @param string|SqlExpression $field * @param string $value * @return string */ public function getConditionalAssignment($field, string $value): string { $field = $field instanceof SqlExpression ? $field->compile() : $this->quote($field); $hash = $this->convertToDbString(sha1($value)); $value = $this->convertToDbString($value); return 'case when ' . $this->getSha1Function($field) . ' = ' . $hash . ' then ' . $field . ' else ' . $value . ' end'; } /** * Makes an insert statement which will ignore duplicate keys errors. * * @abstract * @param string $tableName Table to insert. * @param integer $fields Fields list in braces. * @param integer $sql Select or values sql. * * @return string */ public function getInsertIgnore($tableName, $fields, $sql) { throw new Main\NotImplementedException('Method should be implemented in a child class.'); } /** * Returns function for getting random number. * * @return string */ public function getRandomFunction() { return 'rand()'; } /** * Returns function to generate sha1 hash. * <p> * $field parameter is SQL unsafe. * * @param string $field Database field or expression. * * @return string */ public function getSha1Function($field) { return 'sha1(' . $field . ')'; } /** * Returns regexp expression. * <p> * All parameters are SQL unsafe. * * @abstract * @param string $field Database field or expression. * @param string $regexp Regexp to match. * * @return string */ public function getRegexpOperator($field, $regexp) { throw new Main\NotImplementedException('Method should be implemented in a child class.'); } /** * Returns identifier for usage in VALUES. * * @abstract * @param string $identifier Column name. * * @return string * @see SqlHelper::quote */ public function values($identifier) { throw new Main\NotImplementedException('Method should be implemented in a child class.'); } /** * @abstract */ public function getMatchFunction($field, $value) { throw new Main\NotImplementedException('Method should be implemented in a child class.'); } /** * @abstract */ public function getMatchAndExpression($values, $prefixSearch = false) { throw new Main\NotImplementedException('Method should be implemented in a child class.'); } /** * @abstract */ public function getMatchOrExpression($values, $prefixSearch = false) { throw new Main\NotImplementedException('Method should be implemented in a child class.'); } /** * Builds the DML strings for the SQL REPLACE INTO command for the given table. * * @abstract * @param string $tableName A table name. * @param array $primaryFields Array("column")[] Primary key columns list. * @param array $insertRows Array(Array("column" => $value)[])[] Rows to insert. * * @return array (replace) */ public function prepareMergeMultiple($tableName, array $primaryFields, array $insertRows) { throw new Main\NotImplementedException('Method should be implemented in a child class.'); } /** * Builds the DML strings for the SQL INSERT INTO ON CONFLICT UPDATE command for the given table. * * @abstract * @param string $tableName A table name. * @param array $primaryFields Array("column")[] Primary key columns list. * @param array $selectFields * @param $select * @param $updateFields * @return string (replace) */ public function prepareMergeSelect($tableName, array $primaryFields, array $selectFields, $select, $updateFields) { throw new Main\NotImplementedException('Method should be implemented in a child class.'); } /** * Builds the DML string for the SQL DELETE command for the given table with limited rows number. * * @abstract * @param string $tableName A table name. * @param array $primaryFields Array("column")[] Primary key columns list. * @param string $where Sql where clause. * @param array $order Array("column" => asc|desc)[] Sort order. * @param integer $limit Rows to delete count. * * @return string (replace) */ public function prepareDeleteLimit($tableName, array $primaryFields, $where, array $order, $limit) { throw new Main\NotImplementedException('Method should be implemented in a child class.'); } /** * @abstract */ public function initRowNumber($variableName) { throw new Main\NotImplementedException('Method should be implemented in a child class.'); } /** * @abstract */ public function getRowNumber($variableName) { throw new Main\NotImplementedException('Method should be implemented in a child class.'); } /** * Builds correlated update DML. * * @abstract * @param string $tableName A table name. * @param string $tableAlias A table alias. * @param array $fields Array("column" => "expression")[] Update columns list. * @param string $from Correlated tables. * @param string $where Where clause. * * @return string */ public function prepareCorrelatedUpdate($tableName, $tableAlias, $fields, $from, $where) { throw new Main\NotImplementedException('Method should be implemented in a child class.'); } /** * Returns prepared sql string for upsert multiple rows * * @param string $tableName Table name * @param array $primaryFields Fields that can be conflicting keys (primary, unique keys) * @param array $insertRows Rows to insert [['FIELD_NAME' =>'value',...],...], Attention! use same columns in each row * @param array $updateFields Fields to update, if empty - update all fields, can be only field names, or fieldname => expression or fieldname => value * * @return string * @throws \Bitrix\Main\ArgumentException */ public function prepareMergeValues(string $tableName, array $primaryFields, array $insertRows, array $updateFields = []): string { $insertColumns = array_keys($insertRows[array_key_first($insertRows)] ?? []); $insertValuesStrings = []; foreach ($insertRows as $row) { [, $rowValues] = $this->prepareInsert($tableName, $row); $insertValuesStrings[] = $rowValues; } if (empty($updateFields)) { $notPrimaryFields = array_diff($insertColumns, $primaryFields); if (empty($notPrimaryFields)) { trigger_error("Only primary fields to update, use getInsertIgnore() or specify fields", E_USER_WARNING); } $updateFields = $notPrimaryFields; } $compatibleUpdateFields = []; foreach ($updateFields as $key => $value) { if (is_numeric($key) && is_string($value)) { $compatibleUpdateFields[$value] = new SqlExpression('?v', $value); } else { $compatibleUpdateFields[$key] = $value; } } $insertValueString = 'values (' . implode('),(', $insertValuesStrings) . ')'; return $this->prepareMergeSelect($tableName, $primaryFields, $insertColumns, $insertValueString, $compatibleUpdateFields); } /** * @param string $field * @param array $values * @param bool $quote * * @return string */ public function getOrderByStringField(string $field, array $values, bool $quote = true): string { return $this->getOrderByField($field, $values, [$this, 'convertToDbString'], $quote); } /** * @param string $field * @param array $values * @param bool $quote * * @return string */ public function getOrderByIntField(string $field, array $values, bool $quote = true): string { return $this->getOrderByField($field, $values, [$this, 'convertFromDbInteger'], $quote); } /** * @param string $field * @param array $values * @param callable $callback * @param bool $quote * * @return string */ protected function getOrderByField(string $field, array $values, callable $callback, bool $quote = true): string { return $quote ? $this->quote($field) : $field; } }