Your IP : 172.28.240.42


Current Path : /var/www/html/clients/nkpgkx11.e-nk.ru/bitrix/modules/main/lib/entity/
Upload File :
Current File : /var/www/html/clients/nkpgkx11.e-nk.ru/bitrix/modules/main/lib/entity/query.php

<?php

namespace Bitrix\Main\Entity;

class Query
{
	protected
		$init_entity;

	protected
		$select = array(),
		$group = array(),
		$order = array(),
		$limit = null,
		$offset = null,
		$count_total = null;

	protected
		$filter = array(),
		$where = array(),
		$having = array();

	/**
	 * @var QueryChain[]
	 */
	protected					  // all chain storages keying by alias
		$select_chains = array(),
		$group_chains = array(),
		$order_chains = array();

	/**
	 * @var QueryChain[]
	 */
	protected
		$filter_chains = array(),
		$where_chains = array(),
		$having_chains = array();

	protected
		$select_expr_chains = array(), // from select expr "build_from"
		$having_expr_chains = array(), // from having expr "build_from"
		$hidden_chains = array(); // all expr "build_from" elements;

	protected
		$runtime_chains;

	protected
		$options;

	/**
	 * @var QueryChain[]
	 */
	protected $global_chains = array(); // keying by both def and alias

	protected $query_build_parts;

	/**
	 * Enable or Disable data doubling for 1:N relations in query filter
	 * If disabled, 1:N entity fields in filter will be trasnformed to exists() subquery
	 * @var bool
	 */
	protected $data_doubling = true;

	protected $table_alias_postfix = '';

	protected
		$join_map = array();

	protected
		$is_executing = false;

	protected
		$last_query;

	protected $replaced_aliases;

	protected
		$DB;


	/**
	 * @param Base|Query|string $source
	 * @throws \Exception
	 */
	public function __construct($source)
	{
		if ($source instanceof $this)
		{
			$this->init_entity = Base::getInstanceByQuery($source);
		}
		elseif ($source instanceof Base)
		{
			$this->init_entity = $source;
		}
		elseif (is_string($source))
		{
			$this->init_entity = Base::getInstance($source);
		}
		else
		{
			throw new \Exception(sprintf(
				'Unknown source type "%s" for new %s', gettype($source), __CLASS__
			));
		}

		$this->DB = $GLOBALS['DB'];
	}

	public function getSelect()
	{
		return $this->select;
	}

	public function setSelect(array $select)
	{
		$this->select = $select;
		return $this;
	}

	public function addSelect($definition, $alias = '')
	{
		if (strlen($alias))
		{
			$this->select[$alias] = $definition;
		}
		else
		{
			$this->select[] = $definition;
		}

		return $this;
	}

	public function getFilter()
	{
		return $this->filter;
	}

	public function setFilter(array $filter)
	{
		$this->filter = $filter;
		return $this;
	}

	public function addFilter($key, $value)
	{
		if (is_null($key) && is_array($value))
		{
			$this->filter[] = $value;
		}
		else
		{
			$this->filter[$key] = $value;
		}

		return $this;
	}

	public function getGroup()
	{
		return $this->group;
	}

	public function setGroup($group)
	{
		$group = !is_array($group) ? array($group) : $group;
		$this->group = $group;

		return $this;
	}

	public function addGroup($group)
	{
		$this->group[] = $group;
		return $this;
	}

	public function getOrder()
	{
		return $this->order;
	}

	public function setOrder(array $order)
	{
		$this->order = array();

		foreach ($order as $k => $v)
		{
			if (is_numeric($k))
			{
				$this->addOrder($v);
			}
			else
			{
				$this->addOrder($k, $v);
			}
		}

		return $this;
	}

	public function addOrder($definition, $order = 'ASC')
	{
		$order = strtoupper($order);
		$definition = strtoupper($definition);

		if (!in_array($order, array('ASC', 'DESC'), true))
		{
			throw new \Exception(sprintf('Invalid order "%s"', $order));
		}

		global $DBType;

		if ($DBType == 'oracle')
		{
			if ($order == 'ASC')
			{
				$order = 'ASC NULLS FIRST';
			}
			else
			{
				$order = 'DESC NULLS LAST';
			}
		}

		$this->order[$definition] = $order;

		return $this;
	}

	public function getLimit()
	{
		return $this->limit;
	}

	public function setLimit($limit)
	{
		$this->limit = $limit;
		return $this;
	}

	public function getOffset()
	{
		return $this->offset;
	}

	public function setOffset($offset)
	{
		$this->offset = $offset;
		return $this;
	}

	public function countTotal($count = null)
	{
		if ($count === null)
		{
			return $this->count_total;
		}
		else
		{
			$this->count_total = (bool) $count;
			return $this;
		}
	}

	public function enableDataDoubling()
	{
		$this->data_doubling = true;
	}

