Your IP : 3.15.154.14
<?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 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.
*
* @return int Value to write to column.
*/
public function convertToDbInteger($value)
{
return intval($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 $field
* @param string $value
* @return string
*/
public function getConditionalAssignment(string $field, string $value): string
{
$field = $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;
}
}