* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 * @author Icinga Development Team * */ // {{{ICINGA_LICENSE_HEADER}}} namespace Icinga\Data\Db; use Icinga\Data\BaseQuery; use Icinga\Filter\Query\Tree; use Icinga\Filter\Query\Node; /** * Converter class that takes a query tree and creates an SQL Query from it's state */ class TreeToSqlParser { /** * The query class to use as the base for converting * * @var AbstractQuery */ private $query; /** * The type of the filter (WHERE or HAVING, depending whether it's an aggregate query) * @var string */ private $type = 'WHERE'; /** * Create a new converter from this query * * @param AbstractQuery $query The query to use for conversion */ public function __construct(BaseQuery $query) { $this->query = $query; } /** * Return the SQL equivalent fo the given text operator * * @param String $operator The operator from the query node * @return string The operator for the sql query part */ private function getSqlOperator($operator, array $right) { switch($operator) { case Node::OPERATOR_EQUALS: if (count($right) > 1) { return 'IN'; } else { return 'LIKE'; } case Node::OPERATOR_EQUALS_NOT: if (count($right) > 1) { return 'NOT IN'; } else { return 'NOT LIKE'; } default: return $operator; } } /** * Convert a Query Tree node to an sql string * * @param Node $node The node to convert * @return string The sql string representing the node's state */ private function nodeToSqlQuery(Node $node) { if ($node->type !== Node::TYPE_OPERATOR) { return $this->parseConjunctionNode($node); } else { return $this->parseOperatorNode($node); } } /** * Parse an AND or OR node to an sql string * * @param Node $node The AND/OR node to parse * @return string The sql string representing this node */ private function parseConjunctionNode(Node $node) { $queryString = ''; $leftQuery = $this->nodeToSqlQuery($node->left); $rightQuery = $this->nodeToSqlQuery($node->right); if ($leftQuery != '') { $queryString .= $leftQuery . ' '; } if ($rightQuery != '') { $queryString .= (($queryString !== '') ? $node->type . ' ' : ' ') . $rightQuery; } return $queryString; } /** * Parse an operator node to an sql string * * @param Node $node The operator node to parse * @return string The sql string representing this node */ private function parseOperatorNode(Node $node) { if (!$this->query->isValidFilterTarget($node->left) && $this->query->getMappedField($node->left)) { return ''; } $this->query->requireColumn($node->left); $queryString = '(' . $this->query->getMappedField($node->left) . ')'; if ($this->query->isAggregateColumn($node->left)) { $this->type = 'HAVING'; } $queryString .= ' ' . (is_integer($node->right) ? $node->operator : $this->getSqlOperator($node->operator, $node->right)) . ' '; $queryString = $this->addValueToQuery($node, $queryString); return $queryString; } /** * Convert a node value to it's sql equivalent * * This currently only detects if the node is in the timestring context and calls strtotime if so and it replaces * '*' with '%' * * @param Node $node The node to retrieve the sql string value from * @return String|int The converted and quoted value */ private function addValueToQuery(Node $node, $query) { $values = array(); foreach ($node->right as $value) { if ($node->operator === Node::OPERATOR_EQUALS || $node->operator === Node::OPERATOR_EQUALS_NOT) { $value = str_replace('*', '%', $value); } if ($this->query->isTimestamp($node->left)) { $node->context = Node::CONTEXT_TIMESTRING; } if ($node->context === Node::CONTEXT_TIMESTRING) { $value = strtotime($value); } $values[] = $this->query->getDatasource()->getConnection()->quote($value); } $valueString = join(',', $values); if (count($values) > 1) { return $query . '( '. $valueString . ')'; } return $query . $valueString; } /** * Apply the given tree to the query, either as where or as having clause * * @param Tree $tree The tree representing the filter * @param \Zend_Db_Select $baseQuery The query to apply the filter on */ public function treeToSql(Tree $tree, $baseQuery) { if ($tree->root == null) { return; } $tree->root = $tree->normalizeTree($tree->root); $sql = $this->nodeToSqlQuery($tree->root); if ($this->filtersAggregate()) { $baseQuery->having($sql); } else { $baseQuery->where($sql); } } /** * Return true if this is an filter that should be applied after aggregation * * @return bool True when having should be used, otherwise false */ private function filtersAggregate() { return $this->type === 'HAVING'; } }