	public function disableDataDoubling()
	{
		$this->data_doubling = false;
	}

	public function getOptions()
	{
		return $this->options;
	}

	public function setOptions($options)
	{
		$this->options = $options;
		return $this;
	}

	public function addOption($option_name, $option_value)
	{
		$this->options[$option_name] = $option_value;
		return $this;
	}

	public function registerRuntimeField($name, $fieldInfo)
	{
		$field = $this->init_entity->initializeField($name, $fieldInfo);

		$chain = new QueryChain;
		$chain->addElement(new QueryChainElement($this->init_entity));
		$chain->addElement(new QueryChainElement($field));

		// add
		$this->registerChain('runtime', $chain);
		return $this;
	}

	public function setTableAliasPostfix($postfix)
	{
		$this->table_alias_postfix = $postfix;
		return $this;
	}

	public function getTableAliasPostfix()
	{
		return $this->table_alias_postfix;
	}

	public function exec()
	{
		$this->is_executing = true;

		$build_parts = $this->buildQuery(true);

		$result = $this->query($build_parts);

		$this->is_executing = false;

		return $result;
	}

	protected function addToSelectChain($definition, $alias = '')
	{
		if (is_array($definition))
		{
			// it is runtime field
			$this->registerRuntimeField($alias, $definition);
			$chain = $this->getRegisteredChain($alias);

			// add
			$this->registerChain('select', $chain);

			// recursively collect all "build_from" fields
			if ($chain->getLastElement()->getValue() instanceof ExpressionField)
			{
				$this->collectExprChains($chain, array('hidden', 'select_expr'));
			}
		}
		else
		{
			// there is normal scalar field, or Reference, or Entity (all fields of)
			$chain = $this->getRegisteredChain($definition, true);

			if (!empty($alias))
			{
				// custom alias
				$chain = clone $chain;
				$chain->setCustomAlias($alias);
			}

			$last_elem = $chain->getLastElement();

			// fill if element is not scalar
			$expand_entity = null;

			if ($last_elem->getValue() instanceof ReferenceField)
			{
				$expand_entity = $last_elem->getValue()->getRefEntity();
			}
			elseif (is_array($last_elem->getValue()))
			{
				list($expand_entity, ) = $last_elem->getValue();
			}
			elseif ($last_elem->getValue() instanceof Base)
			{
				$expand_entity = $last_elem->getValue();
			}

			if ($expand_entity)
			{
				// add all fields of entity
				foreach ($expand_entity->getFields() as $exp_field)
				{
					if (!($exp_field instanceof ReferenceField))
					{
						if ($exp_field instanceof ExpressionField)
						{
							// we should have own copy of build_from_chains to set join aliases there
							// actually is copy&paste from getChainByDefinition
							// it would be correct to form DEFINITIONs here and call getChainByDefinition for each
							$exp_field = clone $exp_field;
						}

						$exp_chain = clone $chain;
						$exp_chain->addElement(new QueryChainElement(
							$exp_field
						));

						// custom alias
						if (!empty($alias))
						{
							$exp_chain->setCustomAlias($alias.$exp_field->getName());
						}

						// add
						$this->registerChain('select', $exp_chain);

						if ($exp_field instanceof ExpressionField)
						{
							$this->collectExprChains($exp_chain, array('hidden', 'select_expr'));
						}
					}
				}
			}
			else
			{
				// scalar field that defined in entity
				$this->registerChain('select', $chain);

				// collect buildFrom fields (recursively)
				if ($chain->getLastElement()->getValue() instanceof ExpressionField)
				{
					$this->collectExprChains($chain, array('hidden', 'select_expr'));
				}
			}
		}

		return $this;
	}

	public function setFilterChains(array $filter, $section = 'filter')
	{
		foreach ($filter as $filter_def => $filter_match)
		{
			if ($filter_def === 'LOGIC')
			{
				continue;
			}

			if (!is_numeric($filter_def))
			{
				$csw_result = \CSQLWhere::makeOperation($filter_def);
				list($definition, ) = array_values($csw_result);

				$chain = $this->getRegisteredChain($definition, true);

				$this->registerChain($section, $chain, $definition);

				// fill hidden select
				if ($chain->getLastElement()->getValue() instanceof ExpressionField)
				{
					$this->collectExprChains($chain);
				}
			}

			if (is_array($filter_match))
			{
				$this->setFilterChains($filter_match, $section);
			}
		}
	}

