* * array( * 'virtualTable' => array( * 'fieldalias1' => 'queryColumn1', * 'fieldalias2' => 'queryColumn2', * .... * ), * 'virtualTable2' => array( * 'host' => 'host_name1' * ) * ) * * * * This allows you to select e.g. fieldalias1, which automatically calls the query code for joining 'virtualTable'. If * you afterwards select 'host', 'virtualTable2' will be joined. The joining logic is up to you, in order to make the * above example work you need to implement the joinVirtualTable() method which contain your * custom (Zend_Db) logic for joining, filtering and querying the data you want. * */ abstract class IdoQuery extends DbQuery { /** * The prefix to use * * @var string */ protected $prefix; /** * An array to map aliases to column names * * @var array */ protected $idxAliasColumn; /** * An array to map aliases to table names * * @var array */ protected $idxAliasTable; /** * An array to map custom aliases to aliases * * @var array */ protected $idxCustomAliases; /** * The column map containing all filterable columns * * This must be overwritten by child classes, in the format * array( * 'virtualTable' => array( * 'fieldalias1' => 'queryColumn1', * 'fieldalias2' => 'queryColumn2', * .... * ) * ) * * @var array */ protected $columnMap = array(); /** * Custom vars available for this query * * @var array */ protected $customVars = array(); /** * Printf compatible string to joins custom vars * * - %1$s Source field, contain the object_id * - %2$s Alias used for the relation * - %3$s Name of the CustomVariable * * @var string */ private $customVarsJoinTemplate = '%1$s = %2$s.object_id AND %2$s.varname = %3$s'; /** * An array with all 'virtual' tables that are already joined * * Virtual tables are the keys of the columnMap array and require a * join%VirtualTableName%() method to be defined in the concrete * query * * @var array */ protected $joinedVirtualTables = array(); /** * A map of virtual table names and corresponding hook instances * * Joins for those tables will be delegated to them * * @var array */ protected $hookedVirtualTables = array(); /** * List of column aliases used for sorting the result * * @var array */ protected $orderColumns = array(); /** * Table to columns map which have to be added to the GROUP BY list if the query is grouped * * @var array */ protected $groupBase = array(); /** * List of table names which initiate grouping if one of them is joined * * @var array */ protected $groupOrigin = array(); /** * Map of table names to query names for which to create subquery filters * * @var array */ protected $subQueryTargets = array(); /** * The primary key column for the instances table * * @var string */ protected $instance_id = 'instance_id'; /** * The primary key column for the objects table * * @var string */ protected $object_id = 'object_id'; /** * The primary key column for the acknowledgements table * * @var string */ protected $acknowledgement_id = 'acknowledgement_id'; /** * The primary key column for the commenthistory table * * @var string */ protected $commenthistory_id = 'commenthistory_id'; /** * The primary key column for the contactnotifications table * * @var string */ protected $contactnotification_id = 'contactnotification_id'; /** * The primary key column for the downtimehistory table * * @var string */ protected $downtimehistory_id = 'downtimehistory_id'; /** * The primary key column for the flappinghistory table * * @var string */ protected $flappinghistory_id = 'flappinghistory_id'; /** * The primary key column for the notifications table * * @var string */ protected $notification_id = 'notification_id'; /** * The primary key column for the statehistory table * * @var string */ protected $statehistory_id = 'statehistory_id'; /** * The primary key column for the comments table * * @var string */ protected $comment_id = 'comment_id'; /** * The primary key column for the customvariablestatus table * * @var string */ protected $customvariablestatus_id = 'customvariablestatus_id'; /** * The primary key column for the hoststatus table * * @var string */ protected $hoststatus_id = 'hoststatus_id'; /** * The primary key column for the programstatus table * * @var string */ protected $programstatus_id = 'programstatus_id'; /** * The primary key column for the runtimevariables table * * @var string */ protected $runtimevariable_id = 'runtimevariable_id'; /** * The primary key column for the scheduleddowntime table * * @var string */ protected $scheduleddowntime_id = 'scheduleddowntime_id'; /** * The primary key column for the servicestatus table * * @var string */ protected $servicestatus_id = 'servicestatus_id'; /** * The primary key column for the contactstatus table * * @var string */ protected $contactstatus_id = 'contactstatus_id'; /** * The primary key column for the commands table * * @var string */ protected $command_id = 'command_id'; /** * The primary key column for the contactgroup_members table * * @var string */ protected $contactgroup_member_id = 'contactgroup_member_id'; /** * The primary key column for the contactgroups table * * @var string */ protected $contactgroup_id = 'contactgroup_id'; /** * The primary key column for the contacts table * * @var string */ protected $contact_id = 'contact_id'; /** * The primary key column for the customvariables table * * @var string */ protected $customvariable_id = 'customvariable_id'; /** * The primary key column for the host_contactgroups table * * @var string */ protected $host_contactgroup_id = 'host_contactgroup_id'; /** * The primary key column for the host_contacts table * * @var string */ protected $host_contact_id = 'host_contact_id'; /** * The primary key column for the hostgroup_members table * * @var string */ protected $hostgroup_member_id = 'hostgroup_member_id'; /** * The primary key column for the hostgroups table * * @var string */ protected $hostgroup_id = 'hostgroup_id'; /** * The primary key column for the hosts table * * @var string */ protected $host_id = 'host_id'; /** * The primary key column for the service_contactgroup table * * @var string */ protected $service_contactgroup_id = 'service_contactgroup_id'; /** * The primary key column for the service_contact table * * @var string */ protected $service_contact_id = 'service_contact_id'; /** * The primary key column for the servicegroup_members table * * @var string */ protected $servicegroup_member_id = 'servicegroup_member_id'; /** * The primary key column for the servicegroups table * * @var string */ protected $servicegroup_id = 'servicegroup_id'; /** * The primary key column for the services table * * @var string */ protected $service_id = 'service_id'; /** * The primary key column for the timeperiods table * * @var string */ protected $timeperiod_id = 'timeperiod_id'; /** * An array containing Column names that cause an aggregation of the query * * @var array */ protected $aggregateColumnIdx = array(); /** * True to allow customvar filters and queries * * @var bool */ protected $allowCustomVars = false; /** * Current IDO version. This is bullshit and needs to be moved somewhere * else. As someone decided that we need no Backend-specific connection * class unfortunately there is no better place right now. And as of the * 'check_source' patch we need a quick fix immediately. So here you go. * * TODO: Fix this. * * @var string */ protected static $idoVersion; /** * List of column aliases mapped to their table where the COLLATE SQL-instruction has been removed * * This list is being populated in case of a PostgreSQL backend only, * to ensure case-insensitive string comparison in WHERE clauses. * * @var array */ protected $caseInsensitiveColumns; /** * Return true when the column is an aggregate column * * @param String $column The column to test * @return bool True when the column is an aggregate column */ public function isAggregateColumn($column) { return array_key_exists($column, $this->aggregateColumnIdx); } /** * Order the result by the given alias * * @param string $alias The column alias to order by * @param int $dir The sort direction or null to use the default direction * * @return $this */ public function order($alias, $dir = null) { $this->requireColumn($alias); if ($this->isCustomvar($alias)) { $column = $this->getCustomvarColumnName($alias); } elseif ($this->hasAliasName($alias)) { $column = $this->aliasToColumnName($alias); $table = $this->aliasToTableName($alias); if (isset($this->caseInsensitiveColumns[$table][$alias])) { $column = 'LOWER(' . $column . ')'; } } else { Logger::info('Can\'t order by column ' . $alias); return $this; } $this->orderColumns[] = $alias; return parent::order($column, $dir); } /** * Return true when the given field can be used for filtering * * @param String $field The field to test * @return bool True when the field can be used for querying, otherwise false */ public function isValidFilterTarget($field) { return $this->getMappedField($field) !== null; } /** * Return the resolved field for an alias * * @param String $field The alias to resolve * @return String The resolved alias or null if unknown */ public function getMappedField($field) { foreach ($this->columnMap as $columnSource => $columnSet) { if (isset($columnSet[$field])) { return $columnSet[$field]; } } if ($this->isCustomVar($field)) { return $this->getCustomvarColumnName($field); } return null; } public function distinct() { $this->select->distinct(); return $this; } /** * Prepare the given query so that it can be linked to the parent * * @param IdoQuery $query * @param string $name * @param FilterExpression $filter The filter which initiated the sub query * @param bool $and Whether it's an AND filter * @param bool $negate Whether it's an != filter * @param FilterExpression $additionalFilter Filters which should be applied to the "parent" query * * @return array The first value is their, the second our key column * * @throws NotImplementedError In case the given query is unknown */ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) { throw new NotImplementedError('Query "%s" is unknown', $name); } /** * Create and return a sub-query filter for the given filter expression * * @param FilterExpression $filter * @param string $queryName * * @return Filter * * @throws QueryException */ protected function createSubQueryFilter(FilterExpression $filter, $queryName) { $expr = $filter->getExpression(); $op = $filter->getSign(); if ($op === '=' && ! is_array($expr) && $op !== '!=') { // We're joining a subquery only if the filter is enclosed in parentheses or if it's a != filter, // e.g. hostgroup_name=(linux...), hostgroup_name!=linux, hostgroup_name!=(linux...) throw new NotImplementedError(''); } $subQuery = $this->createSubQuery($queryName); $subQuery->setIsSubQuery(); $subQueryFilter = clone $filter; if ($op === '!=') { $negate = true; if (! is_array($expr)) { // We assume that expression is an array later on but we'll support subquery joins for != filters // which are not enclosed in parentheses $expr = [$expr]; } } else { $negate = false; } if (count($expr) === 1 && strpos($expr[0], '&') !== false) { // Our current filter implementation does not specify & as a control character so the count of the // expression array is always one in this case $expr = array_unique(explode('&', $expr[0])); $subQueryFilter->setExpression($expr); $and = true; } else { // Or filters are respected by our filter implementation. No special handling needed here $and = false; } $alias = $filter->getColumn(); $column = $subQuery->aliasToColumnName($alias); if (isset($this->caseInsensitiveColumns[$subQuery->aliasToTableName($alias)][$alias])) { $column = 'LOWER( ' . $column . ' )'; $subQueryFilter->setColumn($column); $subQueryFilter->setExpression(array_map('strtolower', (array) $subQueryFilter->getExpression())); } else { $subQueryFilter->setColumn($column); } $additional = null; list($theirs, $ours) = $this->joinSubQuery($subQuery, $queryName, $subQueryFilter, $and, $negate, $additional); $zendSelect = $subQuery->select(); $fromPart = $zendSelect->getPart($zendSelect::FROM); $zendSelect->reset($zendSelect::FROM); foreach ($fromPart as $correlationName => $joinOptions) { if (isset($joinOptions['joinCondition'])) { $joinOptions['joinCondition'] = preg_replace( '/(?<=^|\s)\w+(?=\.)/', 'sub_$0', $joinOptions['joinCondition'] ); } $name = ['sub_' . $correlationName => $joinOptions['tableName']]; switch ($joinOptions['joinType']) { case $zendSelect::FROM: $zendSelect->from($name); break; case $zendSelect::INNER_JOIN: $zendSelect->joinInner($name, $joinOptions['joinCondition'], null); break; case $zendSelect::LEFT_JOIN: $zendSelect->joinLeft($name, $joinOptions['joinCondition'], null); break; default: // TODO: Add support for other join types if required? throw new QueryException( 'Unsupported join type %s. Cannot create subquery filter.', $joinOptions['joinType'] ); } } if ($and || $negate) { // Having is only required for AND and != filters, // e.g. hostgroup_name=(ping&linux), hostgroup_name!=ping, hostgroup_name!=(ping|linux) $groups = $subQuery->getGroup(); if (! empty($groups)) { $group = $groups[0]; $group = preg_replace('/(?<=^|\s)\w+(?=\.)/', 'sub_$0', $group); $cnt = count($expr); $subQuery->select()->having("COUNT(DISTINCT $group) >= $cnt"); } } $subQueryFilter->setColumn(preg_replace( '/(?<=^|\s)\w+(?=\.)/', 'sub_$0', $column )); if ($negate) { // != will be NOT EXISTS later $subQueryFilter = $subQueryFilter->setSign('='); } $subQueryFilter = $subQueryFilter->andFilter(Filter::where( preg_replace('/(?<=^|\s)\w+(?=\.)/', 'sub_$0', $theirs), new Zend_Db_Expr($ours) )); $subQuery ->setFilter($subQueryFilter) ->clearGroupingRules() ->select() ->reset('columns') ->columns([new Zend_Db_Expr('1')]); // EXISTS is the column name because without any column $this->isCustomVar() fails badly otherwise. // Additionally it bypasses the non-required optimizations made by our filter rendering implementation. $exists = new FilterExpression($negate ? 'NOT EXISTS' : 'EXISTS', '', new Zend_Db_Expr($subQuery)); if ($additional !== null) { return Filter::matchAll($exists, $additional); } return $exists; } protected function requireFilterColumns(Filter $filter) { if ($filter instanceof FilterExpression) { $alias = $filter->getColumn(); $virtualTable = $this->aliasToTableName($alias); if (isset($this->subQueryTargets[$virtualTable])) { try { return $this->createSubQueryFilter($filter, $this->subQueryTargets[$virtualTable]); } catch (NotImplementedError $e) { // We don't want to create subquery filters in all cases } } $this->requireColumn($alias); if ($this->isCustomvar($alias)) { $column = $this->getCustomvarColumnName($alias); } else { $column = $this->aliasToColumnName($alias); if (isset($this->caseInsensitiveColumns[$this->aliasToTableName($alias)][$alias])) { $column = 'LOWER(' . $column . ')'; $expression = $filter->getExpression(); if (is_array($expression)) { $filter->setExpression(array_map('strtolower', $expression)); } else { $filter->setExpression(strtolower($expression)); } } } $filter->setColumn($column); } else { if (! $filter instanceof FilterNot) { // Allow subquery filters in a filter chain $columns = $filter->listFilteredColumns(); if (count($columns) === 1) { $column = $columns[0]; $virtualTable = $this->aliasToTableName($column); if (isset($this->subQueryTargets[$virtualTable])) { $lastSign = null; $filters = []; $expressions = []; foreach ($filter->filters() as $child) { switch (true) { case $child instanceof FilterExpression: $expression = $child->getExpression(); if (! is_array($expression)) { break; } // Move to default default: $filters[] = $child; continue 2; } if ($lastSign === null) { $lastSign = $child->getSign(); } else { $sign = $child->getSign(); if ($sign !== $lastSign) { $filters[] = new FilterExpression( $column, $lastSign, $filter->getOperatorSymbol() === '&' ? [implode('&', $expressions)] : $expressions ); $expressions = []; $lastSign = $sign; } } $expressions[] = $expression; } if (! empty($expressions)) { $filters[] = new FilterExpression( $column, $lastSign, $filter->getOperatorSymbol() === '&' ? [implode('&', $expressions)] : $expressions ); } $filter->setFilters($filters); } } } foreach ($filter->filters() as $child) { $replacement = $this->requireFilterColumns($child); if ($replacement !== null) { // setId($child->getId()) is performed because replaceById() doesn't already do it $filter->replaceById($child->getId(), $replacement->setId($child->getId())); } } } } /** * {@inheritdoc} */ public function addFilter(Filter $filter) { $filter = clone $filter; return parent::addFilter($this->requireFilterColumns($filter) ?: $filter); } public function where($condition, $value = null) { $this->requireColumn($condition); $col = $this->getMappedField($condition); if ($col === null) { throw new IcingaException( 'No such field: %s', $condition ); } return parent::where($col, $value); } /** * Add a filter expression, with as less validation as possible * * @param FilterExpression $ex * * @internal If you use this outside the monitoring module, it's your fault if something breaks * @return $this */ public function whereEx(FilterExpression $ex) { $this->requireColumn($ex->getColumn()); $col = $this->getMappedField($ex->getColumn()); if ($col === null) { throw new IcingaException( 'No such field: %s', $ex->getColumn() ); } parent::addFilter((clone $ex)->setColumn($col)); return $this; } /** * Return true if an field contains an explicit timestamp * * @param string $field The field to test for containing an timestamp * * @return bool True when the field represents an timestamp */ public function isTimestamp($field) { if ($this->isCustomVar($field)) { return false; } return stripos($this->getMappedField($field) ?: $field, 'UNIX_TIMESTAMP') !== false; } /** * Return whether the given alias provides case insensitive value comparison * * @param string $alias * * @return bool */ public function isCaseInsensitive($alias) { if ($this->isCustomVar($alias)) { return false; } $column = $this->getMappedField($alias); if (! $column) { return false; } if (empty($this->caseInsensitiveColumns)) { return preg_match('/ COLLATE .+$/', $column) === 1; } if (strpos($column, 'LOWER') === 0) { return true; } $table = $this->aliasToTableName($alias); if (! $table) { return false; } return isset($this->caseInsensitiveColumns[$table][$alias]); } /** * Return our column map * * Might be useful for hooks * * @return array */ public function getColumnMap() { return $this->columnMap; } /** * Apply oracle specific query initialization */ private function initializeForOracle() { // Oracle uses the reserved field 'id' for primary keys, so // these must be used instead of the normally defined ids $this->object_id = $this->host_id = $this->service_id = $this->hostgroup_id = $this->servicegroup_id = $this->contact_id = $this->contactgroup_id = 'id'; $this->customVarsJoinTemplate = '%1$s = %2$s.object_id AND LOWER(%2$s.varname) = %3$s'; foreach ($this->columnMap as &$columns) { foreach ($columns as &$value) { $value = preg_replace('/UNIX_TIMESTAMP/', 'localts2unixts', $value); $value = preg_replace('/ COLLATE .+$/', '', $value); } } } /** * Apply PostgreSQL specific query initialization */ private function initializeForPostgres() { $this->customVarsJoinTemplate = '%1$s = %2$s.object_id AND LOWER(%2$s.varname) = %3$s'; foreach ($this->columnMap as $table => & $columns) { foreach ($columns as $alias => & $column) { if ($column === null) { continue; } // Using a regex here because COLLATE may occur anywhere in the string $column = preg_replace('/ COLLATE .+$/', '', $column, -1, $count); if ($count > 0) { $this->caseInsensitiveColumns[$table][$alias] = true; } $column = preg_replace( '/inet_aton\(([[:word:].]+)\)/i', '(CASE WHEN $1 ~ \'(?:[0-9]{1,3}\\\\.){3}[0-9]{1,3}\' THEN $1::inet - \'0.0.0.0\' ELSE NULL END)', $column ); if (version_compare($this->getIdoVersion(), '1.14.2', '>=')) { $column = str_replace('NOW()', 'NOW() AT TIME ZONE \'UTC\'', $column); } else { $column = preg_replace( '/UNIX_TIMESTAMP(\((?>[^()]|(?-1))*\))/i', 'CASE WHEN ($1 < \'1970-01-03 00:00:00+00\'::timestamp with time zone) THEN 0 ELSE UNIX_TIMESTAMP($1) END', $column ); } } } } /** * Set up this query and join the initial tables * * @see IdoQuery::initializeForPostgres For postgresql specific setup */ protected function init() { parent::init(); $this->prefix = $this->ds->getTablePrefix(); foreach (Hook::all('monitoring/idoQueryExtension') as $hook) { $extensions = $hook->extendColumnMap($this); if (! is_array($extensions)) { continue; } foreach ($extensions as $vTable => $cols) { if (! array_key_exists($vTable, $this->columnMap)) { $this->hookedVirtualTables[$vTable] = $hook; $this->columMap[$vTable] = array(); } foreach ($cols as $k => $v) { $this->columnMap[$vTable][$k] = $v; } } } $dbType = $this->ds->getDbType(); if ($dbType === 'oracle') { $this->initializeForOracle(); } elseif ($dbType === 'pgsql') { $this->initializeForPostgres(); } else { $charset = $this->ds->getConfig()->get('charset') ?: 'latin1'; $this->customVarsJoinTemplate .= " COLLATE {$charset}_general_ci"; } $this->joinBaseTables(); $this->select->columns($this->columns); $this->prepareAliasIndexes(); } /** * Join the base tables for this query */ protected function joinBaseTables() { reset($this->columnMap); $table = key($this->columnMap); $this->select->from( array($table => $this->prefix . $table), array() ); $this->joinedVirtualTables = array($table => true); } /** * Populates the idxAliasTAble and idxAliasColumn properties */ protected function prepareAliasIndexes() { foreach ($this->columnMap as $tbl => & $cols) { foreach ($cols as $alias => $col) { $this->idxAliasTable[$alias] = $tbl; $this->idxAliasColumn[$alias] = preg_replace('~\n\s*~', ' ', $col); } } } /** * Resolve columns aliases to their database field using the columnMap * * @param array $columns * * @return array */ public function resolveColumns($columns) { $resolvedColumns = array(); foreach ($columns as $alias => $col) { if ($col instanceof Zend_Db_Expr) { // Support selecting NULL as column for example $resolvedColumns[$alias] = $col; continue; } $this->requireColumn($col); if ($this->isCustomvar($col)) { $name = $this->getCustomvarColumnName($col); } else { $name = $this->aliasToColumnName($col); } if (is_int($alias)) { $alias = $col; } else { $this->idxCustomAliases[$alias] = $col; } $resolvedColumns[$alias] = preg_replace('|\n|', ' ', $name); } return $resolvedColumns; } /** * Return all columns that will be selected when no columns are given in the constructor or from * * @return array An array of column aliases */ public function getDefaultColumns() { reset($this->columnMap); $table = key($this->columnMap); return array_keys($this->columnMap[$table]); } /** * Modify the query to the given alias can be used in the result set or queries * * This calls requireVirtualTable if needed * * @param string $alias The alias of the column to require * * @return $this Fluent interface * @see IdoQuery::requireVirtualTable The method initializing required joins * @throws \Icinga\Exception\ProgrammingError When an unknown column is requested */ public function requireColumn($alias) { if ($this->hasAliasName($alias)) { $this->requireVirtualTable($this->aliasToTableName($alias)); } elseif ($this->isCustomVar($alias)) { $this->requireCustomvar($alias); } else { throw new ProgrammingError( '%s : Got invalid column: %s', get_called_class(), $alias ); } return $this; } /** * Return true if the given alias exists * * @param String $alias The alias to test for * @return bool True when the alias exists, otherwise false */ protected function hasAliasName($alias) { return array_key_exists($alias, $this->idxAliasColumn); } /** * Require a virtual table for the given table name if not already required * * @param String $name The table name to require * @return $this Fluent interface */ protected function requireVirtualTable($name) { if ($this->hasJoinedVirtualTable($name)) { return $this; } if ($this->virtualTableIsHooked($name)) { return $this->joinHookedVirtualTable($name); } else { return $this->joinVirtualTable($name); } } /** * Whether a given virtual table name has been provided by a hook * * @param string $name Virtual table name * * @return boolean */ protected function virtualTableIsHooked($name) { return array_key_exists($name, $this->hookedVirtualTables); } protected function conflictsWithVirtualTable($name) { if ($this->hasJoinedVirtualTable($name)) { throw new ProgrammingError( 'IDO query virtual table conflict with "%s"', $name ); } return $this; } /** * Call the method for joining a virtual table * * This requires a join$Table() method to exist * * @param String $table The table to join by calling join$Table() in the concrete implementation * @return $this Fluent interface * * @throws \Icinga\Exception\ProgrammingError If the join method for this table does not exist */ protected function joinVirtualTable($table) { $func = 'join' . ucfirst($table); if (method_exists($this, $func)) { $this->$func(); } else { throw new ProgrammingError( 'Cannot join "%s", no such table found', $table ); } $this->joinedVirtualTables[$table] = true; return $this; } /** * Tell a hook to join a virtual table * * @param String $table * @return $this */ protected function joinHookedVirtualTable($table) { $this->hookedVirtualTables[$table]->joinVirtualTable($this, $table); $this->joinedVirtualTables[$table] = true; return $this; } /** * Get the table for a specific alias * * @param String $alias The alias to request the table for * @return String The table for the alias or null if it doesn't exist */ protected function aliasToTableName($alias) { return isset($this->idxAliasTable[$alias]) ? $this->idxAliasTable[$alias] : null; } /** * Return whether this query allows to join custom variables * * @return bool */ public function allowsCustomVars() { return $this->allowCustomVars; } /** * Return true if the given alias denotes a custom variable * * @param String $alias The alias to test for being a customvariable * @return bool True if the alias is a customvariable, otherwise false */ protected function isCustomVar($alias) { return $this->allowCustomVars && $alias[0] === '_'; } protected function requireCustomvar($customvar) { if (! $this->hasCustomvar($customvar)) { $this->joinCustomvar($customvar); } return $this; } protected function hasCustomvar($customvar) { return array_key_exists(strtolower($customvar), $this->customVars); } protected function joinCustomvar($customvar) { // TODO: This is not generic enough yet list($type, $name) = $this->customvarNameToTypeName($customvar); $alias = ($type === 'host' ? 'hcv_' : 'scv_') . preg_replace('~[^a-zA-Z0-9_]~', '_', $name); // We're replacing any problematic char with an underscore, which will lead to duplicates, this avoids them $from = $this->select->getPart(Zend_Db_Select::FROM); for ($i = 2; array_key_exists($alias, $from); $i++) { $alias = $alias . '_' . $i; } $this->customVars[strtolower($customvar)] = $alias; if ($type === 'host') { if ($this instanceof ServicecommentQuery || $this instanceof ServicedowntimeQuery || $this instanceof ServicecommenthistoryQuery || $this instanceof ServicedowntimestarthistoryQuery || $this instanceof ServiceflappingstarthistoryQuery || $this instanceof ServicegroupQuery || $this instanceof ServicenotificationQuery || $this instanceof ServicestatehistoryQuery || $this instanceof ServicestatusQuery ) { $this->requireVirtualTable('services'); $leftcol = 's.host_object_id'; } else { $leftcol = 'ho.object_id'; if (! $this->hasJoinedTable('ho')) { $this->requireVirtualTable('hosts'); } } } else { // $type === 'service' $leftcol = 'so.object_id'; if (! $this->hasJoinedTable('so')) { $this->requireVirtualTable('services'); } } $mapped = $this->getMappedField($leftcol); if ($mapped !== null) { $this->requireColumn($leftcol); $leftcol = $mapped; } $joinOn = sprintf( $this->customVarsJoinTemplate, $leftcol, $alias, $this->db->quote($name) ); $this->select->joinLeft( array($alias => $this->prefix . 'customvariablestatus'), $joinOn, array() ); return $this; } protected function customvarNameToTypeName($customvar) { $customvar = strtolower($customvar); if (! preg_match('~^_(host|service)_(.+)$~', $customvar, $m)) { throw new ProgrammingError( 'Got invalid custom var: "%s"', $customvar ); } return array($m[1], $m[2]); } protected function hasJoinedVirtualTable($name) { return array_key_exists($name, $this->joinedVirtualTables); } /** * Get the query column of a already joined custom variable * * @param string $customvar * * @return string * @throws QueryException If the custom variable has not been joined */ protected function getCustomvarColumnName($customvar) { if (! isset($this->customVars[($customvar = strtolower($customvar))])) { throw new QueryException('Custom variable %s has not been joined', $customvar); } return $this->customVars[$customvar] . '.varvalue'; } public function aliasToColumnName($alias) { return $this->idxAliasColumn[$alias]; } /** * Get the alias of a column expression as defined in the {@link $columnMap} property. * * @param string $alias Potential custom alias * * @return string */ public function customAliasToAlias($alias) { if (isset($this->idxCustomAliases[$alias])) { return $this->idxCustomAliases[$alias]; } return $alias; } /** * Create a sub query * * @param string $queryName * @param array $columns * * @return static */ protected function createSubQuery($queryName, $columns = array()) { $class = '\\' . substr(__CLASS__, 0, strrpos(__CLASS__, '\\') + 1) . ucfirst($queryName) . 'Query'; $query = new $class($this->ds, $columns); return $query; } /** * Set columns to select * * @param array $columns * * @return $this */ public function columns(array $columns) { $this->idxCustomAliases = array(); $this->columns = $this->resolveColumns($columns); // TODO: we need to refresh our select! // $this->select->columns($columns); return $this; } public function clearGroupingRules() { $this->groupBase = array(); $this->groupOrigin = array(); return $this; } /** * Register the GROUP BY columns required for the given alias * * @param string $alias The alias to register columns for * @param string $table The table the given alias is associated with * @param array $groupedColumns The grouping columns registered so far * @param array $groupedTables The tables for which columns were registered so far */ protected function registerGroupColumns($alias, $table, array &$groupedColumns, array &$groupedTables) { switch ($table) { case 'checktimeperiods': $groupedColumns[] = 'ctp.timeperiod_id'; break; case 'contacts': $groupedColumns[] = 'co.object_id'; $groupedColumns[] = 'c.contact_id'; break; case 'hostobjects': $groupedColumns[] = 'ho.object_id'; break; case 'hosts': $groupedColumns[] = 'h.host_id'; break; case 'hostgroups': $groupedColumns[] = 'hgo.object_id'; $groupedColumns[] = 'hg.hostgroup_id'; break; case 'hoststatus': $groupedColumns[] = 'hs.hoststatus_id'; break; case 'instances': $groupedColumns[] = 'i.instance_id'; break; case 'servicegroups': $groupedColumns[] = 'sgo.object_id'; $groupedColumns[] = 'sg.servicegroup_id'; break; case 'serviceobjects': $groupedColumns[] = 'so.object_id'; break; case 'serviceproblemsummary': $groupedColumns[] = 'sps.unhandled_services_count'; break; case 'services': $groupedColumns[] = 'so.object_id'; $groupedColumns[] = 's.service_id'; break; case 'servicestatus': $groupedColumns[] = 'ss.servicestatus_id'; break; default: return; } $groupedTables[$table] = true; } /** * {@inheritdoc} */ public function getGroup() { $group = parent::getGroup() ?: array(); if (! is_array($group)) { $group = array($group); } $joinedOrigins = array_filter($this->groupOrigin, array($this, 'hasJoinedVirtualTable')); if (empty($joinedOrigins)) { return $group; } $groupedTables = array(); foreach ($this->groupBase as $baseTable => $aliasedPks) { if (! $this->hasJoinedVirtualTable($baseTable)) { continue; } $groupedTables[$baseTable] = true; foreach ($aliasedPks as $aliasedPk) { $group[] = $aliasedPk; } } foreach (new ColumnFilterIterator($this->columns) as $desiredAlias => $desiredColumn) { $alias = is_string($desiredAlias) ? $this->customAliasToAlias($desiredAlias) : $desiredColumn; if ($this->isCustomVar($alias) && $this->getDatasource()->getDbType() === 'pgsql') { $table = $this->customVars[$alias]; if (! isset($groupedTables[$table])) { $group[] = $this->getCustomvarColumnName($alias); $groupedTables[$table] = true; } continue; } $table = $this->aliasToTableName($alias); if ($table && !isset($groupedTables[$table]) && ( in_array($table, $joinedOrigins, true) || $this->getDatasource()->getDbType() === 'pgsql') ) { $this->registerGroupColumns($alias, $table, $group, $groupedTables); } } if (! empty($group) && $this->getDatasource()->getDbType() === 'pgsql') { foreach (new ColumnFilterIterator($this->orderColumns) as $alias) { if ($this->isCustomVar($alias)) { $table = $this->customVars[$alias]; if (! isset($groupedTables[$table])) { $group[] = $this->getCustomvarColumnName($alias); $groupedTables[$table] = true; } continue; } $table = $this->aliasToTableName($alias); if ($table && !isset($groupedTables[$table]) && !in_array($this->getMappedField($alias), $this->columns, true) ) { $this->registerGroupColumns($alias, $table, $group, $groupedTables); } } } return array_unique($group); } // TODO: Move this away, see note related to $idoVersion var protected function getIdoVersion() { if (self::$idoVersion === null) { $dbconf = $this->db->getConfig(); $id = $dbconf['host'] . '/' . $dbconf['dbname']; $session = null; if (Icinga::app()->isWeb()) { // TODO: Once we have version per connection we should choose a // namespace based on resource name $session = Session::getSession()->getNamespace('monitoring/ido/' . $id); if (isset($session->version)) { self::$idoVersion = $session->version; return self::$idoVersion; } } self::$idoVersion = $this->db->fetchOne( $this->db->select()->from($this->prefix . 'dbversion', 'version') ); if ($session !== null) { $session->version = self::$idoVersion; } } return self::$idoVersion; } /** * Return the name of the primary key column for the given table name * * @param string $table * * @return string * * @throws ProgrammingError In case $table is unknown */ protected function getPrimaryKeyColumn($table) { // TODO: For god's sake, make this being a mapping // (instead of matching a ton of properties using a ridiculous long switch case) switch ($table) { case 'instances': return $this->instance_id; case 'objects': return $this->object_id; case 'acknowledgements': return $this->acknowledgement_id; case 'commenthistory': return $this->commenthistory_id; case 'contactnotifiations': return $this->contactnotification_id; case 'downtimehistory': return $this->downtimehistory_id; case 'flappinghistory': return $this->flappinghistory_id; case 'notifications': return $this->notification_id; case 'statehistory': return $this->statehistory_id; case 'comments': return $this->comment_id; case 'customvariablestatus': return $this->customvariablestatus_id; case 'hoststatus': return $this->hoststatus_id; case 'programstatus': return $this->programstatus_id; case 'runtimevariables': return $this->runtimevariable_id; case 'scheduleddowntime': return $this->scheduleddowntime_id; case 'servicestatus': return $this->servicestatus_id; case 'contactstatus': return $this->contactstatus_id; case 'commands': return $this->command_id; case 'contactgroup_members': return $this->contactgroup_member_id; case 'contactgroups': return $this->contactgroup_id; case 'contacts': return $this->contact_id; case 'customvariables': return $this->customvariable_id; case 'host_contactgroups': return $this->host_contactgroup_id; case 'host_contacts': return $this->host_contact_id; case 'hostgroup_members': return $this->hostgroup_member_id; case 'hostgroups': return $this->hostgroup_id; case 'hosts': return $this->host_id; case 'service_contactgroups': return $this->service_contactgroup_id; case 'service_contacts': return $this->service_contact_id; case 'servicegroup_members': return $this->servicegroup_member_id; case 'servicegroups': return $this->servicegroup_id; case 'services': return $this->service_id; case 'timeperiods': return $this->timeperiod_id; default: throw new ProgrammingError('Cannot provide a primary key column. Table "%s" is unknown', $table); } } }