summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Repository/DbRepository.php
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icinga/Repository/DbRepository.php')
-rw-r--r--library/Icinga/Repository/DbRepository.php1077
1 files changed, 1077 insertions, 0 deletions
diff --git a/library/Icinga/Repository/DbRepository.php b/library/Icinga/Repository/DbRepository.php
new file mode 100644
index 0000000..039d221
--- /dev/null
+++ b/library/Icinga/Repository/DbRepository.php
@@ -0,0 +1,1077 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Repository;
+
+use Zend_Db_Expr;
+use Icinga\Data\Db\DbConnection;
+use Icinga\Data\Extensible;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Data\Reducible;
+use Icinga\Data\Updatable;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Exception\StatementException;
+use Icinga\Util\StringHelper;
+
+/**
+ * Abstract base class for concrete database repository implementations
+ *
+ * Additionally provided features:
+ * <ul>
+ * <li>Support for table aliases</li>
+ * <li>Automatic table prefix handling</li>
+ * <li>Insert, update and delete capabilities</li>
+ * <li>Differentiation between statement and query columns</li>
+ * <li>Capability to join additional tables depending on the columns being selected or used in a filter</li>
+ * </ul>
+ *
+ * @method DbConnection getDataSource($table = null)
+ */
+abstract class DbRepository extends Repository implements Extensible, Updatable, Reducible
+{
+ /**
+ * The datasource being used
+ *
+ * @var DbConnection
+ */
+ protected $ds;
+
+ /**
+ * The table aliases being applied
+ *
+ * This must be initialized by repositories which are going to make use of table aliases. Every table for which
+ * aliased columns are provided must be defined in this array using its name as key and the alias being used as
+ * value. Failure to do so will result in invalid queries.
+ *
+ * @var array
+ */
+ protected $tableAliases;
+
+ /**
+ * The join probability rules
+ *
+ * This may be initialized by repositories which make use of the table join capability. It allows to define
+ * probability rules to enhance control how ambiguous column aliases are associated with the correct table.
+ * To define a rule use the name of a base table as key and another array of table names as probable join
+ * targets ordered by priority. (Ascending: Lower means higher priority)
+ * <code>
+ * array(
+ * 'table_name' => array('target1', 'target2', 'target3')
+ * )
+ * </code>
+ *
+ * @todo Support for tree-ish rules
+ *
+ * @var array
+ */
+ protected $joinProbabilities;
+
+ /**
+ * The statement columns being provided
+ *
+ * This may be initialized by repositories which are going to make use of table aliases. It allows to provide
+ * alias-less column names to be used for a statement. The array needs to be in the following format:
+ * <code>
+ * array(
+ * 'table_name' => array(
+ * 'column1',
+ * 'alias1' => 'column2',
+ * 'alias2' => 'column3'
+ * )
+ * )
+ * </code>
+ *
+ * @var array
+ */
+ protected $statementColumns;
+
+ /**
+ * An array to map table names to statement columns/aliases
+ *
+ * @var array
+ */
+ protected $statementAliasTableMap;
+
+ /**
+ * A flattened array to map statement columns to aliases
+ *
+ * @var array
+ */
+ 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 column names or 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;
+
+ /**
+ * Create a new DB repository object
+ *
+ * In case $this->queryColumns has already been initialized, this initializes
+ * $this->caseInsensitiveColumns in case of a PostgreSQL connection.
+ *
+ * @param DbConnection $ds The datasource to use
+ */
+ public function __construct(DbConnection $ds)
+ {
+ parent::__construct($ds);
+
+ if ($ds->getDbType() === 'pgsql' && $this->queryColumns !== null) {
+ $this->queryColumns = $this->removeCollateInstruction($this->queryColumns);
+ }
+ }
+
+ /**
+ * Return the query columns being provided
+ *
+ * Initializes $this->caseInsensitiveColumns in case of a PostgreSQL connection.
+ *
+ * @return array
+ */
+ public function getQueryColumns()
+ {
+ if ($this->queryColumns === null) {
+ $this->queryColumns = parent::getQueryColumns();
+ if ($this->ds->getDbType() === 'pgsql') {
+ $this->queryColumns = $this->removeCollateInstruction($this->queryColumns);
+ }
+ }
+
+ return $this->queryColumns;
+ }
+
+ /**
+ * Return the table aliases to be applied
+ *
+ * Calls $this->initializeTableAliases() in case $this->tableAliases is null.
+ *
+ * @return array
+ */
+ public function getTableAliases()
+ {
+ if ($this->tableAliases === null) {
+ $this->tableAliases = $this->initializeTableAliases();
+ }
+
+ return $this->tableAliases;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the table aliases lazily
+ *
+ * @return array
+ */
+ protected function initializeTableAliases()
+ {
+ return array();
+ }
+
+ /**
+ * Return the join probability rules
+ *
+ * Calls $this->initializeJoinProbabilities() in case $this->joinProbabilities is null.
+ *
+ * @return array
+ */
+ public function getJoinProbabilities()
+ {
+ if ($this->joinProbabilities === null) {
+ $this->joinProbabilities = $this->initializeJoinProbabilities();
+ }
+
+ return $this->joinProbabilities;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the join probabilities lazily
+ *
+ * @return array
+ */
+ protected function initializeJoinProbabilities()
+ {
+ return array();
+ }
+
+ /**
+ * Remove each COLLATE SQL-instruction from all given query columns
+ *
+ * @param array $queryColumns
+ *
+ * @return array $queryColumns, the updated version
+ */
+ protected function removeCollateInstruction($queryColumns)
+ {
+ foreach ($queryColumns as $table => & $columns) {
+ foreach ($columns as $alias => & $column) {
+ // 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][is_string($alias) ? $alias : $column] = true;
+ }
+ }
+ }
+
+ return $queryColumns;
+ }
+
+ /**
+ * Initialize table, column and alias maps
+ *
+ * @throws ProgrammingError In case $this->queryColumns does not provide any column information
+ */
+ protected function initializeAliasMaps()
+ {
+ parent::initializeAliasMaps();
+
+ foreach ($this->aliasTableMap as $alias => $table) {
+ if ($table !== null) {
+ if (strpos($alias, '.') !== false) {
+ $prefixedAlias = str_replace('.', '_', $alias);
+ } else {
+ $prefixedAlias = $table . '_' . $alias;
+ }
+
+ if (array_key_exists($prefixedAlias, $this->aliasTableMap)) {
+ if ($this->aliasTableMap[$prefixedAlias] !== null) {
+ $existingTable = $this->aliasTableMap[$prefixedAlias];
+ $existingColumn = $this->aliasColumnMap[$prefixedAlias];
+ $this->aliasTableMap[$existingTable . '.' . $prefixedAlias] = $existingTable;
+ $this->aliasColumnMap[$existingTable . '.' . $prefixedAlias] = $existingColumn;
+ $this->aliasTableMap[$prefixedAlias] = null;
+ $this->aliasColumnMap[$prefixedAlias] = null;
+ }
+
+ $this->aliasTableMap[$table . '.' . $prefixedAlias] = $table;
+ $this->aliasColumnMap[$table . '.' . $prefixedAlias] = $this->aliasColumnMap[$alias];
+ } else {
+ $this->aliasTableMap[$prefixedAlias] = $table;
+ $this->aliasColumnMap[$prefixedAlias] = $this->aliasColumnMap[$alias];
+ }
+ }
+ }
+ }
+
+ /**
+ * Return the given table with the datasource's prefix being prepended
+ *
+ * @param array|string $table
+ *
+ * @return array|string
+ *
+ * @throws IcingaException In case $table is not of a supported type
+ */
+ protected function prependTablePrefix($table)
+ {
+ $prefix = $this->ds->getTablePrefix();
+ if (! $prefix) {
+ return $table;
+ }
+
+ if (is_array($table)) {
+ foreach ($table as & $tableName) {
+ if (strpos($tableName, $prefix) === false) {
+ $tableName = $prefix . $tableName;
+ }
+ }
+ } elseif (is_string($table)) {
+ $table = (strpos($table, $prefix) === false ? $prefix : '') . $table;
+ } else {
+ throw new IcingaException('Table prefix handling for type "%s" is not supported', type($table));
+ }
+
+ return $table;
+ }
+
+ /**
+ * Remove the datasource's prefix from the given table name and return the remaining part
+ *
+ * @param array|string $table
+ *
+ * @return array|string
+ *
+ * @throws IcingaException In case $table is not of a supported type
+ */
+ protected function removeTablePrefix($table)
+ {
+ $prefix = $this->ds->getTablePrefix();
+ if (! $prefix) {
+ return $table;
+ }
+
+ if (is_array($table)) {
+ foreach ($table as & $tableName) {
+ if (strpos($tableName, $prefix) === 0) {
+ $tableName = str_replace($prefix, '', $tableName);
+ }
+ }
+ } elseif (is_string($table)) {
+ if (strpos($table, $prefix) === 0) {
+ $table = str_replace($prefix, '', $table);
+ }
+ } else {
+ throw new IcingaException('Table prefix handling for type "%s" is not supported', type($table));
+ }
+
+ return $table;
+ }
+
+ /**
+ * Return the given table with its alias being applied
+ *
+ * @param array|string $table
+ * @param string $virtualTable
+ *
+ * @return array|string
+ */
+ protected function applyTableAlias($table, $virtualTable = null)
+ {
+ if (! is_array($table)) {
+ $tableAliases = $this->getTableAliases();
+ if ($virtualTable !== null && isset($tableAliases[$virtualTable])) {
+ return array($tableAliases[$virtualTable] => $table);
+ }
+
+ if (isset($tableAliases[($nonPrefixedTable = $this->removeTablePrefix($table))])) {
+ return array($tableAliases[$nonPrefixedTable] => $table);
+ }
+ }
+
+ return $table;
+ }
+
+ /**
+ * Return the given table with its alias being cleared
+ *
+ * @param array|string $table
+ *
+ * @return string
+ *
+ * @throws IcingaException In case $table is not of a supported type
+ */
+ protected function clearTableAlias($table)
+ {
+ if (is_string($table)) {
+ return $table;
+ }
+
+ if (is_array($table)) {
+ return reset($table);
+ }
+
+ throw new IcingaException('Table alias handling for type "%s" is not supported', type($table));
+ }
+
+ /**
+ * Insert a table row with the given data
+ *
+ * Note that the base implementation does not perform any quoting on the $table argument.
+ * Pass an array with a column name (the same as in $bind) and a PDO::PARAM_* constant as value
+ * as third parameter $types to define a different type than string for a particular column.
+ *
+ * @param string $table
+ * @param array $bind
+ * @param array $types
+ *
+ * @return int The number of affected rows
+ */
+ public function insert($table, array $bind, array $types = array())
+ {
+ $realTable = $this->clearTableAlias($this->requireTable($table));
+
+ foreach ($types as $alias => $type) {
+ unset($types[$alias]);
+ $types[$this->requireStatementColumn($table, $alias)] = $type;
+ }
+
+ return $this->ds->insert($realTable, $this->requireStatementColumns($table, $bind), $types);
+ }
+
+ /**
+ * Update table rows with the given data, optionally limited by using a filter
+ *
+ * Note that the base implementation does not perform any quoting on the $table argument.
+ * Pass an array with a column name (the same as in $bind) and a PDO::PARAM_* constant as value
+ * as fourth parameter $types to define a different type than string for a particular column.
+ *
+ * @param string $table
+ * @param array $bind
+ * @param Filter $filter
+ * @param array $types
+ *
+ * @return int The number of affected rows
+ */
+ public function update($table, array $bind, Filter $filter = null, array $types = array())
+ {
+ $realTable = $this->clearTableAlias($this->requireTable($table));
+
+ if ($filter) {
+ $filter = $this->requireFilter($table, $filter);
+ }
+
+ foreach ($types as $alias => $type) {
+ unset($types[$alias]);
+ $types[$this->requireStatementColumn($table, $alias)] = $type;
+ }
+
+ return $this->ds->update($realTable, $this->requireStatementColumns($table, $bind), $filter, $types);
+ }
+
+ /**
+ * Delete table rows, optionally limited by using a filter
+ *
+ * @param string $table
+ * @param Filter $filter
+ *
+ * @return int The number of affected rows
+ */
+ public function delete($table, Filter $filter = null)
+ {
+ $realTable = $this->clearTableAlias($this->requireTable($table));
+
+ if ($filter) {
+ $filter = $this->requireFilter($table, $filter);
+ }
+
+ return $this->ds->delete($realTable, $filter);
+ }
+
+ /**
+ * Return the statement columns being provided
+ *
+ * Calls $this->initializeStatementColumns() in case $this->statementColumns is null.
+ *
+ * @return array
+ */
+ public function getStatementColumns()
+ {
+ if ($this->statementColumns === null) {
+ $this->statementColumns = $this->initializeStatementColumns();
+ }
+
+ return $this->statementColumns;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the statement columns lazily
+ *
+ * @return array
+ */
+ protected function initializeStatementColumns()
+ {
+ return array();
+ }
+
+ /**
+ * Return an array to map table names to statement columns/aliases
+ *
+ * @return array
+ */
+ protected function getStatementAliasTableMap()
+ {
+ if ($this->statementAliasTableMap === null) {
+ $this->initializeStatementMaps();
+ }
+
+ return $this->statementAliasTableMap;
+ }
+
+ /**
+ * Return a flattened array to map statement columns to aliases
+ *
+ * @return array
+ */
+ protected function getStatementAliasColumnMap()
+ {
+ if ($this->statementAliasColumnMap === null) {
+ $this->initializeStatementMaps();
+ }
+
+ return $this->statementAliasColumnMap;
+ }
+
+ /**
+ * 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->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->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->statementAliasTableMap[$table . '.' . $key] = $table;
+ $this->statementAliasColumnMap[$table . '.' . $key] = $column;
+ } else {
+ $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;
+ }
+ }
+ }
+ }
+
+ /**
+ * Return whether this repository is capable of converting values for the given table and optional column
+ *
+ * This does not check whether any conversion for the given table is available if $column is not given, as it
+ * may be possible that columns from another table where joined in which would otherwise not being converted.
+ *
+ * @param string $table
+ * @param string $column
+ *
+ * @return bool
+ */
+ public function providesValueConversion($table, $column = null)
+ {
+ if ($column !== null) {
+ if ($column instanceof Zend_Db_Expr) {
+ return false;
+ }
+
+ if ($this->validateQueryColumnAssociation($table, $column)) {
+ return parent::providesValueConversion($table, $column);
+ }
+
+ if (($tableName = $this->findTableName($column, $table))) {
+ return parent::providesValueConversion($tableName, $column);
+ }
+
+ return false;
+ }
+
+ $conversionRules = $this->getConversionRules();
+ return !empty($conversionRules);
+ }
+
+ /**
+ * Return the name of the conversion method for the given alias or column name and context
+ *
+ * If a query column or a filter column, which is part of a query filter, needs to be converted,
+ * you'll need to pass $query, otherwise the column is considered a statement column.
+ *
+ * @param string $table The datasource's table
+ * @param string $name The alias or column name for which to return a conversion method
+ * @param string $context The context of the conversion: persist or retrieve
+ * @param RepositoryQuery $query If given the column is considered a query column,
+ * statement column otherwise
+ *
+ * @return string
+ *
+ * @throws ProgrammingError In case a conversion rule is found but not any conversion method
+ */
+ protected function getConverter($table, $name, $context, RepositoryQuery $query = null)
+ {
+ if ($name instanceof Zend_Db_Expr) {
+ return;
+ }
+
+ if (! ($query !== null && $this->validateQueryColumnAssociation($table, $name))
+ && !($query === null && $this->validateStatementColumnAssociation($table, $name))
+ ) {
+ $table = $this->findTableName($name, $table);
+ if (! $table) {
+ if ($query !== null) {
+ // It may be an aliased Zend_Db_Expr
+ $desiredColumns = $query->getColumns();
+ if (isset($desiredColumns[$name]) && $desiredColumns[$name] instanceof Zend_Db_Expr) {
+ return;
+ }
+ }
+
+ throw new ProgrammingError('Column name validation seems to have failed. Did you require the column?');
+ }
+ }
+
+ return parent::getConverter($table, $name, $context, $query);
+ }
+
+ /**
+ * Validate that the requested table exists
+ *
+ * This will prepend the datasource's table prefix and will apply the table's alias, if any.
+ *
+ * @param string $table The table to validate
+ * @param RepositoryQuery $query An optional query to pass as context
+ * (unused by the base implementation)
+ *
+ * @return array|string
+ *
+ * @throws ProgrammingError In case the given table does not exist
+ */
+ public function requireTable($table, RepositoryQuery $query = null)
+ {
+ $virtualTable = null;
+ $statementColumns = $this->getStatementColumns();
+ if (! isset($statementColumns[$table])) {
+ $newTable = parent::requireTable($table);
+ if ($newTable !== $table) {
+ $virtualTable = $table;
+ }
+
+ $table = $newTable;
+ } else {
+ $virtualTables = $this->getVirtualTables();
+ if (isset($virtualTables[$table])) {
+ $virtualTable = $table;
+ $table = $virtualTables[$table];
+ }
+ }
+
+ return $this->prependTablePrefix($this->applyTableAlias($table, $virtualTable));
+ }
+
+ /**
+ * Return the alias for the given table or null if none has been defined
+ *
+ * @param string $table
+ *
+ * @return string|null
+ */
+ public function resolveTableAlias($table)
+ {
+ $tableAliases = $this->getTableAliases();
+ if (isset($tableAliases[$table])) {
+ return $tableAliases[$table];
+ }
+ }
+
+ /**
+ * 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)
+ {
+ $alias = parent::reassembleQueryColumnAlias($table, $column);
+ if ($alias === null
+ && !$this->validateQueryColumnAssociation($table, $column)
+ && ($tableName = $this->findTableName($column, $table))
+ ) {
+ return parent::reassembleQueryColumnAlias($tableName, $column);
+ }
+
+ return $alias;
+ }
+
+ /**
+ * Validate that the given column is a valid query target and return it or the actual name if it's an alias
+ *
+ * Attempts to join the given column from a different table if its association to the given table cannot be
+ * verified.
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The name or alias of the column to validate
+ * @param RepositoryQuery $query An optional query to pass as context,
+ * if not given no join will be attempted
+ *
+ * @return string The given column's name
+ *
+ * @throws QueryException In case the given column is not a valid query column
+ * @throws ProgrammingError In case the given column is not found in $table and cannot be joined in
+ */
+ public function requireQueryColumn($table, $name, RepositoryQuery $query = null)
+ {
+ if ($name instanceof Zend_Db_Expr) {
+ return $name;
+ }
+
+ if ($query === null || $this->validateQueryColumnAssociation($table, $name)) {
+ return parent::requireQueryColumn($table, $name, $query);
+ }
+
+ $column = $this->joinColumn($name, $table, $query);
+ if ($column === null) {
+ if ($query !== null) {
+ // It may be an aliased Zend_Db_Expr
+ $desiredColumns = $query->getColumns();
+ if (isset($desiredColumns[$name]) && $desiredColumns[$name] instanceof Zend_Db_Expr) {
+ $column = $desiredColumns[$name];
+ }
+ }
+
+ if ($column === null) {
+ throw new ProgrammingError(
+ 'Unable to find a valid table for column "%s" to join into "%s"',
+ $name,
+ $table
+ );
+ }
+ }
+
+ return $column;
+ }
+
+ /**
+ * Validate that the given column is a valid filter target and return it or the actual name if it's an alias
+ *
+ * Attempts to join the given column from a different table if its association to the given table cannot be
+ * verified. In case of a PostgreSQL connection and if a COLLATE SQL-instruction is part of the resolved column,
+ * this applies LOWER() on the column and, if given, strtolower() on the filter's expression.
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The name or alias of the column to validate
+ * @param RepositoryQuery $query An optional query to pass as context,
+ * if not given the column is considered being used for a statement filter
+ * @param FilterExpression $filter An optional filter to pass as context
+ *
+ * @return string The given column's name
+ *
+ * @throws QueryException In case the given column is not a valid filter column
+ * @throws ProgrammingError In case the given column is not found in $table and cannot be joined in
+ */
+ public function requireFilterColumn($table, $name, RepositoryQuery $query = null, FilterExpression $filter = null)
+ {
+ if ($name instanceof Zend_Db_Expr) {
+ return $name;
+ }
+
+ $joined = false;
+ if ($query === null) {
+ $column = $this->requireStatementColumn($table, $name);
+ } elseif ($this->validateQueryColumnAssociation($table, $name)) {
+ $column = parent::requireFilterColumn($table, $name, $query, $filter);
+ } else {
+ $column = $this->joinColumn($name, $table, $query);
+ if ($column === null) {
+ if ($query !== null) {
+ // It may be an aliased Zend_Db_Expr
+ $desiredColumns = $query->getColumns();
+ if (isset($desiredColumns[$name]) && $desiredColumns[$name] instanceof Zend_Db_Expr) {
+ $column = $desiredColumns[$name];
+ }
+ }
+
+ if ($column === null) {
+ throw new ProgrammingError(
+ 'Unable to find a valid table for column "%s" to join into "%s"',
+ $name,
+ $table
+ );
+ }
+ } else {
+ $joined = true;
+ }
+ }
+
+ if (! empty($this->caseInsensitiveColumns)) {
+ if ($joined) {
+ $table = $this->findTableName($name, $table);
+ }
+
+ if ($column === $name) {
+ if ($query === null) {
+ $name = $this->reassembleStatementColumnAlias($table, $name);
+ } else {
+ $name = $this->reassembleQueryColumnAlias($table, $name);
+ }
+ }
+
+ if (isset($this->caseInsensitiveColumns[$table][$name])) {
+ $column = 'LOWER(' . $column . ')';
+ if ($filter !== null) {
+ $expression = $filter->getExpression();
+ if (is_array($expression)) {
+ $filter->setExpression(array_map('strtolower', $expression));
+ } else {
+ $filter->setExpression(strtolower($expression));
+ }
+ }
+ }
+ }
+
+ return $column;
+ }
+
+ /**
+ * Return the statement column name for the given alias or null in case the alias does not exist
+ *
+ * @param string $table
+ * @param string $alias
+ *
+ * @return string|null
+ */
+ public function resolveStatementColumnAlias($table, $alias)
+ {
+ $statementAliasColumnMap = $this->getStatementAliasColumnMap();
+ if (isset($statementAliasColumnMap[$alias])) {
+ return $statementAliasColumnMap[$alias];
+ }
+
+ $prefixedAlias = $table . '.' . $alias;
+ 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 = $table . '.' . $column;
+ if (isset($statementColumnAliasMap[$prefixedColumn])) {
+ return $statementColumnAliasMap[$prefixedColumn];
+ }
+ }
+
+ /**
+ * Return whether the given alias or statement column name is available in the given table
+ *
+ * @param string $table
+ * @param string $alias
+ *
+ * @return bool
+ */
+ public function validateStatementColumnAssociation($table, $alias)
+ {
+ $statementAliasTableMap = $this->getStatementAliasTableMap();
+ if (isset($statementAliasTableMap[$alias])) {
+ return $statementAliasTableMap[$alias] === $table;
+ }
+
+ $prefixedAlias = $table . '.' . $alias;
+ if (isset($statementAliasTableMap[$prefixedAlias])) {
+ return true;
+ }
+
+ $statementColumnTableMap = $this->getStatementColumnTableMap();
+ if (isset($statementColumnTableMap[$alias])) {
+ return $statementColumnTableMap[$alias] === $table;
+ }
+
+ return isset($statementColumnTableMap[$prefixedAlias]);
+ }
+
+ /**
+ * Return whether the given column name or alias of the given table is a valid statement column
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The column name or alias to check
+ *
+ * @return bool
+ */
+ public function hasStatementColumn($table, $name)
+ {
+ if (($this->resolveStatementColumnAlias($table, $name) === null
+ && $this->reassembleStatementColumnAlias($table, $name) === null)
+ || !$this->validateStatementColumnAssociation($table, $name)
+ ) {
+ return parent::hasStatementColumn($table, $name);
+ }
+
+ return true;
+ }
+
+ /**
+ * Validate that the given column is a valid statement column and return it or the actual name if it's an alias
+ *
+ * @param string $table The table for which to require the column
+ * @param string $name The name or alias of the column to validate
+ *
+ * @return string The given column's name
+ *
+ * @throws StatementException In case the given column is not a statement column
+ */
+ public function requireStatementColumn($table, $name)
+ {
+ 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, $alias)) {
+ throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table);
+ }
+
+ return $column;
+ }
+
+ /**
+ * Join alias or column $name into $table using $query
+ *
+ * Attempts to find a valid table for the given alias or column name and a method labelled join<TableName>
+ * to process the actual join logic. If neither of those is found, null is returned.
+ * The method is called with the same parameters but in reversed order.
+ *
+ * @param string $name The alias or column name to join into $target
+ * @param string $target The table to join $name into
+ * @param RepositoryQUery $query The query to apply the JOIN-clause on
+ *
+ * @return string|null The resolved alias or $name, null if no join logic is found
+ */
+ public function joinColumn($name, $target, RepositoryQuery $query)
+ {
+ if (! ($tableName = $this->findTableName($name, $target))) {
+ return;
+ }
+
+ if (($column = $this->resolveQueryColumnAlias($tableName, $name)) === null) {
+ $column = $name;
+ }
+
+ if (($joinIdentifier = $this->resolveTableAlias($tableName)) === null) {
+ $joinIdentifier = $this->prependTablePrefix($tableName);
+ }
+ if ($query->getQuery()->hasJoinedTable($joinIdentifier)) {
+ return $column;
+ }
+
+ $joinMethod = 'join' . StringHelper::cname($tableName);
+ if (! method_exists($this, $joinMethod)) {
+ throw new ProgrammingError(
+ 'Unable to join table "%s" into "%s". Method "%s" not found',
+ $tableName,
+ $target,
+ $joinMethod
+ );
+ }
+
+ $this->$joinMethod($query, $target, $name);
+ return $column;
+ }
+
+ /**
+ * Return the table name for the given alias or column name
+ *
+ * @param string $column The alias or column name
+ * @param string $origin The base table of a SELECT query
+ *
+ * @return string|null null in case no table is found
+ */
+ protected function findTableName($column, $origin)
+ {
+ // First, try to produce an exact match since it's faster and cheaper
+ $aliasTableMap = $this->getAliasTableMap();
+ if (isset($aliasTableMap[$column])) {
+ $table = $aliasTableMap[$column];
+ } else {
+ $columnTableMap = $this->getColumnTableMap();
+ if (isset($columnTableMap[$column])) {
+ $table = $columnTableMap[$column];
+ }
+ }
+
+ // But only return it if it's a probable join...
+ $joinProbabilities = $this->getJoinProbabilities();
+ if (isset($joinProbabilities[$origin])) {
+ $probableJoins = $joinProbabilities[$origin];
+ }
+
+ // ...if probability can be determined
+ if (isset($table) && (empty($probableJoins) || in_array($table, $probableJoins, true))) {
+ return $table;
+ }
+
+ // Without a proper exact match, there is only one fast and cheap way to find a suitable table..
+ if (! empty($probableJoins)) {
+ foreach ($probableJoins as $table) {
+ if (isset($aliasTableMap[$table . '.' . $column])) {
+ return $table;
+ }
+ }
+ }
+
+ // Last chance to find a table. Though, this usually ends up with a QueryException..
+ foreach ($aliasTableMap as $prefixedAlias => $table) {
+ if (strpos($prefixedAlias, '.') !== false) {
+ list($_, $alias) = explode('.', $prefixedAlias, 2);
+ if ($alias === $column) {
+ return $table;
+ }
+ }
+ }
+ }
+}