	protected function divideFilter()
	{
		// divide filter to where and having

		$logic = isset($this->filter['LOGIC']) ? $this->filter['LOGIC'] : 'AND';

		if ($logic == 'OR')
		{
			// if has aggr then move all to having
			if ($this->checkFilterAggregation($this->filter))
			{
				$this->where = array();
				$this->where_chains = array();

				$this->having = $this->filter;
				$this->having_chains = $this->filter_chains;
			}
			else
			{
				$this->where = $this->filter;
				$this->where_chains = $this->filter_chains;

				$this->having = array();
				$this->having_chains = array();
			}
		}
		elseif ($logic == 'AND')
		{
			// we can separate root filters
			foreach ($this->filter as $k => $sub_filter)
			{
				if ($k === 'LOGIC')
				{
					$this->where[$k] = $sub_filter;
					$this->having[$k] = $sub_filter;

					continue;
				}

				$tmp_filter = array($k => $sub_filter);

				if ($this->checkFilterAggregation($tmp_filter))
				{
					$this->having[$k] = $sub_filter;
					$this->setFilterChains($tmp_filter, 'having');
				}
				else
				{
					$this->where[$k] = $sub_filter;
					$this->setFilterChains($tmp_filter, 'where');
				}
			}
		}

		// collect "build_from" fields from having
		foreach ($this->having_chains as $chain)
		{
			if ($chain->getLastElement()->getValue() instanceof ExpressionField)
			{
				$this->collectExprChains($chain, array('hidden', 'having_expr'));
			}
		}
	}

	protected function checkFilterAggregation($filter)
	{
		foreach ($filter as $filter_def => $filter_match)
		{
			if ($filter_def === 'LOGIC')
			{
				continue;
			}

			if (!is_numeric($filter_def))
			{
				$csw_result = \CSQLWhere::makeOperation($filter_def);
				list($definition, ) = array_values($csw_result);

				$chain = $this->filter_chains[$definition];
				$last = $chain->getLastElement();

				$is_having = $last->getValue() instanceof ExpressionField && $last->getValue()->isAggregated();
			}
			elseif (is_array($filter_match))
			{
				$is_having = $this->checkFilterAggregation($filter_match);
			}

			if ($is_having)
			{
				return true;
			}
		}

		return false;
	}

	protected function addToGroupChain($definition)
	{
		$chain = $this->getRegisteredChain($definition, true);
		$this->registerChain('group', $chain);

		if ($chain->getLastElement()->getValue() instanceof ExpressionField)
		{
			$this->collectExprChains($chain);
		}
	}

	protected function addToOrderChain($definition)
	{
		$chain = $this->getRegisteredChain($definition, true);
		$this->registerChain('order', $chain);

		if ($chain->getLastElement()->getValue() instanceof ExpressionField)
		{
			$this->collectExprChains($chain);
		}
	}

	protected function buildJoinMap()
	{
		// list of used joins
		$done = array();

		$talias_count = 0;

		foreach ($this->global_chains as $chain)
		{
			if ($chain->getLastElement()->getParameter('talias'))
			{
				// already been here
				continue;
			}

			// in NO_DOUBLING mode skip 1:N relations that presented in filter only
			if (!$this->data_doubling && $chain->hasBackReference())
			{
				$alias = $chain->getAlias();

				if (isset($this->filter_chains[$alias])
					&& !isset($this->select_chains[$alias]) && !isset($this->select_expr_chains[$alias])
					&& !isset($this->group_chains[$alias]) && !isset($this->order_chains[$alias])
				)
				{
					continue;
				}
			}

			$prev_entity = $this->init_entity;
			$prev_alias = strtolower($this->init_entity->getCode());

			$map_key = '';

			/**
			 * elemenets after init entity
			 * @var $elements QueryChainElement[]
			 * */
			$elements = array_slice($chain->getAllElements(), 1);

			foreach ($elements as $element)
			{
				$table_alias = null;

				/**
				 * define main objects
				 * @var $src_entity Base
				 * @var $ref_field ReferenceField
				 * @var $dst_entity Base
				 */
				if ($element->getValue() instanceof ReferenceField)
				{
					// ref to another entity
					$src_entity = $prev_entity;
					$ref_field = $element->getValue();
					$dst_entity = $ref_field->getRefEntity();
				}
				elseif (is_array($element->getValue())
				)
				{
					// link from another entity to this
					$src_entity = $prev_entity;
					list($dst_entity, $ref_field) = $element->getValue();
				}
				else
				{
					// scalar field
					$element->setParameter('talias', $prev_alias.$this->table_alias_postfix);
					continue;
				}

				// mapping
				if (empty($map_key))
				{
					$map_key = $src_entity->getName();
				}

				$map_key .= '/' . $ref_field->getName() . '/' . $dst_entity->getName();

				if (isset($done[$map_key]))
				{
					// already connected
					$table_alias = $done[$map_key];
				}
				else
				{
					// prepare reference
					$reference = $ref_field->getReference();

					if ($element->getValue() instanceof ReferenceField)
					{
						// ref to another entity
						if (is_null($table_alias))
						{
							$table_alias = $prev_alias.'_'.strtolower($ref_field->getName());

							if (strlen($table_alias.$this->table_alias_postfix) > $this->DB->alias_length)
							{
								$table_alias = 'TALIAS'.$this->table_alias_postfix.'_' . (++$talias_count);
							}
						}

						$alias_this = $prev_alias;
						$alias_ref = $table_alias;
					}
					elseif (is_array($element->getValue()))
					{
						if (is_null($table_alias))
						{
							$table_alias = Base::camel2snake($dst_entity->getName()) . '_' . strtolower($ref_field->getName());
							$table_alias = $prev_alias.'_'.$table_alias;

							if (strlen($table_alias.$this->table_alias_postfix) > $this->DB->alias_length)
							{
								$table_alias = 'TALIAS'.$this->table_alias_postfix.'_' . (++$talias_count);
							}
						}

						$alias_this = $table_alias;
						$alias_ref = $prev_alias;

						if ($dst_entity->isUtm())
						{
							// add to $reference
							$reference = array(
								$reference,
								'=this.FIELD_ID' => array('?i', $element->getParameter('ufield')->getFieldId())
							);
						}
					}

					// replace this. and ref. to real definition -- not supported yet
					// instead it we set $alias_this and $alias_ref

					$csw_reference = $this->prepareJoinReference(
						$reference, $alias_this.$this->table_alias_postfix, $alias_ref.$this->table_alias_postfix
					);

					$join = array(
						'type' => $ref_field->getJoinType(),
						'table' => $dst_entity->getDBTableName(),
						'alias' => $table_alias.$this->table_alias_postfix,
						'reference' => $csw_reference
					);

					$this->join_map[] = $join;

					$done[$map_key] = $table_alias;
				}

				// set alias for each element
				$element->setParameter('talias', $table_alias.$this->table_alias_postfix);

				$prev_entity = $dst_entity;
				$prev_alias = $table_alias;
			}
		}
	}

