summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Repository/RepositoryQuery.php
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icinga/Repository/RepositoryQuery.php')
-rw-r--r--library/Icinga/Repository/RepositoryQuery.php797
1 files changed, 797 insertions, 0 deletions
diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php
new file mode 100644
index 0000000..84f7c6e
--- /dev/null
+++ b/library/Icinga/Repository/RepositoryQuery.php
@@ -0,0 +1,797 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Repository;
+
+use Iterator;
+use IteratorAggregate;
+use Traversable;
+use Icinga\Application\Benchmark;
+use Icinga\Application\Logger;
+use Icinga\Data\QueryInterface;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\FilterColumns;
+use Icinga\Data\SortRules;
+use Icinga\Exception\QueryException;
+
+/**
+ * Query class supposed to mediate between a repository and its datasource's query
+ */
+class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Iterator
+{
+ /**
+ * The repository being used
+ *
+ * @var Repository
+ */
+ protected $repository;
+
+ /**
+ * The real query being used
+ *
+ * @var QueryInterface
+ */
+ protected $query;
+
+ /**
+ * The current target to be queried
+ *
+ * @var mixed
+ */
+ protected $target;
+
+ /**
+ * The real query's iterator
+ *
+ * @var Iterator
+ */
+ protected $iterator;
+
+ /**
+ * This query's custom aliases
+ *
+ * @var array
+ */
+ protected $customAliases;
+
+ /**
+ * Create a new repository query
+ *
+ * @param Repository $repository The repository to use
+ */
+ public function __construct(Repository $repository)
+ {
+ $this->repository = $repository;
+ }
+
+ /**
+ * Clone all state relevant properties of this query
+ */
+ public function __clone()
+ {
+ if ($this->query !== null) {
+ $this->query = clone $this->query;
+ }
+ if ($this->iterator !== null) {
+ $this->iterator = clone $this->iterator;
+ }
+ }
+
+ /**
+ * Return a string representation of this query
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return (string) $this->query;
+ }
+
+ /**
+ * Return the real query being used
+ *
+ * @return QueryInterface
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+
+ /**
+ * Set where to fetch which columns
+ *
+ * This notifies the repository about each desired query column.
+ *
+ * @param mixed $target The target from which to fetch the columns
+ * @param array $columns If null or an empty array, all columns will be fetched
+ *
+ * @return $this
+ */
+ public function from($target, array $columns = null)
+ {
+ $this->query = $this->repository->getDataSource($target)->select();
+ $this->query->from($this->repository->requireTable($target, $this));
+ $this->query->columns($this->prepareQueryColumns($target, $columns));
+ $this->target = $target;
+ return $this;
+ }
+
+ /**
+ * Return the columns to fetch
+ *
+ * @return array
+ */
+ public function getColumns()
+ {
+ return $this->query->getColumns();
+ }
+
+ /**
+ * Set which columns to fetch
+ *
+ * This notifies the repository about each desired query column.
+ *
+ * @param array $columns If null or an empty array, all columns will be fetched
+ *
+ * @return $this
+ */
+ public function columns(array $columns)
+ {
+ $this->query->columns($this->prepareQueryColumns($this->target, $columns));
+ return $this;
+ }
+
+ /**
+ * Resolve the given columns supposed to be fetched
+ *
+ * This notifies the repository about each desired query column.
+ *
+ * @param mixed $target The target where to look for each column
+ * @param array $desiredColumns Pass null or an empty array to require all query columns
+ *
+ * @return array The desired columns indexed by their respective alias
+ */
+ protected function prepareQueryColumns($target, array $desiredColumns = null)
+ {
+ $this->customAliases = array();
+ if (empty($desiredColumns)) {
+ $columns = $this->repository->requireAllQueryColumns($target);
+ } else {
+ $columns = array();
+ foreach ($desiredColumns as $customAlias => $columnAlias) {
+ $resolvedColumn = $this->repository->requireQueryColumn($target, $columnAlias, $this);
+ if ($resolvedColumn !== $columnAlias) {
+ if (is_string($customAlias)) {
+ $columns[$customAlias] = $resolvedColumn;
+ $this->customAliases[$customAlias] = $columnAlias;
+ } else {
+ $columns[$columnAlias] = $resolvedColumn;
+ }
+ } elseif (is_string($customAlias)) {
+ $columns[$customAlias] = $columnAlias;
+ $this->customAliases[$customAlias] = $columnAlias;
+ } else {
+ $columns[] = $columnAlias;
+ }
+ }
+ }
+
+ return $columns;
+ }
+
+ /**
+ * Return the native column alias for the given custom alias
+ *
+ * If no custom alias is found with the given name, it is returned unchanged.
+ *
+ * @param string $customAlias
+ *
+ * @return string
+ */
+ protected function getNativeAlias($customAlias)
+ {
+ if (isset($this->customAliases[$customAlias])) {
+ return $this->customAliases[$customAlias];
+ }
+
+ return $customAlias;
+ }
+
+ /**
+ * Return this query's available filter columns with their optional label as key
+ *
+ * @return array
+ */
+ public function getFilterColumns()
+ {
+ return $this->repository->getFilterColumns($this->target);
+ }
+
+ /**
+ * Return this query's available search columns
+ *
+ * @return array
+ */
+ public function getSearchColumns()
+ {
+ return $this->repository->getSearchColumns($this->target);
+ }
+
+ /**
+ * Filter this query using the given column and value
+ *
+ * This notifies the repository about the required filter column.
+ *
+ * @param string $column
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function where($column, $value = null)
+ {
+ $this->addFilter(Filter::where($column, $value));
+ return $this;
+ }
+
+ /**
+ * Add an additional filter expression to this query
+ *
+ * This notifies the repository about each required filter column.
+ *
+ * @param Filter $filter
+ *
+ * @return $this
+ */
+ public function applyFilter(Filter $filter)
+ {
+ return $this->addFilter($filter);
+ }
+
+ /**
+ * Set a filter for this query
+ *
+ * This notifies the repository about each required filter column.
+ *
+ * @param Filter $filter
+ *
+ * @return $this
+ */
+ public function setFilter(Filter $filter)
+ {
+ $this->query->setFilter($this->repository->requireFilter($this->target, $filter, $this));
+ return $this;
+ }
+
+ /**
+ * Add an additional filter expression to this query
+ *
+ * This notifies the repository about each required filter column.
+ *
+ * @param Filter $filter
+ *
+ * @return $this
+ */
+ public function addFilter(Filter $filter)
+ {
+ $this->query->addFilter($this->repository->requireFilter($this->target, $filter, $this));
+ return $this;
+ }
+
+ /**
+ * Return the current filter
+ *
+ * @return Filter
+ */
+ public function getFilter()
+ {
+ return $this->query->getFilter();
+ }
+
+ /**
+ * Return the sort rules being applied on this query
+ *
+ * @return array
+ */
+ public function getSortRules()
+ {
+ return $this->repository->getSortRules($this->target);
+ }
+
+ /**
+ * Add a sort rule for this query
+ *
+ * If called without a specific column, the repository's defaul sort rules will be applied.
+ * This notifies the repository about each column being required as filter column.
+ *
+ * @param string $field The name of the column by which to sort the query's result
+ * @param string $direction The direction to use when sorting (asc or desc, default is asc)
+ * @param bool $ignoreDefault Whether to ignore any default sort rules if $field is given
+ *
+ * @return $this
+ */
+ public function order($field = null, $direction = null, $ignoreDefault = false)
+ {
+ $sortRules = $this->getSortRules();
+ if ($field === null) {
+ // Use first available sort rule as default
+ if (empty($sortRules)) {
+ // Return early in case of no sort defaults and no given $field
+ return $this;
+ }
+
+ $sortColumns = reset($sortRules);
+ if (! array_key_exists('columns', $sortColumns)) {
+ $sortColumns['columns'] = array(key($sortRules));
+ }
+ if ($direction !== null || !array_key_exists('order', $sortColumns)) {
+ $sortColumns['order'] = $direction ?: static::SORT_ASC;
+ }
+ } else {
+ $alias = $this->repository->reassembleQueryColumnAlias($this->target, $field) ?: $field;
+ if (! $ignoreDefault && array_key_exists($alias, $sortRules)) {
+ $sortColumns = $sortRules[$alias];
+ if (! array_key_exists('columns', $sortColumns)) {
+ $sortColumns['columns'] = array($alias);
+ }
+ if ($direction !== null || !array_key_exists('order', $sortColumns)) {
+ $sortColumns['order'] = $direction ?: static::SORT_ASC;
+ }
+ } else {
+ $sortColumns = array(
+ 'columns' => array($alias),
+ 'order' => $direction
+ );
+ }
+ }
+
+ $baseDirection = isset($sortColumns['order']) && strtoupper($sortColumns['order']) === static::SORT_DESC
+ ? static::SORT_DESC
+ : static::SORT_ASC;
+
+ foreach ($sortColumns['columns'] as $column) {
+ list($column, $specificDirection) = $this->splitOrder($column);
+
+ if ($this->hasLimit() && $this->repository->providesValueConversion($this->target, $column)) {
+ Logger::debug(
+ 'Cannot order by column "%s" in repository "%s". The query is'
+ . ' limited and applies value conversion rules on the column',
+ $column,
+ $this->repository->getName()
+ );
+ continue;
+ }
+
+ try {
+ $this->query->order(
+ $this->repository->requireFilterColumn($this->target, $column, $this),
+ $specificDirection ?: $baseDirection
+ // I would have liked the following solution, but hey, a coder should be allowed to produce crap...
+ // $specificDirection && (! $direction || $column !== $field) ? $specificDirection : $baseDirection
+ );
+ } catch (QueryException $_) {
+ Logger::info('Cannot order by column "%s" in repository "%s"', $column, $this->repository->getName());
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Extract and return the name and direction of the given sort column definition
+ *
+ * @param string $field
+ *
+ * @return array An array of two items: $columnName, $direction
+ */
+ protected function splitOrder($field)
+ {
+ $columnAndDirection = explode(' ', $field, 2);
+ if (count($columnAndDirection) === 1) {
+ $column = $field;
+ $direction = null;
+ } else {
+ $column = $columnAndDirection[0];
+ $direction = strtoupper($columnAndDirection[1]) === static::SORT_DESC
+ ? static::SORT_DESC
+ : static::SORT_ASC;
+ }
+
+ return array($column, $direction);
+ }
+
+ /**
+ * Return whether any sort rules were applied to this query
+ *
+ * @return bool
+ */
+ public function hasOrder()
+ {
+ return $this->query->hasOrder();
+ }
+
+ /**
+ * Return the sort rules applied to this query
+ *
+ * @return array
+ */
+ public function getOrder()
+ {
+ return $this->query->getOrder();
+ }
+
+ /**
+ * Set whether this query should peek ahead for more results
+ *
+ * Enabling this causes the current query limit to be increased by one. The potential extra row being yielded will
+ * be removed from the result set. Note that this only applies when fetching multiple results of limited queries.
+ *
+ * @param bool $state
+ *
+ * @return $this
+ */
+ public function peekAhead($state = true)
+ {
+ $this->query->peekAhead($state);
+ return $this;
+ }
+
+ /**
+ * Return whether this query did not yield all available results
+ *
+ * @return bool
+ */
+ public function hasMore()
+ {
+ return $this->query->hasMore();
+ }
+
+ /**
+ * Return whether this query will or has yielded any result
+ *
+ * @return bool
+ */
+ public function hasResult()
+ {
+ return $this->query->hasResult();
+ }
+
+ /**
+ * Limit this query's results
+ *
+ * @param int $count When to stop returning results
+ * @param int $offset When to start returning results
+ *
+ * @return $this
+ */
+ public function limit($count = null, $offset = null)
+ {
+ $this->query->limit($count, $offset);
+ return $this;
+ }
+
+ /**
+ * Return whether this query does not return all available entries from its result
+ *
+ * @return bool
+ */
+ public function hasLimit()
+ {
+ return $this->query->hasLimit();
+ }
+
+ /**
+ * Return the limit when to stop returning results
+ *
+ * @return int
+ */
+ public function getLimit()
+ {
+ return $this->query->getLimit();
+ }
+
+ /**
+ * Return whether this query does not start returning results at the very first entry
+ *
+ * @return bool
+ */
+ public function hasOffset()
+ {
+ return $this->query->hasOffset();
+ }
+
+ /**
+ * Return the offset when to start returning results
+ *
+ * @return int
+ */
+ public function getOffset()
+ {
+ return $this->query->getOffset();
+ }
+
+ /**
+ * Fetch and return the first column of this query's first row
+ *
+ * @return mixed|false False in case of no result
+ */
+ public function fetchOne()
+ {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $result = $this->query->fetchOne();
+ if ($result !== false && $this->repository->providesValueConversion($this->target)) {
+ $columns = $this->getColumns();
+ $column = isset($columns[0]) ? $columns[0] : $this->getNativeAlias(key($columns));
+ return $this->repository->retrieveColumn($this->target, $column, $result, $this);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Fetch and return the first row of this query's result
+ *
+ * @return object|false False in case of no result
+ */
+ public function fetchRow()
+ {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $result = $this->query->fetchRow();
+ if ($result !== false && $this->repository->providesValueConversion($this->target)) {
+ foreach ($this->getColumns() as $alias => $column) {
+ if (! is_string($alias)) {
+ $alias = $column;
+ }
+
+ $result->$alias = $this->repository->retrieveColumn(
+ $this->target,
+ $this->getNativeAlias($alias),
+ $result->$alias,
+ $this
+ );
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Fetch and return the first column of all rows of the result set as an array
+ *
+ * @return array
+ */
+ public function fetchColumn()
+ {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $results = $this->query->fetchColumn();
+ if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
+ $columns = $this->getColumns();
+ $aliases = array_keys($columns);
+ $column = is_int($aliases[0]) ? $columns[0] : $this->getNativeAlias($aliases[0]);
+ if ($this->repository->providesValueConversion($this->target, $column)) {
+ foreach ($results as & $value) {
+ $value = $this->repository->retrieveColumn($this->target, $column, $value, $this);
+ }
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Fetch and return all rows of this query's result set as an array of key-value pairs
+ *
+ * The first column is the key, the second column is the value.
+ *
+ * @return array
+ */
+ public function fetchPairs()
+ {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $results = $this->query->fetchPairs();
+ if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
+ $columns = $this->getColumns();
+ $aliases = array_keys($columns);
+ $colOne = $aliases[0] !== 0 ? $this->getNativeAlias($aliases[0]) : $columns[0];
+ $colTwo = count($aliases) < 2 ? $colOne : (
+ $aliases[1] !== 1 ? $this->getNativeAlias($aliases[1]) : $columns[1]
+ );
+
+ if ($this->repository->providesValueConversion($this->target, $colOne)
+ || $this->repository->providesValueConversion($this->target, $colTwo)
+ ) {
+ $newResults = array();
+ foreach ($results as $colOneValue => $colTwoValue) {
+ $colOneValue = $this->repository->retrieveColumn($this->target, $colOne, $colOneValue, $this);
+ $newResults[$colOneValue] = $this->repository->retrieveColumn(
+ $this->target,
+ $colTwo,
+ $colTwoValue,
+ $this
+ );
+ }
+
+ $results = $newResults;
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Fetch and return all results of this query
+ *
+ * @return array
+ */
+ public function fetchAll()
+ {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $results = $this->query->fetchAll();
+ if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
+ $updateOrder = false;
+ $columns = $this->getColumns();
+ $flippedColumns = array_flip($columns);
+ foreach ($results as $row) {
+ foreach ($columns as $alias => $column) {
+ if (! is_string($alias)) {
+ $alias = $column;
+ }
+
+ $row->$alias = $this->repository->retrieveColumn(
+ $this->target,
+ $this->getNativeAlias($alias),
+ $row->$alias,
+ $this
+ );
+ }
+
+ foreach (($this->getOrder() ?: array()) as $rule) {
+ $nativeAlias = $this->getNativeAlias($rule[0]);
+ if (! array_key_exists($rule[0], $flippedColumns) && property_exists($row, $rule[0])) {
+ if ($this->repository->providesValueConversion($this->target, $nativeAlias)) {
+ $updateOrder = true;
+ $row->{$rule[0]} = $this->repository->retrieveColumn(
+ $this->target,
+ $nativeAlias,
+ $row->{$rule[0]},
+ $this
+ );
+ }
+ } elseif (array_key_exists($rule[0], $flippedColumns)) {
+ if ($this->repository->providesValueConversion($this->target, $nativeAlias)) {
+ $updateOrder = true;
+ }
+ }
+ }
+ }
+
+ if ($updateOrder) {
+ uasort($results, array($this->query, 'compare'));
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Count all results of this query
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ return $this->query->count();
+ }
+
+ /**
+ * Return the current position of this query's iterator
+ *
+ * @return int
+ */
+ public function getIteratorPosition()
+ {
+ return $this->query->getIteratorPosition();
+ }
+
+ /**
+ * Start or rewind the iteration
+ */
+ public function rewind(): void
+ {
+ if ($this->iterator === null) {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ if ($this->query instanceof Traversable) {
+ $iterator = $this->query;
+ } else {
+ $iterator = $this->repository->getDataSource($this->target)->query($this->query);
+ }
+
+ if ($iterator instanceof IteratorAggregate) {
+ $this->iterator = $iterator->getIterator();
+ } else {
+ $this->iterator = $iterator;
+ }
+ }
+
+ $this->iterator->rewind();
+ Benchmark::measure('Query result iteration started');
+ }
+
+ /**
+ * Fetch and return the current row of this query's result
+ *
+ * @return mixed
+ */
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ $row = $this->iterator->current();
+ if ($this->repository->providesValueConversion($this->target)) {
+ foreach ($this->getColumns() as $alias => $column) {
+ if (! is_string($alias)) {
+ $alias = $column;
+ }
+
+ $row->$alias = $this->repository->retrieveColumn(
+ $this->target,
+ $this->getNativeAlias($alias),
+ $row->$alias,
+ $this
+ );
+ }
+ }
+
+ return $row;
+ }
+
+ /**
+ * Return whether the current row of this query's result is valid
+ *
+ * @return bool
+ */
+ public function valid(): bool
+ {
+ if (! $this->iterator->valid()) {
+ Benchmark::measure('Query result iteration finished');
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Return the key for the current row of this query's result
+ *
+ * @return mixed
+ */
+ #[\ReturnTypeWillChange]
+ public function key()
+ {
+ return $this->iterator->key();
+ }
+
+ /**
+ * Advance to the next row of this query's result
+ */
+ public function next(): void
+ {
+ $this->iterator->next();
+ }
+}