[TASK] Use queryCache in generic persistence 55/27555/12
authorFelix Oertel <felix@oer.tel>
Tue, 11 Feb 2014 17:15:17 +0000 (18:15 +0100)
committerWouter Wolters <typo3@wouterwolters.nl>
Mon, 17 Mar 2014 22:24:47 +0000 (23:24 +0100)
At the moment extbase persistence is quite slow. To some degree,
the query building is responsible for that. For each and every
query the parseQuery() and buildQuery() methods are called.

This patch introduces a query cache to store the query structure.
This way a query can be reused, even when fired with different
parameters.

* Introduce a parameterIdentifier to the comparison
In the way the extbase query object model works, it's hard to
identify an object in a way which is unique and still compre-
hensible. To ease this, an identifier is introduced.

* Move parsing to an external QueryParser
All the query parsing is now seperated from the storageBackend.
The query parsing is mostly left the way it was, it should be
re-profiled later.

* introduce cache extbase_typo3dbbackend_queries

* No more statement handling by reference
Make getStatementParts() return the query and parameters in an
array instead of handing down the parameters by reference.

* Split up parseQuery in preparsing and real parsing with caching
In every call the query has to be preparsed to determine the
identifier and extract the parameters. The identifier can then
be used to look up the query structure in the cache. If it is
not (yet) cached, Extbase will do the real parsing which takes
some more time.

* Introduce useQueryCache to querySettings
Like preparedStatements, caching the queries does not always
make sense and will spam your cache under certain conditions.
It's enabled by default and can be disabled on a per-query-
basis.

* Use named parameter's placeholders
Instead of using "?" as parameter's placeholder and relying
on the right order of parameters in the array, named place-
holders are used to ensure the correct association.

* Move unit tests around to fit the new queryParser

Resolves: #55167
Releases: 6.2
Change-Id: I517c0500cad75cae96fd739f2ff7222db80b42a5
Reviewed-on: https://review.typo3.org/27555
Reviewed-by: Anja Leichsenring
Tested-by: Anja Leichsenring
Tested-by: Philipp Gampe
Reviewed-by: Wouter Wolters
Tested-by: Wouter Wolters
typo3/sysext/extbase/Classes/Persistence/Generic/Qom/Comparison.php
typo3/sysext/extbase/Classes/Persistence/Generic/Storage/Typo3DbBackend.php
typo3/sysext/extbase/Classes/Persistence/Generic/Storage/Typo3DbQueryParser.php [new file with mode: 0644]
typo3/sysext/extbase/Classes/Persistence/Generic/Typo3QuerySettings.php
typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Storage/Typo3DbBackendTest.php
typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Storage/Typo3DbQueryParserTest.php [new file with mode: 0644]
typo3/sysext/extbase/ext_localconf.php

index 035570b..80b5be0 100644 (file)
@@ -87,6 +87,11 @@ class Comparison implements \TYPO3\CMS\Extbase\Persistence\Generic\Qom\Compariso
         */
        protected $operand2;
 
+       /*
+        * @var string
+        */
+       protected $parameterIdentifier;
+
        /**
         * Constructs this Comparison instance
         *
@@ -126,4 +131,19 @@ class Comparison implements \TYPO3\CMS\Extbase\Persistence\Generic\Qom\Compariso
        public function getOperand2() {
                return $this->operand2;
        }
+
+       /**
+        * @param string $parameterIdentifier
+        * @return void
+        */
+       public function setParameterIdentifier($parameterIdentifier) {
+               $this->parameterIdentifier = $parameterIdentifier;
+       }
+
+       /**
+        * @return string
+        */
+       public function getParameterIdentifier() {
+               return $this->parameterIdentifier;
+       }
 }