	protected function buildSelect()
	{
		$sql = array();

		foreach ($this->select_chains as $chain)
		{
			$sql[] = $chain->getSqlDefinition(true);
		}

		if (empty($sql))
		{
			$sql[] = 1;
		}

		return "\t".join(",\n\t", $sql);
	}

	protected function buildJoin()
	{
		$sql = array();
		$csw = new \CSQLWhere;

		foreach ($this->join_map as $join)
		{
			// prepare csw fields
			$csw_fields = $this->getJoinCswFields($join['reference']);
			$csw->setFields($csw_fields);

			// sql
			$sql[] = sprintf('%s JOIN %s %s ON %s',
				$join['type'], $join['table'],
				$this->DB->escL . $join['alias'] . $this->DB->escR,
				trim($csw->getQuery($join['reference']))
			);
		}

		return join("\n", $sql);
	}

	protected function buildWhere()
	{
		$csw = new \CSQLWhere;

		$csw_fields = $this->getFilterCswFields($this->where);
		$csw->setFields($csw_fields);

		$sql = trim($csw->getQuery($this->where));

		return $sql;
	}

	protected function buildGroup()
	{
		$sql = array();

		if (!empty($this->group_chains) || !empty($this->having_chains)
			|| $this->checkChainsAggregation($this->select_chains)
			|| $this->checkChainsAggregation($this->order_chains)
		)
		{
			// add non-aggr fields to group
			foreach ($this->global_chains as $chain)
			{
				$alias = $chain->getAlias();

				// skip constants
				if ($chain->isConstant())
				{
					continue;
				}

//				if (isset($this->select_chains[$alias]) || isset($this->select_expr_chains[$alias]) || isset($this->order_chains[$alias])
//					|| isset($this->having_chains[$alias]) || isset($this->having_expr_chains[$alias]))
				if (isset($this->select_chains[$alias]) || isset($this->order_chains[$alias]) || isset($this->having_chains[$alias]))
				{
					//if (!($chain->getLastElement()->getValue() instanceof ExpressionField) && !$chain->hasAggregation())
					// skip subqueries
					if (!$chain->hasAggregation() && !$chain->hasSubquery())
					{
						$this->registerChain('group', $chain);
					}
					// but include build_from of subqueries
					elseif (!$chain->hasAggregation() && $chain->hasSubquery() && $chain->getLastElement()->getValue() instanceof ExpressionField)
					{
						$sub_chains = $chain->getLastElement()->getValue()->getBuildFromChains();

						foreach ($sub_chains as $sub_chain)
						{
							// build real subchain starting from init entity
							$real_sub_chain = clone $chain;

							foreach (array_slice($sub_chain->getAllElements(), 1) as $sub_chain_elem)
							{
								$real_sub_chain->addElement($sub_chain_elem);
							}

							// add to query
							$this->registerChain('group', $this->global_chains[$real_sub_chain->getAlias()]);
						}
					}
				}
				elseif (isset($this->having_expr_chains[$alias]))
				{
					if (!$chain->hasAggregation() && $chain->hasSubquery())
					{
						$this->registerChain('group', $chain);
					}
				}
			}
		}

		foreach ($this->group_chains as $chain)
		{
			$sql[] = $chain->getSqlDefinition();
		}

		return join(', ', $sql);
	}

