2015-05-04 11:39:12 +02:00
|
|
|
<?php
|
|
|
|
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
|
|
|
|
|
|
|
|
namespace Icinga\Repository;
|
|
|
|
|
2015-05-13 09:52:29 +02:00
|
|
|
use Icinga\Data\Db\DbConnection;
|
2015-05-11 13:26:41 +02:00
|
|
|
use Icinga\Data\Extensible;
|
|
|
|
use Icinga\Data\Filter\Filter;
|
|
|
|
use Icinga\Data\Reducible;
|
|
|
|
use Icinga\Data\Updatable;
|
2015-05-04 11:39:12 +02:00
|
|
|
use Icinga\Exception\IcingaException;
|
|
|
|
use Icinga\Exception\ProgrammingError;
|
2015-05-12 15:38:29 +02:00
|
|
|
use Icinga\Exception\StatementException;
|
2015-05-28 13:53:49 +02:00
|
|
|
use Icinga\Util\String;
|
2015-05-04 11:39:12 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Abstract base class for concrete database repository implementations
|
|
|
|
*
|
|
|
|
* Additionally provided features:
|
|
|
|
* <ul>
|
2015-05-28 13:44:51 +02:00
|
|
|
* <li>Support for table aliases</li>
|
2015-05-04 11:39:12 +02:00
|
|
|
* <li>Automatic table prefix handling</li>
|
2015-05-11 13:26:41 +02:00
|
|
|
* <li>Insert, update and delete capabilities</li>
|
|
|
|
* <li>Differentiation between statement and query columns</li>
|
2015-05-28 13:53:49 +02:00
|
|
|
* <li>Capability to join additional tables depending on the columns being selected or used in a filter</li>
|
2015-05-04 11:39:12 +02:00
|
|
|
* </ul>
|
|
|
|
*/
|
2015-05-11 13:26:41 +02:00
|
|
|
abstract class DbRepository extends Repository implements Extensible, Updatable, Reducible
|
2015-05-04 11:39:12 +02:00
|
|
|
{
|
2015-05-13 09:52:29 +02:00
|
|
|
/**
|
|
|
|
* The datasource being used
|
|
|
|
*
|
|
|
|
* @var DbConnection
|
|
|
|
*/
|
|
|
|
protected $ds;
|
|
|
|
|
2015-05-28 13:44:51 +02:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
|
2015-05-11 13:26:41 +02:00
|
|
|
/**
|
|
|
|
* 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:
|
|
|
|
* <pre><code>
|
|
|
|
* array(
|
|
|
|
* 'table_name' => array(
|
|
|
|
* 'column1',
|
|
|
|
* 'alias1' => 'column2',
|
|
|
|
* 'alias2' => 'column3'
|
|
|
|
* )
|
|
|
|
* )
|
|
|
|
* <pre><code>
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
protected $statementColumns;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* An array to map table names to statement columns/aliases
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
protected $statementTableMap;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A flattened array to map statement columns to aliases
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
protected $statementColumnMap;
|
|
|
|
|
2015-05-27 11:47:18 +02:00
|
|
|
/**
|
|
|
|
* List of columns 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 $columnsWithoutCollation;
|
|
|
|
|
2015-05-13 09:52:29 +02:00
|
|
|
/**
|
|
|
|
* Create a new DB repository object
|
|
|
|
*
|
2015-05-27 11:47:18 +02:00
|
|
|
* In case $this->queryColumns has already been initialized, this initializes
|
|
|
|
* $this->columnsWithoutCollation in case of a PostgreSQL connection.
|
|
|
|
*
|
2015-05-13 09:52:29 +02:00
|
|
|
* @param DbConnection $ds The datasource to use
|
|
|
|
*/
|
|
|
|
public function __construct(DbConnection $ds)
|
|
|
|
{
|
|
|
|
parent::__construct($ds);
|
2015-05-27 11:47:18 +02:00
|
|
|
|
|
|
|
$this->columnsWithoutCollation = array();
|
|
|
|
if ($ds->getDbType() === 'pgsql' && $this->queryColumns !== null) {
|
|
|
|
$this->queryColumns = $this->removeCollateInstruction($this->queryColumns);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the query columns being provided
|
|
|
|
*
|
|
|
|
* Initializes $this->columnsWithoutCollation 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;
|
|
|
|
}
|
|
|
|
|
2015-05-28 13:44:51 +02:00
|
|
|
/**
|
|
|
|
* 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();
|
|
|
|
}
|
|
|
|
|
2015-05-27 11:47:18 +02:00
|
|
|
/**
|
|
|
|
* 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 & $columns) {
|
|
|
|
foreach ($columns as & $column) {
|
|
|
|
$column = preg_replace('/ COLLATE .+$/', '', $column, -1, $count);
|
|
|
|
if ($count > 0) {
|
|
|
|
$this->columnsWithoutCollation[] = $column;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $queryColumns;
|
2015-05-13 09:52:29 +02:00
|
|
|
}
|
|
|
|
|
2015-05-04 11:39:12 +02:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
2015-05-11 13:26:41 +02:00
|
|
|
|
2015-05-12 15:38:29 +02:00
|
|
|
/**
|
|
|
|
* Remove the datasource's prefix from the given table name and return the remaining part
|
|
|
|
*
|
2015-05-28 13:44:51 +02:00
|
|
|
* @param array|string $table
|
|
|
|
*
|
|
|
|
* @return array|string
|
2015-05-12 15:38:29 +02:00
|
|
|
*
|
2015-05-28 13:44:51 +02:00
|
|
|
* @throws IcingaException In case $table is not of a supported type
|
2015-05-12 15:38:29 +02:00
|
|
|
*/
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2015-05-28 13:44:51 +02:00
|
|
|
/**
|
|
|
|
* Return the given table with its alias being applied
|
|
|
|
*
|
|
|
|
* @param array|string $table
|
|
|
|
*
|
|
|
|
* @return array|string
|
|
|
|
*/
|
|
|
|
protected function applyTableAlias($table)
|
|
|
|
{
|
|
|
|
$tableAliases = $this->getTableAliases();
|
|
|
|
if (is_array($table) || !isset($tableAliases[($nonPrefixedTable = $this->removeTablePrefix($table))])) {
|
|
|
|
return $table;
|
|
|
|
}
|
|
|
|
|
|
|
|
return array($tableAliases[$nonPrefixedTable] => $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));
|
|
|
|
}
|
|
|
|
|
2015-05-11 13:26:41 +02:00
|
|
|
/**
|
|
|
|
* Insert a table row with the given data
|
|
|
|
*
|
|
|
|
* @param string $table
|
|
|
|
* @param array $bind
|
|
|
|
*/
|
|
|
|
public function insert($table, array $bind)
|
|
|
|
{
|
2015-05-12 15:38:29 +02:00
|
|
|
$this->ds->insert($this->prependTablePrefix($table), $this->requireStatementColumns($table, $bind));
|
2015-05-11 13:26:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update table rows with the given data, optionally limited by using a filter
|
|
|
|
*
|
|
|
|
* @param string $table
|
|
|
|
* @param array $bind
|
|
|
|
* @param Filter $filter
|
|
|
|
*/
|
|
|
|
public function update($table, array $bind, Filter $filter = null)
|
|
|
|
{
|
|
|
|
if ($filter) {
|
2015-05-12 15:38:29 +02:00
|
|
|
$this->requireFilter($table, $filter);
|
2015-05-11 13:26:41 +02:00
|
|
|
}
|
|
|
|
|
2015-05-12 15:38:29 +02:00
|
|
|
$this->ds->update($this->prependTablePrefix($table), $this->requireStatementColumns($table, $bind), $filter);
|
2015-05-11 13:26:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Delete table rows, optionally limited by using a filter
|
|
|
|
*
|
|
|
|
* @param string $table
|
|
|
|
* @param Filter $filter
|
|
|
|
*/
|
|
|
|
public function delete($table, Filter $filter = null)
|
|
|
|
{
|
|
|
|
if ($filter) {
|
2015-05-12 15:38:29 +02:00
|
|
|
$this->requireFilter($table, $filter);
|
2015-05-11 13:26:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$this->ds->delete($this->prependTablePrefix($table), $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 getStatementTableMap()
|
|
|
|
{
|
|
|
|
if ($this->statementTableMap === null) {
|
|
|
|
$this->initializeStatementMaps();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->statementTableMap;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return a flattened array to map statement columns to aliases
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
protected function getStatementColumnMap()
|
|
|
|
{
|
|
|
|
if ($this->statementColumnMap === null) {
|
|
|
|
$this->initializeStatementMaps();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->statementColumnMap;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initialize $this->statementTableMap and $this->statementColumnMap
|
|
|
|
*/
|
|
|
|
protected function initializeStatementMaps()
|
|
|
|
{
|
2015-05-13 13:27:08 +02:00
|
|
|
$this->statementTableMap = array();
|
|
|
|
$this->statementColumnMap = array();
|
2015-05-11 13:26:41 +02:00
|
|
|
foreach ($this->getStatementColumns() as $table => $columns) {
|
|
|
|
foreach ($columns as $alias => $column) {
|
2015-05-13 13:27:08 +02:00
|
|
|
$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;
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->statementTableMap[$table . '.' . $key] = $table;
|
|
|
|
$this->statementColumnMap[$table . '.' . $key] = $column;
|
2015-05-11 13:26:41 +02:00
|
|
|
} else {
|
2015-05-13 13:27:08 +02:00
|
|
|
$this->statementTableMap[$key] = $table;
|
|
|
|
$this->statementColumnMap[$key] = $column;
|
2015-05-11 13:26:41 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-29 11:32:15 +02:00
|
|
|
/**
|
2015-05-29 11:58:21 +02:00
|
|
|
* Return whether this repository is capable of converting values
|
|
|
|
*
|
|
|
|
* This does not check whether any conversion for the given table is available, as it may be possible
|
|
|
|
* that columns from another table where joined in which would otherwise not being converted.
|
2015-05-29 11:32:15 +02:00
|
|
|
*
|
|
|
|
* @param array|string $table
|
|
|
|
*
|
|
|
|
* @return bool
|
|
|
|
*/
|
2015-05-29 11:58:21 +02:00
|
|
|
public function providesValueConversion($_)
|
2015-05-29 11:32:15 +02:00
|
|
|
{
|
2015-05-29 11:58:21 +02:00
|
|
|
$conversionRules = $this->getConversionRules();
|
|
|
|
return !empty($conversionRules);
|
2015-05-29 11:32:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the name of the conversion method for the given alias or column name and context
|
|
|
|
*
|
|
|
|
* @param array|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
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*
|
|
|
|
* @throws ProgrammingError In case a conversion rule is found but not any conversion method
|
|
|
|
*/
|
|
|
|
protected function getConverter($table, $name, $context)
|
|
|
|
{
|
|
|
|
if (
|
|
|
|
$this->validateQueryColumnAssociation($table, $name)
|
|
|
|
|| $this->validateStatementColumnAssociation($table, $name)
|
|
|
|
) {
|
|
|
|
$table = $this->removeTablePrefix($this->clearTableAlias($table));
|
|
|
|
} else {
|
|
|
|
$table = $this->findTableName($name);
|
|
|
|
if (! $table) {
|
|
|
|
throw new ProgrammingError('Column name validation seems to have failed. Did you require the column?');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return parent::getConverter($table, $name, $context);
|
|
|
|
}
|
|
|
|
|
2015-05-21 15:01:13 +02:00
|
|
|
/**
|
|
|
|
* Validate that the requested table exists
|
|
|
|
*
|
2015-05-28 13:44:51 +02:00
|
|
|
* This will prepend the datasource's table prefix and will apply the table's alias, if any.
|
|
|
|
*
|
2015-05-28 13:25:26 +02:00
|
|
|
* @param string $table The table to validate
|
|
|
|
* @param RepositoryQuery $query An optional query to pass as context
|
|
|
|
* (unused by the base implementation)
|
2015-05-21 15:01:13 +02:00
|
|
|
*
|
2015-05-28 13:44:51 +02:00
|
|
|
* @return array|string
|
2015-05-21 15:01:13 +02:00
|
|
|
*
|
2015-05-28 13:25:26 +02:00
|
|
|
* @throws ProgrammingError In case the given table does not exist
|
2015-05-21 15:01:13 +02:00
|
|
|
*/
|
2015-05-28 13:25:26 +02:00
|
|
|
public function requireTable($table, RepositoryQuery $query = null)
|
2015-05-21 15:01:13 +02:00
|
|
|
{
|
|
|
|
$statementColumns = $this->getStatementColumns();
|
|
|
|
if (! isset($statementColumns[$table])) {
|
|
|
|
$table = parent::requireTable($table);
|
|
|
|
}
|
|
|
|
|
2015-05-28 13:44:51 +02:00
|
|
|
return $this->prependTablePrefix($this->applyTableAlias($table));
|
2015-05-21 15:01:13 +02:00
|
|
|
}
|
|
|
|
|
2015-05-27 11:47:18 +02:00
|
|
|
/**
|
|
|
|
* Recurse the given filter, require each column for the given table and convert all values
|
|
|
|
*
|
|
|
|
* In case of a PostgreSQL connection, this applies LOWER() on the column and strtolower()
|
|
|
|
* on the value if a COLLATE SQL-instruction is part of the resolved column.
|
|
|
|
*
|
2015-05-28 13:25:26 +02:00
|
|
|
* @param string $table The table being filtered
|
|
|
|
* @param Filter $filter The filter to recurse
|
|
|
|
* @param RepositoryQuery $query An optional query to pass as context
|
|
|
|
* (Directly passed through to $this->requireFilterColumn)
|
2015-05-27 11:47:18 +02:00
|
|
|
*/
|
2015-05-28 13:25:26 +02:00
|
|
|
public function requireFilter($table, Filter $filter, RepositoryQuery $query = null)
|
2015-05-27 11:47:18 +02:00
|
|
|
{
|
2015-05-28 13:25:26 +02:00
|
|
|
parent::requireFilter($table, $filter, $query);
|
2015-05-27 11:47:18 +02:00
|
|
|
|
|
|
|
if ($filter->isExpression()) {
|
|
|
|
$column = $filter->getColumn();
|
|
|
|
if (in_array($column, $this->columnsWithoutCollation) && strpos($column, 'LOWER') !== 0) {
|
|
|
|
$filter->setColumn('LOWER(' . $column . ')');
|
|
|
|
$expression = $filter->getExpression();
|
|
|
|
if (is_array($expression)) {
|
|
|
|
$filter->setExpression(array_map('strtolower', $expression));
|
|
|
|
} else {
|
|
|
|
$filter->setExpression(strtolower($expression));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-13 13:27:08 +02:00
|
|
|
/**
|
|
|
|
* Return this repository's query columns of the given table mapped to their respective aliases
|
|
|
|
*
|
2015-05-28 13:44:51 +02:00
|
|
|
* @param array|string $table
|
2015-05-13 13:27:08 +02:00
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*
|
|
|
|
* @throws ProgrammingError In case $table does not exist
|
|
|
|
*/
|
|
|
|
public function requireAllQueryColumns($table)
|
|
|
|
{
|
2015-05-28 13:44:51 +02:00
|
|
|
return parent::requireAllQueryColumns($this->removeTablePrefix($this->clearTableAlias($table)));
|
2015-05-13 13:27:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the query column name for the given alias or null in case the alias does not exist
|
|
|
|
*
|
2015-05-28 13:44:51 +02:00
|
|
|
* @param array|string $table
|
|
|
|
* @param string $alias
|
2015-05-13 13:27:08 +02:00
|
|
|
*
|
|
|
|
* @return string|null
|
|
|
|
*/
|
|
|
|
public function resolveQueryColumnAlias($table, $alias)
|
|
|
|
{
|
2015-05-28 13:44:51 +02:00
|
|
|
return parent::resolveQueryColumnAlias($this->removeTablePrefix($this->clearTableAlias($table)), $alias);
|
2015-05-13 13:27:08 +02:00
|
|
|
}
|
|
|
|
|
2015-05-11 13:26:41 +02:00
|
|
|
/**
|
2015-05-12 15:38:29 +02:00
|
|
|
* Return whether the given query column name or alias is available in the given table
|
|
|
|
*
|
2015-05-28 13:44:51 +02:00
|
|
|
* @param array|string $table
|
|
|
|
* @param string $column
|
2015-05-11 13:26:41 +02:00
|
|
|
*
|
2015-05-12 15:38:29 +02:00
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function validateQueryColumnAssociation($table, $column)
|
|
|
|
{
|
2015-05-28 13:44:51 +02:00
|
|
|
return parent::validateQueryColumnAssociation(
|
|
|
|
$this->removeTablePrefix($this->clearTableAlias($table)),
|
|
|
|
$column
|
|
|
|
);
|
2015-05-12 15:38:29 +02:00
|
|
|
}
|
|
|
|
|
2015-05-28 13:53:49 +02:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
public function requireQueryColumn($table, $name, RepositoryQuery $query = null)
|
|
|
|
{
|
|
|
|
if ($query === null || $this->validateQueryColumnAssociation($table, $name)) {
|
|
|
|
return parent::requireQueryColumn($table, $name, $query);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->joinColumn($name, $table, $query);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*
|
|
|
|
* @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,
|
2015-05-29 08:02:12 +02:00
|
|
|
* if not given the column is considered being used for a statement filter
|
2015-05-28 13:53:49 +02:00
|
|
|
*
|
|
|
|
* @return string The given column's name
|
|
|
|
*
|
|
|
|
* @throws QueryException In case the given column is not a valid filter column
|
|
|
|
*/
|
|
|
|
public function requireFilterColumn($table, $name, RepositoryQuery $query = null)
|
|
|
|
{
|
2015-05-29 08:02:12 +02:00
|
|
|
if ($query === null) {
|
|
|
|
return $this->requireStatementColumn($table, $name);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($this->validateQueryColumnAssociation($table, $name)) {
|
2015-05-28 13:53:49 +02:00
|
|
|
return parent::requireFilterColumn($table, $name, $query);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->joinColumn($name, $table, $query);
|
|
|
|
}
|
|
|
|
|
2015-05-12 15:38:29 +02:00
|
|
|
/**
|
2015-05-13 13:27:08 +02:00
|
|
|
* Return the statement column name for the given alias or null in case the alias does not exist
|
2015-05-12 15:38:29 +02:00
|
|
|
*
|
2015-05-28 13:44:51 +02:00
|
|
|
* @param string $table
|
2015-05-13 13:27:08 +02:00
|
|
|
* @param string $alias
|
|
|
|
*
|
|
|
|
* @return string|null
|
|
|
|
*/
|
|
|
|
public function resolveStatementColumnAlias($table, $alias)
|
|
|
|
{
|
|
|
|
$statementColumnMap = $this->getStatementColumnMap();
|
|
|
|
if (isset($statementColumnMap[$alias])) {
|
|
|
|
return $statementColumnMap[$alias];
|
|
|
|
}
|
|
|
|
|
2015-05-20 11:50:09 +02:00
|
|
|
$prefixedAlias = $this->removeTablePrefix($table) . '.' . $alias;
|
2015-05-13 13:27:08 +02:00
|
|
|
if (isset($statementColumnMap[$prefixedAlias])) {
|
|
|
|
return $statementColumnMap[$prefixedAlias];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return whether the given alias or statement column name is available in the given table
|
|
|
|
*
|
2015-05-28 13:44:51 +02:00
|
|
|
* @param string $table
|
2015-05-13 13:27:08 +02:00
|
|
|
* @param string $alias
|
2015-05-12 15:38:29 +02:00
|
|
|
*
|
|
|
|
* @return bool
|
|
|
|
*/
|
2015-05-13 13:27:08 +02:00
|
|
|
public function validateStatementColumnAssociation($table, $alias)
|
2015-05-12 15:38:29 +02:00
|
|
|
{
|
|
|
|
$statementTableMap = $this->getStatementTableMap();
|
2015-05-13 13:27:08 +02:00
|
|
|
if (isset($statementTableMap[$alias])) {
|
|
|
|
return $statementTableMap[$alias] === $this->removeTablePrefix($table);
|
|
|
|
}
|
|
|
|
|
|
|
|
$prefixedAlias = $this->removeTablePrefix($table) . '.' . $alias;
|
|
|
|
return isset($statementTableMap[$prefixedAlias]);
|
2015-05-12 15:38:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return whether the given column name or alias of the given table is a valid statement column
|
|
|
|
*
|
2015-05-28 13:44:51 +02:00
|
|
|
* @param string $table The table where to look for the column or alias
|
2015-05-11 13:26:41 +02:00
|
|
|
* @param string $name The column name or alias to check
|
|
|
|
*
|
|
|
|
* @return bool
|
|
|
|
*/
|
2015-05-12 15:38:29 +02:00
|
|
|
public function hasStatementColumn($table, $name)
|
2015-05-11 13:26:41 +02:00
|
|
|
{
|
2015-05-12 15:38:29 +02:00
|
|
|
if (
|
2015-05-13 13:27:08 +02:00
|
|
|
$this->resolveStatementColumnAlias($table, $name) === null
|
2015-05-12 15:38:29 +02:00
|
|
|
|| !$this->validateStatementColumnAssociation($table, $name)
|
|
|
|
) {
|
|
|
|
return parent::hasStatementColumn($table, $name);
|
2015-05-11 13:26:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validate that the given column is a valid statement column and return it or the actual name if it's an alias
|
|
|
|
*
|
2015-05-28 13:44:51 +02:00
|
|
|
* @param string $table The table for which to require the column
|
2015-05-11 13:26:41 +02:00
|
|
|
* @param string $name The name or alias of the column to validate
|
|
|
|
*
|
|
|
|
* @return string The given column's name
|
|
|
|
*
|
2015-05-12 15:38:29 +02:00
|
|
|
* @throws StatementException In case the given column is not a statement column
|
2015-05-11 13:26:41 +02:00
|
|
|
*/
|
2015-05-12 15:38:29 +02:00
|
|
|
public function requireStatementColumn($table, $name)
|
2015-05-11 13:26:41 +02:00
|
|
|
{
|
2015-05-13 13:27:08 +02:00
|
|
|
if (($column = $this->resolveStatementColumnAlias($table, $name)) === null) {
|
2015-05-12 15:38:29 +02:00
|
|
|
return parent::requireStatementColumn($table, $name);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (! $this->validateStatementColumnAssociation($table, $name)) {
|
|
|
|
throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table);
|
2015-05-11 13:26:41 +02:00
|
|
|
}
|
|
|
|
|
2015-05-13 13:27:08 +02:00
|
|
|
return $column;
|
2015-05-11 13:26:41 +02:00
|
|
|
}
|
2015-05-28 13:53:49 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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, ProgrammingError will be thrown.
|
|
|
|
* 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 array|string $target The table to join $name into
|
|
|
|
* @param RepositoryQUery $query The query to apply the JOIN-clause on
|
|
|
|
*
|
|
|
|
* @return string The resolved alias or $name
|
|
|
|
*
|
|
|
|
* @throws ProgrammingError In case no valid table or join<TableName>-method is found
|
|
|
|
*/
|
|
|
|
public function joinColumn($name, $target, RepositoryQuery $query)
|
|
|
|
{
|
|
|
|
$tableName = $this->findTableName($name);
|
|
|
|
if (! $tableName) {
|
|
|
|
throw new ProgrammingError(
|
|
|
|
'Unable to find a valid table for column "%s" to join into "%s"',
|
|
|
|
$name,
|
|
|
|
$this->removeTablePrefix($this->clearTableAlias($target))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$column = $this->resolveQueryColumnAlias($tableName, $name);
|
|
|
|
|
|
|
|
$prefixedTableName = $this->prependTablePrefix($tableName);
|
|
|
|
if ($query->getQuery()->hasJoinedTable($prefixedTableName)) {
|
|
|
|
return $column;
|
|
|
|
}
|
|
|
|
|
|
|
|
$joinMethod = 'join' . String::cname($tableName);
|
|
|
|
if (! method_exists($this, $joinMethod)) {
|
|
|
|
throw new ProgrammingError(
|
|
|
|
'Unable to join table "%s" into "%s". Method "%s" not found',
|
|
|
|
$tableName,
|
|
|
|
$this->removeTablePrefix($this->clearTableAlias($target)),
|
|
|
|
$joinMethod
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->$joinMethod($query, $target, $name);
|
|
|
|
return $column;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the table name for the given alias or column name
|
|
|
|
*
|
|
|
|
* @param string $column
|
|
|
|
*
|
|
|
|
* @return string|null null in case no table is found
|
|
|
|
*/
|
|
|
|
protected function findTableName($column)
|
|
|
|
{
|
|
|
|
$aliasTableMap = $this->getAliasTableMap();
|
|
|
|
if (isset($aliasTableMap[$column])) {
|
|
|
|
return $aliasTableMap[$column];
|
|
|
|
}
|
|
|
|
|
2015-05-29 11:32:15 +02:00
|
|
|
// TODO(jom): Elaborate whether it makes sense to throw ProgrammingError
|
|
|
|
// instead (duplicate aliases in different tables?)
|
2015-05-28 13:53:49 +02:00
|
|
|
foreach ($aliasTableMap as $alias => $table) {
|
|
|
|
if (strpos($alias, '.') !== false) {
|
2015-05-29 11:37:42 +02:00
|
|
|
list($_, $alias) = explode('.', $column, 2);
|
2015-05-28 13:53:49 +02:00
|
|
|
if ($alias === $column) {
|
|
|
|
return $table;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-05-04 11:39:12 +02:00
|
|
|
}
|