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: * * $query->order('field, 'ASC') * * * @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; } }