index 2c5c39f..3fe0a46 100644 (file)
@@ -36,9 +36,6 @@ use TYPO3\CMS\Extbase\Persistence\QueryInterface;
  */
 class Typo3DbBackend implements \TYPO3\CMS\Extbase\Persistence\Generic\Storage\BackendInterface, \TYPO3\CMS\Core\SingletonInterface {
 
-       const OPERATOR_EQUAL_TO_NULL = 'operatorEqualToNull';
-       const OPERATOR_NOT_EQUAL_TO_NULL = 'operatorNotEqualToNull';
-
        /**
         * The TYPO3 database object
         *
@@ -90,12 +87,23 @@ class Typo3DbBackend implements \TYPO3\CMS\Extbase\Persistence\Generic\Storage\B
        protected $tableColumnCache;
 
        /**
+        * @var \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend
+        */
+       protected $queryCache;
+
+       /**
         * @var \TYPO3\CMS\Extbase\Service\EnvironmentService
         * @inject
         */
        protected $environmentService;
 
        /**
+        * @var \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser
+        * @inject
+        */
+       protected $queryParser;
+
+       /**
         * Constructor. takes the database handle from $GLOBALS['TYPO3_DB']
         */
        public function __construct() {
@@ -109,6 +117,7 @@ class Typo3DbBackend implements \TYPO3\CMS\Extbase\Persistence\Generic\Storage\B
         */
        public function initializeObject() {
                $this->tableColumnCache = $this->cacheManager->getCache('extbase_typo3dbbackend_tablecolumns');
+               $this->queryCache = $this->cacheManager->getCache('extbase_typo3dbbackend_queries');
        }
 
        /**
@@ -281,11 +290,7 @@ class Typo3DbBackend implements \TYPO3\CMS\Extbase\Persistence\Generic\Storage\B
                if ($query->getStatement() instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\Statement) {
                        $rows = $this->getObjectDataByRawQuery($query);
                } else {
-
-                       // @todo with queryParser we don't need this parameters anymore
-                       $parameters = array();
-                       $statementParts = $this->parseQuery($query, $parameters);
-                       $rows = $this->getRowsByStatementParts($query, $statementParts, $parameters);
+                       $rows = $this->getRowsByStatementParts($query);
                }
 
                $rows = $this->doLanguageAndWorkspaceOverlay($query->getSource(), $rows, $query->getQuerySettings());
@@ -315,18 +320,18 @@ class Typo3DbBackend implements \TYPO3\CMS\Extbase\Persistence\Generic\Storage\B
        }
 
        /**
-        * Determines wether to use prepared statement or not and returns the rows from the corresponding method
+        * Determines whether to use prepared statement or not and returns the rows from the corresponding method
         *
         * @param \TYPO3\CMS\Extbase\Persistence\QueryInterface $query
-        * @param array $statementParts
-        * @param array $parameters
         * @return array
         */
-       protected function getRowsByStatementParts(\TYPO3\CMS\Extbase\Persistence\QueryInterface $query, array $statementParts, array $parameters) {
+       protected function getRowsByStatementParts(\TYPO3\CMS\Extbase\Persistence\QueryInterface $query) {
                if ($query->getQuerySettings()->getUsePreparedStatement()) {
+                       list($statementParts, $parameters) = $this->getStatementParts($query, FALSE);
                        $rows = $this->getRowsFromPreparedDatabase($statementParts, $parameters);
                } else {
-                       $rows = $this->getRowsFromDatabase($statementParts, $parameters);
+                       list($statementParts) = $this->getStatementParts($query);
+                       $rows = $this->getRowsFromDatabase($statementParts);
                }
 
                return $rows;
@@ -336,15 +341,10 @@ class Typo3DbBackend implements \TYPO3\CMS\Extbase\Persistence\Generic\Storage\B
         * Fetches the rows directly from the database, not using prepared statement
         *
         * @param array $statementParts
-        * @param array $parameters
         * @return array the result
         */
-       protected function getRowsFromDatabase(array $statementParts, array $parameters) {
+       protected function getRowsFromDatabase(array $statementParts) {
                $queryCommandParameters = $this->createQueryCommandParametersFromStatementParts($statementParts);
-
-               // @todo this work-around will be gone with queryParser
-               $this->replacePlaceholders($queryCommandParameters['whereClause'], $parameters);
-
                $rows = $this->databaseHandle->exec_SELECTgetRows(
                        $queryCommandParameters['selectFields'],
                        $queryCommandParameters['fromTable'],
@@ -437,26 +437,18 @@ class Typo3DbBackend implements \TYPO3\CMS\Extbase\Persistence\Generic\Storage\B
                        throw new \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\BadConstraintException('Could not execute count on queries with a constraint of type TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Qom\\StatementInterface', 1256661045);
                }
 
-               // @todo with queryParser we don't need this parameters anymore
-               $parameters = array();
-               $statementParts = $this->parseQuery($query, $parameters);
-
-               // @todo this work-around will be gone with queryParser
-               $statementParts['where'] = implode('', $statementParts['where']);
-               $this->replacePlaceholders($statementParts['where'], $parameters);
+               list($statementParts) = $this->getStatementParts($query);
 
                $fields = '*';
                if (isset($statementParts['keywords']['distinct'])) {
                        $fields = 'DISTINCT ' . reset($statementParts['tables']) . '.uid';
                }
 
+               $queryCommandParameters = $this->createQueryCommandParametersFromStatementParts($statementParts);
                $count = $this->databaseHandle->exec_SELECTcountRows(
                        $fields,
-                       implode(' ', $statementParts['tables']) . ' ' . implode(' ', $statementParts['unions']),
-                       (!empty($statementParts['where']) ? $statementParts['where'] : '1')
-                               . (!empty($statementParts['additionalWhereClause'])
-                                       ? ' AND ' . implode(' AND ', $statementParts['additionalWhereClause'])
-                                       : '')
+                       $queryCommandParameters['fromTable'],
+                       $queryCommandParameters['whereClause']
                );
                $this->checkSqlErrors();
 
@@ -472,59 +464,88 @@ class Typo3DbBackend implements \TYPO3\CMS\Extbase\Persistence\Generic\Storage\B
        }
 
        /**
-        * Parses the query and returns the SQL statement parts.
+        * Looks for the query in cache or builds it up otherwise
         *
-        * @param \TYPO3\CMS\Extbase\Persistence\QueryInterface $query The query
-        * @param array &$parameters
-        * @return array The SQL statement parts
+        * @param \TYPO3\CMS\Extbase\Persistence\QueryInterface $query
+        * @param bool $resolveParameterPlaceholders whether to resolve the parameters or leave the placeholders
+        * @return array
+        * @throws \Exception
         */
-       public function parseQuery(\TYPO3\CMS\Extbase\Persistence\QueryInterface $query, array &$parameters) {
-               $sql = array();
-               $sql['keywords'] = array();
-               $sql['tables'] = array();
-               $sql['unions'] = array();
-               $sql['fields'] = array();
-               $sql['where'] = array();
-               $sql['additionalWhereClause'] = array();
-               $sql['orderings'] = array();
-               $sql['limit'] = ((int)$query->getLimit() ?: NULL);
-               $sql['offset'] = ((int)$query->getOffset() ?: NULL);
-               $source = $query->getSource();
-               $this->parseSource($source, $sql);
-               $this->parseConstraint($query->getConstraint(), $source, $sql, $parameters);
-               $this->parseOrderings($query->getOrderings(), $source, $sql);
-               $tableNames = array_unique(array_keys($sql['tables'] + $sql['unions']));
-               foreach ($tableNames as $tableName) {
-                       if (is_string($tableName) && strlen($tableName) > 0) {
-                               $this->addAdditionalWhereClause($query->getQuerySettings(), $tableName, $sql);
+       protected function getStatementParts($query, $resolveParameterPlaceholders = TRUE) {
+                       /**
+                        * The queryParser will preparse the query to get the query's hash and parameters.
+                        * If the hash is found in the cache and useQueryCaching is enabled, extbase will
+                        * then take the string representation from cache and build a prepared query with
+                        * the parameters found.
+                        *
+                        * Otherwise extbase will parse the complete query, build the string representation
+                        * and run a usual query.
+                        */
+                       list($queryHash, $parameters) = $this->queryParser->preparseQuery($query);
+
+                       if ($query->getQuerySettings()->getUseQueryCache()) {
+                               $statementParts = $this->queryCache->get($queryHash);
+
+                               if ($queryHash && !$statementParts) {
+                                       $statementParts = $this->queryParser->parseQuery($query);
+                                       $this->queryCache->set($queryHash, $statementParts, array(), 0);
+                               }
+                       } else {
+                               $statementParts = $this->queryParser->parseQuery($query);
                        }
-               }
-               return $sql;
+
+                       if (!$statementParts) {
+                               throw new \Exception('Your query could not be built.', 1394453197);
+                       }
+
+                       // Limit and offset are not cached to allow caching of pagebrowser queries.
+                       $statementParts['limit'] = ((int)$query->getLimit() ?: NULL);
+                       $statementParts['offset'] = ((int)$query->getOffset() ?: NULL);
+
+                       if ($resolveParameterPlaceholders === TRUE) {
+                               $statementParts = $this->resolveParameterPlaceholders($statementParts, $parameters);
+                       }
+
+                       return array($statementParts, $parameters);
        }
 
        /**
-        * Returns the statement, ready to be executed.
+        * Replaces the parameters in the queryStructure with given values
         *
-        * @param array $sql The SQL statement parts
-        * @return string The SQL statement
+        * @param string $whereStatement
+        * @param array $parameters
+        * @return string
         */
-       public function buildQuery(array $sql) {
-               $statement = 'SELECT ' . implode(' ', $sql['keywords']) . ' ' . implode(',', $sql['fields']) . ' FROM ' . implode(' ', $sql['tables']) . ' ' . implode(' ', $sql['unions']);
-               if (!empty($sql['where'])) {
-                       $statement .= ' WHERE ' . implode('', $sql['where']);
-                       if (!empty($sql['additionalWhereClause'])) {
-                               $statement .= ' AND ' . implode(' AND ', $sql['additionalWhereClause']);
+       protected function resolveParameterPlaceholders($statementParts, $parameters = array()) {
+               $tableNameForEscape = (reset($statementParts['tables']) ?: 'foo');
+
+               foreach ($parameters as $parameterPlaceholder => $parameter) {
+                       if ($parameter instanceof \TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy) {
+                               $parameter = $parameter->_loadRealInstance();
                        }
-               } elseif (!empty($sql['additionalWhereClause'])) {
-                       $statement .= ' WHERE ' . implode(' AND ', $sql['additionalWhereClause']);
-               }
-               if (!empty($sql['orderings'])) {
-                       $statement .= ' ORDER BY ' . implode(', ', $sql['orderings']);
-               }
-               if (!empty($sql['limit'])) {
-                       $statement .= ' LIMIT ' . $sql['limit'];
+
+                       if ($parameter instanceof \DateTime) {
+                               $parameter = $parameter->format('U');
+                       } elseif ($parameter instanceof \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface) {
+                               $parameter = (int)$parameter->getUid();
+                       } elseif (is_array($parameter)) {
+                               $subParameters = array();
+                               foreach ($parameter as $subParameter) {
+                                       $subParameters[] = $this->databaseHandle->fullQuoteStr($subParameter, $tableNameForEscape);
+                               }
+                               $parameter = implode(',', $subParameters);
+                       } elseif ($parameter === NULL) {
+                               $parameter = 'NULL';
+                       } elseif (is_bool($input)) {
+                               return ($input === TRUE ? 1 : 0);
+                       } else {
+                               $parameter = $this->databaseHandle->fullQuoteStr((string)$parameter, $tableNameForEscape);
+                       }
+
+                       $statementParts['where'] = str_replace($parameterPlaceholder, $parameter, $statementParts['where']);
                }
-               return $statement;
+
+               return $statementParts;
        }
 
        /**
@@ -532,6 +553,7 @@ class Typo3DbBackend implements \TYPO3\CMS\Extbase\Persistence\Generic\Storage\B
         *
         * @param \TYPO3\CMS\Extbase\DomainObject\AbstractValueObject $object The Value Object
         * @return mixed The matching uid if an object was found, else FALSE
+        * @todo this is the last monster in this persistence series. refactor!
         */
        public function getUidOfAlreadyPersistedValueObject(\TYPO3\CMS\Extbase\DomainObject\AbstractValueObject $object) {
                $fields = array();
@@ -571,205 +593,12 @@ class Typo3DbBackend implements \TYPO3\CMS\Extbase\Persistence\Generic\Storage\B
        }
 
        /**
-        * Transforms a Query Source into SQL and parameter arrays
-        *
-        * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source The source
-        * @param array &$sql
-        * @return void
-        */
-       protected function parseSource(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source, array &$sql) {
-               if ($source instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SelectorInterface) {
-                       $className = $source->getNodeTypeName();
-                       $tableName = $this->dataMapper->getDataMap($className)->getTableName();
-                       $this->addRecordTypeConstraint($className, $sql);
-                       $sql['fields'][$tableName] = $tableName . '.*';
-                       $sql['tables'][$tableName] = $tableName;
-               } elseif ($source instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\JoinInterface) {
-                       $this->parseJoin($source, $sql);
-               }
-       }
-
-       /**
-        * Add a constraint to ensure that the record type of the returned tuples is matching the data type of the repository.
-        *
-        * @param string $className The class name
-        * @param array &$sql The query parts
-        * @return void
-        */
-       protected function addRecordTypeConstraint($className, array &$sql) {
-               if ($className !== NULL) {
-                       $dataMap = $this->dataMapper->getDataMap($className);
-                       if ($dataMap->getRecordTypeColumnName() !== NULL) {
-                               $recordTypes = array();
-                               if ($dataMap->getRecordType() !== NULL) {
-                                       $recordTypes[] = $dataMap->getRecordType();
-                               }
-                               foreach ($dataMap->getSubclasses() as $subclassName) {
-                                       $subclassDataMap = $this->dataMapper->getDataMap($subclassName);
-                                       if ($subclassDataMap->getRecordType() !== NULL) {
-                                               $recordTypes[] = $subclassDataMap->getRecordType();
-                                       }
-                               }
-                               if (count($recordTypes) > 0) {
-                                       $recordTypeStatements = array();
-                                       foreach ($recordTypes as $recordType) {
-                                               $tableName = $dataMap->getTableName();
-                                               $recordTypeStatements[] = $tableName . '.' . $dataMap->getRecordTypeColumnName() . '=' . $this->databaseHandle->fullQuoteStr($recordType, $tableName);
-                                       }
-                                       $sql['additionalWhereClause'][] = '(' . implode(' OR ', $recordTypeStatements) . ')';
-                               }
-                       }
-               }
-       }
-
-       /**
-        * Transforms a Join into SQL and parameter arrays
-        *
-        * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\JoinInterface $join The join
-        * @param array &$sql The query parts
-        * @return void
-        */
-       protected function parseJoin(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\JoinInterface $join, array &$sql) {
-               $leftSource = $join->getLeft();
-               $leftClassName = $leftSource->getNodeTypeName();
-               $this->addRecordTypeConstraint($leftClassName, $sql);
-               $leftTableName = $leftSource->getSelectorName();
-               // $sql['fields'][$leftTableName] = $leftTableName . '.*';
-               $rightSource = $join->getRight();
-               if ($rightSource instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\JoinInterface) {
-                       $rightClassName = $rightSource->getLeft()->getNodeTypeName();
-                       $rightTableName = $rightSource->getLeft()->getSelectorName();
-               } else {
-                       $rightClassName = $rightSource->getNodeTypeName();
-                       $rightTableName = $rightSource->getSelectorName();
-                       $sql['fields'][$leftTableName] = $rightTableName . '.*';
-               }
-               $this->addRecordTypeConstraint($rightClassName, $sql);
-               $sql['tables'][$leftTableName] = $leftTableName;
-               $sql['unions'][$rightTableName] = 'LEFT JOIN ' . $rightTableName;
-               $joinCondition = $join->getJoinCondition();
-               if ($joinCondition instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\EquiJoinCondition) {
-                       $column1Name = $this->dataMapper->convertPropertyNameToColumnName($joinCondition->getProperty1Name(), $leftClassName);
-                       $column2Name = $this->dataMapper->convertPropertyNameToColumnName($joinCondition->getProperty2Name(), $rightClassName);
-                       $sql['unions'][$rightTableName] .= ' ON ' . $joinCondition->getSelector1Name() . '.' . $column1Name . ' = ' . $joinCondition->getSelector2Name() . '.' . $column2Name;
-               }
-               if ($rightSource instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\JoinInterface) {
-                       $this->parseJoin($rightSource, $sql);
-               }
-       }
-
-       /**
-        * Transforms a constraint into SQL and parameter arrays
-        *
-        * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface $constraint The constraint
-        * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source The source
-        * @param array &$sql The query parts
-        * @param array &$parameters The parameters that will replace the markers
-        * @return void
-        */
-       protected function parseConstraint(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface $constraint = NULL, \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source, array &$sql, array &$parameters) {
-               if ($constraint instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\AndInterface) {
-                       $sql['where'][] = '(';
-                       $this->parseConstraint($constraint->getConstraint1(), $source, $sql, $parameters);
-                       $sql['where'][] = ' AND ';
-                       $this->parseConstraint($constraint->getConstraint2(), $source, $sql, $parameters);
-                       $sql['where'][] = ')';
-               } elseif ($constraint instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\OrInterface) {
-                       $sql['where'][] = '(';
-                       $this->parseConstraint($constraint->getConstraint1(), $source, $sql, $parameters);
-                       $sql['where'][] = ' OR ';
-                       $this->parseConstraint($constraint->getConstraint2(), $source, $sql, $parameters);
-                       $sql['where'][] = ')';
-               } elseif ($constraint instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\NotInterface) {
-                       $sql['where'][] = 'NOT (';
-                       $this->parseConstraint($constraint->getConstraint(), $source, $sql, $parameters);
-                       $sql['where'][] = ')';
-               } elseif ($constraint instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface) {
-                       $this->parseComparison($constraint, $source, $sql, $parameters);
-               }
-       }
-
-       /**
-        * Parse a Comparison into SQL and parameter arrays.
-        *
-        * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface $comparison The comparison to parse
-        * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source The source
-        * @param array &$sql SQL query parts to add to
-        * @param array &$parameters Parameters to bind to the SQL
-        * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\RepositoryException
-        * @return void
-        */
-       protected function parseComparison(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface $comparison, \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source, array &$sql, array &$parameters) {
-               $operand1 = $comparison->getOperand1();
-               $operator = $comparison->getOperator();
-               $operand2 = $comparison->getOperand2();
-               if ($operator === \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_IN) {
-                       $items = array();
-                       $hasValue = FALSE;
-                       foreach ($operand2 as $value) {
-                               $value = $this->getPlainValue($value);
-                               if ($value !== NULL) {
-                                       $items[] = $value;
-                                       $hasValue = TRUE;
-                               }
-                       }
-                       if ($hasValue === FALSE) {
-                               $sql['where'][] = '1<>1';
-                       } else {
-                               $this->parseDynamicOperand($operand1, $operator, $source, $sql, $parameters, NULL, $operand2);
-                               $parameters[] = $items;
-                       }
-               } elseif ($operator === \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_CONTAINS) {
-                       if ($operand2 === NULL) {
-                               $sql['where'][] = '1<>1';
-                       } else {
-                               $className = $source->getNodeTypeName();
-                               $tableName = $this->dataMapper->convertClassNameToTableName($className);
-                               $propertyName = $operand1->getPropertyName();
-                               while (strpos($propertyName, '.') !== FALSE) {
-                                       $this->addUnionStatement($className, $tableName, $propertyName, $sql);
-                               }
-                               $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
-                               $dataMap = $this->dataMapper->getDataMap($className);
-                               $columnMap = $dataMap->getColumnMap($propertyName);
-                               $typeOfRelation = $columnMap instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap ? $columnMap->getTypeOfRelation() : NULL;
-                               if ($typeOfRelation === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
-                                       $relationTableName = $columnMap->getRelationTableName();
-                                       $sql['where'][] = $tableName . '.uid IN (SELECT ' . $columnMap->getParentKeyFieldName() . ' FROM ' . $relationTableName . ' WHERE ' . $columnMap->getChildKeyFieldName() . '=?)';
-                                       $parameters[] = (int)$this->getPlainValue($operand2);
-                               } elseif ($typeOfRelation === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_MANY) {
-                                       $parentKeyFieldName = $columnMap->getParentKeyFieldName();
-                                       if (isset($parentKeyFieldName)) {
-                                               $childTableName = $columnMap->getChildTableName();
-                                               $sql['where'][] = $tableName . '.uid=(SELECT ' . $childTableName . '.' . $parentKeyFieldName . ' FROM ' . $childTableName . ' WHERE ' . $childTableName . '.uid=?)';
-                                               $parameters[] = (int)$this->getPlainValue($operand2);
-                                       } else {
-                                               $sql['where'][] = 'FIND_IN_SET(?,' . $tableName . '.' . $columnName . ')';
-                                               $parameters[] = (int)$this->getPlainValue($operand2);
-                                       }
-                               } else {
-                                       throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\RepositoryException('Unsupported or non-existing property name "' . $propertyName . '" used in relation matching.', 1327065745);
-                               }
-                       }
-               } else {
-                       if ($operand2 === NULL) {
-                               if ($operator === \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_EQUAL_TO) {
-                                       $operator = self::OPERATOR_EQUAL_TO_NULL;
-                               } elseif ($operator === \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_NOT_EQUAL_TO) {
-                                       $operator = self::OPERATOR_NOT_EQUAL_TO_NULL;
-                               }
-                       }
-                       $this->parseDynamicOperand($operand1, $operator, $source, $sql, $parameters);
-                       $parameters[] = $this->getPlainValue($operand2);
-               }
-       }
-
-       /**
         * Returns a plain value, i.e. objects are flattened out if possible.
         *
         * @param mixed $input
         * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException
         * @return mixed
+        * @todo remove after getUidOfAlreadyPersistedValueObject is adjusted, this was moved to queryParser
         */
        protected function getPlainValue($input) {
                if (is_array($input)) {
@@ -798,151 +627,6 @@ class Typo3DbBackend implements \TYPO3\CMS\Extbase\Persistence\Generic\Storage\B
        }
 
        /**
-        * Parse a DynamicOperand into SQL and parameter arrays.
-        *
-        * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\DynamicOperandInterface $operand
-        * @param string $operator One of the JCR_OPERATOR_* constants
-        * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source The source
-        * @param array &$sql The query parts
-        * @param array &$parameters The parameters that will replace the markers
-        * @param string $valueFunction an optional SQL function to apply to the operand value
-        * @param null $operand2
-        * @return void
-        */
-       protected function parseDynamicOperand(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\DynamicOperandInterface $operand, $operator, \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source, array &$sql, array &$parameters, $valueFunction = NULL, $operand2 = NULL) {
-               if ($operand instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\LowerCaseInterface) {
-                       $this->parseDynamicOperand($operand->getOperand(), $operator, $source, $sql, $parameters, 'LOWER');
-               } elseif ($operand instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\UpperCaseInterface) {
-                       $this->parseDynamicOperand($operand->getOperand(), $operator, $source, $sql, $parameters, 'UPPER');
-               } elseif ($operand instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\PropertyValueInterface) {
-                       $propertyName = $operand->getPropertyName();
-                       if ($source instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SelectorInterface) {
-                               // FIXME Only necessary to differ from  Join
-                               $className = $source->getNodeTypeName();
-                               $tableName = $this->dataMapper->convertClassNameToTableName($className);
-                               while (strpos($propertyName, '.') !== FALSE) {
-                                       $this->addUnionStatement($className, $tableName, $propertyName, $sql);
-                               }
-                       } elseif ($source instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\JoinInterface) {
-                               $tableName = $source->getJoinCondition()->getSelector1Name();
-                       }
-                       $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
-                       $operator = $this->resolveOperator($operator);
-                       $constraintSQL = '';
-                       if ($valueFunction === NULL) {
-                               $constraintSQL .= (!empty($tableName) ? $tableName . '.' : '') . $columnName . ' ' . $operator;
-                       } else {
-                               $constraintSQL .= $valueFunction . '(' . (!empty($tableName) ? $tableName . '.' : '') . $columnName . ') ' . $operator;
-                       }
-
-                       $constraintSQL .= ' ?';
-
-                       $sql['where'][] = $constraintSQL;
-               }
-       }
-
-       /**
-        * @param string &$className
-        * @param string &$tableName
-        * @param string &$propertyPath
-        * @param array &$sql
-        * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception
-        * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidRelationConfigurationException
-        * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\MissingColumnMapException
-        */
-       protected function addUnionStatement(&$className, &$tableName, &$propertyPath, array &$sql) {
-               $explodedPropertyPath = explode('.', $propertyPath, 2);
-               $propertyName = $explodedPropertyPath[0];
-               $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
-               $tableName = $this->dataMapper->convertClassNameToTableName($className);
-               $columnMap = $this->dataMapper->getDataMap($className)->getColumnMap($propertyName);
-
-               if ($columnMap === NULL) {
-                       throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\MissingColumnMapException('The ColumnMap for property "' . $propertyName . '" of class "' . $className . '" is missing.', 1355142232);
-               }
-
-               $parentKeyFieldName = $columnMap->getParentKeyFieldName();
-               $childTableName = $columnMap->getChildTableName();
-
-               if ($childTableName === NULL) {
-                       throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidRelationConfigurationException('The relation information for property "' . $propertyName . '" of class "' . $className . '" is missing.', 1353170925);
-               }
-
-               if ($columnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_ONE) {
-                       if (isset($parentKeyFieldName)) {
-                               $sql['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $tableName . '.uid=' . $childTableName . '.' . $parentKeyFieldName;
-                       } else {
-                               $sql['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $tableName . '.' . $columnName . '=' . $childTableName . '.uid';
-                       }
-                       $className = $this->dataMapper->getType($className, $propertyName);
-               } elseif ($columnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_MANY) {
-                       if (isset($parentKeyFieldName)) {
-                               $sql['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $tableName . '.uid=' . $childTableName . '.' . $parentKeyFieldName;
-                       } else {
-                               $onStatement = '(FIND_IN_SET(' . $childTableName . '.uid, ' . $tableName . '.' . $columnName . '))';
-                               $sql['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $onStatement;
-                       }
-                       $className = $this->dataMapper->getType($className, $propertyName);
-               } elseif ($columnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
-                       $relationTableName = $columnMap->getRelationTableName();
-                       $sql['unions'][$relationTableName] = 'LEFT JOIN ' . $relationTableName . ' ON ' . $tableName . '.uid=' . $relationTableName . '.' . $columnMap->getParentKeyFieldName();
-                       $sql['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $relationTableName . '.' . $columnMap->getChildKeyFieldName() . '=' . $childTableName . '.uid';
-                       $className = $this->dataMapper->getType($className, $propertyName);
-               } else {
-                       throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception('Could not determine type of relation.', 1252502725);
-               }
-               // TODO check if there is another solution for this
-               $sql['keywords']['distinct'] = 'DISTINCT';
-               $propertyPath = $explodedPropertyPath[1];
-               $tableName = $childTableName;
-       }
-
-       /**
-        * Returns the SQL operator for the given JCR operator type.
-        *
-        * @param string $operator One of the JCR_OPERATOR_* constants
-        * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception
-        * @return string an SQL operator
-        */
-       protected function resolveOperator($operator) {
-               switch ($operator) {
-                       case self::OPERATOR_EQUAL_TO_NULL:
-                               $operator = 'IS';
-                               break;
-                       case self::OPERATOR_NOT_EQUAL_TO_NULL:
-                               $operator = 'IS NOT';
-                               break;
-                       case \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_IN:
-                               $operator = 'IN';
-                               break;
-                       case \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_EQUAL_TO:
-                               $operator = '=';
-                               break;
-                       case \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_NOT_EQUAL_TO:
-                               $operator = '!=';
-                               break;
-                       case \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_LESS_THAN:
-                               $operator = '<';
-                               break;
-                       case \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_LESS_THAN_OR_EQUAL_TO:
-                               $operator = '<=';
-                               break;
-                       case \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_GREATER_THAN:
-                               $operator = '>';
-                               break;
-                       case \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_GREATER_THAN_OR_EQUAL_TO:
-                               $operator = '>=';
-                               break;
-                       case \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_LIKE:
-                               $operator = 'LIKE';
-                               break;
-                       default:
-                               throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception('Unsupported operator encountered.', 1242816073);
-               }
-               return $operator;
-       }
-
-       /**
         * Replace query placeholders in a query part by the given
         * parameters.
         *
@@ -951,6 +635,8 @@ class Typo3DbBackend implements \TYPO3\CMS\Extbase\Persistence\Generic\Storage\B
         * @param string $tableName
         *
         * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception
+        * @deprecated since 6.2, will be removed two versions later
+        * @todo add deprecation notice after getUidOfAlreadyPersistedValueObject is adjusted
         */
        protected function replacePlaceholders(&$sqlString, array $parameters, $tableName = 'foo') {
                // TODO profile this method again
@@ -979,55 +665,13 @@ class Typo3DbBackend implements \TYPO3\CMS\Extbase\Persistence\Generic\Storage\B
        }
 
        /**
-        * Adds additional WHERE statements according to the query settings.
-        *
-        * @param \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
-        * @param string $tableName The table name to add the additional where clause for
-        * @param string &$sql
-        * @return void
-        */
-       protected function addAdditionalWhereClause(\TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings, $tableName, &$sql) {
-               $this->addVisibilityConstraintStatement($querySettings, $tableName, $sql);
-               if ($querySettings->getRespectSysLanguage()) {
-                       $this->addSysLanguageStatement($tableName, $sql, $querySettings);
-               }
-               if ($querySettings->getRespectStoragePage()) {
-                       $this->addPageIdStatement($tableName, $sql, $querySettings->getStoragePageIds());
-               }
-       }
-
-       /**
-        * Builds the enable fields statement
-        *
-        * @param string $tableName The database table name
-        * @param array &$sql The query parts
-        * @return void
-        * @deprecated since Extbase 6.0, will be removed in Extbase 6.2.
-        */
-       protected function addEnableFieldsStatement($tableName, array &$sql) {
-               \TYPO3\CMS\Core\Utility\GeneralUtility::logDeprecatedFunction();
-               if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) {
-                       if ($this->environmentService->isEnvironmentInFrontendMode()) {
-                               $statement = $this->getPageRepository()->enableFields($tableName);
-                       } else {
-                               // TYPO3_MODE === 'BE'
-                               $statement = BackendUtility::deleteClause($tableName);
-                               $statement .= BackendUtility::BEenableFields($tableName);
-                       }
-                       if (!empty($statement)) {
-                               $statement = substr($statement, 5);
-                               $sql['additionalWhereClause'][] = $statement;
-                       }
-               }
-       }
-
-       /**
         * Adds enableFields and deletedClause to the query if necessary
         *
         * @param \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings
         * @param string $tableName The database table name
         * @param array &$sql The query parts
         * @return void
+        * @todo remove after getUidOfAlreadyPersistedValueObject is adjusted, this was moved to queryParser
         */
        protected function addVisibilityConstraintStatement(\TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings, $tableName, array &$sql) {
                $statement = '';
@@ -1057,6 +701,7 @@ class Typo3DbBackend implements \TYPO3\CMS\Extbase\Persistence\Generic\Storage\B
         * @param bool $includeDeleted A flag indicating whether deleted records should be included
         * @return string
         * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException
+        * @todo remove after getUidOfAlreadyPersistedValueObject is adjusted, this was moved to queryParser
         */
        protected function getFrontendConstraintStatement($tableName, $ignoreEnableFields, array $enableFieldsToBeIgnored = array(), $includeDeleted) {
                $statement = '';
@@ -1082,6 +727,7 @@ class Typo3DbBackend implements \TYPO3\CMS\Extbase\Persistence\Generic\Storage\B
         * @param bool $ignoreEnableFields A flag indicating whether the enable fields should be ignored
         * @param bool $includeDeleted A flag indicating whether deleted records should be included
         * @return string
+        * @todo remove after getUidOfAlreadyPersistedValueObject is adjusted, this was moved to queryParser
         */
        protected function getBackendConstraintStatement($tableName, $ignoreEnableFields, $includeDeleted) {
                $statement = '';
@@ -1095,115 +741,6 @@ class Typo3DbBackend implements \TYPO3\CMS\Extbase\Persistence\Generic\Storage\B
        }
 
        /**
-        * Builds the language field statement
-        *
-        * @param string $tableName The database table name
-        * @param array &$sql The query parts
-        * @param \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
-        * @return void
-        */
-       protected function addSysLanguageStatement($tableName, array &$sql, $querySettings) {
-               if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) {
-                       if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])) {
-                               // Select all entries for the current language
-                               $additionalWhereClause = $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . ' IN (' . (int)$querySettings->getLanguageUid() . ',-1)';
-                               // If any language is set -> get those entries which are not translated yet
-                               // They will be removed by t3lib_page::getRecordOverlay if not matching overlay mode
-                               if (isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
-                                       && $querySettings->getLanguageUid() > 0
-                               ) {
-                                       $additionalWhereClause .= ' OR (' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=0' .
-                                               ' AND ' . $tableName . '.uid NOT IN (SELECT ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] .
-                                               ' FROM ' . $tableName .
-                                               ' WHERE ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] . '>0' .
-                                               ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '>0';
-
-                                       // Add delete clause to ensure all entries are loaded
-                                       if (isset($GLOBALS['TCA'][$tableName]['ctrl']['delete'])) {
-                                               $additionalWhereClause .= ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['delete'] . '=0';
-                                       }
-                                       $additionalWhereClause .= '))';
-                               }
-                               $sql['additionalWhereClause'][] = '(' . $additionalWhereClause . ')';
-                       }
-               }
-       }
-
-       /**
-        * Builds the page ID checking statement
-        *
-        * @param string $tableName The database table name
-        * @param array &$sql The query parts
-        * @param array $storagePageIds list of storage page ids
-        * @return void
-        */
-       protected function addPageIdStatement($tableName, array &$sql, array $storagePageIds) {
-               $tableColumns = $this->tableColumnCache->get($tableName);
-               if ($tableColumns === FALSE) {
-                       $tableColumns = $this->databaseHandle->admin_get_fields($tableName);
-                       $this->tableColumnCache->set($tableName, $tableColumns);
-               }
-               if (is_array($GLOBALS['TCA'][$tableName]['ctrl']) && array_key_exists('pid', $tableColumns)) {
-                       $rootLevel = (int)$GLOBALS['TCA'][$tableName]['ctrl']['rootLevel'];
-                       if ($rootLevel) {
-                               if ($rootLevel === 1) {
-                                       $sql['additionalWhereClause'][] = $tableName . '.pid = 0';
-                               }
-                       } else {
-                               if (empty($storagePageIds)) {
-                                       throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException('Missing storage page ids.', 1365779762);
-                               }
-                               $sql['additionalWhereClause'][] = $tableName . '.pid IN (' . implode(', ', $storagePageIds) . ')';
-                       }
-               }
-       }
-
-       /**
-        * Transforms orderings into SQL.
-        *
-        * @param array $orderings An array of orderings (Tx_Extbase_Persistence_QOM_Ordering)
-        * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source The source
-        * @param array &$sql The query parts
-        * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedOrderException
-        * @return void
-        */
-       protected function parseOrderings(array $orderings, \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source, array &$sql) {
-               foreach ($orderings as $propertyName => $order) {
-                       switch ($order) {
-                               case \TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelConstantsInterface::JCR_ORDER_ASCENDING:
-
-                               case \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING:
-                                       $order = 'ASC';
-                                       break;
-                               case \TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelConstantsInterface::JCR_ORDER_DESCENDING:
-
-                               case \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING:
-                                       $order = 'DESC';
-                                       break;
-                               default:
-                                       throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedOrderException('Unsupported order encountered.', 1242816075);
-                       }
-                       $className = '';
-                       $tableName = '';
-                       if ($source instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SelectorInterface) {
-                               $className = $source->getNodeTypeName();
-                               $tableName = $this->dataMapper->convertClassNameToTableName($className);
-                               while (strpos($propertyName, '.') !== FALSE) {
-                                       $this->addUnionStatement($className, $tableName, $propertyName, $sql);
-                               }
-                       } elseif ($source instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\JoinInterface) {
-                               $tableName = $source->getLeft()->getSelectorName();
-                       }
-                       $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
-                       if (strlen($tableName) > 0) {
-                               $sql['orderings'][] = $tableName . '.' . $columnName . ' ' . $order;
-                       } else {
-                               $sql['orderings'][] = $columnName . ' ' . $order;
-                       }
-               }
-       }
-
-       /**
         * Performs workspace and language overlay on the given row array. The language and workspace id is automatically
         * detected (depending on FE or BE context). You can also explicitly set the language/workspace id.
         *
diff --git a/typo3/sysext/extbase/Classes/Persistence/Generic/Storage/Typo3DbQueryParser.php b/typo3/sysext/extbase/Classes/Persistence/Generic/Storage/Typo3DbQueryParser.php
new file mode 100644 (file)
index 0000000..3bd3594
--- /dev/null
@@ -0,0 +1,791 @@
+<?php
+namespace TYPO3\CMS\Extbase\Persistence\Generic\Storage;
+
+/***************************************************************
+ *  Copyright notice
+ *
+ *  (c) 2014 Extbase Team (http://forge.typo3.org/projects/typo3v4-mvc)
+ *  Extbase is a backport of TYPO3 Flow. All credits go to the TYPO3 Flow team.
+ *  All rights reserved
+ *
+ *  This script is part of the TYPO3 project. The TYPO3 project is
+ *  free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The GNU General Public License can be found at
+ *  http://www.gnu.org/copyleft/gpl.html.
+ *  A copy is found in the text file GPL.txt and important notices to the license
+ *  from the author is found in LICENSE.txt distributed with these scripts.
+ *
+ *
+ *  This script is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  This copyright notice MUST APPEAR in all copies of the script!
+ ***************************************************************/
+
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
+
+/**
+ * QueryParser, converting the qom to string representation
+ */
+class Typo3DbQueryParser {
+
+       const OPERATOR_EQUAL_TO_NULL = 'operatorEqualToNull';
+       const OPERATOR_NOT_EQUAL_TO_NULL = 'operatorNotEqualToNull';
+
+       /**
+        * The TYPO3 database object
+        *
+        * @var \TYPO3\CMS\Core\Database\DatabaseConnection
+        */
+       protected $databaseHandle;
+
+       /**
+        * @var \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper
+        * @inject
+        */
+       protected $dataMapper;
+
+       /**
+        * The TYPO3 page repository. Used for language and workspace overlay
+        *
+        * @var \TYPO3\CMS\Frontend\Page\PageRepository
+        */
+       protected $pageRepository;
+
+       /**
+        * @var \TYPO3\CMS\Core\Cache\CacheManager
+        * @inject
+        */
+       protected $cacheManager;
+
+       /**
+        * @var \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend
+        */
+       protected $tableColumnCache;
+
+       /**
+        * @var \TYPO3\CMS\Extbase\Service\EnvironmentService
+        * @inject
+        */
+       protected $environmentService;
+
+       /**
+        * Constructor. takes the database handle from $GLOBALS['TYPO3_DB']
+        */
+       public function __construct() {
+               $this->databaseHandle = $GLOBALS['TYPO3_DB'];
+       }
+
+       /**
+        * Lifecycle method
+        *
+        * @return void
+        */
+       public function initializeObject() {
+               $this->tableColumnCache = $this->cacheManager->getCache('extbase_typo3dbbackend_tablecolumns');
+       }
+
+       /**
+        * Preparses the query and returns the query's hash and the parameters
+        *
+        * @param \TYPO3\CMS\Extbase\Persistence\QueryInterface $query The query
+        * @return array the hash and the parameters
+        */
+       public function preparseQuery(\TYPO3\CMS\Extbase\Persistence\QueryInterface $query) {
+               $parameters = $this->preparseComparison($query->getConstraint());
+               $hashPartials = array(
+                       $query->getQuerySettings(),
+                       $query->getSource(),
+                       array_keys($parameters),
+                       $query->getOrderings(),
+               );
+               $hash = md5(serialize($hashPartials));
+
+               return array($hash, $parameters);
+       }
+
+       /**
+        * Walks through the qom's constraints and extracts the properties and values.
+        *
+        * In the qom the query structure and values are glued together. This walks through the
+        * qom and only extracts the parts necessary for generating the hash and filling the
+        * statement. It leaves out the actual statement generation, as it is the most time
+        * consuming.
+        *
+        * @param object $comparison The constraint. Could be And-, Or-, Not- or ComparisonInterface
+        * @param string $qomPath current position of the child in the qom
+        * @return array string representation of constraint and array of parameters
+        * @throws \Exception
+        */
+       protected function preparseComparison($comparison, $qomPath = '') {
+               $parameters = array();
+               $objectsToParse = array();
+
+               if ($comparison instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\AndInterface) {
+                       $delimiter = 'AND';
+                       $objectsToParse = array($comparison->getConstraint1(), $comparison->getConstraint2());
+               } elseif ($comparison instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\OrInterface) {
+                       $delimiter = 'OR';
+                       $objectsToParse = array($comparison->getConstraint1(), $comparison->getConstraint2());
+               } elseif ($comparison instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\NotInterface) {
+                       $delimiter = 'NOT';
+                       $objectsToParse = array($comparison->getConstraint());
+               } elseif ($comparison instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface) {
+                       $parameterIdentifier = $this->normalizeParameterIdentifier($qomPath . $comparison->getOperand1()->getPropertyName());
+                       $comparison->setParameterIdentifier($parameterIdentifier);
+                       $parameters[$parameterIdentifier] = $comparison->getOperand2();
+               } elseif (!is_object($comparison)) {
+                       return array(array(), $comparison);
+               } else {
+                       throw new \Exception('Can not hash Query Component "' . get_class($comparison) . '".', 1392840462);
+               }
+
+               $childObjectIterator = 0;
+               foreach ($objectsToParse as $objectToParse) {
+                       $preparsedParameters = $this->preparseComparison($objectToParse, $qomPath . $delimiter . $childObjectIterator++);
+                       if (!empty($preparsedParameters)) {
+                               $parameters = array_merge($parameters, $preparsedParameters);
+                       }
+               }
+
+               return $parameters;
+       }
+
+       /**
+        * normalizes the parameter's identifier
+        *
+        * @param string $identifier
+        * @return string
+        * @todo come on, clean up that method!
+        */
+       public function normalizeParameterIdentifier($identifier) {
+               return ':' . preg_replace('/[^A-Za-z0-9]/', '', $identifier);
+       }
+
+       /**
+        * Parses the query and returns the SQL statement parts.
+        *
+        * @param \TYPO3\CMS\Extbase\Persistence\QueryInterface $query The query
+        * @return array The SQL statement parts
+        */
+       public function parseQuery(\TYPO3\CMS\Extbase\Persistence\QueryInterface $query) {
+               $sql = array();
+               $sql['keywords'] = array();
+               $sql['tables'] = array();
+               $sql['unions'] = array();
+               $sql['fields'] = array();
+               $sql['where'] = array();
+               $sql['additionalWhereClause'] = array();
+               $sql['orderings'] = array();
+               $sql['limit'] = ((int)$query->getLimit() ?: NULL);
+               $sql['offset'] = ((int)$query->getOffset() ?: NULL);
+               $source = $query->getSource();
+               $this->parseSource($source, $sql);
+               $this->parseConstraint($query->getConstraint(), $source, $sql);
+               $this->parseOrderings($query->getOrderings(), $source, $sql);
+
+               $tableNames = array_unique(array_keys($sql['tables'] + $sql['unions']));
+               foreach ($tableNames as $tableName) {
+                       if (is_string($tableName) && !empty($tableName)) {
+                               $this->addAdditionalWhereClause($query->getQuerySettings(), $tableName, $sql);
+                       }
+               }
+
+               return $sql;
+       }
+
+       /**
+        * Transforms a Query Source into SQL and parameter arrays
+        *
+        * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source The source
+        * @param array &$sql
+        * @return void
+        */
+       protected function parseSource(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source, array &$sql) {
+               if ($source instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SelectorInterface) {
+                       $className = $source->getNodeTypeName();
+                       $tableName = $this->dataMapper->getDataMap($className)->getTableName();
+                       $this->addRecordTypeConstraint($className, $sql);
+                       $sql['fields'][$tableName] = $tableName . '.*';
+                       $sql['tables'][$tableName] = $tableName;
+               } elseif ($source instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\JoinInterface) {
+                       $this->parseJoin($source, $sql);
+               }
+       }
+
+       /**
+        * Transforms a constraint into SQL and parameter arrays
+        *
+        * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface $constraint The constraint
+        * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source The source
+        * @param array &$sql The query parts
+        * @return void
+        */
+       protected function parseConstraint(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface $constraint = NULL, \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source, array &$sql) {
+               if ($constraint instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\AndInterface) {
+                       $sql['where'][] = '(';
+                       $this->parseConstraint($constraint->getConstraint1(), $source, $sql, $parameters);
+                       $sql['where'][] = ' AND ';
+                       $this->parseConstraint($constraint->getConstraint2(), $source, $sql, $parameters);
+                       $sql['where'][] = ')';
+               } elseif ($constraint instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\OrInterface) {
+                       $sql['where'][] = '(';
+                       $this->parseConstraint($constraint->getConstraint1(), $source, $sql, $parameters);
+                       $sql['where'][] = ' OR ';
+                       $this->parseConstraint($constraint->getConstraint2(), $source, $sql, $parameters);
+                       $sql['where'][] = ')';
+               } elseif ($constraint instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\NotInterface) {
+                       $sql['where'][] = 'NOT (';
+                       $this->parseConstraint($constraint->getConstraint(), $source, $sql, $parameters);
+                       $sql['where'][] = ')';
+               } elseif ($constraint instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface) {
+                       $this->parseComparison($constraint, $source, $sql);
+               }
+       }
+
+       /**
+        * Transforms orderings into SQL.
+        *
+        * @param array $orderings An array of orderings (Tx_Extbase_Persistence_QOM_Ordering)
+        * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source The source
+        * @param array &$sql The query parts
+        * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedOrderException
+        * @return void
+        */
+       protected function parseOrderings(array $orderings, \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source, array &$sql) {
+               foreach ($orderings as $propertyName => $order) {
+                       switch ($order) {
+                               case \TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelConstantsInterface::JCR_ORDER_ASCENDING:
+
+                               case \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING:
+                                       $order = 'ASC';
+                                       break;
+                               case \TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelConstantsInterface::JCR_ORDER_DESCENDING:
+
+                               case \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING:
+                                       $order = 'DESC';
+                                       break;
+                               default:
+                                       throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedOrderException('Unsupported order encountered.', 1242816074);
+                       }
+                       $className = '';
+                       $tableName = '';
+                       if ($source instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SelectorInterface) {
+                               $className = $source->getNodeTypeName();
+                               $tableName = $this->dataMapper->convertClassNameToTableName($className);
+                               while (strpos($propertyName, '.') !== FALSE) {
+                                       $this->addUnionStatement($className, $tableName, $propertyName, $sql);
+                               }
+                       } elseif ($source instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\JoinInterface) {
+                               $tableName = $source->getLeft()->getSelectorName();
+                       }
+                       $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
+                       if ($tableName !== '') {
+                               $sql['orderings'][] = $tableName . '.' . $columnName . ' ' . $order;
+                       } else {
+                               $sql['orderings'][] = $columnName . ' ' . $order;
+                       }
+               }
+       }
+
+       /**
+        * Parse a Comparison into SQL and parameter arrays.
+        *
+        * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface $comparison The comparison to parse
+        * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source The source
+        * @param array &$sql SQL query parts to add to
+        * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\RepositoryException
+        * @return void
+        */
+       protected function parseComparison(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface $comparison, \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source, array &$sql) {
+               $operand1 = $comparison->getOperand1();
+               $operator = $comparison->getOperator();
+               $operand2 = $comparison->getOperand2();
+               if ($operator === \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_IN) {
+                       $items = array();
+                       $hasValue = FALSE;
+                       foreach ($operand2 as $value) {
+                               $value = $this->getPlainValue($value);
+                               if ($value !== NULL) {
+                                       $parameters[] = $value;
+                                       $hasValue = TRUE;
+                               }
+                       }
+                       if ($hasValue === FALSE) {
+                               $sql['where'][] = '1<>1';
+                       } else {
+                               $this->parseDynamicOperand($comparison, $source, $sql, NULL);
+                       }
+               } elseif ($operator === \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_CONTAINS) {
+                       if ($operand2 === NULL) {
+                               $sql['where'][] = '1<>1';
+                       } else {
+                               $className = $source->getNodeTypeName();
+                               $tableName = $this->dataMapper->convertClassNameToTableName($className);
+                               $propertyName = $operand1->getPropertyName();
+                               while (strpos($propertyName, '.') !== FALSE) {
+                                       $this->addUnionStatement($className, $tableName, $propertyName, $sql);
+                               }
+                               $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
+                               $dataMap = $this->dataMapper->getDataMap($className);
+                               $columnMap = $dataMap->getColumnMap($propertyName);
+                               $typeOfRelation = $columnMap instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap ? $columnMap->getTypeOfRelation() : NULL;
+                               if ($typeOfRelation === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
+                                       $relationTableName = $columnMap->getRelationTableName();
+                                       $sql['where'][] = $tableName . '.uid IN (SELECT ' . $columnMap->getParentKeyFieldName() . ' FROM ' . $relationTableName . ' WHERE ' . $columnMap->getChildKeyFieldName() . '=?)';
+                               } elseif ($typeOfRelation === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_MANY) {
+                                       $parentKeyFieldName = $columnMap->getParentKeyFieldName();
+                                       if (isset($parentKeyFieldName)) {
+                                               $childTableName = $columnMap->getChildTableName();
+                                               $sql['where'][] = $tableName . '.uid=(SELECT ' . $childTableName . '.' . $parentKeyFieldName . ' FROM ' . $childTableName . ' WHERE ' . $childTableName . '.uid=?)';
+                                       } else {
+                                               $sql['where'][] = 'FIND_IN_SET(?,' . $tableName . '.' . $columnName . ')';
+                                       }
+                               } else {
+                                       throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\RepositoryException('Unsupported or non-existing property name "' . $propertyName . '" used in relation matching.', 1327065745);
+                               }
+                       }
+               } else {
+                       if ($operand2 === NULL) {
+                               if ($operator === \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_EQUAL_TO) {
+                                       $operator = self::OPERATOR_EQUAL_TO_NULL;
+                               } elseif ($operator === \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_NOT_EQUAL_TO) {
+                                       $operator = self::OPERATOR_NOT_EQUAL_TO_NULL;
+                               }
+                       }
+                       $this->parseDynamicOperand($comparison, $source, $sql, $parameters);
+               }
+       }
+
+       /**
+        * Parse a DynamicOperand into SQL and parameter arrays.
+        *
+        * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\DynamicOperandInterface $operand
+        * @param string $operator One of the JCR_OPERATOR_* constants
+        * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source The source
+        * @param array &$sql The query parts
+        * @param string $valueFunction an optional SQL function to apply to the operand value
+        * @return void
+        */
+       protected function parseDynamicOperand(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface $comparison, \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source, array &$sql, $valueFunction = NULL) {
+               $operand = $comparison->getOperand1();
+               $operator = $comparison->getOperator();
+               $operand2 = $comparison->getOperand2();
+
+               if ($operand instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\LowerCaseInterface) {
+                       $this->parseDynamicOperand($operand->getOperand(), $operator, $source, $sql, $parameters, 'LOWER');
+               } elseif ($operand instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\UpperCaseInterface) {
+                       $this->parseDynamicOperand($operand->getOperand(), $operator, $source, $sql, $parameters, 'UPPER');
+               } elseif ($operand instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\PropertyValueInterface) {
+                       $propertyName = $operand->getPropertyName();
+                       if ($source instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SelectorInterface) {
+                               // FIXME Only necessary to differ from  Join
+                               $className = $source->getNodeTypeName();
+                               $tableName = $this->dataMapper->convertClassNameToTableName($className);
+                               while (strpos($propertyName, '.') !== FALSE) {
+                                       $this->addUnionStatement($className, $tableName, $propertyName, $sql);
+                               }
+                       } elseif ($source instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\JoinInterface) {
+                               $tableName = $source->getJoinCondition()->getSelector1Name();
+                       }
+                       $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
+                       $operator = $this->resolveOperator($operator);
+                       $constraintSQL = '';
+                       if ($valueFunction === NULL) {
+                               $constraintSQL .= (!empty($tableName) ? $tableName . '.' : '') . $columnName . ' ' . $operator . ' ';
+                       } else {
+                               $constraintSQL .= $valueFunction . '(' . (!empty($tableName) ? $tableName . '.' : '') . $columnName . ') ' . $operator . ' ';
+                       }
+
+                       if ($operator === 'LIKE' || $operator === 'IN') {
+                               $constraintSQL .= '(' . $this->normalizeParameterIdentifier($comparison->getParameterIdentifier()) . ')';
+                       } else {
+                               $constraintSQL .= $this->normalizeParameterIdentifier($comparison->getParameterIdentifier());
+                       }
+
+                       $sql['where'][] = $constraintSQL;
+               }
+       }
+
+       /**
+        * Add a constraint to ensure that the record type of the returned tuples is matching the data type of the repository.
+        *
+        * @param string $className The class name
+        * @param array &$sql The query parts
+        * @return void
+        */
+       protected function addRecordTypeConstraint($className, &$sql) {
+               if ($className !== NULL) {
+                       $dataMap = $this->dataMapper->getDataMap($className);
+                       if ($dataMap->getRecordTypeColumnName() !== NULL) {
+                               $recordTypes = array();
+                               if ($dataMap->getRecordType() !== NULL) {
+                                       $recordTypes[] = $dataMap->getRecordType();
+                               }
+                               foreach ($dataMap->getSubclasses() as $subclassName) {
+                                       $subclassDataMap = $this->dataMapper->getDataMap($subclassName);
+                                       if ($subclassDataMap->getRecordType() !== NULL) {
+                                               $recordTypes[] = $subclassDataMap->getRecordType();
+                                       }
+                               }
+                               if (!empty($recordTypes)) {
+                                       $recordTypeStatements = array();
+                                       foreach ($recordTypes as $recordType) {
+                                               $tableName = $dataMap->getTableName();
+                                               $recordTypeStatements[] = $tableName . '.' . $dataMap->getRecordTypeColumnName() . '=' . $this->databaseHandle->fullQuoteStr($recordType, $tableName);
+                                       }
+                                       $sql['additionalWhereClause'][] = '(' . implode(' OR ', $recordTypeStatements) . ')';
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Adds additional WHERE statements according to the query settings.
+        *
+        * @param \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
+        * @param string $tableName The table name to add the additional where clause for
+        * @param string &$sql
+        * @return void
+        */
+       protected function addAdditionalWhereClause(\TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings, $tableName, &$sql) {
+               $this->addVisibilityConstraintStatement($querySettings, $tableName, $sql);
+               if ($querySettings->getRespectSysLanguage()) {
+                       $this->addSysLanguageStatement($tableName, $sql, $querySettings);
+               }
+               if ($querySettings->getRespectStoragePage()) {
+                       $this->addPageIdStatement($tableName, $sql, $querySettings->getStoragePageIds());
+               }
+       }
+
+       /**
+        * Adds enableFields and deletedClause to the query if necessary
+        *
+        * @param \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings
+        * @param string $tableName The database table name
+        * @param array &$sql The query parts
+        * @return void
+        */
+       protected function addVisibilityConstraintStatement(\TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings, $tableName, array &$sql) {
+               $statement = '';
+               if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) {
+                       $ignoreEnableFields = $querySettings->getIgnoreEnableFields();
+                       $enableFieldsToBeIgnored = $querySettings->getEnableFieldsToBeIgnored();
+                       $includeDeleted = $querySettings->getIncludeDeleted();
+                       if ($this->environmentService->isEnvironmentInFrontendMode()) {
+                               $statement .= $this->getFrontendConstraintStatement($tableName, $ignoreEnableFields, $enableFieldsToBeIgnored, $includeDeleted);
+                       } else {
+                               // TYPO3_MODE === 'BE'
+                               $statement .= $this->getBackendConstraintStatement($tableName, $ignoreEnableFields, $includeDeleted);
+                       }
+                       if (!empty($statement)) {
+                               $statement = strtolower(substr($statement, 1, 3)) === 'and' ? substr($statement, 5) : $statement;
+                               $sql['additionalWhereClause'][] = $statement;
+                       }
+               }
+       }
+
+       /**
+        * Returns constraint statement for frontend context
+        *
+        * @param string $tableName
+        * @param bool $ignoreEnableFields A flag indicating whether the enable fields should be ignored
+        * @param array $enableFieldsToBeIgnored If $ignoreEnableFields is true, this array specifies enable fields to be ignored. If it is NULL or an empty array (default) all enable fields are ignored.
+        * @param bool $includeDeleted A flag indicating whether deleted records should be included
+        * @return string
+        * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException
+        */
+       protected function getFrontendConstraintStatement($tableName, $ignoreEnableFields, array $enableFieldsToBeIgnored = array(), $includeDeleted) {
+               $statement = '';
+               if ($ignoreEnableFields && !$includeDeleted) {
+                       if (count($enableFieldsToBeIgnored)) {
+                               // array_combine() is necessary because of the way \TYPO3\CMS\Frontend\Page\PageRepository::enableFields() is implemented
+                               $statement .= $this->getPageRepository()->enableFields($tableName, -1, array_combine($enableFieldsToBeIgnored, $enableFieldsToBeIgnored));
+                       } else {
+                               $statement .= $this->getPageRepository()->deleteClause($tableName);
+                       }
+               } elseif (!$ignoreEnableFields && !$includeDeleted) {
+                       $statement .= $this->getPageRepository()->enableFields($tableName);
+               } elseif (!$ignoreEnableFields && $includeDeleted) {
+                       throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException('Query setting "ignoreEnableFields=FALSE" can not be used together with "includeDeleted=TRUE" in frontend context.', 1327678173);
+               }
+               return $statement;
+       }
+
+       /**
+        * Returns constraint statement for backend context
+        *
+        * @param string $tableName
+        * @param bool $ignoreEnableFields A flag indicating whether the enable fields should be ignored
+        * @param bool $includeDeleted A flag indicating whether deleted records should be included
+        * @return string
+        */
+       protected function getBackendConstraintStatement($tableName, $ignoreEnableFields, $includeDeleted) {
+               $statement = '';
+               if (!$ignoreEnableFields) {
+                       $statement .= BackendUtility::BEenableFields($tableName);
+               }
+               if (!$includeDeleted) {
+                       $statement .= BackendUtility::deleteClause($tableName);
+               }
+               return $statement;
+       }
+
+       /**
+        * Builds the language field statement
+        *
+        * @param string $tableName The database table name
+        * @param array &$sql The query parts
+        * @param \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
+        * @return void
+        */
+       protected function addSysLanguageStatement($tableName, array &$sql, $querySettings) {
+               if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) {
+                       if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])) {
+                               // Select all entries for the current language
+                               $additionalWhereClause = $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . ' IN (' . (int)$querySettings->getLanguageUid() . ',-1)';
+                               // If any language is set -> get those entries which are not translated yet
+                               // They will be removed by t3lib_page::getRecordOverlay if not matching overlay mode
+                               if (isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
+                                       && $querySettings->getLanguageUid() > 0
+                               ) {
+                                       $additionalWhereClause .= ' OR (' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=0' .
+                                               ' AND ' . $tableName . '.uid NOT IN (SELECT ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] .
+                                               ' FROM ' . $tableName .
+                                               ' WHERE ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] . '>0' .
+                                               ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '>0';
+
+                                       // Add delete clause to ensure all entries are loaded
+                                       if (isset($GLOBALS['TCA'][$tableName]['ctrl']['delete'])) {
+                                               $additionalWhereClause .= ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['delete'] . '=0';
+                                       }
+                                       $additionalWhereClause .= '))';
+                               }
+                               $sql['additionalWhereClause'][] = '(' . $additionalWhereClause . ')';
+                       }
+               }
+       }
+
+       /**
+        * Builds the page ID checking statement
+        *
+        * @param string $tableName The database table name
+        * @param array &$sql The query parts
+        * @param array $storagePageIds list of storage page ids
+        * @return void
+        */
+       protected function addPageIdStatement($tableName, array &$sql, array $storagePageIds) {
+               $tableColumns = $this->tableColumnCache->get($tableName);
+               if ($tableColumns === FALSE) {
+                       $tableColumns = $this->databaseHandle->admin_get_fields($tableName);
+                       $this->tableColumnCache->set($tableName, $tableColumns);
+               }
+               if (is_array($GLOBALS['TCA'][$tableName]['ctrl']) && array_key_exists('pid', $tableColumns)) {
+                       $rootLevel = (int)$GLOBALS['TCA'][$tableName]['ctrl']['rootLevel'];
+                       if ($rootLevel) {
+                               if ($rootLevel === 1) {
+                                       $sql['additionalWhereClause'][] = $tableName . '.pid = 0';
+                               }
+                       } else {
+                               if (empty($storagePageIds)) {
+                                       throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException('Missing storage page ids.', 1365779762);
+                               }
+                               $sql['additionalWhereClause'][] = $tableName . '.pid IN (' . implode(', ', $storagePageIds) . ')';
+                       }
+               }
+       }
+
+       /**
+        * Transforms a Join into SQL and parameter arrays
+        *
+        * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\JoinInterface $join The join
+        * @param array &$sql The query parts
+        * @return void
+        */
+       protected function parseJoin(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\JoinInterface $join, array &$sql) {
+               $leftSource = $join->getLeft();
+               $leftClassName = $leftSource->getNodeTypeName();
+               $this->addRecordTypeConstraint($leftClassName, $sql);
+               $leftTableName = $leftSource->getSelectorName();
+               $rightSource = $join->getRight();
+               if ($rightSource instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\JoinInterface) {
+                       $rightClassName = $rightSource->getLeft()->getNodeTypeName();
+                       $rightTableName = $rightSource->getLeft()->getSelectorName();
+               } else {
+                       $rightClassName = $rightSource->getNodeTypeName();
+                       $rightTableName = $rightSource->getSelectorName();
+                       $sql['fields'][$leftTableName] = $rightTableName . '.*';
+               }
+               $this->addRecordTypeConstraint($rightClassName, $sql);
+               $sql['tables'][$leftTableName] = $leftTableName;
+               $sql['unions'][$rightTableName] = 'LEFT JOIN ' . $rightTableName;
+               $joinCondition = $join->getJoinCondition();
+               if ($joinCondition instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\EquiJoinCondition) {
+                       $column1Name = $this->dataMapper->convertPropertyNameToColumnName($joinCondition->getProperty1Name(), $leftClassName);
+                       $column2Name = $this->dataMapper->convertPropertyNameToColumnName($joinCondition->getProperty2Name(), $rightClassName);
+                       $sql['unions'][$rightTableName] .= ' ON ' . $joinCondition->getSelector1Name() . '.' . $column1Name . ' = ' . $joinCondition->getSelector2Name() . '.' . $column2Name;
+               }
+               if ($rightSource instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\JoinInterface) {
+                       $this->parseJoin($rightSource, $sql);
+               }
+       }
+
+       /**
+        * Returns a plain value, i.e. objects are flattened out if possible.
+        *
+        * @param mixed $input
+        * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException
+        * @return mixed
+        */
+       protected function getPlainValue($input) {
+               if (is_array($input)) {
+                       throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException('An array could not be converted to a plain value.', 1274799932);
+               }
+               if ($input instanceof \DateTime) {
+                       return $input->format('U');
+               } elseif (TypeHandlingUtility::isCoreType($input)) {
+                       return (string) $input;
+               } elseif (is_object($input)) {
+                       if ($input instanceof \TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy) {
+                               $realInput = $input->_loadRealInstance();
+                       } else {
+                               $realInput = $input;
+                       }
+                       if ($realInput instanceof \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface) {
+                               return $realInput->getUid();
+                       } else {
+                               throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException('An object of class "' . get_class($realInput) . '" could not be converted to a plain value.', 1274799934);
+                       }
+               } elseif (is_bool($input)) {
+                       return $input === TRUE ? 1 : 0;
+               } else {
+                       return $input;
+               }
+       }
+
+
+       /**
+        * adds a union statement to the query, mostly for tables referenced in the where condition.
+        *
+        * @param string &$className
+        * @param string &$tableName
+        * @param array &$propertyPath
+        * @param array &$sql
+        * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception
+        * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidRelationConfigurationException
+        * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\MissingColumnMapException
+        */
+       protected function addUnionStatement(&$className, &$tableName, &$propertyPath, array &$sql) {
+               $explodedPropertyPath = explode('.', $propertyPath, 2);
+               $propertyName = $explodedPropertyPath[0];
+               $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
+               $tableName = $this->dataMapper->convertClassNameToTableName($className);
+               $columnMap = $this->dataMapper->getDataMap($className)->getColumnMap($propertyName);
+
+               if ($columnMap === NULL) {
+                       throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\MissingColumnMapException('The ColumnMap for property "' . $propertyName . '" of class "' . $className . '" is missing.', 1355142232);
+               }
+
+               $parentKeyFieldName = $columnMap->getParentKeyFieldName();
+               $childTableName = $columnMap->getChildTableName();
+
+               if ($childTableName === NULL) {
+                       throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidRelationConfigurationException('The relation information for property "' . $propertyName . '" of class "' . $className . '" is missing.', 1353170925);
+               }
+
+               if ($columnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_ONE) {
+                       if (isset($parentKeyFieldName)) {
+                               $sql['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $tableName . '.uid=' . $childTableName . '.' . $parentKeyFieldName;
+                       } else {
+                               $sql['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $tableName . '.' . $columnName . '=' . $childTableName . '.uid';
+                       }
+                       $className = $this->dataMapper->getType($className, $propertyName);
+               } elseif ($columnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_MANY) {
+                       if (isset($parentKeyFieldName)) {
+                               $sql['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $tableName . '.uid=' . $childTableName . '.' . $parentKeyFieldName;
+                       } else {
+                               $onStatement = '(FIND_IN_SET(' . $childTableName . '.uid, ' . $tableName . '.' . $columnName . '))';
+                               $sql['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $onStatement;
+                       }
+                       $className = $this->dataMapper->getType($className, $propertyName);
+               } elseif ($columnMap->getTypeOfRelation() === \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
+                       $relationTableName = $columnMap->getRelationTableName();
+                       $sql['unions'][$relationTableName] = 'LEFT JOIN ' . $relationTableName . ' ON ' . $tableName . '.uid=' . $relationTableName . '.' . $columnMap->getParentKeyFieldName();
+                       $sql['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $relationTableName . '.' . $columnMap->getChildKeyFieldName() . '=' . $childTableName . '.uid';
+                       $className = $this->dataMapper->getType($className, $propertyName);
+               } else {
+                       throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception('Could not determine type of relation.', 1252502725);
+               }
+               // TODO check if there is another solution for this
+               $sql['keywords']['distinct'] = 'DISTINCT';
+               $propertyPath = $explodedPropertyPath[1];
+               $tableName = $childTableName;
+       }
+
+       /**
+        * Returns the SQL operator for the given JCR operator type.
+        *
+        * @param string $operator One of the JCR_OPERATOR_* constants
+        * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception
+        * @return string an SQL operator
+        */
+       protected function resolveOperator($operator) {
+               switch ($operator) {
+                       case self::OPERATOR_EQUAL_TO_NULL:
+                               $operator = 'IS';
+                               break;
+                       case self::OPERATOR_NOT_EQUAL_TO_NULL:
+                               $operator = 'IS NOT';
+                               break;
+                       case \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_IN:
+                               $operator = 'IN';
+                               break;
+                       case \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_EQUAL_TO:
+                               $operator = '=';
+                               break;
+                       case \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_NOT_EQUAL_TO:
+                               $operator = '!=';
+                               break;
+                       case \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_LESS_THAN:
+                               $operator = '<';
+                               break;
+                       case \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_LESS_THAN_OR_EQUAL_TO:
+                               $operator = '<=';
+                               break;
+                       case \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_GREATER_THAN:
+                               $operator = '>';
+                               break;
+                       case \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_GREATER_THAN_OR_EQUAL_TO:
+                               $operator = '>=';
+                               break;
+                       case \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_LIKE:
+                               $operator = 'LIKE';
+                               break;
+                       default:
+                               throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception('Unsupported operator encountered.', 1242816073);
+               }
+               return $operator;
+       }
+
+       /**
+        * @return \TYPO3\CMS\Frontend\Page\PageRepository
+        */
+       protected function getPageRepository() {
+               if (!$this->pageRepository instanceof \TYPO3\CMS\Frontend\Page\PageRepository) {
+                       if ($this->environmentService->isEnvironmentInFrontendMode() && is_object($GLOBALS['TSFE'])) {
+                               $this->pageRepository = $GLOBALS['TSFE']->sys_page;
+                       } else {
+                               $this->pageRepository = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Frontend\\Page\\PageRepository');
+                       }
+               }
+
+               return $this->pageRepository;
+       }
+}
index 5443119..6e82b74 100644 (file)
@@ -119,6 +119,13 @@ class Typo3QuerySettings implements \TYPO3\CMS\Extbase\Persistence\Generic\Query
        protected $usePreparedStatement = FALSE;
 
        /**
+        * Flag whether the query should be cached using the caching framework
+        *
+        * @var bool
+        */
+       protected $useQueryCache = TRUE;
+
+       /**
         * As long as we use a feature flag ignoreAllEnableFieldsInBe to determine the default behavior, the
         * initializeObject is responsible for handling that.
         */
@@ -424,4 +431,20 @@ class Typo3QuerySettings implements \TYPO3\CMS\Extbase\Persistence\Generic\Query
        public function getUsePreparedStatement() {
                return (bool)$this->usePreparedStatement;
        }
+
+       /**
+        * @param bool $useQueryCache
+        * @return \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface
+        */
+       public function useQueryCache($useQueryCache) {
+               $this->useQueryCache = $useQueryCache;
+               return $this;
+       }
+
+       /**
+        * @return bool
+        */
+       public function getUseQueryCache() {
+               return (bool)$this->useQueryCache;
+       }
 }
index 8150f85..847e3b7 100644 (file)
@@ -34,366 +34,6 @@ namespace TYPO3\CMS\Extbase\Tests\Unit\Persistence\Generic\Storage;
 class Typo3DbBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
 
        /**
-        * This is the data provider for the statement generation with a basic comparison
-        *
-        * @return array An array of data
-        */
-       public function providerForBasicComparison() {
-               return array(
-                       'equal' => array(
-                               \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_EQUAL_TO,
-                               'SELECT table_name_from_selector.* FROM table_name_from_selector WHERE table_name_from_property.foo = \'baz\''
-                       ),
-                       'less' => array(
-                               \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_LESS_THAN,
-                               'SELECT table_name_from_selector.* FROM table_name_from_selector WHERE table_name_from_property.foo < \'baz\''
-                       ),
-                       'less or equal' => array(
-                               \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_LESS_THAN_OR_EQUAL_TO,
-                               'SELECT table_name_from_selector.* FROM table_name_from_selector WHERE table_name_from_property.foo <= \'baz\''
-                       ),
-                       'greater' => array(
-                               \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_GREATER_THAN,
-                               'SELECT table_name_from_selector.* FROM table_name_from_selector WHERE table_name_from_property.foo > \'baz\''
-                       ),
-                       'greater or equal' => array(
-                               \TYPO3\CMS\Extbase\Persistence\QueryInterface::OPERATOR_GREATER_THAN_OR_EQUAL_TO,
-                               'SELECT table_name_from_selector.* FROM table_name_from_selector WHERE table_name_from_property.foo >= \'baz\''
-                       )
-               );
-       }
-
-       /**
-        * @test
-        */
-       public function addSysLanguageStatementWorksForDefaultLanguage() {
-               $table = uniqid('tx_coretest_table');
-               $GLOBALS['TCA'][$table]['ctrl'] = array(
-                       'languageField' => 'sys_language_uid'
-               );
-               /** @var \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings|\PHPUnit_Framework_MockObject_MockObject $querySettings */
-               $querySettings = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Typo3QuerySettings');
-               $sql = array();
-               $mockTypo3DbBackend = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend', array('dummy'), array(), '', FALSE);
-               $mockTypo3DbBackend->_callRef('addSysLanguageStatement', $table, $sql, $querySettings);
-               $expectedSql = array('additionalWhereClause' => array('(' . $table . '.sys_language_uid IN (0,-1))'));
-               $this->assertSame($expectedSql, $sql);
-       }
-
-       /**
-        * @test
-        */
-       public function addSysLanguageStatementWorksForNonDefaultLanguage() {
-               $table = uniqid('tx_coretest_table');
-               $GLOBALS['TCA'][$table]['ctrl'] = array(
-                       'languageField' => 'sys_language_uid'
-               );
-               $sql = array();
-               /** @var \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings|\PHPUnit_Framework_MockObject_MockObject $querySettings */
-               $querySettings = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Typo3QuerySettings', array('dummy'));
-               $querySettings->setLanguageUid('1');
-               $mockTypo3DbBackend = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend', array('dummy'), array(), '', FALSE);
-               $mockTypo3DbBackend->_callRef('addSysLanguageStatement', $table, $sql, $querySettings);
-               $result = array('additionalWhereClause' => array('(' . $table . '.sys_language_uid IN (1,-1))'));
-               $this->assertSame($result, $sql);
-       }
-
-       /**
-        * @test
-        */
-       public function addSysLanguageStatementWorksInBackendContextWithNoGlobalTypoScriptFrontendControllerAvailable() {
-               $table = uniqid('tx_coretest_table');
-               $GLOBALS['TCA'][$table]['ctrl'] = array(
-                       'languageField' => 'sys_language_uid'
-               );
-               $sql = array();
-               $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
-               $mockTypo3DbBackend = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend', array('dummy'), array(), '', FALSE);
-               $mockTypo3DbBackend->_callRef('addSysLanguageStatement', $table, $sql, $querySettings);
-               $expectedSql = array('additionalWhereClause' => array('(' . $table . '.sys_language_uid IN (0,-1))'));
-               $this->assertSame($expectedSql, $sql);
-       }
-
-       /**
-        * @test
-        */
-       public function addSysLanguageStatementWorksForDefaultLanguageWithoutDeleteStatementReturned() {
-               $table = uniqid('tx_coretest_table');
-               $GLOBALS['TCA'][$table]['ctrl'] = array(
-                       'languageField' => 'sys_language_uid',
-                       'delete' => 'deleted'
-               );
-               $sql = array();
-               $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
-               $querySettings->setLanguageUid(0);
-               $mockTypo3DbBackend = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend', array('dummy'), array(), '', FALSE);
-               $mockTypo3DbBackend->_callRef('addSysLanguageStatement', $table, $sql, $querySettings);
-               $expectedSql = array('additionalWhereClause' => array('(' . $table . '.sys_language_uid IN (0,-1))'));
-               $this->assertSame($expectedSql, $sql);
-       }
-
-       /**
-        * @test
-        */
-       public function addSysLanguageStatementWorksForForeignLanguageWithoutSubselection() {
-               $table = uniqid('tx_coretest_table');
-               $GLOBALS['TCA'][$table]['ctrl'] = array(
-                       'languageField' => 'sys_language_uid'
-               );
-               $sql = array();
-               $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
-               $querySettings->setLanguageUid(2);
-               $mockTypo3DbBackend = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend', array('dummy'), array(), '', FALSE);
-               $mockTypo3DbBackend->_callRef('addSysLanguageStatement', $table, $sql, $querySettings);
-               $expectedSql = array('additionalWhereClause' => array('(' . $table . '.sys_language_uid IN (2,-1))'));
-               $this->assertSame($expectedSql, $sql);
-       }
-
-       /**
-        * @test
-        */
-       public function addSysLanguageStatementWorksForForeignLanguageWithSubselectionWithoutDeleteStatementReturned() {
-               $table = uniqid('tx_coretest_table');
-               $GLOBALS['TCA'][$table]['ctrl'] = array(
-                       'languageField' => 'sys_language_uid',
-                       'transOrigPointerField' => 'l10n_parent'
-               );
-               $sql = array();
-               $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
-               $querySettings->setLanguageUid(2);
-               $mockTypo3DbBackend = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend', array('dummy'), array(), '', FALSE);
-               $mockTypo3DbBackend->_callRef('addSysLanguageStatement', $table, $sql, $querySettings);
-               $expectedSql = array('additionalWhereClause' => array('(' . $table . '.sys_language_uid IN (2,-1) OR (' . $table . '.sys_language_uid=0 AND ' . $table . '.uid NOT IN (SELECT ' . $table . '.l10n_parent FROM ' . $table . ' WHERE ' . $table . '.l10n_parent>0 AND ' . $table . '.sys_language_uid>0)))'));
-               $this->assertSame($expectedSql, $sql);
-       }
-
-       /**
-        * @test
-        */
-       public function addSysLanguageStatementWorksForForeignLanguageWithSubselectionTakesDeleteStatementIntoAccountIfNecessary() {
-               $table = uniqid('tx_coretest_table');
-               $GLOBALS['TCA'][$table]['ctrl'] = array(
-                       'languageField' => 'sys_language_uid',
-                       'transOrigPointerField' => 'l10n_parent',
-                       'delete' => 'deleted'
-               );
-               $sql = array();
-               $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
-               $querySettings->setLanguageUid(2);
-               $mockTypo3DbBackend = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend', array('dummy'), array(), '', FALSE);
-               $mockTypo3DbBackend->_callRef('addSysLanguageStatement', $table, $sql, $querySettings);
-               $expectedSql = array('additionalWhereClause' => array(
-                       '(' . $table . '.sys_language_uid IN (2,-1)' .
-                               ' OR (' . $table . '.sys_language_uid=0 AND ' . $table . '.uid NOT IN (' .
-                               'SELECT ' . $table . '.l10n_parent FROM ' . $table .
-                               ' WHERE ' . $table . '.l10n_parent>0 AND ' .
-                               $table . '.sys_language_uid>0 AND ' .
-                               $table . '.deleted=0)))')
-               );
-               $this->assertSame($expectedSql, $sql);
-       }
-
-       /**
-        * @test
-        */
-       public function addSysLanguageStatementWorksInBackendContextWithSubselectionTakesDeleteStatementIntoAccountIfNecessary() {
-               $table = uniqid('tx_coretest_table');
-               $table = 'tt_content';
-               $GLOBALS['TCA'][$table]['ctrl'] = array(
-                       'languageField' => 'sys_language_uid',
-                       'transOrigPointerField' => 'l10n_parent',
-                       'delete' => 'deleted'
-               );
-               $sql = array();
-               $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
-               $querySettings->setLanguageUid(2);
-               $mockTypo3DbBackend = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend', array('dummy'), array(), '', FALSE);
-               $mockTypo3DbBackend->_callRef('addSysLanguageStatement', $table, $sql, $querySettings);
-               $expectedSql = array('additionalWhereClause' => array(
-                       '(' . $table . '.sys_language_uid IN (2,-1)' .
-                               ' OR (' . $table . '.sys_language_uid=0 AND ' . $table . '.uid NOT IN (' .
-                               'SELECT ' . $table . '.l10n_parent FROM ' . $table .
-                               ' WHERE ' . $table . '.l10n_parent>0 AND ' .
-                               $table . '.sys_language_uid>0 AND ' .
-                               $table . '.deleted=0)))')
-               );
-               $this->assertSame($expectedSql, $sql);
-       }
-
-       /**
-        * @test
-        */
-       public function orderStatementGenerationWorks() {
-               $mockSource = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Qom\\Selector', array('getNodeTypeName'), array(), '', FALSE);
-               $mockSource->expects($this->any())->method('getNodeTypeName')->will($this->returnValue('Tx_MyExt_ClassName'));
-               $mockDataMapper = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Mapper\\DataMapper', array('convertPropertyNameToColumnName', 'convertClassNameToTableName'), array(), '', FALSE);
-               $mockDataMapper->expects($this->once())->method('convertClassNameToTableName')->with('Tx_MyExt_ClassName')->will($this->returnValue('tx_myext_tablename'));
-               $mockDataMapper->expects($this->once())->method('convertPropertyNameToColumnName')->with('fooProperty', 'Tx_MyExt_ClassName')->will($this->returnValue('converted_fieldname'));
-               $sql = array();
-               $orderings = array('fooProperty' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING);
-               $mockTypo3DbBackend = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend', array('parserOrderings'), array(), '', FALSE);
-               $mockTypo3DbBackend->_set('dataMapper', $mockDataMapper);
-               $mockTypo3DbBackend->_callRef('parseOrderings', $orderings, $mockSource, $sql);
-               $expectedSql = array('orderings' => array('tx_myext_tablename.converted_fieldname ASC'));
-               $this->assertSame($expectedSql, $sql);
-       }
-
-       /**
-        * @test
-        * @expectedException \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedOrderException
-        */
-       public function orderStatementGenerationThrowsExceptionOnUnsupportedOrder() {
-               $mockSource = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Qom\\Selector', array('getNodeTypeName'), array(), '', FALSE);
-               $mockSource->expects($this->never())->method('getNodeTypeName');
-               $mockDataMapper = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Mapper\\DataMapper', array('convertPropertyNameToColumnName', 'convertClassNameToTableName'), array(), '', FALSE);
-               $mockDataMapper->expects($this->never())->method('convertClassNameToTableName');
-               $mockDataMapper->expects($this->never())->method('convertPropertyNameToColumnName');
-               $sql = array();
-               $orderings = array('fooProperty' => 'unsupported_order');
-               $mockTypo3DbBackend = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend', array('parserOrderings'), array(), '', FALSE);
-               $mockTypo3DbBackend->_set('dataMapper', $mockDataMapper);
-               $mockTypo3DbBackend->_callRef('parseOrderings', $orderings, $mockSource, $sql);
-       }
-
-       /**
-        * @test
-        */
-       public function orderStatementGenerationWorksWithMultipleOrderings() {
-               $mockSource = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Qom\\Selector', array('getNodeTypeName'), array(), '', FALSE);
-               $mockSource->expects($this->any())->method('getNodeTypeName')->will($this->returnValue('Tx_MyExt_ClassName'));
-               $mockDataMapper = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Mapper\\DataMapper', array('convertPropertyNameToColumnName', 'convertClassNameToTableName'), array(), '', FALSE);
-               $mockDataMapper->expects($this->any())->method('convertClassNameToTableName')->with('Tx_MyExt_ClassName')->will($this->returnValue('tx_myext_tablename'));
-               $mockDataMapper->expects($this->any())->method('convertPropertyNameToColumnName')->will($this->returnValue('converted_fieldname'));
-               $sql = array();
-               $orderings = array(
-                       'fooProperty' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING,
-                       'barProperty' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING
-               );
-               $mockTypo3DbBackend = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend', array('parserOrderings'), array(), '', FALSE);
-               $mockTypo3DbBackend->_set('dataMapper', $mockDataMapper);
-               $mockTypo3DbBackend->_callRef('parseOrderings', $orderings, $mockSource, $sql);
-               $expectedSql = array('orderings' => array('tx_myext_tablename.converted_fieldname ASC', 'tx_myext_tablename.converted_fieldname DESC'));
-               $this->assertSame($expectedSql, $sql);
-       }
-
-       public function providerForVisibilityConstraintStatement() {
-               return array(
-                       'in be: include all' => array('BE', TRUE, array(), TRUE, NULL),
-                       'in be: ignore enable fields but do not include deleted' => array('BE', TRUE, array(), FALSE, array('tx_foo_table.deleted_column=0')),
-                       'in be: respect enable fields but include deleted' => array('BE', FALSE, array(), TRUE, array('tx_foo_table.disabled_column=0 AND (tx_foo_table.starttime_column<=123456789)')),
-                       'in be: respect enable fields and do not include deleted' => array('BE', FALSE, array(), FALSE, array('tx_foo_table.disabled_column=0 AND (tx_foo_table.starttime_column<=123456789) AND tx_foo_table.deleted_column=0')),
-                       'in fe: include all' => array('FE', TRUE, array(), TRUE, NULL),
-                       'in fe: ignore enable fields but do not include deleted' => array('FE', TRUE, array(), FALSE, array('tx_foo_table.deleted_column=0')),
-                       'in fe: ignore only starttime and do not include deleted' => array('FE', TRUE, array('starttime'), FALSE, array('tx_foo_table.deleted_column=0 AND tx_foo_table.disabled_column=0')),
-                       'in fe: respect enable fields and do not include deleted' => array('FE', FALSE, array(), FALSE, array('tx_foo_table.deleted_column=0 AND tx_foo_table.disabled_column=0 AND tx_foo_table.starttime_column<=123456789'))
-               );
-       }
-
-       /**
-        * @test
-        * @dataProvider providerForVisibilityConstraintStatement
-        */
-       public function visibilityConstraintStatementIsGeneratedAccordingToTheQuerySettings($mode, $ignoreEnableFields, $enableFieldsToBeIgnored, $deletedValue, $expectedSql) {
-               $tableName = 'tx_foo_table';
-               $GLOBALS['TCA'][$tableName]['ctrl'] = array(
-                       'enablecolumns' => array(
-                               'disabled' => 'disabled_column',
-                               'starttime' => 'starttime_column'
-                       ),
-                       'delete' => 'deleted_column'
-               );
-               $GLOBALS['TSFE'] = new \stdClass();
-               $GLOBALS['TSFE']->sys_page = new \TYPO3\CMS\Frontend\Page\PageRepository();
-               $GLOBALS['SIM_ACCESS_TIME'] = 123456789;
-               $mockQuerySettings = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Typo3QuerySettings', array('getIgnoreEnableFields', 'getEnableFieldsToBeIgnored', 'getIncludeDeleted'), array(), '', FALSE);
-               $mockQuerySettings->expects($this->once())->method('getIgnoreEnableFields')->will($this->returnValue($ignoreEnableFields));
-               $mockQuerySettings->expects($this->once())->method('getEnableFieldsToBeIgnored')->will($this->returnValue($enableFieldsToBeIgnored));
-               $mockQuerySettings->expects($this->once())->method('getIncludeDeleted')->will($this->returnValue($deletedValue));
-               $sql = array();
-
-               /** @var $mockEnvironmentService \TYPO3\CMS\Extbase\Service\EnvironmentService | \PHPUnit_Framework_MockObject_MockObject */
-               $mockEnvironmentService = $this->getMock('TYPO3\\CMS\\Extbase\\Service\\EnvironmentService', array('isEnvironmentInFrontendMode'));
-               $mockEnvironmentService->expects($this->any())->method('isEnvironmentInFrontendMode')->will($this->returnValue($mode == 'FE'));
-
-               /** @var $mockTypo3DbBackend \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbBackend | \PHPUnit_Framework_MockObject_MockObject */
-               $mockTypo3DbBackend = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend', array('dummy'), array(), '', FALSE);
-               $mockTypo3DbBackend->_set('environmentService', $mockEnvironmentService);
-               $mockTypo3DbBackend->_callRef('addVisibilityConstraintStatement', $mockQuerySettings, $tableName, $sql);
-               $this->assertSame($expectedSql, $sql['additionalWhereClause']);
-               unset($GLOBALS['TCA'][$tableName]);
-       }
-
-       public function providerForRespectEnableFields() {
-               return array(
-                       'in be: respectEnableFields=false' => array('BE', FALSE, NULL),
-                       'in be: respectEnableFields=true' => array('BE', TRUE, array('tx_foo_table.disabled_column=0 AND (tx_foo_table.starttime_column<=123456789) AND tx_foo_table.deleted_column=0')),
-                       'in be: respectEnableFields=false' => array('FE', FALSE, NULL),
-                       'in be: respectEnableFields=true' => array('FE', TRUE, array('tx_foo_table.deleted_column=0 AND tx_foo_table.disabled_column=0 AND tx_foo_table.starttime_column<=123456789'))
-               );
-       }
-
-       /**
-        * @test
-        * @dataProvider providerForRespectEnableFields
-        */
-       public function respectEnableFieldsSettingGeneratesCorrectStatement($mode, $respectEnableFields, $expectedSql) {
-               $tableName = 'tx_foo_table';
-               $GLOBALS['TCA'][$tableName]['ctrl'] = array(
-                       'enablecolumns' => array(
-                               'disabled' => 'disabled_column',
-                               'starttime' => 'starttime_column'
-                       ),
-                       'delete' => 'deleted_column'
-               );
-               $GLOBALS['TSFE'] = new \stdClass();
-               $GLOBALS['TSFE']->sys_page = new \TYPO3\CMS\Frontend\Page\PageRepository();
-               $GLOBALS['SIM_ACCESS_TIME'] = 123456789;
-               $mockQuerySettings = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Typo3QuerySettings', array('dummy'), array(), '', FALSE);
-               $mockQuerySettings->setRespectEnableFields($respectEnableFields);
-               $sql = array();
-
-               /** @var $mockEnvironmentService \TYPO3\CMS\Extbase\Service\EnvironmentService | \PHPUnit_Framework_MockObject_MockObject */
-               $mockEnvironmentService = $this->getMock('TYPO3\\CMS\\Extbase\\Service\\EnvironmentService', array('isEnvironmentInFrontendMode'));
-               $mockEnvironmentService->expects($this->any())->method('isEnvironmentInFrontendMode')->will($this->returnValue($mode == 'FE'));
-
-               /** @var $mockTypo3DbBackend \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbBackend | \PHPUnit_Framework_MockObject_MockObject */
-               $mockTypo3DbBackend = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend', array('dummy'), array(), '', FALSE);
-               $mockTypo3DbBackend->_set('environmentService', $mockEnvironmentService);
-               $mockTypo3DbBackend->_callRef('addVisibilityConstraintStatement', $mockQuerySettings, $tableName, $sql);
-               $this->assertSame($expectedSql, $sql['additionalWhereClause']);
-               unset($GLOBALS['TCA'][$tableName]);
-       }
-
-       /**
-        * @test
-        * @expectedException \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException
-        */
-       public function visibilityConstraintStatementGenerationThrowsExceptionIfTheQuerySettingsAreInconsistent() {
-               $tableName = 'tx_foo_table';
-               $GLOBALS['TCA'][$tableName]['ctrl'] = array(
-                       'enablecolumns' => array(
-                               'disabled' => 'disabled_column'
-                       ),
-                       'delete' => 'deleted_column'
-               );
-               $mockQuerySettings = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Typo3QuerySettings', array('getIgnoreEnableFields', 'getEnableFieldsToBeIgnored', 'getIncludeDeleted'), array(), '', FALSE);
-               $mockQuerySettings->expects($this->once())->method('getIgnoreEnableFields')->will($this->returnValue(FALSE));
-               $mockQuerySettings->expects($this->once())->method('getEnableFieldsToBeIgnored')->will($this->returnValue(array()));
-               $mockQuerySettings->expects($this->once())->method('getIncludeDeleted')->will($this->returnValue(TRUE));
-               $sql = array();
-
-               /** @var $mockEnvironmentService \TYPO3\CMS\Extbase\Service\EnvironmentService | \PHPUnit_Framework_MockObject_MockObject */
-               $mockEnvironmentService = $this->getMock('TYPO3\\CMS\\Extbase\\Service\\EnvironmentService', array('isEnvironmentInFrontendMode'));
-               $mockEnvironmentService->expects($this->any())->method('isEnvironmentInFrontendMode')->will($this->returnValue(TRUE));
-
-               /** @var $mockTypo3DbBackend \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbBackend | \PHPUnit_Framework_MockObject_MockObject */
-               $mockTypo3DbBackend = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend', array('dummy'), array(), '', FALSE);
-               $mockTypo3DbBackend->_set('environmentService', $mockEnvironmentService);
-               $mockTypo3DbBackend->_callRef('addVisibilityConstraintStatement', $mockQuerySettings, $tableName, $sql);
-               unset($GLOBALS['TCA'][$tableName]);
-       }
-
-       /**
         * @test
         */
        public function uidOfAlreadyPersistedValueObjectIsDeterminedCorrectly() {
@@ -455,172 +95,4 @@ class Typo3DbBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                $mockTypo3DbBackend->_set('pageRepository', $pageRepositoryMock);
                $this->assertSame(array($comparisonRow), $mockTypo3DbBackend->_call('doLanguageAndWorkspaceOverlay', $sourceMock, array($row), $mockQuerySettings, $workspaceUid));
        }
-
-       /**
-        * DataProvider for addPageIdStatement Tests
-        */
-       public function providerForAddPageIdStatementData() {
-               $table = uniqid('tx_coretest_table');
-               return array(
-                       'set Pid to zero if rootLevel = 1' => array(
-                               '1',
-                               $table,
-                               array('additionalWhereClause' => array($table . '.pid = 0'))
-                       ),
-                       'set Pid to given Pids if rootLevel = 0' => array(
-                               '0',
-                               $table,
-                               array('additionalWhereClause' => array($table . '.pid IN (42, 27)'))
-                       ),
-                       'set no statement if rootLevel = -1' => array(
-                               '-1',
-                               $table,
-                               array()
-                       )
-               );
-       }
-
-       /**
-        * @test
-        * @dataProvider providerForAddPageIdStatementData
-        */
-       public function addPageIdStatementSetsPidToZeroIfTableDeclaresRootlevel($rootLevel, $table, $expectedSql) {
-
-               $GLOBALS['TCA'][$table]['ctrl'] = array(
-                       'rootLevel' => $rootLevel
-               );
-               $sql = array();
-               $storagePageIds = array(42,27);
-               $mockTypo3DbBackend = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend', array('dummy'), array(), '', FALSE);
-               $mockFrontendVariableCache = $this->getMock('TYPO3\\CMS\\Core\\Cache\\Frontend\\VariableFrontend', array(), array(), '', FALSE);
-               $mockTypo3DbBackend->_set('tableColumnCache', $mockFrontendVariableCache);
-               $mockFrontendVariableCache->expects($this->once())->method('get')->will($this->returnValue(array('pid' => '42')));
-               $mockTypo3DbBackend->_callRef('addPageIdStatement', $table, $sql, $storagePageIds);
-
-               $this->assertSame($expectedSql, $sql);
-       }
-
-       /**
-        * @test
-        * @expectedException \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException
-        */
-       public function getPlainValueThrowsExceptionIfInputIsArray() {
-               $mockTypo3DbBackend = $this->getAccessibleMock(
-                       'TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend',
-                       array('dummy'),
-                       array(),
-                       '',
-                       FALSE
-               );
-               $mockTypo3DbBackend->_call('getPlainValue', array());
-       }
-
-       /**
-        * @test
-        */
-       public function getPlainValueReturnsTimestampIfDateTimeObjectIsGiven() {
-               $mockTypo3DbBackend = $this->getAccessibleMock(
-                       'TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend',
-                       array('dummy'),
-                       array(),
-                       '',
-                       FALSE
-               );
-               $input = new \DateTime('@1365866253');
-               $this->assertSame('1365866253', $mockTypo3DbBackend->_call('getPlainValue', $input));
-       }
-
-       /**
-        * @test
-        */
-       public function getPlainValueReturnsIntegerOneIfValueIsBooleanTrue() {
-               $mockTypo3DbBackend = $this->getAccessibleMock(
-                       'TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend',
-                       array('dummy'),
-                       array(),
-                       '',
-                       FALSE
-               );
-               $this->assertSame(1, $mockTypo3DbBackend->_call('getPlainValue', TRUE));
-       }
-
-       /**
-        * @test
-        */
-       public function getPlainValueReturnsIntegerZeroIfValueIsBooleanFalse() {
-               $mockTypo3DbBackend = $this->getAccessibleMock(
-                       'TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend',
-                       array('dummy'),
-                       array(),
-                       '',
-                       FALSE
-               );
-               $this->assertSame(0, $mockTypo3DbBackend->_call('getPlainValue', FALSE));
-       }
-
-       /**
-        * @test
-        * @expectedException \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException
-        */
-       public function getPlainValueCallsGetRealInstanceOnInputIfInputIsInstanceOfLazyLoadingProxy() {
-               $mockTypo3DbBackend = $this->getAccessibleMock(
-                       'TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend',
-                       array('dummy'),
-                       array(),
-                       '',
-                       FALSE
-               );
-               $input = $this->getMock(
-                       'TYPO3\\CMS\\Extbase\\Persistence\\Generic\\LazyLoadingProxy',
-                       array(),
-                       array(),
-                       '',
-                       FALSE
-               );
-               $input
-                       ->expects($this->once())
-                       ->method('_loadRealInstance');
-               $mockTypo3DbBackend->_call('getPlainValue', $input);
-       }
-
-       /**
-        * @test
-        */
-       public function getPlainValueCallsGetUidOnDomainObjectInterfaceInput() {
-               $mockTypo3DbBackend = $this->getAccessibleMock(
-                       'TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend',
-                       array('dummy'),
-                       array(),
-                       '',
-                       FALSE
-               );
-               $input = $this->getMock(
-                       'TYPO3\\CMS\\Extbase\\DomainObject\\DomainObjectInterface',
-                       array(),
-                       array(),
-                       '',
-                       FALSE
-               );
-               $input
-                       ->expects($this->once())
-                       ->method('getUid')
-                       ->will($this->returnValue(23));
-               $this->assertSame(23, $mockTypo3DbBackend->_call('getPlainValue', $input));
-       }
-
-       /**
-        * @test
-        */
-       public function getPlainValueReturnsSimpleType() {
-               $mockTypo3DbBackend = $this->getAccessibleMock(
-                       'TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbBackend',
-                       array('dummy'),
-                       array(),
-                       '',
-                       FALSE
-               );
-               $value = uniqid('foo_');
-               $this->assertSame($value, $mockTypo3DbBackend->_call('getPlainValue', $value));
-       }
-
 }
diff --git a/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Storage/Typo3DbQueryParserTest.php b/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Storage/Typo3DbQueryParserTest.php
new file mode 100644 (file)
index 0000000..a5114c2
--- /dev/null
@@ -0,0 +1,525 @@
+<?php
+namespace TYPO3\CMS\Extbase\Tests\Unit\Persistence\Generic\Storage;
+
+/***************************************************************
+ *  Copyright notice
+ *
+ *  This class is a backport of the corresponding class of TYPO3 Flow.
+ *  All credits go to the TYPO3 Flow team.
+ *  All rights reserved
+ *
+ *  This script is part of the TYPO3 project. The TYPO3 project is
+ *  free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The GNU General Public License can be found at
+ *  http://www.gnu.org/copyleft/gpl.html.
+ *  A copy is found in the text file GPL.txt and important notices to the license
+ *  from the author is found in LICENSE.txt distributed with these scripts.
+ *
+ *
+ *  This script is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  This copyright notice MUST APPEAR in all copies of the script!
+ ***************************************************************/
+class Typo3DbQueryParserTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+       /**
+        * @test
+        */
+       public function addSysLanguageStatementWorksForDefaultLanguage() {
+               $table = uniqid('tx_coretest_table');
+               $GLOBALS['TCA'][$table]['ctrl'] = array(
+                       'languageField' => 'sys_language_uid'
+               );
+               /** @var \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings|\PHPUnit_Framework_MockObject_MockObject $querySettings */
+               $querySettings = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Typo3QuerySettings');
+               $sql = array();
+               $mockTypo3DbQueryParser = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser', array('dummy'), array(), '', FALSE);
+               $mockTypo3DbQueryParser->_callRef('addSysLanguageStatement', $table, $sql, $querySettings);
+               $expectedSql = array('additionalWhereClause' => array('(' . $table . '.sys_language_uid IN (0,-1))'));
+               $this->assertSame($expectedSql, $sql);
+       }
+
+       /**
+        * @test
+        */
+       public function addSysLanguageStatementWorksForNonDefaultLanguage() {
+               $table = uniqid('tx_coretest_table');
+               $GLOBALS['TCA'][$table]['ctrl'] = array(
+                       'languageField' => 'sys_language_uid'
+               );
+               $sql = array();
+               /** @var \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings|\PHPUnit_Framework_MockObject_MockObject $querySettings */
+               $querySettings = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Typo3QuerySettings', array('dummy'));
+               $querySettings->setLanguageUid('1');
+               $mockTypo3DbQueryParser = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser', array('dummy'), array(), '', FALSE);
+               $mockTypo3DbQueryParser->_callRef('addSysLanguageStatement', $table, $sql, $querySettings);
+               $result = array('additionalWhereClause' => array('(' . $table . '.sys_language_uid IN (1,-1))'));
+               $this->assertSame($result, $sql);
+       }
+
+       /**
+        * @test
+        */
+       public function addSysLanguageStatementWorksInBackendContextWithNoGlobalTypoScriptFrontendControllerAvailable() {
+               $table = uniqid('tx_coretest_table');
+               $GLOBALS['TCA'][$table]['ctrl'] = array(
+                       'languageField' => 'sys_language_uid'
+               );
+               $sql = array();
+               $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
+               $mockTypo3DbQueryParser = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser', array('dummy'), array(), '', FALSE);
+               $mockTypo3DbQueryParser->_callRef('addSysLanguageStatement', $table, $sql, $querySettings);
+               $expectedSql = array('additionalWhereClause' => array('(' . $table . '.sys_language_uid IN (0,-1))'));
+               $this->assertSame($expectedSql, $sql);
+       }
+
+       /**
+        * @test
+        */
+       public function addSysLanguageStatementWorksForDefaultLanguageWithoutDeleteStatementReturned() {
+               $table = uniqid('tx_coretest_table');
+               $GLOBALS['TCA'][$table]['ctrl'] = array(
+                       'languageField' => 'sys_language_uid',
+                       'delete' => 'deleted'
+               );
+               $sql = array();
+               $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
+               $querySettings->setLanguageUid(0);
+               $mockTypo3DbQueryParser = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser', array('dummy'), array(), '', FALSE);
+               $mockTypo3DbQueryParser->_callRef('addSysLanguageStatement', $table, $sql, $querySettings);
+               $expectedSql = array('additionalWhereClause' => array('(' . $table . '.sys_language_uid IN (0,-1))'));
+               $this->assertSame($expectedSql, $sql);
+       }
+
+       /**
+        * @test
+        */
+       public function addSysLanguageStatementWorksForForeignLanguageWithoutSubselection() {
+               $table = uniqid('tx_coretest_table');
+               $GLOBALS['TCA'][$table]['ctrl'] = array(
+                       'languageField' => 'sys_language_uid'
+               );
+               $sql = array();
+               $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
+               $querySettings->setLanguageUid(2);
+               $mockTypo3DbQueryParser = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser', array('dummy'), array(), '', FALSE);
+               $mockTypo3DbQueryParser->_callRef('addSysLanguageStatement', $table, $sql, $querySettings);
+               $expectedSql = array('additionalWhereClause' => array('(' . $table . '.sys_language_uid IN (2,-1))'));
+               $this->assertSame($expectedSql, $sql);
+       }
+
+       /**
+        * @test
+        */
+       public function addSysLanguageStatementWorksForForeignLanguageWithSubselectionWithoutDeleteStatementReturned() {
+               $table = uniqid('tx_coretest_table');
+               $GLOBALS['TCA'][$table]['ctrl'] = array(
+                       'languageField' => 'sys_language_uid',
+                       'transOrigPointerField' => 'l10n_parent'
+               );
+               $sql = array();
+               $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
+               $querySettings->setLanguageUid(2);
+               $mockTypo3DbQueryParser = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser', array('dummy'), array(), '', FALSE);
+               $mockTypo3DbQueryParser->_callRef('addSysLanguageStatement', $table, $sql, $querySettings);
+               $expectedSql = array('additionalWhereClause' => array('(' . $table . '.sys_language_uid IN (2,-1) OR (' . $table . '.sys_language_uid=0 AND ' . $table . '.uid NOT IN (SELECT ' . $table . '.l10n_parent FROM ' . $table . ' WHERE ' . $table . '.l10n_parent>0 AND ' . $table . '.sys_language_uid>0)))'));
+               $this->assertSame($expectedSql, $sql);
+       }
+
+       /**
+        * @test
+        */
+       public function addSysLanguageStatementWorksForForeignLanguageWithSubselectionTakesDeleteStatementIntoAccountIfNecessary() {
+               $table = uniqid('tx_coretest_table');
+               $GLOBALS['TCA'][$table]['ctrl'] = array(
+                       'languageField' => 'sys_language_uid',
+                       'transOrigPointerField' => 'l10n_parent',
+                       'delete' => 'deleted'
+               );
+               $sql = array();
+               $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
+               $querySettings->setLanguageUid(2);
+               $mockTypo3DbQueryParser = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser', array('dummy'), array(), '', FALSE);
+               $mockTypo3DbQueryParser->_callRef('addSysLanguageStatement', $table, $sql, $querySettings);
+               $expectedSql = array('additionalWhereClause' => array(
+                       '(' . $table . '.sys_language_uid IN (2,-1)' .
+                               ' OR (' . $table . '.sys_language_uid=0 AND ' . $table . '.uid NOT IN (' .
+                               'SELECT ' . $table . '.l10n_parent FROM ' . $table .
+                               ' WHERE ' . $table . '.l10n_parent>0 AND ' .
+                               $table . '.sys_language_uid>0 AND ' .
+                               $table . '.deleted=0)))')
+               );
+               $this->assertSame($expectedSql, $sql);
+       }
+
+       /**
+        * @test
+        */
+       public function addSysLanguageStatementWorksInBackendContextWithSubselectionTakesDeleteStatementIntoAccountIfNecessary() {
+               $table = uniqid('tx_coretest_table');
+               $table = 'tt_content';
+               $GLOBALS['TCA'][$table]['ctrl'] = array(
+                       'languageField' => 'sys_language_uid',
+                       'transOrigPointerField' => 'l10n_parent',
+                       'delete' => 'deleted'
+               );
+               $sql = array();
+               $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
+               $querySettings->setLanguageUid(2);
+               $mockTypo3DbQueryParser = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser', array('dummy'), array(), '', FALSE);
+               $mockTypo3DbQueryParser->_callRef('addSysLanguageStatement', $table, $sql, $querySettings);
+               $expectedSql = array('additionalWhereClause' => array(
+                       '(' . $table . '.sys_language_uid IN (2,-1)' .
+                               ' OR (' . $table . '.sys_language_uid=0 AND ' . $table . '.uid NOT IN (' .
+                               'SELECT ' . $table . '.l10n_parent FROM ' . $table .
+                               ' WHERE ' . $table . '.l10n_parent>0 AND ' .
+                               $table . '.sys_language_uid>0 AND ' .
+                               $table . '.deleted=0)))')
+               );
+               $this->assertSame($expectedSql, $sql);
+       }
+
+       /**
+        * @test
+        */
+       public function orderStatementGenerationWorks() {
+               $mockSource = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Qom\\Selector', array('getNodeTypeName'), array(), '', FALSE);
+               $mockSource->expects($this->any())->method('getNodeTypeName')->will($this->returnValue('Tx_MyExt_ClassName'));
+               $mockDataMapper = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Mapper\\DataMapper', array('convertPropertyNameToColumnName', 'convertClassNameToTableName'), array(), '', FALSE);
+               $mockDataMapper->expects($this->once())->method('convertClassNameToTableName')->with('Tx_MyExt_ClassName')->will($this->returnValue('tx_myext_tablename'));
+               $mockDataMapper->expects($this->once())->method('convertPropertyNameToColumnName')->with('fooProperty', 'Tx_MyExt_ClassName')->will($this->returnValue('converted_fieldname'));
+               $sql = array();
+               $orderings = array('fooProperty' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING);
+               $mockTypo3DbQueryParser = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser', array('dummy'), array(), '', FALSE);
+               $mockTypo3DbQueryParser->_set('dataMapper', $mockDataMapper);
+               $mockTypo3DbQueryParser->_callRef('parseOrderings', $orderings, $mockSource, $sql);
+               $expectedSql = array('orderings' => array('tx_myext_tablename.converted_fieldname ASC'));
+               $this->assertSame($expectedSql, $sql);
+       }
+
+       /**
+        * @test
+        * @expectedException \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedOrderException
+        */
+       public function orderStatementGenerationThrowsExceptionOnUnsupportedOrder() {
+               $mockSource = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Qom\\Selector', array('getNodeTypeName'), array(), '', FALSE);
+               $mockSource->expects($this->never())->method('getNodeTypeName');
+               $mockDataMapper = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Mapper\\DataMapper', array('convertPropertyNameToColumnName', 'convertClassNameToTableName'), array(), '', FALSE);
+               $mockDataMapper->expects($this->never())->method('convertClassNameToTableName');
+               $mockDataMapper->expects($this->never())->method('convertPropertyNameToColumnName');
+               $sql = array();
+               $orderings = array('fooProperty' => 'unsupported_order');
+               $mockTypo3DbQueryParser = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser', array('dummy'), array(), '', FALSE);
+               $mockTypo3DbQueryParser->_set('dataMapper', $mockDataMapper);
+               $mockTypo3DbQueryParser->_callRef('parseOrderings', $orderings, $mockSource, $sql);
+       }
+
+       /**
+        * @test
+        */
+       public function orderStatementGenerationWorksWithMultipleOrderings() {
+               $mockSource = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Qom\\Selector', array('getNodeTypeName'), array(), '', FALSE);
+               $mockSource->expects($this->any())->method('getNodeTypeName')->will($this->returnValue('Tx_MyExt_ClassName'));
+               $mockDataMapper = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Mapper\\DataMapper', array('convertPropertyNameToColumnName', 'convertClassNameToTableName'), array(), '', FALSE);
+               $mockDataMapper->expects($this->any())->method('convertClassNameToTableName')->with('Tx_MyExt_ClassName')->will($this->returnValue('tx_myext_tablename'));
+               $mockDataMapper->expects($this->any())->method('convertPropertyNameToColumnName')->will($this->returnValue('converted_fieldname'));
+               $sql = array();
+               $orderings = array(
+                       'fooProperty' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING,
+                       'barProperty' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING
+               );
+               $mockTypo3DbQueryParser = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser', array('dummy'), array(), '', FALSE);
+               $mockTypo3DbQueryParser->_set('dataMapper', $mockDataMapper);
+               $mockTypo3DbQueryParser->_callRef('parseOrderings', $orderings, $mockSource, $sql);
+               $expectedSql = array('orderings' => array('tx_myext_tablename.converted_fieldname ASC', 'tx_myext_tablename.converted_fieldname DESC'));
+               $this->assertSame($expectedSql, $sql);
+       }
+
+       public function providerForVisibilityConstraintStatement() {
+               return array(
+                       'in be: include all' => array('BE', TRUE, array(), TRUE, NULL),
+                       'in be: ignore enable fields but do not include deleted' => array('BE', TRUE, array(), FALSE, array('tx_foo_table.deleted_column=0')),
+                       'in be: respect enable fields but include deleted' => array('BE', FALSE, array(), TRUE, array('tx_foo_table.disabled_column=0 AND (tx_foo_table.starttime_column<=123456789)')),
+                       'in be: respect enable fields and do not include deleted' => array('BE', FALSE, array(), FALSE, array('tx_foo_table.disabled_column=0 AND (tx_foo_table.starttime_column<=123456789) AND tx_foo_table.deleted_column=0')),
+                       'in fe: include all' => array('FE', TRUE, array(), TRUE, NULL),
+                       'in fe: ignore enable fields but do not include deleted' => array('FE', TRUE, array(), FALSE, array('tx_foo_table.deleted_column=0')),
+                       'in fe: ignore only starttime and do not include deleted' => array('FE', TRUE, array('starttime'), FALSE, array('tx_foo_table.deleted_column=0 AND tx_foo_table.disabled_column=0')),
+                       'in fe: respect enable fields and do not include deleted' => array('FE', FALSE, array(), FALSE, array('tx_foo_table.deleted_column=0 AND tx_foo_table.disabled_column=0 AND tx_foo_table.starttime_column<=123456789'))
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider providerForVisibilityConstraintStatement
+        */
+       public function visibilityConstraintStatementIsGeneratedAccordingToTheQuerySettings($mode, $ignoreEnableFields, $enableFieldsToBeIgnored, $deletedValue, $expectedSql) {
+               $tableName = 'tx_foo_table';
+               $GLOBALS['TCA'][$tableName]['ctrl'] = array(
+                       'enablecolumns' => array(
+                               'disabled' => 'disabled_column',
+                               'starttime' => 'starttime_column'
+                       ),
+                       'delete' => 'deleted_column'
+               );
+               $GLOBALS['TSFE'] = new \stdClass();
+               $GLOBALS['TSFE']->sys_page = new \TYPO3\CMS\Frontend\Page\PageRepository();
+               $GLOBALS['SIM_ACCESS_TIME'] = 123456789;
+               $mockQuerySettings = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Typo3QuerySettings', array('getIgnoreEnableFields', 'getEnableFieldsToBeIgnored', 'getIncludeDeleted'), array(), '', FALSE);
+               $mockQuerySettings->expects($this->once())->method('getIgnoreEnableFields')->will($this->returnValue($ignoreEnableFields));
+               $mockQuerySettings->expects($this->once())->method('getEnableFieldsToBeIgnored')->will($this->returnValue($enableFieldsToBeIgnored));
+               $mockQuerySettings->expects($this->once())->method('getIncludeDeleted')->will($this->returnValue($deletedValue));
+               $sql = array();
+
+               /** @var $mockEnvironmentService \TYPO3\CMS\Extbase\Service\EnvironmentService | \PHPUnit_Framework_MockObject_MockObject */
+               $mockEnvironmentService = $this->getMock('TYPO3\\CMS\\Extbase\\Service\\EnvironmentService', array('isEnvironmentInFrontendMode'));
+               $mockEnvironmentService->expects($this->any())->method('isEnvironmentInFrontendMode')->will($this->returnValue($mode == 'FE'));
+
+               $mockTypo3DbQueryParser = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser', array('dummy'), array(), '', FALSE);
+               $mockTypo3DbQueryParser->_set('environmentService', $mockEnvironmentService);
+               $mockTypo3DbQueryParser->_callRef('addVisibilityConstraintStatement', $mockQuerySettings, $tableName, $sql);
+               $this->assertSame($expectedSql, $sql['additionalWhereClause']);
+               unset($GLOBALS['TCA'][$tableName]);
+       }
+
+       public function providerForRespectEnableFields() {
+               return array(
+                       'in be: respectEnableFields=false' => array('BE', FALSE, NULL),
+                       'in be: respectEnableFields=true' => array('BE', TRUE, array('tx_foo_table.disabled_column=0 AND (tx_foo_table.starttime_column<=123456789) AND tx_foo_table.deleted_column=0')),
+                       'in be: respectEnableFields=false' => array('FE', FALSE, NULL),
+                       'in be: respectEnableFields=true' => array('FE', TRUE, array('tx_foo_table.deleted_column=0 AND tx_foo_table.disabled_column=0 AND tx_foo_table.starttime_column<=123456789'))
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider providerForRespectEnableFields
+        */
+       public function respectEnableFieldsSettingGeneratesCorrectStatement($mode, $respectEnableFields, $expectedSql) {
+               $tableName = 'tx_foo_table';
+               $GLOBALS['TCA'][$tableName]['ctrl'] = array(
+                       'enablecolumns' => array(
+                               'disabled' => 'disabled_column',
+                               'starttime' => 'starttime_column'
+                       ),
+                       'delete' => 'deleted_column'
+               );
+               $GLOBALS['TSFE'] = new \stdClass();
+               $GLOBALS['TSFE']->sys_page = new \TYPO3\CMS\Frontend\Page\PageRepository();
+               $GLOBALS['SIM_ACCESS_TIME'] = 123456789;
+               $mockQuerySettings = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Typo3QuerySettings', array('dummy'), array(), '', FALSE);
+               $mockQuerySettings->setRespectEnableFields($respectEnableFields);
+               $sql = array();
+
+               /** @var $mockEnvironmentService \TYPO3\CMS\Extbase\Service\EnvironmentService | \PHPUnit_Framework_MockObject_MockObject */
+               $mockEnvironmentService = $this->getMock('TYPO3\\CMS\\Extbase\\Service\\EnvironmentService', array('isEnvironmentInFrontendMode'));
+               $mockEnvironmentService->expects($this->any())->method('isEnvironmentInFrontendMode')->will($this->returnValue($mode == 'FE'));
+
+               $mockTypo3DbQueryParser = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser', array('dummy'), array(), '', FALSE);
+               $mockTypo3DbQueryParser->_set('environmentService', $mockEnvironmentService);
+               $mockTypo3DbQueryParser->_callRef('addVisibilityConstraintStatement', $mockQuerySettings, $tableName, $sql);
+               $this->assertSame($expectedSql, $sql['additionalWhereClause']);
+               unset($GLOBALS['TCA'][$tableName]);
+       }
+
+       /**
+        * @test
+        * @expectedException \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException
+        */
+       public function visibilityConstraintStatementGenerationThrowsExceptionIfTheQuerySettingsAreInconsistent() {
+               $tableName = 'tx_foo_table';
+               $GLOBALS['TCA'][$tableName]['ctrl'] = array(
+                       'enablecolumns' => array(
+                               'disabled' => 'disabled_column'
+                       ),
+                       'delete' => 'deleted_column'
+               );
+               $mockQuerySettings = $this->getMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Typo3QuerySettings', array('getIgnoreEnableFields', 'getEnableFieldsToBeIgnored', 'getIncludeDeleted'), array(), '', FALSE);
+               $mockQuerySettings->expects($this->once())->method('getIgnoreEnableFields')->will($this->returnValue(FALSE));
+               $mockQuerySettings->expects($this->once())->method('getEnableFieldsToBeIgnored')->will($this->returnValue(array()));
+               $mockQuerySettings->expects($this->once())->method('getIncludeDeleted')->will($this->returnValue(TRUE));
+               $sql = array();
+
+               /** @var $mockEnvironmentService \TYPO3\CMS\Extbase\Service\EnvironmentService | \PHPUnit_Framework_MockObject_MockObject */
+               $mockEnvironmentService = $this->getMock('TYPO3\\CMS\\Extbase\\Service\\EnvironmentService', array('isEnvironmentInFrontendMode'));
+               $mockEnvironmentService->expects($this->any())->method('isEnvironmentInFrontendMode')->will($this->returnValue(TRUE));
+
+               $mockTypo3DbQueryParser = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser', array('dummy'), array(), '', FALSE);
+               $mockTypo3DbQueryParser->_set('environmentService', $mockEnvironmentService);
+               $mockTypo3DbQueryParser->_callRef('addVisibilityConstraintStatement', $mockQuerySettings, $tableName, $sql);
+               unset($GLOBALS['TCA'][$tableName]);
+       }
+       /**
+        * DataProvider for addPageIdStatement Tests
+        */
+       public function providerForAddPageIdStatementData() {
+               $table = uniqid('tx_coretest_table');
+               return array(
+                       'set Pid to zero if rootLevel = 1' => array(
+                               '1',
+                               $table,
+                               array('additionalWhereClause' => array($table . '.pid = 0'))
+                       ),
+                       'set Pid to given Pids if rootLevel = 0' => array(
+                               '0',
+                               $table,
+                               array('additionalWhereClause' => array($table . '.pid IN (42, 27)'))
+                       ),
+                       'set no statement if rootLevel = -1' => array(
+                               '-1',
+                               $table,
+                               array()
+                       )
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider providerForAddPageIdStatementData
+        */
+       public function addPageIdStatementSetsPidToZeroIfTableDeclaresRootlevel($rootLevel, $table, $expectedSql) {
+
+               $GLOBALS['TCA'][$table]['ctrl'] = array(
+                       'rootLevel' => $rootLevel
+               );
+               $sql = array();
+               $storagePageIds = array(42,27);
+               $mockTypo3DbQueryParser = $this->getAccessibleMock('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser', array('dummy'), array(), '', FALSE);
+               $mockFrontendVariableCache = $this->getMock('TYPO3\\CMS\\Core\\Cache\\Frontend\\VariableFrontend', array(), array(), '', FALSE);
+               $mockTypo3DbQueryParser->_set('tableColumnCache', $mockFrontendVariableCache);
+               $mockFrontendVariableCache->expects($this->once())->method('get')->will($this->returnValue(array('pid' => '42')));
+               $mockTypo3DbQueryParser->_callRef('addPageIdStatement', $table, $sql, $storagePageIds);
+
+               $this->assertSame($expectedSql, $sql);
+       }
+
+       /**
+        * @test
+        * @expectedException \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException
+        */
+       public function getPlainValueThrowsExceptionIfInputIsArray() {
+               $mockTypo3DbQueryParser = $this->getAccessibleMock(
+                       'TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser',
+                       array('dummy'),
+                       array(),
+                       '',
+                       FALSE
+               );
+               $mockTypo3DbQueryParser->_call('getPlainValue', array());
+       }
+
+       /**
+        * @test
+        */
+       public function getPlainValueReturnsTimestampIfDateTimeObjectIsGiven() {
+               $mockTypo3DbQueryParser = $this->getAccessibleMock(
+                       'TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser',
+                       array('dummy'),
+                       array(),
+                       '',
+                       FALSE
+               );
+               $input = new \DateTime('@1365866253');
+               $this->assertSame('1365866253', $mockTypo3DbQueryParser->_call('getPlainValue', $input));
+       }
+
+       /**
+        * @test
+        */
+       public function getPlainValueReturnsIntegerOneIfValueIsBooleanTrue() {
+               $mockTypo3DbQueryParser = $this->getAccessibleMock(
+                       'TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser',
+                       array('dummy'),
+                       array(),
+                       '',
+                       FALSE
+               );
+               $this->assertSame(1, $mockTypo3DbQueryParser->_call('getPlainValue', TRUE));
+       }
+
+       /**
+        * @test
+        */
+       public function getPlainValueReturnsIntegerZeroIfValueIsBooleanFalse() {
+               $mockTypo3DbQueryParser = $this->getAccessibleMock(
+                       'TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser',
+                       array('dummy'),
+                       array(),
+                       '',
+                       FALSE
+               );
+               $this->assertSame(0, $mockTypo3DbQueryParser->_call('getPlainValue', FALSE));
+       }
+
+       /**
+        * @test
+        * @expectedException \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException
+        */
+       public function getPlainValueCallsGetRealInstanceOnInputIfInputIsInstanceOfLazyLoadingProxy() {
+               $mockTypo3DbQueryParser = $this->getAccessibleMock(
+                       'TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser',
+                       array('dummy'),
+                       array(),
+                       '',
+                       FALSE
+               );
+               $input = $this->getMock(
+                       'TYPO3\\CMS\\Extbase\\Persistence\\Generic\\LazyLoadingProxy',
+                       array(),
+                       array(),
+                       '',
+                       FALSE
+               );
+               $input
+                       ->expects($this->once())
+                       ->method('_loadRealInstance');
+               $mockTypo3DbQueryParser->_call('getPlainValue', $input);
+       }
+
+       /**
+        * @test
+        */
+       public function getPlainValueCallsGetUidOnDomainObjectInterfaceInput() {
+               $mockTypo3DbQueryParser = $this->getAccessibleMock(
+                       'TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser',
+                       array('dummy'),
+                       array(),
+                       '',
+                       FALSE
+               );
+               $input = $this->getMock(
+                       'TYPO3\\CMS\\Extbase\\DomainObject\\DomainObjectInterface',
+                       array(),
+                       array(),
+                       '',
+                       FALSE
+               );
+               $input
+                       ->expects($this->once())
+                       ->method('getUid')
+                       ->will($this->returnValue(23));
+               $this->assertSame(23, $mockTypo3DbQueryParser->_call('getPlainValue', $input));
+       }
+
+       /**
+        * @test
+        */
+       public function getPlainValueReturnsSimpleType() {
+               $mockTypo3DbQueryParser = $this->getAccessibleMock(
+                       'TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser',
+                       array('dummy'),
+                       array(),
+                       '',
+                       FALSE
+               );
+               $value = uniqid('foo_');
+               $this->assertSame($value, $mockTypo3DbQueryParser->_call('getPlainValue', $value));
+       }
+
+}
index 8b06fcf..eeba0b9 100644 (file)
@@ -11,6 +11,11 @@ if (!is_array($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations
                'groups' => array('system')
        );
 }
+if (!is_array($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['extbase_typo3dbbackend_queries'])) {
+       $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['extbase_typo3dbbackend_queries'] = array(
+               'groups' => array('system')
+       );
+}
 if (!is_array($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['extbase_datamapfactory_datamap'])) {
        $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['extbase_datamapfactory_datamap'] = array(
                'groups' => array('system')