	protected function buildHaving()
	{
		$csw = new \CSQLWhere;

		$csw_fields = $this->getFilterCswFields($this->having);
		$csw->setFields($csw_fields);

		$sql = trim($csw->getQuery($this->having));

		return $sql;
	}

	protected function buildOrder()
	{
		$sql = array();

		foreach ($this->order_chains as $chain)
		{
			$sql[] = $chain->getSqlDefinition() . ' ' . $this->order[$chain->getDefinition()];
		}

		return join(', ', $sql);
	}

	protected function buildQuery($returnBuildParts = false)
	{
		if ($this->query_build_parts === null)
		{
			foreach ($this->select as $key => $value)
			{
				$this->addToSelectChain($value, is_numeric($key) ? '' : $key);
			}

			$this->setFilterChains($this->filter);
			$this->divideFilter($this->filter);

			foreach ($this->group as $value)
			{
				$this->addToGroupChain($value);
			}

			foreach ($this->order as $key => $value)
			{
				$this->addToOrderChain($key);
			}

			$this->buildJoinMap();

			// ------------------

			$sqlSelect = $this->buildSelect();
			$sqlJoin = $this->buildJoin();
			$sqlWhere = $this->buildWhere();
			$sqlGroup = $this->buildGroup();
			$sqlHaving = $this->buildHaving();
			$sqlOrder = $this->buildOrder();

			$sqlFrom = $this->init_entity->getDBTableName();
			$sqlFrom .= ' '.$this->DB->escL . strtolower($this->init_entity->getCode()) . $this->table_alias_postfix . $this->DB->escR;
			$sqlFrom .= ' '.$sqlJoin;

			$this->query_build_parts = array_filter(array(
				'SELECT' => $sqlSelect, 'FROM' => $sqlFrom,
				'WHERE' => $sqlWhere, 'GROUP BY' => $sqlGroup,
				'HAVING' => $sqlHaving, 'ORDER BY' => $sqlOrder
			));
		}

		if ($returnBuildParts)
		{
			return $this->query_build_parts;
		}

		$build_parts = $this->query_build_parts;

		foreach ($build_parts as $k => &$v)
		{
			if (strlen($v))
			{
				$v = $k . ' ' . $v;
			}
		}

		$query = join("\n", $build_parts);

		list($query, ) = $this->replaceSelectAliases($query);

		if (!empty($this->options))
		{
			foreach ($this->options as $opt => $value)
			{
				$query = str_replace('%'.$opt.'%', $value, $query);
			}
		}

		if (empty($this->limit))
		{
			return $query;
		}
		elseif (array_key_exists('nPageTop', $this->limit))
		{
			$query = $this->DB->topSql($query, intval($this->limit['nPageTop']));
			return $query;
		}
		else
		{
			// can't get "paginated" query through DB, return base query
			// yes, it is BUG
			return $query;
		}
	}

	protected function getFilterCswFields(&$filter)
	{
		$fields = array();

		foreach ($filter as $filter_def => &$filter_match)
		{
			if ($filter_def === 'LOGIC')
			{
				continue;
			}

			if (!is_numeric($filter_def))
			{
				$csw_result = \CSQLWhere::makeOperation($filter_def);
				list($definition, ) = array_values($csw_result);

				$chain = $this->filter_chains[$definition];
				$last = $chain->getLastElement();

				$field_type = $last->getValue()->getDataType();

				// rewrite type & value for CSQLWhere
				if ($field_type == 'integer')
				{
					$field_type = 'int';
				}
				elseif ($field_type == 'boolean')
				{
					$field_type = 'string';

					if (is_scalar($filter_match))
					{
						$filter_match = $last->getValue()->normalizeValue($filter_match);
					}
				}
				elseif ($field_type == 'float')
				{
					$field_type = 'double';
				}

				//$is_having = $last->getValue() instanceof ExpressionField && $last->getValue()->isAggregated();

				// if back-reference found (Entity:REF)
				// if NO_DOUBLING mode enabled, then change getSQLDefinition to subquery exists(...)
				// and those chains should not be in joins if it is possible

				$callback = null;

				/*if (!$this->data_doubling && $chain->hasBackReference())
				{
					$field_type = 'callback';
					$init_query = $this;

					$callback = function ($field, $operation, $value) use ($init_query, $chain)
					{
						$init_entity = $init_query->getEntity();
						$init_table_alias = CBaseEntity::camel2snake($init_entity->getName()).$init_query->getTableAliasPostfix();

						$filter = array();

						// add primary linking with main query
						foreach ($init_entity->getPrimaryArray() as $primary)
						{
							$filter['='.$primary] = new CSQLWhereExpression('?#', $init_table_alias.'.'.$primary);
						}

						// add value filter
						$filter[CSQLWhere::getOperationByCode($operation).$chain->getDefinition()] = $value;

						// build subquery
						$query_class = __CLASS__;
						$sub_query = new $query_class($init_entity);
						$sub_query->setFilter($filter);
						$sub_query->setTableAliasPostfix('_sub');

						return 'EXISTS(' . $sub_query->getQuery() . ')';
					};
				}*/

				$fields[$definition] = array(
					'TABLE_ALIAS' => 'table',
					'FIELD_NAME' => $chain->getSqlDefinition(),
					'FIELD_TYPE' => $field_type,
					'MULTIPLE' => '',
					'JOIN' => '',
					'CALLBACK' => $callback
				);
			}

			if (is_array($filter_match))
			{
				$fields = array_merge($fields, $this->getFilterCswFields($filter_match));
			}
		}

		return $fields;
	}

