summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Data/SimpleQuery.php
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--library/Icinga/Data/SimpleQuery.php650
1 files changed, 650 insertions, 0 deletions
diff --git a/library/Icinga/Data/SimpleQuery.php b/library/Icinga/Data/SimpleQuery.php
new file mode 100644
index 0000000..1ef0c27
--- /dev/null
+++ b/library/Icinga/Data/SimpleQuery.php
@@ -0,0 +1,650 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+use Iterator;
+use IteratorAggregate;
+use Icinga\Application\Benchmark;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\ProgrammingError;
+
+class SimpleQuery implements QueryInterface, Queryable, Iterator
+{
+ /**
+ * Query data source
+ *
+ * @var mixed
+ */
+ protected $ds;
+
+ /**
+ * This query's iterator
+ *
+ * @var Iterator
+ */
+ protected $iterator;
+
+ /**
+ * The current position of this query's iterator
+ *
+ * @var int
+ */
+ protected $iteratorPosition;
+
+ /**
+ * The amount of rows previously calculated
+ *
+ * @var int
+ */
+ protected $cachedCount;
+
+ /**
+ * The target you are going to query
+ *
+ * @var mixed
+ */
+ protected $target;
+
+ /**
+ * The columns you asked for
+ *
+ * All columns if null, no column if empty??? Alias handling goes here!
+ *
+ * @var array
+ */
+ protected $desiredColumns = array();
+
+ /**
+ * The columns you are interested in
+ *
+ * All columns if null, no column if empty??? Alias handling goes here!
+ *
+ * @var array
+ */
+ protected $columns = array();
+
+ /**
+ * The columns and their aliases flipped in order to handle aliased sort columns
+ *
+ * Supposed to be used and populated by $this->compare *only*.
+ *
+ * @var array
+ */
+ protected $flippedColumns;
+
+ /**
+ * The columns you're using to sort the query result
+ *
+ * @var array
+ */
+ protected $order = array();
+
+ /**
+ * Number of rows to return
+ *
+ * @var int
+ */
+ protected $limitCount;
+
+ /**
+ * Result starts with this row
+ *
+ * @var int
+ */
+ protected $limitOffset;
+
+ /**
+ * Whether to peek ahead for more results
+ *
+ * @var bool
+ */
+ protected $peekAhead;
+
+ /**
+ * Whether the query did not yield all available results
+ *
+ * @var bool
+ */
+ protected $hasMore;
+
+ protected $filter;
+
+ /**
+ * Constructor
+ *
+ * @param mixed $ds
+ */
+ public function __construct($ds, $columns = null)
+ {
+ $this->ds = $ds;
+ $this->filter = Filter::matchAll();
+ if ($columns !== null) {
+ $this->desiredColumns = $columns;
+ }
+ $this->init();
+ if ($this->desiredColumns !== null) {
+ $this->columns($this->desiredColumns);
+ }
+ }
+
+ /**
+ * Initialize query
+ *
+ * Overwrite this instead of __construct (it's called at the end of the construct) to
+ * implement custom initialization logic on construction time
+ */
+ protected function init()
+ {
+ }
+
+ /**
+ * Get the data source
+ *
+ * @return mixed
+ */
+ public function getDatasource()
+ {
+ return $this->ds;
+ }
+
+ /**
+ * Return the current position of this query's iterator
+ *
+ * @return int
+ */
+ public function getIteratorPosition()
+ {
+ return $this->iteratorPosition;
+ }
+
+ /**
+ * Start or rewind the iteration
+ */
+ public function rewind(): void
+ {
+ if ($this->iterator === null) {
+ $iterator = $this->ds->query($this);
+ if ($iterator instanceof IteratorAggregate) {
+ $this->iterator = $iterator->getIterator();
+ } else {
+ $this->iterator = $iterator;
+ }
+ }
+
+ $this->iterator->rewind();
+ $this->iteratorPosition = null;
+ Benchmark::measure('Query result iteration started');
+ }
+
+ /**
+ * Fetch and return the current row of this query's result
+ *
+ * @return mixed
+ */
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ return $this->iterator->current();
+ }
+
+ /**
+ * Return whether the current row of this query's result is valid
+ *
+ * @return bool
+ */
+ public function valid(): bool
+ {
+ $valid = $this->iterator->valid();
+ if ($valid && $this->peekAhead && $this->hasLimit() && $this->iteratorPosition + 1 === $this->getLimit()) {
+ $this->hasMore = true;
+ $valid = false; // We arrived at the last result, which is the requested extra row, so stop the iteration
+ } elseif (! $valid) {
+ $this->hasMore = false;
+ }
+
+ if (! $valid) {
+ Benchmark::measure('Query result iteration finished');
+ return false;
+ } elseif ($this->iteratorPosition === null) {
+ $this->iteratorPosition = 0;
+ }
+
+ 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();
+ $this->iteratorPosition += 1;
+ }
+
+ /**
+ * Choose a table and the columns you are interested in
+ *
+ * Query will return all available columns if none are given here.
+ *
+ * @param mixed $target
+ * @param array $fields
+ *
+ * @return $this
+ */
+ public function from($target, array $fields = null)
+ {
+ $this->target = $target;
+ if ($fields !== null) {
+ $this->columns($fields);
+ }
+ return $this;
+ }
+
+ /**
+ * Add a where condition to the query by and
+ *
+ * The syntax of the condition and valid values are defined by the concrete backend-specific query implementation.
+ *
+ * @param string $condition
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function where($condition, $value = null)
+ {
+ // TODO: more intelligence please
+ $this->filter->addFilter(Filter::expression($condition, '=', $value));
+ return $this;
+ }
+
+ public function getFilter()
+ {
+ return $this->filter;
+ }
+
+ public function applyFilter(Filter $filter)
+ {
+ return $this->addFilter($filter);
+ }
+
+ public function addFilter(Filter $filter)
+ {
+ $this->filter->addFilter($filter);
+ return $this;
+ }
+
+ public function setFilter(Filter $filter)
+ {
+ $this->filter = $filter;
+ return $this;
+ }
+
+ public function setOrderColumns(array $orderColumns)
+ {
+ throw new IcingaException('This function does nothing and will be removed');
+ }
+
+ /**
+ * Split order field into its field and sort direction
+ *
+ * @param string $field
+ *
+ * @return array
+ */
+ public function splitOrder($field)
+ {
+ $fieldAndDirection = explode(' ', $field, 2);
+ if (count($fieldAndDirection) === 1) {
+ $direction = null;
+ } else {
+ $field = $fieldAndDirection[0];
+ $direction = (strtoupper(trim($fieldAndDirection[1])) === 'DESC') ?
+ Sortable::SORT_DESC : Sortable::SORT_ASC;
+ }
+ return array($field, $direction);
+ }
+
+ /**
+ * Sort result set by the given field (and direction)
+ *
+ * Preferred usage:
+ * <code>
+ * $query->order('field, 'ASC')
+ * </code>
+ *
+ * @param string $field
+ * @param string $direction
+ *
+ * @return $this
+ */
+ public function order($field, $direction = null)
+ {
+ if ($direction === null) {
+ list($field, $direction) = $this->splitOrder($field);
+ if ($direction === null) {
+ $direction = Sortable::SORT_ASC;
+ }
+ } else {
+ switch (($direction = strtoupper($direction))) {
+ case Sortable::SORT_ASC:
+ case Sortable::SORT_DESC:
+ break;
+ default:
+ $direction = Sortable::SORT_ASC;
+ break;
+ }
+ }
+ $this->order[] = array($field, $direction);
+ return $this;
+ }
+
+ /**
+ * Compare $a with $b based on this query's sort rules and column aliases
+ *
+ * @param object $a
+ * @param object $b
+ * @param int $orderIndex
+ *
+ * @return int
+ */
+ public function compare($a, $b, $orderIndex = 0)
+ {
+ if (! array_key_exists($orderIndex, $this->order)) {
+ return 0; // Last column to sort reached, rows are considered being equal
+ }
+
+ if ($this->flippedColumns === null) {
+ $this->flippedColumns = array_flip($this->columns);
+ }
+
+ $column = $this->order[$orderIndex][0];
+ if (array_key_exists($column, $this->flippedColumns) && is_string($this->flippedColumns[$column])) {
+ $column = $this->flippedColumns[$column];
+ }
+
+ $result = strcmp(strtolower($a->$column ?? ''), strtolower($b->$column ?? ''));
+ if ($result === 0) {
+ return $this->compare($a, $b, ++$orderIndex);
+ }
+
+ $direction = $this->order[$orderIndex][1];
+ if ($direction === self::SORT_ASC) {
+ return $result;
+ } else {
+ return $result * -1;
+ }
+ }
+
+ /**
+ * Clear the order if any
+ *
+ * @return $this
+ */
+ public function clearOrder()
+ {
+ $this->order = array();
+ return $this;
+ }
+
+ /**
+ * Whether an order is set
+ *
+ * @return bool
+ */
+ public function hasOrder()
+ {
+ return ! empty($this->order);
+ }
+
+ /**
+ * Get the order
+ *
+ * @return array
+ */
+ public function getOrder()
+ {
+ return $this->order;
+ }
+
+ /**
+ * 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.
+ *
+ * @return $this
+ */
+ public function peekAhead($state = true)
+ {
+ $this->peekAhead = (bool) $state;
+ return $this;
+ }
+
+ /**
+ * Return whether this query did not yield all available results
+ *
+ * @return bool
+ *
+ * @throws ProgrammingError In case the query did not run yet
+ */
+ public function hasMore()
+ {
+ if ($this->hasMore === null) {
+ throw new ProgrammingError('Query did not run. Cannot determine whether there are more results.');
+ }
+
+ return $this->hasMore;
+ }
+
+ /**
+ * Return whether this query will or has yielded any result
+ *
+ * @return bool
+ */
+ public function hasResult()
+ {
+ return $this->cachedCount > 0 || $this->iteratorPosition !== null || $this->fetchRow() !== false;
+ }
+
+ /**
+ * Set a limit count and offset to the query
+ *
+ * @param int $count Number of rows to return
+ * @param int $offset Start returning after this many rows
+ *
+ * @return $this
+ */
+ public function limit($count = null, $offset = null)
+ {
+ $this->limitCount = $count !== null ? (int) $count : null;
+ $this->limitOffset = (int) $offset;
+ return $this;
+ }
+
+ /**
+ * Whether a limit is set
+ *
+ * @return bool
+ */
+ public function hasLimit()
+ {
+ return $this->limitCount !== null && $this->limitCount > 0;
+ }
+
+ /**
+ * Get the limit if any
+ *
+ * @return int|null
+ */
+ public function getLimit()
+ {
+ return $this->peekAhead && $this->hasLimit() ? $this->limitCount + 1 : $this->limitCount;
+ }
+
+ /**
+ * Whether an offset is set
+ *
+ * @return bool
+ */
+ public function hasOffset()
+ {
+ return $this->limitOffset > 0;
+ }
+
+ /**
+ * Get the offset if any
+ *
+ * @return int|null
+ */
+ public function getOffset()
+ {
+ return $this->limitOffset;
+ }
+
+ /**
+ * Retrieve an array containing all rows of the result set
+ *
+ * @return array
+ */
+ public function fetchAll()
+ {
+ Benchmark::measure('Fetching all results started');
+ $results = $this->ds->fetchAll($this);
+ Benchmark::measure('Fetching all results finished');
+
+ if ($this->peekAhead && $this->hasLimit() && count($results) === $this->getLimit()) {
+ $this->hasMore = true;
+ array_pop($results);
+ } else {
+ $this->hasMore = false;
+ }
+
+ return $results;
+ }
+
+ /**
+ * Fetch the first row of the result set
+ *
+ * @return mixed
+ */
+ public function fetchRow()
+ {
+ Benchmark::measure('Fetching one row started');
+ $row = $this->ds->fetchRow($this);
+ Benchmark::measure('Fetching one row finished');
+ return $row;
+ }
+
+ /**
+ * Fetch the first column of all rows of the result set as an array
+ *
+ * @return array
+ */
+ public function fetchColumn()
+ {
+ Benchmark::measure('Fetching one column started');
+ $values = $this->ds->fetchColumn($this);
+ Benchmark::measure('Fetching one column finished');
+
+ if ($this->peekAhead && $this->hasLimit() && count($values) === $this->getLimit()) {
+ $this->hasMore = true;
+ array_pop($values);
+ } else {
+ $this->hasMore = false;
+ }
+
+ return $values;
+ }
+
+ /**
+ * Fetch the first column of the first row of the result set
+ *
+ * @return string
+ */
+ public function fetchOne()
+ {
+ Benchmark::measure('Fetching one value started');
+ $value = $this->ds->fetchOne($this);
+ Benchmark::measure('Fetching one value finished');
+ return $value;
+ }
+
+ /**
+ * Fetch all rows of the 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()
+ {
+ Benchmark::measure('Fetching pairs started');
+ $pairs = $this->ds->fetchPairs($this);
+ Benchmark::measure('Fetching pairs finished');
+
+ if ($this->peekAhead && $this->hasLimit() && count($pairs) === $this->getLimit()) {
+ $this->hasMore = true;
+ array_pop($pairs);
+ } else {
+ $this->hasMore = false;
+ }
+
+ return $pairs;
+ }
+
+ /**
+ * Count all rows of the result set, ignoring limit and offset
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ $query = clone $this;
+ $query->limit(0, 0);
+ Benchmark::measure('Counting all results started');
+ $count = $this->ds->count($query);
+ $this->cachedCount = $count;
+ Benchmark::measure('Counting all results finished');
+ return $count;
+ }
+
+ /**
+ * Set columns
+ *
+ * @param array $columns
+ *
+ * @return $this
+ */
+ public function columns(array $columns)
+ {
+ $this->columns = $columns;
+ $this->flippedColumns = null; // Reset, due to updated columns
+ return $this;
+ }
+
+ public function getColumns()
+ {
+ return $this->columns;
+ }
+
+ /**
+ * Deep clone self::$filter
+ */
+ public function __clone()
+ {
+ $this->filter = clone $this->filter;
+ }
+}