diff --git a/library/Icinga/Repository/DbRepository.php b/library/Icinga/Repository/DbRepository.php index 93f9ceaeb..0ef4d32e9 100644 --- a/library/Icinga/Repository/DbRepository.php +++ b/library/Icinga/Repository/DbRepository.php @@ -69,14 +69,28 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, * * @var array */ - protected $statementTableMap; + protected $statementAliasTableMap; /** * A flattened array to map statement columns to aliases * * @var array */ - protected $statementColumnMap; + protected $statementAliasColumnMap; + + /** + * An array to map table names to statement columns + * + * @var array + */ + protected $statementColumnTableMap; + + /** + * A flattened array to map aliases to statement columns + * + * @var array + */ + protected $statementColumnAliasMap; /** * List of columns where the COLLATE SQL-instruction has been removed @@ -348,13 +362,13 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, * * @return array */ - protected function getStatementTableMap() + protected function getStatementAliasTableMap() { - if ($this->statementTableMap === null) { + if ($this->statementAliasTableMap === null) { $this->initializeStatementMaps(); } - return $this->statementTableMap; + return $this->statementAliasTableMap; } /** @@ -362,40 +376,87 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, * * @return array */ - protected function getStatementColumnMap() + protected function getStatementAliasColumnMap() { - if ($this->statementColumnMap === null) { + if ($this->statementAliasColumnMap === null) { $this->initializeStatementMaps(); } - return $this->statementColumnMap; + return $this->statementAliasColumnMap; } /** - * Initialize $this->statementTableMap and $this->statementColumnMap + * Return an array to map table names to statement columns + * + * @return array + */ + protected function getStatementColumnTableMap() + { + if ($this->statementColumnTableMap === null) { + $this->initializeStatementMaps(); + } + + return $this->statementColumnTableMap; + } + + /** + * Return a flattened array to map aliases to statement columns + * + * @return array + */ + protected function getStatementColumnAliasMap() + { + if ($this->statementColumnAliasMap === null) { + $this->initializeStatementMaps(); + } + + return $this->statementColumnAliasMap; + } + + /** + * Initialize $this->statementAliasTableMap and $this->statementAliasColumnMap */ protected function initializeStatementMaps() { - $this->statementTableMap = array(); - $this->statementColumnMap = array(); + $this->statementAliasTableMap = array(); + $this->statementAliasColumnMap = array(); + $this->statementColumnTableMap = array(); + $this->statementColumnAliasMap = array(); foreach ($this->getStatementColumns() as $table => $columns) { foreach ($columns as $alias => $column) { $key = is_string($alias) ? $alias : $column; - if (array_key_exists($key, $this->statementTableMap)) { - if ($this->statementTableMap[$key] !== null) { - $existingTable = $this->statementTableMap[$key]; - $existingColumn = $this->statementColumnMap[$key]; - $this->statementTableMap[$existingTable . '.' . $key] = $existingTable; - $this->statementColumnMap[$existingTable . '.' . $key] = $existingColumn; - $this->statementTableMap[$key] = null; - $this->statementColumnMap[$key] = null; + if (array_key_exists($key, $this->statementAliasTableMap)) { + if ($this->statementAliasTableMap[$key] !== null) { + $existingTable = $this->statementAliasTableMap[$key]; + $existingColumn = $this->statementAliasColumnMap[$key]; + $this->statementAliasTableMap[$existingTable . '.' . $key] = $existingTable; + $this->statementAliasColumnMap[$existingTable . '.' . $key] = $existingColumn; + $this->statementAliasTableMap[$key] = null; + $this->statementAliasColumnMap[$key] = null; } - $this->statementTableMap[$table . '.' . $key] = $table; - $this->statementColumnMap[$table . '.' . $key] = $column; + $this->statementAliasTableMap[$table . '.' . $key] = $table; + $this->statementAliasColumnMap[$table . '.' . $key] = $column; } else { - $this->statementTableMap[$key] = $table; - $this->statementColumnMap[$key] = $column; + $this->statementAliasTableMap[$key] = $table; + $this->statementAliasColumnMap[$key] = $column; + } + + if (array_key_exists($column, $this->statementColumnTableMap)) { + if ($this->statementColumnTableMap[$column] !== null) { + $existingTable = $this->statementColumnTableMap[$column]; + $existingAlias = $this->statementColumnAliasMap[$column]; + $this->statementColumnTableMap[$existingTable . '.' . $column] = $existingTable; + $this->statementColumnAliasMap[$existingTable . '.' . $column] = $existingAlias; + $this->statementColumnTableMap[$column] = null; + $this->statementColumnAliasMap[$column] = null; + } + + $this->statementColumnTableMap[$table . '.' . $column] = $table; + $this->statementColumnAliasMap[$table . '.' . $column] = $key; + } else { + $this->statementColumnTableMap[$column] = $table; + $this->statementColumnAliasMap[$column] = $key; } } } @@ -529,6 +590,28 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, return parent::resolveQueryColumnAlias($this->removeTablePrefix($this->clearTableAlias($table)), $alias); } + /** + * Return the alias for the given query column name or null in case the query column name does not exist + * + * @param array|string $table + * @param string $column + * + * @return string|null + */ + public function reassembleQueryColumnAlias($table, $column) + { + $alias = parent::reassembleQueryColumnAlias($this->removeTablePrefix($this->clearTableAlias($table)), $column); + if ( + $alias === null + && !$this->validateQueryColumnAssociation($table, $column) + && ($tableName = $this->findTableName($column)) + ) { + return parent::reassembleQueryColumnAlias($tableName, $column); + } + + return $alias; + } + /** * Return whether the given query column name or alias is available in the given table * @@ -607,14 +690,35 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, */ public function resolveStatementColumnAlias($table, $alias) { - $statementColumnMap = $this->getStatementColumnMap(); - if (isset($statementColumnMap[$alias])) { - return $statementColumnMap[$alias]; + $statementAliasColumnMap = $this->getStatementAliasColumnMap(); + if (isset($statementAliasColumnMap[$alias])) { + return $statementAliasColumnMap[$alias]; } $prefixedAlias = $this->removeTablePrefix($table) . '.' . $alias; - if (isset($statementColumnMap[$prefixedAlias])) { - return $statementColumnMap[$prefixedAlias]; + if (isset($statementAliasColumnMap[$prefixedAlias])) { + return $statementAliasColumnMap[$prefixedAlias]; + } + } + + /** + * Return the alias for the given statement column name or null in case the statement column does not exist + * + * @param string $table + * @param string $column + * + * @return string|null + */ + public function reassembleStatementColumnAlias($table, $column) + { + $statementColumnAliasMap = $this->getStatementColumnAliasMap(); + if (isset($statementColumnAliasMap[$column])) { + return $statementColumnAliasMap[$column]; + } + + $prefixedColumn = $this->removeTablePrefix($table) . '.' . $column; + if (isset($statementColumnAliasMap[$prefixedColumn])) { + return $statementColumnAliasMap[$prefixedColumn]; } } @@ -628,13 +732,20 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, */ public function validateStatementColumnAssociation($table, $alias) { - $statementTableMap = $this->getStatementTableMap(); - if (isset($statementTableMap[$alias])) { - return $statementTableMap[$alias] === $this->removeTablePrefix($table); + $tableName = $this->removeTablePrefix($table); + + $statementAliasTableMap = $this->getStatementAliasTableMap(); + if (isset($statementAliasTableMap[$alias])) { + return $statementAliasTableMap[$alias] === $tableName; } - $prefixedAlias = $this->removeTablePrefix($table) . '.' . $alias; - return isset($statementTableMap[$prefixedAlias]); + $statementColumnTableMap = $this->getStatementColumnTableMap(); + if (isset($statementColumnTableMap[$alias])) { + return $statementColumnTableMap[$alias] === $tableName; + } + + $prefixedAlias = $tableName . '.' . $alias; + return isset($statementAliasTableMap[$prefixedAlias]) || isset($statementColumnTableMap[$prefixedAlias]); } /** @@ -648,7 +759,8 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, public function hasStatementColumn($table, $name) { if ( - $this->resolveStatementColumnAlias($table, $name) === null + ($this->resolveStatementColumnAlias($table, $name) === null + && $this->reassembleStatementColumnAlias($table, $name) === null) || !$this->validateStatementColumnAssociation($table, $name) ) { return parent::hasStatementColumn($table, $name); @@ -669,11 +781,15 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, */ public function requireStatementColumn($table, $name) { - if (($column = $this->resolveStatementColumnAlias($table, $name)) === null) { + if (($column = $this->resolveStatementColumnAlias($table, $name)) !== null) { + $alias = $name; + } elseif (($alias = $this->reassembleStatementColumnAlias($table, $name)) !== null) { + $column = $name; + } else { return parent::requireStatementColumn($table, $name); } - if (! $this->validateStatementColumnAssociation($table, $name)) { + if (! $this->validateStatementColumnAssociation($table, $alias)) { throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table); } @@ -706,7 +822,9 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, ); } - $column = $this->resolveQueryColumnAlias($tableName, $name); + if (($column = $this->resolveQueryColumnAlias($tableName, $name)) === null) { + $column = $name; + } $prefixedTableName = $this->prependTablePrefix($tableName); if ($query->getQuery()->hasJoinedTable($prefixedTableName)) { @@ -741,11 +859,16 @@ abstract class DbRepository extends Repository implements Extensible, Updatable, return $aliasTableMap[$column]; } + $columnTableMap = $this->getColumnTableMap(); + if (isset($columnTableMap[$column])) { + return $columnTableMap[$column]; + } + // TODO(jom): Elaborate whether it makes sense to throw ProgrammingError // instead (duplicate aliases in different tables?) - foreach ($aliasTableMap as $alias => $table) { - if (strpos($alias, '.') !== false) { - list($_, $alias) = explode('.', $column, 2); + foreach ($aliasTableMap as $prefixedAlias => $table) { + if (strpos($prefixedAlias, '.') !== false) { + list($_, $alias) = explode('.', $prefixedAlias, 2); if ($alias === $column) { return $table; } diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php index 98b3e0ab4..ada50e3f6 100644 --- a/library/Icinga/Repository/Repository.php +++ b/library/Icinga/Repository/Repository.php @@ -134,6 +134,20 @@ abstract class Repository implements Selectable */ protected $aliasColumnMap; + /** + * An array to map table names to query columns + * + * @var array + */ + protected $columnTableMap; + + /** + * A flattened array to map aliases to query columns + * + * @var array + */ + protected $columnAliasMap; + /** * Create a new repository object * @@ -144,6 +158,8 @@ abstract class Repository implements Selectable $this->ds = $ds; $this->aliasTableMap = array(); $this->aliasColumnMap = array(); + $this->columnTableMap = array(); + $this->columnAliasMap = array(); $this->init(); } @@ -347,6 +363,34 @@ abstract class Repository implements Selectable return $this->aliasColumnMap; } + /** + * Return an array to map table names to query columns + * + * @return array + */ + protected function getColumnTableMap() + { + if (empty($this->columnTableMap)) { + $this->initializeAliasMaps(); + } + + return $this->columnTableMap; + } + + /** + * Return a flattened array to map aliases to query columns + * + * @return array + */ + protected function getColumnAliasMap() + { + if (empty($this->columnAliasMap)) { + $this->initializeAliasMaps(); + } + + return $this->columnAliasMap; + } + /** * Initialize $this->aliasTableMap and $this->aliasColumnMap * @@ -384,6 +428,23 @@ abstract class Repository implements Selectable $this->aliasTableMap[$key] = $table; $this->aliasColumnMap[$key] = $column; } + + if (array_key_exists($column, $this->columnTableMap)) { + if ($this->columnTableMap[$column] !== null) { + $existingTable = $this->columnTableMap[$column]; + $existingAlias = $this->columnAliasMap[$column]; + $this->columnTableMap[$existingTable . '.' . $column] = $existingTable; + $this->columnAliasMap[$existingTable . '.' . $column] = $existingAlias; + $this->columnTableMap[$column] = null; + $this->columnAliasMap[$column] = null; + } + + $this->columnTableMap[$table . '.' . $column] = $table; + $this->columnAliasMap[$table . '.' . $column] = $key; + } else { + $this->columnTableMap[$column] = $table; + $this->columnAliasMap[$column] = $key; + } } } } @@ -472,32 +533,35 @@ abstract class Repository implements Selectable } $tableRules = $conversionRules[$table]; + if (($alias = $this->reassembleQueryColumnAlias($table, $name)) === null) { + $alias = $name; + } // Check for a conversion method for the alias/column first - if (array_key_exists($name, $tableRules) || in_array($name, $tableRules)) { - $methodName = $context . join('', array_map('ucfirst', explode('_', $name))); + if (array_key_exists($alias, $tableRules) || in_array($alias, $tableRules)) { + $methodName = $context . join('', array_map('ucfirst', explode('_', $alias))); if (method_exists($this, $methodName)) { return $methodName; } } // The conversion method for the type is just a fallback, but it is required to exist if defined - if (isset($tableRules[$name])) { - $identifier = join('', array_map('ucfirst', explode('_', $tableRules[$name]))); + if (isset($tableRules[$alias])) { + $identifier = join('', array_map('ucfirst', explode('_', $tableRules[$alias]))); if (! method_exists($this, $context . $identifier)) { // Do not throw an error in case at least one conversion method exists if (! method_exists($this, ($context === 'persist' ? 'retrieve' : 'persist') . $identifier)) { throw new ProgrammingError( 'Cannot find any conversion method for type "%s"' . '. Add a proper conversion method or remove the type definition', - $tableRules[$name] + $tableRules[$alias] ); } Logger::debug( 'Conversion method "%s" for type definition "%s" does not exist in repository "%s".', $context . $identifier, - $tableRules[$name], + $tableRules[$alias], $this->getName() ); } else { @@ -729,6 +793,27 @@ abstract class Repository implements Selectable } } + /** + * Return the alias for the given query column name or null in case the query column name does not exist + * + * @param string $table + * @param string $column + * + * @return string|null + */ + public function reassembleQueryColumnAlias($table, $column) + { + $columnAliasMap = $this->getColumnAliasMap(); + if (isset($columnAliasMap[$column])) { + return $columnAliasMap[$column]; + } + + $prefixedColumn = $table . '.' . $column; + if (isset($columnAliasMap[$prefixedColumn])) { + return $columnAliasMap[$prefixedColumn]; + } + } + /** * Return whether the given alias or query column name is available in the given table * @@ -744,8 +829,13 @@ abstract class Repository implements Selectable return $aliasTableMap[$alias] === $table; } + $columnTableMap = $this->getColumnTableMap(); + if (isset($columnTableMap[$alias])) { + return $columnTableMap[$alias] === $table; + } + $prefixedAlias = $table . '.' . $alias; - return isset($aliasTableMap[$prefixedAlias]); + return isset($aliasTableMap[$prefixedAlias]) || isset($columnTableMap[$prefixedAlias]); } /** @@ -758,12 +848,13 @@ abstract class Repository implements Selectable */ public function hasQueryColumn($table, $name) { - if (in_array($name, $this->getFilterColumns())) { + if ($this->resolveQueryColumnAlias($table, $name) !== null) { + $alias = $name; + } elseif (($alias = $this->reassembleQueryColumnAlias($table, $name)) === null) { return false; } - return $this->resolveQueryColumnAlias($table, $name) !== null - && $this->validateQueryColumnAssociation($table, $name); + return !in_array($alias, $this->getFilterColumns()) && $this->validateQueryColumnAssociation($table, $name); } /** @@ -779,15 +870,19 @@ abstract class Repository implements Selectable */ public function requireQueryColumn($table, $name, RepositoryQuery $query = null) { - if (in_array($name, $this->getFilterColumns())) { - throw new QueryException(t('Filter column "%s" cannot be queried'), $name); - } - - if (($column = $this->resolveQueryColumnAlias($table, $name)) === null) { + if (($column = $this->resolveQueryColumnAlias($table, $name)) !== null) { + $alias = $name; + } elseif (($alias = $this->reassembleQueryColumnAlias($table, $name)) !== null) { + $column = $name; + } else { throw new QueryException(t('Query column "%s" not found'), $name); } - if (! $this->validateQueryColumnAssociation($table, $name)) { + if (in_array($alias, $this->getFilterColumns())) { + throw new QueryException(t('Filter column "%s" cannot be queried'), $name); + } + + if (! $this->validateQueryColumnAssociation($table, $alias)) { throw new QueryException(t('Query column "%s" not found in table "%s"'), $name, $table); } @@ -804,7 +899,8 @@ abstract class Repository implements Selectable */ public function hasFilterColumn($table, $name) { - return $this->resolveQueryColumnAlias($table, $name) !== null + return ($this->resolveQueryColumnAlias($table, $name) !== null + || $this->reassembleQueryColumnAlias($table, $name) !== null) && $this->validateQueryColumnAssociation($table, $name); } @@ -821,11 +917,15 @@ abstract class Repository implements Selectable */ public function requireFilterColumn($table, $name, RepositoryQuery $query = null) { - if (($column = $this->resolveQueryColumnAlias($table, $name)) === null) { + if (($column = $this->resolveQueryColumnAlias($table, $name)) !== null) { + $alias = $name; + } elseif (($alias = $this->reassembleQueryColumnAlias($table, $name)) !== null) { + $column = $name; + } else { throw new QueryException(t('Filter column "%s" not found'), $name); } - if (! $this->validateQueryColumnAssociation($table, $name)) { + if (! $this->validateQueryColumnAssociation($table, $alias)) { throw new QueryException(t('Filter column "%s" not found in table "%s"'), $name, $table); } @@ -857,15 +957,19 @@ abstract class Repository implements Selectable */ public function requireStatementColumn($table, $name) { - if (in_array($name, $this->filterColumns)) { - throw new StatementException('Filter column "%s" cannot be referenced in a statement', $name); - } - - if (($column = $this->resolveQueryColumnAlias($table, $name)) === null) { + if (($column = $this->resolveQueryColumnAlias($table, $name)) !== null) { + $alias = $name; + } elseif (($alias = $this->reassembleQueryColumnAlias($table, $name)) !== null) { + $column = $name; + } else { throw new StatementException('Statement column "%s" not found', $name); } - if (! $this->validateQueryColumnAssociation($table, $name)) { + if (in_array($alias, $this->getFilterColumns())) { + throw new StatementException('Filter column "%s" cannot be referenced in a statement', $name); + } + + if (! $this->validateQueryColumnAssociation($table, $alias)) { throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table); } diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php index 5b2765fe2..cf0cd9fae 100644 --- a/library/Icinga/Repository/RepositoryQuery.php +++ b/library/Icinga/Repository/RepositoryQuery.php @@ -241,20 +241,23 @@ class RepositoryQuery implements QueryInterface, Iterator if ($direction !== null || !array_key_exists('order', $sortColumns)) { $sortColumns['order'] = $direction ?: static::SORT_ASC; } - } elseif (! $ignoreDefault && array_key_exists($field, $sortRules)) { - $sortColumns = $sortRules[$field]; - if (! array_key_exists('columns', $sortColumns)) { - $sortColumns['columns'] = array($field); - } - if ($direction !== null || !array_key_exists('order', $sortColumns)) { - $sortColumns['order'] = $direction ?: static::SORT_ASC; - } } else { - $sortColumns = array( - 'columns' => array($field), - 'order' => $direction - ); - }; + $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 = strtoupper($sortColumns['order']) === static::SORT_DESC ? static::SORT_DESC : static::SORT_ASC;