	protected function prepareJoinReference($reference, $alias_this, $alias_ref)
	{
		$new = array();

		foreach ($reference as $k => $v)
		{
			if ($k === 'LOGIC')
			{
				$new[$k] = $v;
				continue;
			}

			if (is_numeric($k))
			{
				// subfilter, recursive call
				$new[$k] = $this->prepareJoinReference($v, $alias_this, $alias_ref);
			}
			else
			{
				// key
				$csw_result = \CSQLWhere::makeOperation($k);
				list($field, ) = array_values($csw_result);

				if (strpos($field, 'this.') === 0)
				{
					$k = str_replace('this.', $this->DB->escL.$alias_this.$this->DB->escR.'.'.$this->DB->escL, $k);
					$k .= $this->DB->escR;
				}
				elseif (strpos($field, 'ref.') === 0)
				{
					$k = str_replace('ref.', $this->DB->escL.$alias_ref.$this->DB->escR.'.'.$this->DB->escL, $k);
					$k .= $this->DB->escR;
				}
				else
				{
					throw new \Exception();
				}

				// value
				if (is_array($v))
				{
					// field = expression
					$v = new \CSQLWhereExpression($v[0], array_slice($v, 1));
				}
				else
				{
					// field = field
					if (strpos($v, 'this.') === 0)
					{
						$field_def = str_replace('this.', $alias_this.'.', $v);
					}
					elseif (strpos($v, 'ref.') === 0)
					{
						$field_def = str_replace('ref.', $alias_ref.'.', $v);
					}

					$v = new \CSQLWhereExpression('?#', $field_def);
				}

				$new[$k] = $v;
			}
		}

		return $new;
	}

	protected function getJoinCswFields($reference)
	{
		$fields = array();

		foreach ($reference as $k => $v)
		{
			if ($k === 'LOGIC')
			{
				continue;
			}

			if (is_numeric($k))
			{
				$fields = array_merge($fields, $this->getJoinCswFields($v));
			}
			else
			{
				// key
				$csw_result = \CSQLWhere::makeOperation($k);
				list($field, ) = array_values($csw_result);

				$fields[$field] = array(
					'TABLE_ALIAS' => 'alias',
					'FIELD_NAME' => $field,
					'FIELD_TYPE' => 'string',
					'MULTIPLE' => '',
					'JOIN' => ''
				);

				// no need to add values as csw fields
			}
		}

		return $fields;
	}

	protected function checkChainsAggregation($chain)
	{
		$chains = is_array($chain) ? $chain : array($chain);

		foreach ($chains as $chain)
		{
			$last = $chain->getLastElement();
			$is_aggr = $last->getValue() instanceof ExpressionField && $last->getValue()->isAggregated();

			if ($is_aggr)
			{
				return true;
			}
		}

		return false;
	}

	protected function collectExprChains(QueryChain $chain, $storages = array('hidden'))
	{
		$last_elem = $chain->getLastElement();
		$bf_chains = $last_elem->getValue()->getBuildFromChains();

		$pre_chain = clone $chain;
		//$pre_chain->removeLastElement();

		foreach ($bf_chains as $bf_chain)
		{
			// collect hidden chain
			$tmp_chain = clone $pre_chain;

			// exclude init entity
			$bf_elements = array_slice($bf_chain->getAllElements(), 1);

			// add elements
			foreach ($bf_elements as $bf_element)
			{
				$tmp_chain->addElement($bf_element);
			}

			//if (!($bf_chain->getLastElement()->getValue() instanceof ExpressionField))
			{
				foreach ($storages as $storage)
				{
					$reg_chain = $this->registerChain($storage, $tmp_chain);
				}

				// replace "build_from" chain end by registered chain end
				// actually it's better and more correctly to replace the whole chain
				$bf_chain->removeLastElement();
				$bf_chain->addElement($reg_chain->getLastElement());
			}

			// check elements to recursive collect hidden chains
			foreach ($bf_elements as $bf_element)
			{
				if ($bf_element->getValue() instanceof ExpressionField)
				{
					$this->collectExprChains($tmp_chain);
				}
			}
		}
	}

	public function registerChain($section, QueryChain $chain, $opt_key = null)
	{
		$alias = $chain->getAlias();

		if (isset($this->global_chains[$alias]))
		{
			$reg_chain = $this->global_chains[$alias];
		}
		else
		{
			$reg_chain = $chain;
			$def = $reg_chain->getDefinition();

			$this->global_chains[$alias] = $chain;
			$this->global_chains[$def] = $chain;
		}

		$storage_name = $section . '_chains';
		$this->{$storage_name}[$alias] = $reg_chain;

		if (!is_null($opt_key))
		{
			$this->{$storage_name}[$opt_key] = $reg_chain;
		}

		return $reg_chain;
	}

	public function getRegisteredChain($key, $force_create = false)
	{
		if (isset($this->global_chains[$key]))
		{
			return $this->global_chains[$key];
		}

		if ($force_create)
		{
			$chain = QueryChain::getChainByDefinition($this->init_entity, $key);
			$this->registerChain('global', $chain);

			return $chain;
		}

		return false;
	}

	protected function query($build_parts)
	{
		// nosql support with new platform only
		if(file_exists($_SERVER["DOCUMENT_ROOT"]."/bitrix/d7.php"))
		{
			// check nosql configuration
			$configuration = $this->init_entity->getConnection()->getConfiguration();

			if (isset($configuration['handlersocket']['read']))
			{
				$nosqlConnectionName = $configuration['handlersocket']['read'];

				$nosqlConnection = \Bitrix\Main\Application::getInstance()->getDbConnectionPool()->getConnection($nosqlConnectionName);
				$isNosqlCapable = NosqlPrimarySelector::checkQuery($nosqlConnection, $this);

				if ($isNosqlCapable)
				{
					$nosqlResult = NosqlPrimarySelector::relayQuery($nosqlConnection, $this);

					$result = new \CDBResult();
					$result->initFromArray($nosqlResult);

					return $result;
				}
			}
		}


		foreach ($build_parts as $k => &$v)
		{
			if (strlen($v))
			{
				$v = $k . ' ' . $v;
			}
		}

		if (!empty($this->options))
		{
			foreach ($this->options as $opt => $value)
			{
				$build_parts = str_replace('%'.$opt.'%', $value, $build_parts);
			}
		}

		$query = join("\n", $build_parts);

		list($query, $replaced_aliases) = $this->replaceSelectAliases($query);

		if ($this->count_total || !is_null($this->offset))
		{
			$cnt_body_elements = $build_parts;

			// remove order
			unset($cnt_body_elements['ORDER BY']);

			$cnt_query = join("\n", $cnt_body_elements);

			// remove long aliases
			list($cnt_query, ) = $this->replaceSelectAliases($cnt_query);

			// select count
			$cnt_query = 'SELECT COUNT(1) AS TMP_ROWS_CNT FROM ('.$cnt_query.') xxx';
			$result = $this->DB->query($cnt_query);
			$result = $result->fetch();
			$cnt = $result["TMP_ROWS_CNT"];
		}

		if (empty($this->limit))
		{
			$result = $this->DB->query($query);
			$result->arReplacedAliases = $replaced_aliases;
		}
		elseif (!empty($this->limit) && is_null($this->offset))
		{
			$query = $this->DB->topSql($query, intval($this->limit));
			$result = $this->DB->query($query);
			$result->arReplacedAliases = $replaced_aliases;
		}
		else
		{
			// main query
			$result = new \CDBResult();
			$result->arReplacedAliases = $replaced_aliases;
			$db_limit = array(
				'nPageSize' => $this->limit,
				'iNumPage' => $this->offset ? (($this->offset / $this->limit) + 1) : 1,
				'bShowAll' => true
			);;
			$result->navQuery($query, $cnt, $db_limit);
		}

		$this->last_query = $query;

		return $result;
	}

	protected function replaceSelectAliases($query)
	{
		$replaced = array();
		$length = (int) $this->DB->alias_length;

		preg_match_all(
			'/ AS '.preg_quote($this->DB->escL).'([a-z0-9_]{'.($length+1).',})'.preg_quote($this->DB->escR).'/i',
			$query, $matches
		);

		if (!empty($matches[1]))
		{
			foreach ($matches[1] as $alias)
			{
				$newAlias = 'FALIAS_'.count($replaced);
				$replaced[$newAlias] = $alias;

				$query = str_replace(
					' AS ' . $this->DB->escL . $alias . $this->DB->escR,
					' AS ' . $this->DB->escL . $newAlias . $this->DB->escR . '/* '.$alias.' */',
					$query
				);
			}
		}

		$this->replaced_aliases = $replaced;

		return array($query, $replaced);
	}

	/**
	 * @return array|QueryChain[]
	 */
	public function getChains()
	{
		return $this->global_chains;
	}

	/**
	 * @return array|QueryChain[]
	 */
	public function getGroupChains()
	{
		return $this->group_chains;
	}

	/**
	 * @return array
	 */
	public function getHiddenChains()
	{
		return $this->hidden_chains;
	}

	/**
	 * @return array|QueryChain[]
	 */
	public function getHavingChains()
	{
		return $this->having_chains;
	}

	/**
	 * @return array|QueryChain[]
	 */
	public function getFilterChains()
	{
		return $this->filter_chains;
	}

	/**
	 * @return array|QueryChain[]
	 */
	public function getOrderChains()
	{
		return $this->order_chains;
	}

	/**
	 * @return array|QueryChain[]
	 */
	public function getSelectChains()
	{
		return $this->select_chains;
	}

	/**
	 * @return array|QueryChain[]
	 */
	public function getWhereChains()
	{
		return $this->where_chains;
	}

	public function getJoinMap()
	{
		return $this->join_map;
	}

	public function getQuery()
	{
		return $this->buildQuery(false);
	}

	public function getLastQuery()
	{
		return $this->last_query;
	}

	public function getEntity()
	{
		return $this->init_entity;
	}

	public function getReplacedAliases()
	{
		return $this->replaced_aliases;
	}

	public function dump()
	{
		echo '<pre>';

		echo 'last query: ';
		var_dump($this->last_query);
		echo PHP_EOL;

		echo 'size of select_chains: '.count($this->select_chains);
		echo PHP_EOL;
		foreach ($this->select_chains as $num => $chain)
		{
			echo '  chain ['.$num.'] has '.$chain->getSize().' elements: '.PHP_EOL;
			$chain->dump();
			echo PHP_EOL;
		}

		echo PHP_EOL.PHP_EOL;

		echo 'size of where_chains: '.count($this->where_chains);
		echo PHP_EOL;
		foreach ($this->where_chains as $num => $chain)
		{
			echo '  chain ['.$num.'] has '.$chain->getSize().' elements: '.PHP_EOL;
			$chain->dump();
			echo PHP_EOL;
		}

		echo PHP_EOL.PHP_EOL;

		echo 'size of group_chains: '.count($this->group_chains);
		echo PHP_EOL;
		foreach ($this->group_chains as $num => $chain)
		{
			echo '  chain ['.$num.'] has '.$chain->getSize().' elements: '.PHP_EOL;
			$chain->dump();
			echo PHP_EOL;
		}

		echo PHP_EOL.PHP_EOL;

		echo 'size of having_chains: '.count($this->having_chains);
		echo PHP_EOL;
		foreach ($this->having_chains as $num => $chain)
		{
			echo '  chain ['.$num.'] has '.$chain->getSize().' elements: '.PHP_EOL;
			$chain->dump();
			echo PHP_EOL;
		}

		echo PHP_EOL.PHP_EOL;

		echo 'size of filter_chains: '.count($this->filter_chains);
		echo PHP_EOL;
		foreach ($this->filter_chains as $num => $chain)
		{
			echo '  chain ['.$num.'] has '.$chain->getSize().' elements: '.PHP_EOL;
			$chain->dump();
			echo PHP_EOL;
		}

		echo PHP_EOL.PHP_EOL;

		echo 'size of select_expr_chains: '.count($this->select_expr_chains);
		echo PHP_EOL;
		foreach ($this->select_expr_chains as $num => $chain)
		{
			echo '  chain ['.$num.'] has '.$chain->getSize().' elements: '.PHP_EOL;
			$chain->dump();
			echo PHP_EOL;
		}

		echo PHP_EOL.PHP_EOL;

		echo 'size of hidden_chains: '.count($this->hidden_chains);
		echo PHP_EOL;
		foreach ($this->hidden_chains as $num => $chain)
		{
			echo '  chain ['.$num.'] has '.$chain->getSize().' elements: '.PHP_EOL;
			$chain->dump();
			echo PHP_EOL;
		}

		echo PHP_EOL.PHP_EOL;

		echo 'size of global_chains: '.count($this->global_chains);
		echo PHP_EOL;
		foreach ($this->global_chains as $num => $chain)
		{
			echo '  chain ['.$num.'] has '.$chain->getSize().' elements: '.PHP_EOL;
			$chain->dump();
			echo PHP_EOL;
		}

		echo PHP_EOL.PHP_EOL;

		var_dump($this->join_map);

		echo '</pre>';
	}
}